role based auth
implemented superadmin -> admin -> traniner
This commit is contained in:
1 parent
16217f46ff
commit
624cdfc45c
16 files changed
+1105
-54
No files matched your search
@@ -4,6 +4,7 @@ import { getDatabase } from "@/lib/database";
|
||||
import { ensureUserSynced } from "@/lib/sync-user";
|
||||
import { successResponse } from "@/lib/api/responses";
|
||||
import { db as rawDb, sql } from "@fitai/database";
|
||||
import { getUsersByGym, getClientsByGym } from "@/lib/gym-context";
|
||||
|
||||
interface UserGrowthPoint {
|
||||
label: string;
|
||||
@@ -45,12 +46,36 @@ export async function GET(req: NextRequest) {
|
||||
const url = new URL(req.url);
|
||||
const months = parseInt(url.searchParams.get("months") || "6");
|
||||
|
||||
const allUsers = await database.getAllUsers();
|
||||
const allClients = await database.getAllClients();
|
||||
// Get target gym based on role
|
||||
const targetGymId =
|
||||
user.role === "superAdmin"
|
||||
? (url.searchParams.get("gymId") ?? undefined)
|
||||
: (user.gymId ?? undefined);
|
||||
|
||||
const paymentsRaw = await rawDb.all(
|
||||
sql`SELECT * FROM payments WHERE status = 'completed' AND paid_at IS NOT NULL`,
|
||||
);
|
||||
// Validate gym access for non-superAdmins
|
||||
if (user.role !== "superAdmin" && !targetGymId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Forbidden - No gym assigned" },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
// Get gym-scoped data
|
||||
const allUsers = targetGymId
|
||||
? await getUsersByGym(targetGymId)
|
||||
: await database.getAllUsers();
|
||||
|
||||
const allClients = targetGymId
|
||||
? await getClientsByGym(targetGymId)
|
||||
: await database.getAllClients();
|
||||
|
||||
// For payments, we'd need a similar filter - for now, skip payments if gym-scoped
|
||||
// TODO: Add getPaymentsByGym when needed
|
||||
const paymentsRaw = targetGymId
|
||||
? [] // Skip payments for gym-scoped for now
|
||||
: await rawDb.all(
|
||||
sql`SELECT * FROM payments WHERE status = 'completed' AND paid_at IS NOT NULL`,
|
||||
);
|
||||
const payments: any[] = paymentsRaw || [];
|
||||
|
||||
const now = new Date();
|
||||
|
||||
@@ -1,23 +1,39 @@
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getDatabase } from "@/lib/database";
|
||||
import { ensureUserSynced } from "@/lib/sync-user";
|
||||
import { successResponse } from "@/lib/api/responses";
|
||||
import { getAttendanceByGym } from "@/lib/gym-context";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return new NextResponse("Unauthorized", { status: 401 });
|
||||
|
||||
const db = await getDatabase();
|
||||
|
||||
const user = await ensureUserSynced(userId, db);
|
||||
|
||||
if (!user || (user.role !== "admin" && user.role !== "superAdmin")) {
|
||||
return new NextResponse("Forbidden", { status: 403 });
|
||||
}
|
||||
|
||||
const attendance = await db.getAllAttendance();
|
||||
// Get target gym based on role
|
||||
const url = new URL(req.url);
|
||||
const targetGymId =
|
||||
user.role === "superAdmin"
|
||||
? (url.searchParams.get("gymId") ?? undefined)
|
||||
: (user.gymId ?? undefined);
|
||||
|
||||
// Validate gym access for non-superAdmins
|
||||
if (user.role !== "superAdmin" && !targetGymId) {
|
||||
return new NextResponse("Forbidden - No gym assigned", { status: 403 });
|
||||
}
|
||||
|
||||
// Get attendance filtered by gym
|
||||
const attendance = targetGymId
|
||||
? await getAttendanceByGym(targetGymId)
|
||||
: await db.getAllAttendance();
|
||||
|
||||
return successResponse({ records: attendance });
|
||||
} catch (error) {
|
||||
console.error("Admin attendance error:", error);
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getDatabase } from "../../../../lib/database/index";
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import log from "@/lib/logger";
|
||||
import { fitnessProfileSchema } from "@/lib/validation/schemas";
|
||||
import {
|
||||
validateRequestBody,
|
||||
validationErrorResponse,
|
||||
} from "@/lib/validation/helpers";
|
||||
import { getFitnessProfilesByGym } from "@/lib/gym-context";
|
||||
import { ensureUserSynced } from "@/lib/sync-user";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -63,12 +66,47 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const userId = searchParams.get("userId");
|
||||
// First authenticate
|
||||
const { userId: clerkUserId } = await auth();
|
||||
if (!clerkUserId) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
const profile = await db.getFitnessProfileByUserId(userId);
|
||||
const db = await getDatabase();
|
||||
const currentUser = await ensureUserSynced(clerkUserId, db);
|
||||
|
||||
if (!currentUser) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const targetUserId = searchParams.get("userId");
|
||||
|
||||
// If accessing another user's profile, verify gym access
|
||||
if (targetUserId && targetUserId !== clerkUserId) {
|
||||
const isStaff =
|
||||
currentUser.role === "admin" ||
|
||||
currentUser.role === "superAdmin" ||
|
||||
currentUser.role === "trainer";
|
||||
|
||||
if (!isStaff) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
// 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 NextResponse.json(
|
||||
{ error: "Forbidden - Cannot access users from other gyms" },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetUserId) {
|
||||
const profile = await db.getFitnessProfileByUserId(targetUserId);
|
||||
if (!profile) {
|
||||
return NextResponse.json(
|
||||
{ error: "Profile not found" },
|
||||
@@ -78,8 +116,28 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ profile });
|
||||
}
|
||||
|
||||
const profiles = await db.getAllFitnessProfiles();
|
||||
return NextResponse.json({ profiles });
|
||||
// Staff get gym-scoped profiles
|
||||
const isStaff =
|
||||
currentUser.role === "admin" ||
|
||||
currentUser.role === "superAdmin" ||
|
||||
currentUser.role === "trainer";
|
||||
|
||||
if (isStaff) {
|
||||
const targetGymId =
|
||||
currentUser.role === "superAdmin"
|
||||
? (searchParams.get("gymId") ?? undefined)
|
||||
: (currentUser.gymId ?? undefined);
|
||||
|
||||
const profiles = targetGymId
|
||||
? await getFitnessProfilesByGym(targetGymId)
|
||||
: await db.getAllFitnessProfiles();
|
||||
|
||||
return NextResponse.json({ profiles });
|
||||
}
|
||||
|
||||
// Regular users only get their own
|
||||
const profile = await db.getFitnessProfileByUserId(clerkUserId);
|
||||
return NextResponse.json({ profile: profile ? [profile] : [] });
|
||||
} catch (error) {
|
||||
log.error("Failed to get fitness profiles", error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
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 {
|
||||
@@ -25,38 +27,59 @@ export async function GET(request: NextRequest) {
|
||||
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");
|
||||
|
||||
const db = await getDatabase();
|
||||
|
||||
// If no userId provided, check if staff and return all recommendations
|
||||
// If no userId provided, staff gets gym-scoped recommendations
|
||||
if (!targetUserId) {
|
||||
const currentUser = await db.getUserById(currentUserId);
|
||||
const isStaff =
|
||||
currentUser?.role === "admin" ||
|
||||
currentUser?.role === "superAdmin" ||
|
||||
currentUser?.role === "trainer";
|
||||
currentUser.role === "admin" ||
|
||||
currentUser.role === "superAdmin" ||
|
||||
currentUser.role === "trainer";
|
||||
|
||||
if (!isStaff) {
|
||||
return badRequestResponse("User ID is required");
|
||||
}
|
||||
|
||||
const recommendations = await db.getAllRecommendations();
|
||||
// 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
|
||||
const currentUser = await db.getUserById(currentUserId);
|
||||
// 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";
|
||||
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);
|
||||
|
||||
@@ -24,14 +24,42 @@ import {
|
||||
internalErrorResponse,
|
||||
badRequestResponse,
|
||||
} from "@/lib/api/responses";
|
||||
import { getUsersByGym } from "@/lib/gym-context";
|
||||
import { ensureUserSynced } from "@/lib/sync-user";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// First authenticate the user
|
||||
const { userId: clerkUserId } = await auth();
|
||||
if (!clerkUserId) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
const db = await getDatabase();
|
||||
const currentUser = await ensureUserSynced(clerkUserId, db);
|
||||
|
||||
if (!currentUser) {
|
||||
return forbiddenResponse("User not found");
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const role = searchParams.get("role");
|
||||
|
||||
let users = await db.getAllUsers();
|
||||
// Get target gym based on role
|
||||
const targetGymId =
|
||||
currentUser.role === "superAdmin"
|
||||
? (searchParams.get("gymId") ?? undefined)
|
||||
: (currentUser.gymId ?? undefined);
|
||||
|
||||
// Validate gym access for non-superAdmins
|
||||
if (currentUser.role !== "superAdmin" && !targetGymId) {
|
||||
return forbiddenResponse("No gym assigned");
|
||||
}
|
||||
|
||||
// Get users filtered by gym
|
||||
let users = targetGymId
|
||||
? await getUsersByGym(targetGymId)
|
||||
: await db.getAllUsers();
|
||||
|
||||
// Hydrate gymId from raw DB to ensure consistency with writes
|
||||
const rawUserRows = await rawDb.all(sql`SELECT id, gym_id FROM users`);
|
||||
|
||||
+32
-14
@@ -14,11 +14,26 @@ import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
|
||||
import { useDashboardStats } from "@/hooks/use-api";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { GymSelector } from "@/components/gym/GymSelector";
|
||||
|
||||
export default function Home() {
|
||||
const { data: stats, isLoading, refetch, isFetching } = useDashboardStats();
|
||||
const { user } = useUser();
|
||||
const searchParams = useSearchParams();
|
||||
const gymId = searchParams.get("gymId") ?? undefined;
|
||||
|
||||
const {
|
||||
data: stats,
|
||||
isLoading,
|
||||
refetch,
|
||||
isFetching,
|
||||
} = useDashboardStats(gymId);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Get user role from metadata
|
||||
const userRole = (user?.publicMetadata?.role as string) ?? "client";
|
||||
|
||||
const handleRefresh = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["dashboard-stats"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["analytics"] });
|
||||
@@ -40,18 +55,21 @@ export default function Home() {
|
||||
title="Dashboard"
|
||||
description="Welcome back! Here's what's happening with your gym today."
|
||||
actions={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={isFetching}
|
||||
className="gap-2"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${isFetching ? "animate-spin" : ""}`}
|
||||
/>
|
||||
Refresh
|
||||
</Button>
|
||||
<div className="flex items-center gap-3">
|
||||
<GymSelector userRole={userRole} />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={isFetching}
|
||||
className="gap-2"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${isFetching ? "animate-spin" : ""}`}
|
||||
/>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -125,7 +143,7 @@ export default function Home() {
|
||||
Overview of your gym metrics
|
||||
</p>
|
||||
</div>
|
||||
<AnalyticsDashboard />
|
||||
<AnalyticsDashboard gymId={gymId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { UserManagement } from "@/components/users/UserManagement";
|
||||
import { PageHeader } from "@/components/ui/PageHeader";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { GymSelector } from "@/components/gym/GymSelector";
|
||||
|
||||
export default function UsersPage() {
|
||||
const { user } = useUser();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const userRole = (user?.publicMetadata?.role as string) ?? "client";
|
||||
const gymId = searchParams.get("gymId") ?? undefined;
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<PageHeader
|
||||
title="Users"
|
||||
description="Manage your gym members, trainers, and administrators"
|
||||
breadcrumbs={[{ label: "Users", href: "/users" }]}
|
||||
actions={<GymSelector userRole={userRole} />}
|
||||
/>
|
||||
|
||||
<div className="card-modern">
|
||||
<UserManagement />
|
||||
<UserManagement gymId={gymId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user