Files
multiverse/jumpa-iam/app/api/admin/route.ts
T

241 lines
9.2 KiB
TypeScript

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<string, string> = {};
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<string, string> = { 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 });
}
}