working on onboarding flow

This commit is contained in:
dimitar committed 2025-12-13 06:26:23 +01:00
1 parent 868eaa5e3d
commit d3a36b6103
19 files changed
+2062 -393

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
import { NextResponse } from 'next/server'
import { auth } from '@clerk/nextjs/server'
import { db, users as usersTable, gyms as gymsTable, eq } from '@fitai/database'
/**
* PATCH /api/users/gym
* Body: { gymId: string | null }
* - Updates the current authenticated user's gym selection.
* - gymId can be null to proceed without a gym.
* - If gymId is provided, it must exist and be active.
*/
export async function PATCH(req: Request) {
try {
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 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 })
// Validate gym when provided
if (gymId) {
const gym = await db.select().from(gymsTable).where(eq(gymsTable.id, gymId)).get()
if (!gym) {
return NextResponse.json({ error: 'Gym not found' }, { status: 404 })
}
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()
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 })
}
}
+90 -24
View File
@@ -45,14 +45,14 @@ export async function GET(request: NextRequest) {
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
checkInsThisWeek = attendanceHistory.filter(
a => new Date(a.checkInTime) >= weekAgo
(a) => new Date(a.checkInTime) >= weekAgo,
).length;
// Calculate check-ins in last 30 days
const monthAgo = new Date();
monthAgo.setDate(monthAgo.getDate() - 30);
checkInsThisMonth = attendanceHistory.filter(
a => new Date(a.checkInTime) >= monthAgo
(a) => new Date(a.checkInTime) >= monthAgo,
).length;
}
@@ -63,7 +63,7 @@ export async function GET(request: NextRequest) {
checkInTime,
lastCheckInTime,
checkInsThisWeek,
checkInsThisMonth
checkInsThisMonth,
};
}),
);
@@ -88,12 +88,12 @@ export async function POST(request: NextRequest) {
const db = await getDatabase();
// Get current user to check role
// Note: In a real app, we'd map Clerk ID to our DB ID.
// Note: In a real app, we'd map Clerk ID to our DB ID.
// For now, we'll assume we can find the user by some means or trust the Clerk metadata if we synced it.
// Since we don't have Clerk ID in our local DB users table yet (we only have our own ID),
// we might need to rely on the user being synced.
// Let's assume the user calling this API is already in our DB.
// For the prototype, we'll fetch the user by matching the Clerk ID if we stored it,
// For the prototype, we'll fetch the user by matching the Clerk ID if we stored it,
// OR we'll assume the first user is Super Admin if no users exist?
// Actually, we should look up the user by email if we can't by ID, or add a clerkId column.
// For this step, let's assume we can get the user.
@@ -105,7 +105,10 @@ export async function POST(request: NextRequest) {
const currentUser = await db.getUserById(clerkUserId);
if (!currentUser) {
return NextResponse.json({ error: "Current user not found in database" }, { status: 403 });
return NextResponse.json(
{ error: "Current user not found in database" },
{ status: 403 },
);
}
const body = await request.json();
@@ -123,14 +126,14 @@ export async function POST(request: NextRequest) {
superAdmin: ["admin", "trainer", "client"],
admin: ["trainer", "client"],
trainer: ["client"],
client: []
client: [],
};
const userRole = currentUser.role as keyof typeof allowed;
if (!allowed[userRole] || !allowed[userRole].includes(role)) {
return NextResponse.json(
{ error: `You are not authorized to create a ${role}` },
{ status: 403 }
{ status: 403 },
);
}
@@ -152,20 +155,24 @@ export async function POST(request: NextRequest) {
publicMetadata: {
role,
},
ignoreExisting: true // Don't fail if invite exists
ignoreExisting: true, // Don't fail if invite exists
});
} catch (clerkError: any) {
console.error("Clerk invitation error:", clerkError);
// If user already exists in Clerk, we might want to handle it.
// But for now, let's proceed to create local record if invite sent or if they exist.
if (clerkError.errors?.[0]?.code === 'form_identifier_exists') {
if (clerkError.errors?.[0]?.code === "form_identifier_exists") {
return NextResponse.json(
{ error: "User already exists in Clerk system" },
{ status: 409 },
);
}
return NextResponse.json(
{ error: "Failed to send invitation: " + (clerkError.message || "Unknown error") },
{
error:
"Failed to send invitation: " +
(clerkError.message || "Unknown error"),
},
{ status: 500 },
);
}
@@ -182,16 +189,19 @@ export async function POST(request: NextRequest) {
});
// If creating a client, create the client record too
if (role === 'client') {
if (role === "client") {
await db.createClient({
userId: newUserId.id,
membershipType: 'basic',
membershipStatus: 'active',
joinDate: new Date()
membershipType: "basic",
membershipStatus: "active",
joinDate: new Date(),
});
}
return NextResponse.json({ userId: newUserId.id, message: "Invitation sent" }, { status: 201 });
return NextResponse.json(
{ userId: newUserId.id, message: "Invitation sent" },
{ status: 201 },
);
} catch (error) {
console.error("Create user error:", error);
return NextResponse.json(
@@ -205,7 +215,7 @@ export async function PUT(request: NextRequest) {
try {
const db = await getDatabase();
const body = await request.json();
const { id, email, firstName, lastName, role, phone } = body;
const { id, email, firstName, lastName, role, phone, gymId } = body;
if (!id) {
return NextResponse.json(
@@ -214,12 +224,43 @@ export async function PUT(request: NextRequest) {
);
}
// Get existing user
// Authenticate requester
const { userId: requesterId } = await auth();
if (!requesterId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Fetch requester and target user
const requester = await db.getUserById(requesterId);
if (!requester) {
return NextResponse.json(
{ error: "Requester not found" },
{ status: 403 },
);
}
const existingUser = await db.getUserById(id);
if (!existingUser) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
// Authorization: determine allowed role changes
const requesterRole = requester.role;
const allowedByRole: Record<string, string[]> = {
superAdmin: ["superAdmin", "admin", "trainer", "client", "generalUser"],
admin: ["admin", "trainer", "client", "generalUser"],
trainer: [], // trainers cannot change roles
client: [], // clients cannot change roles
generalUser: [], // general users cannot change roles
};
if (role && !allowedByRole[requesterRole]?.includes(role)) {
return NextResponse.json(
{ error: `Not authorized to assign role '${role}'` },
{ status: 403 },
);
}
// Check if email is being changed and if it's already taken
if (email && email !== existingUser.email) {
const userWithEmail = await db.getUserByEmail(email);
@@ -231,14 +272,39 @@ export async function PUT(request: NextRequest) {
}
}
// Update user
// Update Clerk publicMetadata (role/gymId) to propagate via webhook
// Note: Only update metadata when a change is requested
try {
const client = await clerkClient();
const publicMetadata: Record<string, unknown> = {};
if (role) {
publicMetadata.role = role;
}
if (gymId !== undefined) {
publicMetadata.gymId = gymId === null ? null : String(gymId);
}
if (Object.keys(publicMetadata).length > 0) {
await client.users.updateUser(id, { publicMetadata });
}
} catch (clerkErr: any) {
console.error("Clerk metadata update error:", clerkErr);
return NextResponse.json(
{ error: "Failed to update role/gym in identity provider" },
{ status: 500 },
);
}
// Update local DB for immediate UI feedback (webhook will also sync)
await db.updateUser(id, {
email: email || existingUser.email,
firstName: firstName || existingUser.firstName,
lastName: lastName || existingUser.lastName,
role: role || existingUser.role,
email: email ?? existingUser.email,
firstName: firstName ?? existingUser.firstName,
lastName: lastName ?? existingUser.lastName,
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) {