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:
Gemini Agent
2026-01-24 06:18:41 +00:00
parent 5555c1e6b5
commit 1455b0acd1
27 changed files with 1039 additions and 75 deletions

160
src/lib/auth.ts Normal file
View 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,
};
}