馃 distributed transcription service
thistle.dunkirk.sh
1import db from "../db/schema";
2
3const SESSION_DURATION = 7 * 24 * 60 * 60; // 7 days in seconds
4
5export interface User {
6 id: number;
7 email: string;
8 name: string | null;
9 avatar: string;
10 created_at: number;
11}
12
13export interface Session {
14 id: string;
15 user_id: number;
16 ip_address: string | null;
17 user_agent: string | null;
18 created_at: number;
19 expires_at: number;
20}
21
22export async function hashPassword(password: string): Promise<string> {
23 return await Bun.password.hash(password, {
24 algorithm: "argon2id",
25 memoryCost: 19456,
26 timeCost: 2,
27 });
28}
29
30export async function verifyPassword(
31 password: string,
32 hash: string,
33): Promise<boolean> {
34 return await Bun.password.verify(password, hash, "argon2id");
35}
36
37export function createSession(
38 userId: number,
39 ipAddress?: string,
40 userAgent?: string,
41): string {
42 const sessionId = crypto.randomUUID();
43 const expiresAt = Math.floor(Date.now() / 1000) + SESSION_DURATION;
44
45 db.run(
46 "INSERT INTO sessions (id, user_id, ip_address, user_agent, expires_at) VALUES (?, ?, ?, ?, ?)",
47 [sessionId, userId, ipAddress ?? null, userAgent ?? null, expiresAt],
48 );
49
50 return sessionId;
51}
52
53export function getSession(sessionId: string): Session | null {
54 const now = Math.floor(Date.now() / 1000);
55
56 const session = db
57 .query<Session, [string, number]>(
58 "SELECT id, user_id, ip_address, user_agent, created_at, expires_at FROM sessions WHERE id = ? AND expires_at > ?",
59 )
60 .get(sessionId, now);
61
62 return session ?? null;
63}
64
65export function getUserBySession(sessionId: string): User | null {
66 const session = getSession(sessionId);
67 if (!session) return null;
68
69 const user = db
70 .query<User, [number]>(
71 "SELECT id, email, name, avatar, created_at FROM users WHERE id = ?",
72 )
73 .get(session.user_id);
74
75 return user ?? null;
76}
77
78export function deleteSession(sessionId: string): void {
79 db.run("DELETE FROM sessions WHERE id = ?", [sessionId]);
80}
81
82export function cleanupExpiredSessions(): void {
83 const now = Math.floor(Date.now() / 1000);
84 db.run("DELETE FROM sessions WHERE expires_at <= ?", [now]);
85}
86
87export async function createUser(
88 email: string,
89 password: string,
90 name?: string,
91): Promise<User> {
92 const passwordHash = await hashPassword(password);
93
94 const result = db.run(
95 "INSERT INTO users (email, password_hash, name) VALUES (?, ?, ?)",
96 [email, passwordHash, name ?? null],
97 );
98
99 const user = db
100 .query<User, [number]>("SELECT id, email, name, avatar, created_at FROM users WHERE id = ?")
101 .get(Number(result.lastInsertRowid));
102
103 if (!user) {
104 throw new Error("Failed to create user");
105 }
106
107 return user;
108}
109
110export async function authenticateUser(
111 email: string,
112 password: string,
113): Promise<User | null> {
114 const result = db
115 .query<{ id: number; email: string; name: string | null; password_hash: string; created_at: number }, [string]>(
116 "SELECT id, email, name, avatar, password_hash, created_at FROM users WHERE email = ?",
117 )
118 .get(email);
119
120 if (!result) return null;
121
122 const isValid = await verifyPassword(password, result.password_hash);
123 if (!isValid) return null;
124
125 return {
126 id: result.id,
127 email: result.email,
128 name: result.name,
129 avatar: result.avatar,
130 created_at: result.created_at,
131 };
132}
133
134export function getUserSessionsForUser(userId: number): Session[] {
135 const now = Math.floor(Date.now() / 1000);
136
137 const sessions = db
138 .query<Session, [number, number]>(
139 "SELECT id, user_id, ip_address, user_agent, created_at, expires_at FROM sessions WHERE user_id = ? AND expires_at > ? ORDER BY created_at DESC",
140 )
141 .all(userId, now);
142
143 return sessions;
144}
145
146export function getSessionFromRequest(req: Request): string | null {
147 const cookie = req.headers.get("cookie");
148 if (!cookie) return null;
149
150 const match = cookie.match(/session=([^;]+)/);
151 return match?.[1] ?? null;
152}
153
154export function deleteUser(userId: number): void {
155 db.run("DELETE FROM users WHERE id = ?", [userId]);
156}
157
158export function updateUserEmail(userId: number, newEmail: string): void {
159 db.run("UPDATE users SET email = ? WHERE id = ?", [newEmail, userId]);
160}
161
162export function updateUserName(userId: number, newName: string): void {
163 db.run("UPDATE users SET name = ? WHERE id = ?", [newName, userId]);
164}
165
166export function updateUserAvatar(userId: number, avatar: string): void {
167 db.run("UPDATE users SET avatar = ? WHERE id = ?", [avatar, userId]);
168}
169
170export async function updateUserPassword(
171 userId: number,
172 newPassword: string,
173): Promise<void> {
174 const hash = await hashPassword(newPassword);
175 db.run("UPDATE users SET password_hash = ? WHERE id = ?", [hash, userId]);
176}