143 lines
3.0 KiB
TypeScript
143 lines
3.0 KiB
TypeScript
import "server-only";
|
|
|
|
import { randomUUID } from "node:crypto";
|
|
import bcrypt from "bcryptjs";
|
|
import { cache } from "react";
|
|
import { cookies } from "next/headers";
|
|
import { redirect } from "next/navigation";
|
|
import {
|
|
createSessionRecord,
|
|
deleteSessionRecord,
|
|
getSessionUser,
|
|
getUserByUsername,
|
|
normalizeUsername,
|
|
} from "@/lib/db";
|
|
import { appConfig } from "@/lib/config";
|
|
import type { PublicUser, SessionRecord, UserRecord } from "@/lib/types";
|
|
|
|
export const SESSION_COOKIE_NAME = "image_prompt_session";
|
|
|
|
export function toPublicUser(user: UserRecord | null): PublicUser | null {
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: user.id,
|
|
username: user.username,
|
|
role: user.role,
|
|
isActive: user.isActive,
|
|
monthlyQuota: user.monthlyQuota,
|
|
createdAt: user.createdAt,
|
|
updatedAt: user.updatedAt,
|
|
};
|
|
}
|
|
|
|
export function hashPassword(password: string): string {
|
|
return bcrypt.hashSync(String(password), 12);
|
|
}
|
|
|
|
export function verifyPassword(
|
|
password: string,
|
|
passwordHash: string,
|
|
): boolean {
|
|
return bcrypt.compareSync(String(password), passwordHash);
|
|
}
|
|
|
|
export function authenticateUser(
|
|
username: string,
|
|
password: string,
|
|
): UserRecord | null {
|
|
const user = getUserByUsername(normalizeUsername(username));
|
|
|
|
if (!user || !user.isActive) {
|
|
return null;
|
|
}
|
|
|
|
if (!verifyPassword(password, user.passwordHash)) {
|
|
return null;
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
export function createSessionForUser(userId: string): SessionRecord {
|
|
const sessionId = randomUUID();
|
|
const expiresAt = new Date(
|
|
Date.now() + appConfig.sessionDays * 24 * 60 * 60 * 1000,
|
|
).toISOString();
|
|
|
|
createSessionRecord({
|
|
sessionId,
|
|
userId,
|
|
expiresAt,
|
|
});
|
|
|
|
return {
|
|
sessionId,
|
|
userId,
|
|
expiresAt,
|
|
};
|
|
}
|
|
|
|
export function getSessionCookieOptions(expiresAt: string) {
|
|
return {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === "production",
|
|
sameSite: "lax" as const,
|
|
path: "/",
|
|
expires: new Date(expiresAt),
|
|
};
|
|
}
|
|
|
|
export function clearSessionById(sessionId: string | undefined): void {
|
|
if (!sessionId) {
|
|
return;
|
|
}
|
|
|
|
deleteSessionRecord(sessionId);
|
|
}
|
|
|
|
export function getUserFromSessionToken(
|
|
sessionToken: string | undefined,
|
|
): UserRecord | null {
|
|
if (!sessionToken) {
|
|
return null;
|
|
}
|
|
|
|
return getSessionUser(sessionToken);
|
|
}
|
|
|
|
export async function getCurrentUser(): Promise<UserRecord | null> {
|
|
const cookieStore = await cookies();
|
|
const sessionToken = cookieStore.get(SESSION_COOKIE_NAME)?.value;
|
|
|
|
return getUserFromSessionToken(sessionToken);
|
|
}
|
|
|
|
export const getCachedCurrentUser = cache(getCurrentUser);
|
|
|
|
export async function requireCurrentUser(): Promise<UserRecord> {
|
|
const user = await getCachedCurrentUser();
|
|
|
|
if (!user) {
|
|
redirect("/login");
|
|
}
|
|
|
|
if (!user.isActive) {
|
|
redirect("/login?reason=inactive");
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
export async function requireAdminUser(): Promise<UserRecord> {
|
|
const user = await requireCurrentUser();
|
|
|
|
if (user.role !== "admin") {
|
|
redirect("/studio");
|
|
}
|
|
|
|
return user;
|
|
}
|