🪻 distributed transcription service thistle.dunkirk.sh
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: fix all biome linting issues

- Migrate biome config to version 2.3.2
- Replace explicit any types with proper interfaces
- Remove non-null assertions with proper null checks
- Apply all auto-formatting fixes

💖 Generated with Crush

Co-Authored-By: Crush <crush@charm.land>

+683 -367
+1 -1
biome.json
··· 1 1 { 2 - "$schema": "https://biomejs.dev/schemas/2.2.7/schema.json", 2 + "$schema": "https://biomejs.dev/schemas/2.3.2/schema.json", 3 3 "vcs": { 4 4 "enabled": false, 5 5 "clientKind": "git",
+11 -17
crush.json
··· 1 1 { 2 - "$schema": "https://charm.land/crush.json", 3 - "lsp": { 4 - "biome": { 5 - "command": "bunx", 6 - "args": [ 7 - "biome", 8 - "lsp-proxy" 9 - ] 10 - }, 11 - "typescript": { 12 - "command": "bunx", 13 - "args": [ 14 - "typescript-language-server", 15 - "--stdio" 16 - ] 17 - } 18 - } 2 + "$schema": "https://charm.land/crush.json", 3 + "lsp": { 4 + "biome": { 5 + "command": "bunx", 6 + "args": ["biome", "lsp-proxy"] 7 + }, 8 + "typescript": { 9 + "command": "bunx", 10 + "args": ["typescript-language-server", "--stdio"] 11 + } 12 + } 19 13 }
+26 -26
package.json
··· 1 1 { 2 - "name": "thistle", 3 - "module": "src/index.ts", 4 - "type": "module", 5 - "private": true, 6 - "scripts": { 7 - "dev": "bun run src/index.ts --hot", 8 - "clean": "rm -rf transcripts uploads thistle.db", 9 - "test": "bun test", 10 - "test:integration": "bun test src/index.test.ts" 11 - }, 12 - "devDependencies": { 13 - "@biomejs/biome": "^2.3.2", 14 - "@simplewebauthn/types": "^12.0.0", 15 - "@types/bun": "latest" 16 - }, 17 - "peerDependencies": { 18 - "typescript": "^5" 19 - }, 20 - "dependencies": { 21 - "@simplewebauthn/browser": "^13.2.2", 22 - "@simplewebauthn/server": "^13.2.2", 23 - "eventsource-client": "^1.2.0", 24 - "lit": "^3.3.1", 25 - "nanoid": "^5.1.6", 26 - "ua-parser-js": "^2.0.6" 27 - } 2 + "name": "thistle", 3 + "module": "src/index.ts", 4 + "type": "module", 5 + "private": true, 6 + "scripts": { 7 + "dev": "bun run src/index.ts --hot", 8 + "clean": "rm -rf transcripts uploads thistle.db", 9 + "test": "bun test", 10 + "test:integration": "bun test src/index.test.ts" 11 + }, 12 + "devDependencies": { 13 + "@biomejs/biome": "^2.3.2", 14 + "@simplewebauthn/types": "^12.0.0", 15 + "@types/bun": "latest" 16 + }, 17 + "peerDependencies": { 18 + "typescript": "^5" 19 + }, 20 + "dependencies": { 21 + "@simplewebauthn/browser": "^13.2.2", 22 + "@simplewebauthn/server": "^13.2.2", 23 + "eventsource-client": "^1.2.0", 24 + "lit": "^3.3.1", 25 + "nanoid": "^5.1.6", 26 + "ua-parser-js": "^2.0.6" 27 + } 28 28 }
+18 -8
scripts/test-classes.ts
··· 1 1 #!/usr/bin/env bun 2 2 import db from "../src/db/schema"; 3 - import { createClass, enrollUserInClass, getMeetingTimesForClass, createMeetingTime } from "../src/lib/classes"; 3 + import { 4 + createClass, 5 + createMeetingTime, 6 + enrollUserInClass, 7 + getMeetingTimesForClass, 8 + } from "../src/lib/classes"; 4 9 5 10 // Create a test user (admin) 6 11 const email = "admin@thistle.test"; ··· 11 16 let userId: number; 12 17 13 18 if (!existingUser) { 14 - db.run( 15 - "INSERT INTO users (email, password_hash, role) VALUES (?, ?, ?)", 16 - [email, "test-hash", "admin"], 17 - ); 18 - userId = db.query<{ id: number }, []>("SELECT last_insert_rowid() as id").get()!.id; 19 + db.run("INSERT INTO users (email, password_hash, role) VALUES (?, ?, ?)", [ 20 + email, 21 + "test-hash", 22 + "admin", 23 + ]); 24 + userId = db 25 + .query<{ id: number }, []>("SELECT last_insert_rowid() as id") 26 + .get()?.id; 19 27 console.log(`✅ Created admin user: ${email} (ID: ${userId})`); 20 28 } else { 21 29 userId = existingUser.id; ··· 31 39 year: 2024, 32 40 }); 33 41 34 - console.log(`✅ Created class: ${cls.course_code} - ${cls.name} (ID: ${cls.id})`); 42 + console.log( 43 + `✅ Created class: ${cls.course_code} - ${cls.name} (ID: ${cls.id})`, 44 + ); 35 45 36 46 // Enroll the admin in the class 37 47 enrollUserInClass(userId, cls.id); ··· 51 61 console.log(`- Course: ${cls.course_code} - ${cls.name}`); 52 62 console.log(`- Professor: ${cls.professor}`); 53 63 console.log(`- Semester: ${cls.semester} ${cls.year}`); 54 - console.log(`- Meetings: ${meetings.map(m => m.label).join(", ")}`); 64 + console.log(`- Meetings: ${meetings.map((m) => m.label).join(", ")}`);
+22 -6
src/components/admin-classes.ts
··· 513 513 514 514 private handleDeleteClick(id: string, type: "class" | "waitlist") { 515 515 // If this is a different item or timeout expired, reset 516 - if (!this.deleteState || this.deleteState.id !== id || this.deleteState.type !== type) { 516 + if ( 517 + !this.deleteState || 518 + this.deleteState.id !== id || 519 + this.deleteState.type !== type 520 + ) { 517 521 // Clear any existing timeout 518 522 if (this.deleteState?.timeout) { 519 523 clearTimeout(this.deleteState.timeout); ··· 556 560 } 557 561 558 562 private getDeleteButtonText(id: string, type: "class" | "waitlist"): string { 559 - if (!this.deleteState || this.deleteState.id !== id || this.deleteState.type !== type) { 563 + if ( 564 + !this.deleteState || 565 + this.deleteState.id !== id || 566 + this.deleteState.type !== type 567 + ) { 560 568 return "Delete"; 561 569 } 562 570 ··· 633 641 <div class="tabs"> 634 642 <button 635 643 class="tab ${this.activeTab === "classes" ? "active" : ""}" 636 - @click=${() => { this.activeTab = "classes"; }} 644 + @click=${() => { 645 + this.activeTab = "classes"; 646 + }} 637 647 > 638 648 Classes 639 649 </button> 640 650 <button 641 651 class="tab ${this.activeTab === "waitlist" ? "active" : ""}" 642 - @click=${() => { this.activeTab = "waitlist"; }} 652 + @click=${() => { 653 + this.activeTab = "waitlist"; 654 + }} 643 655 > 644 656 Waitlist 645 657 ${this.waitlist.length > 0 ? html`<span class="tab-badge">${this.waitlist.length}</span>` : ""} ··· 787 799 if (entry.meeting_times) { 788 800 try { 789 801 const parsed = JSON.parse(entry.meeting_times); 790 - this.meetingTimes = Array.isArray(parsed) && parsed.length > 0 ? parsed : []; 802 + this.meetingTimes = 803 + Array.isArray(parsed) && parsed.length > 0 ? parsed : []; 791 804 } catch { 792 805 this.meetingTimes = []; 793 806 } ··· 866 879 }; 867 880 } catch (error) { 868 881 console.error("Error in submitApproval:", error); 869 - this.error = error instanceof Error ? error.message : "Failed to approve waitlist entry. Please try again."; 882 + this.error = 883 + error instanceof Error 884 + ? error.message 885 + : "Failed to approve waitlist entry. Please try again."; 870 886 } 871 887 } 872 888
+25 -6
src/components/admin-pending-recordings.ts
··· 16 16 status: string; 17 17 } 18 18 19 + interface Class { 20 + id: string; 21 + name: string; 22 + course_code: string; 23 + } 24 + 25 + interface Transcription { 26 + id: string; 27 + original_filename: string; 28 + status: string; 29 + meeting_time_id: string | null; 30 + created_at: number; 31 + } 32 + 33 + interface MeetingTime { 34 + id: string; 35 + label: string; 36 + } 37 + 19 38 @customElement("admin-pending-recordings") 20 39 export class AdminPendingRecordings extends LitElement { 21 40 @state() recordings: PendingRecording[] = []; ··· 228 247 const classesGrouped = data.classes || {}; 229 248 230 249 // Flatten all classes 231 - const allClasses: any[] = []; 250 + const allClasses: Class[] = []; 232 251 for (const classes of Object.values(classesGrouped)) { 233 - allClasses.push(...(classes as any[])); 252 + allClasses.push(...(classes as Class[])); 234 253 } 235 254 236 255 // Fetch transcriptions for each class ··· 243 262 if (!classResponse.ok) return; 244 263 245 264 const classData = await classResponse.json(); 246 - const pendingTranscriptions = (classData.transcriptions || []).filter( 247 - (t: any) => t.status === "pending", 248 - ); 265 + const pendingTranscriptions = ( 266 + classData.transcriptions || [] 267 + ).filter((t: Transcription) => t.status === "pending"); 249 268 250 269 for (const transcription of pendingTranscriptions) { 251 270 // Get user info ··· 258 277 259 278 // Find meeting label 260 279 const meetingTime = classData.meetingTimes.find( 261 - (m: any) => m.id === transcription.meeting_time_id, 280 + (m: MeetingTime) => m.id === transcription.meeting_time_id, 262 281 ); 263 282 264 283 pendingRecordings.push({
+7 -4
src/components/admin-transcriptions.ts
··· 202 202 } 203 203 204 204 try { 205 - const response = await fetch(`/api/admin/transcriptions/${transcriptionId}`, { 206 - method: "DELETE", 207 - }); 205 + const response = await fetch( 206 + `/api/admin/transcriptions/${transcriptionId}`, 207 + { 208 + method: "DELETE", 209 + }, 210 + ); 208 211 209 212 if (!response.ok) { 210 213 throw new Error("Failed to delete transcription"); ··· 242 245 return this.transcriptions.filter( 243 246 (t) => 244 247 t.original_filename.toLowerCase().includes(query) || 245 - (t.user_name && t.user_name.toLowerCase().includes(query)) || 248 + t.user_name?.toLowerCase().includes(query) || 246 249 t.user_email.toLowerCase().includes(query), 247 250 ); 248 251 }
+8 -2
src/components/admin-users.ts
··· 235 235 } 236 236 } 237 237 238 - private async handleRoleChange(userId: number, email: string, newRole: "user" | "admin", oldRole: "user" | "admin", event: Event) { 238 + private async handleRoleChange( 239 + userId: number, 240 + email: string, 241 + newRole: "user" | "admin", 242 + oldRole: "user" | "admin", 243 + event: Event, 244 + ) { 239 245 const select = event.target as HTMLSelectElement; 240 246 241 247 const isDemotingSelf = ··· 345 351 return this.users.filter( 346 352 (u) => 347 353 u.email.toLowerCase().includes(query) || 348 - (u.name && u.name.toLowerCase().includes(query)), 354 + u.name?.toLowerCase().includes(query), 349 355 ); 350 356 } 351 357
+36 -13
src/components/class-view.ts
··· 26 26 meeting_time_id: string | null; 27 27 filename: string; 28 28 original_filename: string; 29 - status: "pending" | "selected" | "uploading" | "processing" | "transcribing" | "completed" | "failed"; 29 + status: 30 + | "pending" 31 + | "selected" 32 + | "uploading" 33 + | "processing" 34 + | "transcribing" 35 + | "completed" 36 + | "failed"; 30 37 progress: number; 31 38 error_message: string | null; 32 39 created_at: number; 33 40 updated_at: number; 41 + vttContent?: string; 42 + audioUrl?: string; 34 43 } 35 44 36 45 @customElement("class-view") ··· 312 321 private extractClassId() { 313 322 const path = window.location.pathname; 314 323 const match = path.match(/^\/classes\/(.+)$/); 315 - if (match && match[1]) { 324 + if (match?.[1]) { 316 325 this.classId = match[1]; 317 326 } 318 327 } ··· 351 360 } 352 361 353 362 private async loadVTTForCompleted() { 354 - const completed = this.transcriptions.filter((t) => t.status === "completed"); 363 + const completed = this.transcriptions.filter( 364 + (t) => t.status === "completed", 365 + ); 355 366 356 367 await Promise.all( 357 368 completed.map(async (transcription) => { 358 369 try { 359 - const response = await fetch(`/api/transcriptions/${transcription.id}?format=vtt`); 370 + const response = await fetch( 371 + `/api/transcriptions/${transcription.id}?format=vtt`, 372 + ); 360 373 if (response.ok) { 361 374 const vttContent = await response.text(); 362 - (transcription as any).vttContent = vttContent; 363 - (transcription as any).audioUrl = `/api/transcriptions/${transcription.id}/audio`; 375 + transcription.vttContent = vttContent; 376 + transcription.audioUrl = `/api/transcriptions/${transcription.id}/audio`; 364 377 this.requestUpdate(); 365 378 } 366 379 } catch (error) { ··· 371 384 } 372 385 373 386 private connectToTranscriptionStreams() { 374 - const activeStatuses = ["selected", "uploading", "processing", "transcribing"]; 387 + const activeStatuses = [ 388 + "selected", 389 + "uploading", 390 + "processing", 391 + "transcribing", 392 + ]; 375 393 for (const transcription of this.transcriptions) { 376 394 if (activeStatuses.includes(transcription.status)) { 377 395 this.connectToStream(transcription.id); ··· 382 400 private connectToStream(transcriptionId: string) { 383 401 if (this.eventSources.has(transcriptionId)) return; 384 402 385 - const eventSource = new EventSource(`/api/transcriptions/${transcriptionId}/stream`); 403 + const eventSource = new EventSource( 404 + `/api/transcriptions/${transcriptionId}/stream`, 405 + ); 386 406 387 407 eventSource.addEventListener("update", async (event) => { 388 408 const update = JSON.parse(event.data); 389 - const transcription = this.transcriptions.find((t) => t.id === transcriptionId); 409 + const transcription = this.transcriptions.find( 410 + (t) => t.id === transcriptionId, 411 + ); 390 412 391 413 if (transcription) { 392 414 if (update.status !== undefined) transcription.status = update.status; 393 - if (update.progress !== undefined) transcription.progress = update.progress; 415 + if (update.progress !== undefined) 416 + transcription.progress = update.progress; 394 417 395 418 if (update.status === "completed") { 396 419 await this.loadVTTForCompleted(); ··· 557 580 } 558 581 559 582 ${ 560 - t.status === "completed" && (t as any).audioUrl && (t as any).vttContent 583 + t.status === "completed" && t.audioUrl && t.vttContent 561 584 ? html` 562 585 <div class="audio-player"> 563 - <audio id="audio-${t.id}" preload="metadata" controls src="${(t as any).audioUrl}"></audio> 586 + <audio id="audio-${t.id}" preload="metadata" controls src="${t.audioUrl}"></audio> 564 587 </div> 565 - <vtt-viewer .vttContent=${(t as any).vttContent} .audioId=${`audio-${t.id}`}></vtt-viewer> 588 + <vtt-viewer .vttContent=${t.vttContent} .audioId=${`audio-${t.id}`}></vtt-viewer> 566 589 ` 567 590 : "" 568 591 }
+3 -1
src/components/upload-recording-modal.ts
··· 273 273 } catch (error) { 274 274 console.error("Upload failed:", error); 275 275 this.error = 276 - error instanceof Error ? error.message : "Upload failed. Please try again."; 276 + error instanceof Error 277 + ? error.message 278 + : "Upload failed. Please try again."; 277 279 } finally { 278 280 this.uploading = false; 279 281 }
+7 -9
src/db/schema.test.ts
··· 1 1 import { Database } from "bun:sqlite"; 2 - import { expect, test, afterEach } from "bun:test"; 2 + import { afterEach, expect, test } from "bun:test"; 3 3 import { unlinkSync } from "node:fs"; 4 4 5 5 const TEST_DB = "test-schema.db"; ··· 160 160 ]); 161 161 162 162 const enrollment = db 163 - .query< 164 - { class_id: string; user_id: number }, 165 - [] 166 - >("SELECT class_id, user_id FROM class_members") 163 + .query<{ class_id: string; user_id: number }, []>( 164 + "SELECT class_id, user_id FROM class_members", 165 + ) 167 166 .get(); 168 167 169 168 expect(enrollment?.class_id).toBe("class-1"); ··· 219 218 ); 220 219 221 220 const transcription = db 222 - .query< 223 - { status: string; progress: number }, 224 - [] 225 - >("SELECT status, progress FROM transcriptions WHERE id = 'trans-1'") 221 + .query<{ status: string; progress: number }, []>( 222 + "SELECT status, progress FROM transcriptions WHERE id = 'trans-1'", 223 + ) 226 224 .get(); 227 225 228 226 expect(transcription?.status).toBe("pending");
+470 -226
src/index.test.ts
··· 1 - import { describe, expect, test, beforeAll, afterAll, beforeEach } from "bun:test"; 1 + import { 2 + afterAll, 3 + beforeAll, 4 + beforeEach, 5 + describe, 6 + expect, 7 + test, 8 + } from "bun:test"; 2 9 import db from "./db/schema"; 3 10 import { hashPasswordClient } from "./lib/client-auth"; 4 11 ··· 17 24 serverAvailable = response.ok || response.status === 404; 18 25 } catch { 19 26 console.warn( 20 - `\n⚠️ Test server not running on port ${TEST_PORT}. Start it with:\n PORT=${TEST_PORT} bun run src/index.ts\n Then run tests in another terminal.\n` 27 + `\n⚠️ Test server not running on port ${TEST_PORT}. Start it with:\n PORT=${TEST_PORT} bun run src/index.ts\n Then run tests in another terminal.\n`, 21 28 ); 22 29 serverAvailable = false; 23 30 } ··· 43 50 }; 44 51 45 52 // Helper to hash passwords like the client would 46 - async function clientHashPassword(email: string, password: string): Promise<string> { 53 + async function clientHashPassword( 54 + email: string, 55 + password: string, 56 + ): Promise<string> { 47 57 return await hashPasswordClient(password, email); 48 58 } 49 59 50 60 // Helper to extract session cookie 51 - function extractSessionCookie(response: Response): string | null { 61 + function extractSessionCookie(response: Response): string { 52 62 const setCookie = response.headers.get("set-cookie"); 53 - if (!setCookie) return null; 63 + if (!setCookie) throw new Error("No set-cookie header found"); 54 64 const match = setCookie.match(/session=([^;]+)/); 55 - return match ? match[1] : null; 65 + if (!match) throw new Error("No session cookie found in set-cookie header"); 66 + return match[1]; 56 67 } 57 68 58 69 // Helper to make authenticated requests ··· 74 85 function cleanupTestData() { 75 86 // Delete test users and their related data (cascade will handle most of it) 76 87 // Include 'newemail%' to catch users whose emails were updated during tests 77 - db.run("DELETE FROM sessions WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')"); 78 - db.run("DELETE FROM passkeys WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')"); 79 - db.run("DELETE FROM transcriptions WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')"); 80 - db.run("DELETE FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%'"); 88 + db.run( 89 + "DELETE FROM sessions WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')", 90 + ); 91 + db.run( 92 + "DELETE FROM passkeys WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')", 93 + ); 94 + db.run( 95 + "DELETE FROM transcriptions WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')", 96 + ); 97 + db.run( 98 + "DELETE FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%'", 99 + ); 81 100 82 101 // Clear ALL rate limit data to prevent accumulation across tests 83 102 // (IP-based rate limits don't contain test/admin in the key) ··· 110 129 describe("API Endpoints - Authentication", () => { 111 130 describe("POST /api/auth/register", () => { 112 131 serverTest("should register a new user successfully", async () => { 113 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 132 + const hashedPassword = await clientHashPassword( 133 + TEST_USER.email, 134 + TEST_USER.password, 135 + ); 114 136 115 137 const response = await fetch(`${BASE_URL}/api/auth/register`, { 116 138 method: "POST", ··· 143 165 expect(data.error).toBe("Email and password required"); 144 166 }); 145 167 146 - serverTest("should reject registration with invalid password format", async () => { 147 - const response = await fetch(`${BASE_URL}/api/auth/register`, { 148 - method: "POST", 149 - headers: { "Content-Type": "application/json" }, 150 - body: JSON.stringify({ 151 - email: TEST_USER.email, 152 - password: "short", 153 - }), 154 - }); 168 + serverTest( 169 + "should reject registration with invalid password format", 170 + async () => { 171 + const response = await fetch(`${BASE_URL}/api/auth/register`, { 172 + method: "POST", 173 + headers: { "Content-Type": "application/json" }, 174 + body: JSON.stringify({ 175 + email: TEST_USER.email, 176 + password: "short", 177 + }), 178 + }); 155 179 156 - expect(response.status).toBe(400); 157 - const data = await response.json(); 158 - expect(data.error).toBe("Invalid password format"); 159 - }); 180 + expect(response.status).toBe(400); 181 + const data = await response.json(); 182 + expect(data.error).toBe("Invalid password format"); 183 + }, 184 + ); 160 185 161 186 serverTest("should reject duplicate email registration", async () => { 162 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 187 + const hashedPassword = await clientHashPassword( 188 + TEST_USER.email, 189 + TEST_USER.password, 190 + ); 163 191 164 192 // First registration 165 193 await fetch(`${BASE_URL}/api/auth/register`, { ··· 189 217 }); 190 218 191 219 serverTest("should enforce rate limiting on registration", async () => { 192 - const hashedPassword = await clientHashPassword("test@example.com", "password"); 220 + const hashedPassword = await clientHashPassword( 221 + "test@example.com", 222 + "password", 223 + ); 193 224 194 225 // Make registration attempts until rate limit is hit (limit is 5 per hour) 195 226 let rateLimitHit = false; ··· 217 248 describe("POST /api/auth/login", () => { 218 249 serverTest("should login successfully with valid credentials", async () => { 219 250 // Register user first 220 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 251 + const hashedPassword = await clientHashPassword( 252 + TEST_USER.email, 253 + TEST_USER.password, 254 + ); 221 255 await fetch(`${BASE_URL}/api/auth/register`, { 222 256 method: "POST", 223 257 headers: { "Content-Type": "application/json" }, ··· 247 281 248 282 serverTest("should reject login with invalid credentials", async () => { 249 283 // Register user first 250 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 284 + const hashedPassword = await clientHashPassword( 285 + TEST_USER.email, 286 + TEST_USER.password, 287 + ); 251 288 await fetch(`${BASE_URL}/api/auth/register`, { 252 289 method: "POST", 253 290 headers: { "Content-Type": "application/json" }, ··· 258 295 }); 259 296 260 297 // Login with wrong password 261 - const wrongPassword = await clientHashPassword(TEST_USER.email, "WrongPassword123!"); 298 + const wrongPassword = await clientHashPassword( 299 + TEST_USER.email, 300 + "WrongPassword123!", 301 + ); 262 302 const response = await fetch(`${BASE_URL}/api/auth/login`, { 263 303 method: "POST", 264 304 headers: { "Content-Type": "application/json" }, ··· 288 328 }); 289 329 290 330 serverTest("should enforce rate limiting on login attempts", async () => { 291 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 331 + const hashedPassword = await clientHashPassword( 332 + TEST_USER.email, 333 + TEST_USER.password, 334 + ); 292 335 293 336 // Make 11 login attempts (limit is 10 per 15 minutes per IP) 294 337 let rateLimitHit = false; ··· 316 359 describe("POST /api/auth/logout", () => { 317 360 serverTest("should logout successfully", async () => { 318 361 // Register and login 319 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 362 + const hashedPassword = await clientHashPassword( 363 + TEST_USER.email, 364 + TEST_USER.password, 365 + ); 320 366 const loginResponse = await fetch(`${BASE_URL}/api/auth/register`, { 321 367 method: "POST", 322 368 headers: { "Content-Type": "application/json" }, ··· 325 371 password: hashedPassword, 326 372 }), 327 373 }); 328 - const sessionCookie = extractSessionCookie(loginResponse)!; 374 + const sessionCookie = extractSessionCookie(loginResponse); 329 375 330 376 // Logout 331 - const response = await authRequest(`${BASE_URL}/api/auth/logout`, sessionCookie, { 332 - method: "POST", 333 - }); 377 + const response = await authRequest( 378 + `${BASE_URL}/api/auth/logout`, 379 + sessionCookie, 380 + { 381 + method: "POST", 382 + }, 383 + ); 334 384 335 385 expect(response.status).toBe(200); 336 386 const data = await response.json(); ··· 353 403 }); 354 404 355 405 describe("GET /api/auth/me", () => { 356 - serverTest("should return current user info when authenticated", async () => { 357 - // Register user 358 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 359 - const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 360 - method: "POST", 361 - headers: { "Content-Type": "application/json" }, 362 - body: JSON.stringify({ 363 - email: TEST_USER.email, 364 - password: hashedPassword, 365 - name: TEST_USER.name, 366 - }), 367 - }); 368 - const sessionCookie = extractSessionCookie(registerResponse)!; 406 + serverTest( 407 + "should return current user info when authenticated", 408 + async () => { 409 + // Register user 410 + const hashedPassword = await clientHashPassword( 411 + TEST_USER.email, 412 + TEST_USER.password, 413 + ); 414 + const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 415 + method: "POST", 416 + headers: { "Content-Type": "application/json" }, 417 + body: JSON.stringify({ 418 + email: TEST_USER.email, 419 + password: hashedPassword, 420 + name: TEST_USER.name, 421 + }), 422 + }); 423 + const sessionCookie = extractSessionCookie(registerResponse); 369 424 370 - // Get current user 371 - const response = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie); 425 + // Get current user 426 + const response = await authRequest( 427 + `${BASE_URL}/api/auth/me`, 428 + sessionCookie, 429 + ); 372 430 373 - expect(response.status).toBe(200); 374 - const data = await response.json(); 375 - expect(data.email).toBe(TEST_USER.email); 376 - expect(data.name).toBe(TEST_USER.name); 377 - expect(data.role).toBeDefined(); 378 - }); 431 + expect(response.status).toBe(200); 432 + const data = await response.json(); 433 + expect(data.email).toBe(TEST_USER.email); 434 + expect(data.name).toBe(TEST_USER.name); 435 + expect(data.role).toBeDefined(); 436 + }, 437 + ); 379 438 380 439 serverTest("should return 401 when not authenticated", async () => { 381 440 const response = await fetch(`${BASE_URL}/api/auth/me`); ··· 386 445 }); 387 446 388 447 serverTest("should return 401 with invalid session", async () => { 389 - const response = await authRequest(`${BASE_URL}/api/auth/me`, "invalid-session"); 448 + const response = await authRequest( 449 + `${BASE_URL}/api/auth/me`, 450 + "invalid-session", 451 + ); 390 452 391 453 expect(response.status).toBe(401); 392 454 const data = await response.json(); ··· 399 461 describe("GET /api/sessions", () => { 400 462 serverTest("should return user sessions", async () => { 401 463 // Register user 402 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 464 + const hashedPassword = await clientHashPassword( 465 + TEST_USER.email, 466 + TEST_USER.password, 467 + ); 403 468 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 404 469 method: "POST", 405 470 headers: { "Content-Type": "application/json" }, ··· 408 473 password: hashedPassword, 409 474 }), 410 475 }); 411 - const sessionCookie = extractSessionCookie(registerResponse)!; 476 + const sessionCookie = extractSessionCookie(registerResponse); 412 477 413 478 // Get sessions 414 - const response = await authRequest(`${BASE_URL}/api/sessions`, sessionCookie); 479 + const response = await authRequest( 480 + `${BASE_URL}/api/sessions`, 481 + sessionCookie, 482 + ); 415 483 416 484 expect(response.status).toBe(200); 417 485 const data = await response.json(); ··· 432 500 describe("DELETE /api/sessions", () => { 433 501 serverTest("should delete specific session", async () => { 434 502 // Register user and create multiple sessions 435 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 503 + const hashedPassword = await clientHashPassword( 504 + TEST_USER.email, 505 + TEST_USER.password, 506 + ); 436 507 const session1Response = await fetch(`${BASE_URL}/api/auth/register`, { 437 508 method: "POST", 438 509 headers: { "Content-Type": "application/json" }, ··· 441 512 password: hashedPassword, 442 513 }), 443 514 }); 444 - const session1Cookie = extractSessionCookie(session1Response)!; 515 + const session1Cookie = extractSessionCookie(session1Response); 445 516 446 517 const session2Response = await fetch(`${BASE_URL}/api/auth/login`, { 447 518 method: "POST", ··· 451 522 password: hashedPassword, 452 523 }), 453 524 }); 454 - const session2Cookie = extractSessionCookie(session2Response)!; 525 + const session2Cookie = extractSessionCookie(session2Response); 455 526 456 527 // Get sessions list 457 - const sessionsResponse = await authRequest(`${BASE_URL}/api/sessions`, session1Cookie); 528 + const sessionsResponse = await authRequest( 529 + `${BASE_URL}/api/sessions`, 530 + session1Cookie, 531 + ); 458 532 const sessionsData = await sessionsResponse.json(); 459 533 const targetSessionId = sessionsData.sessions.find( 460 - (s: any) => s.id === session2Cookie 534 + (s: { id: string }) => s.id === session2Cookie, 461 535 )?.id; 462 536 463 537 // Delete session 2 464 - const response = await authRequest(`${BASE_URL}/api/sessions`, session1Cookie, { 465 - method: "DELETE", 466 - headers: { "Content-Type": "application/json" }, 467 - body: JSON.stringify({ sessionId: targetSessionId }), 468 - }); 538 + const response = await authRequest( 539 + `${BASE_URL}/api/sessions`, 540 + session1Cookie, 541 + { 542 + method: "DELETE", 543 + headers: { "Content-Type": "application/json" }, 544 + body: JSON.stringify({ sessionId: targetSessionId }), 545 + }, 546 + ); 469 547 470 548 expect(response.status).toBe(200); 471 549 const data = await response.json(); 472 550 expect(data.success).toBe(true); 473 551 474 552 // Verify session 2 is deleted 475 - const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, session2Cookie); 553 + const verifyResponse = await authRequest( 554 + `${BASE_URL}/api/auth/me`, 555 + session2Cookie, 556 + ); 476 557 expect(verifyResponse.status).toBe(401); 477 558 }); 478 559 479 560 serverTest("should not delete another user's session", async () => { 480 561 // Register two users 481 - const hashedPassword1 = await clientHashPassword(TEST_USER.email, TEST_USER.password); 562 + const hashedPassword1 = await clientHashPassword( 563 + TEST_USER.email, 564 + TEST_USER.password, 565 + ); 482 566 const user1Response = await fetch(`${BASE_URL}/api/auth/register`, { 483 567 method: "POST", 484 568 headers: { "Content-Type": "application/json" }, ··· 487 571 password: hashedPassword1, 488 572 }), 489 573 }); 490 - const user1Cookie = extractSessionCookie(user1Response)!; 574 + const user1Cookie = extractSessionCookie(user1Response); 491 575 492 - const hashedPassword2 = await clientHashPassword(TEST_USER_2.email, TEST_USER_2.password); 576 + const hashedPassword2 = await clientHashPassword( 577 + TEST_USER_2.email, 578 + TEST_USER_2.password, 579 + ); 493 580 const user2Response = await fetch(`${BASE_URL}/api/auth/register`, { 494 581 method: "POST", 495 582 headers: { "Content-Type": "application/json" }, ··· 498 585 password: hashedPassword2, 499 586 }), 500 587 }); 501 - const user2Cookie = extractSessionCookie(user2Response)!; 588 + const user2Cookie = extractSessionCookie(user2Response); 502 589 503 590 // Try to delete user2's session using user1's credentials 504 - const response = await authRequest(`${BASE_URL}/api/sessions`, user1Cookie, { 505 - method: "DELETE", 506 - headers: { "Content-Type": "application/json" }, 507 - body: JSON.stringify({ sessionId: user2Cookie }), 508 - }); 591 + const response = await authRequest( 592 + `${BASE_URL}/api/sessions`, 593 + user1Cookie, 594 + { 595 + method: "DELETE", 596 + headers: { "Content-Type": "application/json" }, 597 + body: JSON.stringify({ sessionId: user2Cookie }), 598 + }, 599 + ); 509 600 510 601 expect(response.status).toBe(404); 511 602 }); ··· 516 607 describe("DELETE /api/user", () => { 517 608 serverTest("should delete user account", async () => { 518 609 // Register user 519 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 610 + const hashedPassword = await clientHashPassword( 611 + TEST_USER.email, 612 + TEST_USER.password, 613 + ); 520 614 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 521 615 method: "POST", 522 616 headers: { "Content-Type": "application/json" }, ··· 525 619 password: hashedPassword, 526 620 }), 527 621 }); 528 - const sessionCookie = extractSessionCookie(registerResponse)!; 622 + const sessionCookie = extractSessionCookie(registerResponse); 529 623 530 624 // Delete account 531 - const response = await authRequest(`${BASE_URL}/api/user`, sessionCookie, { 532 - method: "DELETE", 533 - }); 625 + const response = await authRequest( 626 + `${BASE_URL}/api/user`, 627 + sessionCookie, 628 + { 629 + method: "DELETE", 630 + }, 631 + ); 534 632 535 633 expect(response.status).toBe(200); 536 634 const data = await response.json(); 537 635 expect(data.success).toBe(true); 538 636 539 637 // Verify user is deleted 540 - const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie); 638 + const verifyResponse = await authRequest( 639 + `${BASE_URL}/api/auth/me`, 640 + sessionCookie, 641 + ); 541 642 expect(verifyResponse.status).toBe(401); 542 643 }); 543 644 ··· 553 654 describe("PUT /api/user/email", () => { 554 655 serverTest("should update user email", async () => { 555 656 // Register user 556 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 657 + const hashedPassword = await clientHashPassword( 658 + TEST_USER.email, 659 + TEST_USER.password, 660 + ); 557 661 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 558 662 method: "POST", 559 663 headers: { "Content-Type": "application/json" }, ··· 562 666 password: hashedPassword, 563 667 }), 564 668 }); 565 - const sessionCookie = extractSessionCookie(registerResponse)!; 669 + const sessionCookie = extractSessionCookie(registerResponse); 566 670 567 671 // Update email 568 672 const newEmail = "newemail@example.com"; 569 - const response = await authRequest(`${BASE_URL}/api/user/email`, sessionCookie, { 570 - method: "PUT", 571 - headers: { "Content-Type": "application/json" }, 572 - body: JSON.stringify({ email: newEmail }), 573 - }); 673 + const response = await authRequest( 674 + `${BASE_URL}/api/user/email`, 675 + sessionCookie, 676 + { 677 + method: "PUT", 678 + headers: { "Content-Type": "application/json" }, 679 + body: JSON.stringify({ email: newEmail }), 680 + }, 681 + ); 574 682 575 683 expect(response.status).toBe(200); 576 684 const data = await response.json(); 577 685 expect(data.success).toBe(true); 578 686 579 687 // Verify email updated 580 - const meResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie); 688 + const meResponse = await authRequest( 689 + `${BASE_URL}/api/auth/me`, 690 + sessionCookie, 691 + ); 581 692 const meData = await meResponse.json(); 582 693 expect(meData.email).toBe(newEmail); 583 694 }); 584 695 585 696 serverTest("should reject duplicate email", async () => { 586 697 // Register two users 587 - const hashedPassword1 = await clientHashPassword(TEST_USER.email, TEST_USER.password); 698 + const hashedPassword1 = await clientHashPassword( 699 + TEST_USER.email, 700 + TEST_USER.password, 701 + ); 588 702 await fetch(`${BASE_URL}/api/auth/register`, { 589 703 method: "POST", 590 704 headers: { "Content-Type": "application/json" }, ··· 594 708 }), 595 709 }); 596 710 597 - const hashedPassword2 = await clientHashPassword(TEST_USER_2.email, TEST_USER_2.password); 711 + const hashedPassword2 = await clientHashPassword( 712 + TEST_USER_2.email, 713 + TEST_USER_2.password, 714 + ); 598 715 const user2Response = await fetch(`${BASE_URL}/api/auth/register`, { 599 716 method: "POST", 600 717 headers: { "Content-Type": "application/json" }, ··· 603 720 password: hashedPassword2, 604 721 }), 605 722 }); 606 - const user2Cookie = extractSessionCookie(user2Response)!; 723 + const user2Cookie = extractSessionCookie(user2Response); 607 724 608 725 // Try to update user2's email to user1's email 609 - const response = await authRequest(`${BASE_URL}/api/user/email`, user2Cookie, { 610 - method: "PUT", 611 - headers: { "Content-Type": "application/json" }, 612 - body: JSON.stringify({ email: TEST_USER.email }), 613 - }); 726 + const response = await authRequest( 727 + `${BASE_URL}/api/user/email`, 728 + user2Cookie, 729 + { 730 + method: "PUT", 731 + headers: { "Content-Type": "application/json" }, 732 + body: JSON.stringify({ email: TEST_USER.email }), 733 + }, 734 + ); 614 735 615 736 expect(response.status).toBe(400); 616 737 const data = await response.json(); ··· 621 742 describe("PUT /api/user/password", () => { 622 743 serverTest("should update user password", async () => { 623 744 // Register user 624 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 745 + const hashedPassword = await clientHashPassword( 746 + TEST_USER.email, 747 + TEST_USER.password, 748 + ); 625 749 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 626 750 method: "POST", 627 751 headers: { "Content-Type": "application/json" }, ··· 630 754 password: hashedPassword, 631 755 }), 632 756 }); 633 - const sessionCookie = extractSessionCookie(registerResponse)!; 757 + const sessionCookie = extractSessionCookie(registerResponse); 634 758 635 759 // Update password 636 - const newPassword = await clientHashPassword(TEST_USER.email, "NewPassword123!"); 637 - const response = await authRequest(`${BASE_URL}/api/user/password`, sessionCookie, { 638 - method: "PUT", 639 - headers: { "Content-Type": "application/json" }, 640 - body: JSON.stringify({ password: newPassword }), 641 - }); 760 + const newPassword = await clientHashPassword( 761 + TEST_USER.email, 762 + "NewPassword123!", 763 + ); 764 + const response = await authRequest( 765 + `${BASE_URL}/api/user/password`, 766 + sessionCookie, 767 + { 768 + method: "PUT", 769 + headers: { "Content-Type": "application/json" }, 770 + body: JSON.stringify({ password: newPassword }), 771 + }, 772 + ); 642 773 643 774 expect(response.status).toBe(200); 644 775 const data = await response.json(); ··· 658 789 659 790 serverTest("should reject invalid password format", async () => { 660 791 // Register user 661 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 792 + const hashedPassword = await clientHashPassword( 793 + TEST_USER.email, 794 + TEST_USER.password, 795 + ); 662 796 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 663 797 method: "POST", 664 798 headers: { "Content-Type": "application/json" }, ··· 667 801 password: hashedPassword, 668 802 }), 669 803 }); 670 - const sessionCookie = extractSessionCookie(registerResponse)!; 804 + const sessionCookie = extractSessionCookie(registerResponse); 671 805 672 806 // Try to update with invalid format 673 - const response = await authRequest(`${BASE_URL}/api/user/password`, sessionCookie, { 674 - method: "PUT", 675 - headers: { "Content-Type": "application/json" }, 676 - body: JSON.stringify({ password: "short" }), 677 - }); 807 + const response = await authRequest( 808 + `${BASE_URL}/api/user/password`, 809 + sessionCookie, 810 + { 811 + method: "PUT", 812 + headers: { "Content-Type": "application/json" }, 813 + body: JSON.stringify({ password: "short" }), 814 + }, 815 + ); 678 816 679 817 expect(response.status).toBe(400); 680 818 const data = await response.json(); ··· 685 823 describe("PUT /api/user/name", () => { 686 824 serverTest("should update user name", async () => { 687 825 // Register user 688 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 826 + const hashedPassword = await clientHashPassword( 827 + TEST_USER.email, 828 + TEST_USER.password, 829 + ); 689 830 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 690 831 method: "POST", 691 832 headers: { "Content-Type": "application/json" }, ··· 695 836 name: TEST_USER.name, 696 837 }), 697 838 }); 698 - const sessionCookie = extractSessionCookie(registerResponse)!; 839 + const sessionCookie = extractSessionCookie(registerResponse); 699 840 700 841 // Update name 701 842 const newName = "Updated Name"; 702 - const response = await authRequest(`${BASE_URL}/api/user/name`, sessionCookie, { 703 - method: "PUT", 704 - headers: { "Content-Type": "application/json" }, 705 - body: JSON.stringify({ name: newName }), 706 - }); 843 + const response = await authRequest( 844 + `${BASE_URL}/api/user/name`, 845 + sessionCookie, 846 + { 847 + method: "PUT", 848 + headers: { "Content-Type": "application/json" }, 849 + body: JSON.stringify({ name: newName }), 850 + }, 851 + ); 707 852 708 853 expect(response.status).toBe(200); 709 854 const data = await response.json(); 710 855 expect(data.success).toBe(true); 711 856 712 857 // Verify name updated 713 - const meResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie); 858 + const meResponse = await authRequest( 859 + `${BASE_URL}/api/auth/me`, 860 + sessionCookie, 861 + ); 714 862 const meData = await meResponse.json(); 715 863 expect(meData.name).toBe(newName); 716 864 }); 717 865 718 866 serverTest("should reject missing name", async () => { 719 867 // Register user 720 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 868 + const hashedPassword = await clientHashPassword( 869 + TEST_USER.email, 870 + TEST_USER.password, 871 + ); 721 872 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 722 873 method: "POST", 723 874 headers: { "Content-Type": "application/json" }, ··· 726 877 password: hashedPassword, 727 878 }), 728 879 }); 729 - const sessionCookie = extractSessionCookie(registerResponse)!; 880 + const sessionCookie = extractSessionCookie(registerResponse); 730 881 731 - const response = await authRequest(`${BASE_URL}/api/user/name`, sessionCookie, { 732 - method: "PUT", 733 - headers: { "Content-Type": "application/json" }, 734 - body: JSON.stringify({}), 735 - }); 882 + const response = await authRequest( 883 + `${BASE_URL}/api/user/name`, 884 + sessionCookie, 885 + { 886 + method: "PUT", 887 + headers: { "Content-Type": "application/json" }, 888 + body: JSON.stringify({}), 889 + }, 890 + ); 736 891 737 892 expect(response.status).toBe(400); 738 893 }); ··· 741 896 describe("PUT /api/user/avatar", () => { 742 897 serverTest("should update user avatar", async () => { 743 898 // Register user 744 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 899 + const hashedPassword = await clientHashPassword( 900 + TEST_USER.email, 901 + TEST_USER.password, 902 + ); 745 903 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 746 904 method: "POST", 747 905 headers: { "Content-Type": "application/json" }, ··· 750 908 password: hashedPassword, 751 909 }), 752 910 }); 753 - const sessionCookie = extractSessionCookie(registerResponse)!; 911 + const sessionCookie = extractSessionCookie(registerResponse); 754 912 755 913 // Update avatar 756 914 const newAvatar = "👨‍💻"; 757 - const response = await authRequest(`${BASE_URL}/api/user/avatar`, sessionCookie, { 758 - method: "PUT", 759 - headers: { "Content-Type": "application/json" }, 760 - body: JSON.stringify({ avatar: newAvatar }), 761 - }); 915 + const response = await authRequest( 916 + `${BASE_URL}/api/user/avatar`, 917 + sessionCookie, 918 + { 919 + method: "PUT", 920 + headers: { "Content-Type": "application/json" }, 921 + body: JSON.stringify({ avatar: newAvatar }), 922 + }, 923 + ); 762 924 763 925 expect(response.status).toBe(200); 764 926 const data = await response.json(); 765 927 expect(data.success).toBe(true); 766 928 767 929 // Verify avatar updated 768 - const meResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie); 930 + const meResponse = await authRequest( 931 + `${BASE_URL}/api/auth/me`, 932 + sessionCookie, 933 + ); 769 934 const meData = await meResponse.json(); 770 935 expect(meData.avatar).toBe(newAvatar); 771 936 }); ··· 774 939 775 940 describe("API Endpoints - Transcriptions", () => { 776 941 describe("GET /api/transcriptions/health", () => { 777 - serverTest("should return transcription service health status", async () => { 778 - const response = await fetch(`${BASE_URL}/api/transcriptions/health`); 942 + serverTest( 943 + "should return transcription service health status", 944 + async () => { 945 + const response = await fetch(`${BASE_URL}/api/transcriptions/health`); 779 946 780 - expect(response.status).toBe(200); 781 - const data = await response.json(); 782 - expect(data).toHaveProperty("available"); 783 - expect(typeof data.available).toBe("boolean"); 784 - }); 947 + expect(response.status).toBe(200); 948 + const data = await response.json(); 949 + expect(data).toHaveProperty("available"); 950 + expect(typeof data.available).toBe("boolean"); 951 + }, 952 + ); 785 953 }); 786 954 787 955 describe("GET /api/transcriptions", () => { 788 956 serverTest("should return user transcriptions", async () => { 789 957 // Register user 790 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 958 + const hashedPassword = await clientHashPassword( 959 + TEST_USER.email, 960 + TEST_USER.password, 961 + ); 791 962 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 792 963 method: "POST", 793 964 headers: { "Content-Type": "application/json" }, ··· 796 967 password: hashedPassword, 797 968 }), 798 969 }); 799 - const sessionCookie = extractSessionCookie(registerResponse)!; 970 + const sessionCookie = extractSessionCookie(registerResponse); 800 971 801 972 // Get transcriptions 802 - const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie); 973 + const response = await authRequest( 974 + `${BASE_URL}/api/transcriptions`, 975 + sessionCookie, 976 + ); 803 977 804 978 expect(response.status).toBe(200); 805 979 const data = await response.json(); ··· 817 991 describe("POST /api/transcriptions", () => { 818 992 serverTest("should upload audio file and start transcription", async () => { 819 993 // Register user 820 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 994 + const hashedPassword = await clientHashPassword( 995 + TEST_USER.email, 996 + TEST_USER.password, 997 + ); 821 998 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 822 999 method: "POST", 823 1000 headers: { "Content-Type": "application/json" }, ··· 826 1003 password: hashedPassword, 827 1004 }), 828 1005 }); 829 - const sessionCookie = extractSessionCookie(registerResponse)!; 1006 + const sessionCookie = extractSessionCookie(registerResponse); 830 1007 831 1008 // Create a test audio file 832 1009 const audioBlob = new Blob(["fake audio data"], { type: "audio/mp3" }); ··· 835 1012 formData.append("class_name", "Test Class"); 836 1013 837 1014 // Upload 838 - const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie, { 839 - method: "POST", 840 - body: formData, 841 - }); 1015 + const response = await authRequest( 1016 + `${BASE_URL}/api/transcriptions`, 1017 + sessionCookie, 1018 + { 1019 + method: "POST", 1020 + body: formData, 1021 + }, 1022 + ); 842 1023 843 1024 expect(response.status).toBe(200); 844 1025 const data = await response.json(); ··· 848 1029 849 1030 serverTest("should reject non-audio files", async () => { 850 1031 // Register user 851 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 1032 + const hashedPassword = await clientHashPassword( 1033 + TEST_USER.email, 1034 + TEST_USER.password, 1035 + ); 852 1036 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 853 1037 method: "POST", 854 1038 headers: { "Content-Type": "application/json" }, ··· 857 1041 password: hashedPassword, 858 1042 }), 859 1043 }); 860 - const sessionCookie = extractSessionCookie(registerResponse)!; 1044 + const sessionCookie = extractSessionCookie(registerResponse); 861 1045 862 1046 // Try to upload non-audio file 863 1047 const textBlob = new Blob(["text file"], { type: "text/plain" }); 864 1048 const formData = new FormData(); 865 1049 formData.append("audio", textBlob, "test.txt"); 866 1050 867 - const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie, { 868 - method: "POST", 869 - body: formData, 870 - }); 1051 + const response = await authRequest( 1052 + `${BASE_URL}/api/transcriptions`, 1053 + sessionCookie, 1054 + { 1055 + method: "POST", 1056 + body: formData, 1057 + }, 1058 + ); 871 1059 872 1060 expect(response.status).toBe(400); 873 1061 }); 874 1062 875 1063 serverTest("should reject files exceeding size limit", async () => { 876 1064 // Register user 877 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 1065 + const hashedPassword = await clientHashPassword( 1066 + TEST_USER.email, 1067 + TEST_USER.password, 1068 + ); 878 1069 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 879 1070 method: "POST", 880 1071 headers: { "Content-Type": "application/json" }, ··· 883 1074 password: hashedPassword, 884 1075 }), 885 1076 }); 886 - const sessionCookie = extractSessionCookie(registerResponse)!; 1077 + const sessionCookie = extractSessionCookie(registerResponse); 887 1078 888 1079 // Create a file larger than 100MB (the actual limit) 889 - const largeBlob = new Blob([new ArrayBuffer(101 * 1024 * 1024)], { type: "audio/mp3" }); 1080 + const largeBlob = new Blob([new ArrayBuffer(101 * 1024 * 1024)], { 1081 + type: "audio/mp3", 1082 + }); 890 1083 const formData = new FormData(); 891 1084 formData.append("audio", largeBlob, "large.mp3"); 892 1085 893 - const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie, { 894 - method: "POST", 895 - body: formData, 896 - }); 1086 + const response = await authRequest( 1087 + `${BASE_URL}/api/transcriptions`, 1088 + sessionCookie, 1089 + { 1090 + method: "POST", 1091 + body: formData, 1092 + }, 1093 + ); 897 1094 898 1095 expect(response.status).toBe(400); 899 1096 const data = await response.json(); ··· 924 1121 if (!serverAvailable) return; 925 1122 926 1123 // Create admin user 927 - const adminHash = await clientHashPassword(TEST_ADMIN.email, TEST_ADMIN.password); 1124 + const adminHash = await clientHashPassword( 1125 + TEST_ADMIN.email, 1126 + TEST_ADMIN.password, 1127 + ); 928 1128 const adminResponse = await fetch(`${BASE_URL}/api/auth/register`, { 929 1129 method: "POST", 930 1130 headers: { "Content-Type": "application/json" }, ··· 934 1134 name: TEST_ADMIN.name, 935 1135 }), 936 1136 }); 937 - adminCookie = extractSessionCookie(adminResponse)!; 1137 + adminCookie = extractSessionCookie(adminResponse); 938 1138 939 1139 // Manually set admin role in database 940 - db.run("UPDATE users SET role = 'admin' WHERE email = ?", [TEST_ADMIN.email]); 1140 + db.run("UPDATE users SET role = 'admin' WHERE email = ?", [ 1141 + TEST_ADMIN.email, 1142 + ]); 941 1143 942 1144 // Create regular user 943 - const userHash = await clientHashPassword(TEST_USER.email, TEST_USER.password); 1145 + const userHash = await clientHashPassword( 1146 + TEST_USER.email, 1147 + TEST_USER.password, 1148 + ); 944 1149 const userResponse = await fetch(`${BASE_URL}/api/auth/register`, { 945 1150 method: "POST", 946 1151 headers: { "Content-Type": "application/json" }, ··· 950 1155 name: TEST_USER.name, 951 1156 }), 952 1157 }); 953 - userCookie = extractSessionCookie(userResponse)!; 1158 + userCookie = extractSessionCookie(userResponse); 954 1159 955 1160 // Get user ID 956 - const userIdResult = db.query<{ id: number }, [string]>( 957 - "SELECT id FROM users WHERE email = ?" 958 - ).get(TEST_USER.email); 959 - userId = userIdResult!.id; 1161 + const userIdResult = db 1162 + .query<{ id: number }, [string]>("SELECT id FROM users WHERE email = ?") 1163 + .get(TEST_USER.email); 1164 + userId = userIdResult?.id; 960 1165 }); 961 1166 962 1167 describe("GET /api/admin/users", () => { 963 1168 serverTest("should return all users for admin", async () => { 964 - const response = await authRequest(`${BASE_URL}/api/admin/users`, adminCookie); 1169 + const response = await authRequest( 1170 + `${BASE_URL}/api/admin/users`, 1171 + adminCookie, 1172 + ); 965 1173 966 1174 expect(response.status).toBe(200); 967 1175 const data = await response.json(); ··· 970 1178 }); 971 1179 972 1180 serverTest("should reject non-admin users", async () => { 973 - const response = await authRequest(`${BASE_URL}/api/admin/users`, userCookie); 1181 + const response = await authRequest( 1182 + `${BASE_URL}/api/admin/users`, 1183 + userCookie, 1184 + ); 974 1185 975 1186 expect(response.status).toBe(403); 976 1187 }); ··· 984 1195 985 1196 describe("GET /api/admin/transcriptions", () => { 986 1197 serverTest("should return all transcriptions for admin", async () => { 987 - const response = await authRequest(`${BASE_URL}/api/admin/transcriptions`, adminCookie); 1198 + const response = await authRequest( 1199 + `${BASE_URL}/api/admin/transcriptions`, 1200 + adminCookie, 1201 + ); 988 1202 989 1203 expect(response.status).toBe(200); 990 1204 const data = await response.json(); ··· 992 1206 }); 993 1207 994 1208 serverTest("should reject non-admin users", async () => { 995 - const response = await authRequest(`${BASE_URL}/api/admin/transcriptions`, userCookie); 1209 + const response = await authRequest( 1210 + `${BASE_URL}/api/admin/transcriptions`, 1211 + userCookie, 1212 + ); 996 1213 997 1214 expect(response.status).toBe(403); 998 1215 }); ··· 1005 1222 adminCookie, 1006 1223 { 1007 1224 method: "DELETE", 1008 - } 1225 + }, 1009 1226 ); 1010 1227 1011 1228 expect(response.status).toBe(200); ··· 1013 1230 expect(data.success).toBe(true); 1014 1231 1015 1232 // Verify user is deleted 1016 - const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, userCookie); 1233 + const verifyResponse = await authRequest( 1234 + `${BASE_URL}/api/auth/me`, 1235 + userCookie, 1236 + ); 1017 1237 expect(verifyResponse.status).toBe(401); 1018 1238 }); 1019 1239 ··· 1023 1243 userCookie, 1024 1244 { 1025 1245 method: "DELETE", 1026 - } 1246 + }, 1027 1247 ); 1028 1248 1029 1249 expect(response.status).toBe(403); ··· 1039 1259 method: "PUT", 1040 1260 headers: { "Content-Type": "application/json" }, 1041 1261 body: JSON.stringify({ role: "admin" }), 1042 - } 1262 + }, 1043 1263 ); 1044 1264 1045 1265 expect(response.status).toBe(200); ··· 1047 1267 expect(data.success).toBe(true); 1048 1268 1049 1269 // Verify role updated 1050 - const meResponse = await authRequest(`${BASE_URL}/api/auth/me`, userCookie); 1270 + const meResponse = await authRequest( 1271 + `${BASE_URL}/api/auth/me`, 1272 + userCookie, 1273 + ); 1051 1274 const meData = await meResponse.json(); 1052 1275 expect(meData.role).toBe("admin"); 1053 1276 }); ··· 1060 1283 method: "PUT", 1061 1284 headers: { "Content-Type": "application/json" }, 1062 1285 body: JSON.stringify({ role: "superadmin" }), 1063 - } 1286 + }, 1064 1287 ); 1065 1288 1066 1289 expect(response.status).toBe(400); ··· 1071 1294 serverTest("should return user details for admin", async () => { 1072 1295 const response = await authRequest( 1073 1296 `${BASE_URL}/api/admin/users/${userId}/details`, 1074 - adminCookie 1297 + adminCookie, 1075 1298 ); 1076 1299 1077 1300 expect(response.status).toBe(200); ··· 1085 1308 serverTest("should reject non-admin users", async () => { 1086 1309 const response = await authRequest( 1087 1310 `${BASE_URL}/api/admin/users/${userId}/details`, 1088 - userCookie 1311 + userCookie, 1089 1312 ); 1090 1313 1091 1314 expect(response.status).toBe(403); ··· 1102 1325 method: "PUT", 1103 1326 headers: { "Content-Type": "application/json" }, 1104 1327 body: JSON.stringify({ name: newName }), 1105 - } 1328 + }, 1106 1329 ); 1107 1330 1108 1331 expect(response.status).toBe(200); ··· 1118 1341 method: "PUT", 1119 1342 headers: { "Content-Type": "application/json" }, 1120 1343 body: JSON.stringify({ name: "" }), 1121 - } 1344 + }, 1122 1345 ); 1123 1346 1124 1347 expect(response.status).toBe(400); ··· 1135 1358 method: "PUT", 1136 1359 headers: { "Content-Type": "application/json" }, 1137 1360 body: JSON.stringify({ email: newEmail }), 1138 - } 1361 + }, 1139 1362 ); 1140 1363 1141 1364 expect(response.status).toBe(200); ··· 1151 1374 method: "PUT", 1152 1375 headers: { "Content-Type": "application/json" }, 1153 1376 body: JSON.stringify({ email: TEST_ADMIN.email }), 1154 - } 1377 + }, 1155 1378 ); 1156 1379 1157 1380 expect(response.status).toBe(400); ··· 1164 1387 serverTest("should return user sessions as admin", async () => { 1165 1388 const response = await authRequest( 1166 1389 `${BASE_URL}/api/admin/users/${userId}/sessions`, 1167 - adminCookie 1390 + adminCookie, 1168 1391 ); 1169 1392 1170 1393 expect(response.status).toBe(200); ··· 1180 1403 adminCookie, 1181 1404 { 1182 1405 method: "DELETE", 1183 - } 1406 + }, 1184 1407 ); 1185 1408 1186 1409 expect(response.status).toBe(200); ··· 1188 1411 expect(data.success).toBe(true); 1189 1412 1190 1413 // Verify sessions are deleted 1191 - const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, userCookie); 1414 + const verifyResponse = await authRequest( 1415 + `${BASE_URL}/api/auth/me`, 1416 + userCookie, 1417 + ); 1192 1418 expect(verifyResponse.status).toBe(401); 1193 1419 }); 1194 1420 }); ··· 1201 1427 if (!serverAvailable) return; 1202 1428 1203 1429 // Register user 1204 - const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 1430 + const hashedPassword = await clientHashPassword( 1431 + TEST_USER.email, 1432 + TEST_USER.password, 1433 + ); 1205 1434 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 1206 1435 method: "POST", 1207 1436 headers: { "Content-Type": "application/json" }, ··· 1210 1439 password: hashedPassword, 1211 1440 }), 1212 1441 }); 1213 - sessionCookie = extractSessionCookie(registerResponse)!; 1442 + sessionCookie = extractSessionCookie(registerResponse); 1214 1443 }); 1215 1444 1216 1445 describe("GET /api/passkeys", () => { 1217 1446 serverTest("should return user passkeys", async () => { 1218 - const response = await authRequest(`${BASE_URL}/api/passkeys`, sessionCookie); 1447 + const response = await authRequest( 1448 + `${BASE_URL}/api/passkeys`, 1449 + sessionCookie, 1450 + ); 1219 1451 1220 1452 expect(response.status).toBe(200); 1221 1453 const data = await response.json(); ··· 1231 1463 }); 1232 1464 1233 1465 describe("POST /api/passkeys/register/options", () => { 1234 - serverTest("should return registration options for authenticated user", async () => { 1235 - const response = await authRequest( 1466 + serverTest( 1467 + "should return registration options for authenticated user", 1468 + async () => { 1469 + const response = await authRequest( 1470 + `${BASE_URL}/api/passkeys/register/options`, 1471 + sessionCookie, 1472 + { 1473 + method: "POST", 1474 + }, 1475 + ); 1476 + 1477 + expect(response.status).toBe(200); 1478 + const data = await response.json(); 1479 + expect(data).toHaveProperty("challenge"); 1480 + expect(data).toHaveProperty("rp"); 1481 + expect(data).toHaveProperty("user"); 1482 + }, 1483 + ); 1484 + 1485 + serverTest("should require authentication", async () => { 1486 + const response = await fetch( 1236 1487 `${BASE_URL}/api/passkeys/register/options`, 1237 - sessionCookie, 1238 1488 { 1239 1489 method: "POST", 1240 - } 1490 + }, 1241 1491 ); 1242 1492 1243 - expect(response.status).toBe(200); 1244 - const data = await response.json(); 1245 - expect(data).toHaveProperty("challenge"); 1246 - expect(data).toHaveProperty("rp"); 1247 - expect(data).toHaveProperty("user"); 1248 - }); 1249 - 1250 - serverTest("should require authentication", async () => { 1251 - const response = await fetch(`${BASE_URL}/api/passkeys/register/options`, { 1252 - method: "POST", 1253 - }); 1254 - 1255 1493 expect(response.status).toBe(401); 1256 1494 }); 1257 1495 }); 1258 1496 1259 1497 describe("POST /api/passkeys/authenticate/options", () => { 1260 1498 serverTest("should return authentication options for email", async () => { 1261 - const response = await fetch(`${BASE_URL}/api/passkeys/authenticate/options`, { 1262 - method: "POST", 1263 - headers: { "Content-Type": "application/json" }, 1264 - body: JSON.stringify({ email: TEST_USER.email }), 1265 - }); 1499 + const response = await fetch( 1500 + `${BASE_URL}/api/passkeys/authenticate/options`, 1501 + { 1502 + method: "POST", 1503 + headers: { "Content-Type": "application/json" }, 1504 + body: JSON.stringify({ email: TEST_USER.email }), 1505 + }, 1506 + ); 1266 1507 1267 1508 expect(response.status).toBe(200); 1268 1509 const data = await response.json(); ··· 1270 1511 }); 1271 1512 1272 1513 serverTest("should handle non-existent email", async () => { 1273 - const response = await fetch(`${BASE_URL}/api/passkeys/authenticate/options`, { 1274 - method: "POST", 1275 - headers: { "Content-Type": "application/json" }, 1276 - body: JSON.stringify({ email: "nonexistent@example.com" }), 1277 - }); 1514 + const response = await fetch( 1515 + `${BASE_URL}/api/passkeys/authenticate/options`, 1516 + { 1517 + method: "POST", 1518 + headers: { "Content-Type": "application/json" }, 1519 + body: JSON.stringify({ email: "nonexistent@example.com" }), 1520 + }, 1521 + ); 1278 1522 1279 1523 // Should still return options for privacy (don't leak user existence) 1280 1524 expect([200, 404]).toContain(response.status);
+10 -5
src/index.ts
··· 26 26 updateUserRole, 27 27 } from "./lib/auth"; 28 28 import { 29 + addToWaitlist, 29 30 createClass, 30 31 createMeetingTime, 31 32 deleteClass, 32 33 deleteMeetingTime, 34 + deleteWaitlistEntry, 33 35 enrollUserInClass, 36 + getAllWaitlistEntries, 34 37 getClassById, 35 38 getClassesForUser, 36 39 getClassMembers, ··· 42 45 searchClassesByCourseCode, 43 46 toggleClassArchive, 44 47 updateMeetingTime, 45 - addToWaitlist, 46 - getAllWaitlistEntries, 47 - deleteWaitlistEntry, 48 48 } from "./lib/classes"; 49 49 import { handleError, ValidationErrors } from "./lib/errors"; 50 50 import { requireAdmin, requireAuth } from "./lib/middleware"; ··· 981 981 const formData = await req.formData(); 982 982 const file = formData.get("audio") as File; 983 983 const classId = formData.get("class_id") as string | null; 984 - const meetingTimeId = formData.get("meeting_time_id") as string | null; 984 + const meetingTimeId = formData.get("meeting_time_id") as 985 + | string 986 + | null; 985 987 986 988 if (!file) throw ValidationErrors.missingField("audio"); 987 989 ··· 1853 1855 .get(transcriptId); 1854 1856 1855 1857 if (transcription) { 1856 - whisperService.startTranscription(transcriptId, transcription.filename); 1858 + whisperService.startTranscription( 1859 + transcriptId, 1860 + transcription.filename, 1861 + ); 1857 1862 } 1858 1863 1859 1864 return Response.json({ success: true });
+11 -11
src/lib/classes.test.ts
··· 1 + import { Database } from "bun:sqlite"; 1 2 import { afterEach, beforeEach, expect, test } from "bun:test"; 2 3 import { unlinkSync } from "node:fs"; 3 4 import { 4 5 createClass, 5 6 createMeetingTime, 6 7 enrollUserInClass, 7 - getClassById, 8 8 getClassesForUser, 9 9 getMeetingTimesForClass, 10 - getTranscriptionsForClass, 11 10 isUserEnrolledInClass, 12 11 } from "./classes"; 13 - import { Database } from "bun:sqlite"; 14 12 15 13 const TEST_DB = "test-classes.db"; 16 14 let db: Database; ··· 110 108 const userId = db 111 109 .query<{ id: number }, []>("SELECT last_insert_rowid() as id") 112 110 .get()?.id; 111 + if (!userId) throw new Error("Failed to create user"); 113 112 114 113 // Create class 115 114 const cls = createClass({ ··· 121 120 }); 122 121 123 122 // Enroll user 124 - enrollUserInClass(userId!, cls.id); 123 + enrollUserInClass(userId, cls.id); 125 124 126 125 // Verify enrollment 127 - const isEnrolled = isUserEnrolledInClass(userId!, cls.id); 126 + const isEnrolled = isUserEnrolledInClass(userId, cls.id); 128 127 expect(isEnrolled).toBe(true); 129 128 }); 130 129 ··· 137 136 const userId = db 138 137 .query<{ id: number }, []>("SELECT last_insert_rowid() as id") 139 138 .get()?.id; 139 + if (!userId) throw new Error("Failed to create user"); 140 140 141 141 // Create two classes 142 142 const cls1 = createClass({ ··· 147 147 year: 2024, 148 148 }); 149 149 150 - const cls2 = createClass({ 150 + const _cls2 = createClass({ 151 151 course_code: "CS 102", 152 152 name: "Data Structures", 153 153 professor: "Dr. Jones", ··· 156 156 }); 157 157 158 158 // Enroll user in only one class 159 - enrollUserInClass(userId!, cls1.id); 159 + enrollUserInClass(userId, cls1.id); 160 160 161 161 // Get classes for user 162 - const classes = getClassesForUser(userId!, false); 162 + const classes = getClassesForUser(userId, false); 163 163 expect(classes.length).toBe(1); 164 164 expect(classes[0]?.id).toBe(cls1.id); 165 165 166 166 // Admin should see all 167 - const allClasses = getClassesForUser(userId!, true); 167 + const allClasses = getClassesForUser(userId, true); 168 168 expect(allClasses.length).toBe(2); 169 169 }); 170 170 ··· 177 177 year: 2024, 178 178 }); 179 179 180 - const meeting1 = createMeetingTime(cls.id, "Monday Lecture"); 181 - const meeting2 = createMeetingTime(cls.id, "Wednesday Lab"); 180 + const _meeting1 = createMeetingTime(cls.id, "Monday Lecture"); 181 + const _meeting2 = createMeetingTime(cls.id, "Wednesday Lab"); 182 182 183 183 const meetings = getMeetingTimesForClass(cls.id); 184 184 expect(meetings.length).toBe(2);
+1 -5
src/lib/classes.ts
··· 28 28 /** 29 29 * Get all classes for a user (either enrolled or admin sees all) 30 30 */ 31 - export function getClassesForUser( 32 - userId: number, 33 - isAdmin: boolean, 34 - ): Class[] { 31 + export function getClassesForUser(userId: number, isAdmin: boolean): Class[] { 35 32 if (isAdmin) { 36 33 return db 37 34 .query<Class, []>( ··· 377 374 export function deleteWaitlistEntry(id: string): void { 378 375 db.query("DELETE FROM class_waitlist WHERE id = ?").run(id); 379 376 } 380 -
+27 -27
tsconfig.json
··· 1 1 { 2 - "compilerOptions": { 3 - // Environment setup & latest features 4 - "lib": ["ESNext", "DOM", "DOM.Iterable"], 5 - "target": "ESNext", 6 - "module": "Preserve", 7 - "moduleDetection": "force", 8 - "jsx": "preserve", 9 - "allowJs": true, 2 + "compilerOptions": { 3 + // Environment setup & latest features 4 + "lib": ["ESNext", "DOM", "DOM.Iterable"], 5 + "target": "ESNext", 6 + "module": "Preserve", 7 + "moduleDetection": "force", 8 + "jsx": "preserve", 9 + "allowJs": true, 10 10 11 - // Bundler mode 12 - "moduleResolution": "bundler", 13 - "allowImportingTsExtensions": true, 14 - "verbatimModuleSyntax": true, 15 - "noEmit": true, 11 + // Bundler mode 12 + "moduleResolution": "bundler", 13 + "allowImportingTsExtensions": true, 14 + "verbatimModuleSyntax": true, 15 + "noEmit": true, 16 16 17 - // Decorators 18 - "experimentalDecorators": true, 19 - "useDefineForClassFields": false, 17 + // Decorators 18 + "experimentalDecorators": true, 19 + "useDefineForClassFields": false, 20 20 21 - // Best practices 22 - "strict": true, 23 - "skipLibCheck": true, 24 - "noFallthroughCasesInSwitch": true, 25 - "noUncheckedIndexedAccess": true, 26 - "noImplicitOverride": true, 21 + // Best practices 22 + "strict": true, 23 + "skipLibCheck": true, 24 + "noFallthroughCasesInSwitch": true, 25 + "noUncheckedIndexedAccess": true, 26 + "noImplicitOverride": true, 27 27 28 - // Some stricter flags (disabled by default) 29 - "noUnusedLocals": false, 30 - "noUnusedParameters": false, 31 - "noPropertyAccessFromIndexSignature": false 32 - } 28 + // Some stricter flags (disabled by default) 29 + "noUnusedLocals": false, 30 + "noUnusedParameters": false, 31 + "noPropertyAccessFromIndexSignature": false 32 + } 33 33 }