Files
quietthanks/src/lib/auth.ts
Gemini Agent 504f07a106 Add multiple entries per day, user management, reminders, and AI reflections
- Multiple entries per day: Home page now starts fresh, Save & New button
- Admin user management: Add/delete users, reset passwords, toggle admin
- Daily reminders: Browser notifications at configurable time
- AI reflections: Generate insights from entries using Claude API
- Remove cloud sync placeholder (already have user accounts)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 12:05:39 +00:00

166 lines
3.8 KiB
TypeScript

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;
isAdmin: boolean;
}
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,
isAdmin: schema.users.isAdmin,
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,
isAdmin: result[0].isAdmin === 1,
};
}
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,
isAdmin: false,
},
};
}
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,
isAdmin: user.isAdmin === 1,
},
sessionId,
};
}