kinaiok
Initial deployment setup for Hugging Face Spaces
5ef6e9d
import { Router } from "express";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import { db, usersTable, configTable, creditTransactionsTable } from "@workspace/db";
import { eq, count } from "drizzle-orm";
const router = Router();
const JWT_SECRET = process.env.SESSION_SECRET || "dev-secret-change-me";
const COOKIE_NAME = "sf_auth";
const COOKIE_OPTS = {
httpOnly: true,
sameSite: "lax" as const,
secure: process.env.NODE_ENV === "production",
maxAge: 30 * 24 * 60 * 60 * 1000,
path: "/",
};
function signToken(userId: number, email: string) {
return jwt.sign({ userId, email }, JWT_SECRET, { expiresIn: "30d" });
}
async function isFirstUser(): Promise<boolean> {
const [row] = await db.select({ count: count() }).from(usersTable);
return Number(row.count) === 0;
}
router.post("/signup", async (req, res) => {
const { email, password, displayName } = req.body as {
email?: string;
password?: string;
displayName?: string;
};
if (!email || !password) {
return res.status(400).json({ error: "Email and password are required" });
}
if (password.length < 6) {
return res.status(400).json({ error: "Password must be at least 6 characters" });
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return res.status(400).json({ error: "Invalid email format" });
}
const existing = await db.select({ id: usersTable.id }).from(usersTable).where(eq(usersTable.email, email.toLowerCase())).limit(1);
if (existing.length > 0) {
return res.status(409).json({ error: "Email already registered" });
}
const firstUser = await isFirstUser();
const passwordHash = await bcrypt.hash(password, 12);
const [user] = await db.insert(usersTable).values({
email: email.toLowerCase(),
passwordHash,
displayName: displayName?.trim() || null,
isAdmin: firstUser,
}).returning({
id: usersTable.id,
email: usersTable.email,
displayName: usersTable.displayName,
isAdmin: usersTable.isAdmin,
});
// Grant signup credits from config
const signupCreditRow = await db.select({ value: configTable.value }).from(configTable).where(eq(configTable.key, "signup_credits")).limit(1);
const signupCredits = Number(signupCreditRow[0]?.value) || 0;
if (signupCredits > 0) {
await db.update(usersTable).set({ credits: signupCredits }).where(eq(usersTable.id, user.id));
await db.insert(creditTransactionsTable).values({ userId: user.id, amount: signupCredits, type: "signup", description: "新用戶註冊贈點" });
}
const token = signToken(user.id, user.email);
res.cookie(COOKIE_NAME, token, COOKIE_OPTS);
res.json({ id: user.id, email: user.email, displayName: user.displayName, isAdmin: user.isAdmin, credits: signupCredits });
});
router.post("/login", async (req, res) => {
const { email, password } = req.body as { email?: string; password?: string };
if (!email || !password) {
return res.status(400).json({ error: "Email and password are required" });
}
const [user] = await db.select().from(usersTable).where(eq(usersTable.email, email.toLowerCase())).limit(1);
if (!user) {
return res.status(401).json({ error: "Invalid email or password" });
}
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) {
return res.status(401).json({ error: "Invalid email or password" });
}
const token = signToken(user.id, user.email);
res.cookie(COOKIE_NAME, token, COOKIE_OPTS);
res.json({ id: user.id, email: user.email, displayName: user.displayName, isAdmin: user.isAdmin });
});
router.post("/logout", (_req, res) => {
res.clearCookie(COOKIE_NAME, { path: "/" });
res.json({ success: true });
});
router.get("/me", async (req, res) => {
const token = req.cookies?.[COOKIE_NAME];
if (!token) return res.status(401).json({ error: "Not authenticated" });
try {
const payload = jwt.verify(token, JWT_SECRET) as { userId: number; email: string };
const [user] = await db
.select({ id: usersTable.id, email: usersTable.email, displayName: usersTable.displayName, isAdmin: usersTable.isAdmin })
.from(usersTable)
.where(eq(usersTable.id, payload.userId))
.limit(1);
if (!user) {
res.clearCookie(COOKIE_NAME, { path: "/" });
return res.status(401).json({ error: "User not found" });
}
res.json(user);
} catch {
res.clearCookie(COOKIE_NAME, { path: "/" });
res.status(401).json({ error: "Session expired" });
}
});
export function requireJwtAuth(req: any, res: any, next: any) {
const token = req.cookies?.[COOKIE_NAME];
if (!token) return res.status(401).json({ error: "Unauthorized" });
try {
const payload = jwt.verify(token, JWT_SECRET) as { userId: number; email: string };
req.jwtUserId = payload.userId;
req.jwtEmail = payload.email;
next();
} catch {
res.clearCookie(COOKIE_NAME, { path: "/" });
res.status(401).json({ error: "Session expired" });
}
}
export function optionalJwtAuth(req: any, _res: any, next: any) {
const token = req.cookies?.[COOKIE_NAME];
if (token) {
try {
const payload = jwt.verify(token, JWT_SECRET) as { userId: number; email: string };
req.jwtUserId = payload.userId;
req.jwtEmail = payload.email;
} catch {
// invalid token, continue as guest
}
}
next();
}
export default router;