build errors fixed

This commit is contained in:
echo 2025-12-18 17:19:11 +01:00
parent d3a36b6103
commit 6580564767
8 changed files with 132 additions and 93 deletions

View File

@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@ -38,7 +38,7 @@ export async function GET(req: Request) {
if (!user.gymId) { if (!user.gymId) {
return new NextResponse("Admin gymId not set", { status: 400 }); return new NextResponse("Admin gymId not set", { status: 400 });
} }
targetGymId = user.gymId; targetGymId = user.gymId as string;
} else if (user.role === "superAdmin") { } else if (user.role === "superAdmin") {
targetGymId = requestedGymId; targetGymId = requestedGymId;
} }

View File

@ -1,6 +1,6 @@
import { NextResponse } from 'next/server' import { NextResponse } from "next/server";
import { auth } from '@clerk/nextjs/server' import { auth } from "@clerk/nextjs/server";
import { db, users as usersTable, gyms as gymsTable, eq } from '@fitai/database' import { db, users as usersTable, eq, sql } from "@fitai/database";
/** /**
* PATCH /api/users/gym * 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) { export async function PATCH(req: Request) {
try { try {
const { userId } = await auth() const { userId } = await auth();
if (!userId) return new NextResponse('Unauthorized', { status: 401 }) if (!userId) return new NextResponse("Unauthorized", { status: 401 });
const body = await req.json().catch(() => null) const body = await req.json().catch(() => null);
if (!body || typeof body !== 'object' || !('gymId' in body)) { if (!body || typeof body !== "object" || !("gymId" in body)) {
return NextResponse.json({ error: 'gymId is required in body (can be null)' }, { status: 400 }) 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 // Ensure user exists
const user = await db.select().from(usersTable).where(eq(usersTable.id, userId)).get() const user = await db
if (!user) return new NextResponse('User not found', { status: 404 }) .select()
.from(usersTable)
.where(eq(usersTable.id, userId))
.get();
if (!user) return new NextResponse("User not found", { status: 404 });
// Validate gym when provided // Validate gym when provided
if (gymId) { 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) { if (!gym) {
return NextResponse.json({ error: 'Gym not found' }, { status: 404 }) return NextResponse.json({ error: "Gym not found" }, { status: 404 });
} }
if (gym.status !== 'active') { if (gym.status !== "active") {
return NextResponse.json({ error: 'Gym is not active' }, { status: 400 }) return NextResponse.json(
{ error: "Gym is not active" },
{ status: 400 },
);
} }
} }
// Update user's gym selection // Update user's gym selection
await db await db.run(
.update(usersTable) sql`UPDATE users SET gym_id = ${gymId ?? null}, updated_at = ${new Date()} WHERE id = ${userId}`,
.set({ );
gymId: gymId ?? null,
updatedAt: new Date(),
})
.where(eq(usersTable.id, userId))
.run()
const updated = await db.select().from(usersTable).where(eq(usersTable.id, userId)).get() const updated = await db
return NextResponse.json(updated) .select()
.from(usersTable)
.where(eq(usersTable.id, userId))
.get();
return NextResponse.json(updated);
} catch (error) { } catch (error) {
console.error('PATCH /users/gym error:', error) console.error("PATCH /users/gym error:", error);
return new NextResponse('Internal Server Error', { status: 500 }) return new NextResponse("Internal Server Error", { status: 500 });
} }
} }

View File

@ -304,7 +304,7 @@ export async function PUT(request: NextRequest) {
role: role ?? existingUser.role, role: role ?? existingUser.role,
phone: phone !== undefined ? phone : existingUser.phone, phone: phone !== undefined ? phone : existingUser.phone,
gymId: gymId !== undefined ? gymId : existingUser.gymId, gymId: gymId !== undefined ? gymId : existingUser.gymId,
} as any); });
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} catch (error) { } catch (error) {

View File

@ -86,7 +86,7 @@ export async function POST(req: Request) {
| "trainer" | "trainer"
| "client" | "client"
| "generalUser") || "client"; | "generalUser") || "client";
const gymId = (public_metadata?.gymId as string | null) ?? null; let gymId = (public_metadata?.gymId as string | null) ?? null;
const inviterUserId = const inviterUserId =
(public_metadata?.inviterUserId as string | undefined) ?? undefined; (public_metadata?.inviterUserId as string | undefined) ?? undefined;
const roleAssigned = const roleAssigned =
@ -147,8 +147,8 @@ export async function POST(req: Request) {
inviterUserId inviterUserId
) { ) {
const inviterGymRow = db const inviterGymRow = db
.prepare("SELECT gymId FROM users WHERE id = ?") .prepare("SELECT gym_id FROM users WHERE id = ?")
.get(inviterUserId) as { gymId?: string } | undefined; .get(inviterUserId) as { gym_id?: string } | undefined;
if (inviterGymRow?.gym_id) { if (inviterGymRow?.gym_id) {
const inheritStmt = db.prepare(` const inheritStmt = db.prepare(`
@ -194,7 +194,7 @@ export async function POST(req: Request) {
| "trainer" | "trainer"
| "client" | "client"
| "generalUser") || "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 // Update user in database
const now = new Date().toISOString(); const now = new Date().toISOString();
@ -225,7 +225,7 @@ export async function POST(req: Request) {
.prepare("SELECT gym_id FROM users WHERE id = ?") .prepare("SELECT gym_id FROM users WHERE id = ?")
.get(inviterUserId) as { gym_id?: string } | undefined; .get(inviterUserId) as { gym_id?: string } | undefined;
if (inviterGymRow?.gymId) { if (inviterGymRow?.gym_id) {
const inheritStmt = db.prepare(` const inheritStmt = db.prepare(`
UPDATE users UPDATE users
SET gym_id = ?, updated_at = ? SET gym_id = ?, updated_at = ?

View File

@ -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 // Database Entity Types
export interface User extends SharedUser { 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; password: string;
} }
export type { Client, FitnessProfile, Attendance, Recommendation, FitnessGoal }; export type { Client, FitnessProfile, Attendance, Recommendation, FitnessGoal };
// Database Interface - allows us to swap implementations // Database Interface - allows us to swap implementations
export interface IDatabase { export interface IDatabase {
// Connection management // Connection management
@ -58,7 +66,10 @@ export interface IDatabase {
// Recommendation operations // Recommendation operations
createRecommendation( createRecommendation(
recommendation: Omit<Recommendation, "createdAt" | "approvedAt" | "approvedBy">, recommendation: Omit<
Recommendation,
"createdAt" | "approvedAt" | "approvedBy"
>,
): Promise<Recommendation>; ): Promise<Recommendation>;
getRecommendationsByUserId(userId: string): Promise<Recommendation[]>; getRecommendationsByUserId(userId: string): Promise<Recommendation[]>;
getAllRecommendations(): Promise<Recommendation[]>; getAllRecommendations(): Promise<Recommendation[]>;
@ -70,16 +81,22 @@ export interface IDatabase {
// Fitness Goals operations // Fitness Goals operations
createFitnessGoal( createFitnessGoal(
goal: Omit<FitnessGoal, "createdAt" | "updatedAt"> goal: Omit<FitnessGoal, "createdAt" | "updatedAt">,
): Promise<FitnessGoal>; ): Promise<FitnessGoal>;
getFitnessGoalById(id: string): Promise<FitnessGoal | null>; getFitnessGoalById(id: string): Promise<FitnessGoal | null>;
getFitnessGoalsByUserId(userId: string, status?: string): Promise<FitnessGoal[]>; getFitnessGoalsByUserId(
userId: string,
status?: string,
): Promise<FitnessGoal[]>;
updateFitnessGoal( updateFitnessGoal(
id: string, id: string,
updates: Partial<FitnessGoal> updates: Partial<FitnessGoal>,
): Promise<FitnessGoal | null>; ): Promise<FitnessGoal | null>;
deleteFitnessGoal(id: string): Promise<boolean>; deleteFitnessGoal(id: string): Promise<boolean>;
updateGoalProgress(id: string, currentValue: number): Promise<FitnessGoal | null>; updateGoalProgress(
id: string,
currentValue: number,
): Promise<FitnessGoal | null>;
completeGoal(id: string): Promise<FitnessGoal | null>; completeGoal(id: string): Promise<FitnessGoal | null>;
// Dashboard operations // Dashboard operations

View File

@ -1,62 +1,72 @@
import { currentUser } from '@clerk/nextjs/server' import { currentUser } from "@clerk/nextjs/server";
import { IDatabase } from './database/types' import type { IDatabase, User } from "./database/types";
export async function ensureUserSynced(userId: string, db: IDatabase) { export async function ensureUserSynced(
const existingUser = await db.getUserById(userId) userId: string,
if (existingUser) return existingUser db: IDatabase,
): Promise<User | null> {
const existingUser = await db.getUserById(userId);
if (existingUser) return existingUser;
console.log('User not found in DB by ID, checking Clerk:', userId) console.log("User not found in DB by ID, checking Clerk:", userId);
const clerkUser = await currentUser() const clerkUser = await currentUser();
if (!clerkUser || clerkUser.id !== userId) { if (!clerkUser || clerkUser.id !== userId) {
// If we can't get the user from Clerk (e.g. running locally without full auth sync), // 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. // we might want to fail gracefully or throw.
// For now, throw to be safe. // For now, throw to be safe.
throw new Error('Could not fetch Clerk user details') throw new Error("Could not fetch Clerk user details");
} }
const email = clerkUser.emailAddresses[0]?.emailAddress const email = clerkUser.emailAddresses[0]?.emailAddress;
if (!email) throw new Error('User has no email') if (!email) throw new Error("User has no email");
// Check if user exists by email (e.g. seeded user) // Check if user exists by email (e.g. seeded user)
const existingByEmail = await db.getUserByEmail(email) const existingByEmail = await db.getUserByEmail(email);
if (existingByEmail) { if (existingByEmail) {
console.log('User found by email but ID mismatch. Migrating ID...', { console.log("User found by email but ID mismatch. Migrating ID...", {
oldId: existingByEmail.id, oldId: existingByEmail.id,
newId: userId newId: userId,
}) });
// Update the ID to match Clerk ID // 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 // 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. // 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), // 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. // we should add a method to IDatabase or use a workaround.
// Actually, `SQLiteDatabase` is what we have at runtime. // Actually, `SQLiteDatabase` is what we have at runtime.
// Let's assume we can cast it or add `updateUserId` to the interface. // 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. // 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 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. // 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). // 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. // But since it's a seeded super admin with no data, it's fine.
// However, if they had clients, we'd lose the link. // However, if they had clients, we'd lose the link.
// Let's add `migrateUserId(oldId, newId)` to IDatabase. // Let's add `migrateUserId(oldId, newId)` to IDatabase.
await db.migrateUserId(existingByEmail.id, userId) await db.migrateUserId(existingByEmail.id, userId);
return db.getUserById(userId) return db.getUserById(userId);
} }
console.log('Creating new user from Clerk data:', userId) console.log("Creating new user from Clerk data:", userId);
const user = await db.createUser({ const user = await db.createUser({
id: userId, id: userId,
email, email,
firstName: clerkUser.firstName || '', firstName: clerkUser.firstName || "",
lastName: clerkUser.lastName || '', lastName: clerkUser.lastName || "",
password: '', // Managed by Clerk password: "", // Managed by Clerk
phone: clerkUser.phoneNumbers[0]?.phoneNumber || undefined, phone: clerkUser.phoneNumbers[0]?.phoneNumber || undefined,
role: (clerkUser.publicMetadata.role as 'admin' | 'client' | 'superAdmin') || 'client' 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;
} }

View File

@ -5,7 +5,7 @@ export interface User {
lastName: string; lastName: string;
password?: string; password?: string;
phone?: string; phone?: string;
role: "superAdmin" | "admin" | "trainer" | "client" | "generalUser"; role: "superAdmin" | "admin" | "trainer" | "client";
gymId?: string; gymId?: string;
imageUrl?: string; imageUrl?: string;
createdAt: Date; createdAt: Date;