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