chore(deps): Update dependencies and refactor API routes for improved error handling

- Updated various dependencies in package.json and pnpm-lock.yaml, including '@clerk/nextjs' to version 6.7.1 and several others for better performance and security.
- Refactored API routes to use Promise.resolve for context parameters, enhancing reliability in asynchronous contexts.
- Improved error handling in the dashboard and studies components, ensuring better user experience during data fetching.
- Removed unused favicon.ico file to clean up the project structure.
- Enhanced the dashboard components to utilize a new utility function for API URL fetching, promoting code reusability.
This commit is contained in:
2024-12-05 11:52:22 -05:00
parent 29ce631901
commit 207f4d7fb8
25 changed files with 719 additions and 682 deletions

View File

@@ -16,7 +16,7 @@
"test:email": "tsx src/scripts/test-email.ts"
},
"dependencies": {
"@clerk/nextjs": "^6.4.0",
"@clerk/nextjs": "^6.7.1",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
@@ -29,31 +29,32 @@
"@types/nodemailer": "^6.4.17",
"@vercel/analytics": "^1.4.1",
"@vercel/postgres": "^0.10.0",
"class-variance-authority": "^0.7.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dotenv": "^16.4.5",
"drizzle-orm": "^0.36.3",
"lucide-react": "^0.454.0",
"dotenv": "^16.4.7",
"drizzle-orm": "^0.37.0",
"lucide-react": "^0.468.0",
"next": "15.0.3",
"ngrok": "5.0.0-beta.2",
"nodemailer": "^6.9.16",
"punycode": "^2.3.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"svix": "^1.41.0",
"tailwind-merge": "^2.5.4",
"svix": "^1.42.0",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/node": "^22.9.1",
"@types/react": "^18.3.12",
"@types/node": "^22.10.1",
"@types/react": "^18.3.13",
"@types/react-dom": "^18.3.1",
"drizzle-kit": "^0.27.2",
"eslint": "^9.15.0",
"eslint-config-next": "15.0.2",
"drizzle-kit": "^0.29.1",
"eslint": "^9.16.0",
"eslint-config-next": "15.0.3",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.15",
"tailwindcss": "^3.4.16",
"tsx": "^4.19.2",
"typescript": "^5.6.3"
"typescript": "^5.7.2"
}
}

1084
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -10,7 +10,7 @@ export async function GET(
context: { params: { id: string } }
) {
const { userId } = await auth();
const { id } = await context.params;
const { id } = await Promise.resolve(context.params);
if (!userId) {
return ApiError.Unauthorized();
@@ -52,7 +52,7 @@ export async function POST(
context: { params: { id: string } }
) {
const { userId } = await auth();
const { id } = await context.params;
const { id } = await Promise.resolve(context.params);
if (!userId) {
return ApiError.Unauthorized();
@@ -98,7 +98,7 @@ export async function DELETE(
context: { params: { id: string } }
) {
const { userId } = await auth();
const { id } = await context.params;
const { id } = await Promise.resolve(context.params);
if (!userId) {
return ApiError.Unauthorized();

View File

@@ -11,7 +11,7 @@ export async function GET(
context: { params: { id: string } }
) {
const { userId } = await auth();
const { id } = context.params;
const { id } = await Promise.resolve(context.params);
if (!userId) {
return ApiError.Unauthorized();

View File

@@ -9,7 +9,7 @@ export async function PUT(
context: { params: { id: string; userId: string } }
) {
try {
const { id, userId } = await context.params;
const { id, userId } = await Promise.resolve(context.params);
const studyId = parseInt(id);
if (isNaN(studyId)) {

View File

@@ -9,7 +9,7 @@ export async function GET(
context: { params: { id: string } }
) {
try {
const { id } = await context.params;
const { id } = await Promise.resolve(context.params);
const studyId = parseInt(id);
if (isNaN(studyId)) {

View File

@@ -1,12 +1,13 @@
'use client';
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { BookOpen, Settings2 } from "lucide-react";
import { useToast } from "~/hooks/use-toast";
import { Breadcrumb } from "~/components/breadcrumb";
import { getApiUrl } from "~/lib/fetch-utils";
interface DashboardStats {
studyCount: number;
@@ -22,15 +23,11 @@ export default function Dashboard() {
const router = useRouter();
const { toast } = useToast();
useEffect(() => {
fetchDashboardStats();
}, []);
const fetchDashboardStats = async () => {
const fetchDashboardStats = useCallback(async () => {
try {
const studiesRes = await fetch('/api/studies');
const studiesRes = await fetch(getApiUrl('/api/studies'));
const studies = await studiesRes.json();
// For now, just show study count
setStats({
studyCount: studies.data.length,
@@ -46,7 +43,12 @@ export default function Dashboard() {
} finally {
setLoading(false);
}
};
}, [toast]);
useEffect(() => {
fetchDashboardStats();
}, [fetchDashboardStats]);
if (loading) {
return (
@@ -59,7 +61,7 @@ export default function Dashboard() {
return (
<div className="space-y-6">
<Breadcrumb />
<div>
<h1 className="text-2xl font-bold">Dashboard</h1>
<p className="text-muted-foreground">Overview of your research studies</p>

View File

@@ -21,6 +21,7 @@ import {
SelectValue
} from "~/components/ui/select";
import { usePermissions } from "~/hooks/usePermissions";
import { getApiUrl } from "~/lib/fetch-utils";
interface Study {
id: number;
@@ -47,7 +48,7 @@ export default function Participants() {
const fetchStudies = async () => {
try {
const response = await fetch('/api/studies');
const response = await fetch(getApiUrl('/api/studies'));
const data = await response.json();
setStudies(data);
} catch (error) {
@@ -60,7 +61,7 @@ export default function Participants() {
const fetchParticipants = async (studyId: number) => {
try {
console.log(`Fetching participants for studyId: ${studyId}`);
const response = await fetch(`/api/participants?studyId=${studyId}`);
const response = await fetch(getApiUrl(`/api/participants?studyId=${studyId}`));
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
@@ -84,7 +85,7 @@ export default function Participants() {
if (!selectedStudyId) return;
try {
const response = await fetch(`/api/participants`, {
const response = await fetch(getApiUrl('/api/participants'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -109,7 +110,7 @@ export default function Participants() {
const deleteParticipant = async (id: number) => {
try {
const response = await fetch(`/api/participants/${id}`, {
const response = await fetch(getApiUrl('/api/participants/${id}'), {
method: 'DELETE',
});

View File

@@ -1,6 +1,6 @@
'use client';
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
@@ -14,6 +14,7 @@ import {
} from "lucide-react";
import { useToast } from "~/hooks/use-toast";
import Link from "next/link";
import { getApiUrl } from "~/lib/fetch-utils";
interface StudyStats {
participantCount: number;
@@ -31,13 +32,9 @@ export default function StudyDashboard() {
const { id } = useParams();
const { toast } = useToast();
useEffect(() => {
fetchStudyStats();
}, [id]);
const fetchStudyStats = async () => {
const fetchStudyStats = useCallback(async () => {
try {
const response = await fetch(`/api/studies/${id}/stats`);
const response = await fetch(getApiUrl(`/api/studies/${id}/stats`));
if (!response.ok) throw new Error("Failed to fetch study statistics");
const data = await response.json();
setStats(data.data);
@@ -51,7 +48,11 @@ export default function StudyDashboard() {
} finally {
setLoading(false);
}
};
}, [toast, id]);
useEffect(() => {
fetchStudyStats();
}, [fetchStudyStats]);
if (loading) {
return (

View File

@@ -12,6 +12,7 @@ import Link from "next/link";
import { useActiveStudy } from "~/context/active-study";
import { hasPermission } from "~/lib/permissions-client";
import { PERMISSIONS } from "~/lib/permissions";
import { getApiUrl } from "~/lib/fetch-utils";
export default function NewParticipant() {
const [name, setName] = useState("");
@@ -36,7 +37,7 @@ export default function NewParticipant() {
setIsSubmitting(true);
try {
const response = await fetch(`/api/studies/${id}/participants`, {
const response = await fetch(getApiUrl(`/api/studies/${id}/participants`), {
method: "POST",
headers: {
"Content-Type": "application/json",

View File

@@ -1,6 +1,6 @@
'use client';
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
@@ -29,7 +29,7 @@ import {
TableHeader,
TableRow,
} from "~/components/ui/table";
import { getApiUrl } from "~/lib/fetch-utils";
interface Participant {
id: number;
name: string;
@@ -48,13 +48,9 @@ export default function ParticipantsList() {
const canDeleteParticipant = activeStudy && hasPermission(activeStudy.permissions, PERMISSIONS.DELETE_PARTICIPANT);
const canViewNames = activeStudy && hasPermission(activeStudy.permissions, PERMISSIONS.VIEW_PARTICIPANT_NAMES);
useEffect(() => {
fetchParticipants();
}, [id]);
const fetchParticipants = async () => {
const fetchParticipants = useCallback(async () => {
try {
const response = await fetch(`/api/studies/${id}/participants`, {
const response = await fetch(getApiUrl(`/api/studies/${id}/participants`), {
method: "GET",
headers: {
"Content-Type": "application/json",
@@ -74,11 +70,15 @@ export default function ParticipantsList() {
} finally {
setIsLoading(false);
}
};
}, [toast, id]);
useEffect(() => {
fetchParticipants();
}, [fetchParticipants]);
const handleDelete = async (participantId: number) => {
try {
const response = await fetch(`/api/studies/${id}/participants`, {
const response = await fetch(getApiUrl(`/api/studies/${id}/participants`), {
method: "DELETE",
headers: {
"Content-Type": "application/json",

View File

@@ -10,6 +10,7 @@ import { PERMISSIONS } from "~/lib/permissions-client";
import { Button } from "~/components/ui/button";
import { Settings2Icon, UsersIcon, UserIcon } from "lucide-react";
import { cn } from "~/lib/utils";
import { getApiUrl } from "~/lib/fetch-utils";
interface Study {
id: number;
@@ -29,7 +30,7 @@ export default function StudySettings() {
useEffect(() => {
const fetchStudy = async () => {
try {
const response = await fetch(`/api/studies/${id}`);
const response = await fetch(getApiUrl(`/api/studies/${id}`));
if (!response.ok) {
if (response.status === 403) {
router.push('/dashboard/studies');

View File

@@ -13,7 +13,7 @@ import Link from "next/link";
import { useActiveStudy } from "~/context/active-study";
import { hasPermission } from "~/lib/permissions-client";
import { PERMISSIONS } from "~/lib/permissions";
import { getApiUrl } from "~/lib/fetch-utils";
export default function NewStudy() {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
@@ -27,7 +27,7 @@ export default function NewStudy() {
setIsSubmitting(true);
try {
const response = await fetch('/api/studies', {
const response = await fetch(getApiUrl('/api/studies'), {
method: "POST",
headers: {
"Content-Type": "application/json",

View File

@@ -21,6 +21,7 @@ import {
AlertDialogFooter
} from "~/components/ui/alert-dialog";
import { ROLES } from "~/lib/roles";
import { getApiUrl } from "~/lib/fetch-utils";
interface Study {
id: number;
@@ -50,7 +51,7 @@ export default function Studies() {
const fetchStudies = async () => {
try {
const response = await fetch("/api/studies");
const response = await fetch(getApiUrl("/api/studies"));
if (!response.ok) throw new Error("Failed to fetch studies");
const data = await response.json();
setStudies(data.data || []);
@@ -67,7 +68,7 @@ export default function Studies() {
const createStudy = async (e: React.FormEvent) => {
e.preventDefault();
try {
const response = await fetch("/api/studies", {
const response = await fetch(getApiUrl("/api/studies"), {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -99,7 +100,7 @@ export default function Studies() {
const deleteStudy = async (id: number) => {
try {
const response = await fetch(`/api/studies/${id}`, {
const response = await fetch(getApiUrl(`/api/studies/${id}`), {
method: "DELETE",
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -187,15 +187,19 @@ export function Sidebar() {
<div className="border-t border-[hsl(var(--sidebar-separator))]">
<div className="flex items-center justify-between pt-4">
<div className="flex items-center space-x-4">
<UserButton />
<div>
<p className="text-sm font-medium text-[hsl(var(--sidebar-foreground))]">
{user?.fullName ?? user?.username ?? 'User'}
</p>
<p className="text-xs text-[hsl(var(--sidebar-muted))]">
{user?.primaryEmailAddress?.emailAddress ?? 'user@example.com'}
</p>
<div className="w-8 h-8">
<UserButton afterSignOutUrl="/" />
</div>
{user && (
<div className="min-w-0">
<p className="text-sm font-medium text-[hsl(var(--sidebar-foreground))] truncate">
{user.fullName ?? user.username ?? 'User'}
</p>
<p className="text-xs text-[hsl(var(--sidebar-muted))] truncate">
{user.primaryEmailAddress?.emailAddress ?? 'user@example.com'}
</p>
</div>
)}
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect } from "react";
import { useCallback, useEffect, useState } from "react";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
import { useToast } from "~/hooks/use-toast";
@@ -28,11 +28,7 @@ export function InvitationsTab({ studyId, permissions }: InvitationsTabProps) {
const hasPermission = (permission: string) => permissions.includes(permission);
const canManageRoles = hasPermission(PERMISSIONS.MANAGE_ROLES);
useEffect(() => {
fetchInvitations();
}, [studyId]);
const fetchInvitations = async () => {
const fetchInvitations = useCallback(async () => {
try {
const response = await fetch(`/api/invitations?studyId=${studyId}`);
if (!response.ok) throw new Error("Failed to fetch invitations");
@@ -48,7 +44,12 @@ export function InvitationsTab({ studyId, permissions }: InvitationsTabProps) {
} finally {
setIsLoading(false);
}
};
}, [studyId, toast]);
useEffect(() => {
fetchInvitations();
}, [fetchInvitations]);
const handleDeleteInvitation = async (invitationId: string) => {
try {

View File

@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import { Button } from "~/components/ui/button";
import {
Dialog,
@@ -41,14 +41,7 @@ export function InviteUserDialog({ studyId, onInviteSent }: InviteUserDialogProp
const [roles, setRoles] = useState<Role[]>([]);
const { toast } = useToast();
// Fetch available roles when dialog opens
useEffect(() => {
if (isOpen) {
fetchRoles();
}
}, [isOpen]);
const fetchRoles = async () => {
const fetchRoles = useCallback(async () => {
try {
const response = await fetch("/api/roles");
if (!response.ok) {
@@ -67,7 +60,12 @@ export function InviteUserDialog({ studyId, onInviteSent }: InviteUserDialogProp
variant: "destructive",
});
}
};
}, [toast]);
// Fetch available roles when dialog opens
useEffect(() => {
fetchRoles();
}, [fetchRoles]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

View File

@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect } from "react";
import { useCallback, useEffect, useState } from "react";
import { PlusIcon, Trash2Icon } from "lucide-react";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
@@ -43,11 +43,7 @@ export function ParticipantsTab({ studyId, permissions }: ParticipantsTabProps)
const canDeleteParticipant = hasPermission(PERMISSIONS.DELETE_PARTICIPANT);
const canViewNames = hasPermission(PERMISSIONS.VIEW_PARTICIPANT_NAMES);
useEffect(() => {
fetchParticipants();
}, [studyId]);
const fetchParticipants = async () => {
const fetchParticipants = useCallback(async () => {
try {
const response = await fetch(`/api/studies/${studyId}/participants`);
if (!response.ok) throw new Error("Failed to fetch participants");
@@ -63,7 +59,11 @@ export function ParticipantsTab({ studyId, permissions }: ParticipantsTabProps)
} finally {
setIsLoading(false);
}
};
}, [toast, studyId]);
useEffect(() => {
fetchParticipants();
}, [fetchParticipants]);
const createParticipant = async (e: React.FormEvent) => {
e.preventDefault();

View File

@@ -72,7 +72,7 @@ export function SettingsTab({ study }: SettingsTabProps) {
<Card>
<CardContent className="py-8">
<p className="text-center text-muted-foreground">
You don't have permission to edit this study.
You don&apos;t have permission to edit this study.
</p>
</CardContent>
</Card>

View File

@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import { UserAvatar } from "~/components/user-avatar";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
@@ -38,8 +38,7 @@ import {
interface User {
id: string;
email: string;
firstName: string | null;
lastName: string | null;
name: string | null;
roles: Array<{ id: number; name: string }>;
}
@@ -72,23 +71,7 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
const hasPermission = (permission: string) => permissions.includes(permission);
const canManageRoles = hasPermission(PERMISSIONS.MANAGE_ROLES);
useEffect(() => {
fetchData();
}, [studyId]);
const fetchData = async () => {
try {
await Promise.all([
fetchUsers(),
fetchInvitations(),
fetchRoles(),
]);
} finally {
setIsLoading(false);
}
};
const fetchUsers = async () => {
const fetchUsers = useCallback(async () => {
try {
const response = await fetch(`/api/studies/${studyId}/users`);
if (!response.ok) throw new Error("Failed to fetch users");
@@ -102,9 +85,9 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
variant: "destructive",
});
}
};
}, [studyId, toast]);
const fetchInvitations = async () => {
const fetchInvitations = useCallback(async () => {
try {
const response = await fetch(`/api/invitations?studyId=${studyId}`);
if (!response.ok) throw new Error("Failed to fetch invitations");
@@ -118,9 +101,9 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
variant: "destructive",
});
}
};
}, [studyId, toast]);
const fetchRoles = async () => {
const fetchRoles = useCallback(async () => {
try {
const response = await fetch("/api/roles");
if (!response.ok) throw new Error("Failed to fetch roles");
@@ -136,7 +119,24 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
variant: "destructive",
});
}
};
}, [toast]);
const fetchData = useCallback(async () => {
setIsLoading(true);
try {
await Promise.all([
fetchUsers(),
fetchInvitations(),
fetchRoles(),
]);
} finally {
setIsLoading(false);
}
}, [fetchUsers, fetchInvitations, fetchRoles]);
useEffect(() => {
fetchData();
}, [fetchData]);
const handleRoleChange = async (userId: string, newRoleId: string) => {
try {

View File

@@ -1,7 +1,7 @@
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { createContext, useContext, useEffect, useState, useCallback } from 'react';
import { usePathname, useRouter } from 'next/navigation';
interface Study {
id: number;
@@ -30,10 +30,10 @@ export function ActiveStudyProvider({ children }: { children: React.ReactNode })
const [studies, setStudies] = useState<Study[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const pathname = usePathname();
const router = useRouter();
const fetchStudies = async () => {
const fetchStudies = useCallback(async () => {
try {
const response = await fetch('/api/studies', {
method: 'GET',
@@ -55,6 +55,7 @@ export function ActiveStudyProvider({ children }: { children: React.ReactNode })
if (studiesWithDates.length === 1 && !activeStudy) {
setActiveStudy(studiesWithDates[0]);
router.push(`/dashboard/studies/${studiesWithDates[0].id}`);
}
} catch (error) {
console.error('Error fetching studies:', error);
@@ -62,11 +63,11 @@ export function ActiveStudyProvider({ children }: { children: React.ReactNode })
} finally {
setIsLoading(false);
}
};
}, [router]);
useEffect(() => {
fetchStudies();
}, []);
}, [fetchStudies]);
useEffect(() => {
const studyIdMatch = pathname.match(/\/dashboard\/studies\/(\d+)/);
@@ -77,7 +78,9 @@ export function ActiveStudyProvider({ children }: { children: React.ReactNode })
setActiveStudy(study);
}
} else if (!pathname.includes('/studies/new')) {
setActiveStudy(null);
if (activeStudy) {
setActiveStudy(null);
}
}
}, [pathname, studies]);

View File

@@ -3,7 +3,14 @@ import { eq } from "drizzle-orm";
import { db } from "./index";
import { PERMISSIONS } from "~/lib/permissions";
import { ROLES, ROLE_PERMISSIONS } from "~/lib/roles";
import { permissionsTable, rolesTable, rolePermissionsTable, userRolesTable, usersTable, studyTable } from "./schema";
import {
permissionsTable,
rolesTable,
rolePermissionsTable,
userRolesTable,
usersTable,
studyTable,
} from "~/db/schema";
// Load environment variables from .env.local
config({ path: ".env.local" });
@@ -14,7 +21,8 @@ async function seed() {
// Insert roles
console.log("Inserting roles...");
for (const [roleKey, roleName] of Object.entries(ROLES)) {
await db.insert(rolesTable)
await db
.insert(rolesTable)
.values({
name: roleName,
description: getRoleDescription(roleKey),
@@ -25,7 +33,8 @@ async function seed() {
// Insert permissions
console.log("Inserting permissions...");
for (const [permKey, permCode] of Object.entries(PERMISSIONS)) {
await db.insert(permissionsTable)
await db
.insert(permissionsTable)
.values({
name: formatPermissionName(permKey),
code: permCode,
@@ -41,14 +50,19 @@ async function seed() {
// 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]);
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]);
const permission = permissions.find(
(p) => p.code === PERMISSIONS[permissionCode]
);
if (!permission) continue;
await db.insert(rolePermissionsTable)
await db
.insert(rolePermissionsTable)
.values({
roleId: role.id,
permissionId: permission.id,
@@ -61,7 +75,7 @@ async function seed() {
console.log("Setting up initial user roles...");
const users = await db.select().from(usersTable);
if (users.length > 0) {
const piRole = roles.find(r => r.name === ROLES.PRINCIPAL_INVESTIGATOR);
const piRole = roles.find((r) => r.name === ROLES.PRINCIPAL_INVESTIGATOR);
if (piRole) {
// Get all studies owned by the first user
const userStudies = await db
@@ -71,7 +85,8 @@ async function seed() {
// Assign PI role for each study
for (const study of userStudies) {
await db.insert(userRolesTable)
await db
.insert(userRolesTable)
.values({
userId: users[0].id,
roleId: piRole.id,
@@ -88,8 +103,10 @@ async function seed() {
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",
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",
@@ -122,10 +139,11 @@ function getPermissionDescription(permKey: string): string {
}
function formatPermissionName(permKey: string): string {
return permKey.toLowerCase()
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return permKey
.toLowerCase()
.split("_")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
seed().catch(console.error);

9
src/lib/fetch-utils.ts Normal file
View File

@@ -0,0 +1,9 @@
const getBaseUrl = () => {
if (typeof window !== 'undefined') return ''; // browser should use relative url
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
}
export function getApiUrl(path: string) {
return `${getBaseUrl()}${path}`;
}