mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 14:11:12 -05:00
175 lines
4.6 KiB
TypeScript
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,
|
|
};
|