馃 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 function createSession(
23 userId: number,
24 ipAddress?: string,
25 userAgent?: string,
26): string {
27 const sessionId = crypto.randomUUID();
28 const expiresAt = Math.floor(Date.now() / 1000) + SESSION_DURATION;
29
30 db.run(
31 "INSERT INTO sessions (id, user_id, ip_address, user_agent, expires_at) VALUES (?, ?, ?, ?, ?)",
32 [sessionId, userId, ipAddress ?? null, userAgent ?? null, expiresAt],
33 );
34
35 return sessionId;
36}
37
38export function getSession(sessionId: string): Session | null {
39 const now = Math.floor(Date.now() / 1000);
40
41 const session = db
42 .query<Session, [string, number]>(
43 "SELECT id, user_id, ip_address, user_agent, created_at, expires_at FROM sessions WHERE id = ? AND expires_at > ?",
44 )
45 .get(sessionId, now);
46
47 return session ?? null;
48}
49
50export function getUserBySession(sessionId: string): User | null {
51 const session = getSession(sessionId);
52 if (!session) return null;
53
54 const user = db
55 .query<User, [number]>(
56 "SELECT id, email, name, avatar, created_at FROM users WHERE id = ?",
57 )
58 .get(session.user_id);
59
60 return user ?? null;
61}
62
63export function deleteSession(sessionId: string): void {
64 db.run("DELETE FROM sessions WHERE id = ?", [sessionId]);
65}
66
67export function cleanupExpiredSessions(): void {
68 const now = Math.floor(Date.now() / 1000);
69 db.run("DELETE FROM sessions WHERE expires_at <= ?", [now]);
70}
71
72export async function createUser(
73 email: string,
74 password: string,
75 name?: string,
76): Promise<User> {
77 const result = db.run(
78 "INSERT INTO users (email, password_hash, name) VALUES (?, ?, ?)",
79 [email, password, name ?? null],
80 );
81
82 const user = db
83 .query<User, [number]>(
84 "SELECT id, email, name, avatar, created_at FROM users WHERE id = ?",
85 )
86 .get(Number(result.lastInsertRowid));
87
88 if (!user) {
89 throw new Error("Failed to create user");
90 }
91
92 return user;
93}
94
95export async function authenticateUser(
96 email: string,
97 password: string,
98): Promise<User | null> {
99 const result = db
100 .query<
101 {
102 id: number;
103 email: string;
104 name: string | null;
105 avatar: string;
106 password_hash: string;
107 created_at: number;
108 },
109 [string]
110 >(
111 "SELECT id, email, name, avatar, password_hash, created_at FROM users WHERE email = ?",
112 )
113 .get(email);
114
115 if (!result) {
116 // Dummy comparison to prevent timing-based account enumeration
117 const dummyHash = "0".repeat(64);
118 password === dummyHash;
119 return null;
120 }
121
122 if (password !== result.password_hash) return null;
123
124 return {
125 id: result.id,
126 email: result.email,
127 name: result.name,
128 avatar: result.avatar,
129 created_at: result.created_at,
130 };
131}
132
133export function getUserSessionsForUser(userId: number): Session[] {
134 const now = Math.floor(Date.now() / 1000);
135
136 const sessions = db
137 .query<Session, [number, number]>(
138 "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",
139 )
140 .all(userId, now);
141
142 return sessions;
143}
144
145export function getSessionFromRequest(req: Request): string | null {
146 const cookie = req.headers.get("cookie");
147 if (!cookie) return null;
148
149 const match = cookie.match(/session=([^;]+)/);
150 return match?.[1] ?? null;
151}
152
153export function deleteUser(userId: number): void {
154 db.run("DELETE FROM users WHERE id = ?", [userId]);
155}
156
157export function updateUserEmail(userId: number, newEmail: string): void {
158 db.run("UPDATE users SET email = ? WHERE id = ?", [newEmail, userId]);
159}
160
161export function updateUserName(userId: number, newName: string): void {
162 db.run("UPDATE users SET name = ? WHERE id = ?", [newName, userId]);
163}
164
165export function updateUserAvatar(userId: number, avatar: string): void {
166 db.run("UPDATE users SET avatar = ? WHERE id = ?", [avatar, userId]);
167}
168
169export async function updateUserPassword(
170 userId: number,
171 newPassword: string,
172): Promise<void> {
173 db.run("UPDATE users SET password_hash = ? WHERE id = ?", [
174 newPassword,
175 userId,
176 ]);
177 db.run("DELETE FROM sessions WHERE user_id = ?", [userId]);
178}