Add admin pinning and user favorites with role management

This commit is contained in:
爱喝水的木子
2026-03-20 13:55:27 +08:00
parent e6788d0e8f
commit 8e6bd210a8
19 changed files with 629 additions and 101 deletions

View File

@@ -6,6 +6,9 @@ const encoder = new TextEncoder();
let cachedKey: CryptoKey | null = null;
let cachedSecret: string | null = null;
export const USER_ROLE_VALUES = ["user", "sponsor", "admin"] as const;
export type UserRole = (typeof USER_ROLE_VALUES)[number];
function getSecret() {
const secret = process.env.SESSION_SECRET;
if (!secret) {
@@ -15,7 +18,7 @@ function getSecret() {
}
export type SessionPayload = {
role: "admin" | "user";
role: UserRole;
iat: number;
exp?: number;
uid?: string;
@@ -33,6 +36,10 @@ export function isAdminName(name?: string | null) {
return Boolean(adminName && value && adminName === value);
}
export function resolveUserRole(value?: unknown): UserRole | null {
return USER_ROLE_VALUES.includes(value as UserRole) ? (value as UserRole) : null;
}
export function isAdminSession(session?: SessionPayload | null) {
return session?.role === "admin";
}
@@ -41,6 +48,7 @@ async function getHmacKey(secret: string) {
if (cachedKey && cachedSecret === secret) {
return cachedKey;
}
cachedSecret = secret;
cachedKey = await crypto.subtle.importKey(
"raw",
@@ -67,14 +75,17 @@ export async function signSession(payload: SessionPayload): Promise<string> {
export async function verifySession(token?: string): Promise<SessionPayload | null> {
if (!token) return null;
const secret = getSecret();
const [base, sig] = token.split(".");
if (!base || !sig) return null;
const check = await hmacSha256(base, secret);
if (check !== sig) return null;
try {
const payload = JSON.parse(Buffer.from(base, "base64url").toString());
if (payload?.role !== "admin" && payload?.role !== "user") {
if (!resolveUserRole(payload?.role)) {
return null;
}
if (typeof payload?.exp !== "number") {
@@ -83,6 +94,7 @@ export async function verifySession(token?: string): Promise<SessionPayload | nu
if (Date.now() > payload.exp) {
return null;
}
return payload;
} catch {
return null;

View File

@@ -78,6 +78,18 @@ export function canDeletePost(doc: any, session?: SessionPayload | null) {
return Boolean(doc && isAdminSession(session));
}
export function canPinPost(doc: any, session?: SessionPayload | null) {
return Boolean(doc && isAdminSession(session));
}
export function buildPinnedSort() {
return {
isPinned: -1 as const,
pinnedAt: -1 as const,
createdAt: -1 as const
};
}
export function serializePost(doc: any): Post {
return {
_id: doc._id?.toString(),
@@ -91,6 +103,10 @@ export function serializePost(doc: any): Post {
ownerId: doc.ownerId,
createdAt: doc.createdAt ?? new Date().toISOString(),
updatedAt: doc.updatedAt ?? doc.createdAt ?? new Date().toISOString(),
views: doc.views ?? 0
views: doc.views ?? 0,
isPinned: Boolean(doc.isPinned),
pinnedAt: doc.pinnedAt,
favoriteCount: typeof doc.favoriteCount === "number" ? doc.favoriteCount : undefined,
isFavorited: typeof doc.isFavorited === "boolean" ? doc.isFavorited : undefined
};
}