super admin added

This commit is contained in:
echo 2025-11-19 04:00:03 +01:00
parent b1bb5d8166
commit a87b94219d
9 changed files with 189 additions and 9 deletions

Binary file not shown.

View File

@ -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();

View File

@ -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();

View File

@ -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(

View File

@ -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 (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${colorClass}`}>

View File

@ -221,6 +221,12 @@ export function UserManagement() {
>
Admins
</Button>
<Button
variant={filter === "superAdmin" ? "primary" : "secondary"}
onClick={() => setFilter("superAdmin")}
>
Super Admins
</Button>
</div>
</div>
@ -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. */}
<option value="client">Client</option>
<option value="trainer">Trainer</option>
<option value="admin">Admin</option>
<option value="superAdmin">Super Admin</option>
</select>
<p className="text-xs text-gray-500 mt-1">
Note: You can only assign roles lower than your own.
</p>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Phone</label>
@ -436,8 +449,8 @@ export function UserManagement() {
<span className="font-medium">Last Visit:</span>{" "}
{selectedUser.client.lastVisit
? new Date(
selectedUser.client.lastVisit,
).toLocaleDateString()
selectedUser.client.lastVisit,
).toLocaleDateString()
: "Never"}
</p>
</div>

View File

@ -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
)

View File

@ -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;
}

View File

@ -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"),