mirror of
https://github.com/Tony0410/quietthanks.git
synced 2026-05-25 05:41:38 +08:00
Add user authentication with login/register
- Add users and sessions tables to database schema - Add bcryptjs for password hashing - Create auth API routes (login, register, logout, me) - Add AuthProvider context for client-side auth state - Update all API routes to require authentication and filter by userId - Create login and register pages - Add AppShell component for authenticated layout - Update all pages to use AppShell and show user info - Each user now has their own private entries and tags Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
160
src/lib/auth.ts
Normal file
160
src/lib/auth.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { db, schema } from "./db";
|
||||
import { eq, and, gt } from "drizzle-orm";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
const SESSION_COOKIE = "qt_session";
|
||||
const SESSION_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
||||
|
||||
export interface AuthUser {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return bcrypt.hash(password, 10);
|
||||
}
|
||||
|
||||
export async function verifyPassword(
|
||||
password: string,
|
||||
hash: string
|
||||
): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
export async function createSession(userId: string): Promise<string> {
|
||||
const sessionId = uuidv4();
|
||||
const now = Date.now();
|
||||
|
||||
await db.insert(schema.sessions).values({
|
||||
id: sessionId,
|
||||
userId,
|
||||
expiresAt: now + SESSION_DURATION_MS,
|
||||
createdAt: now,
|
||||
});
|
||||
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
export async function setSessionCookie(sessionId: string) {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set(SESSION_COOKIE, sessionId, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
maxAge: SESSION_DURATION_MS / 1000,
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
|
||||
export async function clearSessionCookie() {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete(SESSION_COOKIE);
|
||||
}
|
||||
|
||||
export async function getSession(): Promise<AuthUser | null> {
|
||||
const cookieStore = await cookies();
|
||||
const sessionId = cookieStore.get(SESSION_COOKIE)?.value;
|
||||
|
||||
if (!sessionId) return null;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const result = await db
|
||||
.select({
|
||||
sessionId: schema.sessions.id,
|
||||
userId: schema.users.id,
|
||||
email: schema.users.email,
|
||||
name: schema.users.name,
|
||||
expiresAt: schema.sessions.expiresAt,
|
||||
})
|
||||
.from(schema.sessions)
|
||||
.innerJoin(schema.users, eq(schema.sessions.userId, schema.users.id))
|
||||
.where(
|
||||
and(eq(schema.sessions.id, sessionId), gt(schema.sessions.expiresAt, now))
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (result.length === 0) return null;
|
||||
|
||||
return {
|
||||
id: result[0].userId,
|
||||
email: result[0].email,
|
||||
name: result[0].name,
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteSession(sessionId: string) {
|
||||
await db.delete(schema.sessions).where(eq(schema.sessions.id, sessionId));
|
||||
}
|
||||
|
||||
export async function registerUser(
|
||||
email: string,
|
||||
password: string,
|
||||
name?: string
|
||||
): Promise<{ user?: AuthUser; error?: string }> {
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(schema.users)
|
||||
.where(eq(schema.users.email, email.toLowerCase()))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
return { error: "Email already registered" };
|
||||
}
|
||||
|
||||
const userId = uuidv4();
|
||||
const passwordHash = await hashPassword(password);
|
||||
const now = Date.now();
|
||||
|
||||
await db.insert(schema.users).values({
|
||||
id: userId,
|
||||
email: email.toLowerCase(),
|
||||
passwordHash,
|
||||
name: name || null,
|
||||
createdAt: now,
|
||||
});
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: userId,
|
||||
email: email.toLowerCase(),
|
||||
name: name || null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function loginUser(
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<{ user?: AuthUser; sessionId?: string; error?: string }> {
|
||||
const users = await db
|
||||
.select()
|
||||
.from(schema.users)
|
||||
.where(eq(schema.users.email, email.toLowerCase()))
|
||||
.limit(1);
|
||||
|
||||
if (users.length === 0) {
|
||||
return { error: "Invalid email or password" };
|
||||
}
|
||||
|
||||
const user = users[0];
|
||||
const valid = await verifyPassword(password, user.passwordHash);
|
||||
|
||||
if (!valid) {
|
||||
return { error: "Invalid email or password" };
|
||||
}
|
||||
|
||||
const sessionId = await createSession(user.id);
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
},
|
||||
sessionId,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user