diff --git a/apps/admin/next-env.d.ts b/apps/admin/next-env.d.ts
index c4b7818..9edff1c 100644
--- a/apps/admin/next-env.d.ts
+++ b/apps/admin/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/dev/types/routes.d.ts";
+import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/apps/admin/src/app/api/admin/trainers/route.ts b/apps/admin/src/app/api/admin/trainers/route.ts
index d112055..1db2fbc 100644
--- a/apps/admin/src/app/api/admin/trainers/route.ts
+++ b/apps/admin/src/app/api/admin/trainers/route.ts
@@ -38,7 +38,7 @@ export async function GET(req: Request) {
if (!user.gymId) {
return new NextResponse("Admin gymId not set", { status: 400 });
}
- targetGymId = user.gymId;
+ targetGymId = user.gymId as string;
} else if (user.role === "superAdmin") {
targetGymId = requestedGymId;
}
diff --git a/apps/admin/src/app/api/users/gym/route.ts b/apps/admin/src/app/api/users/gym/route.ts
index 98ae14c..93efe00 100644
--- a/apps/admin/src/app/api/users/gym/route.ts
+++ b/apps/admin/src/app/api/users/gym/route.ts
@@ -1,6 +1,6 @@
-import { NextResponse } from 'next/server'
-import { auth } from '@clerk/nextjs/server'
-import { db, users as usersTable, gyms as gymsTable, eq } from '@fitai/database'
+import { NextResponse } from "next/server";
+import { auth } from "@clerk/nextjs/server";
+import { db, users as usersTable, eq, sql } from "@fitai/database";
/**
* PATCH /api/users/gym
@@ -11,45 +11,57 @@ import { db, users as usersTable, gyms as gymsTable, eq } from '@fitai/database'
*/
export async function PATCH(req: Request) {
try {
- const { userId } = await auth()
- if (!userId) return new NextResponse('Unauthorized', { status: 401 })
+ const { userId } = await auth();
+ if (!userId) return new NextResponse("Unauthorized", { status: 401 });
- const body = await req.json().catch(() => null)
- if (!body || typeof body !== 'object' || !('gymId' in body)) {
- return NextResponse.json({ error: 'gymId is required in body (can be null)' }, { status: 400 })
+ const body = await req.json().catch(() => null);
+ if (!body || typeof body !== "object" || !("gymId" in body)) {
+ return NextResponse.json(
+ { error: "gymId is required in body (can be null)" },
+ { status: 400 },
+ );
}
- const gymId = body.gymId === null ? null : String(body.gymId)
+ const gymId = body.gymId === null ? null : String(body.gymId);
// Ensure user exists
- const user = await db.select().from(usersTable).where(eq(usersTable.id, userId)).get()
- if (!user) return new NextResponse('User not found', { status: 404 })
+ const user = await db
+ .select()
+ .from(usersTable)
+ .where(eq(usersTable.id, userId))
+ .get();
+ if (!user) return new NextResponse("User not found", { status: 404 });
// Validate gym when provided
if (gymId) {
- const gym = await db.select().from(gymsTable).where(eq(gymsTable.id, gymId)).get()
+ const rows = await db.all(
+ sql`SELECT status FROM gyms WHERE id = ${gymId} LIMIT 1`,
+ );
+ const gym = rows?.[0] as { status?: string } | undefined;
if (!gym) {
- return NextResponse.json({ error: 'Gym not found' }, { status: 404 })
+ return NextResponse.json({ error: "Gym not found" }, { status: 404 });
}
- if (gym.status !== 'active') {
- return NextResponse.json({ error: 'Gym is not active' }, { status: 400 })
+ if (gym.status !== "active") {
+ return NextResponse.json(
+ { error: "Gym is not active" },
+ { status: 400 },
+ );
}
}
// Update user's gym selection
- await db
- .update(usersTable)
- .set({
- gymId: gymId ?? null,
- updatedAt: new Date(),
- })
- .where(eq(usersTable.id, userId))
- .run()
+ await db.run(
+ sql`UPDATE users SET gym_id = ${gymId ?? null}, updated_at = ${new Date()} WHERE id = ${userId}`,
+ );
- const updated = await db.select().from(usersTable).where(eq(usersTable.id, userId)).get()
- return NextResponse.json(updated)
+ const updated = await db
+ .select()
+ .from(usersTable)
+ .where(eq(usersTable.id, userId))
+ .get();
+ return NextResponse.json(updated);
} catch (error) {
- console.error('PATCH /users/gym error:', error)
- return new NextResponse('Internal Server Error', { status: 500 })
+ console.error("PATCH /users/gym error:", error);
+ return new NextResponse("Internal Server Error", { status: 500 });
}
}
diff --git a/apps/admin/src/app/api/users/route.ts b/apps/admin/src/app/api/users/route.ts
index 2737e6f..28aac92 100644
--- a/apps/admin/src/app/api/users/route.ts
+++ b/apps/admin/src/app/api/users/route.ts
@@ -304,7 +304,7 @@ export async function PUT(request: NextRequest) {
role: role ?? existingUser.role,
phone: phone !== undefined ? phone : existingUser.phone,
gymId: gymId !== undefined ? gymId : existingUser.gymId,
- } as any);
+ });
return NextResponse.json({ success: true });
} catch (error) {
diff --git a/apps/admin/src/app/api/webhooks/route.ts b/apps/admin/src/app/api/webhooks/route.ts
index e67feae..4e4282e 100644
--- a/apps/admin/src/app/api/webhooks/route.ts
+++ b/apps/admin/src/app/api/webhooks/route.ts
@@ -86,7 +86,7 @@ export async function POST(req: Request) {
| "trainer"
| "client"
| "generalUser") || "client";
- const gymId = (public_metadata?.gymId as string | null) ?? null;
+ let gymId = (public_metadata?.gymId as string | null) ?? null;
const inviterUserId =
(public_metadata?.inviterUserId as string | undefined) ?? undefined;
const roleAssigned =
@@ -147,8 +147,8 @@ export async function POST(req: Request) {
inviterUserId
) {
const inviterGymRow = db
- .prepare("SELECT gymId FROM users WHERE id = ?")
- .get(inviterUserId) as { gymId?: string } | undefined;
+ .prepare("SELECT gym_id FROM users WHERE id = ?")
+ .get(inviterUserId) as { gym_id?: string } | undefined;
if (inviterGymRow?.gym_id) {
const inheritStmt = db.prepare(`
@@ -194,7 +194,7 @@ export async function POST(req: Request) {
| "trainer"
| "client"
| "generalUser") || "client";
- const gymId = (public_metadata?.gymId as string | null) ?? null;
+ let gymId = (public_metadata?.gymId as string | null) ?? null;
// Update user in database
const now = new Date().toISOString();
@@ -225,7 +225,7 @@ export async function POST(req: Request) {
.prepare("SELECT gym_id FROM users WHERE id = ?")
.get(inviterUserId) as { gym_id?: string } | undefined;
- if (inviterGymRow?.gymId) {
+ if (inviterGymRow?.gym_id) {
const inheritStmt = db.prepare(`
UPDATE users
SET gym_id = ?, updated_at = ?
diff --git a/apps/admin/src/lib/database/types.ts b/apps/admin/src/lib/database/types.ts
index 0804d2d..174a95d 100644
--- a/apps/admin/src/lib/database/types.ts
+++ b/apps/admin/src/lib/database/types.ts
@@ -1,13 +1,21 @@
-import { User as SharedUser, Client, FitnessProfile, Attendance, Recommendation, FitnessGoal } from "@fitai/shared";
+import {
+ User as SharedUser,
+ Client,
+ FitnessProfile,
+ Attendance,
+ Recommendation,
+ FitnessGoal,
+} from "@fitai/shared";
// Database Entity Types
export interface User extends SharedUser {
+ // Explicitly include gymId so server imports can rely on it even if shared types lag behind build
+ gymId?: string;
password: string;
}
export type { Client, FitnessProfile, Attendance, Recommendation, FitnessGoal };
-
// Database Interface - allows us to swap implementations
export interface IDatabase {
// Connection management
@@ -58,7 +66,10 @@ export interface IDatabase {
// Recommendation operations
createRecommendation(
- recommendation: Omit,
+ recommendation: Omit<
+ Recommendation,
+ "createdAt" | "approvedAt" | "approvedBy"
+ >,
): Promise;
getRecommendationsByUserId(userId: string): Promise;
getAllRecommendations(): Promise;
@@ -70,16 +81,22 @@ export interface IDatabase {
// Fitness Goals operations
createFitnessGoal(
- goal: Omit
+ goal: Omit,
): Promise;
getFitnessGoalById(id: string): Promise;
- getFitnessGoalsByUserId(userId: string, status?: string): Promise;
+ getFitnessGoalsByUserId(
+ userId: string,
+ status?: string,
+ ): Promise;
updateFitnessGoal(
id: string,
- updates: Partial
+ updates: Partial,
): Promise;
deleteFitnessGoal(id: string): Promise;
- updateGoalProgress(id: string, currentValue: number): Promise;
+ updateGoalProgress(
+ id: string,
+ currentValue: number,
+ ): Promise;
completeGoal(id: string): Promise;
// Dashboard operations
diff --git a/apps/admin/src/lib/sync-user.ts b/apps/admin/src/lib/sync-user.ts
index 410dcfd..4965b42 100644
--- a/apps/admin/src/lib/sync-user.ts
+++ b/apps/admin/src/lib/sync-user.ts
@@ -1,62 +1,72 @@
-import { currentUser } from '@clerk/nextjs/server'
-import { IDatabase } from './database/types'
+import { currentUser } from "@clerk/nextjs/server";
+import type { IDatabase, User } from "./database/types";
-export async function ensureUserSynced(userId: string, db: IDatabase) {
- const existingUser = await db.getUserById(userId)
- if (existingUser) return existingUser
+export async function ensureUserSynced(
+ userId: string,
+ db: IDatabase,
+): Promise {
+ const existingUser = await db.getUserById(userId);
+ if (existingUser) return existingUser;
- console.log('User not found in DB by ID, checking Clerk:', userId)
- const clerkUser = await currentUser()
+ console.log("User not found in DB by ID, checking Clerk:", userId);
+ const clerkUser = await currentUser();
- if (!clerkUser || clerkUser.id !== userId) {
- // If we can't get the user from Clerk (e.g. running locally without full auth sync),
- // we might want to fail gracefully or throw.
- // For now, throw to be safe.
- throw new Error('Could not fetch Clerk user details')
- }
+ if (!clerkUser || clerkUser.id !== userId) {
+ // If we can't get the user from Clerk (e.g. running locally without full auth sync),
+ // we might want to fail gracefully or throw.
+ // For now, throw to be safe.
+ throw new Error("Could not fetch Clerk user details");
+ }
- const email = clerkUser.emailAddresses[0]?.emailAddress
- if (!email) throw new Error('User has no email')
+ const email = clerkUser.emailAddresses[0]?.emailAddress;
+ if (!email) throw new Error("User has no email");
- // Check if user exists by email (e.g. seeded user)
- const existingByEmail = await db.getUserByEmail(email)
- if (existingByEmail) {
- console.log('User found by email but ID mismatch. Migrating ID...', {
- oldId: existingByEmail.id,
- newId: userId
- })
+ // Check if user exists by email (e.g. seeded user)
+ const existingByEmail = await db.getUserByEmail(email);
+ if (existingByEmail) {
+ console.log("User found by email but ID mismatch. Migrating ID...", {
+ oldId: existingByEmail.id,
+ newId: userId,
+ });
- // Update the ID to match Clerk ID
- // We need to do this manually via SQL because IDatabase interface might not expose a direct ID update method easily
- // But we can use a raw query if we had access, or add a method.
- // Since we don't have direct access to `db.db` here (it's hidden behind IDatabase),
- // we should add a method to IDatabase or use a workaround.
- // Actually, `SQLiteDatabase` is what we have at runtime.
- // Let's assume we can cast it or add `updateUserId` to the interface.
+ // Update the ID to match Clerk ID
+ // We need to do this manually via SQL because IDatabase interface might not expose a direct ID update method easily
+ // But we can use a raw query if we had access, or add a method.
+ // Since we don't have direct access to `db.db` here (it's hidden behind IDatabase),
+ // we should add a method to IDatabase or use a workaround.
+ // Actually, `SQLiteDatabase` is what we have at runtime.
+ // Let's assume we can cast it or add `updateUserId` to the interface.
- // For now, let's try to update it using `updateUser` but `updateUser` usually updates fields based on ID.
- // We can't update the ID itself using `updateUser(id, { id: newId })` because `updateUser` implementation filters out `id` from updates.
+ // For now, let's try to update it using `updateUser` but `updateUser` usually updates fields based on ID.
+ // We can't update the ID itself using `updateUser(id, { id: newId })` because `updateUser` implementation filters out `id` from updates.
- // We need to add a method to migrate user ID in IDatabase.
- // Or, strictly for this prototype, we can delete the old user and create a new one (DATA LOSS RISK).
- // But since it's a seeded super admin with no data, it's fine.
- // However, if they had clients, we'd lose the link.
+ // We need to add a method to migrate user ID in IDatabase.
+ // Or, strictly for this prototype, we can delete the old user and create a new one (DATA LOSS RISK).
+ // But since it's a seeded super admin with no data, it's fine.
+ // However, if they had clients, we'd lose the link.
- // Let's add `migrateUserId(oldId, newId)` to IDatabase.
- await db.migrateUserId(existingByEmail.id, userId)
- return db.getUserById(userId)
- }
+ // Let's add `migrateUserId(oldId, newId)` to IDatabase.
+ await db.migrateUserId(existingByEmail.id, userId);
+ return db.getUserById(userId);
+ }
- console.log('Creating new user from Clerk data:', userId)
- const user = await db.createUser({
- id: userId,
- email,
- firstName: clerkUser.firstName || '',
- lastName: clerkUser.lastName || '',
- password: '', // Managed by Clerk
- phone: clerkUser.phoneNumbers[0]?.phoneNumber || undefined,
- role: (clerkUser.publicMetadata.role as 'admin' | 'client' | 'superAdmin') || 'client'
- })
+ console.log("Creating new user from Clerk data:", userId);
+ const user = await db.createUser({
+ id: userId,
+ email,
+ firstName: clerkUser.firstName || "",
+ lastName: clerkUser.lastName || "",
+ password: "", // Managed by Clerk
+ phone: clerkUser.phoneNumbers[0]?.phoneNumber || undefined,
+ gymId: (clerkUser.publicMetadata.gymId as string | undefined) || undefined,
+ role: ((): any => {
+ const r = clerkUser.publicMetadata.role as string | undefined;
+ return r &&
+ ["superAdmin", "admin", "trainer", "client", "generalUser"].includes(r)
+ ? r
+ : "client";
+ })(),
+ });
- return user
+ return user;
}
diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts
index a68c5bb..c22cc82 100644
--- a/packages/shared/src/types/index.ts
+++ b/packages/shared/src/types/index.ts
@@ -5,7 +5,7 @@ export interface User {
lastName: string;
password?: string;
phone?: string;
- role: "superAdmin" | "admin" | "trainer" | "client" | "generalUser";
+ role: "superAdmin" | "admin" | "trainer" | "client";
gymId?: string;
imageUrl?: string;
createdAt: Date;