Skip to content

Commit

Permalink
feat: enabled auth with context
Browse files Browse the repository at this point in the history
  • Loading branch information
miguel-merlin committed Dec 12, 2024
1 parent 41fe185 commit 02592a1
Show file tree
Hide file tree
Showing 10 changed files with 704 additions and 9 deletions.
299 changes: 291 additions & 8 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@
},
"dependencies": {
"@types/react-dom": "^19.0.2",
"amazon-cognito-identity-js": "^6.3.12",
"axios": "^1.7.9",
"prop-types": "^15.8.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@types/node": "^22.10.2",
"@types/prop-types": "^15.7.14",
"@types/react": "^19.0.1",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
Expand Down
15 changes: 15 additions & 0 deletions src/api/apiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import axios, { AxiosRequestConfig } from 'axios';

const getApiClient = (baseUrl: string) => {
const apiClient = axios.create({
baseURL: baseUrl,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
withCredentials: true,
} satisfies AxiosRequestConfig);
return apiClient;
};

export default getApiClient;
35 changes: 35 additions & 0 deletions src/api/lib/organizations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { AxiosResponse, AxiosError } from 'axios';
import { RequestParamsI } from '../../interface/Request';

interface OrganizationError extends Error {
statusCode?: number;
}

/**
* Fetches all organizations from the API
* @param request - Authenticated request function
* @returns Promise resolving to the API response
* @throws {OrganizationError} When the API request fails
*/
export const getAllOrganizations = async (
request: ({ method, uri, body }: RequestParamsI) => Promise<AxiosResponse>
): Promise<AxiosResponse> => {
try {
const response = await request({
method: 'GET',
uri: '/api/organizations',
body: {},
});
return response;
} catch (error) {
const organizationError: OrganizationError = new Error(
(error as Error).message || 'Failed to fetch organizations'
);

if ((error as AxiosError).isAxiosError) {
const axiosError = error as AxiosError;
organizationError.statusCode = axiosError.response?.status;
}
throw organizationError;
}
};
23 changes: 23 additions & 0 deletions src/auth/AuthContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createContext } from 'react';
import { AuthContextType } from './AuthProvider';
import { CognitoUser } from 'amazon-cognito-identity-js';

export interface State {
isAuthenticated: boolean;
isInitialized: boolean;
user: CognitoUser | null;
}

export const initialState: State = {
isAuthenticated: false,
isInitialized: false,
user: null,
};

export const AuthContext = createContext<AuthContextType>({
...initialState,
method: 'cognito',
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
register: () => {},
});
240 changes: 240 additions & 0 deletions src/auth/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import {
AuthenticationDetails,
CognitoUser,
CognitoUserPool,
CognitoUserAttribute,
CognitoUserSession,
} from 'amazon-cognito-identity-js';
import { AuthContext, State, initialState } from './AuthContext';
import { useCallback, useEffect, useReducer, ReactNode } from 'react';
import axios from 'axios';

export const UserPool = new CognitoUserPool({
UserPoolId: process.env.REACT_APP_USER_POOL_ID || '',
ClientId: process.env.REACT_APP_CLIENT_ID || '',
});

interface Action {
type: string;
payload?: {
isAuthenticated?: boolean;
user?: CognitoUser | null;
};
}

interface UserAttributes {
[key: string]: string;
}

interface Session {
getIdToken: () => {
getJwtToken: () => string;
};
}

interface GetSessionResult {
user: CognitoUser;
session: Session;
headers: { Authorization: string };
attributes: UserAttributes;
}

export interface AuthContextType extends State {
method: 'cognito';
login: (
email: string,
password: string
) => Promise<CognitoUserSession | { message: string } | void>;
logout: () => void;
register: (
email: string,
password: string,
firstName: string,
lastName: string
) => void;
}

const handlers: { [key: string]: (state: State, action: Action) => State } = {
AUTHENTICATE: (state: State, action: Action) => {
const { isAuthenticated, user } = action.payload!;
return {
...state,
isAuthenticated: isAuthenticated!,
isInitialized: true,
user: user ?? null,
};
},
LOGOUT: (state: State) => ({
...state,
isAuthenticated: false,
user: null,
}),
};

const reducer = (state: State, action: Action): State =>
handlers[action.type] ? handlers[action.type](state, action) : state;

interface AuthProviderProps {
children: ReactNode;
}

export function AuthProvider({ children }: AuthProviderProps) {
const [state, dispatch] = useReducer(reducer, initialState);

const getUserAttributes = useCallback(
(currentUser: CognitoUser): Promise<UserAttributes> =>
new Promise((resolve, reject) => {
currentUser.getUserAttributes((err, attributes) => {
if (err) {
reject(err);
return;
}
const results: UserAttributes = {};
attributes?.forEach((attribute) => {
results[attribute.Name] = attribute.Value;
});
resolve(results);
});
}),
[]
);

const getSession = useCallback(
() =>
new Promise((resolve, reject) => {
const user = UserPool.getCurrentUser();
if (user) {
user.getSession(async (error: Error | null, session: Session) => {
if (error) {
reject(error);
return;
}
const attributes: UserAttributes = await getUserAttributes(user);
const token: string = session.getIdToken().getJwtToken();
axios.defaults.headers.common.Authorization = token;
dispatch({
type: 'AUTHENTICATE',
payload: {
isAuthenticated: true,
user,
},
});
resolve({
user,
session,
headers: { Authorization: token },
attributes,
} as GetSessionResult);
});
} else {
dispatch({
type: 'AUTHENTICATE',
payload: {
isAuthenticated: false,
user: null,
},
});
}
}),
[getUserAttributes]
);

const initial = useCallback(async () => {
try {
await getSession();
} catch {
dispatch({
type: 'AUTHENTICATE',
payload: {
isAuthenticated: false,
user: null,
},
});
}
}, [getSession]);

useEffect(() => {
initial();
}, [initial]);

const login = useCallback(
(
email: string,
password: string
): Promise<CognitoUserSession | { message: string }> =>
new Promise((resolve, reject) => {
const user = new CognitoUser({
Username: email,
Pool: UserPool,
});

const authDetails = new AuthenticationDetails({
Username: email,
Password: password,
});

user.authenticateUser(authDetails, {
onSuccess: (data) => {
getSession();
resolve(data as CognitoUserSession);
},
onFailure: (err) => {
reject(err);
return;
},
newPasswordRequired: () => {
resolve({ message: 'newPasswordRequired' });
},
});
}),
[getSession]
);

const logout = () => {
const user = UserPool.getCurrentUser();
if (user) {
user.signOut();
dispatch({ type: 'LOGOUT' });
}
};

const register = (
email: string,
password: string,
firstName: string,
lastName: string
) => {
UserPool.signUp(
email,
password,
[
new CognitoUserAttribute({ Name: 'email', Value: email }),
new CognitoUserAttribute({
Name: 'name',
Value: `${firstName} ${lastName}`,
}),
],
[],
(err) => {
if (err) {
throw err;
}
window.location.href = '/login';
}
);
};

return (
<AuthContext.Provider
value={{
...state,
method: 'cognito',
login,
logout,
register,
}}
>
{children}
</AuthContext.Provider>
);
}
Loading

0 comments on commit 02592a1

Please sign in to comment.