[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 });
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,113 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-brand: #00a884;
|
||||
--color-brand-glow: rgba(0, 168, 132, 0.4);
|
||||
--font-sans: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* LIGHT MODE (WhatsApp Web Light) */
|
||||
--background: #f0f2f5;
|
||||
--foreground: #111b21;
|
||||
--header-bg: #f0f2f5;
|
||||
--sidebar-bg: #ffffff;
|
||||
--chat-list-bg: #ffffff;
|
||||
--chat-active: #f0f2f5;
|
||||
--chat-hover: #f5f6f6;
|
||||
--chat-window-bg: #efeae2;
|
||||
--bubble-me: #d9fdd3;
|
||||
--bubble-other: #ffffff;
|
||||
--bubble-text: #111b21;
|
||||
--input-bg: #ffffff;
|
||||
--panel-bg: #ffffff;
|
||||
--panel-border: #e9edef;
|
||||
--glass-bg: rgba(255, 255, 255, 0.85);
|
||||
--glass-border: rgba(0, 0, 0, 0.08);
|
||||
--wa-bg-opacity: 0.06;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* DARK MODE (WhatsApp Web Dark) */
|
||||
--background: #0b141a;
|
||||
--foreground: #e9edef;
|
||||
--header-bg: #202c33;
|
||||
--sidebar-bg: #111b21;
|
||||
--chat-list-bg: #111b21;
|
||||
--chat-active: #2a3942;
|
||||
--chat-hover: #202c33;
|
||||
--chat-window-bg: #0b141a;
|
||||
--bubble-me: #005c4b;
|
||||
--bubble-other: #202c33;
|
||||
--bubble-text: #e9edef;
|
||||
--input-bg: #2a3942;
|
||||
--panel-bg: #202c33;
|
||||
--panel-border: #222d34;
|
||||
--glass-bg: rgba(32, 44, 51, 0.85);
|
||||
--glass-border: rgba(255, 255, 255, 0.08);
|
||||
--wa-bg-opacity: 0.06;
|
||||
}
|
||||
|
||||
/* HarmonyOS, Samsung, Safari & Global Browser Parity Optimization */
|
||||
html, body {
|
||||
height: 100%;
|
||||
min-height: -webkit-fill-available;
|
||||
overflow-x: hidden;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-moz-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
scroll-behavior: smooth;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-sans);
|
||||
margin: 0;
|
||||
padding: env(safe-area-inset-top) 0 env(safe-area-inset-bottom) 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
touch-action: manipulation;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
/* Touch-Optimized Interactions */
|
||||
button, a, input, select, textarea {
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar for Premium Feel - Cross Browser */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(134, 150, 160, 0.2); border-radius: 10px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(134, 150, 160, 0.3); }
|
||||
|
||||
/* Firefox Scrollbar */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(134, 150, 160, 0.2) transparent;
|
||||
}
|
||||
|
||||
/* Android/Huawei/Samsung Input Reset */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: var(--foreground) !important;
|
||||
-webkit-box-shadow: 0 0 0px 1000px var(--background) inset !important;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation: fadeIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { io, Socket } from "../../../lib/zero-socket";
|
||||
|
||||
export default function GuestPortal() {
|
||||
const params = useParams();
|
||||
const room = params.room as string;
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [pin, setPin] = useState("");
|
||||
const [status, setStatus] = useState<"idle" | "knocking" | "approved" | "denied">("idle");
|
||||
const [socket, setSocket] = useState<Socket | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const newSocket = io(window.location.origin, {
|
||||
path: '/c/socket.io',
|
||||
transports: ['websocket']
|
||||
});
|
||||
setSocket(newSocket);
|
||||
|
||||
newSocket.on("guest_approved", () => {
|
||||
setStatus("approved");
|
||||
// Set a temporary cookie or local storage to bypass auth in the Chat UI
|
||||
// In this specific implementation, we will just redirect to the room with a temporary token
|
||||
// For simplicity, we just change the state to 'approved' and render the chat UI here or redirect
|
||||
window.location.href = `/?guest_room=${encodeURIComponent(room)}&guest_name=${encodeURIComponent(name)}&guest_token=true`;
|
||||
});
|
||||
|
||||
newSocket.on("guest_denied", () => {
|
||||
setStatus("denied");
|
||||
newSocket.disconnect();
|
||||
});
|
||||
|
||||
return () => { newSocket.disconnect(); };
|
||||
}, [room, name]);
|
||||
|
||||
const handleKnock = async () => {
|
||||
if (!name.trim() || !pin.trim()) return alert("Nama dan PIN wajib diisi!");
|
||||
if (!socket) return;
|
||||
|
||||
// Validasi PIN ke API sebelum mengetuk
|
||||
try {
|
||||
setStatus("knocking");
|
||||
const resp = await fetch('/c/api/guest/knock', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ room, pin, name })
|
||||
});
|
||||
const data = await resp.json();
|
||||
|
||||
if (data.success) {
|
||||
// PIN Valid, sekarang server menembakkan notifikasi ke Host
|
||||
socket.emit('guest_knock', { room, name, pinId: data.pinId });
|
||||
} else {
|
||||
alert(data.error || "PIN tidak valid atau sudah kedaluwarsa.");
|
||||
setStatus("idle");
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Terjadi kesalahan.");
|
||||
setStatus("idle");
|
||||
}
|
||||
};
|
||||
|
||||
if (status === "approved") {
|
||||
return <div className="min-h-screen bg-[#050b14] flex items-center justify-center text-white">Memasuki Ruangan...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#050b14] flex items-center justify-center p-4 relative overflow-hidden">
|
||||
{/* Background Decorators */}
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-brand rounded-full mix-blend-screen filter blur-[200px] opacity-10 animate-pulse"></div>
|
||||
|
||||
<div className="w-full max-w-md bg-[#111b21]/80 backdrop-blur-xl border border-gray-700/50 p-8 rounded-3xl shadow-2xl z-10">
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<div className="w-16 h-16 bg-brand/20 rounded-full flex items-center justify-center mb-4 border border-brand/30">
|
||||
<svg className="w-8 h-8 text-brand" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path></svg>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white mb-2">Akses Tamu (Guest)</h1>
|
||||
<p className="text-gray-400 text-center text-sm">
|
||||
Anda diundang ke ruangan: <br/>
|
||||
<strong className="text-brand block mt-1 text-lg">{decodeURIComponent(room)}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{status === "idle" && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-gray-400 text-xs font-bold mb-2 uppercase tracking-wider">Nama Lengkap / Instansi</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Contoh: Budi (Kemenkes)"
|
||||
className="w-full bg-[#202c33] text-white px-4 py-3 rounded-xl outline-none focus:ring-2 focus:ring-brand border border-transparent transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-gray-400 text-xs font-bold mb-2 uppercase tracking-wider">PIN Masuk (6-Digit)</label>
|
||||
<input
|
||||
type="text"
|
||||
maxLength={6}
|
||||
value={pin}
|
||||
onChange={(e) => setPin(e.target.value.replace(/\D/g, ''))}
|
||||
placeholder="000000"
|
||||
className="w-full bg-[#202c33] text-white px-4 py-3 rounded-xl outline-none focus:ring-2 focus:ring-brand border border-transparent transition-all text-center tracking-[0.5em] font-mono text-xl"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleKnock}
|
||||
disabled={!name || pin.length !== 6}
|
||||
className="w-full mt-6 py-4 bg-brand hover:bg-opacity-90 text-black font-extrabold rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-[0_0_20px_rgba(0,255,136,0.2)] hover:shadow-[0_0_30px_rgba(0,255,136,0.4)]">
|
||||
Ketuk Pintu (Minta Akses)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === "knocking" && (
|
||||
<div className="flex flex-col items-center py-8 text-center">
|
||||
<div className="w-16 h-16 border-4 border-gray-700 border-t-[var(--color-brand)] rounded-full animate-spin mb-6"></div>
|
||||
<h3 className="text-xl font-bold text-white mb-2">Menunggu Tuan Rumah...</h3>
|
||||
<p className="text-gray-400">PIN Anda valid. Host sedang meninjau permintaan Anda untuk masuk ke ruangan.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === "denied" && (
|
||||
<div className="flex flex-col items-center py-8 text-center">
|
||||
<div className="w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center mb-6 border border-red-500/50">
|
||||
<svg className="w-8 h-8 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-red-500 mb-2">Akses Ditolak</h3>
|
||||
<p className="text-gray-400">Tuan rumah menolak permintaan Anda untuk masuk ke ruangan ini.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import type { Viewport } from "next";
|
||||
import crypto from "crypto";
|
||||
import { OmniSyncProvider } from "../components/OmniSyncProvider";
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: "#0b141a",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "JUMPA.ID | Enterprise Real-Time Chat",
|
||||
description: "Secure WhatsApp-Native Chat Platform",
|
||||
manifest: "/manifest.json",
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: "black-translucent",
|
||||
title: "JUMPA Chat",
|
||||
},
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
|
||||
// TSM Versioning Format: [TSM.ID].hh.mm.ss.DD.MM.YYYY.XXXX
|
||||
const date = new Date();
|
||||
const format2 = (n: number) => n.toString().padStart(2, '0');
|
||||
const hh = format2(date.getUTCHours());
|
||||
const mm = format2(date.getUTCMinutes());
|
||||
const ss = format2(date.getUTCSeconds());
|
||||
const DD = format2(date.getUTCDate());
|
||||
const MM = format2(date.getUTCMonth() + 1);
|
||||
const YYYY = date.getUTCFullYear();
|
||||
const XXXX = crypto.randomBytes(2).toString('hex').toUpperCase();
|
||||
const tsmVersion = `[TSM.ID].${hh}.${mm}.${ss}.${DD}.${MM}.${YYYY}.${XXXX}`;
|
||||
|
||||
return (
|
||||
<html lang="id" data-theme="dark">
|
||||
<head>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" href="/icon512_maskable.png" />
|
||||
<script dangerouslySetInnerHTML={{__html: `
|
||||
// PKX NUCLEAR CACHE BUSTER: Unregister all Service Workers & clear all caches
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations().then(function(registrations) {
|
||||
for (var i = 0; i < registrations.length; i++) {
|
||||
registrations[i].unregister();
|
||||
}
|
||||
});
|
||||
}
|
||||
if ('caches' in window) {
|
||||
caches.keys().then(function(names) {
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
caches.delete(names[i]);
|
||||
}
|
||||
});
|
||||
}
|
||||
`}} />
|
||||
</head>
|
||||
<body className="antialiased font-sans transition-colors duration-500">
|
||||
<OmniSyncProvider initialLocale="id">
|
||||
{children}
|
||||
{/* TSM PERMANENT WATERMARK */}
|
||||
<div className="fixed bottom-1 right-1 z-[9999] opacity-30 pointer-events-none select-none">
|
||||
<span className="text-[8px] font-mono text-white tracking-widest drop-shadow-[0_0_5px_rgba(255,255,255,0.8)]">
|
||||
{tsmVersion}
|
||||
</span>
|
||||
</div>
|
||||
</OmniSyncProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
// @ts-nocheck
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { io, Socket } from "../lib/zero-socket";
|
||||
import CryptoJS from "crypto-js";
|
||||
import { useOmni } from "../components/OmniSyncProvider";
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
client_id?: string;
|
||||
sender: string;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
status: 'sent' | 'delivered' | 'read' | 'deleted';
|
||||
isEdited?: boolean;
|
||||
type?: 'text' | 'image' | 'video' | 'file' | 'audio';
|
||||
}
|
||||
|
||||
interface Chat {
|
||||
id: string;
|
||||
name: string;
|
||||
lastMessage?: string;
|
||||
timestamp?: string;
|
||||
unreadCount?: number;
|
||||
isGroup?: boolean;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
export default function JumpaChat() {
|
||||
const { theme, setTheme, locale, setLocale } = useOmni();
|
||||
|
||||
const [socket, setSocket] = useState<Socket | null>(null);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [chats, setChats] = useState<Chat[]>([]);
|
||||
const [activeChat, setActiveChat] = useState<Chat | null>(null);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isJoined, setIsJoined] = useState(false);
|
||||
|
||||
// BYOK & Security
|
||||
const [quantumToken, setQuantumToken] = useState<string | null>(null);
|
||||
const [byokKey, setByokKey] = useState<string>("xcu_default_vault_2026");
|
||||
const [byokLevel, setByokLevel] = useState<string>("SYSTEM");
|
||||
const [allowedModules, setAllowedModules] = useState<string[]>([]);
|
||||
const [uiMatrix, setUiMatrix] = useState<Record<string, any> | null>(null);
|
||||
|
||||
// Call State
|
||||
const [isVideoCallActive, setIsVideoCallActive] = useState(false);
|
||||
const [incomingCall, setIncomingCall] = useState<{ caller: string, room: string } | null>(null);
|
||||
|
||||
// UI States
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 1. Fetch Quantum Identity & BYOK
|
||||
useEffect(() => {
|
||||
const initIdentity = async () => {
|
||||
try {
|
||||
const iamBaseUrl = process.env.NEXT_PUBLIC_IAM_URL || 'http://forge.ultramodul.xyz';
|
||||
const [meResp, tokenResp, usersResp] = await Promise.all([
|
||||
fetch("/c/api/auth/me"),
|
||||
fetch(`${iamBaseUrl}/api/auth/quantum_token`, { credentials: 'include' }),
|
||||
fetch("/c/api/users")
|
||||
]);
|
||||
|
||||
const meData = await meResp.json();
|
||||
const tokenData = await tokenResp.json();
|
||||
|
||||
if (meData.error) {
|
||||
window.location.href = "/dashboard";
|
||||
return;
|
||||
}
|
||||
|
||||
setUsername(meData.email);
|
||||
setQuantumToken(tokenData.token);
|
||||
setAllowedModules(tokenData.modules || []);
|
||||
if (tokenData.ui) setUiMatrix(tokenData.ui);
|
||||
if (tokenData.byokActive && tokenData.byok !== 'none') {
|
||||
setByokKey(tokenData.byok);
|
||||
setByokLevel(tokenData.byokLevel);
|
||||
}
|
||||
|
||||
// Map Real Postgres Users to Chats
|
||||
if (usersResp.ok) {
|
||||
const usersData = await usersResp.json();
|
||||
if (usersData.users) {
|
||||
const mappedChats = usersData.users.map((u: any) => ({
|
||||
id: u.email,
|
||||
name: u.email.split('@')[0],
|
||||
lastMessage: "No recent messages",
|
||||
timestamp: "",
|
||||
unreadCount: 0,
|
||||
isGroup: false
|
||||
}));
|
||||
|
||||
// Add Global Omni Room if cross-group is allowed
|
||||
if (meData.allowCrossGroup) {
|
||||
mappedChats.unshift({
|
||||
id: "GENERAL-HQ", name: "JUMPA General HQ", lastMessage: "Omni-Tenant Broadcast Active", timestamp: "", unreadCount: 0, isGroup: true
|
||||
});
|
||||
}
|
||||
setChats(mappedChats);
|
||||
}
|
||||
}
|
||||
|
||||
setIsJoined(true);
|
||||
} catch (e) {
|
||||
window.location.href = "/dashboard";
|
||||
}
|
||||
};
|
||||
initIdentity();
|
||||
}, []);
|
||||
|
||||
// 2. Socket Orchestration
|
||||
useEffect(() => {
|
||||
if (isJoined && username) {
|
||||
const newSocket = io(window.location.origin, {
|
||||
path: '/c/socket.io',
|
||||
transports: ['websocket']
|
||||
});
|
||||
setSocket(newSocket);
|
||||
|
||||
newSocket.on("connect", () => {
|
||||
setIsConnected(true);
|
||||
newSocket.emit("register_user", username);
|
||||
});
|
||||
|
||||
newSocket.on("new_message", (msg: Message) => {
|
||||
// Decrypt message if it's encrypted
|
||||
try {
|
||||
if (msg.content.startsWith("XCU|")) {
|
||||
const encrypted = msg.content.substring(4);
|
||||
const bytes = CryptoJS.AES.decrypt(encrypted, byokKey);
|
||||
msg.content = bytes.toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
} catch (e) {}
|
||||
setMessages(prev => [...prev, msg]);
|
||||
// Update chat list last message
|
||||
setChats(prev => prev.map(c => c.id === msg.room ? { ...c, lastMessage: msg.content, timestamp: 'Now' } : c));
|
||||
});
|
||||
|
||||
newSocket.on("incoming_call", (data: { caller: string, room: string }) => {
|
||||
if (data.caller !== username) setIncomingCall(data);
|
||||
});
|
||||
|
||||
return () => { newSocket.disconnect(); };
|
||||
}
|
||||
}, [isJoined, username, byokKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (socket && isConnected && activeChat && username) {
|
||||
socket.emit("join_chat", { username, room: activeChat.id });
|
||||
}
|
||||
}, [activeChat, socket, isConnected, username]);
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [messages]);
|
||||
|
||||
// 3. WhatsApp-Native Actions
|
||||
const handleSend = (e?: React.FormEvent) => {
|
||||
if (e) e.preventDefault();
|
||||
if (!inputValue.trim() || !socket || !activeChat) return;
|
||||
|
||||
// BYOK Encryption
|
||||
const encrypted = "XCU|" + CryptoJS.AES.encrypt(inputValue.trim(), byokKey).toString();
|
||||
|
||||
const newMsg: Message = {
|
||||
id: 'temp-' + Date.now(),
|
||||
sender: username,
|
||||
content: inputValue.trim(), // Local show unencrypted
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'sent',
|
||||
type: 'text'
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, newMsg]);
|
||||
socket.emit("send_message", { ...newMsg, content: encrypted, room: activeChat.id });
|
||||
setInputValue("");
|
||||
};
|
||||
|
||||
const handleStartCall = (audioOnly = false) => {
|
||||
if (!activeChat) return;
|
||||
setIsVideoCallActive(true);
|
||||
if (socket) {
|
||||
socket.emit("initiate_call", { caller: username, room: activeChat.id, isAudioOnly: audioOnly });
|
||||
}
|
||||
};
|
||||
|
||||
const canSendMessage = uiMatrix?.['jc.ui.send_message'] !== false && allowedModules.includes('CHAT_SEND_MESSAGE');
|
||||
const canAttach = uiMatrix?.['jc.ui.file_upload'] !== false && allowedModules.includes('CHAT_ATTACH_FILE');
|
||||
const canVideoCall = uiMatrix?.['jc.ui.video_call'] !== false && allowedModules.includes('CHAT_VIDEO_CALL');
|
||||
const canVoiceCall = uiMatrix?.['jc.ui.voice_call'] !== false && allowedModules.includes('CHAT_VOICE_CALL');
|
||||
const canReactions = uiMatrix?.['jc.ui.reactions'] !== false;
|
||||
const canVoiceNotes = uiMatrix?.['jc.ui.voice_notes'] !== false;
|
||||
|
||||
// WhatsApp-Native Bubble Logic
|
||||
const renderMessage = (msg: Message, i: number) => {
|
||||
const isMe = msg.sender === username;
|
||||
const isFirstInGroup = i === 0 || messages[i-1].sender !== msg.sender;
|
||||
|
||||
return (
|
||||
<div key={msg.id} className={`flex ${isMe ? 'justify-end' : 'justify-start'} mb-[2px] ${isFirstInGroup ? 'mt-4' : ''} animate-in fade-in slide-in-from-bottom-1 px-4 md:px-12`}>
|
||||
<div className={`relative max-w-[85%] md:max-w-[65%] px-4 py-3 rounded-2xl shadow-lg border ${isMe ? 'bg-emerald-600/90 border-emerald-500 text-black font-medium rounded-tr-none' : 'bg-black/60 border-white/10 text-white/90 rounded-tl-none backdrop-blur-xl'}`}>
|
||||
|
||||
{!isMe && isFirstInGroup && <div className="text-[11px] font-black text-emerald-400 mb-1 uppercase tracking-wider flex items-center gap-2"><svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 11c0 3.517-1.009 6.799-2.753 9.571m-3.44-2.04l.054-.09A13.916 13.916 0 008 11a4 4 0 118 0c0 1.017-.07 2.019-.203 3m-2.118 6.844A21.88 21.88 0 0015.171 17m3.839 1.132c.645-2.266.99-4.659.99-7.132A8 8 0 008 4.07M3 15.364c.64-1.319 1-2.8 1-4.364 0-1.457.39-2.823 1.07-4"></path></svg>{msg.sender.split('@')[0]}</div>}
|
||||
|
||||
<div className="flex items-center gap-2 mb-1.5 opacity-60">
|
||||
<span className="px-1.5 py-0.5 rounded text-[7px] font-black uppercase tracking-[0.2em] bg-black/30 border border-black/20">BYOK XChaCha20</span>
|
||||
</div>
|
||||
|
||||
<div className="text-[14px] leading-relaxed break-words whitespace-pre-wrap pr-14 pb-1 font-mono tracking-tight">
|
||||
{msg.content}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-2 right-3 flex items-center gap-1.5">
|
||||
<span className={`text-[9px] font-mono font-bold ${isMe ? 'text-black/60' : 'text-white/40'}`}>{new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false })}</span>
|
||||
{isMe && (
|
||||
<div className="flex -space-x-1">
|
||||
<svg className={`w-3.5 h-3.5 ${msg.status === 'read' ? 'text-black' : 'text-black/40'}`} fill="currentColor" viewBox="0 0 24 24"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-black text-slate-200 font-sans selection:bg-emerald-500/30 overflow-hidden relative">
|
||||
|
||||
{/* Background Quantum */}
|
||||
<div className="absolute inset-0 z-0 opacity-20 pointer-events-none bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-emerald-900/40 via-black to-black"></div>
|
||||
|
||||
{/* Sidebar - Supreme Matrix Chat List */}
|
||||
<aside className={`w-full md:w-[400px] border-r border-white/10 flex flex-col z-20 transition-all duration-500 bg-black/40 backdrop-blur-3xl ${activeChat ? 'hidden md:flex' : 'flex'}`}>
|
||||
{/* Profile Header */}
|
||||
<header className="h-20 bg-black/20 px-6 flex items-center justify-between shrink-0 border-b border-white/5">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-emerald-600 flex items-center justify-center text-black font-black text-sm shadow-inner border border-emerald-500/20">
|
||||
{username.substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold truncate max-w-[120px]">{username.split('@')[0]}</span>
|
||||
<span className="text-[9px] text-emerald-500 font-black uppercase tracking-widest">{byokLevel} CRYPTO</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-[#aebac1]">
|
||||
<button onClick={() => setShowSettings(!showSettings)} className={`transition-all ${showSettings ? 'text-emerald-500 rotate-90' : 'hover:text-white'}`} title="Pengaturan">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
|
||||
</button>
|
||||
<button className="hover:text-white transition-colors">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Settings Popover - Matrix Command */}
|
||||
{showSettings && (
|
||||
<div className="p-5 bg-black/80 backdrop-blur-3xl border-b border-white/10 animate-in fade-in slide-in-from-top-2 duration-300 shadow-[0_10px_30px_rgba(16,185,129,0.1)]">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<svg className="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path></svg>
|
||||
<span className="text-[10px] font-black text-emerald-500 uppercase tracking-[0.2em]">XCO Matrix Command</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-[10px] font-black text-[#8696a0] uppercase tracking-widest mb-2 block">Tema</label>
|
||||
<div className="flex bg-[var(--sidebar-bg)] rounded-xl p-1 border border-[var(--panel-border)]">
|
||||
<button
|
||||
onClick={() => setTheme('dark')}
|
||||
className={`flex-1 py-1.5 rounded-lg text-[10px] font-bold uppercase transition-all ${theme === 'dark' ? 'bg-[#00a884] text-black shadow-lg' : 'text-[#8696a0] hover:text-white'}`}
|
||||
>
|
||||
Gelap
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme('light')}
|
||||
className={`flex-1 py-1.5 rounded-lg text-[10px] font-bold uppercase transition-all ${theme === 'light' ? 'bg-[#00a884] text-black shadow-lg' : 'text-[#8696a0] hover:text-[#111b21]'}`}
|
||||
>
|
||||
Terang
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-black text-[#8696a0] uppercase tracking-widest mb-2 block">Bahasa</label>
|
||||
<div className="flex bg-[var(--sidebar-bg)] rounded-xl p-1 border border-[var(--panel-border)]">
|
||||
<button
|
||||
onClick={() => setLocale('id')}
|
||||
className={`flex-1 py-1.5 rounded-lg text-[10px] font-bold uppercase transition-all ${locale === 'id' ? 'bg-[#00a884] text-black shadow-lg' : 'text-[#8696a0] hover:text-white'}`}
|
||||
>
|
||||
ID
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLocale('en')}
|
||||
className={`flex-1 py-1.5 rounded-lg text-[10px] font-bold uppercase transition-all ${locale === 'en' ? 'bg-[#00a884] text-black shadow-lg' : 'text-[#8696a0] hover:text-white'}`}
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mb-4 mt-6 border-t border-white/10 pt-6">
|
||||
<svg className="w-4 h-4 text-fuchsia-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>
|
||||
<span className="text-[10px] font-black text-fuchsia-500 uppercase tracking-[0.2em]">BYOK Matrix (Tenant)</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex items-center justify-between bg-white/5 p-3 rounded-xl border border-white/5 hover:border-emerald-500/30 transition-all cursor-pointer group">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-black uppercase text-emerald-400">Post-Quantum Shield</span>
|
||||
<span className="text-[8px] text-gray-500 font-mono tracking-wider">Kyber-1024 Handshake</span>
|
||||
</div>
|
||||
<div className="w-8 h-4 bg-emerald-500 rounded-full relative shadow-[0_0_10px_#10b981]"><div className="absolute right-0.5 top-0.5 w-3 h-3 bg-black rounded-full"></div></div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between bg-white/5 p-3 rounded-xl border border-white/5 hover:border-amber-500/30 transition-all cursor-pointer group opacity-50 cursor-not-allowed">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-black uppercase text-amber-400">Aegis Watermark</span>
|
||||
<span className="text-[8px] text-gray-500 font-mono tracking-wider">Forensic Injector</span>
|
||||
</div>
|
||||
<div className="w-8 h-4 bg-gray-600 rounded-full relative"><div className="absolute left-0.5 top-0.5 w-3 h-3 bg-black rounded-full"></div></div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between bg-white/5 p-3 rounded-xl border border-white/5 hover:border-fuchsia-500/30 transition-all cursor-pointer group">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-black uppercase text-fuchsia-400">Neural Whisper</span>
|
||||
<span className="text-[8px] text-gray-500 font-mono tracking-wider">AI Voice Cleanup</span>
|
||||
</div>
|
||||
<div className="w-8 h-4 bg-fuchsia-500 rounded-full relative shadow-[0_0_10px_#d946ef]"><div className="absolute right-0.5 top-0.5 w-3 h-3 bg-black rounded-full"></div></div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between bg-white/5 p-3 rounded-xl border border-white/5 hover:border-blue-500/30 transition-all cursor-pointer group">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-black uppercase text-blue-400">Doppler Matrix</span>
|
||||
<span className="text-[8px] text-gray-500 font-mono tracking-wider">Ultrasonic Comm</span>
|
||||
</div>
|
||||
<div className="w-8 h-4 bg-blue-500 rounded-full relative shadow-[0_0_10px_#3b82f6]"><div className="absolute right-0.5 top-0.5 w-3 h-3 bg-black rounded-full"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search & Filter */}
|
||||
<div className="p-4 shrink-0 border-b border-white/5 bg-black/20">
|
||||
<div className="relative group">
|
||||
<input
|
||||
type="text" placeholder="Cari transmisi..."
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-12 py-3 text-xs text-white placeholder-white/40 focus:outline-none focus:border-emerald-500/50 focus:bg-white/10 transition-all shadow-inner"
|
||||
/>
|
||||
<svg className="w-4 h-4 absolute left-4 top-3 text-white/40 group-focus-within:text-emerald-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat List */}
|
||||
<div className="flex-1 overflow-y-auto custom-scroll bg-[var(--chat-list-bg)]">
|
||||
{chats.map((chat) => (
|
||||
<button
|
||||
key={chat.id}
|
||||
onClick={() => setActiveChat(chat)}
|
||||
className={`w-full flex items-center gap-3 px-3 py-0 transition-colors ${activeChat?.id === chat.id ? 'bg-[var(--chat-active)]' : 'hover:bg-[var(--chat-hover)]'}`}
|
||||
>
|
||||
<div className={`w-12 h-12 rounded-full shrink-0 flex items-center justify-center font-black text-xl shadow-lg ${chat.isGroup ? 'bg-[#005c4b] text-[#e9edef]' : 'bg-[#6a7175] text-[#e9edef]'}`}>
|
||||
{chat.name.substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 border-b border-[var(--panel-border)] pb-3 pt-4 text-left">
|
||||
<div className="flex justify-between items-center mb-0.5">
|
||||
<span className="font-medium text-[var(--foreground)] truncate text-[16px]">{chat.name}</span>
|
||||
<span className={`text-[11px] ${chat.unreadCount ? 'text-[#00a884] font-black' : 'text-[#8696a0]'}`}>{chat.timestamp}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1 text-[#8696a0] flex-1 truncate">
|
||||
{chat.isGroup && <span className="text-[12px] font-bold text-[#53bdeb] shrink-0">Admin:</span>}
|
||||
<p className="text-[13px] truncate">{chat.lastMessage}</p>
|
||||
</div>
|
||||
{chat.unreadCount > 0 && (
|
||||
<span className="w-5 h-5 bg-[#00a884] rounded-full flex items-center justify-center text-black text-[10px] font-black ml-2 shadow-lg shadow-[#00a884]/20">{chat.unreadCount}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Chat Area - Supreme Matrix UI */}
|
||||
<main className={`flex-1 flex flex-col relative transition-all duration-500 z-10 ${!activeChat ? 'hidden md:flex' : 'flex'}`}>
|
||||
{!activeChat ? (
|
||||
<div className="flex-1 flex flex-col items-center justify-center bg-black/20 border-b-[6px] border-emerald-500/30 relative backdrop-blur-sm">
|
||||
<div className="absolute inset-0 opacity-[0.03] pointer-events-none bg-[url('https://www.transparenttextures.com/patterns/carbon-fibre.png')] bg-repeat" />
|
||||
<div className="w-48 h-48 mb-10 opacity-30 animate-pulse text-emerald-500 filter drop-shadow-[0_0_20px_rgba(16,185,129,0.5)]">
|
||||
<svg className="w-full h-full" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path></svg>
|
||||
</div>
|
||||
<h2 className="text-3xl font-light text-white mb-4 tracking-[0.2em] uppercase">JUMPA XCOM ULTRA</h2>
|
||||
<p className="text-emerald-500/60 text-xs max-w-md text-center leading-relaxed font-bold tracking-widest uppercase">
|
||||
Kanal Transmisi Terenkripsi Kuantum <br />
|
||||
<span className="text-fuchsia-500 font-black mt-2 inline-block">FORGE SECURE</span>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Chat Header */}
|
||||
<header className="h-20 bg-black/60 backdrop-blur-xl px-6 flex items-center justify-between shrink-0 z-30 shadow-md border-b border-white/10">
|
||||
<div className="flex items-center gap-3 cursor-pointer group">
|
||||
<button onClick={(e) => { e.stopPropagation(); setActiveChat(null); }} className="md:hidden p-2 text-[#aebac1] hover:text-white transition-colors">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M15 19l-7-7 7-7"/></svg>
|
||||
</button>
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-black text-lg shadow-inner ${activeChat.isGroup ? 'bg-[#005c4b]' : 'bg-[#6a7175]'}`}>
|
||||
{activeChat.name.substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-[var(--foreground)] font-bold leading-tight truncate text-[15px]">{activeChat.name}</h2>
|
||||
<p className="text-[11px] text-[#00a884] font-medium tracking-tight animate-pulse">Terhubung</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 text-[#aebac1]">
|
||||
{canVideoCall && (
|
||||
<button onClick={() => handleStartCall(false)} className="hover:text-white transition-all hover:scale-110 active:scale-95 p-1.5" title="Panggilan Video">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>
|
||||
</button>
|
||||
)}
|
||||
{canVoiceCall && (
|
||||
<button onClick={() => handleStartCall(true)} className="hover:text-white transition-all hover:scale-110 active:scale-95 p-1.5" title="Panggilan Suara">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.01 15.38c-1.23 0-2.42-.2-3.53-.56-.35-.12-.74-.03-1.01.24l-1.57 1.97c-2.83-1.35-5.48-3.9-6.89-6.83l1.95-1.66c.27-.28.35-.67.24-1.02-.37-1.11-.56-2.3-.56-3.53 0-.54-.45-.99-.99-.99H4.19C3.65 2 3 2.24 3 2.99c0 9.39 7.63 17.01 17.01 17.01.71 0 .99-.65.99-1.19v-2.45c0-.54-.45-.99-.99-.99z"/></svg>
|
||||
</button>
|
||||
)}
|
||||
<button className="hover:text-white transition-colors p-1.5">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Chat Background & Messages */}
|
||||
<div className="flex-1 overflow-y-auto pt-6 pb-6 space-y-1 bg-transparent custom-scroll relative">
|
||||
|
||||
<div className="flex justify-center mb-8 relative z-10">
|
||||
<div className="bg-black/60 backdrop-blur-md px-6 py-2.5 rounded-2xl text-[9px] font-black uppercase tracking-[0.2em] text-emerald-500 shadow-[0_0_20px_rgba(16,185,129,0.1)] border border-emerald-500/20 flex flex-col items-center gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm-2 16l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9l-8 8z"/></svg>
|
||||
XCU MILITARY-GRADE ZERO-KNOWLEDGE SHIELD ENGAGED
|
||||
</div>
|
||||
<span className="text-white/40 text-[8px]">AES-GCM 256-BIT / XCHACHA20-POLY1305</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 px-4 md:px-12">
|
||||
{messages.length === 0 && (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center opacity-30 pt-20">
|
||||
<svg className="w-16 h-16 text-emerald-500 mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>
|
||||
<p className="text-xs font-black uppercase tracking-[0.3em] text-emerald-500">Kanal Transmisi Kosong</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.map((msg, i) => renderMessage(msg, i))}
|
||||
</div>
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Chat Input - Supreme Matrix */}
|
||||
<footer className="bg-black/60 backdrop-blur-2xl px-6 py-4 flex items-end gap-4 z-40 shadow-[0_-10px_30px_rgba(0,0,0,0.5)] border-t border-white/10">
|
||||
<div className="flex items-center gap-2 mb-1 text-white/40">
|
||||
{canReactions && (
|
||||
<button className="p-2.5 hover:text-emerald-400 hover:bg-white/5 rounded-xl transition-colors"><svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z"/></svg></button>
|
||||
)}
|
||||
{canAttach && (
|
||||
<button className="p-2.5 hover:text-emerald-400 hover:bg-white/5 rounded-xl transition-colors rotate-45" title="Attach">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.31 2.69 6 6 6s6-2.69 6-6V6h-1.5z"/></svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 bg-white/5 rounded-2xl px-5 py-3 min-h-[50px] flex items-center shadow-inner border border-white/10 focus-within:border-emerald-500/50 focus-within:bg-white/10 transition-all">
|
||||
<textarea
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (canSendMessage) handleSend(); } }}
|
||||
placeholder={canSendMessage ? "Ketik pesan rahasia..." : "Transmisi diblokir"}
|
||||
disabled={!canSendMessage}
|
||||
className="w-full bg-transparent border-none focus:ring-0 text-white placeholder-white/30 text-[14px] resize-none max-h-48 py-1.5 leading-relaxed font-medium"
|
||||
rows={1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-0.5">
|
||||
{inputValue.trim() ? (
|
||||
<button onClick={() => handleSend()} className="p-3.5 bg-emerald-500 text-black rounded-2xl hover:bg-emerald-400 hover:scale-105 active:scale-95 transition-all shadow-[0_0_20px_rgba(16,185,129,0.3)]">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
|
||||
</button>
|
||||
) : (
|
||||
canVoiceNotes ? (
|
||||
<button className="p-3.5 bg-emerald-600/30 text-emerald-400 border border-emerald-500/50 rounded-2xl hover:bg-emerald-500 hover:text-black hover:scale-105 active:scale-95 transition-all shadow-[0_0_20px_rgba(16,185,129,0.1)]" title="Voice Message">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/><path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>
|
||||
</button>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* CALL OVERLAY - Supreme Ultra Native Experience */}
|
||||
{isVideoCallActive && activeChat && (
|
||||
<div className="fixed inset-0 z-[1000] bg-black animate-in zoom-in-95 duration-500 overflow-hidden flex flex-col">
|
||||
<header className="absolute top-0 left-0 right-0 h-24 px-8 md:px-16 flex items-center justify-between z-20 bg-linear-to-b from-black/80 to-transparent">
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="w-14 h-14 rounded-full bg-emerald-600 flex items-center justify-center text-black font-black text-2xl shadow-[0_0_30px_rgba(16,185,129,0.3)]">
|
||||
{activeChat.name.substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight text-white">{activeChat.name}</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></span>
|
||||
<p className="text-emerald-500 text-[10px] font-black uppercase tracking-[0.3em]">Ultra-Sync Encrypted</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden md:flex items-center gap-3 bg-white/5 backdrop-blur-3xl px-5 py-2.5 rounded-2xl border border-white/10 shadow-2xl">
|
||||
<div className="w-2 h-2 bg-emerald-500 rounded-full"></div>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-white/60">Enkripsi Kedaulatan: {byokLevel}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 relative">
|
||||
<iframe
|
||||
id="vc-iframe"
|
||||
src={`/vc/room/${activeChat.id}?embed=true&username=${encodeURIComponent(username)}&token=${quantumToken}`}
|
||||
className="w-full h-full border-none"
|
||||
allow="camera; microphone; display-capture; autoplay; clipboard-write; encrypted-media"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-12 left-1/2 -translate-x-1/2 flex items-center gap-10 z-20 bg-black/40 backdrop-blur-3xl px-12 py-7 rounded-[40px] border border-white/10 shadow-[0_0_80px_rgba(0,0,0,0.9)]">
|
||||
<button onClick={() => setIsVideoCallActive(false)} className="w-20 h-20 rounded-full bg-red-600 flex items-center justify-center text-white shadow-2xl hover:bg-red-700 hover:scale-110 active:scale-90 transition-all shadow-red-600/20">
|
||||
<svg className="w-10 h-10" fill="currentColor" viewBox="0 0 24 24"><path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08a.994.994 0 010-1.41C4.11 7.91 8.82 5 14 5s9.89 2.91 13.71 6.67c.39.39.39 1.02 0 1.41l-2.48 2.48c-.18.18-.43.29-.71.29s-.53-.11-.7-.29c-.79-.74-1.69-1.36-2.67-1.85-.33-.16-.56-.5-.56-.9v-3.1C15.15 9.25 13.6 9 12 9z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Incoming Call UI - Supreme Radar */}
|
||||
{incomingCall && (
|
||||
<div className="fixed inset-0 z-[2000] bg-black/95 backdrop-blur-[100px] flex flex-col items-center justify-center animate-in fade-in zoom-in duration-500">
|
||||
<div className="relative mb-16">
|
||||
<div className="absolute inset-0 bg-emerald-500 rounded-full blur-[100px] opacity-40 animate-pulse"></div>
|
||||
<div className="absolute inset-[-50px] border border-emerald-500/30 rounded-full animate-[ping_3s_infinite]"></div>
|
||||
<div className="absolute inset-[-100px] border border-emerald-500/10 rounded-full animate-[ping_4s_infinite_1s]"></div>
|
||||
<div className="w-56 h-56 rounded-full bg-emerald-950/80 border-[10px] border-emerald-500/30 flex items-center justify-center text-emerald-400 text-7xl font-black shadow-[0_0_100px_rgba(16,185,129,0.5)] relative z-10 backdrop-blur-xl">
|
||||
{incomingCall.caller.substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-5xl font-black mb-4 tracking-[0.2em] uppercase text-white drop-shadow-[0_0_10px_rgba(255,255,255,0.5)]">{incomingCall.caller.split('@')[0]}</h2>
|
||||
<p className="text-emerald-500 font-black uppercase tracking-[0.5em] animate-pulse mb-24 text-sm bg-emerald-500/10 px-6 py-2 rounded-full border border-emerald-500/30">INCOMING QUANTUM TRANSMISSION</p>
|
||||
|
||||
<div className="flex gap-24 relative z-20">
|
||||
<button onClick={() => setIncomingCall(null)} className="w-24 h-24 rounded-full bg-red-600/80 backdrop-blur-md border border-red-500/50 flex items-center justify-center text-white hover:bg-red-600 hover:scale-110 active:scale-90 transition-all shadow-[0_0_50px_rgba(220,38,38,0.5)]">
|
||||
<svg className="w-12 h-12" fill="currentColor" viewBox="0 0 24 24"><path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08a.994.994 0 010-1.41C4.11 7.91 8.82 5 14 5s9.89 2.91 13.71 6.67c.39.39.39 1.02 0 1.41l-2.48 2.48c-.18.18-.43.29-.71.29s-.53-.11-.7-.29c-.79-.74-1.69-1.36-2.67-1.85-.33-.16-.56-.5-.56-.9v-3.1C15.15 9.25 13.6 9 12 9z"/></svg>
|
||||
</button>
|
||||
<button onClick={() => { setActiveChat({ id: incomingCall.room, name: incomingCall.caller.split('@')[0] }); setIsVideoCallActive(true); setIncomingCall(null); }} className="w-24 h-24 rounded-full bg-emerald-500/80 backdrop-blur-md border border-emerald-400 flex items-center justify-center text-black hover:bg-emerald-400 hover:scale-110 active:scale-90 transition-all shadow-[0_0_50px_rgba(16,185,129,0.8)] animate-bounce">
|
||||
<svg className="w-12 h-12" fill="currentColor" viewBox="0 0 24 24"><path d="M20.01 15.38c-1.23 0-2.42-.2-3.53-.56-.35-.12-.74-.03-1.01.24l-1.57 1.97c-2.83-1.35-5.48-3.9-6.89-6.83l1.95-1.66c.27-.28.35-.67.24-1.02-.37-1.11-.56-2.3-.56-3.53 0-.54-.45-.99-.99-.99H4.19C3.65 2 3 2.24 3 2.99c0 9.39 7.63 17.01 17.01 17.01.71 0 .99-.65.99-1.19v-2.45c0-.54-.45-.99-.99-.99z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style dangerouslySetInnerHTML={{__html: `
|
||||
.custom-scroll::-webkit-scrollbar { width: 6px; }
|
||||
.custom-scroll::-webkit-scrollbar-track { background: transparent; }
|
||||
.custom-scroll::-webkit-scrollbar-thumb { background: rgba(134, 150, 160, 0.2); border-radius: 10px; }
|
||||
.custom-scroll::-webkit-scrollbar-thumb:hover { background: rgba(134, 150, 160, 0.3); }
|
||||
`}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user