115 lines
3.0 KiB
TypeScript
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 };
|
|
}
|