175 lines
6.6 KiB
TypeScript
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 });
|
|
}
|
|
}
|