[TSM.ID].[11031972] PXE : Platform X Ecosystem I [118 Module -LIVE-]
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
export async function GET() {
|
||||
const cookieStore = await cookies();
|
||||
const tokenString = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!tokenString) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
// BARU-S1 FIX: Verify JWT signature instead of blind base64 decode
|
||||
const user = jwt.verify(tokenString, process.env.JWT_SECRET as string) as any;
|
||||
|
||||
return NextResponse.json({
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
tenantId: user.tenantId,
|
||||
tenantName: user.tenantName,
|
||||
licenses: user.licenses || ['chat', 'vc'],
|
||||
allowCrossGroup: user.allowCrossGroup,
|
||||
chatEngineStrategy: user.chatEngineStrategy
|
||||
});
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: 'Invalid Token' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Pool } from 'pg';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgresql://jumpa_admin:JumpaS3cur3%21%40%23@127.0.0.1:5432/jumpadb',
|
||||
});
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
let decoded: any;
|
||||
try {
|
||||
decoded = jwt.verify(token, process.env.JWT_SECRET as string);
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: 'Invalid Token' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { room } = await req.json();
|
||||
if (!room) {
|
||||
return NextResponse.json({ error: 'Room is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Generate 6-digit random PIN dengan Kriptografi Node.js
|
||||
const pin = (crypto.getRandomValues(new Uint32Array(1))[0] % 900000 + 100000).toString();
|
||||
|
||||
// Insert into guest_invites
|
||||
await pool.query(
|
||||
'INSERT INTO guest_invites (room, host_id, pin) VALUES ($1, $2, $3)',
|
||||
[room, decoded.email, pin]
|
||||
);
|
||||
|
||||
return NextResponse.json({ success: true, pin }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error('[API GUEST INVITE ERROR]', error);
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgresql://jumpa_admin:JumpaS3cur3%21%40%23@127.0.0.1:5432/jumpadb',
|
||||
});
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { room, pin, name } = await req.json();
|
||||
|
||||
if (!room || !pin || !name) {
|
||||
return NextResponse.json({ error: 'Data tidak lengkap' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Cari tiket PIN yang belum dipakai dan belum expired (kita anggap belum expired kalau ada)
|
||||
const result = await pool.query(
|
||||
'SELECT id FROM guest_invites WHERE room = $1 AND pin = $2 AND is_used = false',
|
||||
[room, pin]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return NextResponse.json({ error: 'PIN tidak valid atau sudah digunakan.' }, { status: 401 });
|
||||
}
|
||||
|
||||
const pinId = result.rows[0].id;
|
||||
|
||||
// PIN Valid! Kita kembalikan success.
|
||||
// WebSocket di client akan mengirim sinyal 'guest_knock' setelah ini
|
||||
return NextResponse.json({ success: true, pinId }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error('[API GUEST KNOCK ERROR]', error);
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import Redis from "ioredis";
|
||||
import { Pool } from "pg";
|
||||
|
||||
// Klien Redis untuk Publisher
|
||||
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
||||
const redis = new Redis(redisUrl);
|
||||
|
||||
// Koneksi ke PostgreSQL VPS
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgresql://jumpa_admin:JumpaS3cur3%21%40%23@127.0.0.1:5432/jumpadb'
|
||||
});
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
// Tangani logika khusus DB jika event adalah host_approve_guest
|
||||
if (event === "host_approve_guest") {
|
||||
try {
|
||||
await pool.query('UPDATE guest_invites SET is_used = true WHERE id = $1', [payload.pinId]);
|
||||
} catch (e) {
|
||||
console.error('[DB Error] Failed to approve guest:', e);
|
||||
}
|
||||
}
|
||||
|
||||
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: unknown) {
|
||||
const msg = error instanceof Error ? error.message : "Unknown error";
|
||||
console.error("[EMIT] Gagal memancarkan sinyal:", msg);
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
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: any) => `${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,65 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
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) => {
|
||||
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,109 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import crypto from "crypto";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
// 1. Quantum Encryption (AES-256-GCM) - Kriptografi Kelas Militer
|
||||
function encryptBuffer(buffer: Buffer): { encrypted: Buffer; iv: Buffer; tag: Buffer } {
|
||||
// Jika QUANTUM_MASTER_KEY tidak disetel di .env, kita pakai Kunci Fallback Absolut
|
||||
// WAJIB: Panjang kunci harus tepat 32 byte untuk AES-256
|
||||
const rawKey = process.env.QUANTUM_MASTER_KEY || "JUMPA-XCU-ABSOLUTE-SOVEREIGN-KEY";
|
||||
const key = crypto.createHash("sha256").update(rawKey).digest();
|
||||
|
||||
const iv = crypto.randomBytes(12); // GCM Standard IV (12 bytes)
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||
|
||||
const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
|
||||
const tag = cipher.getAuthTag(); // 16 bytes auth tag
|
||||
|
||||
return { encrypted, iv, tag };
|
||||
}
|
||||
|
||||
// 2. Omni-Storage Uploader (Zero-Dependency Cloud Sync)
|
||||
async function syncToOmniCloud(fileName: string, encryptedPayload: Buffer) {
|
||||
const cloudUrl = process.env.OMNI_CLOUD_URL;
|
||||
if (!cloudUrl) return; // Jika tidak ada cloud external, data mutlak 100% lokal Forge
|
||||
|
||||
console.log(`[OMNI-CLOUD] Menyinkronkan ${fileName} ke ujung alam semesta (Cloud External)...`);
|
||||
|
||||
// Signature Universal (Mendukung S3 WebDAV / Bearer Token API)
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": encryptedPayload.length.toString(),
|
||||
};
|
||||
|
||||
if (process.env.OMNI_CLOUD_TOKEN) {
|
||||
headers["Authorization"] = `Bearer ${process.env.OMNI_CLOUD_TOKEN}`;
|
||||
}
|
||||
|
||||
// Native Fetch PUT request (Kompatibel dengan segala jenis object storage yang mendukung REST PUT)
|
||||
// Tidak perlu lagi SDK puluhan MB dari AWS!
|
||||
try {
|
||||
const response = await fetch(`${cloudUrl}/${fileName}`, {
|
||||
method: "PUT",
|
||||
headers,
|
||||
body: new Uint8Array(encryptedPayload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("[OMNI-CLOUD] Gagal sinkronisasi. Kode:", response.status);
|
||||
} else {
|
||||
console.log(`[OMNI-CLOUD] Sukses menyinkronkan ${fileName} ke cloud eksternal dalam wujud enkripsi.`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[OMNI-CLOUD] Jaringan terputus ke cloud eksternal:", err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const formData = await req.formData();
|
||||
const file = formData.get("file") as File;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
|
||||
}
|
||||
|
||||
const originalBuffer = Buffer.from(await file.arrayBuffer());
|
||||
const fileExtension = file.name.split(".").pop() || "bin";
|
||||
const fileName = `${uuidv4()}.${fileExtension}.vault`; // Extensi diubah ke .vault
|
||||
|
||||
console.log(`[QUANTUM VAULT] Memulai enkripsi militer untuk file: ${file.name}`);
|
||||
|
||||
// EKSEKUSI KRIPTOGRAFI MILITER (AES-256-GCM)
|
||||
const { encrypted, iv, tag } = encryptBuffer(originalBuffer);
|
||||
|
||||
// Format Paket Vault: [IV (12 bytes)] + [Auth Tag (16 bytes)] + [Encrypted Data]
|
||||
const vaultPayload = Buffer.concat([iv, tag, encrypted]);
|
||||
|
||||
// PENYIMPANAN 1: DISTRIBUTED OBJECT STORAGE MANDIRI (Lokal Forge)
|
||||
// Otomatis menaruh di direktori absolut VPS
|
||||
const localDir = process.env.OMNI_LOCAL_DIR || "/var/www/omni-storage";
|
||||
await fs.mkdir(localDir, { recursive: true });
|
||||
|
||||
const localFilePath = path.join(localDir, fileName);
|
||||
await fs.writeFile(localFilePath, vaultPayload);
|
||||
console.log(`[QUANTUM VAULT] File diamankan di brankas baja mandiri: ${localFilePath}`);
|
||||
|
||||
// PENYIMPANAN 2: OMNI-CLOUD SYNC (Asinkron agar tidak membuat pengguna menunggu)
|
||||
syncToOmniCloud(fileName, vaultPayload);
|
||||
|
||||
// URL Publik diarahkan ke Decoder Internal JUMPA.ID
|
||||
// Browser memanggil /api/vault/[fileName] untuk mengunduh dan mendekripsi secara on-the-fly
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "http://127.0.0.1:3000";
|
||||
const publicUrl = `${baseUrl}/api/vault/${fileName}`;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
url: publicUrl,
|
||||
fileName: file.name,
|
||||
fileType: "application/quantum-vault",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Quantum Vault error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Pool } from 'pg';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgresql://jumpa_admin:JumpaS3cur3%21%40%23@127.0.0.1:5432/jumpadb',
|
||||
});
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
let decoded: any;
|
||||
try {
|
||||
decoded = jwt.verify(token, process.env.JWT_SECRET as string);
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: 'Invalid Token' }, { status: 401 });
|
||||
}
|
||||
|
||||
const currentUserEmail = decoded.email;
|
||||
const currentTenantId = decoded.tenantId;
|
||||
const allowCrossGroup = decoded.allowCrossGroup === true;
|
||||
|
||||
// ALGORITMA ISOLASI MULTI-TENANT (CLOSED GROUP)
|
||||
let result;
|
||||
if (allowCrossGroup) {
|
||||
// Cross Group Aktif: Bisa lihat sesama tenant + tenant lain yang juga open
|
||||
result = await pool.query(`
|
||||
SELECT u.email, u.role, u.tenant_id
|
||||
FROM users u
|
||||
JOIN tenants t ON u.tenant_id = t.id
|
||||
WHERE u.email != $1 AND (u.tenant_id = $2 OR t.allow_cross_group = true)
|
||||
`, [currentUserEmail, currentTenantId]);
|
||||
} else {
|
||||
// Closed Group Aktif (Default Enterprise): HANYA bisa lihat orang di perusahaan yang sama
|
||||
result = await pool.query(`
|
||||
SELECT email, role, tenant_id
|
||||
FROM users
|
||||
WHERE email != $1 AND tenant_id = $2
|
||||
`, [currentUserEmail, currentTenantId]);
|
||||
}
|
||||
|
||||
return NextResponse.json({ users: result.rows }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error('[API USERS ERROR]', error);
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import crypto from "crypto";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
function decryptBuffer(vaultPayload: Buffer): Buffer {
|
||||
const rawKey = process.env.QUANTUM_MASTER_KEY || "JUMPA-XCU-ABSOLUTE-SOVEREIGN-KEY";
|
||||
const key = crypto.createHash("sha256").update(rawKey).digest();
|
||||
|
||||
// Ekstrak komponen dari paket vault: [IV (12 bytes)] + [Auth Tag (16 bytes)] + [Encrypted Data]
|
||||
const iv = vaultPayload.subarray(0, 12);
|
||||
const tag = vaultPayload.subarray(12, 28);
|
||||
const encrypted = vaultPayload.subarray(28);
|
||||
|
||||
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ filename: string }> }
|
||||
) {
|
||||
try {
|
||||
const { filename } = await params;
|
||||
|
||||
// Mencegah eksploitasi Path Traversal
|
||||
if (filename.includes("..") || filename.includes("/")) {
|
||||
return NextResponse.json({ error: "Invalid filename" }, { status: 400 });
|
||||
}
|
||||
|
||||
const localDir = process.env.OMNI_LOCAL_DIR || "/var/www/omni-storage";
|
||||
const filePath = path.join(localDir, filename);
|
||||
|
||||
// 1. Baca data terenkripsi (Ciphertext) dari brankas baja lokal
|
||||
const vaultPayload = await fs.readFile(filePath);
|
||||
|
||||
// 2. Dekripsi On-The-Fly (Cleartext) untuk disajikan ke pengguna yang berhak
|
||||
const decryptedBuffer = decryptBuffer(vaultPayload);
|
||||
|
||||
// Deteksi tipe konten dari nama file asli (membuang .vault)
|
||||
const originalExt = filename.replace(".vault", "").split(".").pop()?.toLowerCase();
|
||||
|
||||
let contentType = "application/octet-stream";
|
||||
if (originalExt === "jpg" || originalExt === "jpeg") contentType = "image/jpeg";
|
||||
else if (originalExt === "png") contentType = "image/png";
|
||||
else if (originalExt === "gif") contentType = "image/gif";
|
||||
else if (originalExt === "webp") contentType = "image/webp";
|
||||
else if (originalExt === "pdf") contentType = "application/pdf";
|
||||
else if (originalExt === "mp4") contentType = "video/mp4";
|
||||
|
||||
return new NextResponse(new Uint8Array(decryptedBuffer), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
// Caching agresif karena file objek bersifat immutable
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error: unknown) {
|
||||
const msg = error instanceof Error ? error.message : "Unknown error";
|
||||
console.error("[QUANTUM VAULT] Dekripsi gagal atau file hilang:", msg);
|
||||
return NextResponse.json({ error: "Brankas tidak dapat diakses atau kunci enkripsi salah." }, { status: 404 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user