mirror of
https://github.com/soconnor0919/hristudio.git
synced 2025-12-11 22:54:45 -05:00
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:
@@ -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' }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' }
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
222
src/db/seed.ts
222
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<string, string> = {
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
70
src/lib/roles.ts
Normal file
70
src/lib/roles.ts
Normal 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',
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user