Files
thrilltrack-explorer/lib/services/auth/oauthService.ts

175 lines
4.6 KiB
TypeScript

/**
* 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,
};