241 lines
9.2 KiB
TypeScript
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 });
|
|
}
|
|
}
|