p 1 and 2 compeleted
This commit is contained in:
1 parent
6adb541967
commit
3573709d99
28 files changed
+2533
-240
No files matched your search
@@ -21,7 +21,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
const profile = db
|
||||
.prepare(`SELECT * FROM fitness_profiles WHERE userId = ?`)
|
||||
.prepare(`SELECT * FROM fitness_profiles WHERE user_id = ?`)
|
||||
.get(userId);
|
||||
|
||||
if (profile) {
|
||||
@@ -78,8 +78,8 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Check if profile exists
|
||||
const existingProfile = db
|
||||
.prepare(`SELECT userId FROM fitness_profiles WHERE userId = ?`)
|
||||
.get(userId) as { userId: string } | undefined;
|
||||
.prepare(`SELECT user_id FROM fitness_profiles WHERE user_id = ?`)
|
||||
.get(userId) as { user_id: string } | undefined;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const fitnessGoalsJson = JSON.stringify(fitnessGoals || []);
|
||||
@@ -92,14 +92,13 @@ export async function POST(request: NextRequest) {
|
||||
weight = ?,
|
||||
age = ?,
|
||||
gender = ?,
|
||||
fitnessGoals = ?,
|
||||
activityLevel = ?,
|
||||
medicalConditions = ?,
|
||||
fitness_goals = ?,
|
||||
activity_level = ?,
|
||||
medical_conditions = ?,
|
||||
allergies = ?,
|
||||
injuries = ?,
|
||||
preferences = ?,
|
||||
updatedAt = ?
|
||||
WHERE userId = ?`,
|
||||
updated_at = ?
|
||||
WHERE user_id = ?`,
|
||||
).run(
|
||||
height,
|
||||
weight,
|
||||
@@ -110,7 +109,6 @@ export async function POST(request: NextRequest) {
|
||||
medicalConditions || null,
|
||||
allergies || null,
|
||||
injuries || null,
|
||||
preferences || null,
|
||||
now,
|
||||
userId,
|
||||
);
|
||||
@@ -121,12 +119,15 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
} else {
|
||||
// Create new profile
|
||||
const profileId = `fp_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO fitness_profiles
|
||||
(userId, height, weight, age, gender, fitnessGoals, activityLevel,
|
||||
medicalConditions, allergies, injuries, preferences, createdAt, updatedAt)
|
||||
(id, user_id, height, weight, age, gender, fitness_goals, activity_level,
|
||||
medical_conditions, allergies, injuries, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
).run(
|
||||
profileId,
|
||||
userId,
|
||||
height,
|
||||
weight,
|
||||
@@ -137,7 +138,6 @@ export async function POST(request: NextRequest) {
|
||||
medicalConditions || null,
|
||||
allergies || null,
|
||||
injuries || null,
|
||||
preferences || null,
|
||||
now,
|
||||
now,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth, clerkClient } from "@clerk/nextjs/server";
|
||||
import { db, users, clients } from "@fitai/database";
|
||||
import { createUserSchema } from "@/lib/validation/user-schemas";
|
||||
import { sendWelcomeEmail, sendInvitationEmail } from "@/lib/email";
|
||||
import {
|
||||
successResponse,
|
||||
errorResponse,
|
||||
unauthorizedResponse,
|
||||
} from "@/lib/api/responses";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
/**
|
||||
* POST /api/users/create
|
||||
*
|
||||
* Create a new user with support for:
|
||||
* - Direct creation (database only, no Clerk account)
|
||||
* - Invitation-based creation (sends Clerk invitation email)
|
||||
* - Client-specific fields (membership type, status)
|
||||
* - Welcome emails for clients
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
if (!userId) {
|
||||
return unauthorizedResponse("Unauthorized");
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const validationResult = createUserSchema.safeParse(body);
|
||||
|
||||
if (!validationResult.success) {
|
||||
return errorResponse("Validation failed", {
|
||||
status: 400,
|
||||
details: validationResult.error.flatten().fieldErrors as Record<
|
||||
string,
|
||||
string[]
|
||||
>,
|
||||
});
|
||||
}
|
||||
|
||||
const data = validationResult.data;
|
||||
|
||||
// Note: Email is required in current schema
|
||||
// TODO: Add schema migration to make email optional for direct creation
|
||||
if (!data.email) {
|
||||
return errorResponse("Email is required", { status: 400 });
|
||||
}
|
||||
|
||||
// Case 1: Send Clerk Invitation (requires email)
|
||||
if (data.sendInvitation && data.email) {
|
||||
try {
|
||||
const client = await clerkClient();
|
||||
const invitation = await client.invitations.createInvitation({
|
||||
emailAddress: data.email,
|
||||
publicMetadata: {
|
||||
role: data.role,
|
||||
gymId: data.gymId,
|
||||
},
|
||||
});
|
||||
|
||||
log.info("Clerk invitation sent", {
|
||||
email: data.email,
|
||||
role: data.role,
|
||||
invitationId: invitation.id,
|
||||
});
|
||||
|
||||
// Send custom invitation email (in addition to Clerk's)
|
||||
if (invitation.url) {
|
||||
await sendInvitationEmail(data.email, data.role, invitation.url);
|
||||
}
|
||||
|
||||
return successResponse(
|
||||
{
|
||||
invitation: {
|
||||
id: invitation.id,
|
||||
email: invitation.emailAddress,
|
||||
status: invitation.status,
|
||||
},
|
||||
},
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
log.error("Failed to send Clerk invitation", error);
|
||||
return errorResponse(`Failed to send invitation: ${errorMessage}`, {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: Direct Creation (database only, no Clerk account)
|
||||
// This creates a user record that can later be linked when they sign up
|
||||
|
||||
// Generate a temporary user ID (will be replaced when they sign up with Clerk)
|
||||
const tempUserId = `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// Insert user into database
|
||||
const [newUser] = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
id: tempUserId,
|
||||
email: data.email,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
role: data.role,
|
||||
phone: data.phone,
|
||||
gymId: data.gymId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
log.info("User created in database", {
|
||||
userId: newUser.id,
|
||||
role: data.role,
|
||||
hasEmail: !!data.email,
|
||||
});
|
||||
|
||||
// Case 3: Create client record if role is client
|
||||
if (data.role === "client" && data.membershipType) {
|
||||
const clientId = `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
await db.insert(clients).values({
|
||||
id: clientId,
|
||||
userId: newUser.id,
|
||||
membershipType: data.membershipType,
|
||||
membershipStatus: data.membershipStatus || "active",
|
||||
joinDate: new Date(),
|
||||
});
|
||||
|
||||
log.info("Client record created", {
|
||||
userId: newUser.id,
|
||||
membershipType: data.membershipType,
|
||||
});
|
||||
|
||||
// Send welcome email if requested and email provided
|
||||
if (data.sendWelcomeEmail && data.email) {
|
||||
await sendWelcomeEmail(data.email, data.firstName);
|
||||
log.info("Welcome email sent", { email: data.email });
|
||||
}
|
||||
}
|
||||
|
||||
return successResponse(
|
||||
{
|
||||
user: {
|
||||
id: newUser.id,
|
||||
email: newUser.email,
|
||||
firstName: newUser.firstName,
|
||||
lastName: newUser.lastName,
|
||||
role: newUser.role,
|
||||
},
|
||||
},
|
||||
{ status: 201 },
|
||||
);
|
||||
} catch (error) {
|
||||
log.error("Failed to create user", error);
|
||||
return errorResponse("Failed to create user", { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,30 @@ export async function POST(req: Request) {
|
||||
now,
|
||||
);
|
||||
|
||||
// Auto-create client record for self-registered users (role is "client")
|
||||
if (role === "client") {
|
||||
const clientId = `cli_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
||||
const clientStmt = db.prepare(`
|
||||
INSERT INTO clients (id, user_id, membership_type, membership_status, join_date, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
clientStmt.run(
|
||||
clientId,
|
||||
id,
|
||||
"basic", // Default membership type
|
||||
"active", // Default membership status
|
||||
now,
|
||||
now,
|
||||
now,
|
||||
);
|
||||
|
||||
log.info("Client record auto-created for self-registered user", {
|
||||
userId: id,
|
||||
clientId,
|
||||
});
|
||||
}
|
||||
|
||||
// If this is a client invited by a trainer, create trainer-client link
|
||||
if (roleAssigned === "client" && inviterUserId && gymId) {
|
||||
const inviterRow = db
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
UserButton,
|
||||
} from "@clerk/nextjs";
|
||||
import { Sidebar } from "@/components/ui/Sidebar";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
@@ -28,10 +29,9 @@ export default function RootLayout({
|
||||
<body className={inter.className}>
|
||||
<div className="flex min-h-screen bg-slate-50">
|
||||
<Sidebar />
|
||||
<main className="flex-1 ml-20 p-8">
|
||||
{children}
|
||||
</main>
|
||||
<main className="flex-1 ml-20 p-8">{children}</main>
|
||||
</div>
|
||||
<Toaster richColors position="top-right" />
|
||||
</body>
|
||||
</html>
|
||||
</ClerkProvider>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
import log from "@/lib/logger";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
@@ -68,14 +69,14 @@ export default function RecommendationsPage() {
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
alert(`Error: ${error.error}`);
|
||||
toast.error(`Error: ${error.error}`);
|
||||
} else {
|
||||
alert("Recommendation generated successfully!");
|
||||
toast.success("Recommendation generated successfully!");
|
||||
fetchData(); // Refresh data
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Failed to generate recommendation", error);
|
||||
alert("Failed to generate recommendation.");
|
||||
toast.error("Failed to generate recommendation.");
|
||||
} finally {
|
||||
setGenerating(null);
|
||||
}
|
||||
@@ -98,13 +99,16 @@ export default function RecommendationsPage() {
|
||||
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json();
|
||||
alert(`Failed to update status: ${errorData.error || "Unknown error"}`);
|
||||
toast.error(
|
||||
`Failed to update status: ${errorData.error || "Unknown error"}`,
|
||||
);
|
||||
} else {
|
||||
toast.success("Recommendation status updated");
|
||||
fetchData(); // Refresh data
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Failed to approve recommendation", error);
|
||||
alert("Error processing request");
|
||||
toast.error("Error processing request");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -136,16 +140,16 @@ export default function RecommendationsPage() {
|
||||
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json();
|
||||
alert(
|
||||
toast.error(
|
||||
`Failed to update recommendation: ${errorData.error || "Unknown error"}`,
|
||||
);
|
||||
} else {
|
||||
alert("Recommendation updated successfully!");
|
||||
toast.success("Recommendation updated successfully!");
|
||||
fetchData(); // Refresh data
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Failed to update recommendation", error);
|
||||
alert("Failed to update recommendation.");
|
||||
toast.error("Failed to update recommendation.");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user