@omniradiology/omnirad 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +438 -0
  2. package/app/api/ai-config/route.ts +131 -0
  3. package/app/api/ai-config/test/route.ts +49 -0
  4. package/app/api/auth/auto-login/route.ts +66 -0
  5. package/app/api/auth/check/route.ts +17 -0
  6. package/app/api/auth/login/route.ts +72 -0
  7. package/app/api/auth/logout/route.ts +25 -0
  8. package/app/api/auth/me/route.ts +75 -0
  9. package/app/api/auth/password/route.ts +49 -0
  10. package/app/api/auth/setup/route.ts +63 -0
  11. package/app/api/auth/users/route.ts +100 -0
  12. package/app/api/auth/wipe/route.ts +27 -0
  13. package/app/api/compliance/anonymize/patient/[id]/route.ts +104 -0
  14. package/app/api/compliance/audit/route.ts +110 -0
  15. package/app/api/compliance/export/patient/[id]/route.ts +108 -0
  16. package/app/api/compliance/restrict/patient/[id]/route.ts +59 -0
  17. package/app/api/compliance/settings/route.ts +93 -0
  18. package/app/api/copilot/annotate/route.ts +94 -0
  19. package/app/api/copilot/chat/route.ts +238 -0
  20. package/app/api/copilot/history/route.ts +95 -0
  21. package/app/api/copilot/reports/route.ts +81 -0
  22. package/app/api/fhir/Bundle/report/[id]/route.ts +85 -0
  23. package/app/api/fhir/DiagnosticReport/[id]/route.ts +45 -0
  24. package/app/api/fhir/ImagingStudy/[id]/route.ts +57 -0
  25. package/app/api/fhir/Patient/[id]/route.ts +26 -0
  26. package/app/api/fhir/ServiceRequest/route.ts +85 -0
  27. package/app/api/fhir/config/route.ts +102 -0
  28. package/app/api/fhir/config/test-connection/route.ts +49 -0
  29. package/app/api/fhir/metadata/route.ts +51 -0
  30. package/app/api/pacs/metadata/route.ts +32 -0
  31. package/app/api/pacs/qido/instances/route.ts +39 -0
  32. package/app/api/pacs/qido/series/route.ts +38 -0
  33. package/app/api/pacs/qido/studies/route.ts +37 -0
  34. package/app/api/pacs/test/route.ts +30 -0
  35. package/app/api/pacs/wado/render/route.ts +51 -0
  36. package/app/api/patients/[id]/reports/route.ts +18 -0
  37. package/app/api/patients/[id]/route.ts +43 -0
  38. package/app/api/patients/merge/route.ts +57 -0
  39. package/app/api/patients/route.ts +67 -0
  40. package/app/api/patients/search/route.ts +25 -0
  41. package/app/api/reports/[id]/route.ts +84 -0
  42. package/app/api/reports/[id]/status/route.ts +87 -0
  43. package/app/api/reports/clear/route.ts +16 -0
  44. package/app/api/reports/route.ts +112 -0
  45. package/app/api/segmentation-config/route.ts +238 -0
  46. package/app/api/settings/route.ts +245 -0
  47. package/app/api/settings/test-supabase/route.ts +103 -0
  48. package/app/api/upload/route.ts +48 -0
  49. package/app/copilot/page.tsx +30 -0
  50. package/app/globals.css +141 -0
  51. package/app/history/page.tsx +242 -0
  52. package/app/icon.svg +3 -0
  53. package/app/layout.tsx +47 -0
  54. package/app/login/page.tsx +175 -0
  55. package/app/pacs/page.tsx +78 -0
  56. package/app/page.tsx +125 -0
  57. package/app/patients/[id]/page.tsx +315 -0
  58. package/app/patients/page.tsx +110 -0
  59. package/app/profile/page.tsx +208 -0
  60. package/app/reports/page.tsx +432 -0
  61. package/app/settings/page.tsx +454 -0
  62. package/app/setup/page.tsx +199 -0
  63. package/components/admin/AuditLogTable.tsx +293 -0
  64. package/components/copilot/ActivityIndicator.tsx +215 -0
  65. package/components/copilot/ChatHistoryPanel.tsx +140 -0
  66. package/components/copilot/ChatMessage.tsx +251 -0
  67. package/components/copilot/ClickableReference.tsx +40 -0
  68. package/components/copilot/CopilotCornerstoneViewer.tsx +562 -0
  69. package/components/copilot/CopilotPanel.tsx +311 -0
  70. package/components/copilot/FindingsList.tsx +75 -0
  71. package/components/copilot/ViewerPanel.tsx +460 -0
  72. package/components/copilot/WorkspaceLayout.tsx +398 -0
  73. package/components/dashboard/AIConfigPanel.tsx +339 -0
  74. package/components/dashboard/AppearancePanel.tsx +491 -0
  75. package/components/dashboard/ApprovalModal.tsx +163 -0
  76. package/components/dashboard/CollaborationPanel.tsx +134 -0
  77. package/components/dashboard/CopilotConfigPanel.tsx +337 -0
  78. package/components/dashboard/DicomViewer.tsx +645 -0
  79. package/components/dashboard/FhirIntegrationPanel.tsx +331 -0
  80. package/components/dashboard/FullReportOverlay.tsx +269 -0
  81. package/components/dashboard/ImageViewer.tsx +541 -0
  82. package/components/dashboard/PatientForm.tsx +597 -0
  83. package/components/dashboard/RejectionModal.tsx +74 -0
  84. package/components/dashboard/ReportEditor.tsx +160 -0
  85. package/components/dashboard/ReportTemplates.tsx +729 -0
  86. package/components/dashboard/ReportView.tsx +539 -0
  87. package/components/dashboard/SegmentationConfigPanel.tsx +490 -0
  88. package/components/dashboard/StudyPlaceholder.tsx +17 -0
  89. package/components/dashboard/SupabaseIntegrationPanel.tsx +345 -0
  90. package/components/dashboard/UserManagementPanel.tsx +272 -0
  91. package/components/layout/ClientLayout.tsx +39 -0
  92. package/components/layout/Header.tsx +20 -0
  93. package/components/layout/Sidebar.tsx +119 -0
  94. package/components/pacs/PacsImageViewerModal.tsx +121 -0
  95. package/components/pacs/PacsSearchFilters.tsx +117 -0
  96. package/components/pacs/PacsSeriesViewer.tsx +190 -0
  97. package/components/pacs/PacsStudyTable.tsx +113 -0
  98. package/components/patients/patient-card.tsx +117 -0
  99. package/components/patients/patient-header.tsx +122 -0
  100. package/components/patients/patient-search.tsx +137 -0
  101. package/components/patients/patient-timeline.tsx +153 -0
  102. package/components/settings/ComplianceSettingsPanel.tsx +278 -0
  103. package/components/settings/SecurityPanel.tsx +418 -0
  104. package/components/ui/badge.tsx +19 -0
  105. package/components/ui/basic.tsx +156 -0
  106. package/db/index.ts +350 -0
  107. package/db/migrations/0000_odd_quasimodo.sql +117 -0
  108. package/db/migrations/meta/0000_snapshot.json +778 -0
  109. package/db/migrations/meta/_journal.json +13 -0
  110. package/db/schema.ts +239 -0
  111. package/drizzle.config.ts +10 -0
  112. package/lib/api.ts +689 -0
  113. package/lib/auth.ts +22 -0
  114. package/lib/copilot/action-executor.ts +94 -0
  115. package/lib/copilot/action-types.ts +72 -0
  116. package/lib/copilot/coordinate-mapper.ts +84 -0
  117. package/lib/dicomImageExtractor.ts +103 -0
  118. package/lib/dicomMetadataParser.ts +111 -0
  119. package/lib/fhir/client.ts +25 -0
  120. package/lib/fhir/constants.ts +21 -0
  121. package/lib/fhir/diagnostic-report.ts +88 -0
  122. package/lib/fhir/helpers.ts +73 -0
  123. package/lib/fhir/imaging-study.ts +49 -0
  124. package/lib/fhir/patient.ts +55 -0
  125. package/lib/fhir/service-request.ts +85 -0
  126. package/lib/fhir.ts +6 -0
  127. package/lib/pacs/dicom-utils.ts +72 -0
  128. package/lib/pacs/dicomweb.ts +72 -0
  129. package/lib/pacs/server-utils.ts +37 -0
  130. package/lib/patients.ts +25 -0
  131. package/lib/pdfHelper.ts +119 -0
  132. package/lib/reportHtmlGenerator.ts +581 -0
  133. package/lib/security/audit.ts +180 -0
  134. package/lib/security/authz.ts +246 -0
  135. package/lib/security/phi-redaction.ts +156 -0
  136. package/lib/security/rate-limit.ts +106 -0
  137. package/lib/security/secrets.ts +179 -0
  138. package/lib/supabase.ts +72 -0
  139. package/lib/utils.ts +6 -0
  140. package/next.config.ts +35 -0
  141. package/package.json +76 -0
  142. package/public/file.svg +1 -0
  143. package/public/globe.svg +1 -0
  144. package/public/logo.svg +8 -0
  145. package/public/next.svg +1 -0
  146. package/public/omnirad-favicon.svg +8 -0
  147. package/public/vercel.svg +1 -0
  148. package/public/window.svg +1 -0
  149. package/tsconfig.json +34 -0
  150. package/types/copilot-viewer.ts +155 -0
  151. package/types/copilot.ts +105 -0
  152. package/types/fhir.ts +21 -0
  153. package/types/html2pdf.d.ts +20 -0
  154. package/types/index.ts +139 -0
  155. package/types/pacs.ts +41 -0
@@ -0,0 +1,17 @@
1
+ import { NextResponse } from "next/server";
2
+ import { db } from "@/db";
3
+ import { users } from "@/db/schema";
4
+ import { count } from "drizzle-orm";
5
+
6
+ export const dynamic = 'force-dynamic';
7
+
8
+ export async function GET() {
9
+ try {
10
+ const usersCountResult = await db.select({ value: count() }).from(users);
11
+ const numUsers = usersCountResult[0].value;
12
+ return NextResponse.json({ hasUsers: numUsers > 0 });
13
+ } catch (error) {
14
+ console.error("[Auth Check] Error checking database:", error);
15
+ return NextResponse.json({ hasUsers: false }, { status: 500 });
16
+ }
17
+ }
@@ -0,0 +1,72 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { db } from '@/db';
3
+ import { users, sessions } from '@/db/schema';
4
+ import { eq, or } from 'drizzle-orm';
5
+ import { verifyPassword, generateSessionId } from '@/lib/auth';
6
+ import { cookies } from 'next/headers';
7
+
8
+ export async function POST(req: Request) {
9
+ try {
10
+ const { identifier, password, rememberMe } = await req.json();
11
+
12
+ if (!identifier || !password) {
13
+ return NextResponse.json({ error: 'Username/Email and password are required' }, { status: 400 });
14
+ }
15
+
16
+ // Find user by either email or username
17
+ const foundUsers = await db.select().from(users).where(
18
+ or(
19
+ eq(users.email, identifier),
20
+ eq(users.username, identifier)
21
+ )
22
+ ).limit(1);
23
+
24
+ const user = foundUsers[0];
25
+
26
+ if (!user) {
27
+ return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
28
+ }
29
+
30
+ // Verify hash
31
+ const isValid = await verifyPassword(password, user.passwordHash);
32
+ if (!isValid) {
33
+ return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
34
+ }
35
+
36
+ // Create Session
37
+ const sessionId = generateSessionId();
38
+
39
+ // Expiration: 30 days if rememberMe, otherwise 24 hours
40
+ const expiresInSeconds = rememberMe ? (60 * 60 * 24 * 30) : (60 * 60 * 24);
41
+ const expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;
42
+
43
+ await db.insert(sessions).values({
44
+ id: sessionId,
45
+ userId: user.id,
46
+ expiresAt
47
+ });
48
+
49
+ // Set session cookie
50
+ const cookieStore = await cookies();
51
+ cookieStore.set('omnirad_session_id', sessionId, {
52
+ httpOnly: true,
53
+ secure: process.env.NODE_ENV === 'production',
54
+ sameSite: 'lax',
55
+ path: '/',
56
+ maxAge: expiresInSeconds
57
+ });
58
+
59
+ return NextResponse.json({
60
+ success: true,
61
+ user: {
62
+ id: user.id,
63
+ fullName: user.fullName,
64
+ username: user.username,
65
+ role: user.role
66
+ }
67
+ });
68
+ } catch (e: any) {
69
+ console.error("Login Error", e);
70
+ return NextResponse.json({ error: 'An unexpected error occurred' }, { status: 500 });
71
+ }
72
+ }
@@ -0,0 +1,25 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { db } from '@/db';
3
+ import { sessions } from '@/db/schema';
4
+ import { eq } from 'drizzle-orm';
5
+ import { cookies } from 'next/headers';
6
+
7
+ export async function POST(req: Request) {
8
+ try {
9
+ const cookieStore = await cookies();
10
+ const sessionId = cookieStore.get('omnirad_session_id')?.value;
11
+
12
+ if (sessionId) {
13
+ // Remove session from DB
14
+ await db.delete(sessions).where(eq(sessions.id, sessionId));
15
+ }
16
+
17
+ // Clear the cookie
18
+ cookieStore.delete('omnirad_session_id');
19
+
20
+ return NextResponse.json({ success: true });
21
+ } catch (e: any) {
22
+ console.error("Logout Error", e);
23
+ return NextResponse.json({ error: 'An unexpected error occurred' }, { status: 500 });
24
+ }
25
+ }
@@ -0,0 +1,75 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { db } from '@/db';
3
+ import { users, sessions } from '@/db/schema';
4
+ import { eq } from 'drizzle-orm';
5
+ import { cookies } from 'next/headers';
6
+
7
+ async function getCurrentUser() {
8
+ const cookieStore = await cookies();
9
+ const sessionId = cookieStore.get('omnirad_session_id')?.value;
10
+ if (!sessionId) return null;
11
+
12
+ const sessionList = await db.select().from(sessions).where(eq(sessions.id, sessionId)).limit(1);
13
+ const session = sessionList[0];
14
+
15
+ if (!session || session.expiresAt * 1000 < Date.now()) return null;
16
+
17
+ const userList = await db.select().from(users).where(eq(users.id, session.userId)).limit(1);
18
+ return userList[0];
19
+ }
20
+
21
+ export async function GET() {
22
+ try {
23
+ const user = await getCurrentUser();
24
+ if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
25
+
26
+ return NextResponse.json({
27
+ id: user.id,
28
+ fullName: user.fullName,
29
+ username: user.username,
30
+ email: user.email,
31
+ role: user.role,
32
+ position: user.position || ""
33
+ });
34
+ } catch (e) {
35
+ return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
36
+ }
37
+ }
38
+
39
+ export async function PUT(req: Request) {
40
+ try {
41
+ const user = await getCurrentUser();
42
+ if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
43
+
44
+ const body = await req.json();
45
+ const { fullName, username, email, position } = body;
46
+
47
+ // Ensure unique username/email
48
+ const existingUsers = await db.select().from(users).where(
49
+ eq(users.username, username)
50
+ );
51
+
52
+ if (existingUsers.length > 0 && existingUsers[0].id !== user.id) {
53
+ return NextResponse.json({ error: 'Username is already taken' }, { status: 400 });
54
+ }
55
+
56
+ const existingEmails = await db.select().from(users).where(
57
+ eq(users.email, email)
58
+ );
59
+
60
+ if (existingEmails.length > 0 && existingEmails[0].id !== user.id) {
61
+ return NextResponse.json({ error: 'Email is already correctly registered' }, { status: 400 });
62
+ }
63
+
64
+ await db.update(users).set({
65
+ fullName,
66
+ username,
67
+ email,
68
+ position
69
+ }).where(eq(users.id, user.id));
70
+
71
+ return NextResponse.json({ success: true });
72
+ } catch (e: any) {
73
+ return NextResponse.json({ error: e.message || 'Internal Server Error' }, { status: 500 });
74
+ }
75
+ }
@@ -0,0 +1,49 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { db } from '@/db';
3
+ import { users, sessions } from '@/db/schema';
4
+ import { eq } from 'drizzle-orm';
5
+ import { cookies } from 'next/headers';
6
+ import { verifyPassword, hashPassword } from '@/lib/auth';
7
+
8
+ async function getCurrentUser() {
9
+ const cookieStore = await cookies();
10
+ const sessionId = cookieStore.get('omnirad_session_id')?.value;
11
+ if (!sessionId) return null;
12
+
13
+ const sessionList = await db.select().from(sessions).where(eq(sessions.id, sessionId)).limit(1);
14
+ const session = sessionList[0];
15
+
16
+ if (!session || session.expiresAt * 1000 < Date.now()) return null;
17
+
18
+ const userList = await db.select().from(users).where(eq(users.id, session.userId)).limit(1);
19
+ return userList[0];
20
+ }
21
+
22
+ export async function PUT(req: Request) {
23
+ try {
24
+ const user = await getCurrentUser();
25
+ if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
26
+
27
+ const { currentPassword, newPassword } = await req.json();
28
+
29
+ if (newPassword.length < 8) {
30
+ return NextResponse.json({ error: 'New password must be at least 8 characters' }, { status: 400 });
31
+ }
32
+
33
+ const isValid = await verifyPassword(currentPassword, user.passwordHash);
34
+ if (!isValid) {
35
+ return NextResponse.json({ error: 'Incorrect current password' }, { status: 401 });
36
+ }
37
+
38
+ const passwordHash = await hashPassword(newPassword);
39
+
40
+ await db.update(users).set({
41
+ passwordHash
42
+ }).where(eq(users.id, user.id));
43
+
44
+ return NextResponse.json({ success: true });
45
+
46
+ } catch (e: any) {
47
+ return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
48
+ }
49
+ }
@@ -0,0 +1,63 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { db } from '@/db';
3
+ import { users, sessions } from '@/db/schema';
4
+ import { count } from 'drizzle-orm';
5
+ import { hashPassword, generateSessionId } from '@/lib/auth';
6
+ import { cookies } from 'next/headers';
7
+
8
+ export async function POST(req: Request) {
9
+ try {
10
+ const { fullName, username, email, password } = await req.json();
11
+
12
+ // 1. Verify there are truly no users (security check)
13
+ const usersCount = await db.select({ value: count() }).from(users);
14
+ if (usersCount[0].value > 0) {
15
+ return NextResponse.json({ error: 'Setup has already been completed.' }, { status: 403 });
16
+ }
17
+
18
+ // 2. Hash password
19
+ const passwordHash = await hashPassword(password);
20
+ const userId = crypto.randomUUID();
21
+
22
+ // 3. Create Admin user
23
+ await db.insert(users).values({
24
+ id: userId,
25
+ fullName,
26
+ username,
27
+ email,
28
+ passwordHash,
29
+ role: 'Admin',
30
+ createdAt: new Date().toISOString()
31
+ });
32
+
33
+ // 4. Create Session
34
+ const sessionId = generateSessionId();
35
+ const expiresAt = Math.floor(Date.now() / 1000) + (60 * 60 * 24 * 7); // 7 days
36
+
37
+ await db.insert(sessions).values({
38
+ id: sessionId,
39
+ userId,
40
+ expiresAt
41
+ });
42
+
43
+ // 5. Set session cookie and setup complete cookie
44
+ const cookieStore = await cookies();
45
+ cookieStore.set('omnirad_session_id', sessionId, {
46
+ httpOnly: true,
47
+ secure: process.env.NODE_ENV === 'production',
48
+ sameSite: 'lax',
49
+ path: '/',
50
+ maxAge: 60 * 60 * 24 * 7 // 7 days
51
+ });
52
+
53
+ cookieStore.set('omnirad_setup_complete', 'true', {
54
+ path: '/',
55
+ maxAge: 60 * 60 * 24 * 365 * 10 // 10 years
56
+ });
57
+
58
+ return NextResponse.json({ success: true });
59
+ } catch (e: any) {
60
+ console.error("Setup Error", e);
61
+ return NextResponse.json({ error: e.message || 'Error creating admin account.' }, { status: 500 });
62
+ }
63
+ }
@@ -0,0 +1,100 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { db } from '@/db';
3
+ import { users, sessions } from '@/db/schema';
4
+ import { eq, not } from 'drizzle-orm';
5
+ import { hashPassword } from '@/lib/auth';
6
+ import { cookies } from 'next/headers';
7
+
8
+ async function checkIsAdmin() {
9
+ const cookieStore = await cookies();
10
+ const sessionId = cookieStore.get('omnirad_session_id')?.value;
11
+ if (!sessionId) return false;
12
+
13
+ const sessionList = await db.select().from(sessions).where(eq(sessions.id, sessionId)).limit(1);
14
+ const session = sessionList[0];
15
+ if (!session || session.expiresAt * 1000 < Date.now()) return false;
16
+
17
+ const userList = await db.select().from(users).where(eq(users.id, session.userId)).limit(1);
18
+ if (!userList[0] || userList[0].role !== 'Admin') return false;
19
+
20
+ return true;
21
+ }
22
+
23
+ export async function GET() {
24
+ if (!await checkIsAdmin()) return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
25
+
26
+ try {
27
+ const allUsers = await db.select({
28
+ id: users.id,
29
+ fullName: users.fullName,
30
+ username: users.username,
31
+ email: users.email,
32
+ role: users.role,
33
+ createdAt: users.createdAt
34
+ }).from(users);
35
+
36
+ return NextResponse.json(allUsers);
37
+ } catch (e: any) {
38
+ return NextResponse.json({ error: e.message }, { status: 500 });
39
+ }
40
+ }
41
+
42
+ export async function POST(req: Request) {
43
+ if (!await checkIsAdmin()) return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
44
+
45
+ try {
46
+ const { fullName, username, email, password, role } = await req.json();
47
+
48
+ // Ensure unique username/email
49
+ const existingUsers = await db.select().from(users).where(eq(users.username, username));
50
+ if (existingUsers.length > 0) return NextResponse.json({ error: 'Username is taken' }, { status: 400 });
51
+
52
+ const passwordHash = await hashPassword(password);
53
+ await db.insert(users).values({
54
+ id: crypto.randomUUID(),
55
+ fullName,
56
+ username,
57
+ email,
58
+ passwordHash,
59
+ role: role || 'User',
60
+ createdAt: new Date().toISOString()
61
+ });
62
+
63
+ return NextResponse.json({ success: true });
64
+ } catch (e: any) {
65
+ return NextResponse.json({ error: e.message }, { status: 500 });
66
+ }
67
+ }
68
+
69
+ export async function PUT(req: Request) {
70
+ if (!await checkIsAdmin()) return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
71
+
72
+ try {
73
+ const { id, role } = await req.json();
74
+
75
+ await db.update(users).set({ role }).where(eq(users.id, id));
76
+
77
+ return NextResponse.json({ success: true });
78
+ } catch (e: any) {
79
+ return NextResponse.json({ error: e.message }, { status: 500 });
80
+ }
81
+ }
82
+
83
+ export async function DELETE(req: Request) {
84
+ if (!await checkIsAdmin()) return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
85
+
86
+ try {
87
+ const url = new URL(req.url);
88
+ const id = url.searchParams.get('id');
89
+ if (!id) return NextResponse.json({ error: 'ID is required' }, { status: 400 });
90
+
91
+ // First remove all their sessions
92
+ await db.delete(sessions).where(eq(sessions.userId, id));
93
+ // Then delete the user
94
+ await db.delete(users).where(eq(users.id, id));
95
+
96
+ return NextResponse.json({ success: true });
97
+ } catch (e: any) {
98
+ return NextResponse.json({ error: e.message }, { status: 500 });
99
+ }
100
+ }
@@ -0,0 +1,27 @@
1
+ import { NextResponse } from "next/server";
2
+ import { db } from "@/db";
3
+ import { users, sessions, reports, config, profile, appearance } from "@/db/schema";
4
+ import { cookies } from "next/headers";
5
+
6
+ // DELETE /api/auth/wipe — Wipe ALL data and reset application to factory state
7
+ export async function DELETE() {
8
+ try {
9
+ // Wipe everything in order (sessions first due to FK)
10
+ db.delete(sessions).run();
11
+ db.delete(reports).run();
12
+ db.delete(users).run();
13
+ db.delete(config).run();
14
+ db.delete(profile).run();
15
+ db.delete(appearance).run();
16
+
17
+ // Clear all auth cookies
18
+ const cookieStore = await cookies();
19
+ cookieStore.delete('omnirad_session_id');
20
+ cookieStore.delete('omnirad_setup_complete');
21
+
22
+ return NextResponse.json({ success: true });
23
+ } catch (error) {
24
+ console.error("[API] Error wiping all data:", error);
25
+ return NextResponse.json({ error: "Failed to wipe data" }, { status: 500 });
26
+ }
27
+ }
@@ -0,0 +1,104 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db, sqlite } from "@/db";
3
+ import { patients, reports } from "@/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+ import { requireUser, requirePermission, handleAuthError } from "@/lib/security/authz";
6
+ import { safeError } from "@/lib/security/phi-redaction";
7
+ import { auditSuccess, auditEventFromContext } from "@/lib/security/audit";
8
+ import { randomUUID } from "crypto";
9
+
10
+ // POST /api/compliance/anonymize/patient/[id] — GDPR Article 17: Right to Erasure (Anonymization)
11
+ export async function POST(
12
+ request: NextRequest,
13
+ { params }: { params: Promise<{ id: string }> }
14
+ ) {
15
+ try {
16
+ const ctx = await requireUser(request);
17
+ requirePermission(ctx, "compliance.manage");
18
+
19
+ const { id } = await params;
20
+
21
+ // Validate patient exists
22
+ const patient = await db.select().from(patients).where(eq(patients.id, id)).limit(1);
23
+ if (patient.length === 0) {
24
+ return NextResponse.json({ error: "Patient not found" }, { status: 404 });
25
+ }
26
+
27
+ const now = new Date().toISOString();
28
+ const anonymizedName = `ANONYMIZED_${Date.now()}`;
29
+
30
+ // Anonymize patient record — replace PII with placeholder values
31
+ db.update(patients).set({
32
+ patientName: anonymizedName,
33
+ patientIdNumber: null,
34
+ dob: null,
35
+ age: null,
36
+ gender: null,
37
+ mobile: null,
38
+ address: null,
39
+ contactInfo: null,
40
+ notes: null,
41
+ updatedAt: now,
42
+ }).where(eq(patients.id, id)).run();
43
+
44
+ // Anonymize reports — strip patient-identifying info from report_data JSON
45
+ const patientReports = sqlite.prepare(
46
+ "SELECT id, report_data FROM reports WHERE patient_id = ?"
47
+ ).all(id) as any[];
48
+
49
+ for (const r of patientReports) {
50
+ try {
51
+ const reportData = JSON.parse(r.report_data);
52
+ // Strip patient section
53
+ if (reportData.patient) {
54
+ reportData.patient = {
55
+ name: anonymizedName,
56
+ patient_id: null,
57
+ dob: null,
58
+ age: null,
59
+ gender: null,
60
+ };
61
+ }
62
+ // Strip image data
63
+ delete reportData.image_data;
64
+
65
+ sqlite.prepare(
66
+ "UPDATE reports SET patient_name = ?, report_data = ?, image_data = NULL WHERE id = ?"
67
+ ).run(anonymizedName, JSON.stringify(reportData), r.id);
68
+ } catch { /* skip reports that fail to parse */ }
69
+ }
70
+
71
+ // Update privacy controls
72
+ const existing = sqlite.prepare(
73
+ "SELECT id FROM patient_privacy_controls WHERE patient_id = ?"
74
+ ).get(id) as any;
75
+
76
+ if (existing) {
77
+ sqlite.prepare(`
78
+ UPDATE patient_privacy_controls
79
+ SET restriction = 'anonymized', anonymized_at = ?, anonymized_by = ?, updated_at = ?
80
+ WHERE patient_id = ?
81
+ `).run(now, ctx.userId, now, id);
82
+ } else {
83
+ sqlite.prepare(`
84
+ INSERT INTO patient_privacy_controls (id, patient_id, restriction, anonymized_at, anonymized_by, created_at, updated_at)
85
+ VALUES (?, ?, 'anonymized', ?, ?, ?, ?)
86
+ `).run(randomUUID(), id, now, ctx.userId, now, now);
87
+ }
88
+
89
+ await auditSuccess(auditEventFromContext(ctx, "patient.anonymize", "patient", {
90
+ resourceId: id,
91
+ patientId: id,
92
+ metadata: { count: patientReports.length },
93
+ }));
94
+
95
+ return NextResponse.json({
96
+ success: true,
97
+ anonymizedReports: patientReports.length,
98
+ });
99
+ } catch (error) {
100
+ if ((error as any)?.statusCode) return handleAuthError(error);
101
+ safeError("Error anonymizing patient:", error);
102
+ return NextResponse.json({ error: "Failed to anonymize patient" }, { status: 500 });
103
+ }
104
+ }
@@ -0,0 +1,110 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { sqlite } from "@/db";
3
+ import { requireUser, requireRole, requirePermission, handleAuthError } from "@/lib/security/authz";
4
+ import { safeError } from "@/lib/security/phi-redaction";
5
+
6
+ // GET /api/compliance/audit — Query audit logs (Admin only)
7
+ export async function GET(request: NextRequest) {
8
+ try {
9
+ const ctx = await requireUser(request);
10
+ requireRole(ctx, ["Admin"]);
11
+
12
+ const { searchParams } = new URL(request.url);
13
+ const page = parseInt(searchParams.get("page") || "1");
14
+ const limit = Math.min(parseInt(searchParams.get("limit") || "50"), 200);
15
+ const action = searchParams.get("action");
16
+ const resourceType = searchParams.get("resourceType");
17
+ const actorUserId = searchParams.get("actorUserId");
18
+ const patientId = searchParams.get("patientId");
19
+ const startDate = searchParams.get("startDate");
20
+ const endDate = searchParams.get("endDate");
21
+ const success = searchParams.get("success");
22
+
23
+ // Build dynamic query
24
+ const conditions: string[] = [];
25
+ const params: any[] = [];
26
+
27
+ if (action) {
28
+ conditions.push("action LIKE ?");
29
+ params.push(`%${action}%`);
30
+ }
31
+ if (resourceType) {
32
+ conditions.push("resource_type = ?");
33
+ params.push(resourceType);
34
+ }
35
+ if (actorUserId) {
36
+ conditions.push("actor_user_id = ?");
37
+ params.push(actorUserId);
38
+ }
39
+ if (patientId) {
40
+ conditions.push("patient_id = ?");
41
+ params.push(patientId);
42
+ }
43
+ if (startDate) {
44
+ conditions.push("created_at >= ?");
45
+ params.push(startDate);
46
+ }
47
+ if (endDate) {
48
+ conditions.push("created_at <= ?");
49
+ params.push(endDate);
50
+ }
51
+ if (success !== null && success !== undefined) {
52
+ conditions.push("success = ?");
53
+ params.push(success === "true" ? 1 : 0);
54
+ }
55
+
56
+ const whereClause = conditions.length > 0
57
+ ? `WHERE ${conditions.join(" AND ")}`
58
+ : "";
59
+
60
+ // Count total
61
+ const countRow = sqlite.prepare(`SELECT COUNT(*) as total FROM audit_logs ${whereClause}`).get(...params) as any;
62
+ const total = countRow?.total || 0;
63
+
64
+ // Fetch page
65
+ const offset = (page - 1) * limit;
66
+ const rows = sqlite.prepare(
67
+ `SELECT * FROM audit_logs ${whereClause} ORDER BY created_at DESC LIMIT ? OFFSET ?`
68
+ ).all(...params, limit, offset) as any[];
69
+
70
+ // Enrich with user names
71
+ const enriched = rows.map((row: any) => {
72
+ let actorName = "System";
73
+ if (row.actor_user_id) {
74
+ const user = sqlite.prepare("SELECT full_name, username FROM users WHERE id = ?").get(row.actor_user_id) as any;
75
+ actorName = user?.full_name || user?.username || row.actor_user_id;
76
+ }
77
+
78
+ return {
79
+ id: row.id,
80
+ actorUserId: row.actor_user_id,
81
+ actorName,
82
+ actorRole: row.actor_role,
83
+ actorType: row.actor_type,
84
+ action: row.action,
85
+ resourceType: row.resource_type,
86
+ resourceId: row.resource_id,
87
+ patientId: row.patient_id,
88
+ ipAddress: row.ip_address,
89
+ success: !!row.success,
90
+ reason: row.reason,
91
+ metadata: row.metadata_json ? JSON.parse(row.metadata_json) : null,
92
+ createdAt: row.created_at,
93
+ };
94
+ });
95
+
96
+ return NextResponse.json({
97
+ data: enriched,
98
+ pagination: {
99
+ page,
100
+ limit,
101
+ total,
102
+ totalPages: Math.ceil(total / limit),
103
+ },
104
+ });
105
+ } catch (error) {
106
+ if ((error as any)?.statusCode) return handleAuthError(error);
107
+ safeError("Error fetching audit logs:", error);
108
+ return NextResponse.json({ error: "Failed to fetch audit logs" }, { status: 500 });
109
+ }
110
+ }