Skip to content

Commit

Permalink
Refactored Authentication
Browse files Browse the repository at this point in the history
Authentication oauth-only, token authorization server-side
  • Loading branch information
R1c4rdCo5t4 committed Jun 19, 2024
1 parent 991f88f commit b7a032c
Show file tree
Hide file tree
Showing 24 changed files with 196 additions and 407 deletions.
2 changes: 2 additions & 0 deletions code/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"dotenv": "^16.4.5",
"eslint-plugin-playwright": "^1.6.0",
"firebase": "^10.12.2",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"msw": "^2.2.14",
"react": "^18.3.1",
Expand All @@ -44,6 +45,7 @@
"@testing-library/user-event": "^14.5.2",
"@types/firebase": "^3.2.1",
"@types/jest": "^29.5.12",
"@types/js-cookie": "^3.0.6",
"@types/lodash": "^4.17.1",
"@types/node": "^20.12.10",
"@types/react": "^18.3.1",
Expand Down
4 changes: 1 addition & 3 deletions code/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import { WorkspaceProvider } from '@ui/contexts/workspace/WorkspaceContext';
import Workspaces from '@ui/pages/workspaces/Workspaces';
import { CommunicationProvider } from '@ui/contexts/communication/CommunicationContext';
import Home from '@ui/pages/home/Home';
import Login from '@ui/pages/auth/login/Login';
import Signup from '@ui/pages/auth/signup/Signup';
import Login from '@ui/pages/login/Login';
import AuthProvider from '@ui/contexts/auth/AuthContext';

function App() {
Expand All @@ -34,7 +33,6 @@ function App() {
}
/>
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route
path="/workspaces/*"
element={
Expand Down
5 changes: 0 additions & 5 deletions code/client/src/services/auth/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ function authService(http: HttpCommunication) {
await http.post('/users', { id, ...data });
}

async function registerUserOAuth(id: string, data: UserData) {
await http.post('/users?oauth=true', { id, ...data });
}

async function getUser(id: string) {
return await http.get(`/users/${id}`);
}
Expand All @@ -24,7 +20,6 @@ function authService(http: HttpCommunication) {

return {
registerUser,
registerUserOAuth,
getUser,
updateUser,
deleteUser,
Expand Down
36 changes: 22 additions & 14 deletions code/client/src/services/communication/http/httpCommunication.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,49 @@
import { SERVER_URL } from '@config';

export interface HttpCommunication {
post: (url: string, data?: any) => Promise<any>;
get: (url: string) => Promise<any>;
put: (url: string, data?: any) => Promise<any>;
delete: (url: string, data?: any) => Promise<any>;
post: (url: string, data?: any, withAuth?: boolean) => Promise<any>;
get: (url: string, withAuth?: boolean) => Promise<any>;
put: (url: string, data?: any, withAuth?: boolean) => Promise<any>;
delete: (url: string, data?: any, withAuth?: boolean) => Promise<any>;
}

async function get(url: string) {
return request(url, 'GET');
async function get(url: string, withAuth: boolean = false) {
return request(url, 'GET', undefined, withAuth);
}

async function post(url: string, body: any) {
return request(url, 'POST', body);
async function post(url: string, body: any, withAuth: boolean = false) {
return request(url, 'POST', body, withAuth);
}

async function put(url: string, body: any) {
return request(url, 'PUT', body);
async function put(url: string, body: any, withAuth: boolean = false) {
return request(url, 'PUT', body, withAuth);
}

async function del(url: string, body: any) {
return request(url, 'DELETE', body);
async function del(url: string, body: any, withAuth: boolean = false) {
return request(url, 'DELETE', body, withAuth);
}

const request = async (url: string, method: string, body?: any) => {
const request = async (url: string, method: string, body: any, withAuth: boolean) => {
const requestInit: RequestInit = {
method,
headers: { 'Content-Type': 'application/json' },
};
if (body) requestInit.body = JSON.stringify(body);
if (withAuth) requestInit.credentials = 'include';

console.log('requestInit:', requestInit);

const response = await fetch(SERVER_URL + url, requestInit);
const noBody = response.status === 204 || response.headers.get('content-length') === '0';
if (noBody) return;
const result = await response.json();
if (response.ok) return result;

if (response.status === 401) {
throw new Error('Unauthorized');
}
if (response.status === 403) {
throw new Error('Forbidden');
}
throw new Error(result.error || 'Failed to fetch');
};

Expand Down
11 changes: 11 additions & 0 deletions code/client/src/ui/components/header/Header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,15 @@
display: flex;
align-items: center;
}

.account {
display: flex;
align-items: center;
margin-right: 1rem;
gap: 2vh;

button {
padding: 1vh;
}
}
}
6 changes: 3 additions & 3 deletions code/client/src/ui/components/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ function Header() {
<p></p>
<div>
{currentUser ? (
<>
<p>{currentUser?.email}</p>
<div className="account">
<p>{currentUser?.displayName}</p>
<button onClick={logout}>Logout</button>
</>
</div>
) : (
<Link to="/login">Login</Link>
)}
Expand Down
69 changes: 14 additions & 55 deletions code/client/src/ui/contexts/auth/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -1,91 +1,53 @@
import Cookies from 'js-cookie';
import { auth, githubAuthProvider, googleAuthProvider } from '@config';
import { createContext, ReactNode, useEffect, useState } from 'react';
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signInWithPopup,
signOut,
updateProfile,
User,
} from 'firebase/auth';
import { validateEmail, validatePassword, validateUsername } from '@ui/contexts/auth/utils';
import { signInWithPopup, signOut, User, type AuthProvider as Provider } from 'firebase/auth';
import useError from '@ui/contexts/error/useError';
import useAuthService from '@services/auth/useAuthService';

export type AuthContextType = {
currentUser: User | null;
login: (email: string, password: string) => Promise<void>;
loginWithGoogle: () => Promise<void>;
loginWithGithub: () => Promise<void>;
signup: (username: string, email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
updateUserProfile: (user: User, profile: UserProfile) => Promise<void>;
};

export const AuthContext = createContext<AuthContextType>({
currentUser: null,
login: async () => {},
loginWithGoogle: async () => {},
loginWithGithub: async () => {},
signup: async () => {},
logout: async () => {},
updateUserProfile: async () => {},
});

type AuthProviderProps = {
children: ReactNode;
};

type UserProfile = {
displayName?: string;
photoURL?: string;
};

export function AuthProvider({ children }: AuthProviderProps) {
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const { publishError } = useError();
const { registerUser, registerUserOAuth } = useAuthService();
const { registerUser } = useAuthService();

const handleAsyncAction = async (action: () => Promise<any>) => {
const loginWithProvider = async (provider: Provider) => {
try {
return await action();
const { user } = await signInWithPopup(auth, provider);
await registerUser(user.uid, { username: user.displayName!, email: user.email! });
const token = await user.getIdToken();
Cookies.set('token', token, { expires: 1, secure: true, sameSite: 'Strict' });
} catch (e) {
publishError(e as Error);
}
};

const signup = (username: string, email: string, password: string) =>
handleAsyncAction(async () => {
validateUsername(username);
validateEmail(email);
validatePassword(password);
const { user } = await createUserWithEmailAndPassword(auth, email, password);
await registerUser(user.uid, { username, email });
});

const login = (email: string, password: string) =>
handleAsyncAction(async () => {
return signInWithEmailAndPassword(auth, email, password).catch(() => {
throw new Error('Invalid credentials');
});
});

const logout = () => handleAsyncAction(() => signOut(auth));

const updateUserProfile = (user: User, profile: UserProfile) => updateProfile(user, profile);
const loginWithGoogle = () => loginWithProvider(googleAuthProvider);

const loginWithGoogle = () =>
handleAsyncAction(async () => {
const { user } = await signInWithPopup(auth, googleAuthProvider);
await registerUserOAuth(user.uid, { username: user.displayName!, email: user.email! });
});
const loginWithGithub = () => loginWithProvider(githubAuthProvider);

const loginWithGithub = () =>
handleAsyncAction(async () => {
const { user } = await signInWithPopup(auth, githubAuthProvider);
await registerUserOAuth(user.uid, { username: user.displayName!, email: user.email! });
});
const logout = () => {
Cookies.remove('token');
return signOut(auth);
};

useEffect(() => {
return auth.onAuthStateChanged(user => {
Expand All @@ -98,12 +60,9 @@ export function AuthProvider({ children }: AuthProviderProps) {
<AuthContext.Provider
value={{
currentUser,
login,
loginWithGoogle,
loginWithGithub,
signup,
logout,
updateUserProfile,
}}
>
{!loading && children}
Expand Down
38 changes: 0 additions & 38 deletions code/client/src/ui/pages/auth/Auth.scss

This file was deleted.

43 changes: 0 additions & 43 deletions code/client/src/ui/pages/auth/components/OAuth.scss

This file was deleted.

33 changes: 0 additions & 33 deletions code/client/src/ui/pages/auth/components/OAuth.tsx

This file was deleted.

Loading

0 comments on commit b7a032c

Please sign in to comment.