diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 0ffdf1e..d803238 100644 Binary files a/apps/admin/data/fitai.db and b/apps/admin/data/fitai.db differ diff --git a/apps/admin/scripts/migrate-roles.js b/apps/admin/scripts/migrate-roles.js new file mode 100644 index 0000000..e207ebe --- /dev/null +++ b/apps/admin/scripts/migrate-roles.js @@ -0,0 +1,62 @@ +const Database = require('better-sqlite3'); +const path = require('path'); + +const dbPath = path.join(__dirname, '../data/fitai.db'); +const db = new Database(dbPath); + +function migrateRoles() { + try { + console.log('Starting migration to update role constraints...'); + + // 1. Disable foreign keys + db.pragma('foreign_keys = OFF'); + + // 2. Start transaction + db.transaction(() => { + // 3. Create new table with updated check constraint + console.log('Creating new users table...'); + db.prepare(` + CREATE TABLE IF NOT EXISTS users_new ( + id TEXT PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + firstName TEXT NOT NULL, + lastName TEXT NOT NULL, + password TEXT NOT NULL, + phone TEXT, + role TEXT NOT NULL CHECK (role IN ('superAdmin', 'admin', 'trainer', 'client')), + createdAt DATETIME NOT NULL, + updatedAt DATETIME NOT NULL + ) + `).run(); + + // 4. Copy data + console.log('Copying data...'); + db.prepare(` + INSERT INTO users_new (id, email, firstName, lastName, password, phone, role, createdAt, updatedAt) + SELECT id, email, firstName, lastName, password, phone, role, createdAt, updatedAt FROM users + `).run(); + + // 5. Drop old table + console.log('Dropping old table...'); + db.prepare('DROP TABLE users').run(); + + // 6. Rename new table + console.log('Renaming new table...'); + db.prepare('ALTER TABLE users_new RENAME TO users').run(); + + // 7. Re-enable foreign keys (in a separate step usually, but good to be safe) + })(); + + db.pragma('foreign_keys = ON'); + + console.log('Migration completed successfully.'); + + } catch (error) { + console.error('Migration failed:', error); + process.exit(1); + } finally { + db.close(); + } +} + +migrateRoles(); diff --git a/apps/admin/scripts/seed-superadmin.js b/apps/admin/scripts/seed-superadmin.js new file mode 100644 index 0000000..7141a4a --- /dev/null +++ b/apps/admin/scripts/seed-superadmin.js @@ -0,0 +1,51 @@ +const Database = require('better-sqlite3'); +const path = require('path'); +const bcrypt = require('bcryptjs'); + +const dbPath = path.join(__dirname, '../data/fitai.db'); +const db = new Database(dbPath); + +async function seedSuperAdmin() { + const email = 'taratur@gmail.com'; + const password = 'password123'; + const firstName = 'Super'; + const lastName = 'Admin'; + + // Hash password + const hashedPassword = await bcrypt.hash(password, 12); + + const id = 'user_superadmin_' + Math.random().toString(36).substr(2, 9); + const now = new Date().toISOString(); + + try { + console.log('Creating Super Admin...'); + + // Check if exists + const existing = db.prepare('SELECT * FROM users WHERE email = ?').get(email); + if (existing) { + console.log('Super Admin already exists. Updating role...'); + db.prepare('UPDATE users SET role = "superAdmin" WHERE email = ?').run(email); + console.log('Role updated.'); + return; + } + + db.prepare(` + INSERT INTO users (id, email, firstName, lastName, password, role, createdAt, updatedAt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `).run(id, email, firstName, lastName, hashedPassword, 'superAdmin', now, now); + + console.log(`Super Admin created successfully.`); + console.log(`Email: ${email}`); + console.log(`Password: ${password}`); + console.log(`ID: ${id}`); + + console.log('\nIMPORTANT: You must also create this user in Clerk manually or sign up with this email to link the accounts if you want to log in as this user.'); + + } catch (error) { + console.error('Error creating Super Admin:', error); + } finally { + db.close(); + } +} + +seedSuperAdmin(); diff --git a/apps/admin/src/app/api/admin/attendance/route.ts b/apps/admin/src/app/api/admin/attendance/route.ts index 3eb3059..4135941 100644 --- a/apps/admin/src/app/api/admin/attendance/route.ts +++ b/apps/admin/src/app/api/admin/attendance/route.ts @@ -10,7 +10,7 @@ export async function GET(req: Request) { const db = await getDatabase() const user = await db.getUserById(userId) - if (!user || user.role !== 'admin') { + if (!user || (user.role !== 'admin' && user.role !== 'superAdmin')) { return new NextResponse('Forbidden', { status: 403 }) } diff --git a/apps/admin/src/app/api/admin/stats/route.ts b/apps/admin/src/app/api/admin/stats/route.ts index 45e4eab..b419d6a 100644 --- a/apps/admin/src/app/api/admin/stats/route.ts +++ b/apps/admin/src/app/api/admin/stats/route.ts @@ -8,6 +8,12 @@ export async function GET() { if (!userId) return new NextResponse('Unauthorized', { status: 401 }) const db = await getDatabase() + const user = await db.getUserById(userId) + + if (!user || (user.role !== 'admin' && user.role !== 'superAdmin')) { + return new NextResponse('Forbidden', { status: 403 }) + } + const stats = await db.getDashboardStats() return NextResponse.json(stats) diff --git a/apps/admin/src/app/api/users/route.ts b/apps/admin/src/app/api/users/route.ts index 8b54bea..aa56c85 100644 --- a/apps/admin/src/app/api/users/route.ts +++ b/apps/admin/src/app/api/users/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getDatabase } from "../../../lib/database/index"; import bcrypt from "bcryptjs"; +import { auth } from "@clerk/nextjs/server"; export async function GET(request: NextRequest) { try { @@ -34,7 +35,34 @@ export async function GET(request: NextRequest) { export async function POST(request: NextRequest) { try { + const { userId: clerkUserId } = await auth(); + if (!clerkUserId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const db = await getDatabase(); + + // Get current user to check role + // 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, + // 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. + + // WAIT: The current `users` table has `id` as a string. Is it the Clerk ID? + // In `sync-user.ts`, we use `evt.data.id` as the `id` when creating the user. + // So yes, `users.id` IS the Clerk ID. + + const currentUser = await db.getUserById(clerkUserId); + + if (!currentUser) { + return NextResponse.json({ error: "Current user not found in database" }, { status: 403 }); + } + const body = await request.json(); const { email, password, firstName, lastName, role, phone } = body; @@ -45,6 +73,22 @@ export async function POST(request: NextRequest) { ); } + // Enforce Hierarchy + const allowed = { + superAdmin: ["admin", "trainer", "client"], // Super Admin can create anyone (except maybe another superAdmin via this UI?) + admin: ["trainer", "client"], + trainer: ["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 } + ); + } + // Check if user already exists const existingUser = await db.getUserByEmail(email); if (existingUser) { @@ -58,7 +102,7 @@ export async function POST(request: NextRequest) { const hashedPassword = await bcrypt.hash(password, 12); // Create user - const userId = await db.createUser({ + const newUserId = await db.createUser({ email, password: hashedPassword, firstName, @@ -67,7 +111,17 @@ export async function POST(request: NextRequest) { phone, }); - return NextResponse.json({ userId }, { status: 201 }); + // If creating a client, create the client record too + if (role === 'client') { + await db.createClient({ + userId: newUserId.id, + membershipType: 'basic', + membershipStatus: 'active', + joinDate: new Date() + }); + } + + return NextResponse.json({ userId: newUserId.id }, { status: 201 }); } catch (error) { console.error("Create user error:", error); return NextResponse.json( diff --git a/apps/admin/src/components/users/UserGrid.tsx b/apps/admin/src/components/users/UserGrid.tsx index f29e4cb..6f0c3b9 100644 --- a/apps/admin/src/components/users/UserGrid.tsx +++ b/apps/admin/src/components/users/UserGrid.tsx @@ -69,8 +69,8 @@ export function UserGrid({ filter: "agTextColumnFilter", sortable: true, cellRenderer: (params: any) => { - if (!params.value) return null; const roleColors = { + superAdmin: "bg-red-100 text-red-800", admin: "bg-purple-100 text-purple-800", trainer: "bg-blue-100 text-blue-800", client: "bg-green-100 text-green-800", @@ -79,7 +79,7 @@ export function UserGrid({ roleColors[params.value as keyof typeof roleColors] || "bg-gray-100 text-gray-800"; - const label = params.value.charAt(0).toUpperCase() + params.value.slice(1); + const label = params.value === 'superAdmin' ? 'Super Admin' : params.value.charAt(0).toUpperCase() + params.value.slice(1); return ( diff --git a/apps/admin/src/components/users/UserManagement.tsx b/apps/admin/src/components/users/UserManagement.tsx index 73ca79c..7aa62b4 100644 --- a/apps/admin/src/components/users/UserManagement.tsx +++ b/apps/admin/src/components/users/UserManagement.tsx @@ -221,6 +221,12 @@ export function UserManagement() { > Admins + @@ -316,10 +322,17 @@ export function UserManagement() { className="w-full border border-gray-300 rounded px-3 py-2" required > + {/* Ideally we fetch current user role to filter these. + For now, we show all but the API will enforce it. + We can add a visual indicator or fetch "me" to filter. */} + +

+ Note: You can only assign roles lower than your own. +

@@ -436,8 +449,8 @@ export function UserManagement() { Last Visit:{" "} {selectedUser.client.lastVisit ? new Date( - selectedUser.client.lastVisit, - ).toLocaleDateString() + selectedUser.client.lastVisit, + ).toLocaleDateString() : "Never"}

diff --git a/apps/admin/src/lib/database/sqlite.ts b/apps/admin/src/lib/database/sqlite.ts index 9877336..a89f4a7 100644 --- a/apps/admin/src/lib/database/sqlite.ts +++ b/apps/admin/src/lib/database/sqlite.ts @@ -60,7 +60,7 @@ export class SQLiteDatabase implements IDatabase { lastName TEXT NOT NULL, password TEXT NOT NULL, phone TEXT, - role TEXT NOT NULL CHECK (role IN ('admin', 'client')), + role TEXT NOT NULL CHECK (role IN ('superAdmin', 'admin', 'trainer', 'client')), createdAt DATETIME NOT NULL, updatedAt DATETIME NOT NULL ) diff --git a/apps/admin/src/lib/database/types.ts b/apps/admin/src/lib/database/types.ts index 16de2b5..993da2a 100644 --- a/apps/admin/src/lib/database/types.ts +++ b/apps/admin/src/lib/database/types.ts @@ -6,7 +6,7 @@ export interface User { lastName: string; password: string; phone?: string; - role: "admin" | "trainer" | "client"; + role: "superAdmin" | "admin" | "trainer" | "client"; createdAt: Date; updatedAt: Date; } diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx index 62b58cd..850d91e 100644 --- a/apps/mobile/src/app/(tabs)/index.tsx +++ b/apps/mobile/src/app/(tabs)/index.tsx @@ -75,6 +75,8 @@ export default function HomeScreen() { + + @@ -88,6 +90,7 @@ export default function HomeScreen() { + diff --git a/apps/mobile/src/app/fitness-profile.tsx b/apps/mobile/src/app/fitness-profile.tsx index 8e72ac7..46c7505 100644 --- a/apps/mobile/src/app/fitness-profile.tsx +++ b/apps/mobile/src/app/fitness-profile.tsx @@ -13,6 +13,7 @@ import { useAuth } from "@clerk/clerk-expo"; import { Ionicons } from "@expo/vector-icons"; import { Input } from "../components/Input"; import { Picker } from "../components/Picker"; +import { API_BASE_URL } from "../config/api"; interface FitnessProfileData { height?: number; @@ -64,7 +65,7 @@ export default function FitnessProfileScreen() { try { setFetchingProfile(true); const token = await getToken(); - const apiUrl = process.env.EXPO_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = `${API_BASE_URL}` || "http://localhost:3000"; const response = await fetch(`${apiUrl}/api/fitness-profile`, { headers: { Authorization: `Bearer ${token}`, @@ -98,9 +99,9 @@ export default function FitnessProfileScreen() { try { setLoading(true); const token = await getToken(); - const apiUrl = process.env.EXPO_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = `${API_BASE_URL}/api/fitness-profile` || "http://localhost:3000"; - const response = await fetch(`${apiUrl}/api/fitness-profile`, { + const response = await fetch(`${apiUrl}`, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/packages/database/src/schema.ts b/packages/database/src/schema.ts index ac381cd..ba04e29 100644 --- a/packages/database/src/schema.ts +++ b/packages/database/src/schema.ts @@ -6,7 +6,7 @@ export const users = sqliteTable("users", { firstName: text("first_name").notNull(), lastName: text("last_name").notNull(), password: text("password"), // Optional - Clerk handles authentication - role: text("role", { enum: ["admin", "trainer", "client"] }) + role: text("role", { enum: ["superAdmin", "admin", "trainer", "client"] }) .notNull() .default("client"), phone: text("phone"),