import { NextResponse } from 'next/server'; import { db, writerDb } from "@/drizzle/db"; import { users, tenants, systemFeatures, saasPackages } from "@/drizzle/schema"; import { eq } from 'drizzle-orm'; import { cookies } from 'next/headers'; import jwt from 'jsonwebtoken'; import bcrypt from 'bcryptjs'; export const dynamic = 'force-dynamic'; export async function GET(_req: Request) { try { const cookieStore = await cookies(); const token = cookieStore.get('jumpa_token')?.value; if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string; tenantId: string }; if (decoded.role !== 'admin' && decoded.role !== 'superadmin') { return NextResponse.json({ error: 'Forbidden: Admins only' }, { status: 403 }); } // Ambil semua user di tenant ini const tenantUsers = await db.select({ id: users.id, email: users.email, role: users.role, licenses: users.licenses, createdAt: users.createdAt }).from(users).where(eq(users.tenantId, decoded.tenantId)); // Ambil status Closed Group tenant ini const tenantData = await db.select({ id: tenants.id, allowCrossGroup: tenants.allowCrossGroup, name: tenants.name, licenses: tenants.licenses, licenseNumber: tenants.licenseNumber, packageId: tenants.packageId, brandColor: tenants.brandColor, platformName: tenants.platformName, securityTier: tenants.securityTier, byokEnabled: tenants.byokEnabled, byokKey: tenants.byokKey }).from(tenants).where(eq(tenants.id, decoded.tenantId)).limit(1); // Ambil system features const allFeatures = await db.select().from(systemFeatures); // Gabungkan Package Features jika Tenant memiliki Paket let mergedLicenses = tenantData[0].licenses; if (tenantData[0].packageId) { const pkgData = await db.select().from(saasPackages).where(eq(saasPackages.id, tenantData[0].packageId)).limit(1); if (pkgData.length > 0) { try { let tenantCustom: Record = {}; if (typeof mergedLicenses === 'string') tenantCustom = JSON.parse(mergedLicenses) || {}; else tenantCustom = (mergedLicenses as any) || {}; const pkgFeats: string[] = JSON.parse(pkgData[0].features || "[]"); pkgFeats.forEach(f => { tenantCustom[f] = "GRANTED"; }); mergedLicenses = JSON.stringify(tenantCustom); } catch(e) {} } } const finalTenant = { ...tenantData[0], licenses: mergedLicenses }; return NextResponse.json({ users: tenantUsers, tenant: finalTenant, systemFeatures: allFeatures }); } catch (error: any) { console.error('[ADMIN GET ERROR]', error); return NextResponse.json({ error: error.message || 'Internal Server Error' }, { status: 500 }); } } export async function POST(req: Request) { try { const cookieStore = await cookies(); const token = cookieStore.get('jumpa_token')?.value; if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string; tenantId: string }; if (decoded.role !== 'admin' && decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); const body = await req.json(); if (body.action === 'toggle_cross_group') { const newStatus = !!body.allowCrossGroup; await writerDb.update(tenants) .set({ allowCrossGroup: newStatus }) .where(eq(tenants.id, decoded.tenantId)); return NextResponse.json({ success: true, allowCrossGroup: newStatus }); } if (body.action === 'add_user') { const { email, password, role } = body; if (!email || !password || password.length < 6) { return NextResponse.json({ error: 'Email and password (min 6 chars) are required' }, { status: 400 }); } // Check if email already exists const existingUser = await db.select().from(users).where(eq(users.email, email)).limit(1); if (existingUser.length > 0) { return NextResponse.json({ error: 'Email sudah terdaftar.' }, { status: 409 }); } // Create the user with bcrypt-hashed password (Kelas Militer) const hashedPassword = await bcrypt.hash(password, 12); const newUserResult = await writerDb.insert(users).values({ email: email, passwordHash: hashedPassword, tenantId: decoded.tenantId, role: role || 'user' }).returning({ id: users.id, email: users.email, role: users.role, createdAt: users.createdAt }); return NextResponse.json({ success: true, user: newUserResult[0] }); } if (body.action === 'change_package') { const { newLicenses } = body; // Array of strings e.g. ["chat", "vc"] if (!Array.isArray(newLicenses)) { return NextResponse.json({ error: 'Invalid licenses format' }, { status: 400 }); } const licensesStr = JSON.stringify(newLicenses); await writerDb.update(tenants) .set({ licenses: licensesStr }) .where(eq(tenants.id, decoded.tenantId)); return NextResponse.json({ success: true, licenses: licensesStr }); } if (body.action === 'update_white_label') { const { brandColor, platformName } = body; await writerDb.update(tenants) .set({ brandColor, platformName }) .where(eq(tenants.id, decoded.tenantId)); return NextResponse.json({ success: true }); } if (body.action === 'update_user_licenses') { const { targetUserId, newLicenses, byokEnabled, byokKey } = body; // Pastikan targetUser ada di tenant yang sama const targetUserCheck = await db.select().from(users).where(eq(users.id, targetUserId)).limit(1); if (targetUserCheck.length === 0 || targetUserCheck[0].tenantId !== decoded.tenantId) { return NextResponse.json({ error: 'User not found or access denied' }, { status: 403 }); } const updateData: any = { licenses: JSON.stringify(newLicenses) }; if (typeof byokEnabled === 'boolean') updateData.byokEnabled = byokEnabled; if (typeof byokKey === 'string') updateData.byokKey = byokKey; await writerDb.update(users) .set(updateData) .where(eq(users.id, targetUserId)); return NextResponse.json({ success: true }); } if (body.action === 'edit_user') { const { id, email, role, password } = body; const targetUserCheck = await db.select().from(users).where(eq(users.id, id)).limit(1); if (targetUserCheck.length === 0 || targetUserCheck[0].tenantId !== decoded.tenantId) { return NextResponse.json({ error: 'User not found or access denied' }, { status: 403 }); } const updateData: Record = { email, role }; if (password && password.trim() !== '') { updateData.passwordHash = await bcrypt.hash(password, 12); } await writerDb.update(users).set(updateData).where(eq(users.id, id)); return NextResponse.json({ success: true }); } if (body.action === 'bulk_delete') { const { userIds } = body; // Array of strings if (!Array.isArray(userIds) || userIds.length === 0) return NextResponse.json({ success: true }); // Ensure we only delete users from this tenant for (const uid of userIds) { const uCheck = await db.select().from(users).where(eq(users.id, uid)).limit(1); if (uCheck.length > 0 && uCheck[0].tenantId === decoded.tenantId) { await writerDb.delete(users).where(eq(users.id, uid)); } } return NextResponse.json({ success: true }); } if (body.action === 'update_tenant_byok') { const { byokEnabled, byokKey } = body; // Ensure the tenant has BYOK allowed by Supreme Admin const tenantCheck = await db.select().from(tenants).where(eq(tenants.id, decoded.tenantId)).limit(1); if (!tenantCheck[0].byokEnabled && byokEnabled) { return NextResponse.json({ error: 'BYOK not enabled for this tenant by Supreme Admin.' }, { status: 403 }); } await writerDb.update(tenants) .set({ byokEnabled, byokKey }) .where(eq(tenants.id, decoded.tenantId)); return NextResponse.json({ success: true }); } if (body.action === 'update_user_byok') { const { targetUserId, byokEnabled, byokKey } = body; const targetUserCheck = await db.select().from(users).where(eq(users.id, targetUserId)).limit(1); if (targetUserCheck.length === 0 || targetUserCheck[0].tenantId !== decoded.tenantId) { return NextResponse.json({ error: 'User not found or access denied' }, { status: 403 }); } await writerDb.update(users) .set({ byokEnabled: !!byokEnabled, byokKey: byokKey }) .where(eq(users.id, targetUserId)); return NextResponse.json({ success: true }); } return NextResponse.json({ error: 'Invalid action' }, { status: 400 }); } catch (e) { console.error("[API/ADMIN POST Error]", e); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); } }