mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-23 19:27:51 -04:00
feat: Enforce study membership access for file uploads and integrate live system statistics.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user