feat: Enforce study membership access for file uploads and integrate live system statistics.

This commit is contained in:
2026-03-06 00:22:22 -05:00
parent 0051946bde
commit c37acad3d2
3 changed files with 55 additions and 25 deletions

View File

@@ -1,4 +1,4 @@
import { eq } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import { NextResponse, type NextRequest } from "next/server";
import { z } from "zod";
import {
@@ -9,7 +9,7 @@ import {
} from "~/lib/storage/minio";
import { auth } from "~/server/auth";
import { db } from "~/server/db";
import { mediaCaptures, trials } from "~/server/db/schema";
import { experiments, mediaCaptures, studyMembers, trials } from "~/server/db/schema";
const uploadSchema = z.object({
trialId: z.string().optional(),
@@ -71,16 +71,37 @@ export async function POST(request: NextRequest) {
// Check trial access if trialId is provided
if (validatedTrialId) {
const trial = await db
.select()
.select({
id: trials.id,
studyId: experiments.studyId,
})
.from(trials)
.innerJoin(experiments, eq(trials.experimentId, experiments.id))
.where(eq(trials.id, validatedTrialId))
.limit(1);
if (!trial.length) {
if (!trial.length || !trial[0]) {
return NextResponse.json({ error: "Trial not found" }, { status: 404 });
}
// TODO: Check if user has access to this trial through study membership
// Check if user has access to this trial through study membership
const membership = await db
.select()
.from(studyMembers)
.where(
and(
eq(studyMembers.studyId, trial[0].studyId),
eq(studyMembers.userId, session.user.id)
)
)
.limit(1);
if (!membership.length) {
return NextResponse.json(
{ error: "Insufficient permissions to upload to this trial" },
{ status: 403 }
);
}
}
// Generate unique file key

View File

@@ -2,13 +2,12 @@
import { Badge } from "~/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { api } from "~/trpc/react";
export function SystemStats() {
// TODO: Implement admin.getSystemStats API endpoint
// const { data: stats, isLoading } = api.admin.getSystemStats.useQuery({});
const isLoading = false;
const { data: stats, isLoading } = api.admin.getSystemStats.useQuery({});
if (isLoading) {
if (isLoading || !stats) {
return (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
@@ -26,19 +25,30 @@ export function SystemStats() {
);
}
// Mock data for now since we don't have the actual admin router implemented
const mockStats = {
totalUsers: 42,
totalStudies: 15,
totalExperiments: 38,
totalTrials: 127,
activeTrials: 3,
systemHealth: "healthy",
uptime: "7 days, 14 hours",
storageUsed: "2.3 GB",
const formatBytes = (bytes: number) => {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
};
const displayStats = mockStats;
const formatUptime = (seconds: number) => {
const d = Math.floor(seconds / (3600 * 24));
const h = Math.floor((seconds % (3600 * 24)) / 3600);
return `${d} days, ${h} hours`;
};
const displayStats = {
totalUsers: stats.users.total,
totalStudies: stats.studies.total,
totalExperiments: stats.experiments.total,
totalTrials: stats.trials.total,
activeTrials: stats.trials.running,
systemHealth: "healthy",
uptime: formatUptime(stats.system.uptime),
storageUsed: formatBytes(stats.storage.totalSize),
};
return (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">

View File

@@ -21,6 +21,7 @@ import {
PackageSearch,
PanelRightClose,
} from "lucide-react";
import { toast } from "sonner";
/**
* InspectorPanel
@@ -371,15 +372,13 @@ export function InspectorPanel({
actionDefinitions={actionRegistry.getAllActions()}
studyPlugins={studyPlugins}
onReconcileAction={(actionId) => {
// Placeholder: future diff modal / signature update
console.log("Reconcile TODO for action:", actionId);
toast.info("Action Reconcile coming soon!");
}}
onRefreshDependencies={() => {
console.log("Refresh dependencies TODO");
toast.info("Refresh dependencies coming soon!");
}}
onInstallPlugin={(pluginId) => {
console.log("Install plugin TODO:", pluginId);
toast.info("Install plugin coming soon!");
}}
/>
</div>