/**
* POST /api/auth/login — 共有トークンを検証して `dwatch_session` cookie を発行する。
*
* ★このルートは認証をかけない★(`lib/proxy/authenticate.ts` の `isPublicPath()` で通す)。
* 保護すると誰もログインできない。IP制限は proxy が別途かけるので、
* 許可されないIPからはここに到達する前に 403 になる。
*
* ★入力値(トークン)をログにも本文にも絶対に出さないこと★
* ★失敗しても「トークンが違う」以上の情報を返さないこと★
* (「そのトークンは存在するが期限切れ」等を返すと総当たりの手がかりになる)
*/
import { NextResponse } from "next/server";
import { isJsonObject } from "@/lib/api/json-body";
import {
SESSION_COOKIE_NAME,
SESSION_COOKIE_OPTIONS,
} from "@/lib/auth/cookie";
import { readAuthToken, verifyToken } from "@/lib/auth/token";
import { logEvent, withRequestId } from "@/lib/logger";
import type { ApiError, ErrorCode } from "@/lib/types";
const ROUTE = "/api/auth/login";
function errorResponse(status: number, code: ErrorCode, message: string) {
const body: ApiError = { error: { code, message, details: null } };
return NextResponse.json(body, { status });
}
export async function POST(request: Request) {
return withRequestId(request, async () => {
let body: unknown;
try {
body = await request.json();
} catch {
return errorResponse(422, "validation_failed", "JSONの形式が不正です");
}
// ★本文がオブジェクトでない(`null`・配列・数値・文字列)なら422★(指示書BB A5・D19。今の「JSON が読めない」と同じ文言)
if (!isJsonObject(body)) {
return errorResponse(422, "validation_failed", "JSONの形式が不正です");
}
const token = body.token;
if (typeof token !== "string" || token.length === 0) {
return errorResponse(422, "validation_failed", "tokenを指定してください");
}
// ★定数時間比較。ここで === を書かないこと★
if (!verifyToken(token, readAuthToken())) {
// ★入力されたトークンは載せない(成否だけ)★
// ★client_ip も載せない★ — ここに来た時点でIP判定は通過済みで、
// 接続元は定義上 ALLOWED_IPS に載っている既知の値(D-101)
logEvent("warn", {
event: "auth.login_failed",
route: ROUTE,
method: "POST",
});
return errorResponse(401, "invalid_token", "トークンが違います");
}
logEvent("info", {
event: "auth.login_success",
route: ROUTE,
method: "POST",
});
// 本文なしの 204。cookie は Set-Cookie で返す
const response = new NextResponse(null, { status: 204 });
// ★expires / maxAge は付けない(CLAUDE.md §7 でトークン期限は禁止)★
response.cookies.set({
name: SESSION_COOKIE_NAME,
value: token,
...SESSION_COOKIE_OPTIONS,
});
return response;
});
}