···11-import { LitElement, html, css } from "lit";
11+import { css, html, LitElement } from "lit";
22import { customElement, property, state } from "lit/decorators.js";
3344interface Session {
···327327328328 this.user = await res.json();
329329 } catch (err) {
330330- this.error = err instanceof Error ? err.message : "Failed to load user details";
330330+ this.error =
331331+ err instanceof Error ? err.message : "Failed to load user details";
331332 this.user = null;
332333 } finally {
333334 this.loading = false;
···335336 }
336337337338 private close() {
338338- this.dispatchEvent(new CustomEvent("close", { bubbles: true, composed: true }));
339339+ this.dispatchEvent(
340340+ new CustomEvent("close", { bubbles: true, composed: true }),
341341+ );
339342 }
340343341344 private formatTimestamp(timestamp: number) {
···365368 return;
366369 }
367370368368- const submitBtn = form.querySelector('button[type="submit"]') as HTMLButtonElement;
371371+ const submitBtn = form.querySelector(
372372+ 'button[type="submit"]',
373373+ ) as HTMLButtonElement;
369374 submitBtn.disabled = true;
370375 submitBtn.textContent = "Updating...";
371376···382387383388 alert("Name updated successfully");
384389 await this.loadUserDetails();
385385- this.dispatchEvent(new CustomEvent("user-updated", { bubbles: true, composed: true }));
390390+ this.dispatchEvent(
391391+ new CustomEvent("user-updated", { bubbles: true, composed: true }),
392392+ );
386393 } catch {
387394 alert("Failed to update name");
388395 } finally {
···402409 return;
403410 }
404411405405- const submitBtn = form.querySelector('button[type="submit"]') as HTMLButtonElement;
412412+ const submitBtn = form.querySelector(
413413+ 'button[type="submit"]',
414414+ ) as HTMLButtonElement;
406415 submitBtn.disabled = true;
407416 submitBtn.textContent = "Updating...";
408417···420429421430 alert("Email updated successfully");
422431 await this.loadUserDetails();
423423- this.dispatchEvent(new CustomEvent("user-updated", { bubbles: true, composed: true }));
432432+ this.dispatchEvent(
433433+ new CustomEvent("user-updated", { bubbles: true, composed: true }),
434434+ );
424435 } catch (error) {
425436 alert(error instanceof Error ? error.message : "Failed to update email");
426437 } finally {
···440451 return;
441452 }
442453443443- if (!confirm("Are you sure you want to change this user's password? This will log them out of all devices.")) {
454454+ if (
455455+ !confirm(
456456+ "Are you sure you want to change this user's password? This will log them out of all devices.",
457457+ )
458458+ ) {
444459 return;
445460 }
446461447447- const submitBtn = form.querySelector('button[type="submit"]') as HTMLButtonElement;
462462+ const submitBtn = form.querySelector(
463463+ 'button[type="submit"]',
464464+ ) as HTMLButtonElement;
448465 submitBtn.disabled = true;
449466 submitBtn.textContent = "Updating...";
450467···459476 throw new Error("Failed to update password");
460477 }
461478462462- alert("Password updated successfully. User has been logged out of all devices.");
479479+ alert(
480480+ "Password updated successfully. User has been logged out of all devices.",
481481+ );
463482 input.value = "";
464483 await this.loadUserDetails();
465484 } catch {
···471490 }
472491473492 private async handleLogoutAll() {
474474- if (!confirm("Are you sure you want to logout this user from ALL devices? This will terminate all active sessions.")) {
493493+ if (
494494+ !confirm(
495495+ "Are you sure you want to logout this user from ALL devices? This will terminate all active sessions.",
496496+ )
497497+ ) {
475498 return;
476499 }
477500···492515 }
493516494517 private async handleRevokeSession(sessionId: string) {
495495- if (!confirm("Revoke this session? The user will be logged out of this device.")) {
518518+ if (
519519+ !confirm(
520520+ "Revoke this session? The user will be logged out of this device.",
521521+ )
522522+ ) {
496523 return;
497524 }
498525499526 try {
500500- const res = await fetch(`/api/admin/users/${this.userId}/sessions/${sessionId}`, {
501501- method: "DELETE",
502502- });
527527+ const res = await fetch(
528528+ `/api/admin/users/${this.userId}/sessions/${sessionId}`,
529529+ {
530530+ method: "DELETE",
531531+ },
532532+ );
503533504534 if (!res.ok) {
505535 throw new Error("Failed to revoke session");
···512542 }
513543514544 private async handleRevokePasskey(passkeyId: string) {
515515- if (!confirm("Are you sure you want to revoke this passkey? The user will no longer be able to use it to sign in.")) {
545545+ if (
546546+ !confirm(
547547+ "Are you sure you want to revoke this passkey? The user will no longer be able to use it to sign in.",
548548+ )
549549+ ) {
516550 return;
517551 }
518552519553 try {
520520- const res = await fetch(`/api/admin/users/${this.userId}/passkeys/${passkeyId}`, {
521521- method: "DELETE",
522522- });
554554+ const res = await fetch(
555555+ `/api/admin/users/${this.userId}/passkeys/${passkeyId}`,
556556+ {
557557+ method: "DELETE",
558558+ },
559559+ );
523560524561 if (!res.ok) {
525562 throw new Error("Failed to revoke passkey");
···146146 CREATE INDEX IF NOT EXISTS idx_users_last_login ON users(last_login);
147147 `,
148148 },
149149+ {
150150+ version: 9,
151151+ name: "Add class_name to transcriptions",
152152+ sql: `
153153+ ALTER TABLE transcriptions ADD COLUMN class_name TEXT;
154154+ CREATE INDEX IF NOT EXISTS idx_transcriptions_class_name ON transcriptions(class_name);
155155+ `,
156156+ },
149157];
150158151159function getCurrentVersion(): number {
···11-import { afterEach, beforeEach, expect, test } from "bun:test";
21import { Database } from "bun:sqlite";
22+import { afterEach, beforeEach, expect, test } from "bun:test";
3344let testDb: Database;
55···183183 .all(userId);
184184 expect(sessions.length).toBe(0);
185185});
186186-
+6-6
src/lib/auth.test.ts
···11-import { test, expect } from "bun:test";
11+import { expect, test } from "bun:test";
22+import db from "../db/schema";
23import {
34 createSession,
44- getSession,
55 deleteSession,
66+ getSession,
67 getSessionFromRequest,
78} from "./auth";
88-import db from "../db/schema";
991010test("createSession generates UUID and stores in database", () => {
1111 const userId = 1;
···125125 }
126126127127 // Verify sessions table still exists
128128- const result = db
129129- .query("SELECT COUNT(*) as count FROM sessions")
130130- .get() as { count: number };
128128+ const result = db.query("SELECT COUNT(*) as count FROM sessions").get() as {
129129+ count: number;
130130+ };
131131 expect(typeof result.count).toBe("number");
132132});
+9-13
src/lib/auth.ts
···231231 last_login: number | null;
232232 },
233233 []
234234- >("SELECT id, email, name, avatar, created_at, role, last_login FROM users ORDER BY created_at DESC")
234234+ >(
235235+ "SELECT id, email, name, avatar, created_at, role, last_login FROM users ORDER BY created_at DESC",
236236+ )
235237 .all();
236238}
237239···329331 .all(userId, now);
330332}
331333332332-export function deleteSessionById(
333333- sessionId: string,
334334- userId: number,
335335-): boolean {
336336- const result = db.run(
337337- "DELETE FROM sessions WHERE id = ? AND user_id = ?",
338338- [sessionId, userId],
339339- );
334334+export function deleteSessionById(sessionId: string, userId: number): boolean {
335335+ const result = db.run("DELETE FROM sessions WHERE id = ? AND user_id = ?", [
336336+ sessionId,
337337+ userId,
338338+ ]);
340339 return result.changes > 0;
341340}
342341···344343 db.run("DELETE FROM sessions WHERE user_id = ?", [userId]);
345344}
346345347347-export function updateUserEmailAddress(
348348- userId: number,
349349- newEmail: string,
350350-): void {
346346+export function updateUserEmailAddress(userId: number, newEmail: string): void {
351347 db.run("UPDATE users SET email = ? WHERE id = ?", [newEmail, userId]);
352348}
353349
+1-1
src/lib/client-auth.test.ts
···11-import { test, expect } from "bun:test";
11+import { expect, test } from "bun:test";
22import { hashPasswordClient } from "./client-auth";
3344test("hashPasswordClient produces consistent output", async () => {
···11import {
22 generateAuthenticationOptions,
33 generateRegistrationOptions,
44+ type VerifiedAuthenticationResponse,
55+ type VerifiedRegistrationResponse,
46 verifyAuthenticationResponse,
57 verifyRegistrationResponse,
66- type VerifiedAuthenticationResponse,
77- type VerifiedRegistrationResponse,
88} from "@simplewebauthn/server";
99import type {
1010 AuthenticationResponseJSON,
···163163 // credential.publicKey is a Uint8Array that needs conversion
164164 const passkeyId = crypto.randomUUID();
165165 const credentialIdBase64 = credential.id;
166166- const publicKeyBase64 = Buffer.from(credential.publicKey).toString("base64url");
166166+ const publicKeyBase64 = Buffer.from(credential.publicKey).toString(
167167+ "base64url",
168168+ );
167169 const transports = response.response.transports?.join(",") || null;
168170169171 db.run(
···224226225227 const options = await generateAuthenticationOptions({
226228 rpID,
227227- allowCredentials: allowCredentials.length > 0 ? allowCredentials : undefined,
229229+ allowCredentials:
230230+ allowCredentials.length > 0 ? allowCredentials : undefined,
228231 userVerification: "preferred",
229232 });
230233···299302300303 // Update last used timestamp and counter for passkey
301304 const now = Math.floor(Date.now() / 1000);
302302- db.run(
303303- "UPDATE passkeys SET last_used_at = ?, counter = ? WHERE id = ?",
304304- [now, verification.authenticationInfo.newCounter, passkey.id],
305305- );
305305+ db.run("UPDATE passkeys SET last_used_at = ?, counter = ? WHERE id = ?", [
306306+ now,
307307+ verification.authenticationInfo.newCounter,
308308+ passkey.id,
309309+ ]);
306310307311 // Update user's last_login
308308- db.run("UPDATE users SET last_login = ? WHERE id = ?", [now, passkey.user_id]);
312312+ db.run("UPDATE users SET last_login = ? WHERE id = ?", [
313313+ now,
314314+ passkey.user_id,
315315+ ]);
309316310317 // Get user
311318 const user = db
+1-1
src/lib/rate-limit.test.ts
···11import { expect, test } from "bun:test";
22-import { checkRateLimit, cleanupOldAttempts } from "./rate-limit";
32import db from "../db/schema";
33+import { checkRateLimit, cleanupOldAttempts } from "./rate-limit";
4455// Clean up before tests
66db.run("DELETE FROM rate_limit_attempts");