fitaiProto/apps/admin/src/app/api/recommendations/route.ts

263 lines
7.3 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { getDatabase } from "@/lib/database";
import log from "@/lib/logger";
import {
legacyRecommendationSchema,
recommendationUpdateRequestSchema,
} from "@/lib/validation/schemas";
import {
validateRequestBody,
validationErrorResponse,
} from "@/lib/validation/helpers";
import {
successResponse,
unauthorizedResponse,
forbiddenResponse,
badRequestResponse,
internalErrorResponse,
} from "@/lib/api/responses";
import { getRecommendationsByGym } from "@/lib/gym-context";
import { ensureUserSynced } from "@/lib/sync-user";
export async function GET(request: NextRequest) {
try {
const { userId: currentUserId } = await auth();
if (!currentUserId) {
return unauthorizedResponse();
}
const db = await getDatabase();
const currentUser = await ensureUserSynced(currentUserId, db);
if (!currentUser) {
return forbiddenResponse("User not found");
}
const { searchParams } = new URL(request.url);
const targetUserId = searchParams.get("userId");
// If no userId provided, staff gets gym-scoped recommendations
if (!targetUserId) {
const isStaff =
currentUser.role === "admin" ||
currentUser.role === "superAdmin" ||
currentUser.role === "trainer";
if (!isStaff) {
return badRequestResponse("User ID is required");
}
// Get target gym based on role
const targetGymId =
currentUser.role === "superAdmin"
? (searchParams.get("gymId") ?? undefined)
: (currentUser.gymId ?? undefined);
// Get recommendations filtered by gym
const recommendations = targetGymId
? await getRecommendationsByGym(targetGymId)
: await db.getAllRecommendations();
return successResponse({ recommendations });
}
// Check permissions: Users can view their own, Admins/Trainers can view anyone's in their gym
const isStaff =
currentUser.role === "admin" ||
currentUser.role === "superAdmin" ||
currentUser.role === "trainer";
if (currentUserId !== targetUserId) {
if (!isStaff) {
return forbiddenResponse();
}
// Staff need to verify target user is in same gym
if (currentUser.role !== "superAdmin") {
const targetUser = await db.getUserById(targetUserId);
if (!targetUser || targetUser.gymId !== currentUser.gymId) {
return forbiddenResponse("Cannot access users from other gyms");
}
}
}
let recommendations = await db.getRecommendationsByUserId(targetUserId);
// Non-staff users should only see approved recommendations
if (!isStaff) {
recommendations = recommendations.filter(
(rec: any) => rec.status === "approved",
);
}
return successResponse(recommendations);
} catch (error) {
console.error("Error fetching recommendations:", error);
return internalErrorResponse();
}
}
export async function POST(request: NextRequest) {
try {
const { userId: currentUserId } = await auth();
if (!currentUserId) {
return unauthorizedResponse();
}
const db = await getDatabase();
const currentUser = await ensureUserSynced(currentUserId, db);
const isStaff =
currentUser?.role === "admin" ||
currentUser?.role === "superAdmin" ||
currentUser?.role === "trainer";
if (!isStaff) {
return forbiddenResponse();
}
// Validate request body
const validation = await validateRequestBody(
request,
legacyRecommendationSchema,
);
if (!validation.success) {
log.warn("Recommendation creation validation failed", {
errors: validation.errors,
});
return validationErrorResponse(validation.errors);
}
const {
userId,
fitnessProfileId,
recommendationText,
activityPlan,
dietPlan,
status,
type,
content,
} = validation.data;
const targetUser = await db.getUserById(userId);
if (!targetUser) {
return badRequestResponse("Target user not found");
}
if (
currentUser?.role !== "superAdmin" &&
(!currentUser?.gymId || targetUser.gymId !== currentUser.gymId)
) {
return forbiddenResponse("Cannot create recommendations for other gyms");
}
// Handle AI Plan (Legacy/Specific)
if (recommendationText && activityPlan && dietPlan && fitnessProfileId) {
const recommendation = await db.createRecommendation({
id: crypto.randomUUID(),
userId,
fitnessProfileId,
type: "ai_plan",
recommendationText: recommendationText,
activityPlan,
dietPlan,
status: status || "pending",
} as any);
return successResponse(recommendation);
}
// Handle User Goal (Generic)
if (type && content) {
const recommendation = await db.createRecommendation({
id: crypto.randomUUID(),
userId,
type,
recommendationText: content,
status: status || "pending",
} as any);
return successResponse(recommendation);
}
return badRequestResponse("Missing required fields");
} catch (error) {
log.error("Failed to create recommendation", error);
return internalErrorResponse();
}
}
export async function PUT(request: NextRequest) {
try {
const { userId: currentUserId } = await auth();
if (!currentUserId) {
return unauthorizedResponse();
}
// Validate request body
const validation = await validateRequestBody(
request,
recommendationUpdateRequestSchema,
);
if (!validation.success) {
log.warn("Recommendation update validation failed", {
errors: validation.errors,
});
return validationErrorResponse(validation.errors);
}
const { id, status, recommendationText, activityPlan, dietPlan, content } =
validation.data;
const db = await getDatabase();
const currentUser = await ensureUserSynced(currentUserId, db);
if (!currentUser) {
return forbiddenResponse("User not found");
}
const isStaff =
currentUser.role === "admin" ||
currentUser.role === "superAdmin" ||
currentUser.role === "trainer";
if (!isStaff) {
return forbiddenResponse();
}
const existingRecommendation = (await db.getAllRecommendations()).find(
(recommendation) => recommendation.id === id,
);
if (!existingRecommendation) {
return badRequestResponse("Recommendation not found");
}
if (currentUser.role !== "superAdmin") {
const targetUser = await db.getUserById(existingRecommendation.userId);
if (
!currentUser.gymId ||
!targetUser ||
targetUser.gymId !== currentUser.gymId
) {
return forbiddenResponse(
"Cannot modify recommendations for other gyms",
);
}
}
const updated = await db.updateRecommendation(id, {
...(status && { status }),
...(recommendationText && { recommendationText }),
...(content && { recommendationText: content }),
...(activityPlan && { activityPlan }),
...(dietPlan && { dietPlan }),
} as any);
return successResponse(updated);
} catch (error) {
log.error("Failed to update recommendation", error);
return internalErrorResponse();
}
}