Add registration flow and improve admin post management

This commit is contained in:
爱喝水的木子
2026-03-19 20:17:56 +08:00
parent 50a1e476c8
commit 17f5f6adcb
28 changed files with 799 additions and 192 deletions

View File

@@ -3,6 +3,8 @@ import { NextRequest } from "next/server";
const COOKIE_NAME = "admin_session";
const encoder = new TextEncoder();
let cachedKey: CryptoKey | null = null;
let cachedSecret: string | null = null;
function getSecret() {
const secret = process.env.SESSION_SECRET;
@@ -15,16 +17,32 @@ function getSecret() {
export type SessionPayload = {
role: "admin";
iat: number;
exp?: number;
uid?: string;
name?: string;
};
async function hmacSha256(data: string, secret: string): Promise<string> {
const key = await crypto.subtle.importKey(
export function getAdminName() {
return process.env.ADMIN_NAME?.trim() || "Admin";
}
async function getHmacKey(secret: string) {
if (cachedKey && cachedSecret === secret) {
return cachedKey;
}
cachedSecret = secret;
cachedKey = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
return cachedKey;
}
async function hmacSha256(data: string, secret: string): Promise<string> {
const key = await getHmacKey(secret);
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
return Buffer.from(sig).toString("base64url");
}
@@ -45,6 +63,12 @@ export async function verifySession(token?: string): Promise<SessionPayload | nu
if (check !== sig) return null;
try {
const payload = JSON.parse(Buffer.from(base, "base64url").toString());
if (typeof payload?.exp !== "number") {
return null;
}
if (Date.now() > payload.exp) {
return null;
}
return payload;
} catch {
return null;
@@ -57,11 +81,12 @@ export async function requireAdminFromRequest(req: NextRequest): Promise<boolean
}
export function setAdminCookie(token: string) {
const isProd = process.env.NODE_ENV === "production";
cookies().set(COOKIE_NAME, token, {
httpOnly: true,
sameSite: "lax",
secure: true,
maxAge: 60 * 60 * 24 * 30
secure: isProd,
maxAge: 60 * 60 * 24
});
}

23
lib/opc.ts Normal file
View File

@@ -0,0 +1,23 @@
export const OPC_SIGNAL_VALUES = ["solo-feed"] as const;
export type OpcSignalValue = (typeof OPC_SIGNAL_VALUES)[number];
export const OPC_SIGNAL_LABELS: Record<OpcSignalValue, string> = {
"solo-feed": "solo-feed"
};
export const DEFAULT_OPC_SIGNAL: OpcSignalValue = "solo-feed";
const OPC_SIGNAL_SET = new Set<string>(OPC_SIGNAL_VALUES);
export function isOpcSignal(value?: string | null): value is OpcSignalValue {
if (!value) return false;
return OPC_SIGNAL_SET.has(value);
}
export function getOpcSignalLabel(value?: string | null): string {
if (value && OPC_SIGNAL_SET.has(value)) {
return OPC_SIGNAL_LABELS[value as OpcSignalValue];
}
return OPC_SIGNAL_LABELS[DEFAULT_OPC_SIGNAL];
}

20
lib/password.ts Normal file
View File

@@ -0,0 +1,20 @@
import crypto from "crypto";
const ITERATIONS = 100_000;
const KEY_LENGTH = 32;
const DIGEST = "sha256";
const SALT_BYTES = 16;
export function hashPassword(password: string, salt?: string) {
const realSalt = salt ?? crypto.randomBytes(SALT_BYTES).toString("hex");
const hash = crypto.pbkdf2Sync(password, realSalt, ITERATIONS, KEY_LENGTH, DIGEST).toString("hex");
return { salt: realSalt, hash };
}
export function verifyPassword(password: string, salt: string, hash: string) {
const next = hashPassword(password, salt).hash;
const a = Buffer.from(next, "hex");
const b = Buffer.from(hash, "hex");
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}