mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 05:11:13 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
237
lib/services/auth/authService.ts
Normal file
237
lib/services/auth/authService.ts
Normal 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,
|
||||
};
|
||||
10
lib/services/auth/index.ts
Normal file
10
lib/services/auth/index.ts
Normal 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';
|
||||
210
lib/services/auth/mfaService.ts
Normal file
210
lib/services/auth/mfaService.ts
Normal 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,
|
||||
};
|
||||
174
lib/services/auth/oauthService.ts
Normal file
174
lib/services/auth/oauthService.ts
Normal 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,
|
||||
};
|
||||
141
lib/services/auth/tokenStorage.ts
Normal file
141
lib/services/auth/tokenStorage.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user