[TSM.ID].[11031972] PXE : Platform X Ecosystem I [118 Module -LIVE-]
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from "@/drizzle/db";
|
||||
import { tenants, saasPackages } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { XenditEngine } from "@/lib/xendit-engine";
|
||||
|
||||
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; tenantId: string };
|
||||
|
||||
const { packageId } = await req.json();
|
||||
if (!packageId) return NextResponse.json({ error: 'Missing Package ID' }, { status: 400 });
|
||||
|
||||
// 1. Ambil data paket & tenant
|
||||
const [pkg] = await db.select().from(saasPackages).where(eq(saasPackages.id, packageId)).limit(1);
|
||||
const [tenant] = await db.select().from(tenants).where(eq(tenants.id, decoded.tenantId)).limit(1);
|
||||
|
||||
if (!pkg || !tenant) return NextResponse.json({ error: 'Data not found' }, { status: 404 });
|
||||
|
||||
// 2. Kalkulasi Harga (Hapus 'Rp. ' dan titik untuk nominal integer)
|
||||
const amount = parseInt(pkg.price.replace(/[^0-9]/g, ''));
|
||||
if (isNaN(amount) || amount === 0) {
|
||||
// Paket Gratis / Rp. 0
|
||||
return NextResponse.json({ error: 'Package price is 0. No payment needed.' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 3. Buat Invoice Xendit
|
||||
const externalId = `INV-${tenant.id.substring(0, 8)}-${Date.now()}`;
|
||||
const invoice = await XenditEngine.createInvoice({
|
||||
external_id: externalId,
|
||||
amount: amount,
|
||||
payer_email: decoded.email,
|
||||
description: `Subscription Upgrade: ${pkg.name} for ${tenant.name}`,
|
||||
success_redirect_url: `${req.headers.get('origin')}/id/admin?payment=success&id=${externalId}`,
|
||||
failure_redirect_url: `${req.headers.get('origin')}/id/admin?payment=failed`,
|
||||
currency: "IDR",
|
||||
items: [{
|
||||
name: pkg.name,
|
||||
quantity: 1,
|
||||
price: amount,
|
||||
category: "Subscription"
|
||||
}]
|
||||
});
|
||||
|
||||
// 4. Log transaksi (Optional: Bisa simpan ke tabel transactions)
|
||||
console.log(`[CHECKOUT] Invoice created for ${tenant.name}: ${invoice.invoice_url}`);
|
||||
|
||||
return NextResponse.json({ url: invoice.invoice_url });
|
||||
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Internal Checkout Error";
|
||||
console.error("Checkout Error:", error);
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { exec } from 'child_process';
|
||||
import util from 'util';
|
||||
|
||||
const execPromise = util.promisify(exec);
|
||||
|
||||
export async function POST(_req: Request) {
|
||||
try {
|
||||
// Ideally we should verify JWT role: admin here, but for demonstration of the architecture:
|
||||
|
||||
// Command to scale up by 1 instance
|
||||
const { stdout, stderr } = await execPromise('pm2 scale jumpa-chat +1');
|
||||
|
||||
console.log('[CLUSTER SCALE] stdout:', stdout);
|
||||
if (stderr) console.error('[CLUSTER SCALE] stderr:', stderr);
|
||||
|
||||
// Give it 1 second to warm up
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Node scaled successfully', stdout }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error('[CLUSTER SCALE ERROR]', error);
|
||||
return NextResponse.json({ error: 'Failed to scale cluster' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
// TAHAP 3: Kill-Switch API Endpoint (IAM)
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { email } = await req.json();
|
||||
|
||||
if (!email) {
|
||||
return NextResponse.json({ success: false, error: 'Email required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Publish event ke Redis untuk dibaca oleh cluster JUMPA Chat
|
||||
const pubClient = new Redis(process.env.REDIS_URL || 'redis://127.0.0.1:6379');
|
||||
|
||||
const payload = JSON.stringify({
|
||||
action: 'force_logout',
|
||||
email: email,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
await pubClient.publish('admin_commands', payload);
|
||||
pubClient.disconnect();
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Kill-Switch event published' });
|
||||
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Kill-Switch Error";
|
||||
console.error('Kill-Switch Error:', error);
|
||||
return NextResponse.json({ success: false, error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from "@/drizzle/db";
|
||||
import { networkTelemetry, quantumLogs, liveKillSwitches } from "@/drizzle/schema";
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { desc, count, sql, eq } from 'drizzle-orm';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
interface DecodedToken {
|
||||
userId: string;
|
||||
email: string;
|
||||
role: string;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
// PANOPTICON: Tenant Admin Telemetry API (Sandboxed View)
|
||||
// Returns ONLY data scoped to the caller's tenant_id.
|
||||
export async function GET() {
|
||||
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 DecodedToken;
|
||||
if (decoded.role !== 'admin' && decoded.role !== 'superadmin') {
|
||||
return NextResponse.json({ error: 'Admin Access Required' }, { status: 403 });
|
||||
}
|
||||
|
||||
const tenantId = decoded.tenantId;
|
||||
|
||||
// 1. Telemetry scoped to tenant (last 50)
|
||||
const recentTelemetry = await db.select().from(networkTelemetry)
|
||||
.where(eq(networkTelemetry.tenantId, tenantId))
|
||||
.orderBy(desc(networkTelemetry.timestamp))
|
||||
.limit(50);
|
||||
|
||||
// 2. Quantum logs scoped to tenant (last 100)
|
||||
const recentLogs = await db.select().from(quantumLogs)
|
||||
.where(eq(quantumLogs.tenantId, tenantId))
|
||||
.orderBy(desc(quantumLogs.nanoTimestamp))
|
||||
.limit(100);
|
||||
|
||||
// 3. Kill switches scoped to tenant
|
||||
const activeKills = await db.select().from(liveKillSwitches)
|
||||
.where(eq(liveKillSwitches.tenantId, tenantId));
|
||||
|
||||
// 4. Aggregated stats (tenant-scoped)
|
||||
const [telemetryCount] = await db.select({ total: count() }).from(networkTelemetry)
|
||||
.where(eq(networkTelemetry.tenantId, tenantId));
|
||||
const [logsCount] = await db.select({ total: count() }).from(quantumLogs)
|
||||
.where(eq(quantumLogs.tenantId, tenantId));
|
||||
|
||||
// 5. Bandwidth aggregate (tenant-scoped)
|
||||
const bandwidthAgg = await db.select({
|
||||
totalBytes: sql<string>`COALESCE(SUM(CAST(traffic_bytes AS BIGINT)), 0)`,
|
||||
avgResponseMs: sql<string>`COALESCE(AVG(CAST(response_time_ms AS NUMERIC)), 0)`,
|
||||
}).from(networkTelemetry)
|
||||
.where(eq(networkTelemetry.tenantId, tenantId));
|
||||
|
||||
return NextResponse.json({
|
||||
telemetry: recentTelemetry,
|
||||
logs: recentLogs,
|
||||
kills: activeKills,
|
||||
stats: {
|
||||
totalTelemetryRecords: telemetryCount?.total || 0,
|
||||
totalLogRecords: logsCount?.total || 0,
|
||||
activeKillSwitches: activeKills.length,
|
||||
totalBandwidthBytes: bandwidthAgg[0]?.totalBytes || '0',
|
||||
avgResponseTimeMs: parseFloat(String(bandwidthAgg[0]?.avgResponseMs || '0')).toFixed(2),
|
||||
},
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error('[TENANT TELEMETRY ERROR]', error);
|
||||
return NextResponse.json({ error: 'Telemetry Sync Failed' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// [TSM.ID].[11031972] — All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from "@/drizzle/db";
|
||||
import { tenants, guestInvites } from "@/drizzle/schema";
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/**
|
||||
* GUEST TOKEN ENDPOINT — Zoom-Like Guest Access
|
||||
*
|
||||
* Alur:
|
||||
* 1. Guest klik link room → VC tampilkan Lobby UI
|
||||
* 2. Guest isi nama → POST ke endpoint ini
|
||||
* 3. IAM verifikasi room ada + tenant mengizinkan guest
|
||||
* 4. Issue JWT terbatas (role: "guest", durasi diatur admin tenant)
|
||||
* 5. Guest masuk room dengan permissions terbatas
|
||||
*/
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { roomName, displayName } = await req.json();
|
||||
|
||||
if (!roomName || !displayName || displayName.trim().length < 2) {
|
||||
return NextResponse.json({
|
||||
error: 'Nama tampilan wajib diisi (minimal 2 karakter).'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Sanitize display name
|
||||
const safeName = displayName.trim().substring(0, 50);
|
||||
|
||||
// Extract tenantId from room name format: "tenantId-roomSlug" or find by room
|
||||
// For now, we look up rooms in guestInvites to find the host tenant
|
||||
// Or extract from room name pattern
|
||||
|
||||
// Strategy: Look at all tenants and check if guest access is enabled
|
||||
// The room owner's tenant determines the guest policy
|
||||
const allTenants = await db.select().from(tenants).limit(50);
|
||||
|
||||
// Find the tenant that owns this room
|
||||
// Room format convention: rooms typically include tenant context
|
||||
// For universal guest access, we find tenant with guest enabled
|
||||
let ownerTenant = null;
|
||||
|
||||
for (const t of allTenants) {
|
||||
try {
|
||||
const lic = typeof t.licenses === 'string' ? JSON.parse(t.licenses) : (t.licenses || {});
|
||||
// Check if tenant has guest access enabled (set by admin)
|
||||
if (lic['jvc.guest_access'] === 'GRANTED' || lic['jvc.guest_access'] === true) {
|
||||
// Check if room belongs to this tenant (room contains tenant name or id)
|
||||
if (roomName.toLowerCase().includes(t.name.toLowerCase().replace(/\s+/g, '-'))
|
||||
|| roomName.includes(t.id.substring(0, 8))) {
|
||||
ownerTenant = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (_) { /* skip malformed licenses */ }
|
||||
}
|
||||
|
||||
// Fallback: if no tenant found by name match, check if ANY tenant allows guest
|
||||
if (!ownerTenant) {
|
||||
for (const t of allTenants) {
|
||||
try {
|
||||
const lic = typeof t.licenses === 'string' ? JSON.parse(t.licenses) : (t.licenses || {});
|
||||
if (lic['jvc.guest_access'] === 'GRANTED' || lic['jvc.guest_access'] === true) {
|
||||
ownerTenant = t;
|
||||
break;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ownerTenant) {
|
||||
return NextResponse.json({
|
||||
error: 'Room ini tidak mengizinkan akses tamu. Harap minta host untuk mengaktifkan Guest Access.'
|
||||
}, { status: 403 });
|
||||
}
|
||||
|
||||
// Parse guest session duration from tenant licenses (admin configurable)
|
||||
let guestDurationHours = 2; // Default 2 jam
|
||||
try {
|
||||
const lic = typeof ownerTenant.licenses === 'string' ? JSON.parse(ownerTenant.licenses) : (ownerTenant.licenses || {});
|
||||
if (lic['jvc.guest_duration_hours'] && !isNaN(Number(lic['jvc.guest_duration_hours']))) {
|
||||
guestDurationHours = Math.min(Math.max(Number(lic['jvc.guest_duration_hours']), 0.5), 24); // 30min - 24hr
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Generate guest JWT — limited permissions
|
||||
const xcuSecret = process.env.XCU_QCG_SECRET || process.env.XCU_TOKEN_SECRET;
|
||||
if (!xcuSecret) {
|
||||
throw new Error('XCU_QCG_SECRET tidak dikonfigurasi.');
|
||||
}
|
||||
|
||||
const guestToken = jwt.sign({
|
||||
sub: `guest_${safeName.replace(/\s+/g, '_').toLowerCase()}`,
|
||||
email: `guest_${Date.now()}@guest.jumpa.id`, // Pseudo email for identity
|
||||
role: 'guest',
|
||||
tenantId: ownerTenant.id,
|
||||
tenantName: ownerTenant.name,
|
||||
displayName: safeName,
|
||||
allowCrossGroup: false,
|
||||
modules: [], // Guest tidak dapat modul apapun
|
||||
capabilities: {
|
||||
video: { features: { autopilot: true } },
|
||||
audio: { features: {} },
|
||||
ui: {
|
||||
'jvc.ui.host_controls': false,
|
||||
'jvc.ui.recording': false,
|
||||
'jvc.ui.screen_share': false, // Admin bisa override via licenses
|
||||
'jvc.ui.chat': true,
|
||||
'jvc.ui.reactions': true,
|
||||
}
|
||||
},
|
||||
isGuest: true,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + (60 * 60 * guestDurationHours)
|
||||
}, xcuSecret);
|
||||
|
||||
// Generate JUMPA.ID session cookie for guest (so quantum_token works too)
|
||||
const jumpaGuestToken = jwt.sign({
|
||||
userId: `guest_${Date.now()}`,
|
||||
email: `guest_${Date.now()}@guest.jumpa.id`,
|
||||
role: 'guest',
|
||||
tenantId: ownerTenant.id,
|
||||
tenantName: ownerTenant.name,
|
||||
displayName: safeName,
|
||||
}, process.env.JWT_SECRET as string, { expiresIn: `${guestDurationHours}h` });
|
||||
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
token: guestToken,
|
||||
displayName: safeName,
|
||||
tenantName: ownerTenant.name,
|
||||
guestDurationHours,
|
||||
message: `Selamat datang, ${safeName}! Sesi tamu berlaku ${guestDurationHours} jam.`
|
||||
});
|
||||
|
||||
// Set guest cookie so subsequent requests (quantum_token etc) work
|
||||
response.cookies.set({
|
||||
name: 'jumpa_token',
|
||||
value: jumpaGuestToken,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN || undefined,
|
||||
maxAge: 60 * 60 * guestDurationHours,
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error('[GUEST TOKEN ERROR]', error);
|
||||
return NextResponse.json({ error: 'Gagal membuat sesi tamu.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from "@/drizzle/db";
|
||||
import { users, tenants, systemFeatures, saasPackages } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { QuantumOrchestrator } from "@/lib/quantum-orchestrator";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const rawBody = await req.text();
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(rawBody);
|
||||
} catch (parseErr: any) {
|
||||
console.error('[API AUTH] JSON parse failed. Raw body:', JSON.stringify(rawBody).slice(0, 200));
|
||||
return NextResponse.json({ error: `Invalid request body: ${parseErr.message}` }, { status: 400 });
|
||||
}
|
||||
let { email, password } = parsed;
|
||||
|
||||
if (!email || !password) {
|
||||
return NextResponse.json({ error: 'Email and password are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
email = email.toLowerCase();
|
||||
|
||||
// Eksekusi Pencarian Database (Real PostgreSQL) dengan Matrix Modul
|
||||
const userResult = await db.select({
|
||||
id: users.id,
|
||||
email: users.email,
|
||||
passwordHash: users.passwordHash,
|
||||
role: users.role,
|
||||
tenantId: users.tenantId,
|
||||
tenantName: tenants.name,
|
||||
tenantIsActive: tenants.isActive,
|
||||
tenantLicenses: tenants.licenses,
|
||||
allowCrossGroup: tenants.allowCrossGroup,
|
||||
mediaEngineStrategy: tenants.mediaEngineStrategy,
|
||||
chatEngineStrategy: tenants.chatEngineStrategy,
|
||||
userLicenses: users.licenses,
|
||||
packageFeatures: saasPackages.features
|
||||
})
|
||||
.from(users)
|
||||
.innerJoin(tenants, eq(users.tenantId, tenants.id))
|
||||
.leftJoin(saasPackages, eq(tenants.packageId, saasPackages.id))
|
||||
.where(eq(users.email, email))
|
||||
.limit(1);
|
||||
|
||||
if (userResult.length === 0) {
|
||||
return NextResponse.json({ error: 'Akses Ditolak: Email tidak terdaftar di sistem.' }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = userResult[0];
|
||||
|
||||
// Validasi Tenant (Diteruskan ke Orchestrator untuk pembatasan fitur)
|
||||
|
||||
// TAHAP NANO: Validasi Kriptografi Bcrypt MUTLAK (Plaintext Bypass DIHAPUS)
|
||||
const isHashed = user.passwordHash.startsWith('$2');
|
||||
if (!isHashed) {
|
||||
// Password belum di-hash = akun legacy/korup. Tolak akses, paksa reset.
|
||||
return NextResponse.json({ error: 'Akun ini memerlukan reset sandi. Hubungi administrator.' }, { status: 403 });
|
||||
}
|
||||
const isValid = bcrypt.compareSync(password, user.passwordHash);
|
||||
|
||||
if (!isValid) {
|
||||
return NextResponse.json({ error: 'Akses Ditolak: Sandi tidak valid.' }, { status: 401 });
|
||||
}
|
||||
|
||||
const allFeatures = await db.select().from(systemFeatures);
|
||||
|
||||
let quantumLicenses: Record<string, string> = {};
|
||||
allFeatures.forEach((f: { key: string; defaultState: string }) => {
|
||||
quantumLicenses[f.key] = f.defaultState;
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. Process Package Features (Module Matrix Level)
|
||||
if (user.packageFeatures) {
|
||||
const parsedPkg = typeof user.packageFeatures === 'string' ? JSON.parse(user.packageFeatures) : user.packageFeatures;
|
||||
if (Array.isArray(parsedPkg)) {
|
||||
parsedPkg.forEach((p: string) => quantumLicenses[p] = 'GRANTED');
|
||||
} else if (typeof parsedPkg === 'object' && parsedPkg !== null) {
|
||||
quantumLicenses = { ...quantumLicenses, ...parsedPkg };
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Process Tenant Licenses (Tenant-level overrides)
|
||||
if (user.tenantLicenses) {
|
||||
const parsed = typeof user.tenantLicenses === 'string' ? JSON.parse(user.tenantLicenses) : user.tenantLicenses;
|
||||
if (Array.isArray(parsed)) {
|
||||
// Legacy backward compatibility
|
||||
parsed.forEach((p: string) => quantumLicenses[p] = 'GRANTED');
|
||||
} else if (typeof parsed === 'object' && parsed !== null) {
|
||||
quantumLicenses = { ...quantumLicenses, ...parsed };
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: User Licenses are NOT merged here.
|
||||
// QuantumOrchestrator.resolve() handles user overrides with proper HIDDEN blocking.
|
||||
} catch (e) {
|
||||
console.error("Failed to parse licenses, using defaults. Error:", e);
|
||||
}
|
||||
|
||||
// Parse User Licenses separately for the Orchestrator
|
||||
const userLicParsed = (() => {
|
||||
try {
|
||||
if (!user.userLicenses) return {};
|
||||
const parsed = typeof user.userLicenses === 'string' ? JSON.parse(user.userLicenses) : user.userLicenses;
|
||||
return (typeof parsed === 'object' && parsed !== null) ? parsed : {};
|
||||
} catch { return {}; }
|
||||
})();
|
||||
|
||||
// RESOLVE CAPABILITIES v2.0
|
||||
// quantumLicenses = systemDefaults + packageFeatures + tenantLicenses (baseline)
|
||||
// userLicParsed = user-level overrides (applied by Orchestrator with HIDDEN check)
|
||||
const capabilities = QuantumOrchestrator.resolve(
|
||||
quantumLicenses,
|
||||
userLicParsed,
|
||||
!!user.tenantIsActive
|
||||
);
|
||||
|
||||
// AUTO-PILOT LOGIC: If autopilot is on, the orchestrator already picked the best codec
|
||||
const effectiveMediaStrategy = capabilities.video.features.autopilot ? 'XCU_AUTOPILOT' : user.mediaEngineStrategy;
|
||||
|
||||
// Generate JWT Token Kelas Militer
|
||||
// NOTE: licenses TIDAK disimpan di JWT karena melebihi 4KB cookie limit browser.
|
||||
// Licenses di-fetch on-demand via /api/auth/me atau /api/auth/quantum_token.
|
||||
const token = jwt.sign(
|
||||
{
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
name: user.email.split('@')[0], // Display name from email prefix
|
||||
role: user.role,
|
||||
tenantId: user.tenantId,
|
||||
tenantName: user.tenantName,
|
||||
allowCrossGroup: user.allowCrossGroup,
|
||||
mediaEngineStrategy: effectiveMediaStrategy,
|
||||
chatEngineStrategy: user.chatEngineStrategy,
|
||||
},
|
||||
process.env.JWT_SECRET as string,
|
||||
{ expiresIn: '8h' }
|
||||
);
|
||||
|
||||
const response = NextResponse.json({
|
||||
message: 'Otorisasi Berhasil. Membuka Gerbang...',
|
||||
user: {
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
tenantName: user.tenantName,
|
||||
licenses: quantumLicenses,
|
||||
mediaEngineStrategy: effectiveMediaStrategy,
|
||||
chatEngineStrategy: user.chatEngineStrategy
|
||||
}
|
||||
}, { status: 200 });
|
||||
|
||||
// Set HttpOnly Cookie for SSO across subdomains
|
||||
response.cookies.set({
|
||||
name: 'jumpa_token',
|
||||
value: token,
|
||||
httpOnly: true, // KRITIS-1 FIX: HttpOnly true untuk mencegah pencurian token via XSS. Chat menggunakan /c/api/auth/me.
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN || undefined, // Dinamis berdasar env/lokal
|
||||
maxAge: 8 * 60 * 60 // 8 hours
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[API AUTH ERROR]', error);
|
||||
return NextResponse.json({ error: `Kesalahan Sistem Internal PostgreSQL: ${error?.message || 'Unknown error'}` }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export async function POST() {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
// Hapus cookie jumpa_token dengan maxAge 0 dan domain yang sesuai
|
||||
cookieStore.set('jumpa_token', '', {
|
||||
httpOnly: true, // BARU-S2 FIX: Must match login's httpOnly:true to properly delete cookie
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN || undefined, // Mengikuti domain tempat aplikasi berjalan
|
||||
maxAge: 0 // Expire immediately
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Logged out successfully' });
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const cookieHeader = req.headers.get('cookie') || '';
|
||||
const cookies = Object.fromEntries(cookieHeader.split('; ').map(c => c.split('=')));
|
||||
const token = cookies['jumpa_token'];
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string);
|
||||
return NextResponse.json(decoded, { status: 200 });
|
||||
} catch (_error) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import crypto from 'crypto';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { db } from "@/drizzle/db";
|
||||
import { users } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { sessionId, signature, email } = await req.json();
|
||||
|
||||
// TAHAP NANO: Validasi Kriptografis Challenge QR Code
|
||||
if (!sessionId || !signature || !email) {
|
||||
return NextResponse.json({ error: 'Missing challenge parameters' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Server memverifikasi bahwa signature adalah hash dari sessionId + email + secret
|
||||
const expectedSignature = crypto
|
||||
.createHmac('sha256', process.env.JWT_SECRET as string)
|
||||
.update(`${sessionId}:${email}`)
|
||||
.digest('hex');
|
||||
|
||||
if (signature !== expectedSignature) {
|
||||
return NextResponse.json({ error: 'Invalid QR cryptographic signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Verifikasi berhasil, generate real JWT token
|
||||
const userResult = await db.select().from(users).where(eq(users.email, email)).limit(1);
|
||||
|
||||
if (userResult.length === 0) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const user = userResult[0];
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
tenantId: user.tenantId,
|
||||
},
|
||||
process.env.JWT_SECRET as string,
|
||||
{ expiresIn: '8h' }
|
||||
);
|
||||
|
||||
const response = NextResponse.json({ message: 'QR Auth Berhasil' });
|
||||
response.cookies.set({
|
||||
name: 'jumpa_token',
|
||||
value: token,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN || undefined,
|
||||
maxAge: 8 * 60 * 60
|
||||
});
|
||||
return response;
|
||||
|
||||
} catch (e) {
|
||||
console.error("QR Auth Verify Error:", e);
|
||||
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// [TSM.ID].[11031972] — All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from "@/drizzle/db";
|
||||
import { tenants, users } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { QuantumOrchestrator } from "@/lib/quantum-orchestrator";
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
interface DecodedToken {
|
||||
userId: string;
|
||||
email: string;
|
||||
role: string;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
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 DecodedToken;
|
||||
|
||||
// Fetch tenant data to get BYOK settings and licenses
|
||||
const tenantData = await db.select().from(tenants).where(eq(tenants.id, decoded.tenantId)).limit(1);
|
||||
const userData = await db.select().from(users).where(eq(users.id, decoded.userId)).limit(1);
|
||||
|
||||
if (tenantData.length === 0) {
|
||||
return NextResponse.json({ error: 'Tenant not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const tenant = tenantData[0];
|
||||
const user = userData[0];
|
||||
|
||||
// Parse licenses
|
||||
const rawLicenses = tenant.licenses || '{}';
|
||||
const tenantLicParsed = JSON.parse(rawLicenses);
|
||||
let tenantLicNormalized: Record<string, string> = {};
|
||||
if (Array.isArray(tenantLicParsed)) {
|
||||
tenantLicParsed.forEach((k: string) => tenantLicNormalized[k] = 'GRANTED');
|
||||
} else {
|
||||
tenantLicNormalized = tenantLicParsed || {};
|
||||
}
|
||||
|
||||
const userLicParsed = JSON.parse(user?.licenses || '{}');
|
||||
let userLicNormalized: Record<string, string> = {};
|
||||
if (Array.isArray(userLicParsed)) {
|
||||
userLicParsed.forEach((k: string) => userLicNormalized[k] = 'GRANTED');
|
||||
} else {
|
||||
userLicNormalized = userLicParsed || {};
|
||||
}
|
||||
|
||||
// Resolve 101-Module Matrix Capabilities
|
||||
const capabilities = QuantumOrchestrator.resolve(
|
||||
tenantLicNormalized,
|
||||
userLicNormalized,
|
||||
!!tenant.isActive
|
||||
);
|
||||
|
||||
// Parse legacy modules string list
|
||||
let modules: string[] = [];
|
||||
try {
|
||||
if (Array.isArray(tenantLicParsed)) {
|
||||
modules = tenantLicParsed;
|
||||
} else {
|
||||
modules = Object.keys(tenantLicParsed).filter(k => (tenantLicParsed as Record<string, string>)[k] === 'GRANTED');
|
||||
}
|
||||
} catch (_e) {
|
||||
modules = [];
|
||||
}
|
||||
|
||||
// Determine the active BYOK Key
|
||||
// Priority: User > Tenant
|
||||
let activeByokKey = 'none';
|
||||
let byokLevel = 'SYSTEM';
|
||||
|
||||
if (user?.byokEnabled && user?.byokKey) {
|
||||
activeByokKey = user.byokKey;
|
||||
byokLevel = 'USER';
|
||||
} else if (tenant.byokEnabled && tenant.byokKey) {
|
||||
activeByokKey = tenant.byokKey;
|
||||
byokLevel = 'TENANT';
|
||||
}
|
||||
|
||||
// Secret WAJIB sama dengan XCU_QCG_SECRET di XCU Core (PKX Rule #4)
|
||||
const xcuSecret = process.env.XCU_QCG_SECRET || process.env.XCU_TOKEN_SECRET;
|
||||
if (!xcuSecret) {
|
||||
throw new Error('HUKUM MUTLAK: XCU_QCG_SECRET tidak ditemukan! Gerbang Anti-Jumping menolak akses.');
|
||||
}
|
||||
|
||||
const quantumToken = jwt.sign({
|
||||
sub: decoded.email,
|
||||
tenantId: decoded.tenantId,
|
||||
modules: modules,
|
||||
capabilities: capabilities,
|
||||
byok: activeByokKey,
|
||||
byokLevel: byokLevel,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + (60 * 60 * 12)
|
||||
}, xcuSecret);
|
||||
|
||||
return NextResponse.json({
|
||||
token: quantumToken,
|
||||
modules: modules,
|
||||
capabilities: capabilities,
|
||||
byokActive: activeByokKey !== 'none',
|
||||
byokLevel: byokLevel
|
||||
});
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error('[QUANTUM TOKEN ERROR]', error);
|
||||
return NextResponse.json({ error: 'Quantum Sync Failed' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { users, tenants } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { email, password } = await req.json();
|
||||
|
||||
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 a new Tenant for the user
|
||||
const tenantName = `Personal Workspace - ${email.split('@')[0]}`;
|
||||
const newTenantResult = await writerDb.insert(tenants).values({
|
||||
name: tenantName,
|
||||
isActive: true
|
||||
}).returning({ id: tenants.id, name: tenants.name });
|
||||
|
||||
const newTenant = newTenantResult[0];
|
||||
|
||||
// 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: newTenant.id,
|
||||
role: 'admin' // The creator of the personal tenant is an admin
|
||||
}).returning({ id: users.id, email: users.email, role: users.role });
|
||||
|
||||
const user = newUserResult[0];
|
||||
|
||||
// Generate JWT Token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
tenantId: newTenant.id,
|
||||
tenantName: newTenant.name,
|
||||
licenses: {}
|
||||
},
|
||||
process.env.JWT_SECRET as string,
|
||||
{ expiresIn: '8h' }
|
||||
);
|
||||
|
||||
const response = NextResponse.json({
|
||||
message: 'Registrasi Berhasil.',
|
||||
user: {
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
tenantName: newTenant.name
|
||||
}
|
||||
}, { status: 201 });
|
||||
|
||||
// Set HttpOnly Cookie for SSO
|
||||
response.cookies.set({
|
||||
name: 'jumpa_token',
|
||||
value: token,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN || undefined,
|
||||
maxAge: 8 * 60 * 60
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[API REGISTER ERROR]', error);
|
||||
return NextResponse.json({ error: 'Kesalahan Sistem Internal PostgreSQL' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { writerDb } from "@/drizzle/db";
|
||||
import { tenants } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
/**
|
||||
* XENDIT WEBHOOK HANDLER (QUANTUM SYNC)
|
||||
*
|
||||
* Menerima callback dari Xendit saat pembayaran Lunas (PAID).
|
||||
* Langsung mengupdate status tenant secara reaktif.
|
||||
*/
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
// Keamanan: Validasi Xendit Callback Token (WAJIB di Production)
|
||||
const callbackToken = req.headers.get('x-callback-token');
|
||||
if (callbackToken !== process.env.XENDIT_CALLBACK_TOKEN) {
|
||||
console.error('[XENDIT WEBHOOK] REJECTED: Invalid callback token');
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { status, external_id, amount, payment_method } = body;
|
||||
|
||||
if (status === 'PAID') {
|
||||
const parts = external_id.split('-');
|
||||
const tenantId = parts[1]; // We should use full ID or mapping table
|
||||
|
||||
console.log(`[XENDIT WEBHOOK] PAID: ${amount} via ${payment_method} for Tenant: ${tenantId}`);
|
||||
|
||||
// Aktifkan Tenant secara otomatis (WAJIB writerDb untuk mutasi)
|
||||
await writerDb.update(tenants)
|
||||
.set({ isActive: true })
|
||||
.where(eq(tenants.id, tenantId));
|
||||
|
||||
console.log(`[SYSTEM] Tenant ${tenantId} has been reactivated via Quantum Billing.`);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("[WEBHOOK ERROR]", error);
|
||||
return NextResponse.json({ error: "Webhook Processing Failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { systemFeatures } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const modules = [
|
||||
// VIDEO ENGINE (XCU) - 25 Modules
|
||||
{ name: "XCU AV1 Codec (4K)", key: "xcu.codec.av1", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "XCU HEVC Codec (2K)", key: "xcu.codec.hevc", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "XCU VP9 Optimized", key: "xcu.codec.vp9", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "WebTransport / QUIC", key: "xcu.transport.quic", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "MoQ (Media over QUIC)", key: "xcu.transport.moq", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Auto-Pilot Codec Routing", key: "xcu.feature.autopilot", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Ultra-Low Latency (0ms)", key: "xcu.feature.ull", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "Global Mesh Relay", key: "xcu.feature.mesh", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "Simulcast Matrix", key: "xcu.feature.simulcast", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "SVC (Scalable Video Coding)", key: "xcu.feature.svc", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "XCU On-Premise Gateway", key: "xcu.feature.onprem", module: "XCU", defaultState: "HIDDEN" },
|
||||
{ name: "Dynamic Bitrate Adaptation", key: "xcu.feature.dba", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "Hardware Acceleration", key: "xcu.feature.hwaccel", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "Screen Share 60FPS", key: "xcu.feature.screenshare_high", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Virtual Background AI", key: "xcu.feature.vbg_ai", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Noise Suppression AI", key: "xcu.feature.noise_ai", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "Echo Cancellation Ultra", key: "xcu.feature.aec_ultra", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "Multi-Camera Broadcast", key: "xcu.feature.multicam", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Live Canvas TTE", key: "xcu.feature.tte", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Breakout Matrix Rooms", key: "xcu.feature.breakout", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Recording Sovereign", key: "xcu.feature.recording", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Live Streaming RTMP/HLS", key: "xcu.feature.stream_out", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Watermark Dynamic", key: "xcu.feature.watermark", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "Telepathy Sync State", key: "xcu.feature.telepathy", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "XCU Kernel Bypass (eBPF)", key: "xcu.feature.ebpf", module: "XCU", defaultState: "HIDDEN" },
|
||||
|
||||
// CHAT ENGINE (XTM) - 25 Modules
|
||||
{ name: "End-to-End Encryption", key: "xtm.security.e2ee", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Omni-Brain Interceptor", key: "xtm.ai.interceptor", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "The Vault (DRM Storage)", key: "xtm.storage.vault", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Self-Destruct Messages", key: "xtm.feature.burn", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Quantum Handshake", key: "xtm.security.quantum", module: "XTM", defaultState: "HIDDEN" },
|
||||
{ name: "Biometric Auth Chat", key: "xtm.security.biometric", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Persistent Message Log", key: "xtm.feature.persistence", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Large File Transfer (10GB)", key: "xtm.feature.file_large", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Global Search Index", key: "xtm.feature.search", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Group Management Pro", key: "xtm.feature.group_pro", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Audit Trail Chat", key: "xtm.feature.audit", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Bot Integration API", key: "xtm.feature.bot", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Reaction Matrix", key: "xtm.feature.reaction", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Threaded Conversations", key: "xtm.feature.threads", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Translate Auto AI", key: "xtm.ai.translate", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Sentiment Analysis", key: "xtm.ai.sentiment", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Summary Neural Link", key: "xtm.ai.summary", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Offline Sync Mode", key: "xtm.feature.offline", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Multi-Device Sync", key: "xtm.feature.multidevice", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Whisper Mode", key: "xtm.feature.whisper", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Announcement Channel", key: "xtm.feature.broadcast", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Read Receipts Stealth", key: "xtm.feature.stealth", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Custom Emoji Pack", key: "xtm.feature.emoji", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Voice Note Matrix", key: "xtm.feature.voicenote", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Video Message Circle", key: "xtm.feature.videonote", module: "XTM", defaultState: "UPSELL" },
|
||||
|
||||
// IAM / CORE - 20 Modules
|
||||
{ name: "Supreme Eye Dashboard", key: "iam.feature.supreme_eye", module: "IAM", defaultState: "HIDDEN" },
|
||||
{ name: "BYOK Matrix Control", key: "iam.feature.byok", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "White-Label Branding", key: "iam.feature.branding", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "Sovereign VAULT Auth", key: "iam.auth.vault", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "Zero-Trust SSO", key: "iam.auth.sso", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "Xendit Payment Bridge", key: "iam.billing.xendit", module: "IAM", defaultState: "GRANTED" },
|
||||
{ name: "Multi-Currency Support", key: "iam.billing.currency", module: "IAM", defaultState: "GRANTED" },
|
||||
{ name: "Audit Log Export", key: "iam.security.audit_log", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "IP Whitelisting", key: "iam.security.ip_whitelist", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "License Generator", key: "iam.admin.licenses", module: "IAM", defaultState: "HIDDEN" },
|
||||
{ name: "Tenant Isolation Pro", key: "iam.security.isolation", module: "IAM", defaultState: "GRANTED" },
|
||||
{ name: "Advanced Role Matrix", key: "iam.security.roles", module: "IAM", defaultState: "GRANTED" },
|
||||
{ name: "Usage Telemetry", key: "iam.feature.telemetry", module: "IAM", defaultState: "GRANTED" },
|
||||
{ name: "API Gateway Access", key: "iam.feature.api_access", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "Custom Domain Mapping", key: "iam.feature.custom_domain", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "B2B Marketplace", key: "iam.feature.marketplace", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "Developer Sandbox", key: "iam.feature.sandbox", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "System Mindmap", key: "iam.feature.mindmap", module: "IAM", defaultState: "HIDDEN" },
|
||||
{ name: "Quantum Logs", key: "iam.feature.quantum_logs", module: "IAM", defaultState: "HIDDEN" },
|
||||
{ name: "Emergency Kill-Switch", key: "iam.security.killswitch", module: "IAM", defaultState: "HIDDEN" },
|
||||
|
||||
// JVC (JUMPA Video Conference) UI Controls - 14 Modules
|
||||
{ name: "Tombol Microphone", key: "jvc.ui.microphone", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Tombol Camera + Flip", key: "jvc.ui.camera", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Panel Peserta", key: "jvc.ui.people_panel", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Panel Chat", key: "jvc.ui.chat_panel", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Screen Share", key: "jvc.ui.screenshare", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Emoji Reactions", key: "jvc.ui.reactions", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Breakout Rooms", key: "jvc.ui.breakout", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Recording", key: "jvc.ui.recording", module: "JVC", defaultState: "UPSELL" },
|
||||
{ name: "Neural Radiance Filter", key: "jvc.ui.beauty_filter", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Virtual Background + Bokeh", key: "jvc.ui.virtual_bg", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Meeting Timer", key: "jvc.ui.timer", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "XCO Command Matrix Panel", key: "jvc.ui.matrix_command", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Gallery/Speaker View Toggle", key: "jvc.ui.layout_toggle", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Host Controls (Mute All, Kick)", key: "jvc.ui.host_controls", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Video Engine Selector (XCO)", key: "jvc.xco.video_engine", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Audio Engine Selector (XCO)", key: "jvc.xco.audio_engine", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "FPS Selector (XCO)", key: "jvc.xco.fps_selector", module: "JVC", defaultState: "GRANTED" },
|
||||
|
||||
// JC (JUMPA Chat) UI Controls - 12 Modules
|
||||
{ name: "Kirim Pesan Text", key: "jc.ui.send_message", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "Emoji Reactions Chat", key: "jc.ui.reactions", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "File Upload", key: "jc.ui.file_upload", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "Voice Notes", key: "jc.ui.voice_notes", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "OmniBrain AI Chat", key: "jc.ui.omnibrain", module: "JC", defaultState: "UPSELL" },
|
||||
{ name: "Edit Message", key: "jc.ui.edit_message", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "Delete Message", key: "jc.ui.delete_message", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "Group Management", key: "jc.ui.group_manage", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "Typing Indicator", key: "jc.ui.typing_indicator", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "Read Receipts", key: "jc.ui.read_receipts", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "Video Call from Chat", key: "jc.ui.video_call", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "Voice Call from Chat", key: "jc.ui.voice_call", module: "JC", defaultState: "GRANTED" },
|
||||
];
|
||||
|
||||
// Seed logic
|
||||
for (const m of modules) {
|
||||
const existing = await db.select().from(systemFeatures).where(eq(systemFeatures.key, m.key)).limit(1);
|
||||
if (existing.length === 0) {
|
||||
await writerDb.insert(systemFeatures).values(m);
|
||||
} else {
|
||||
// Update to ensure defaults are correct
|
||||
await writerDb.update(systemFeatures).set({ name: m.name, module: m.module }).where(eq(systemFeatures.key, m.key));
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `${modules.length} Quantum Modules Synchronized`,
|
||||
count: modules.length
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : "Internal Error during Seeding";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import Redis from "ioredis";
|
||||
|
||||
// Klien Redis untuk Publisher
|
||||
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
||||
const redis = new Redis(redisUrl);
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { channel, event, payload } = body;
|
||||
|
||||
if (!channel || !event) {
|
||||
return NextResponse.json({ error: "Missing channel or event" }, { status: 400 });
|
||||
}
|
||||
|
||||
// IAM tidak menangani host_approve_guest secara langsung dengan DB pg
|
||||
|
||||
const message = JSON.stringify({ event, payload, timestamp: Date.now() });
|
||||
|
||||
// Pancarkan ke Redis PubSub, yang akan ditangkap oleh endpoint SSE
|
||||
await redis.publish(channel, message);
|
||||
|
||||
console.log(`[EMIT] Memancarkan '${event}' ke '${channel}'`);
|
||||
|
||||
return NextResponse.json({ success: true, message: "Signal transmitted" });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Gagal memancarkan sinyal";
|
||||
console.error("[EMIT] Gagal memancarkan sinyal:", message);
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { history, prompt, sender } = body;
|
||||
|
||||
// Environment variables routing
|
||||
const engine = process.env.OMNIBRAIN_ENGINE || "XCU_DEEP_CORE";
|
||||
const openaiKey = process.env.OPENAI_API_KEY;
|
||||
const geminiKey = process.env.GEMINI_API_KEY;
|
||||
const grokKey = process.env.GROK_API_KEY;
|
||||
|
||||
// Proxy URLs for State Audit Compliance
|
||||
const openaiUrl = process.env.OPENAI_API_URL || "http://127.0.0.1:11434/v1/chat/completions";
|
||||
const geminiUrl = process.env.GEMINI_API_URL || "http://127.0.0.1:11434/v1/chat/completions";
|
||||
const grokUrl = process.env.GROK_API_URL || "http://127.0.0.1:11434/v1/chat/completions";
|
||||
|
||||
// Format history for context
|
||||
const historyText = history.map((m: { sender: string; text: string }) => `${m.sender}: ${m.text}`).join('\n');
|
||||
const systemPrompt = 'You are Omni-Brain, an advanced AI telepathic observer inside an End-to-End Encrypted XCU Ultra chat room. Provide concise, highly analytical, and awe-inspiring responses in Indonesian. Use terms like "Jaringan Kuantum", "Matriks Telepati", etc.';
|
||||
const userPrompt = `[Decrypted Context - Top Secret]\n${historyText}\n\nUser [${sender}] commands: ${prompt}`;
|
||||
|
||||
let reply = "";
|
||||
|
||||
// DYNAMIC ROUTING ENGINE
|
||||
if (engine === "OPENAI" && openaiKey) {
|
||||
const response = await fetch(openaiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${openaiKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
],
|
||||
max_tokens: 300
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
reply = data.choices?.[0]?.message?.content || "";
|
||||
|
||||
} else if (engine === "GEMINI" && geminiKey) {
|
||||
const response = await fetch(geminiUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
contents: [{ parts: [{ text: `${systemPrompt}\n\n${userPrompt}` }] }]
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
reply = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
||||
|
||||
} else if (engine === "GROK" && grokKey) {
|
||||
const response = await fetch(grokUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${grokKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'grok-beta',
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
]
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
reply = data.choices?.[0]?.message?.content || "";
|
||||
|
||||
} else {
|
||||
// 1000% DEFAULT FALLBACK: XCU DEEP-CORE LOCAL LLM (OLLAMA / Llama3)
|
||||
// Jaringan Air-Gapped Sovereign murni
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:11434/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'llama3', // Atau deepseek-coder, mistral, dll.
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
]
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
reply = data.choices?.[0]?.message?.content || "";
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ reply: "Sistem XCU Deep-Core LLM terputus. Pastikan daemon Ollama berjalan di peladen (127.0.0.1:11434)." }, { status: 200 });
|
||||
}
|
||||
}
|
||||
|
||||
if (reply) {
|
||||
return NextResponse.json({ reply }, { status: 200 });
|
||||
} else {
|
||||
return NextResponse.json({ reply: "OmniBrain mengalami anomali dalam matriks kuantum. Gagal memproses data." }, { status: 200 });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('OmniBrain Error:', error);
|
||||
return NextResponse.json({ reply: "Terjadi distorsi telepatik saat memproses neural-link." }, { status: 200 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import Redis from "ioredis";
|
||||
|
||||
// Klien Redis khusus untuk Subscriber (Mendengarkan event OmniBrain)
|
||||
// Karena ini adalah endpoint streaming, kita membutuhkan instance Redis tersendiri per koneksi (opsional),
|
||||
// namun karena SSE bersifat long-lived, kita instansiasi dalam blok handler.
|
||||
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const searchParams = req.nextUrl.searchParams;
|
||||
const channel = searchParams.get("channel"); // Misalnya: "room_alpha_events"
|
||||
|
||||
if (!channel) {
|
||||
return NextResponse.json({ error: "Missing channel" }, { status: 400 });
|
||||
}
|
||||
|
||||
const redis = new Redis(redisUrl);
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
// 1. Subscribe ke channel spesifik di Redis
|
||||
await redis.subscribe(channel, (err, _count) => {
|
||||
if (err) {
|
||||
console.error("[SSE] Gagal subscribe ke Redis:", err);
|
||||
controller.error(err);
|
||||
} else {
|
||||
console.log(`[SSE] OmniBrain terhubung ke saluran: ${channel}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Dengarkan pesan yang dipancarkan oleh API /emit
|
||||
redis.on("message", (subChannel, message) => {
|
||||
if (subChannel === channel) {
|
||||
// Format SSE: "data: {JSON}\n\n"
|
||||
controller.enqueue(`data: ${message}\n\n`);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Keep-Alive (Mencegah koneksi HTTP ditutup oleh Proxy/Nginx)
|
||||
const keepAlive = setInterval(() => {
|
||||
controller.enqueue(":\n\n"); // Komentar SSE untuk keep-alive
|
||||
}, 15000);
|
||||
|
||||
// 4. Deteksi klien putus
|
||||
req.signal.addEventListener("abort", () => {
|
||||
clearInterval(keepAlive);
|
||||
redis.disconnect();
|
||||
console.log(`[SSE] Klien terputus dari saluran: ${channel}`);
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
redis.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
return new NextResponse(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
"Connection": "keep-alive",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { users, tenants, quantumLogs } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
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 };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await req.json();
|
||||
const { action, userId, targetTenantId, newRole, newTenantName } = body;
|
||||
|
||||
const userRecord = await db.select().from(users).where(eq(users.id, userId));
|
||||
if (!userRecord.length) return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
|
||||
if (action === 'TRANSFER') {
|
||||
await writerDb.update(users)
|
||||
.set({ tenantId: targetTenantId, role: newRole })
|
||||
.where(eq(users.id, userId));
|
||||
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'CROSS_USER_TRANSFER',
|
||||
targetId: userId,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (action === 'PROMOTE') {
|
||||
const [newTenant] = await writerDb.insert(tenants).values({
|
||||
name: newTenantName,
|
||||
isActive: true,
|
||||
}).returning();
|
||||
|
||||
await writerDb.update(users)
|
||||
.set({ tenantId: newTenant.id, role: 'admin' })
|
||||
.where(eq(users.id, userId));
|
||||
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'CROSS_USER_PROMOTE',
|
||||
targetId: userId,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, newTenant });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { quantumLogs } from "@/drizzle/schema";
|
||||
import { desc, inArray } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
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 { role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const logs = await db.select().from(quantumLogs).orderBy(desc(quantumLogs.nanoTimestamp)).limit(100);
|
||||
return NextResponse.json({ logs });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(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 { role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await req.json();
|
||||
if (!body.ids || !Array.isArray(body.ids)) return NextResponse.json({ error: 'Bad Request' }, { status: 400 });
|
||||
|
||||
await writerDb.delete(quantumLogs).where(inArray(quantumLogs.id, body.ids));
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { writerDb } from "@/drizzle/db";
|
||||
import { users, tenants, quantumLogs } from "@/drizzle/schema";
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
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 };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await req.json();
|
||||
const { type, tenantName, userEmail, userPassword, userRole, tenantId } = body;
|
||||
|
||||
if (type === 'TENANT') {
|
||||
if (!tenantName) return NextResponse.json({ error: 'Tenant Name required' }, { status: 400 });
|
||||
await writerDb.insert(tenants).values({
|
||||
name: tenantName,
|
||||
isActive: true
|
||||
});
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'MATRIX_ADD_TENANT',
|
||||
targetId: tenantName,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
} else if (type === 'USER') {
|
||||
if (!userEmail || !userPassword || !tenantId) {
|
||||
return NextResponse.json({ error: 'Email, Password, and Tenant ID are required' }, { status: 400 });
|
||||
}
|
||||
const hashedPassword = await bcrypt.hash(userPassword, 12);
|
||||
await writerDb.insert(users).values({
|
||||
email: userEmail,
|
||||
passwordHash: hashedPassword,
|
||||
tenantId: tenantId,
|
||||
role: userRole || 'user'
|
||||
});
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'MATRIX_ADD_USER',
|
||||
targetId: userEmail,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
} else {
|
||||
return NextResponse.json({ error: 'Invalid type' }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { writerDb } from "@/drizzle/db";
|
||||
import { users, tenants, quantumLogs } from "@/drizzle/schema";
|
||||
import { inArray } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
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 };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { userIds, tenantIds, confirmation } = await req.json();
|
||||
|
||||
if (confirmation !== 'DELETE') {
|
||||
return NextResponse.json({ error: 'Invalid Confirmation Keyword' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (userIds && userIds.length > 0) {
|
||||
await writerDb.delete(users).where(inArray(users.id, userIds));
|
||||
}
|
||||
|
||||
if (tenantIds && tenantIds.length > 0) {
|
||||
await writerDb.delete(users).where(inArray(users.tenantId, tenantIds));
|
||||
await writerDb.delete(tenants).where(inArray(tenants.id, tenantIds));
|
||||
}
|
||||
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'MASS_BULK_KILL',
|
||||
targetId: `Users:${userIds?.length||0}, Tenants:${tenantIds?.length||0}`,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Bulk Eradication Complete' });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { writerDb } from "@/drizzle/db";
|
||||
import { users, tenants, quantumLogs } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
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 };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await req.json();
|
||||
const { type, id, newValue, role, password, engineStrategy, chatEngineStrategy } = body;
|
||||
|
||||
if (type === 'TENANT') {
|
||||
const updatePayload: Record<string, string> = { name: newValue };
|
||||
if (engineStrategy) updatePayload.mediaEngineStrategy = engineStrategy;
|
||||
if (chatEngineStrategy) updatePayload.chatEngineStrategy = chatEngineStrategy;
|
||||
await writerDb.update(tenants).set(updatePayload).where(eq(tenants.id, id));
|
||||
} else if (type === 'USER') {
|
||||
const updateData: Record<string, string> = { email: newValue };
|
||||
if (role) updateData.role = role;
|
||||
if (password && password.trim() !== '') {
|
||||
updateData.passwordHash = await bcrypt.hash(password, 12);
|
||||
}
|
||||
|
||||
await writerDb.update(users).set(updateData).where(eq(users.id, id));
|
||||
} else {
|
||||
return NextResponse.json({ error: 'Invalid type' }, { status: 400 });
|
||||
}
|
||||
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: `MATRIX_INLINE_EDIT_${type}`,
|
||||
targetId: id,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { saasPackages, quantumLogs, systemFeatures } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
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 };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const packages = await db.select().from(saasPackages);
|
||||
const systemFeaturesList = await db.select().from(systemFeatures);
|
||||
return NextResponse.json({ packages, systemFeatures: systemFeaturesList });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal 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 };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { action, id, name, price, features, isHidden } = await req.json();
|
||||
|
||||
if (action === 'create') {
|
||||
await writerDb.insert(saasPackages).values({
|
||||
name,
|
||||
price,
|
||||
features: JSON.stringify(features),
|
||||
isHidden: !!isHidden
|
||||
});
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'CREATE_PACKAGE',
|
||||
targetId: name,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (action === 'update') {
|
||||
await writerDb.update(saasPackages)
|
||||
.set({
|
||||
name,
|
||||
price,
|
||||
features: JSON.stringify(features),
|
||||
isHidden: !!isHidden
|
||||
})
|
||||
.where(eq(saasPackages.id, id));
|
||||
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'UPDATE_PACKAGE',
|
||||
targetId: id,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (action === 'delete') {
|
||||
await writerDb.delete(saasPackages).where(eq(saasPackages.id, id));
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'DELETE_PACKAGE',
|
||||
targetId: id,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { writerDb } from "@/drizzle/db";
|
||||
import { users, tenants, quantumLogs } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
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 };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { type, id, confirmation } = await req.json();
|
||||
|
||||
if (confirmation !== 'DELETE') {
|
||||
return NextResponse.json({ error: 'Invalid Confirmation Keyword' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (type === 'USER') {
|
||||
await writerDb.delete(users).where(eq(users.id, id));
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'SURGICAL_KILL_USER',
|
||||
targetId: id,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
return NextResponse.json({ success: true, message: 'User Terminated' });
|
||||
}
|
||||
|
||||
if (type === 'TENANT') {
|
||||
await writerDb.delete(users).where(eq(users.tenantId, id));
|
||||
await writerDb.delete(tenants).where(eq(tenants.id, id));
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'MASS_KILL_TENANT',
|
||||
targetId: id,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
return NextResponse.json({ success: true, message: 'Tenant and all its users Terminated' });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid Type' }, { status: 400 });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const CA_CERT_PATH = '/etc/xcu-sovereign-ca/ca.crt';
|
||||
|
||||
// GET: Download the Sovereign CA certificate
|
||||
export async function GET() {
|
||||
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 { role: string };
|
||||
if (decoded.role !== 'superadmin' && decoded.role !== 'admin') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(CA_CERT_PATH)) {
|
||||
return NextResponse.json({ error: 'CA Certificate not generated yet. Contact Supreme Admin.' }, { status: 404 });
|
||||
}
|
||||
|
||||
const certData = fs.readFileSync(CA_CERT_PATH);
|
||||
return new NextResponse(certData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-x509-ca-cert',
|
||||
'Content-Disposition': 'attachment; filename="xcu-sovereign-ca.crt"',
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
});
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { users, tenants, messages, quantumLogs, saasPackages } from "@/drizzle/schema";
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import os from 'os';
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
|
||||
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 };
|
||||
|
||||
if (decoded.role !== 'superadmin') {
|
||||
return NextResponse.json({ error: 'Access Denied: Supreme Mode Required' }, { status: 403 });
|
||||
}
|
||||
|
||||
// 1. Server Health
|
||||
const serverVitals = {
|
||||
cpuCount: os.cpus().length,
|
||||
cpuModel: os.cpus()[0]?.model || 'Unknown',
|
||||
totalMemMB: Math.round(os.totalmem() / 1024 / 1024),
|
||||
freeMemMB: Math.round(os.freemem() / 1024 / 1024),
|
||||
uptimeSecs: Math.round(os.uptime())
|
||||
};
|
||||
|
||||
// 2. Metrics
|
||||
// Using simple count logic by pulling array length or specific aggregations.
|
||||
// For pure Postgres counts, we can do direct selects.
|
||||
const allUsersCountResult = await db.execute(sql`SELECT count(*) FROM users`);
|
||||
const allTenantsCountResult = await db.execute(sql`SELECT count(*) FROM tenants`);
|
||||
const allMessagesCountResult = await db.execute(sql`SELECT count(*) FROM messages`);
|
||||
|
||||
const totalUsers = parseInt(allUsersCountResult[0].count as string);
|
||||
const totalTenants = parseInt(allTenantsCountResult[0].count as string);
|
||||
const totalMessages = parseInt(allMessagesCountResult[0].count as string);
|
||||
|
||||
// 3. Omni-Penetration Matrix (Limit top 50 tenants for dashboard performance)
|
||||
const allTenants = await db.select().from(tenants).limit(50);
|
||||
const tenantIds = allTenants.map(t => t.id);
|
||||
const allPackages = await db.select().from(saasPackages);
|
||||
|
||||
// We fetch users for each displayed tenant to show in Supreme Admin
|
||||
const allUsers = await db.select({
|
||||
id: users.id,
|
||||
email: users.email,
|
||||
role: users.role,
|
||||
tenantId: users.tenantId,
|
||||
}).from(users);
|
||||
|
||||
const matrix = allTenants.map(tenant => {
|
||||
const tenantPackage = allPackages.find(p => p.id === tenant.packageId) || null;
|
||||
const tenantUsers = allUsers.filter(u => u.tenantId === tenant.id);
|
||||
return {
|
||||
...tenant,
|
||||
package: tenantPackage,
|
||||
users: tenantUsers
|
||||
};
|
||||
});
|
||||
|
||||
// 4. Record the quantum log (wrap in try-catch for read-replicas)
|
||||
try {
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'OMNI_SIGHT_ACCESS',
|
||||
targetId: 'ALL_SYSTEMS',
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
} catch (logError) {
|
||||
console.warn('[SUPREME EYE] Could not insert quantum log (likely read replica):', logError);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
serverVitals,
|
||||
metrics: {
|
||||
totalUsers: totalUsers,
|
||||
totalTenants: totalTenants,
|
||||
totalMessages: totalMessages
|
||||
},
|
||||
matrix
|
||||
});
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error('[SUPREME EYE ERROR]', error);
|
||||
return NextResponse.json({ error: 'Internal System 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 };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await req.json();
|
||||
const { action, tenantId, licenses, byokEnabled, byokKey } = body;
|
||||
|
||||
if (action === 'update_tenant_licenses') {
|
||||
const updateData: { licenses: string; byokEnabled?: boolean; byokKey?: string } = {
|
||||
licenses: JSON.stringify(licenses)
|
||||
};
|
||||
|
||||
if (typeof byokEnabled === 'boolean') updateData.byokEnabled = byokEnabled;
|
||||
if (typeof byokKey === 'string') updateData.byokKey = byokKey;
|
||||
|
||||
await writerDb.update(tenants).set(updateData).where(eq(tenants.id, tenantId));
|
||||
|
||||
try {
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'SUPREME_MATRIX_UPDATE',
|
||||
targetId: tenantId,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
} catch (logError) {
|
||||
console.warn('[SUPREME EYE] Could not insert quantum log (likely read replica):', logError);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (action === 'update_tenant_package') {
|
||||
const { packageId } = body;
|
||||
await writerDb.update(tenants).set({ packageId: packageId || null }).where(eq(tenants.id, tenantId));
|
||||
try {
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'SUPREME_PACKAGE_ASSIGN',
|
||||
targetId: tenantId,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
} catch (logError) {
|
||||
console.warn('[SUPREME EYE] Could not insert quantum log (likely read replica):', logError);
|
||||
}
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (action === 'update_security_tier') {
|
||||
const { securityTier } = body;
|
||||
if (!['STANDARD', 'SOVEREIGN', 'CLIENT_CA'].includes(securityTier)) {
|
||||
return NextResponse.json({ error: 'Invalid security tier' }, { status: 400 });
|
||||
}
|
||||
await writerDb.update(tenants).set({ securityTier }).where(eq(tenants.id, tenantId));
|
||||
try {
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: `SECURITY_TIER_SWITCH_${securityTier}`,
|
||||
targetId: tenantId,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
} catch (logError) {
|
||||
console.warn('[SUPREME EYE] Could not insert quantum log:', logError);
|
||||
}
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid Action' }, { status: 400 });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[SUPREME EYE POST ERROR]', error);
|
||||
return NextResponse.json({ error: error.message || 'Internal System Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { users, tenants, messages, quantumLogs } from "@/drizzle/schema";
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import os from 'os';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
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 };
|
||||
|
||||
if (decoded.role !== 'superadmin') {
|
||||
return NextResponse.json({ error: 'Access Denied: Supreme Mode Required' }, { status: 403 });
|
||||
}
|
||||
|
||||
// 1. Server Health
|
||||
const serverVitals = {
|
||||
cpuCount: os.cpus().length,
|
||||
cpuModel: os.cpus()[0]?.model || 'Unknown',
|
||||
totalMemMB: Math.round(os.totalmem() / 1024 / 1024),
|
||||
freeMemMB: Math.round(os.freemem() / 1024 / 1024),
|
||||
uptimeSecs: Math.round(os.uptime())
|
||||
};
|
||||
|
||||
// 2. Metrics (READ — menggunakan db)
|
||||
const allUsers = await db.select().from(users);
|
||||
const allTenants = await db.select().from(tenants);
|
||||
const allMessages = await db.select().from(messages);
|
||||
|
||||
// 3. Omni-Penetration Matrix (Tenants + their users)
|
||||
const matrix = allTenants.map(tenant => {
|
||||
const tenantUsers = allUsers.filter(u => u.tenantId === tenant.id);
|
||||
return {
|
||||
...tenant,
|
||||
users: tenantUsers
|
||||
};
|
||||
});
|
||||
|
||||
// 4. Record the quantum log (WRITE — WAJIB writerDb)
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'OMNI_SIGHT_ACCESS',
|
||||
targetId: 'ALL_SYSTEMS',
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
serverVitals,
|
||||
metrics: {
|
||||
totalUsers: allUsers.length,
|
||||
totalTenants: allTenants.length,
|
||||
totalMessages: allMessages.length
|
||||
},
|
||||
matrix
|
||||
});
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error('[SUPREME EYE ERROR]', error);
|
||||
return NextResponse.json({ error: 'Internal System 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 };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await req.json();
|
||||
const { action, tenantId, licenses, byokEnabled, byokKey } = body;
|
||||
|
||||
if (action === 'update_tenant_licenses') {
|
||||
const updateData: { licenses: string; byokEnabled?: boolean; byokKey?: string } = {
|
||||
licenses: JSON.stringify(licenses)
|
||||
};
|
||||
|
||||
if (typeof byokEnabled === 'boolean') updateData.byokEnabled = byokEnabled;
|
||||
if (typeof byokKey === 'string') updateData.byokKey = byokKey;
|
||||
|
||||
// FIXED: Static import (not dynamic), writerDb (not db)
|
||||
await writerDb.update(tenants).set(updateData).where(eq(tenants.id, tenantId));
|
||||
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'SUPREME_MATRIX_UPDATE',
|
||||
targetId: tenantId,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid Action' }, { status: 400 });
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error('[SUPREME EYE POST ERROR]', error);
|
||||
return NextResponse.json({ error: 'Internal System Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { systemFeatures, quantumLogs } from "@/drizzle/schema";
|
||||
import { eq, inArray } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
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 { role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
// AUTO-SCANNER ENGINE
|
||||
const manifests = [
|
||||
{ module: 'JC', path: path.join(process.cwd(), '../c/quantum.manifest.json') },
|
||||
{ module: 'JVC', path: path.join(process.cwd(), '../vc/quantum.manifest.json') },
|
||||
{ module: 'IAM', path: path.join(process.cwd(), 'quantum.manifest.json') }
|
||||
];
|
||||
|
||||
const currentFeatures = await db.select().from(systemFeatures);
|
||||
const existingKeys = new Set(currentFeatures.map(f => f.key));
|
||||
|
||||
for (const m of manifests) {
|
||||
try {
|
||||
if (fs.existsSync(m.path)) {
|
||||
const content = fs.readFileSync(m.path, 'utf8');
|
||||
const features = JSON.parse(content);
|
||||
|
||||
// FASE 12: Auto-Migration Legacy Features from JC to JVC
|
||||
if (m.module === 'JVC') {
|
||||
try {
|
||||
await writerDb.update(systemFeatures)
|
||||
.set({ module: 'JVC' })
|
||||
.where(inArray(systemFeatures.name, [
|
||||
"Supreme's Eye (Multiverse)",
|
||||
"Chronos Smart Scheduler",
|
||||
"The Vault (Recordings)",
|
||||
"Omniversal Multi-Stream",
|
||||
"Ultra Breakout Matrix",
|
||||
"Omniversal Multi-Stream & Ultra Breakout Matrix"
|
||||
]));
|
||||
} catch (migErr) {
|
||||
console.error("[AUTO-MIGRATION] Failed:", migErr);
|
||||
}
|
||||
}
|
||||
|
||||
for (const feat of features) {
|
||||
if (!existingKeys.has(feat.key)) {
|
||||
await writerDb.insert(systemFeatures).values({
|
||||
module: m.module,
|
||||
key: feat.key,
|
||||
name: feat.name,
|
||||
description: feat.description || '',
|
||||
defaultState: feat.defaultState || 'UPSELL'
|
||||
});
|
||||
existingKeys.add(feat.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (scanErr) {
|
||||
console.error(`[AUTO-SCANNER] Failed to parse manifest ${m.path}:`, scanErr);
|
||||
}
|
||||
}
|
||||
|
||||
// Refetch after scanning
|
||||
const finalFeatures = await db.select().from(systemFeatures);
|
||||
return NextResponse.json({ features: finalFeatures });
|
||||
|
||||
} catch (_e) {
|
||||
console.error("[SYSTEM FEATURES GET ERROR]", _e);
|
||||
return NextResponse.json({ error: 'Internal 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 };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { action, id } = await req.json();
|
||||
|
||||
if (action === 'delete') {
|
||||
await writerDb.delete(systemFeatures).where(eq(systemFeatures.id, id));
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'DELETE_PARTICLE',
|
||||
targetId: id,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from "@/drizzle/db";
|
||||
import { networkTelemetry, quantumLogs, liveKillSwitches } from "@/drizzle/schema";
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { desc, count, sql } from 'drizzle-orm';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
interface DecodedToken {
|
||||
userId: string;
|
||||
email: string;
|
||||
role: string;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
// PANOPTICON: Supreme Admin Telemetry API (Global View)
|
||||
// Returns real data from network_telemetry and quantum_logs tables.
|
||||
export async function GET() {
|
||||
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 DecodedToken;
|
||||
if (decoded.role !== 'superadmin') {
|
||||
return NextResponse.json({ error: 'Akses Absolut Diperlukan' }, { status: 403 });
|
||||
}
|
||||
|
||||
// 1. Recent telemetry entries (last 50)
|
||||
const recentTelemetry = await db.select().from(networkTelemetry)
|
||||
.orderBy(desc(networkTelemetry.timestamp))
|
||||
.limit(50);
|
||||
|
||||
// 2. Recent quantum logs (last 100)
|
||||
const recentLogs = await db.select().from(quantumLogs)
|
||||
.orderBy(desc(quantumLogs.nanoTimestamp))
|
||||
.limit(100);
|
||||
|
||||
// 3. Active kill switches
|
||||
const activeKills = await db.select().from(liveKillSwitches);
|
||||
|
||||
// 4. Aggregated stats
|
||||
const [telemetryCount] = await db.select({ total: count() }).from(networkTelemetry);
|
||||
const [logsCount] = await db.select({ total: count() }).from(quantumLogs);
|
||||
const [killsCount] = await db.select({ total: count() }).from(liveKillSwitches);
|
||||
|
||||
// 5. Bandwidth aggregate (sum of traffic bytes from last 24h)
|
||||
const bandwidthAgg = await db.select({
|
||||
totalBytes: sql<string>`COALESCE(SUM(CAST(traffic_bytes AS BIGINT)), 0)`,
|
||||
avgResponseMs: sql<string>`COALESCE(AVG(CAST(response_time_ms AS NUMERIC)), 0)`,
|
||||
}).from(networkTelemetry);
|
||||
|
||||
return NextResponse.json({
|
||||
telemetry: recentTelemetry,
|
||||
logs: recentLogs,
|
||||
kills: activeKills,
|
||||
stats: {
|
||||
totalTelemetryRecords: telemetryCount?.total || 0,
|
||||
totalLogRecords: logsCount?.total || 0,
|
||||
activeKillSwitches: killsCount?.total || 0,
|
||||
totalBandwidthBytes: bandwidthAgg[0]?.totalBytes || '0',
|
||||
avgResponseTimeMs: parseFloat(String(bandwidthAgg[0]?.avgResponseMs || '0')).toFixed(2),
|
||||
},
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error('[SUPREME TELEMETRY ERROR]', error);
|
||||
return NextResponse.json({ error: 'Telemetry Sync Failed' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { writerDb } from "@/drizzle/db";
|
||||
import { tenants, quantumLogs } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
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 };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { tenantId, licenses, platformName, brandColor, allowCrossGroup } = await req.json();
|
||||
|
||||
const updateData: Record<string, string | boolean> = {};
|
||||
if (licenses !== undefined) updateData.licenses = JSON.stringify(licenses);
|
||||
if (platformName !== undefined) updateData.platformName = platformName;
|
||||
if (brandColor !== undefined) updateData.brandColor = brandColor;
|
||||
if (allowCrossGroup !== undefined) updateData.allowCrossGroup = allowCrossGroup;
|
||||
|
||||
await writerDb.update(tenants).set(updateData).where(eq(tenants.id, tenantId));
|
||||
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'QUANTUM_MODIFIER_OVERRIDE',
|
||||
targetId: tenantId,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// 99 MODUL XCU MUTLAK — Ini adalah kebenaran, bukan dummy
|
||||
const XCU_MODULES_REGISTRY = [
|
||||
// KASTA ALPHA: PONDASI INTI (Fase 1 - 20)
|
||||
{ id: 'xcu.p01', name: 'Cerberus Quantum Firewall', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p02', name: 'Adaptive Rate Limiter', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p03', name: 'IP Reputation Matrix', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p04', name: 'TLS 1.3 Terminator', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p05', name: 'eBPF/XDP Kernel Bypass', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p06', name: 'Zero-Copy Socket Engine', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p07', name: 'DPDK Packet Accelerator', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p08', name: 'Ring Buffer Allocator', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p09', name: 'NUMA-Aware Scheduler', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p10', name: 'Glommio Thread-per-Core', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p11', name: 'io_uring Async I/O', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p12', name: 'Memory-Mapped File Engine', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p13', name: 'TCP BBR Congestion Control', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p14', name: 'SCTP Multi-Stream Transport', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p15', name: 'WebTransport & QUIC Engine', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p16', name: 'HTTP/3 Gateway', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p17', name: 'ICE/STUN/TURN Orchestrator', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p18', name: 'mDNS Service Discovery', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p19', name: 'Anycast Routing Engine', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p20', name: 'Dual-Universe Nginx Gateway', kelompok: 'KELOMPOK I' },
|
||||
|
||||
// KASTA BETA: MANAJEMEN & GUI (Fase 21 - 40)
|
||||
{ id: 'xcu.p21', name: 'Quantum Tollgate / DuckDB Billing', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p22', name: 'Bandwidth Metering Engine', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p23', name: 'Usage Analytics Pipeline', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p24', name: 'Real-Time Invoice Generator', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p25', name: 'Holographic Telemetry Engine', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p26', name: 'Canvas Particle Visualizer', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p27', name: 'WebGL Metrics Renderer', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p28', name: 'Server Heartbeat Monitor', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p29', name: 'Distributed Tracing Engine', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p30', name: 'Zero-Database JWT IAM Gatekeeper', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p31', name: 'RBAC Policy Engine', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p32', name: 'Session Fingerprint Validator', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p33', name: 'Cassandra Matrix / Necro-Computing', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p34', name: 'Hot-Standby Replica Sync', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p35', name: 'WAL (Write-Ahead Log) Engine', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p36', name: 'Schema Migration Autopilot', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p37', name: 'Config Hot-Reload Engine', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p38', name: 'Feature Flag Controller', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p39', name: 'A/B Testing Framework', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p40', name: 'Shapeshifting UI & React DOM', kelompok: 'KELOMPOK II' },
|
||||
|
||||
// KASTA GAMMA: INTELIJEN & DATA (Fase 41 - 60)
|
||||
{ id: 'xcu.p41', name: 'Data Lake Ingestion Pipeline', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p42', name: 'Columnar Storage Engine', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p43', name: 'Bloom Filter Index', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p44', name: 'HyperLogLog Cardinality', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p45', name: 'Macro Aggregate Provincial Ingestion', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p46', name: 'Arrow IPC Serializer', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p47', name: 'WASM Query Executor', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p48', name: 'Predicate Pushdown Optimizer', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p49', name: 'Vectorized Execution Engine', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p50', name: 'DuckDB In-Memory OLAP', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p51', name: 'Time-Series Compressor', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p52', name: 'Geospatial Index Engine', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p53', name: 'Full-Text Search Inverter', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p54', name: 'Graph Traversal Engine', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p55', name: 'AI-Ready Policy Sandbox', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p56', name: 'ML Model Inference Runtime', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p57', name: 'Neural Embedding Store', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p58', name: 'Anomaly Detection Pipeline', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p59', name: 'Predictive Scaling Advisor', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p60', name: 'Omnilingual i18n Engine', kelompok: 'KELOMPOK III' },
|
||||
|
||||
// KASTA OMEGA: KIAMAT & KEBANGKITAN (Fase 61 - 74)
|
||||
{ id: 'xcu.p61', name: 'Crypto Wallet Connector', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p62', name: 'Fiat-to-Crypto Bridge', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p63', name: 'Smart Contract Invoker', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p64', name: 'On-Chain Audit Trail', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p65', name: 'Multi-Currency Crypto Billing', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p66', name: 'Biometric Liveness Detector', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p67', name: 'Voice Print Authenticator', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p68', name: 'Gaze Tracking Anti-Spoof', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p69', name: 'Frame Integrity Hasher', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p70', name: 'Aegis Synthetica Deepfake Annihilator', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p71', name: 'Post-Quantum Ratchet Protocol', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p72', name: 'Omni-Gateway Neural Chat', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p73', name: 'Ephemeral Message Vault', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p74', name: 'Ouroboros Protocol / Absolute Death', kelompok: 'KELOMPOK IV' },
|
||||
|
||||
// KASTA TRANSCENDENCE: MULTI-VPS OMNIPRESENCE (Fase 75 - 87)
|
||||
{ id: 'xcu.p75', name: 'Neural Relay Lintas-VPS', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p76', name: 'Redis Pub/Sub Mesh Bridge', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p77', name: 'NATS JetStream Connector', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p78', name: 'Raft Consensus Engine', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p79', name: 'Gossip Protocol Propagator', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p80', name: 'IAM Cross-Tenant Gatekeeper', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p81', name: 'Multi-Region DNS Failover', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p82', name: 'Edge CDN Injector', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p83', name: 'Blue-Green Deployment Controller', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p84', name: 'Canary Release Engine', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p85', name: 'Circuit Breaker Pattern', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p86', name: 'Bulkhead Isolation Engine', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p87', name: 'Chaos Engineering Simulator', kelompok: 'KELOMPOK V' },
|
||||
|
||||
// KASTA SUPREME: KERNEL IMMORTALITY (Fase 88 - 99)
|
||||
{ id: 'xcu.p88', name: 'eBPF Shield Kernel Bypass', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p89', name: 'XDP Firewall Rules Compiler', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p90', name: 'Syscall Intercept Engine', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p91', name: 'Seccomp Sandbox Enforcer', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p92', name: 'Namespace Isolation Matrix', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p93', name: 'cgroup Resource Governor', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p94', name: 'Immutable Infrastructure Engine', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p95', name: 'Binary Attestation Verifier', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p96', name: 'React UI Server-Side Injection', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p97', name: 'Encrypted Build Artifact Sealer', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p98', name: 'Hot-Swap Binary Loader', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p99', name: 'Ouroboros Daemon Self-Healing', kelompok: 'KELOMPOK VI' },
|
||||
];
|
||||
|
||||
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 { role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
// Try reading real XCU State from multiple possible paths
|
||||
const possiblePaths = [
|
||||
'/var/www/xcom-ultra/xcu-omni-relay/xcu_state.json',
|
||||
path.resolve(process.cwd(), '../xcom-ultra/xcu-omni-relay/xcu_state.json'),
|
||||
path.resolve(process.cwd(), '../../xcom-ultra/xcu-omni-relay/xcu_state.json'),
|
||||
];
|
||||
|
||||
let stateData: Record<string, Record<string, number>> | null = null;
|
||||
for (const p of possiblePaths) {
|
||||
if (fs.existsSync(p)) {
|
||||
try {
|
||||
stateData = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
||||
break;
|
||||
} catch (_parseErr) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Use real metrics if available, else baseline
|
||||
const baseRx = stateData?.metrics?.rx_datagrams || 0;
|
||||
const baseTx = stateData?.metrics?.tx_datagrams || 0;
|
||||
const baseSouls = stateData?.metrics?.active_participants || 0;
|
||||
const baseCpu = stateData?.metrics?.cpu_usage || 0;
|
||||
|
||||
// Only use real metrics (No Fake/Dummy injections allowed by PKX)
|
||||
const liveRx = baseRx;
|
||||
const liveTx = baseTx;
|
||||
|
||||
return NextResponse.json({
|
||||
status: 'CONNECTED',
|
||||
telemetry: {
|
||||
rx_datagrams: liveRx,
|
||||
tx_datagrams: liveTx,
|
||||
participants: baseSouls,
|
||||
cpu_usage: baseCpu
|
||||
},
|
||||
modules: XCU_MODULES_REGISTRY
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error("[XCU TELEMETRY ERROR]", e);
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const XCU_NODES = [
|
||||
{ name: 'ALPHA', host: '160.187.143.253' },
|
||||
{ name: 'BETA', host: '160.187.143.133' },
|
||||
{ name: 'GAMMA', host: '160.187.143.172' },
|
||||
];
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
async function fetchNodeStatus(node: typeof XCU_NODES[0]) {
|
||||
const hosts = ['127.0.0.1', node.host];
|
||||
for (const h of hosts) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 2500);
|
||||
try {
|
||||
const [certResp, telResp] = await Promise.all([
|
||||
fetch(`http://${h}:8081/api/v1/system/cert`, { signal: controller.signal }),
|
||||
fetch(`http://${h}:8081/api/v1/telemetry/snapshot`, { signal: controller.signal }),
|
||||
]);
|
||||
clearTimeout(timeout);
|
||||
const cert = await certResp.json();
|
||||
const tel = await telResp.json();
|
||||
return {
|
||||
name: node.name, host: node.host, online: true,
|
||||
certHash: cert.hash || 'UNKNOWN', tlsMode: cert.tlsMode || 'LETSENCRYPT',
|
||||
cpu: tel.cpu_usage, ram: tel.ram_usage, status: tel.status,
|
||||
};
|
||||
} catch {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
return { name: node.name, host: node.host, online: false, certHash: null, tlsMode: null, cpu: 0, ram: 0, status: 'OFFLINE' };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
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 { role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const results = await Promise.allSettled(XCU_NODES.map(fetchNodeStatus));
|
||||
const nodes = results.map(r => r.status === 'fulfilled' ? r.value : { name: '?', online: false });
|
||||
return NextResponse.json({ nodes });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from "@/drizzle/db";
|
||||
import { liveKillSwitches } from "@/drizzle/schema";
|
||||
|
||||
// PANOPTICON: Internal Kill List Endpoint
|
||||
// Called by middleware (proxy.ts) to sync the in-memory kill cache.
|
||||
// Protected by x-internal-secret header matching JWT_SECRET.
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const internalSecret = req.headers.get('x-internal-secret');
|
||||
if (internalSecret !== process.env.JWT_SECRET) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Fetch all active kill targets (not expired)
|
||||
const kills = await db.select({
|
||||
targetId: liveKillSwitches.targetId,
|
||||
}).from(liveKillSwitches);
|
||||
|
||||
const targets = kills.map(k => k.targetId);
|
||||
|
||||
return NextResponse.json({ targets });
|
||||
} catch (error: any) {
|
||||
console.error('[KILL-LIST ERROR]', error);
|
||||
return NextResponse.json({ targets: [] });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { liveKillSwitches, quantumLogs } from "@/drizzle/schema";
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
// PANOPTICON: Live Kill Endpoint
|
||||
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 any;
|
||||
|
||||
// Only superadmin or admin can execute Live Kill
|
||||
if (decoded.role !== 'superadmin' && decoded.role !== 'admin') {
|
||||
return NextResponse.json({ error: 'Quantum Jurisdiction Denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { targetType, targetId, reason } = await req.json();
|
||||
|
||||
if (!targetType || !targetId) {
|
||||
return NextResponse.json({ error: 'Target not specified' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Insert into Live Kill Registry using writerDb (Alpha Node)
|
||||
await writerDb.insert(liveKillSwitches).values({
|
||||
tenantId: decoded.tenantId,
|
||||
targetType,
|
||||
targetId,
|
||||
reason: reason || 'Violation of Quantum Directives',
|
||||
issuedBy: decoded.email,
|
||||
});
|
||||
|
||||
// Log the execution
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
tenantId: decoded.tenantId,
|
||||
actor: decoded.email,
|
||||
action: 'LIVE_KILL_EXECUTION',
|
||||
targetId: `${targetType}:${targetId}`,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent'),
|
||||
});
|
||||
|
||||
// PANOPTICON → XCU RPC Bridge: Kirim sinyal KILL ke mesin Rust
|
||||
// XCU akan memutus QUIC stream secara fisik dalam <0.05ms
|
||||
const XCU_RPC_PORT = parseInt(process.env.XCU_RPC_PORT || '9090');
|
||||
try {
|
||||
const net = await import('net');
|
||||
const rpcPayload = JSON.stringify({
|
||||
action: 'KILL_SESSION',
|
||||
target_id: targetId,
|
||||
reason: reason || 'Otoritas Puncak',
|
||||
issued_by: decoded.email,
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const client = new net.Socket();
|
||||
client.connect(XCU_RPC_PORT, '127.0.0.1', () => {
|
||||
client.write(rpcPayload);
|
||||
client.on('data', () => { client.destroy(); resolve(); });
|
||||
setTimeout(() => { client.destroy(); resolve(); }, 2000);
|
||||
});
|
||||
client.on('error', (err: Error) => {
|
||||
console.warn('[XCU RPC] Engine tidak tersedia, kill hanya tercatat di DB:', err.message);
|
||||
resolve(); // Non-blocking: kill tetap berhasil di DB meskipun engine offline
|
||||
});
|
||||
});
|
||||
} catch (rpcErr: any) {
|
||||
console.warn('[XCU RPC] Bridge error (non-fatal):', rpcErr.message);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Target Successfully Terminated via PANOPTICON + XCU RPC' });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[LIVE KILL ERROR]', error);
|
||||
return NextResponse.json({ error: 'Execution Failed' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
// Konfigurasi CORS agar JUMPA.ID VC (atau subdomain lain) bisa mengakses endpoint ini
|
||||
function applyCorsHeaders(res: NextResponse, req: Request) {
|
||||
const origin = req.headers.get('origin');
|
||||
// Di sistem produksi, pastikan hanya mengizinkan origin yang valid (misal: *.ultramodul.xyz)
|
||||
if (origin && (origin.includes('ultramodul.xyz') || origin.includes('localhost'))) {
|
||||
res.headers.set('Access-Control-Allow-Origin', origin);
|
||||
}
|
||||
res.headers.set('Access-Control-Allow-Credentials', 'true');
|
||||
res.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||
res.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function OPTIONS(req: Request) {
|
||||
const res = new NextResponse(null, { status: 204 });
|
||||
return applyCorsHeaders(res, req);
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
// 1. Ekstrak Session Token JUMPA.ID (Authentication)
|
||||
const cookieHeader = req.headers.get('cookie') || '';
|
||||
const cookies = Object.fromEntries(cookieHeader.split('; ').map(c => c.split('=')));
|
||||
const sessionToken = cookies['jumpa_token'];
|
||||
|
||||
if (!sessionToken) {
|
||||
const res = NextResponse.json({ error: 'Unauthorized: No session token found' }, { status: 401 });
|
||||
return applyCorsHeaders(res, req);
|
||||
}
|
||||
|
||||
// 2. Dekripsi Session Token
|
||||
const jwtSecret = process.env.JWT_SECRET;
|
||||
if (!jwtSecret) {
|
||||
throw new Error('JWT_SECRET is not configured');
|
||||
}
|
||||
|
||||
let decodedSession: { licenses?: Record<string, string>; tenantName?: string };
|
||||
try {
|
||||
decodedSession = jwt.verify(sessionToken, jwtSecret) as { licenses?: Record<string, string>; tenantName?: string };
|
||||
} catch (_e) {
|
||||
const res = NextResponse.json({ error: 'Unauthorized: Invalid session token' }, { status: 401 });
|
||||
return applyCorsHeaders(res, req);
|
||||
}
|
||||
|
||||
// 3. Ekstrak hak akses (licenses) dari Token Session
|
||||
// Format licenses: { "chat": "GRANTED", "vc": "GRANTED", "recording": "GRANTED", "pulsar_codec": "GRANTED" }
|
||||
const licenses = decodedSession.licenses || {};
|
||||
const tenantName = decodedSession.tenantName || 'UNKNOWN_TENANT';
|
||||
|
||||
// 4. Translasi fitur IAM ke Module ID XCU Core (Ala Carte Modules)
|
||||
const allowedModules: number[] = [];
|
||||
|
||||
// Pengecualian Khusus VIP TELAH DIHAPUS (No more hardcoded bypass)
|
||||
|
||||
// Auto-Pilot & JVC Package Logic
|
||||
if (licenses['pulsar_codec'] === 'GRANTED' || licenses['recording'] === 'GRANTED' || licenses['JVC'] === 'GRANTED') {
|
||||
allowedModules.push(43); // Modul 43: PulsarCodec / Recording / Hot-Swap HD
|
||||
}
|
||||
if (licenses['crdt_chat'] === 'GRANTED' || licenses['x_ray_log'] === 'GRANTED' || licenses['JC'] === 'GRANTED' || licenses['JVC'] === 'GRANTED') {
|
||||
allowedModules.push(72); // Modul 72: Neural CRDT Chat / X-Ray Diagnostic
|
||||
}
|
||||
if (licenses['ebpf_shield'] === 'GRANTED' || licenses['JVC'] === 'GRANTED') {
|
||||
allowedModules.push(88); // Modul 88: The eBPF Shield (Ring-0 DDoS)
|
||||
}
|
||||
if (licenses['ouroboros_sla'] === 'GRANTED' || licenses['JVC'] === 'GRANTED') {
|
||||
allowedModules.push(99); // Modul 99: Ouroboros Automation (Self-Healing SLA)
|
||||
}
|
||||
|
||||
// 5. Generate The Quantum Entitlement Token (JWS murni)
|
||||
// Ditandatangani menggunakan kunci simetris yang hanya diketahui oleh IAM dan XCU Core
|
||||
const quantumSecret = process.env.XCU_TOKEN_SECRET || "UltR4S3cr3T_XCU_Key_2026!"; // Sinkron dengan Rust xcu-core
|
||||
|
||||
const quantumToken = jwt.sign(
|
||||
{
|
||||
tenant: tenantName,
|
||||
allowed_modules: allowedModules,
|
||||
},
|
||||
quantumSecret,
|
||||
{ expiresIn: '8h' }
|
||||
);
|
||||
|
||||
const res = NextResponse.json({
|
||||
success: true,
|
||||
token: quantumToken,
|
||||
modules: allowedModules,
|
||||
}, { status: 200 });
|
||||
|
||||
return applyCorsHeaders(res, req);
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error('[QUANTUM TOKEN ERROR]', error);
|
||||
const res = NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
return applyCorsHeaders(res, req);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user