Files
bunapi/src/services/user.service.ts
2025-12-17 17:09:16 -07:00

115 lines
3.0 KiB
TypeScript

// User service
import { db } from "./db";
import type { AuthUser, UserRole, RegisterRequest } from "../types/auth";
interface UserRow {
id: string;
email: string;
password_hash: string;
role: string;
created_at: string;
updated_at: string;
}
function rowToUser(row: UserRow): AuthUser & { passwordHash: string } {
return {
id: row.id,
email: row.email,
passwordHash: row.password_hash,
role: row.role as UserRole,
createdAt: new Date(row.created_at),
updatedAt: new Date(row.updated_at),
};
}
export async function hashPassword(password: string): Promise<string> {
return await Bun.password.hash(password, {
algorithm: "argon2id",
memoryCost: 65536,
timeCost: 2,
});
}
export async function verifyPassword(
password: string,
hash: string
): Promise<boolean> {
return await Bun.password.verify(password, hash);
}
export async function findUserByEmail(
email: string
): Promise<(AuthUser & { passwordHash: string }) | null> {
const row = db.query("SELECT * FROM users WHERE email = ?").get(email.toLowerCase()) as UserRow | null;
return row ? rowToUser(row) : null;
}
export async function findUserById(id: string): Promise<AuthUser | null> {
const row = db.query("SELECT * FROM users WHERE id = ?").get(id) as UserRow | null;
if (!row) return null;
const user = rowToUser(row);
const { passwordHash, ...userWithoutPassword } = user;
return userWithoutPassword;
}
export async function createUser(
data: RegisterRequest
): Promise<{ user: AuthUser; error?: never } | { user?: never; error: string }> {
const email = data.email.toLowerCase().trim();
const existing = db.query("SELECT id FROM users WHERE email = ?").get(email);
if (existing) {
return { error: "User with this email already exists" };
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return { error: "Invalid email format" };
}
if (data.password.length < 8) {
return { error: "Password must be at least 8 characters long" };
}
const passwordHash = await hashPassword(data.password);
const now = new Date().toISOString();
const userId = crypto.randomUUID();
db.prepare(`
INSERT INTO users (id, email, password_hash, role, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
`).run(userId, email, passwordHash, "user", now, now);
return {
user: {
id: userId,
email,
role: "user" as UserRole,
createdAt: new Date(now),
updatedAt: new Date(now),
},
};
}
export async function authenticateUser(
email: string,
password: string
): Promise<{ user: AuthUser; error?: never } | { user?: never; error: string }> {
const user = await findUserByEmail(email);
if (!user) {
return { error: "Invalid email or password" };
}
const isValidPassword = await verifyPassword(password, user.passwordHash);
if (!isValidPassword) {
return { error: "Invalid email or password" };
}
const { passwordHash, ...userWithoutPassword } = user;
return { user: userWithoutPassword };
}