randomayzer/src/app/api/auth/vk/callback/route.ts

105 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}