Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
reactStrictMode: true,
distDir: 'build',
};

module.exports = nextConfig;
65 changes: 36 additions & 29 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"export": "next build && next export",
"start": "next start",
"lint": "eslint 'src/**/*.{ts,tsx}' --fix && eslint 'pages/**/*.{ts,tsx}' --fix",
"prettify": "prettier -c --write src/**/* && prettier -c --write pages/**/*",
Expand All @@ -12,43 +13,48 @@
"pre-commit": "lint-staged"
},
"dependencies": {
"@reduxjs/toolkit": "^1.9.1",
"axios": "^1.2.1",
"next": "13.1.1",
"next-redux-wrapper": "^8.0.0",
"@reduxjs/toolkit": "^1.9.5",
"axios": "^1.4.0",
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"next": "13.4.4",
"next-redux-wrapper": "^8.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-redux": "^8.0.5",
"redux": "^4.2.0",
"redux-saga": "^1.2.2",
"reselect": "^4.1.7"
"react-toastify": "^9.1.3",
"redux": "^4.2.1",
"redux-saga": "^1.2.3",
"reselect": "^4.1.8",
"universal-cookie": "^4.0.4"
},
"devDependencies": {
"@types/node": "18.11.17",
"@types/react": "18.0.26",
"@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.47.0",
"autoprefixer": "^10.4.13",
"eslint": "8.30.0",
"@types/lodash": "^4.14.195",
"@types/node": "20.2.5",
"@types/react": "18.2.7",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.8",
"autoprefixer": "^10.4.14",
"eslint": "8.41.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-next": "13.1.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-config-next": "13.4.4",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-redux": "^4.0.0",
"eslint-plugin-simple-import-sort": "^8.0.0",
"husky": "^8.0.2",
"immer": "^9.0.16",
"lint-staged": "^13.1.0",
"postcss": "^8.4.20",
"prettier": "^2.8.1",
"sass": "^1.57.1",
"tailwindcss": "^3.2.4",
"typescript": "4.9.4"
"eslint-plugin-simple-import-sort": "^10.0.0",
"husky": "^8.0.3",
"immer": "^10.0.2",
"lint-staged": "^13.2.2",
"postcss": "^8.4.24",
"prettier": "^2.8.8",
"sass": "^1.62.1",
"tailwindcss": "^3.3.2",
"typescript": "5.0.4"
},
"husky": {
"hooks": {
Expand All @@ -68,7 +74,8 @@
]
},
"engines": {
"npm": "8.19.2",
"node": "18.12.1"
"npm": "please-use-yarn",
"node": "18.16.0",
"yarn": ">=1.22.0"
}
}
142 changes: 142 additions & 0 deletions pages-auth/next-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import jwt_decode from 'jwt-decode';
import {
GetServerSideProps,
GetServerSidePropsContext,
GetServerSidePropsResult,
} from 'next';
import { Store } from 'redux';
// helper classes
import { BaseError } from 'classes/BaseError';
// helper
import Utils from 'utils/Utils';
// constants
import { getDefaultRoute } from 'constants/DefaultRoutes';
import NextRouteConfig from 'constants/NextRouteConfig';
import { StatusCodes } from 'constants/status-codes';
import { Roles } from 'enums/Roles';
// services
import storageService from 'services/StorageService';
// store
import { authFetchMeAction } from 'store/actions/auth.action';
import { AppState } from 'store/reducers';

/**
* This is a TypeScript function that handles authentication and authorization for server-side
* rendering in Next.js, with options for allowed roles and public access.
* @param store - The Redux store for the application.
* @param {null | ((context: GetServerSidePropsContext) => Promise<any>)} [callback] - The `callback`
* parameter is an optional function that can be passed to `nextAuth` as an argument. It is a function
* that takes a `GetServerSidePropsContext` object as its argument and returns a Promise that resolves
* to an object. This object can contain any data that needs to be
* @param [config] - The `config` parameter is an optional object that can contain the following
* properties:
* `dataKey?: string;`
* `allowedRoles?: Roles[];`
* `allowPublic?: boolean;`
* @example
* export const getServerSideProps = wrapper.getServerSideProps((store) =>
nextAuth(store, null, { allowPublic: false, allowedRoles: [Roles.ADMIN] })
);
* @returns A higher-order function that takes in a `store`, `callback`, and `config` as arguments and
* returns a `GetServerSideProps` function. The returned function takes in a `context` object as an
* argument and returns a `GetServerSidePropsResult` object. The `GetServerSidePropsResult` object can
* either have a `props` property containing a `serverData` object
*/
const nextAuth =
(
store: Store<AppState>,
callback?: null | ((context: GetServerSidePropsContext) => Promise<any>),
config?: {
dataKey?: string;
allowedRoles?: Roles[];
allowPublic?: boolean;
}
): GetServerSideProps =>
async (
context: GetServerSidePropsContext
): Promise<GetServerSidePropsResult<any>> => {
storageService.setCookies(context.req.headers.cookie);

try {
let serverData: any = {
query: context.query,
params: Utils.sanitizeObject(context.params),
};

let userType = null;
let userId = null;
let tokenExpired = true;

// checking token expiration
const authToken = await storageService.getAuthToken();

if (authToken) {
const decoded: any = jwt_decode(authToken || '');
const current = Math.floor(Date.now() / 1000);
tokenExpired = (decoded?.exp || 0) - current < 600;
if (!tokenExpired) {
userType = decoded?.userType;
userId = decoded?.id;
}
}

serverData.userType = userType;
serverData.userId = userId;
serverData.tokenExpired = tokenExpired;

if (!tokenExpired && authToken && !store.getState().auth.userID) {
store.dispatch(authFetchMeAction());
serverData = {
...serverData,
};
}

if (
(config?.allowedRoles || []).includes(userType) ||
config?.allowPublic
) {
if (callback) {
serverData = {
...serverData,
[config?.dataKey || 'data']: await callback?.(context),
};
}

if (!serverData?.authUser && !tokenExpired && authToken) {
serverData = {
...serverData,
};
}

return {
props: {
serverData,
},
};
}

return {
redirect: {
permanent: false,
destination: getDefaultRoute(userType),
},
};
} catch (e: any) {
if (e.status === StatusCodes.UNAUTHORIZED) {
return {
redirect: {
permanent: false,
destination: NextRouteConfig.logout,
},
};
}

return {
props: {
error: BaseError.toJSON(e),
},
};
}
};

export default nextAuth;
11 changes: 11 additions & 0 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import React, { FC } from 'react';
import { AppProps } from 'next/app';
import { Provider } from 'react-redux';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.min.css';
// store
import wrapper from '../src/store';
// css
import '../src/index.scss';

const App: FC<AppProps> = ({ Component, ...rest }) => {
const { store, props } = wrapper.useWrappedStore(rest);
return (
<Provider store={store}>
<ToastContainer
theme='dark'
limit={5}
closeButton={false}
pauseOnFocusLoss={false}
toastClassName={`relative flex p-4 rounded-10 tracking-wider justify-between overflow-hidden cursor-pointer font-bold text-sm `}
/>
<Component {...props.pageProps} />
</Provider>
);
Expand Down
64 changes: 64 additions & 0 deletions src/classes/BaseError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { AxiosError } from 'axios';
import { StatusCodes, StatusMessage } from 'constants/status-codes';

export enum ErrorCode {
UNIDENTIFIED,
}

export type ErrorStatusCode = ErrorCode | StatusCodes;

export type ValidationErrorType = {
field: string;
message: string;
rule: string;
};

const DEFAULT_ERROR = 'An unexpected error occurred. Please try again';

export class BaseError {
constructor(
readonly message: string = DEFAULT_ERROR,
readonly status: ErrorStatusCode = ErrorCode.UNIDENTIFIED,
readonly errors: ValidationErrorType[] = [],
readonly data: any = []
) {}

static fromJSON(axiosError: AxiosError<any>): BaseError {
if (axiosError.code === 'ECONNABORTED') {
return new BaseError(`Request Timeout (${axiosError.message})`);
}

if (!axiosError.response) {
return new BaseError(
'Unable to connect to server. Please check your internet connection try again.'
);
}

const { status, data } = axiosError.response;
const message: string =
StatusMessage[status as StatusCodes] ?? DEFAULT_ERROR;

return new BaseError(message, status, data?.errors ?? [], data?.data ?? []);
}

static toJSON(json: BaseError) {
return {
message: json.message || 'Error. Please Try Again.',
status: json.status || -1,
errors: json.errors || [],
data: json.data || [],
};
}

isValidationError(): boolean {
return (this.errors || []).length > 0;
}

errorsByKey(key: string): ValidationErrorType | undefined {
return this.errors?.find((er: ValidationErrorType) => er.field === key);
}

hasErrorByKey(key: string): boolean {
return !!this.errorsByKey(key);
}
}
11 changes: 11 additions & 0 deletions src/constants/DefaultRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import NextRouteConfig from 'constants/NextRouteConfig';
import { Roles } from 'enums/Roles';

export const getDefaultRoute = (userType: Roles): string => {
switch (userType) {
case Roles.USER:
return NextRouteConfig.home._ROOT;
default:
return '/';
}
};
11 changes: 11 additions & 0 deletions src/constants/NextRouteConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const NextRouteConfig = {
login: '/login',
logout: '/logout',
signup: '/signup',
resetPassword: '/reset-password',
home: {
_ROOT: '/',
},
};

export default NextRouteConfig;
Loading