Compare commits
1 Commits
347dfc1f4a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e93c0bafa |
@@ -108,12 +108,13 @@ export async function GET(request: Request) {
|
|||||||
res.cookies.set(AUTH_COOKIE, serializeAuthSession(session), authCookieOptions());
|
res.cookies.set(AUTH_COOKIE, serializeAuthSession(session), authCookieOptions());
|
||||||
res.cookies.delete(OAUTH_STATE_COOKIE);
|
res.cookies.delete(OAUTH_STATE_COOKIE);
|
||||||
return res;
|
return res;
|
||||||
} catch (e) {
|
} catch {
|
||||||
if (accessToken) await revokeToken(accessToken, 'access_token');
|
if (accessToken) await revokeToken(accessToken, 'access_token');
|
||||||
if (refreshToken) await revokeToken(refreshToken, 'refresh_token');
|
if (refreshToken) await revokeToken(refreshToken, 'refresh_token');
|
||||||
const message =
|
// 외부 오류 원문에는 계정 식별자 등이 포함될 수 있어 고정 문구만 전달한다.
|
||||||
e instanceof Error ? e.message : '치지직 로그인에 실패했습니다.';
|
const res = NextResponse.redirect(
|
||||||
const res = NextResponse.redirect(homeRedirect(origin, message));
|
homeRedirect(origin, '치지직 로그인에 실패했습니다. 다시 시도해 주세요.'),
|
||||||
|
);
|
||||||
res.cookies.delete(OAUTH_STATE_COOKIE);
|
res.cookies.delete(OAUTH_STATE_COOKIE);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,14 +103,11 @@ export async function POST(request: Request) {
|
|||||||
if (room.settings.chzzkEnabled) {
|
if (room.settings.chzzkEnabled) {
|
||||||
try {
|
try {
|
||||||
await ensureRoomChat(room.id);
|
await ensureRoomChat(room.id);
|
||||||
} catch (e) {
|
} catch {
|
||||||
if (created) {
|
if (created) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error:
|
error: '채팅 연결에 실패했습니다. 방송 중인지 확인해 주세요.',
|
||||||
e instanceof Error
|
|
||||||
? e.message
|
|
||||||
: '채팅 연결에 실패했습니다. 방송 중인지 확인해 주세요.',
|
|
||||||
room: toPublicRoom(room),
|
room: toPublicRoom(room),
|
||||||
created,
|
created,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -120,7 +120,8 @@ export async function fetchChatAccessToken(chatChannelId: string): Promise<strin
|
|||||||
}
|
}
|
||||||
const token = data.content?.accessToken;
|
const token = data.content?.accessToken;
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error(data.message || '채팅 accessToken을 받지 못했습니다.');
|
// 외부 응답 원문은 로그/화면으로 전달하지 않는다.
|
||||||
|
throw new Error('채팅 연결 인증 정보를 받지 못했습니다.');
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
|
import { createHash } from 'crypto';
|
||||||
import {
|
import {
|
||||||
chatServerUrl,
|
chatServerUrl,
|
||||||
fetchChannelInfo,
|
fetchChannelInfo,
|
||||||
@@ -98,7 +99,7 @@ function messageKey(
|
|||||||
): string | null {
|
): string | null {
|
||||||
const id = chat.msgUid ?? chat.messageId ?? null;
|
const id = chat.msgUid ?? chat.messageId ?? null;
|
||||||
if (id != null && String(id).length > 0) {
|
if (id != null && String(id).length > 0) {
|
||||||
return `${kind}:${String(id)}`;
|
return hashMessageKey(`${kind}:${String(id)}`);
|
||||||
}
|
}
|
||||||
const time = messageTimeMs(chat);
|
const time = messageTimeMs(chat);
|
||||||
const profile = parseMaybeJson<{ nickname?: string; userIdHash?: string }>(
|
const profile = parseMaybeJson<{ nickname?: string; userIdHash?: string }>(
|
||||||
@@ -109,9 +110,16 @@ function messageKey(
|
|||||||
const msg = String(chat.msg ?? chat.content ?? '');
|
const msg = String(chat.msg ?? chat.content ?? '');
|
||||||
if (time == null && !who && !msg) return null;
|
if (time == null && !who && !msg) return null;
|
||||||
if (kind === 'donation') {
|
if (kind === 'donation') {
|
||||||
return `donation:${time ?? 0}:${who}:${Number(extras?.payAmount ?? 0)}:${msg}`;
|
return hashMessageKey(
|
||||||
|
`donation:${time ?? 0}:${who}:${Number(extras?.payAmount ?? 0)}:${msg}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return `chat:${time ?? 0}:${who}:${msg}`;
|
return hashMessageKey(`chat:${time ?? 0}:${who}:${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 중복 검사용 키에는 사용자 해시·닉네임·메시지 원문을 보관하지 않는다. */
|
||||||
|
function hashMessageKey(value: string): string {
|
||||||
|
return createHash('sha256').update(value).digest('base64url');
|
||||||
}
|
}
|
||||||
|
|
||||||
function rememberMessageKey(handle: SessionHandle, key: string | null): boolean {
|
function rememberMessageKey(handle: SessionHandle, key: string | null): boolean {
|
||||||
@@ -475,11 +483,12 @@ export async function connectRoomChat(roomId: string) {
|
|||||||
setRoomMeta(roomId, { channelName: info.channelName, chatStatus: 'connecting' });
|
setRoomMeta(roomId, { channelName: info.channelName, chatStatus: 'connecting' });
|
||||||
notifyRoom(roomId);
|
notifyRoom(roomId);
|
||||||
await openSocket(roomId, channelId);
|
await openSocket(roomId, channelId);
|
||||||
} catch (e) {
|
} catch {
|
||||||
const message = e instanceof Error ? e.message : '채팅 연결 실패';
|
// 외부 라이브러리 오류에는 URL·토큰·식별자가 섞일 수 있어 원문을 보관하지 않는다.
|
||||||
|
const message = '채팅 연결에 실패했습니다. 방송 상태를 확인해 주세요.';
|
||||||
setRoomMeta(roomId, { chatStatus: 'error', chatError: message });
|
setRoomMeta(roomId, { chatStatus: 'error', chatError: message });
|
||||||
notifyRoom(roomId);
|
notifyRoom(roomId);
|
||||||
throw e instanceof Error ? e : new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ export async function exchangeAuthorizationCode(code: string, state: string) {
|
|||||||
tokenType?: string;
|
tokenType?: string;
|
||||||
expiresIn?: string | number;
|
expiresIn?: string | number;
|
||||||
};
|
};
|
||||||
message?: string;
|
|
||||||
code?: number;
|
code?: number;
|
||||||
};
|
};
|
||||||
if (!res.ok || !json.content?.accessToken) {
|
if (!res.ok || !json.content?.accessToken) {
|
||||||
throw new Error(json.message || `토큰 발급 실패 (${res.status})`);
|
// 외부 API 원문은 계정 정보 등이 섞일 수 있으므로 전달·기록하지 않는다.
|
||||||
|
throw new Error(`치지직 인증 처리 실패 (${res.status})`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
accessToken: json.content.accessToken,
|
accessToken: json.content.accessToken,
|
||||||
@@ -54,10 +54,9 @@ export async function fetchAuthedUser(accessToken: string) {
|
|||||||
});
|
});
|
||||||
const json = (await res.json().catch(() => ({}))) as {
|
const json = (await res.json().catch(() => ({}))) as {
|
||||||
content?: { channelId?: string; channelName?: string };
|
content?: { channelId?: string; channelName?: string };
|
||||||
message?: string;
|
|
||||||
};
|
};
|
||||||
if (!res.ok || !json.content?.channelId) {
|
if (!res.ok || !json.content?.channelId) {
|
||||||
throw new Error(json.message || `유저 정보 조회 실패 (${res.status})`);
|
throw new Error(`치지직 사용자 확인 실패 (${res.status})`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
channelId: json.content.channelId.toLowerCase(),
|
channelId: json.content.channelId.toLowerCase(),
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import { fileURLToPath } from "url";
|
|||||||
const projectRoot = path.dirname(fileURLToPath(import.meta.url));
|
const projectRoot = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
// 개발 서버에서도 OAuth code/state가 포함된 요청 URL을 콘솔에 남기지 않음.
|
||||||
|
// 운영 서버는 별도 access log를 사용하지 않으며, 앱에서도 요청 본문을 기록하지 않는다.
|
||||||
|
logging: {
|
||||||
|
incomingRequests: false,
|
||||||
|
},
|
||||||
// 홈 디렉터리 package-lock.json 오인 방지 (외부 도메인/IP 접속 시에도 안정)
|
// 홈 디렉터리 package-lock.json 오인 방지 (외부 도메인/IP 접속 시에도 안정)
|
||||||
turbopack: {
|
turbopack: {
|
||||||
root: projectRoot,
|
root: projectRoot,
|
||||||
|
|||||||
Reference in New Issue
Block a user