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

175 lines
6.6 KiB
TypeScript

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