馃 distributed transcription service thistle.dunkirk.sh
1import { 2authenticateUser, 3cleanupExpiredSessions, 4createSession, 5createUser, 6deleteSession, 7deleteUser, 8getSession, 9getSessionFromRequest, 10getUserBySession, 11getUserSessionsForUser, 12updateUserAvatar, 13updateUserEmail, 14updateUserName, 15 updateUserPassword, 16} from "./lib/auth"; 17import indexHTML from "./pages/index.html"; 18import settingsHTML from "./pages/settings.html"; 19 20// Clean up expired sessions every hour 21setInterval(cleanupExpiredSessions, 60 * 60 * 1000); 22 23const server = Bun.serve({ 24 port: 3000, 25 routes: { 26 "/": indexHTML, 27 "/settings": settingsHTML, 28 "/api/auth/register": { 29 POST: async (req) => { 30 try { 31 const body = await req.json(); 32 const { email, password, name } = body; 33 34 if (!email || !password) { 35 return Response.json( 36 { error: "Email and password required" }, 37 { status: 400 }, 38 ); 39 } 40 41 if (password.length < 8) { 42 return Response.json( 43 { error: "Password must be at least 8 characters" }, 44 { status: 400 }, 45 ); 46 } 47 48 const user = await createUser(email, password, name); 49 const ipAddress = 50 req.headers.get("x-forwarded-for") ?? 51 req.headers.get("x-real-ip") ?? 52 "unknown"; 53 const userAgent = req.headers.get("user-agent") ?? "unknown"; 54 const sessionId = createSession(user.id, ipAddress, userAgent); 55 56 return Response.json( 57 { user: { id: user.id, email: user.email } }, 58 { 59 headers: { 60 "Set-Cookie": `session=${sessionId}; HttpOnly; Path=/; Max-Age=${7 * 24 * 60 * 60}; SameSite=Lax`, 61 }, 62 }, 63 ); 64 } catch (err: unknown) { 65 const error = err as { message?: string }; 66 if (error.message?.includes("UNIQUE constraint failed")) { 67 return Response.json( 68 { error: "Email already registered" }, 69 { status: 400 }, 70 ); 71 } 72 return Response.json( 73 { error: "Registration failed" }, 74 { status: 500 }, 75 ); 76 } 77 }, 78 }, 79 "/api/auth/login": { 80 POST: async (req) => { 81 try { 82 const body = await req.json(); 83 const { email, password } = body; 84 85 if (!email || !password) { 86 return Response.json( 87 { error: "Email and password required" }, 88 { status: 400 }, 89 ); 90 } 91 92 const user = await authenticateUser(email, password); 93 94 if (!user) { 95 return Response.json( 96 { error: "Invalid email or password" }, 97 { status: 401 }, 98 ); 99 } 100 101 const ipAddress = 102 req.headers.get("x-forwarded-for") ?? 103 req.headers.get("x-real-ip") ?? 104 "unknown"; 105 const userAgent = req.headers.get("user-agent") ?? "unknown"; 106 const sessionId = createSession(user.id, ipAddress, userAgent); 107 108 return Response.json( 109 { user: { id: user.id, email: user.email } }, 110 { 111 headers: { 112 "Set-Cookie": `session=${sessionId}; HttpOnly; Path=/; Max-Age=${7 * 24 * 60 * 60}; SameSite=Lax`, 113 }, 114 }, 115 ); 116 } catch (_) { 117 return Response.json({ error: "Login failed" }, { status: 500 }); 118 } 119 }, 120 }, 121 "/api/auth/logout": { 122 POST: (req) => { 123 const sessionId = getSessionFromRequest(req); 124 if (sessionId) { 125 deleteSession(sessionId); 126 } 127 128 return Response.json( 129 { success: true }, 130 { 131 headers: { 132 "Set-Cookie": 133 "session=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax", 134 }, 135 }, 136 ); 137 }, 138 }, 139 "/api/auth/me": { 140 GET: (req) => { 141 const sessionId = getSessionFromRequest(req); 142 if (!sessionId) { 143 return Response.json({ error: "Not authenticated" }, { status: 401 }); 144 } 145 146 const user = getUserBySession(sessionId); 147 if (!user) { 148 return Response.json({ error: "Invalid session" }, { status: 401 }); 149 } 150 151 return Response.json({ 152 email: user.email, 153 name: user.name, 154 avatar: user.avatar, 155 created_at: user.created_at, 156 }); 157 }, 158 }, 159 "/api/sessions": { 160 GET: (req) => { 161 const sessionId = getSessionFromRequest(req); 162 if (!sessionId) { 163 return Response.json({ error: "Not authenticated" }, { status: 401 }); 164 } 165 166 const user = getUserBySession(sessionId); 167 if (!user) { 168 return Response.json({ error: "Invalid session" }, { status: 401 }); 169 } 170 171 const sessions = getUserSessionsForUser(user.id); 172 return Response.json({ 173 sessions: sessions.map((s) => ({ 174 id: s.id, 175 ip_address: s.ip_address, 176 user_agent: s.user_agent, 177 created_at: s.created_at, 178 expires_at: s.expires_at, 179 is_current: s.id === sessionId, 180 })), 181 }); 182 }, 183 DELETE: async (req) => { 184 const currentSessionId = getSessionFromRequest(req); 185 if (!currentSessionId) { 186 return Response.json({ error: "Not authenticated" }, { status: 401 }); 187 } 188 189 const user = getUserBySession(currentSessionId); 190 if (!user) { 191 return Response.json({ error: "Invalid session" }, { status: 401 }); 192 } 193 194 const body = await req.json(); 195 const targetSessionId = body.sessionId; 196 197 if (!targetSessionId) { 198 return Response.json( 199 { error: "Session ID required" }, 200 { status: 400 }, 201 ); 202 } 203 204 // Verify the session belongs to the user 205 const targetSession = getSession(targetSessionId); 206 if (!targetSession || targetSession.user_id !== user.id) { 207 return Response.json({ error: "Session not found" }, { status: 404 }); 208 } 209 210 deleteSession(targetSessionId); 211 212 return Response.json({ success: true }); 213 }, 214 }, 215 "/api/auth/delete-account": { 216 DELETE: (req) => { 217 const sessionId = getSessionFromRequest(req); 218 if (!sessionId) { 219 return Response.json({ error: "Not authenticated" }, { status: 401 }); 220 } 221 222 const user = getUserBySession(sessionId); 223 if (!user) { 224 return Response.json({ error: "Invalid session" }, { status: 401 }); 225 } 226 227 deleteUser(user.id); 228 229 return Response.json( 230 { success: true }, 231 { 232 headers: { 233 "Set-Cookie": 234 "session=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax", 235 }, 236 }, 237 ); 238 }, 239 }, 240 "/api/user/email": { 241 PUT: async (req) => { 242 const sessionId = getSessionFromRequest(req); 243 if (!sessionId) { 244 return Response.json({ error: "Not authenticated" }, { status: 401 }); 245 } 246 247 const user = getUserBySession(sessionId); 248 if (!user) { 249 return Response.json({ error: "Invalid session" }, { status: 401 }); 250 } 251 252 const body = await req.json(); 253 const { email } = body; 254 255 if (!email) { 256 return Response.json({ error: "Email required" }, { status: 400 }); 257 } 258 259 try { 260 updateUserEmail(user.id, email); 261 return Response.json({ success: true }); 262 } catch (err: unknown) { 263 const error = err as { message?: string }; 264 if (error.message?.includes("UNIQUE constraint failed")) { 265 return Response.json( 266 { error: "Email already in use" }, 267 { status: 400 }, 268 ); 269 } 270 return Response.json( 271 { error: "Failed to update email" }, 272 { status: 500 }, 273 ); 274 } 275 }, 276 }, 277 "/api/user/password": { 278 PUT: async (req) => { 279 const sessionId = getSessionFromRequest(req); 280 if (!sessionId) { 281 return Response.json({ error: "Not authenticated" }, { status: 401 }); 282 } 283 284 const user = getUserBySession(sessionId); 285 if (!user) { 286 return Response.json({ error: "Invalid session" }, { status: 401 }); 287 } 288 289 const body = await req.json(); 290 const { password } = body; 291 292 if (!password) { 293 return Response.json({ error: "Password required" }, { status: 400 }); 294 } 295 296 if (password.length < 8) { 297 return Response.json( 298 { error: "Password must be at least 8 characters" }, 299 { status: 400 }, 300 ); 301 } 302 303 try { 304 await updateUserPassword(user.id, password); 305 return Response.json({ success: true }); 306 } catch { 307 return Response.json( 308 { error: "Failed to update password" }, 309 { status: 500 }, 310 ); 311 } 312 }, 313 }, 314 "/api/user/name": { 315 PUT: async (req) => { 316 const sessionId = getSessionFromRequest(req); 317 if (!sessionId) { 318 return Response.json({ error: "Not authenticated" }, { status: 401 }); 319 } 320 321 const user = getUserBySession(sessionId); 322 if (!user) { 323 return Response.json({ error: "Invalid session" }, { status: 401 }); 324 } 325 326 const body = await req.json(); 327 const { name } = body; 328 329 if (!name) { 330 return Response.json({ error: "Name required" }, { status: 400 }); 331 } 332 333 try { 334 updateUserName(user.id, name); 335 return Response.json({ success: true }); 336 } catch { 337 return Response.json( 338 { error: "Failed to update name" }, 339 { status: 500 }, 340 ); 341 } 342 }, 343 }, 344 "/api/user/avatar": { 345 PUT: async (req) => { 346 const sessionId = getSessionFromRequest(req); 347 if (!sessionId) { 348 return Response.json({ error: "Not authenticated" }, { status: 401 }); 349 } 350 351 const user = getUserBySession(sessionId); 352 if (!user) { 353 return Response.json({ error: "Invalid session" }, { status: 401 }); 354 } 355 356 const body = await req.json(); 357 const { avatar } = body; 358 359 if (!avatar) { 360 return Response.json({ error: "Avatar required" }, { status: 400 }); 361 } 362 363 try { 364 updateUserAvatar(user.id, avatar); 365 return Response.json({ success: true }); 366 } catch { 367 return Response.json( 368 { error: "Failed to update avatar" }, 369 { status: 500 }, 370 ); 371 } 372 }, 373 }, 374 }, 375 development: { 376 hmr: true, 377 console: true, 378 }, 379}); 380 381console.log(`馃 Thistle running at http://localhost:${server.port}`);