105 lines
3.9 KiB
TypeScript
105 lines
3.9 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
||
import { defaultOAuthTransactionStore } from '@/lib/auth/oauth-state';
|
||
import { getOAuthClient } from '../start/route';
|
||
import { defaultTokenVault } from '@/lib/auth/token-vault';
|
||
import { defaultUserRepository } from '@/lib/repository/user-repository';
|
||
import { defaultSessionStore, setSessionCookie } from '@/lib/auth/session';
|
||
import { handleApiError, ValidationError } from '@/core/errors/http-errors';
|
||
import { validateSafeRedirectTarget } from '@/lib/auth/safe-redirect';
|
||
|
||
export const dynamic = 'force-dynamic';
|
||
|
||
export async function GET(req: NextRequest) {
|
||
try {
|
||
const { searchParams } = new URL(req.url);
|
||
const code = searchParams.get('code');
|
||
const state = searchParams.get('state');
|
||
const errorParam = searchParams.get('error');
|
||
const errorDescription = searchParams.get('error_description');
|
||
|
||
const origin = req.nextUrl.origin || 'http://localhost:3000';
|
||
|
||
// 1. Handle user cancellation or VK authorization rejection
|
||
if (errorParam) {
|
||
// Invalidate state transaction if present so it cannot be reused
|
||
if (state) {
|
||
try {
|
||
await defaultOAuthTransactionStore.consumeTransaction(state);
|
||
} catch {
|
||
// Ignore consumption error on cancellation path
|
||
}
|
||
}
|
||
|
||
const safeErrorMsg = encodeURIComponent(
|
||
(errorDescription || errorParam).replace(/[^\w\sа-яА-ЯёЁ.,-]/gi, '').slice(0, 100)
|
||
);
|
||
return NextResponse.redirect(`${origin}/?auth_error=${safeErrorMsg}`);
|
||
}
|
||
|
||
if (!code) {
|
||
throw new ValidationError('Authorization code is missing from callback query');
|
||
}
|
||
|
||
if (!state) {
|
||
throw new ValidationError('OAuth state parameter is missing from callback query');
|
||
}
|
||
|
||
// 2. Validate and consume single-use state transaction (recovers codeVerifier and redirectTarget)
|
||
const { codeVerifier, redirectTarget } = await defaultOAuthTransactionStore.consumeTransaction(state);
|
||
const safeRedirect = validateSafeRedirectTarget(redirectTarget);
|
||
|
||
const clientId = process.env.VK_APP_ID || (process.env.NODE_ENV === 'test' ? 'test_vk_app_id' : '');
|
||
if (!clientId) {
|
||
throw new ValidationError('VK_APP_ID is not configured in server environment');
|
||
}
|
||
|
||
const clientSecret = process.env.VK_CLIENT_SECRET;
|
||
const redirectUri = process.env.VK_REDIRECT_URI || `${origin}/api/auth/vk/callback`;
|
||
|
||
// 3. Exchange code for access token via dedicated VkOAuthClient
|
||
const oauthClient = getOAuthClient();
|
||
const tokenResponse = await oauthClient.exchangeCode({
|
||
code,
|
||
codeVerifier,
|
||
clientId,
|
||
clientSecret,
|
||
redirectUri,
|
||
state,
|
||
});
|
||
|
||
// 4. Encrypt sensitive tokens at rest before storing
|
||
const encryptedAccessToken = await defaultTokenVault.encrypt(tokenResponse.access_token);
|
||
const encryptedRefreshToken = tokenResponse.refresh_token
|
||
? await defaultTokenVault.encrypt(tokenResponse.refresh_token)
|
||
: undefined;
|
||
|
||
// 5. Retrieve organizer profile from VK API
|
||
const userProfile = await oauthClient.getUserProfile(
|
||
tokenResponse.access_token,
|
||
tokenResponse.user_id
|
||
);
|
||
|
||
// 6. Upsert user in repository
|
||
const sessionUser = await defaultUserRepository.upsertUserWithTokens({
|
||
vkUserId: String(tokenResponse.user_id),
|
||
firstName: userProfile.firstName,
|
||
lastName: userProfile.lastName,
|
||
username: userProfile.username,
|
||
avatarUrl: userProfile.avatarUrl,
|
||
encryptedAccessToken,
|
||
encryptedRefreshToken,
|
||
expiresIn: tokenResponse.expires_in,
|
||
scope: tokenResponse.scope,
|
||
});
|
||
|
||
// 7. Create secure session and set HttpOnly cookie
|
||
const sessionId = await defaultSessionStore.createSession(sessionUser);
|
||
|
||
const response = NextResponse.redirect(`${origin}${safeRedirect}`);
|
||
setSessionCookie(response, sessionId);
|
||
|
||
return response;
|
||
} catch (error: any) {
|
||
return handleApiError(error);
|
||
}
|
||
}
|