113 lines
3.6 KiB
TypeScript
113 lines
3.6 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import { getPublicOrigin } from '@/lib/auth/public-origin';
|
|
import {
|
|
AUTH_COOKIE,
|
|
OAUTH_STATE_COOKIE,
|
|
authCookieOptions,
|
|
createAuthSession,
|
|
parseOAuthStateCookie,
|
|
serializeAuthSession,
|
|
} from '@/lib/auth/session';
|
|
import {
|
|
exchangeAuthorizationCode,
|
|
fetchAuthedUser,
|
|
revokeToken,
|
|
} from '@/lib/chzzk/oauth';
|
|
import {
|
|
cancelScheduledDisconnectRoomChat,
|
|
ensureRoomChat,
|
|
} from '@/lib/chzzk/chat-session';
|
|
import {
|
|
createRoom,
|
|
findRoomByChannelId,
|
|
getRoom,
|
|
} from '@/lib/rooms/store';
|
|
|
|
export const runtime = 'nodejs';
|
|
|
|
function homeRedirect(origin: string, error?: string) {
|
|
const url = new URL('/', origin);
|
|
if (error) url.searchParams.set('authError', error);
|
|
else url.searchParams.set('auth', 'ok');
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* 치지직 OAuth 콜백.
|
|
* 채널 ID 확인 → 우리 세션 쿠키만 저장 → 치지직 토큰 폐기 → 룸 입장
|
|
*/
|
|
export async function GET(request: Request) {
|
|
const url = new URL(request.url);
|
|
const origin = getPublicOrigin(request);
|
|
const code = url.searchParams.get('code') || '';
|
|
const state = url.searchParams.get('state') || '';
|
|
const stateCookie = request.headers
|
|
.get('cookie')
|
|
?.match(/(?:^|;\s*)drinkmarble_oauth_state=([^;]+)/)?.[1];
|
|
|
|
const decodedState = stateCookie
|
|
? parseOAuthStateCookie(decodeURIComponent(stateCookie))
|
|
: null;
|
|
|
|
if (!code || !state || !decodedState || decodedState.state !== state) {
|
|
const res = NextResponse.redirect(
|
|
homeRedirect(origin, '로그인 검증에 실패했습니다. 다시 시도해 주세요.'),
|
|
);
|
|
res.cookies.delete(OAUTH_STATE_COOKIE);
|
|
return res;
|
|
}
|
|
|
|
let accessToken: string | null = null;
|
|
let refreshToken: string | null = null;
|
|
|
|
try {
|
|
const tokens = await exchangeAuthorizationCode(code, state);
|
|
accessToken = tokens.accessToken;
|
|
refreshToken = tokens.refreshToken;
|
|
|
|
const user = await fetchAuthedUser(accessToken);
|
|
const session = createAuthSession(user.channelId, user.channelName);
|
|
|
|
// 치지직 토큰은 저장하지 않고 즉시 폐기
|
|
await revokeToken(accessToken, 'access_token');
|
|
if (refreshToken) await revokeToken(refreshToken, 'refresh_token');
|
|
accessToken = null;
|
|
refreshToken = null;
|
|
|
|
let room = findRoomByChannelId(user.channelId);
|
|
if (!room) {
|
|
room = createRoom({
|
|
chzzkEnabled: true,
|
|
liveUrl: `https://chzzk.naver.com/live/${user.channelId}`,
|
|
channelId: user.channelId,
|
|
cheesePerDice: decodedState.cheesePerDice,
|
|
multiplyByCheese: decodedState.multiplyByCheese,
|
|
maxMultiplier: decodedState.maxMultiplier,
|
|
});
|
|
}
|
|
|
|
cancelScheduledDisconnectRoomChat(room.id);
|
|
try {
|
|
await ensureRoomChat(room.id);
|
|
} catch {
|
|
// 방송 중이 아니어도 세션·룸은 유지하고 게임으로 보냄
|
|
}
|
|
|
|
// room.id = 서버 룸 UUID (게임 URL용)
|
|
// user.channelId = 치지직 채널 (세션·room.settings.channelId에 보관)
|
|
const roomId = getRoom(room.id)?.id ?? room.id;
|
|
const res = NextResponse.redirect(new URL(`/game?roomId=${roomId}`, origin));
|
|
res.cookies.set(AUTH_COOKIE, serializeAuthSession(session), authCookieOptions());
|
|
res.cookies.delete(OAUTH_STATE_COOKIE);
|
|
return res;
|
|
} catch (e) {
|
|
if (accessToken) await revokeToken(accessToken, 'access_token');
|
|
if (refreshToken) await revokeToken(refreshToken, 'refresh_token');
|
|
const message =
|
|
e instanceof Error ? e.message : '치지직 로그인에 실패했습니다.';
|
|
const res = NextResponse.redirect(homeRedirect(origin, message));
|
|
res.cookies.delete(OAUTH_STATE_COOKIE);
|
|
return res;
|
|
}
|
|
}
|