refactor: simplify OAuth routes, add type safety, deduplicate UI components
- Extract handleOAuthCallback to eliminate GET/POST duplication in oauth.ts - Add P2002 race condition handling in findOrCreateUser - Add .unref() to stateStore cleanup timer to not block process exit - Use Provider union type instead of bare strings throughout OAuth code - Export API_BASE from api.ts, reuse in OAuthButtons - Extract MobileBranding component to deduplicate Login/Register mobile brand - Extract shared Logo component in AuthBranding - Remove unnecessary WHAT comments Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,9 @@ type ProviderUser = {
|
||||
avatarUrl: string | null;
|
||||
};
|
||||
|
||||
const providers: Record<string, ProviderConfig> = {
|
||||
export type Provider = 'google' | 'github' | 'apple';
|
||||
|
||||
const providers: Record<Provider, ProviderConfig> = {
|
||||
google: {
|
||||
authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
||||
tokenUrl: 'https://oauth2.googleapis.com/token',
|
||||
@@ -35,14 +37,14 @@ const providers: Record<string, ProviderConfig> = {
|
||||
},
|
||||
};
|
||||
|
||||
function getClientId(provider: string): string {
|
||||
function getClientId(provider: Provider): string {
|
||||
const envKey = provider === 'apple' ? 'APPLE_CLIENT_ID' : `${provider.toUpperCase()}_CLIENT_ID`;
|
||||
const value = process.env[envKey];
|
||||
if (!value) throw new Error(`Missing env: ${envKey}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function getClientSecret(provider: string): string {
|
||||
function getClientSecret(provider: Provider): string {
|
||||
if (provider === 'apple') return buildAppleClientSecret();
|
||||
const envKey = `${provider.toUpperCase()}_CLIENT_SECRET`;
|
||||
const value = process.env[envKey];
|
||||
@@ -50,28 +52,28 @@ function getClientSecret(provider: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
function getCallbackUrl(provider: string): string {
|
||||
function getCallbackUrl(provider: Provider): string {
|
||||
const base = process.env.OAUTH_CALLBACK_BASE_URL || 'http://localhost:3000';
|
||||
return `${base}/api/auth/oauth/${provider}/callback`;
|
||||
}
|
||||
|
||||
// --- State management (CSRF protection) ---
|
||||
const stateStore = new Map<string, { provider: string; createdAt: number }>();
|
||||
|
||||
setInterval(() => {
|
||||
const cleanupTimer = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, value] of stateStore) {
|
||||
if (now - value.createdAt > 10 * 60 * 1000) stateStore.delete(key);
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
cleanupTimer.unref();
|
||||
|
||||
function generateState(provider: string): string {
|
||||
function generateState(provider: Provider): string {
|
||||
const state = crypto.randomBytes(32).toString('hex');
|
||||
stateStore.set(state, { provider, createdAt: Date.now() });
|
||||
return state;
|
||||
}
|
||||
|
||||
function validateState(state: string, provider: string): boolean {
|
||||
function validateState(state: string, provider: Provider): boolean {
|
||||
const entry = stateStore.get(state);
|
||||
if (!entry) return false;
|
||||
if (entry.provider !== provider) return false;
|
||||
@@ -83,7 +85,6 @@ function validateState(state: string, provider: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Apple client_secret JWT ---
|
||||
function buildAppleClientSecret(): string {
|
||||
const teamId = process.env.APPLE_TEAM_ID;
|
||||
const keyId = process.env.APPLE_KEY_ID;
|
||||
@@ -105,9 +106,7 @@ function buildAppleClientSecret(): string {
|
||||
return `${signingInput}.${sig.toString('base64url')}`;
|
||||
}
|
||||
|
||||
// --- Public API ---
|
||||
|
||||
export function buildAuthUrl(provider: string): string {
|
||||
export function buildAuthUrl(provider: Provider): string {
|
||||
const config = providers[provider];
|
||||
if (!config) throw new Error(`Unknown provider: ${provider}`);
|
||||
|
||||
@@ -127,7 +126,7 @@ export function buildAuthUrl(provider: string): string {
|
||||
return `${config.authUrl}?${params.toString()}`;
|
||||
}
|
||||
|
||||
export async function exchangeCodeForToken(provider: string, code: string): Promise<string> {
|
||||
export async function exchangeCodeForToken(provider: Provider, code: string): Promise<string> {
|
||||
const config = providers[provider];
|
||||
if (!config) throw new Error(`Unknown provider: ${provider}`);
|
||||
|
||||
@@ -163,7 +162,7 @@ export async function exchangeCodeForToken(provider: string, code: string): Prom
|
||||
return data.access_token as string;
|
||||
}
|
||||
|
||||
export async function fetchProviderUser(provider: string, token: string): Promise<ProviderUser> {
|
||||
export async function fetchProviderUser(provider: Provider, token: string): Promise<ProviderUser> {
|
||||
if (provider === 'apple') {
|
||||
return parseAppleIdToken(token);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user