From 3a955a0568eca0eab362b6a4949903aeeeb24212 Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Tue, 3 Dec 2024 15:42:43 -0500 Subject: [PATCH] feat(api): Enhance study creation and user management with improved error handling and logging - Updated the study creation endpoint to return JSON responses for errors and success. - Added user existence verification before creating a study. - Enhanced error logging for better debugging. - Refactored user creation in webhooks to use transactions for atomicity. - Improved response structure for webhook processing. - Updated the seed script to streamline role and permission insertion. - Added new permissions for editing studies and participants. --- src/app/api/studies/route.ts | 62 ++++++-- src/app/api/webhooks/clerk/route.ts | 86 ++++++----- src/app/dashboard/studies/page.tsx | 13 ++ src/db/index.ts | 9 +- src/db/seed.ts | 222 ++++++++++++---------------- src/lib/permissions.ts | 12 ++ src/lib/roles.ts | 70 +++++++++ 7 files changed, 291 insertions(+), 183 deletions(-) create mode 100644 src/lib/roles.ts diff --git a/src/app/api/studies/route.ts b/src/app/api/studies/route.ts index 2f10dcd..be6a97d 100644 --- a/src/app/api/studies/route.ts +++ b/src/app/api/studies/route.ts @@ -2,7 +2,7 @@ import { eq } from "drizzle-orm"; import { NextResponse } from "next/server"; import { auth } from "@clerk/nextjs/server"; import { db } from "~/db"; -import { studyTable } from "~/db/schema"; +import { studyTable, usersTable } from "~/db/schema"; export async function GET() { const { userId } = await auth(); @@ -23,19 +23,59 @@ export async function POST(request: Request) { const { userId } = await auth(); if (!userId) { - return new NextResponse("Unauthorized", { status: 401 }); + return new Response(JSON.stringify({ error: "Unauthorized" }), { + status: 401, + headers: { 'Content-Type': 'application/json' } + }); } - const { title, description } = await request.json(); + try { + // Debug log + console.log("Creating study for user:", userId); - const study = await db - .insert(studyTable) - .values({ - title, - description, + // Verify user exists first + const existingUser = await db + .select() + .from(usersTable) + .where(eq(usersTable.id, userId)); + + console.log("Found user:", existingUser[0]); // Debug log + + const { title, description } = await request.json(); + + // Debug log + console.log("Study data:", { title, description, userId }); + + const study = await db + .insert(studyTable) + .values({ + title, + description, + userId: userId, // Explicitly use the userId from auth + }) + .returning(); + + console.log("Created study:", study[0]); // Debug log + + return new Response(JSON.stringify(study[0]), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } catch (error) { + // Enhanced error logging + console.error("Error details:", { + error, userId, - }) - .returning(); + errorMessage: error instanceof Error ? error.message : 'Unknown error', + errorName: error instanceof Error ? error.name : 'Unknown error type' + }); - return NextResponse.json(study[0]); + return new Response(JSON.stringify({ + error: "Failed to create study", + details: error instanceof Error ? error.message : 'Unknown error' + }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } } diff --git a/src/app/api/webhooks/clerk/route.ts b/src/app/api/webhooks/clerk/route.ts index deccfef..3c7cff0 100644 --- a/src/app/api/webhooks/clerk/route.ts +++ b/src/app/api/webhooks/clerk/route.ts @@ -59,59 +59,57 @@ export async function POST(req: Request) { // Combine first and last name const fullName = [first_name, last_name].filter(Boolean).join(' '); - // Create/update user - await db - .insert(usersTable) - .values({ - id, - name: fullName, - email: primaryEmail, - imageUrl: image_url, - }) - .onConflictDoUpdate({ - target: usersTable.id, - set: { + // Create/update user with a transaction + await db.transaction(async (tx) => { + // Create/update user + await tx + .insert(usersTable) + .values({ + id, name: fullName, email: primaryEmail, imageUrl: image_url, - updatedAt: new Date(), - }, - }); - - // Assign default role (Observer) - const observerRole = await db - .select() - .from(rolesTable) - .where(eq(rolesTable.name, 'Observer')) - .limit(1); - - if (observerRole[0]) { - await db - .insert(userRolesTable) - .values({ - userId: id, - roleId: observerRole[0].id, }) - .onConflictDoNothing(); - } + .onConflictDoUpdate({ + target: usersTable.id, + set: { + name: fullName, + email: primaryEmail, + imageUrl: image_url, + updatedAt: new Date(), + }, + }); + + // Get or create Observer role + const observerRole = await tx + .select() + .from(rolesTable) + .where(eq(rolesTable.name, 'Observer')) + .limit(1); + + if (observerRole[0]) { + await tx + .insert(userRolesTable) + .values({ + userId: id, + roleId: observerRole[0].id, + }) + .onConflictDoNothing(); + } + }); return new Response('User created successfully', { status: 200 }); } catch (error) { console.error('Error creating user:', error); - return new Response('Database error', { status: 500 }); + return new Response(JSON.stringify({ error: 'Database error' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); } } - if (eventType === 'user.deleted') { - try { - await db - .delete(usersTable) - .where(eq(usersTable.id, String(event.data.id))); - } catch (error) { - console.error('Error deleting user from database:', error); - return new Response('Database error', { status: 500 }); - } - } - - return new Response('Webhook processed successfully', { status: 200 }); + return new Response(JSON.stringify({ message: 'Webhook processed successfully' }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); } \ No newline at end of file diff --git a/src/app/dashboard/studies/page.tsx b/src/app/dashboard/studies/page.tsx index 0f81d6c..e3377b4 100644 --- a/src/app/dashboard/studies/page.tsx +++ b/src/app/dashboard/studies/page.tsx @@ -47,6 +47,11 @@ export default function Studies() { const createStudy = async (e: React.FormEvent) => { e.preventDefault(); try { + console.log("Sending study data:", { + title: newStudyTitle, + description: newStudyDescription + }); + const response = await fetch('/api/studies', { method: 'POST', headers: { @@ -57,12 +62,20 @@ export default function Studies() { description: newStudyDescription, }), }); + + if (!response.ok) { + const errorData = await response.json(); + console.error("Server response:", errorData); + throw new Error(errorData.error || 'Failed to create study'); + } + const newStudy = await response.json(); setStudies([...studies, newStudy]); setNewStudyTitle(""); setNewStudyDescription(""); } catch (error) { console.error('Error creating study:', error); + alert(error instanceof Error ? error.message : 'Failed to create study'); } }; diff --git a/src/db/index.ts b/src/db/index.ts index 38b342b..667f1f8 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,4 +1,9 @@ -import { sql } from '@vercel/postgres'; -import { drizzle } from 'drizzle-orm/vercel-postgres'; +import { drizzle } from "drizzle-orm/vercel-postgres"; +import { sql } from "@vercel/postgres"; +import { config } from "dotenv"; +// Load environment variables +config({ path: ".env.local" }); + +// Create the database instance export const db = drizzle(sql); diff --git a/src/db/seed.ts b/src/db/seed.ts index 03479c6..b515cfe 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -1,135 +1,105 @@ -import { db } from "~/db"; -import { - permissionsTable, - rolesTable, - rolePermissionsTable, -} from "~/db/schema"; import { config } from "dotenv"; +import { db } from "./index"; +import { PERMISSIONS } from "~/lib/permissions"; +import { ROLES, ROLE_PERMISSIONS } from "~/lib/roles"; +import { permissionsTable, rolesTable, rolePermissionsTable } from "./schema"; +// Load environment variables from .env.local config({ path: ".env.local" }); async function seed() { - try { - console.log("Starting seed..."); + console.log("🌱 Seeding database..."); - // Create permissions - const createdPermissions = await db - .insert(permissionsTable) - .values([ - { - name: "View Participant Names", - code: "view_participant_names", - description: "Can view participant names", - }, - { - name: "Create Participant", - code: "create_participant", - description: "Can create new participants", - }, - { - name: "Delete Participant", - code: "delete_participant", - description: "Can delete participants", - }, - { - name: "Create Study", - code: "create_study", - description: "Can create new studies", - }, - { - name: "Delete Study", - code: "delete_study", - description: "Can delete studies", - }, - { - name: "Manage Roles", - code: "manage_roles", - description: "Can manage user roles", - }, - ]) - .returning(); - - console.log("Created permissions:", createdPermissions); - - // Create roles - const createdRoles = await db - .insert(rolesTable) - .values([ - { - name: "Admin", - description: "Full system access", - }, - { - name: "Researcher", - description: "Can manage studies and participants", - }, - { - name: "Observer", - description: "Can view participant names only", - }, - ]) - .returning(); - - console.log("Created roles:", createdRoles); - - // Find roles by name - const adminRole = createdRoles.find((r) => r.name === "Admin"); - const researcherRole = createdRoles.find((r) => r.name === "Researcher"); - const observerRole = createdRoles.find((r) => r.name === "Observer"); - - // Assign permissions to roles - if (adminRole) { - // Admin gets all permissions - await db.insert(rolePermissionsTable).values( - createdPermissions.map((p) => ({ - roleId: adminRole.id, - permissionId: p.id, - })) - ); - console.log("Assigned all permissions to Admin role"); - } - - if (researcherRole) { - // Researcher gets specific permissions - const researcherPermissions = createdPermissions.filter((p) => - [ - "view_participant_names", - "create_participant", - "create_study", - ].includes(p.code) - ); - - await db.insert(rolePermissionsTable).values( - researcherPermissions.map((p) => ({ - roleId: researcherRole.id, - permissionId: p.id, - })) - ); - console.log("Assigned permissions to Researcher role"); - } - - if (observerRole) { - // Observer gets view-only permissions - const observerPermissions = createdPermissions.filter((p) => - ["view_participant_names"].includes(p.code) - ); - - await db.insert(rolePermissionsTable).values( - observerPermissions.map((p) => ({ - roleId: observerRole.id, - permissionId: p.id, - })) - ); - console.log("Assigned permissions to Observer role"); - } - - console.log("Seeding completed successfully"); - } catch (error) { - console.error("Error seeding database:", error); - throw error; + // Insert roles + console.log("Inserting roles..."); + for (const [roleKey, roleName] of Object.entries(ROLES)) { + await db.insert(rolesTable) + .values({ + name: roleName, + description: getRoleDescription(roleKey), + }) + .onConflictDoNothing(); } + + // Insert permissions + console.log("Inserting permissions..."); + for (const [permKey, permCode] of Object.entries(PERMISSIONS)) { + await db.insert(permissionsTable) + .values({ + name: formatPermissionName(permKey), + code: permCode, + description: getPermissionDescription(permKey), + }) + .onConflictDoNothing(); + } + + // Get role and permission IDs + const roles = await db.select().from(rolesTable); + const permissions = await db.select().from(permissionsTable); + + // Insert role permissions + console.log("Inserting role permissions..."); + for (const [roleKey, permissionCodes] of Object.entries(ROLE_PERMISSIONS)) { + const role = roles.find(r => r.name === ROLES[roleKey as keyof typeof ROLES]); + if (!role) continue; + + for (const permissionCode of permissionCodes) { + const permission = permissions.find(p => p.code === PERMISSIONS[permissionCode]); + if (!permission) continue; + + await db.insert(rolePermissionsTable) + .values({ + roleId: role.id, + permissionId: permission.id, + }) + .onConflictDoNothing(); + } + } + + console.log("✅ Seeding complete!"); } -seed() - .catch(console.error) - .finally(() => process.exit()); +function getRoleDescription(roleKey: string): string { + const descriptions: Record = { + ADMIN: "Full system administrator with all permissions", + PRINCIPAL_INVESTIGATOR: "Lead researcher responsible for study design and oversight", + RESEARCHER: "Study team member with data collection and analysis capabilities", + WIZARD: "Operator controlling robot behavior during experiments", + OBSERVER: "Team member observing and annotating experiments", + ASSISTANT: "Support staff with limited view access", + }; + return descriptions[roleKey] || ""; +} + +function getPermissionDescription(permKey: string): string { + const descriptions: Record = { + CREATE_STUDY: "Create new research studies", + EDIT_STUDY: "Modify existing study parameters", + DELETE_STUDY: "Remove studies from the system", + VIEW_STUDY: "View study details and progress", + VIEW_PARTICIPANT_NAMES: "Access participant identifying information", + CREATE_PARTICIPANT: "Add new participants to studies", + EDIT_PARTICIPANT: "Update participant information", + DELETE_PARTICIPANT: "Remove participants from studies", + CONTROL_ROBOT: "Operate robot during experiments", + VIEW_ROBOT_STATUS: "Monitor robot state and sensors", + RECORD_EXPERIMENT: "Start/stop experiment recording", + VIEW_EXPERIMENT: "View experiment progress and details", + VIEW_EXPERIMENT_DATA: "Access collected experiment data", + EXPORT_EXPERIMENT_DATA: "Download experiment data", + ANNOTATE_EXPERIMENT: "Add notes and annotations to experiments", + MANAGE_ROLES: "Assign and modify user roles", + MANAGE_USERS: "Add and remove system users", + MANAGE_SYSTEM_SETTINGS: "Configure system-wide settings", + }; + return descriptions[permKey] || ""; +} + +function formatPermissionName(permKey: string): string { + return permKey.toLowerCase() + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +} + +seed().catch(console.error); diff --git a/src/lib/permissions.ts b/src/lib/permissions.ts index a784b1a..10227e1 100644 --- a/src/lib/permissions.ts +++ b/src/lib/permissions.ts @@ -13,6 +13,18 @@ export const PERMISSIONS = { CREATE_STUDY: "create_study", DELETE_STUDY: "delete_study", MANAGE_ROLES: "manage_roles", + EDIT_STUDY: "edit_study", + VIEW_STUDY: "view_study", + EDIT_PARTICIPANT: "edit_participant", + CONTROL_ROBOT: "control_robot", + VIEW_ROBOT_STATUS: "view_robot_status", + RECORD_EXPERIMENT: "record_experiment", + VIEW_EXPERIMENT: "view_experiment", + VIEW_EXPERIMENT_DATA: "view_experiment_data", + EXPORT_EXPERIMENT_DATA: "export_experiment_data", + ANNOTATE_EXPERIMENT: "annotate_experiment", + MANAGE_USERS: "manage_users", + MANAGE_SYSTEM_SETTINGS: "manage_system_settings", } as const; export type PermissionCode = keyof typeof PERMISSIONS; diff --git a/src/lib/roles.ts b/src/lib/roles.ts new file mode 100644 index 0000000..42195d4 --- /dev/null +++ b/src/lib/roles.ts @@ -0,0 +1,70 @@ +import { PERMISSIONS } from './permissions'; + +export const ROLES = { + ADMIN: 'admin', + PRINCIPAL_INVESTIGATOR: 'principal_investigator', + RESEARCHER: 'researcher', + WIZARD: 'wizard', + OBSERVER: 'observer', + ASSISTANT: 'assistant', +} as const; + +export type RoleCode = keyof typeof ROLES; + +export const ROLE_PERMISSIONS: Record> = { + ADMIN: Object.keys(PERMISSIONS) as Array, + + PRINCIPAL_INVESTIGATOR: [ + 'CREATE_STUDY', + 'EDIT_STUDY', + 'DELETE_STUDY', + 'VIEW_STUDY', + 'VIEW_PARTICIPANT_NAMES', + 'CREATE_PARTICIPANT', + 'EDIT_PARTICIPANT', + 'DELETE_PARTICIPANT', + 'VIEW_ROBOT_STATUS', + 'VIEW_EXPERIMENT', + 'VIEW_EXPERIMENT_DATA', + 'EXPORT_EXPERIMENT_DATA', + 'ANNOTATE_EXPERIMENT', + 'MANAGE_ROLES', + 'MANAGE_USERS', + ], + + RESEARCHER: [ + 'VIEW_STUDY', + 'VIEW_PARTICIPANT_NAMES', + 'CREATE_PARTICIPANT', + 'EDIT_PARTICIPANT', + 'VIEW_ROBOT_STATUS', + 'VIEW_EXPERIMENT', + 'VIEW_EXPERIMENT_DATA', + 'EXPORT_EXPERIMENT_DATA', + 'ANNOTATE_EXPERIMENT', + ], + + WIZARD: [ + 'VIEW_STUDY', + 'VIEW_PARTICIPANT_NAMES', + 'VIEW_ROBOT_STATUS', + 'CONTROL_ROBOT', + 'RECORD_EXPERIMENT', + 'VIEW_EXPERIMENT', + 'ANNOTATE_EXPERIMENT', + ], + + OBSERVER: [ + 'VIEW_STUDY', + 'VIEW_ROBOT_STATUS', + 'VIEW_EXPERIMENT', + 'VIEW_EXPERIMENT_DATA', + 'ANNOTATE_EXPERIMENT', + ], + + ASSISTANT: [ + 'VIEW_STUDY', + 'VIEW_ROBOT_STATUS', + 'VIEW_EXPERIMENT', + ], +}; \ No newline at end of file