Refactor code structure and remove redundant changes

This commit is contained in:
pacnpal
2025-11-09 16:31:34 -05:00
parent 2884bc23ce
commit eb68cf40c6
1080 changed files with 27361 additions and 56687 deletions

View File

@@ -0,0 +1,237 @@
/**
* Authentication Service
*
* Core authentication service that handles login, registration, logout,
* token refresh, and user profile management with the Django backend.
*/
import { apiClient } from '@/lib/api/client';
import {
LoginCredentials,
RegisterData,
AuthResponse,
User,
ChangePasswordData,
UpdateProfileData,
UpdatePreferencesData,
UserPreferences,
UserRole,
UserPermissions,
UserStats,
} from '@/lib/types/auth';
import { tokenStorage } from './tokenStorage';
/**
* Login with email and password
*/
export async function login(credentials: LoginCredentials): Promise<AuthResponse> {
const response = await apiClient.post<AuthResponse>('/api/v1/auth/login', credentials);
// Store tokens
tokenStorage.setAccessToken(response.data.access);
tokenStorage.setRefreshToken(response.data.refresh);
return response.data;
}
/**
* Register a new user account
*/
export async function register(data: RegisterData): Promise<User> {
const response = await apiClient.post<User>('/api/v1/auth/register', data);
// Note: Django returns 201 and user profile, but doesn't auto-login
// The frontend should prompt to login after successful registration
return response.data;
}
/**
* Logout and clear tokens
*/
export async function logout(): Promise<void> {
try {
// Call logout endpoint (blacklists refresh token on server)
await apiClient.post('/api/v1/auth/logout');
} catch (error) {
// Continue with local logout even if server call fails
console.error('Logout API call failed:', error);
} finally {
// Always clear local tokens
tokenStorage.clearTokens();
}
}
/**
* Refresh access token using refresh token
*/
export async function refreshAccessToken(): Promise<AuthResponse> {
const refreshToken = tokenStorage.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await apiClient.post<AuthResponse>('/api/v1/auth/token/refresh', {
refresh: refreshToken,
});
// Store new tokens
tokenStorage.setAccessToken(response.data.access);
tokenStorage.setRefreshToken(response.data.refresh);
return response.data;
}
/**
* Get current user profile
*/
export async function getCurrentUser(): Promise<User> {
const response = await apiClient.get<User>('/api/v1/auth/me');
return response.data;
}
/**
* Update current user profile
*/
export async function updateProfile(data: UpdateProfileData): Promise<User> {
const response = await apiClient.patch<User>('/api/v1/auth/me', data);
return response.data;
}
/**
* Get current user's role
*/
export async function getUserRole(): Promise<UserRole> {
const response = await apiClient.get<UserRole>('/api/v1/auth/me/role');
return response.data;
}
/**
* Get current user's permissions
*/
export async function getUserPermissions(): Promise<UserPermissions> {
const response = await apiClient.get<UserPermissions>('/api/v1/auth/me/permissions');
return response.data;
}
/**
* Get current user's statistics
*/
export async function getUserStats(): Promise<UserStats> {
const response = await apiClient.get<UserStats>('/api/v1/auth/me/stats');
return response.data;
}
/**
* Get current user's preferences
*/
export async function getUserPreferences(): Promise<UserPreferences> {
const response = await apiClient.get<UserPreferences>('/api/v1/auth/me/preferences');
return response.data;
}
/**
* Update current user's preferences
*/
export async function updatePreferences(data: UpdatePreferencesData): Promise<UserPreferences> {
const response = await apiClient.patch<UserPreferences>('/api/v1/auth/me/preferences', data);
return response.data;
}
/**
* Change password for current user
*/
export async function changePassword(data: ChangePasswordData): Promise<void> {
await apiClient.post('/api/v1/auth/password/change', data);
}
/**
* Request password reset email
*/
export async function requestPasswordReset(email: string): Promise<void> {
await apiClient.post('/api/v1/auth/password/reset', { email });
}
/**
* Confirm password reset with token
*/
export async function confirmPasswordReset(
token: string,
password: string,
passwordConfirm: string
): Promise<void> {
await apiClient.post('/api/v1/auth/password/reset/confirm', {
token,
password,
password_confirm: passwordConfirm,
});
}
/**
* Verify email with token
*/
export async function verifyEmail(token: string): Promise<void> {
await apiClient.post('/api/v1/auth/verify-email', { token });
}
/**
* Resend email verification
*/
export async function resendVerification(): Promise<void> {
await apiClient.post('/api/v1/auth/verify-email/resend');
}
/**
* Request email change
*/
export async function requestEmailChange(newEmail: string): Promise<void> {
await apiClient.post('/api/v1/auth/change-email', {
new_email: newEmail,
});
}
/**
* Confirm email change with token
*/
export async function confirmEmailChange(token: string): Promise<void> {
await apiClient.post('/api/v1/auth/change-email/confirm', { token });
}
/**
* Check if user is authenticated
*/
export function isAuthenticated(): boolean {
return tokenStorage.hasValidAccessToken();
}
/**
* Get access token
*/
export function getAccessToken(): string | null {
return tokenStorage.getAccessToken();
}
// Export all functions as a service object for convenience
export const authService = {
login,
register,
logout,
refreshAccessToken,
getCurrentUser,
updateProfile,
getUserRole,
getUserPermissions,
getUserStats,
getUserPreferences,
updatePreferences,
changePassword,
requestPasswordReset,
confirmPasswordReset,
verifyEmail,
resendVerification,
requestEmailChange,
confirmEmailChange,
isAuthenticated,
getAccessToken,
};

View File

@@ -0,0 +1,10 @@
/**
* Authentication Services Export
*
* Central export point for all authentication-related services.
*/
export * from './authService';
export * from './mfaService';
export * from './oauthService';
export * from './tokenStorage';

View File

@@ -0,0 +1,210 @@
/**
* MFA (Multi-Factor Authentication) Service
*
* Handles TOTP (Time-based One-Time Password) setup, verification,
* and management for two-factor authentication.
*/
import { apiClient } from '@/lib/api/client';
import {
TOTPSetupResponse,
TOTPConfirmData,
TOTPVerifyData,
MFAChallengeData,
AuthResponse,
} from '@/lib/types/auth';
import { tokenStorage } from './tokenStorage';
/**
* Enable MFA/2FA for current user
* Returns TOTP secret and QR code URL
*/
export async function setupTOTP(): Promise<TOTPSetupResponse> {
const response = await apiClient.post<TOTPSetupResponse>('/api/v1/auth/mfa/enable');
return response.data;
}
/**
* Confirm MFA setup with verification token
* Completes MFA setup after verifying the token is valid
*/
export async function confirmTOTP(data: TOTPConfirmData): Promise<void> {
await apiClient.post('/api/v1/auth/mfa/confirm', data);
}
/**
* Disable MFA/2FA for current user
* Removes all TOTP devices and disables MFA requirement
*/
export async function disableTOTP(): Promise<void> {
await apiClient.post('/api/v1/auth/mfa/disable');
}
/**
* Verify MFA token (for testing)
* Returns whether the token is valid
*/
export async function verifyTOTP(data: TOTPVerifyData): Promise<boolean> {
try {
await apiClient.post('/api/v1/auth/mfa/verify', data);
return true;
} catch (error) {
return false;
}
}
/**
* Complete MFA challenge during login
* Called when user has MFA enabled and needs to provide token
*/
export async function challengeMFA(data: MFAChallengeData): Promise<AuthResponse> {
const response = await apiClient.post<AuthResponse>('/api/v1/auth/mfa/challenge', {
token: data.code,
});
// Store tokens after successful MFA challenge
tokenStorage.setAccessToken(response.data.access);
tokenStorage.setRefreshToken(response.data.refresh);
return response.data;
}
/**
* Generate new backup codes
* Returns a list of one-time use backup codes
*/
export async function generateBackupCodes(): Promise<string[]> {
const response = await apiClient.post<{ backup_codes: string[] }>('/api/v1/auth/mfa/backup-codes');
return response.data.backup_codes;
}
/**
* Use a backup code to login when TOTP is unavailable
*/
export async function useBackupCode(code: string): Promise<AuthResponse> {
const response = await apiClient.post<AuthResponse>('/api/v1/auth/mfa/backup-code', {
code,
});
// Store tokens after successful backup code use
tokenStorage.setAccessToken(response.data.access);
tokenStorage.setRefreshToken(response.data.refresh);
return response.data;
}
/**
* Remove MFA with password confirmation
*/
export async function removeMFA(password: string): Promise<void> {
await apiClient.post('/api/v1/auth/mfa/remove', {
password,
});
}
/**
* WebAuthn/Passkey Support
* Django-allauth provides WebAuthn through its MFA module
*/
export interface WebAuthnCredential {
id: string;
name: string;
created_at: string;
last_used?: string;
}
/**
* Get list of registered WebAuthn credentials (passkeys)
*/
export async function getWebAuthnCredentials(): Promise<WebAuthnCredential[]> {
const response = await apiClient.get<{ credentials: WebAuthnCredential[] }>(
'/accounts/mfa/webauthn/list/'
);
return response.data.credentials;
}
/**
* Start WebAuthn registration process
* Returns challenge data for credential creation
*/
export async function startWebAuthnRegistration(): Promise<PublicKeyCredentialCreationOptions> {
const response = await apiClient.get<PublicKeyCredentialCreationOptions>(
'/accounts/mfa/webauthn/add/'
);
return response.data;
}
/**
* Complete WebAuthn registration
* @param credential The created PublicKeyCredential from browser
* @param name Optional friendly name for the credential
*/
export async function completeWebAuthnRegistration(
credential: PublicKeyCredential,
name?: string
): Promise<void> {
await apiClient.post('/accounts/mfa/webauthn/add/', {
credential: JSON.stringify(credential),
name,
});
}
/**
* Remove a WebAuthn credential
* @param credentialId The ID of the credential to remove
*/
export async function removeWebAuthnCredential(credentialId: string): Promise<void> {
await apiClient.post(`/accounts/mfa/webauthn/remove/${credentialId}/`);
}
/**
* Start WebAuthn authentication challenge
* Used during login when user has passkey enabled
*/
export async function startWebAuthnAuthentication(): Promise<PublicKeyCredentialRequestOptions> {
const response = await apiClient.get<PublicKeyCredentialRequestOptions>(
'/accounts/mfa/webauthn/authenticate/'
);
return response.data;
}
/**
* Complete WebAuthn authentication
* @param credential The assertion credential from browser
*/
export async function completeWebAuthnAuthentication(
credential: PublicKeyCredential
): Promise<AuthResponse> {
const response = await apiClient.post<AuthResponse>(
'/accounts/mfa/webauthn/authenticate/',
{
credential: JSON.stringify(credential),
}
);
// Store tokens after successful WebAuthn authentication
tokenStorage.setAccessToken(response.data.access);
tokenStorage.setRefreshToken(response.data.refresh);
return response.data;
}
// Export all functions as a service object for convenience
export const mfaService = {
setupTOTP,
confirmTOTP,
disableTOTP,
verifyTOTP,
challengeMFA,
generateBackupCodes,
useBackupCode,
removeMFA,
// WebAuthn/Passkey methods
getWebAuthnCredentials,
startWebAuthnRegistration,
completeWebAuthnRegistration,
removeWebAuthnCredential,
startWebAuthnAuthentication,
completeWebAuthnAuthentication,
};

View File

@@ -0,0 +1,174 @@
/**
* OAuth Service
*
* Handles OAuth authentication flow with Google and Discord providers.
* Works with django-allauth backend for seamless OAuth integration.
*/
import { apiClient } from '@/lib/api/client';
import { AuthResponse } from '@/lib/types/auth';
import { tokenStorage } from './tokenStorage';
export type OAuthProvider = 'google' | 'discord';
/**
* OAuth state management for CSRF protection
*/
const OAUTH_STATE_KEY = 'oauth_state';
const OAUTH_REDIRECT_KEY = 'oauth_redirect';
/**
* Generate random state for OAuth CSRF protection
*/
function generateState(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
/**
* Store OAuth state in sessionStorage
*/
function storeState(state: string, redirectUrl?: string): void {
if (typeof window === 'undefined') return;
sessionStorage.setItem(OAUTH_STATE_KEY, state);
if (redirectUrl) {
sessionStorage.setItem(OAUTH_REDIRECT_KEY, redirectUrl);
}
}
/**
* Retrieve and validate OAuth state
*/
function validateState(state: string): boolean {
if (typeof window === 'undefined') return false;
const storedState = sessionStorage.getItem(OAUTH_STATE_KEY);
sessionStorage.removeItem(OAUTH_STATE_KEY);
return storedState === state;
}
/**
* Get stored redirect URL
*/
function getRedirectUrl(): string | null {
if (typeof window === 'undefined') return null;
const redirectUrl = sessionStorage.getItem(OAUTH_REDIRECT_KEY);
sessionStorage.removeItem(OAUTH_REDIRECT_KEY);
return redirectUrl;
}
/**
* Initiate OAuth flow for a provider
*
* @param provider - OAuth provider ('google' or 'discord')
* @param redirectUrl - Optional URL to redirect to after successful OAuth
*/
export async function initiateOAuth(
provider: OAuthProvider,
redirectUrl?: string
): Promise<void> {
// Generate state for CSRF protection
const state = generateState();
storeState(state, redirectUrl);
// Build OAuth initiation URL
const callbackUrl = `${window.location.origin}/auth/oauth/callback`;
const params = new URLSearchParams({
state,
redirect_uri: callbackUrl,
});
// Redirect to OAuth provider via Django backend
const oauthUrl = `${process.env.NEXT_PUBLIC_DJANGO_API_URL}/api/v1/auth/oauth/${provider}/?${params}`;
window.location.href = oauthUrl;
}
/**
* Handle OAuth callback after provider authentication
*
* @param provider - OAuth provider
* @param code - Authorization code from provider
* @param state - State parameter for CSRF validation
* @returns Auth response with JWT tokens and user info
*/
export async function handleOAuthCallback(
provider: OAuthProvider,
code: string,
state: string
): Promise<{ authResponse: AuthResponse; redirectUrl: string | null }> {
// Validate state to prevent CSRF attacks
if (!validateState(state)) {
throw new Error('Invalid OAuth state - possible CSRF attack');
}
// Exchange code for JWT tokens
const response = await apiClient.post<AuthResponse>(
`/api/v1/auth/oauth/${provider}/callback`,
{ code, state }
);
// Store tokens
tokenStorage.setAccessToken(response.data.access);
tokenStorage.setRefreshToken(response.data.refresh);
// Get redirect URL if stored
const redirectUrl = getRedirectUrl();
return {
authResponse: response.data,
redirectUrl,
};
}
/**
* Link OAuth provider to existing account
*
* @param provider - OAuth provider to link
*/
export async function linkOAuthProvider(provider: OAuthProvider): Promise<void> {
const state = generateState();
storeState(state);
const callbackUrl = `${window.location.origin}/auth/oauth/link/callback`;
const params = new URLSearchParams({
state,
redirect_uri: callbackUrl,
link: 'true',
});
const oauthUrl = `${process.env.NEXT_PUBLIC_DJANGO_API_URL}/api/v1/auth/oauth/${provider}/?${params}`;
window.location.href = oauthUrl;
}
/**
* Unlink OAuth provider from account
*
* @param provider - OAuth provider to unlink
*/
export async function unlinkOAuthProvider(provider: OAuthProvider): Promise<void> {
await apiClient.post(`/api/v1/auth/oauth/${provider}/unlink`);
}
/**
* Get list of linked OAuth providers
*/
export async function getLinkedProviders(): Promise<OAuthProvider[]> {
const response = await apiClient.get<{ providers: OAuthProvider[] }>(
'/api/v1/auth/oauth/linked'
);
return response.data.providers;
}
// Export all functions as a service object
export const oauthService = {
initiateOAuth,
handleOAuthCallback,
linkOAuthProvider,
unlinkOAuthProvider,
getLinkedProviders,
};

View File

@@ -0,0 +1,141 @@
/**
* Token Storage Utility
*
* Handles JWT token storage, retrieval, and validation in localStorage.
*/
import { TokenPayload, TokenStorage } from '@/lib/types/auth';
// Storage keys
const ACCESS_TOKEN_KEY = 'auth_access_token';
const REFRESH_TOKEN_KEY = 'auth_refresh_token';
/**
* Decode JWT token payload without verification
* (Verification happens on the server)
*/
function decodeToken(token: string): TokenPayload | null {
try {
const parts = token.split('.');
if (parts.length !== 3) {
return null;
}
const payload = parts[1];
const decoded = JSON.parse(atob(payload));
return decoded as TokenPayload;
} catch (error) {
console.error('Failed to decode token:', error);
return null;
}
}
/**
* Check if a token is expired
*/
function isTokenExpired(token: string): boolean {
const decoded = decodeToken(token);
if (!decoded || !decoded.exp) {
return true;
}
// Check if token expires in the next 60 seconds (add buffer)
const expiryTime = decoded.exp * 1000; // Convert to milliseconds
const now = Date.now();
const bufferTime = 60 * 1000; // 60 seconds buffer
return now >= (expiryTime - bufferTime);
}
/**
* Token storage implementation
*/
export const tokenStorage: TokenStorage = {
getAccessToken(): string | null {
if (typeof window === 'undefined') {
return null;
}
return localStorage.getItem(ACCESS_TOKEN_KEY);
},
setAccessToken(token: string): void {
if (typeof window === 'undefined') {
return;
}
localStorage.setItem(ACCESS_TOKEN_KEY, token);
},
getRefreshToken(): string | null {
if (typeof window === 'undefined') {
return null;
}
return localStorage.getItem(REFRESH_TOKEN_KEY);
},
setRefreshToken(token: string): void {
if (typeof window === 'undefined') {
return;
}
localStorage.setItem(REFRESH_TOKEN_KEY, token);
},
clearTokens(): void {
if (typeof window === 'undefined') {
return;
}
localStorage.removeItem(ACCESS_TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
},
hasValidAccessToken(): boolean {
const token = this.getAccessToken();
if (!token) {
return false;
}
return !isTokenExpired(token);
},
};
/**
* Get the decoded payload from the access token
*/
export function getTokenPayload(): TokenPayload | null {
const token = tokenStorage.getAccessToken();
if (!token) {
return null;
}
return decodeToken(token);
}
/**
* Check if the refresh token is expired
*/
export function isRefreshTokenExpired(): boolean {
const token = tokenStorage.getRefreshToken();
if (!token) {
return true;
}
return isTokenExpired(token);
}
/**
* Get time until access token expires (in milliseconds)
*/
export function getTimeUntilExpiry(): number | null {
const token = tokenStorage.getAccessToken();
if (!token) {
return null;
}
const decoded = decodeToken(token);
if (!decoded || !decoded.exp) {
return null;
}
const expiryTime = decoded.exp * 1000;
const now = Date.now();
const timeRemaining = expiryTime - now;
return timeRemaining > 0 ? timeRemaining : 0;
}