Table of contents:
- Dashboard & Storefront
- Context providers
- Interceptors
- API Routes
Dashboard & Storefront
The dashboard is a part of the application that is used by the administrators to manage the application. The storefront on the other hand, is a part of the application that is used by the customers to browse and buy products. Both of these parts are standalone applications built using Next.js framework and are served by the same server. Their directory structure consists of various parts, among which the most important are:
api
- contains various API route functions that are used to send requests to the backendcomponents
- contains a collection of React components that are used in the dashboardpages
- contains React components that are used as pages in the dashboard, also ensures routing (more information about routing can be found in API routes section)public
- contains static files that are used in the dashboardstyles
- contains styles definitions that are used in the dashboardtypes
- contains TypeScript type definitions that are used in the dashboardutils
- contains various utility functions that are used in the dashboard along with context providers and interceptors (more information about context providers and interceptors can be found in Context providers section and Interceptors section)
Context providers
To be able to access various data in different parts of the application, we use React Context. More information about React Context can be found on the following links:
In further parts of this section, we assume that the reader is familiar with React Context and its usage from the links above.
In ecoseller, we use various context providers, and now we will describe them in more detail.
UserProvider
UserProvider
is a context provider that provides information about the currently logged in user to its children. It is used in both Dashboard
and Storefront
component, although they differ a bit in data they provide.
Parameters
children
: React component that is wrapped by the provider
Return value
user
: fetched data from/user/detail/
endpoint. Consists of:email
- email of the userfirst_name
- first name of the userlast_name
- last name of the userbirth_date
- birth date of the useris_active
- whether the user is activeis_admin
- whether the user is adminis_staff
- whether the user is staff
roles
: fetched data from/roles/user-groups/${email}
. Consists of:name
- name of the roledescription
- description of the rolepermissions
- list of permissions of the role. Each permission consists of:name
- name of the permissiondescription
- description of the permissiontype
- type of the permissionmodel
- model to which the permission corresponds
UserProvider
in Storefront
only provides user
data, while Dashboard
provides both user
and roles
data.
Usage example
UserProvider
already wraps whole application in both Dashboard
and Storefront
components, so we can access user data in any child component. To access user data, we use useUser
hook:
- In dashboard:
const ChildComponent = () => { ... const { user, roles } = useUser(); ... return ( ... ); };
- In storefront:
const ChildComponent = () => { ... const { user } = useUser(); ... return ( ... ); };
PermissionProvider
As mentioned in Authorization section, ecoseller uses roles and permissions to restrict access to certain parts of the application. PermissionProvider
is a context provider that provides information about user’s permissions to its children. It is used in Dashboard
component.
To ensure proper usage, we defined ContextPermissions
type with permissions that may be passed to the provider. The type is defined in dashboard/utils/context/permission.tsx
file.
Parameters
allowedPermissions
: Array ofContextPermissions
- permissions the user needs to have to gain access to the componentchildren
: React component that is wrapped by the provider
Return value
hasPermission
: boolean - true if the user has all permissions fromallowedPermissions
array, false otherwise
Usage example
To check whether the user has user_add_permission
permission for adding new user, wrap the component with PermissionProvider
:
<PermissionProvider allowedPermissions={["user_add_permission"]}>
<EditableContentWrapper>
<CreateUser />
</EditableContentWrapper>
</PermissionProvider>
Now, we can check in respective component whether the user has the permission:
const CreateUser () => {
...
const { hasPermission } = usePermission();
...
return (
...
<TextField
disabled={!hasPermission}
>
Email
</TextField>
...
);
};
const EditableContentWrapper = () => {
...
const { hasPermission } = usePermission();
...
return (
...
<Button
disabled={!hasPermission}
>
Save
</Button>
...
);
};
This will disable the TextField
and Button
components if the user does not have user_add_permission
permission.
CartProvider
CartProvider
is a context provider that provides information about the user’s cart as well as some usefull functions to its children. It is used only in Storefront
component.
Parameters
children
: React component that is wrapped by the provider
Return value
cart
: fetched data from/cart/storefront/<str:token>
endpoint. Consists of:token
- token of the cartcart_items
- items of the cartupdate_at
- date of the last update of the carttotal_items_price_incl_vat_formatted
- total price of the cart including VATtotal_items_price_without_vat_formatted
- total price of the cart without VATtotal_price_incl_vat_formatted
- total price of the cart including VAT and shippingtotal_price_without_vat_formatted
- total price of the cart without VAT and shippingshipping_method_country
- id toShippingMethodCountry
objectpayment_method_country
- id toPaymentMethodCountry
object
cartSize
: number of items in the cartaddToCart
- function for adding item to the cartremoveFromCart
- function for removing item from the cartupdateQueantity
- function for updating quantity of the item in the cartclearCart
- function for clearing the cartcartProductQuantity
- function for getting quantity of the product in the cart
Functions provided by CartProvider
addToCart
Adds item to the cart. If the item is already in the cart, it updates its quantity. Takes following parameters:
sku
: SKU of the productqty
: quantity of the productproduct
: product IDpricelist
: pricelist IDcountry
: country ID
removeFromCart
Deletes item from the cart. Takes following parameters:
sku
: SKU of the product
updateQuantity
Updates quantity of the item in the cart. Takes following parameters:
sku
: SKU of the productquantity
: new quantity of the product
clearCart
Clears the cart. Takes no parameters.
cartProductQuantity
Returns quantity of the product in the cart. Takes following parameters:
sku
: SKU of the product
Usage example
CartProvider
already wraps whole application, so we can access data or functions in any child component. To do so, we use useCart
hook:
const ChildComponent = () => {
...
const { cart, cartSize, addToCart, removeFromCart, updateQuantity, clearCart, cartProductQuantity } = useCart();
...
return (
...
);
};
CookieProvider
CookieProvider
is a context provider that provides information about the user’s cookies as well as some usefull functions to its children. It is used only in Storefront
component.
Parameters
children
: React component that is wrapped by the provider
Return value
cookieState
- set consisting of set of boolean flags:neccessaryCookies
- whether the user has accepted neccessary cookiespreferenceCookies
- whether the user has accepted preference cookiesstatisticalCookies
- whether the user has accepted statistical cookiesadsCookies
- whether the user has accepted ads cookiesopenDisclaimer
- whether to show cookie disclaimer
setCookieState
- function for setting cookie statesetCookieSettingToCookies
- function for setting cookie setting to cookiestoggleDisclaimer
- function for toggling cookie disclaimer
Functions provided by CookieProvider
setCookieState
Sets cookie state. Takes following parameters:
key
: type of cookievalue
: boolean value to set to the cookie
setCookieSettingToCookies
Sets cookie setting to cookies. Takes following parameters:
allTrue
: whether all cookies are accepted
toggleDisclaimer
Toggles cookie disclaimer. Takes following parameters:
value
: whether to show cookie disclaimer
Usage example
CookieProvider
already wraps whole application, so we can access data or functions in any child component. To do so, we use useCookie
hook:
const ChildComponent = () => {
...
const { cookieState, setCookieState, setCookieSettingToCookies, toggleDisclaimer } = useCookie();
...
return (
...
);
};
CountryProvider
CountryProvider
is a context provider that provides information about the country that is currently set by the user, as well as some usefull functions to its children. It is used only in Storefront
component.
Parameters
children
: React component that is wrapped by the provider
Return value
country
: object representing country. Consists of:code
- id of the countryname
- name of the countrylocale
- locale of the countrydefault_price_list
- id of the default price list of the country
countryList
: list ofcountry
objects - all available countriessetCountryCookieAndLocale
- function for setting country cookie and locale
Functions provided by CountryProvider
setCountryCookieAndLocale
Sets country cookie and locale. Takes following parameters:
countryCode
: code of the country
Usage example
CountryProvider
already wraps whole application, so we can access data or functions in any child component. To do so, we use useCountry
hook:
const ChildComponent = () => {
...
const { country, countryList, setCountryCookieAndLocale } = useCountry();
...
return (
...
);
};
RecommenderProvider
RecommenderProvider
is a context provider that provides information about the user’s recommender session as well as some usefull functions to send either recommender event or retrive recommendations. It is used only in Storefront
component.
This context provider creates a new recommender session for each user (if it does not exist yet) and stores it in the cookie storage under rsSession
key.
Parameters
children
: React component that is wrapped by the provider
Recommender events
Recommender events are used to track user’s behaviour on the website. They are sent to the recommender server and are used to generate recommendations. They are defined in RS_EVENT
variablle. Each event has its own payload. More information about recommender events can be found here
Recommender situations
Recommender situations are used to define the context in which the user is currently in. They are defined in RS_RECOMMENDATIONS_SITUATIONS
variable. Each situation has its own payload.
Return value
session
: stringuuid
session idsendEvent
- function for sending recommender eventgetRecommendations
- function for getting recommendations for given situation
Functions provided by RecommenderProvider
User does not have to send session id directly, it’s injected to each request automatically.
sendEvent
Sends recommender event. For example it can be information about adding product to the cart, or leaving product page. Takes following parameters:
event: RS_EVENT
: event to sendpayload: any
: payload of the event (depends on the event) It returns noting (void).
getRecommendations
Gets recommendations for given situation. It returns recommmended products for the given session. Takes following parameters:
situation: RS_RECOMMENDATIONS_SITUATIONS
: situation for which to get recommendationspayload: any
: payload of the situation (depends on the situation - might be product_id, etc.)
Usage example
RecommenderProvider
already wraps whole application, so we can access data or functions in any child component. To do so, we use useRecommender
hook:
const ChildComponent = () => {
...
const { session, sendEvent, getRecommendations } = useRecommender();
...
return (
...
);
};
Interceptors
Interceptors are used to intercept requests and responses before they are handled by the application. In ecoseller, we use them to add authorization token and other data to requests and to handle errors. We use axios
library for handling requests and responses. More information about interceptors can be found on the following links:
In further parts of this section, we assume that the reader is familiar with axios
library and its usage from the links above.
Interceptors in the Dashboard
and Storefront
differ a bit, so we will describe them separately.
Request interceptor - Dashboard
In the Dashboard
, we use request interceptor to add authorization token to requests. The interceptor is defined in dashboard/utils/interceptors/api.ts
file.
Fisrtly, we define api
axios instance with base url and headers:
export const api = axios.create({
baseURL,
headers: {
"Content-Type": "application/json",
},
withCredentials: true,
});
Then, we add request interceptor to the api
instance:
- for the request
api.interceptors.request.use((config) => { let access = ""; let refresh = ""; if (isServer()) { access = getCookie("accessToken", { req, res }) as string; refresh = getCookie("refreshToken", { req, res }) as string; } else { access = Cookies.get("accessToken") || ""; refresh = Cookies.get("refreshToken") || ""; } if (access) { config.headers.Authorization = `JWT ${access}`; } return config; });
- for the response
api.interceptors.response.use( (response) => { return response; }, (error: AxiosError) => { // check conditions to refresh token if ( (error.response?.status === 401 || error.response?.status === 403) && !error.response?.config?.url?.includes("user/refresh-token") && !error.response?.config?.url?.includes("user/login") ) { return refreshToken(error); } return Promise.reject(error); } );
Where the
refreshToken
is a function responsible for fetchiing a new access token and retrying the request. It is defined indashboard/utils/interceptors/api.ts
file.
Request interceptor - Storefront
In the Storefront
, we use request interceptor to add authorization token and country locale to requests. The interceptor is defined in storefront/utils/interceptors/api.ts
file.
Axios instance in the Storefront
is defined similarly as in the Dashboard
(see above).
The difference is in the request interceptor, where we also add a country locale to the Accept-Language
header:
api.interceptors.request.use((config) => {
... // similar to the Dashboard
// set locale (if present)
const locale = getLocale();
if (locale) {
config.headers["Accept-Language"] = locale;
}
return config;
});
API Routes
Next.js API Routes provide a convenient way to create server-side endpoints within your Next.js application. These API routes allow you to handle server-side logic and expose custom API endpoints. This section of the documentation explores the usage of Next.js API Routes in the context of ecoseller.
In the Ecoseller architecture, it is recommended to avoid exposing the backend service directly to the client. By using Next.js API Routes, you can encapsulate server-side logic within your Next.js application, ensuring a secure and controlled environment for handling data and performing server-side operations. This approach provides several benefits:
- Security: By not exposing the backend service to the client, you reduce the risk of potential security vulnerabilities. It prevents unauthorized access or tampering of sensitive data and operations that should be restricted to server-side execution only.
- Imporved control: Keeping the backend service separate from the client-side code gives you better control over the server-side operations and access to the underlying data. It allows you to enforce business logic, perform validations, and apply necessary security measures within the API routes.
- Simplified Architecture: The separation of concerns between the client-side code and the server-side logic simplifies the overall architecture. It enables cleaner code organization and promotes modularity, making it easier to maintain and scale the application in the long run.
This approach also helped us with cookie handling and token refreshing. Setting cookie client side caused some problems with refreshing tokens, so we decided to set cookies in API routes instead.
API Routes in the Dashboard
All API routes can be found in the dashboard/pages/api
directory. All those routes use simmilar logic and send requests to the backend via api
interceptor.
export const productDetailAPI = async (
method: HTTPMETHOD,
id: number,
req?: NextApiRequest,
res?: NextApiResponse
) => {
if (req && res) {
setRequestResponse(req, res);
}
const url = `/product/dashboard/detail/${id}/`;
switch (method) {
case "GET":
return await api
.get(url)
.then((response) => response.data)
.then((data: IProduct) => {
return data;
})
.catch((error: any) => {
throw error;
});
case "DELETE":
return await api
.delete(url)
.then((response) => response.data)
.then((data: IProduct) => {
return data;
})
.catch((error: any) => {
throw error;
});
case "PUT":
const body = req?.body;
if (!body) throw new Error("Body is empty");
return await api
.put(url, body)
.then((response) => response.data)
.then((data: IProduct) => {
return data;
})
.catch((error: any) => {
throw error;
});
default:
throw new Error("Method not supported");
}
};
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
/**
* This is a wrapper for the product detail api in the backend
*/
const { id } = req.query;
const { method } = req;
if (!id) return res.status(400).json({ message: "id is required" });
if (Array.isArray(id) || isNaN(Number(id)))
return res.status(400).json({ message: "id must be a number" });
if (method === "GET" || method === "PUT" || method === "DELETE") {
return productDetailAPI(method, Number(id), req, res)
.then((data) => res.status(200).json(data))
.catch((error) => res.status(400).json(null));
} else {
return res.status(400).json({ message: "Method not supported" });
}
};
export default handler;
API Routes in the Storefront
All API routes can be found in the storefront/pages/api
directory. All those routes use simmilar logic and send requests to the backend via api
interceptor.
export const cartAPI = async (
method: HTTPMETHOD,
req: NextApiRequest,
res: NextApiResponse
) => {
if (req && res) {
setRequestResponse(req, res);
}
const url = `/cart/storefront/`;
switch (method) {
case "POST":
return await api
.post(url, req.body)
.then((response) => response.data)
.then((data) => {
return data;
})
.catch((error: any) => {
throw error;
});
default:
throw new Error("Method not supported");
}
};
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { method } = req;
if (method == "POST") {
return cartAPI("POST", req, res)
.then((data) => res.status(201).json(data))
.catch((error) => res.status(400).json(null));
}
return res.status(404).json({ message: "Method not supported" });
};
export default handler;