This commit is contained in:
2026-05-20 15:40:17 +07:00
parent 230eb9010c
commit dd3fd889a3
48 changed files with 3374 additions and 737 deletions

164
src/lib/db/auth.ts Normal file
View File

@@ -0,0 +1,164 @@
import "server-only";
import { randomBytes } from "node:crypto";
import bcrypt from "bcryptjs";
import { cookies } from "next/headers";
import { and, eq, gt } from "drizzle-orm";
import { db } from "./client";
import { sessions, users } from "./schema";
import type { User } from "@/lib/types";
export const SESSION_COOKIE = "places_session";
const SESSION_DAYS = 30;
function newToken(): string {
return randomBytes(32).toString("base64url");
}
function deriveInitials(name: string): string {
const parts = name.trim().split(/\s+/);
if (parts.length >= 2) {
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
return parts[0].slice(0, 2).toUpperCase();
}
export type AuthResult =
| { ok: true; user: User }
| { ok: false; error: string };
export async function registerUser(
email: string,
password: string,
name: string,
): Promise<AuthResult> {
const cleanEmail = email.trim().toLowerCase();
const cleanName = name.trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(cleanEmail)) {
return { ok: false, error: "Email không hợp lệ" };
}
if (password.length < 8) {
return { ok: false, error: "Mật khẩu cần ít nhất 8 ký tự" };
}
if (cleanName.length < 1) {
return { ok: false, error: "Vui lòng nhập tên" };
}
const existing = await db
.select({ id: users.id })
.from(users)
.where(eq(users.email, cleanEmail))
.limit(1);
if (existing.length) {
return { ok: false, error: "Email đã được đăng ký" };
}
const hash = await bcrypt.hash(password, 10);
const initials = deriveInitials(cleanName);
const [inserted] = await db
.insert(users)
.values({
email: cleanEmail,
passwordHash: hash,
name: cleanName,
initials,
})
.returning({
id: users.id,
email: users.email,
name: users.name,
initials: users.initials,
avatarUrl: users.avatarUrl,
color: users.color,
});
await createSessionCookie(inserted.id);
return {
ok: true,
user: {
id: inserted.id,
email: inserted.email,
name: inserted.name,
initials: inserted.initials,
avatar_url: inserted.avatarUrl,
color: inserted.color ?? undefined,
},
};
}
export async function loginUser(
email: string,
password: string,
): Promise<AuthResult> {
const cleanEmail = email.trim().toLowerCase();
const [row] = await db
.select()
.from(users)
.where(eq(users.email, cleanEmail))
.limit(1);
if (!row) return { ok: false, error: "Email hoặc mật khẩu không đúng" };
const ok = await bcrypt.compare(password, row.passwordHash);
if (!ok) return { ok: false, error: "Email hoặc mật khẩu không đúng" };
await createSessionCookie(row.id);
return {
ok: true,
user: {
id: row.id,
email: row.email,
name: row.name,
initials: row.initials,
avatar_url: row.avatarUrl,
color: row.color ?? undefined,
},
};
}
async function createSessionCookie(userId: number): Promise<void> {
const token = newToken();
const expiresAt = new Date(Date.now() + SESSION_DAYS * 24 * 60 * 60 * 1000);
await db.insert(sessions).values({ id: token, userId, expiresAt });
const c = await cookies();
c.set(SESSION_COOKIE, token, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
expires: expiresAt,
});
}
export async function getCurrentUserId(): Promise<number | null> {
const c = await cookies();
const token = c.get(SESSION_COOKIE)?.value;
if (!token) return null;
const [row] = await db
.select({ userId: sessions.userId, expiresAt: sessions.expiresAt })
.from(sessions)
.where(and(eq(sessions.id, token), gt(sessions.expiresAt, new Date())))
.limit(1);
if (!row) {
// Clean up an expired/invalid token if present.
await db.delete(sessions).where(eq(sessions.id, token));
return null;
}
return row.userId;
}
export async function logout(): Promise<void> {
const c = await cookies();
const token = c.get(SESSION_COOKIE)?.value;
if (token) {
await db.delete(sessions).where(eq(sessions.id, token));
}
c.delete(SESSION_COOKIE);
}
export async function requireUserId(): Promise<number> {
const id = await getCurrentUserId();
if (!id) throw new Error("not authenticated");
return id;
}