Add login gate, calculation history, and AI markdown download.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-13 09:39:38 +08:00
parent abf78cbbb5
commit 462bec2739
23 changed files with 878 additions and 74 deletions
+41
View File
@@ -0,0 +1,41 @@
export const SESSION_COOKIE = "zhimingge_session";
export const SESSION_MAX_AGE_SEC = 60 * 60 * 24 * 7;
export function isAuthEnabled(): boolean {
return !!(
process.env.AUTH_USERNAME?.trim() &&
process.env.AUTH_PASSWORD?.trim() &&
process.env.AUTH_SESSION_SECRET?.trim()
);
}
export function getAuthUsername(): string {
const username = process.env.AUTH_USERNAME?.trim();
if (!username) {
throw new Error("未配置 AUTH_USERNAME");
}
return username;
}
export function getAuthPassword(): string {
const password = process.env.AUTH_PASSWORD?.trim();
if (!password) {
throw new Error("未配置 AUTH_PASSWORD");
}
return password;
}
export function getAuthSessionSecret(): string {
const secret = process.env.AUTH_SESSION_SECRET?.trim();
if (!secret) {
throw new Error("未配置 AUTH_SESSION_SECRET");
}
return secret;
}
export function verifyCredentials(username: string, password: string): boolean {
if (!isAuthEnabled()) {
return true;
}
return username === getAuthUsername() && password === getAuthPassword();
}
+66
View File
@@ -0,0 +1,66 @@
import {
getAuthSessionSecret,
isAuthEnabled,
SESSION_COOKIE,
SESSION_MAX_AGE_SEC,
} from "@/lib/auth/config";
const encoder = new TextEncoder();
async function hmacHex(payload: string, secret: string): Promise<string> {
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
return Array.from(new Uint8Array(sig))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
export async function createSessionToken(username: string): Promise<string> {
const exp = Date.now() + SESSION_MAX_AGE_SEC * 1000;
const payload = `${exp}:${username}`;
const sig = await hmacHex(payload, getAuthSessionSecret());
return `${payload}:${sig}`;
}
export async function verifySessionToken(token: string): Promise<boolean> {
if (!isAuthEnabled()) {
return true;
}
const parts = token.split(":");
if (parts.length < 3) {
return false;
}
const sig = parts.pop()!;
const username = parts.pop()!;
const exp = Number(parts.join(":"));
if (!Number.isFinite(exp) || exp < Date.now()) {
return false;
}
const payload = `${exp}:${username}`;
const expected = await hmacHex(payload, getAuthSessionSecret());
return sig === expected;
}
export async function getSessionUsername(
token: string | undefined,
): Promise<string | null> {
if (!isAuthEnabled()) {
return "guest";
}
if (!token || !(await verifySessionToken(token))) {
return null;
}
const parts = token.split(":");
if (parts.length < 3) {
return null;
}
return parts[parts.length - 2] ?? null;
}
export { SESSION_COOKIE, SESSION_MAX_AGE_SEC };