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.
This commit is contained in:
2024-12-03 15:42:43 -05:00
parent 09b24a0e74
commit 3a955a0568
7 changed files with 291 additions and 183 deletions

View File

@@ -2,7 +2,7 @@ import { eq } from "drizzle-orm";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server"; import { auth } from "@clerk/nextjs/server";
import { db } from "~/db"; import { db } from "~/db";
import { studyTable } from "~/db/schema"; import { studyTable, usersTable } from "~/db/schema";
export async function GET() { export async function GET() {
const { userId } = await auth(); const { userId } = await auth();
@@ -23,19 +23,59 @@ export async function POST(request: Request) {
const { userId } = await auth(); const { userId } = await auth();
if (!userId) { 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 // Verify user exists first
.insert(studyTable) const existingUser = await db
.values({ .select()
title, .from(usersTable)
description, .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, userId,
}) errorMessage: error instanceof Error ? error.message : 'Unknown error',
.returning(); 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' }
});
}
} }

View File

@@ -59,59 +59,57 @@ export async function POST(req: Request) {
// Combine first and last name // Combine first and last name
const fullName = [first_name, last_name].filter(Boolean).join(' '); const fullName = [first_name, last_name].filter(Boolean).join(' ');
// Create/update user // Create/update user with a transaction
await db await db.transaction(async (tx) => {
.insert(usersTable) // Create/update user
.values({ await tx
id, .insert(usersTable)
name: fullName, .values({
email: primaryEmail, id,
imageUrl: image_url,
})
.onConflictDoUpdate({
target: usersTable.id,
set: {
name: fullName, name: fullName,
email: primaryEmail, email: primaryEmail,
imageUrl: image_url, 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 }); return new Response('User created successfully', { status: 200 });
} catch (error) { } catch (error) {
console.error('Error creating user:', 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') { return new Response(JSON.stringify({ message: 'Webhook processed successfully' }), {
try { status: 200,
await db headers: { 'Content-Type': 'application/json' }
.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 });
} }

View File

@@ -47,6 +47,11 @@ export default function Studies() {
const createStudy = async (e: React.FormEvent) => { const createStudy = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
try { try {
console.log("Sending study data:", {
title: newStudyTitle,
description: newStudyDescription
});
const response = await fetch('/api/studies', { const response = await fetch('/api/studies', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -57,12 +62,20 @@ export default function Studies() {
description: newStudyDescription, 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(); const newStudy = await response.json();
setStudies([...studies, newStudy]); setStudies([...studies, newStudy]);
setNewStudyTitle(""); setNewStudyTitle("");
setNewStudyDescription(""); setNewStudyDescription("");
} catch (error) { } catch (error) {
console.error('Error creating study:', error); console.error('Error creating study:', error);
alert(error instanceof Error ? error.message : 'Failed to create study');
} }
}; };

View File

@@ -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); export const db = drizzle(sql);

View File

@@ -1,135 +1,105 @@
import { db } from "~/db";
import {
permissionsTable,
rolesTable,
rolePermissionsTable,
} from "~/db/schema";
import { config } from "dotenv"; 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" }); config({ path: ".env.local" });
async function seed() { async function seed() {
try { console.log("🌱 Seeding database...");
console.log("Starting seed...");
// Create permissions // Insert roles
const createdPermissions = await db console.log("Inserting roles...");
.insert(permissionsTable) for (const [roleKey, roleName] of Object.entries(ROLES)) {
.values([ await db.insert(rolesTable)
{ .values({
name: "View Participant Names", name: roleName,
code: "view_participant_names", description: getRoleDescription(roleKey),
description: "Can view participant names", })
}, .onConflictDoNothing();
{
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 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() function getRoleDescription(roleKey: string): string {
.catch(console.error) const descriptions: Record<string, string> = {
.finally(() => process.exit()); 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<string, string> = {
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);

View File

@@ -13,6 +13,18 @@ export const PERMISSIONS = {
CREATE_STUDY: "create_study", CREATE_STUDY: "create_study",
DELETE_STUDY: "delete_study", DELETE_STUDY: "delete_study",
MANAGE_ROLES: "manage_roles", 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; } as const;
export type PermissionCode = keyof typeof PERMISSIONS; export type PermissionCode = keyof typeof PERMISSIONS;

70
src/lib/roles.ts Normal file
View File

@@ -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<RoleCode, Array<keyof typeof PERMISSIONS>> = {
ADMIN: Object.keys(PERMISSIONS) as Array<keyof typeof PERMISSIONS>,
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',
],
};