Redesign Menu

This commit is contained in:
Thanakarn Klangkasame
2025-10-14 10:58:10 +07:00
parent a20c986d5d
commit 9145e48d7d
26 changed files with 2649 additions and 1386 deletions

149
src/server/auth/service.ts Normal file
View File

@@ -0,0 +1,149 @@
import jwt from "jsonwebtoken";
import {UserSession} from "@/types/auth";
import {Claims} from "@/types/auth";
const DEBUG_TOKEN = (process.env.DEBUG_TOKEN ?? "true").toLowerCase() === "true";
function redactToken(t?: string) {
if (!t) return "<empty>";
const raw = t.startsWith("Bearer ") ? t.slice(7) : t;
if (raw.length <= 12) return raw.replace(/./g, "*");
return `${raw.slice(0, 6)}${raw.slice(-6)}`;
}
function parseBearer(input: string) {
return input?.startsWith("Bearer ") ? input.slice(7) : input;
}
function decodeBase64UrlPart(part: string): unknown {
try {
const pad =
part.length % 4 === 2 ? "==" : part.length % 4 === 3 ? "=" : "";
const s = part.replace(/-/g, "+").replace(/_/g, "/") + pad;
const json = Buffer.from(s, "base64").toString("utf8");
return JSON.parse(json);
} catch {
return null;
}
}
export async function getUserFromToken(
token: string,
): Promise<UserSession | null> {
const input = parseBearer(token);
if (DEBUG_TOKEN) {
console.log("[server-auth] getUserFromToken() input:", {
tokenPreview: redactToken(token),
hasBearerPrefix: token?.startsWith?.("Bearer "),
});
}
if (!input) {
if (DEBUG_TOKEN) console.warn("[server-auth] no token provided");
return null;
}
const HS_SECRET = process.env.JWT_SECRET;
const RS_PUBLIC = process.env.JWT_PUBLIC_KEY;
let decoded: Claims | null = null;
let headerAlg: string | undefined;
try {
const [hPart] = input.split(".");
const hdr = decodeBase64UrlPart(hPart) as { alg?: string } | null;
headerAlg = hdr?.alg;
if (HS_SECRET) {
decoded = jwt.verify(input, HS_SECRET, {
algorithms: ["HS256", "HS384", "HS512"],
clockTolerance: 5,
}) as Claims;
if (DEBUG_TOKEN)
console.log("[server-auth] verified with HS secret", {alg: headerAlg});
} else if (RS_PUBLIC) {
decoded = jwt.verify(input, RS_PUBLIC, {
algorithms: ["RS256", "RS384", "RS512"],
clockTolerance: 5,
}) as Claims;
if (DEBUG_TOKEN)
console.log("[server-auth] verified with RS public key", {
alg: headerAlg,
});
} else {
decoded = jwt.decode(input) as Claims | null;
if (DEBUG_TOKEN)
console.warn("[server-auth] decoded WITHOUT verification", {
alg: headerAlg,
});
}
} catch (e: unknown) {
const err = e as Error;
if (DEBUG_TOKEN) {
console.error("[server-auth] verify/decode failed:", {
message: err.message,
name: err.name,
alg: headerAlg,
});
}
try {
const parts = input.split(".");
if (parts.length >= 2) {
decoded = decodeBase64UrlPart(parts[1]) as Claims | null;
if (DEBUG_TOKEN)
console.warn("[server-auth] manual payload decode fallback used");
}
} catch {
/* ignore */
}
}
if (!decoded) {
if (DEBUG_TOKEN) console.warn("[server-auth] decoded = null");
return null;
}
const now = Math.floor(Date.now() / 1000);
if (typeof decoded.nbf === "number" && now + 5 < decoded.nbf) {
if (DEBUG_TOKEN)
console.warn("[server-auth] token not yet valid", {now, nbf: decoded.nbf});
}
if (typeof decoded.exp === "number" && now - 5 > decoded.exp) {
if (DEBUG_TOKEN)
console.warn("[server-auth] token expired", {now, exp: decoded.exp});
}
const user: UserSession = {
id: decoded.sub ?? "",
email: decoded.email,
tenantId: decoded.tenant_id,
tenantKey: decoded.tenant_key ?? decoded.tenantKey,
roles: Array.isArray(decoded.roles) ? decoded.roles : [],
_dbg: DEBUG_TOKEN
? {
alg: headerAlg,
hasExp: typeof decoded.exp === "number",
hasNbf: typeof decoded.nbf === "number",
iss: decoded.iss,
aud: decoded.aud,
sid: decoded.sid,
jti: decoded.jti,
}
: undefined,
};
if (DEBUG_TOKEN) {
console.log("[server-auth] user parsed:", {
id: user.id,
email: user.email,
tenantId: user.tenantId,
tenantKey: user.tenantKey,
roles: user.roles,
alg: user._dbg?.alg,
});
}
if (!user.id && DEBUG_TOKEN) console.warn("[server-auth] missing sub");
return user;
}