치지직 앱으로 연동하고, 연동 확인된 ID만 접속할 수 있도록 수정, 개발용으로는 채널ID를 수동으로 접속할 수 있도록 변경.
최초 추천 고정 배치 추가.
This commit is contained in:
112
app/api/chzzk/callback/route.ts
Normal file
112
app/api/chzzk/callback/route.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
44
app/api/chzzk/login/route.ts
Normal file
44
app/api/chzzk/login/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { isChzzkOAuthConfigured } from '@/lib/auth/mode';
|
||||
import {
|
||||
createOAuthState,
|
||||
OAUTH_STATE_COOKIE,
|
||||
authCookieOptions,
|
||||
} from '@/lib/auth/session';
|
||||
import { buildChzzkLoginUrl } from '@/lib/chzzk/oauth';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
/**
|
||||
* 치지직 로그인 시작.
|
||||
* 쿼리로 치즈 설정을 넘기면 콜백에서 바로 룸 입장에 사용한다.
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
if (!isChzzkOAuthConfigured()) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
'치지직 OAuth가 설정되지 않았습니다. CHZZK_CLIENT_ID / CHZZK_CLIENT_SECRET / CHZZK_REDIRECT_URI 를 확인해 주세요.',
|
||||
},
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const cheesePerDice = Number(url.searchParams.get('cheesePerDice') || 1000);
|
||||
const multiplyByCheese = url.searchParams.get('multiplyByCheese') !== '0';
|
||||
const maxMultiplier = Number(url.searchParams.get('maxMultiplier') || 0);
|
||||
|
||||
const { state, sealed } = createOAuthState({
|
||||
cheesePerDice: Number.isFinite(cheesePerDice) ? cheesePerDice : 1000,
|
||||
multiplyByCheese,
|
||||
maxMultiplier: Number.isFinite(maxMultiplier) ? maxMultiplier : 0,
|
||||
});
|
||||
|
||||
const res = NextResponse.redirect(buildChzzkLoginUrl(state));
|
||||
res.cookies.set(OAUTH_STATE_COOKIE, sealed, {
|
||||
...authCookieOptions(600),
|
||||
maxAge: 600,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
Reference in New Issue
Block a user