bonesmasher commited on
Commit
abc1805
·
verified ·
1 Parent(s): 870f75f

Upload 56 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
DEBUG_READ_RECEIPTS.md ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Read Receipts Debug Steps
2
+
3
+ ### Current Issue
4
+ Read receipts always show single tick (sent) - never upgrade to double tick (delivered/read).
5
+
6
+ ### What We've Found
7
+ 1. Client sends messages ✓
8
+ 2. Client receives messages ✓
9
+ 3. Client emits `message-delivered` and `message-read` ✓
10
+ 4. Server receives these events (should be in logs) ✓
11
+ 5. **Server broadcasts `message-status` but sender doesn't receive it** ❌
12
+
13
+ ### Problem
14
+ The server terminal shows NO Socket.IO logs at all! This means either:
15
+ - Socket.IO isn't starting
16
+ - Logs are being buffered by Bun
17
+
18
+ ### Next Steps
19
+ 1. **Stop the server** (Ctrl+C in terminal)
20
+ 2. **Restart**: `bun dev`
21
+ 3. **Look for**:
22
+ - "✓ Socket.IO server initialized"
23
+ - "NEW CLIENT CONNECTED:" when you open the chat
24
+
25
+ 4. **If NO logs appear:**
26
+ - Socket.IO might not be working
27
+ - Try using `node server.ts` instead of `bun server.ts`
28
+
29
+ 5. **If logs DO appear:**
30
+ - Send a message
31
+ - Look for "Received message-delivered" in server logs
32
+ - Look for "Broadcasted message-status to room" in server logs
33
+ - Check if "Received message-status" appears in SENDER browser console
34
+
35
+ ### Expected Flow
36
+ ```
37
+ Sender -> send-message -> Server
38
+ Server -> receive-message -> Receiver
39
+ Receiver -> message-delivered/read -> Server
40
+ Server -> message-status -> Sender (THIS ISN'T WORKING)
41
+ Sender updates UI with ✓✓
42
+ ```
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM oven/bun:1 AS base
2
+ WORKDIR /app
3
+
4
+ # Install dependencies
5
+ COPY package.json bun.lock* ./
6
+ RUN bun install --frozen-lockfile
7
+
8
+ # Copy source
9
+ COPY . .
10
+
11
+ # Build
12
+ ENV NODE_ENV=production
13
+ RUN bun run build
14
+
15
+ # Run
16
+ ENV AUTH_TRUST_HOST=true
17
+ EXPOSE 8000
18
+ CMD ["bun", "server.ts"]
README.md CHANGED
Binary files a/README.md and b/README.md differ
 
READ_RECEIPTS.md ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Read Receipts Implementation
2
+
3
+ ## Overview
4
+ Read receipts are now fully functional in the chat application. Messages show different status indicators based on their delivery and read state.
5
+
6
+ ## How It Works
7
+
8
+ ### Client Side (ChatRoom.tsx)
9
+ 1. **Sending Messages**: When a user sends a message, it's added locally with status `"sent"`
10
+ 2. **Receiving Messages**: When a message is received:
11
+ - Client immediately emits `message-delivered` event to server
12
+ - After successful decryption and display, client emits `message-read` event
13
+ 3. **Status Updates**: Client listens for `message-status` events and updates message status accordingly
14
+
15
+ ### Server Side (server.ts)
16
+ 1. **message-delivered**: Broadcasts status update to all users in the room
17
+ 2. **message-read**: Broadcasts status update to all users in the room
18
+ 3. Original sender filters updates by checking `originalSenderId === socket.id`
19
+
20
+ ### Status Progression
21
+ - **sent** (single gray check): Message sent to server
22
+ - **delivered** (double gray check): Message received by recipient
23
+ - **read** (double blue check): Message decrypted and displayed by recipient
24
+
25
+ ## Visual Indicators
26
+ - ✓ (gray) = Sent
27
+ - ✓✓ (gray) = Delivered
28
+ - ✓✓ (blue) = Read
29
+
30
+ ## Testing
31
+ 1. Open two browser windows/tabs
32
+ 2. Join the same room from both
33
+ 3. Send a message from Window A
34
+ 4. Watch the status indicator change:
35
+ - Starts as single gray check (sent)
36
+ - Changes to double gray checks (delivered)
37
+ - Changes to double blue checks (read)
38
+
39
+ ## Technical Details
40
+ - Message IDs are used to track status updates
41
+ - Status updates only apply to the original sender
42
+ - Status can only upgrade (sent → delivered → read), never downgrade
43
+ - All communication is end-to-end encrypted
actions/login.ts ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server";
2
+
3
+ import { signIn } from "@/auth";
4
+ import { AuthError } from "next-auth";
5
+ import User from "@/models/User";
6
+ import dbConnect from "@/lib/db";
7
+ import { compare } from "bcryptjs";
8
+
9
+ export const login = async (values: any) => {
10
+ const { email, password, token } = values;
11
+
12
+ try {
13
+ // Step 1: If no token provided, check credentials and 2FA status
14
+ if (!token) {
15
+ await dbConnect();
16
+ const user = await User.findOne({ email });
17
+
18
+ if (!user || !user.password) {
19
+ return { error: "Invalid credentials" };
20
+ }
21
+
22
+ const isPasswordValid = await compare(password, user.password);
23
+
24
+ if (!isPasswordValid) {
25
+ return { error: "Invalid credentials" };
26
+ }
27
+
28
+ // If 2FA is enabled, signal frontend to show 2FA input
29
+ if (user.isTwoFactorEnabled) {
30
+ return { requires2FA: true };
31
+ }
32
+ }
33
+
34
+ // Step 2: Proceed with signIn (either no 2FA, or 2FA token provided)
35
+ const result = await signIn("credentials", {
36
+ email,
37
+ password,
38
+ token,
39
+ redirect: false,
40
+ });
41
+
42
+ if (result?.error) {
43
+ return { error: "Invalid 2FA token" };
44
+ }
45
+
46
+ return { success: true };
47
+ } catch (error) {
48
+ if (error instanceof AuthError) {
49
+ switch (error.type) {
50
+ case "CredentialsSignin":
51
+ return { error: "Invalid 2FA token" };
52
+ default:
53
+ return { error: "Something went wrong" };
54
+ }
55
+ }
56
+ throw error;
57
+ }
58
+ };
actions/register.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server";
2
+
3
+ import { hash } from "bcryptjs";
4
+ import User from "@/models/User";
5
+ import dbConnect from "@/lib/db";
6
+
7
+ export const registerUser = async (formData: FormData) => {
8
+ const email = formData.get("email") as string;
9
+ const password = formData.get("password") as string;
10
+
11
+ if (!email || !password) {
12
+ return { error: "Email and password are required" };
13
+ }
14
+
15
+ await dbConnect();
16
+
17
+ const existingUser = await User.findOne({ email });
18
+
19
+ if (existingUser) {
20
+ return { error: "User already exists" };
21
+ }
22
+
23
+ const hashedPassword = await hash(password, 10);
24
+
25
+ await User.create({
26
+ email,
27
+ password: hashedPassword,
28
+ });
29
+
30
+ return { success: "User created successfully" };
31
+ };
actions/two-factor.ts ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server";
2
+
3
+ import { auth } from "@/auth";
4
+ import User from "@/models/User";
5
+ import dbConnect from "@/lib/db";
6
+ import { generateTwoFactorSecret, verifyTwoFactorToken } from "@/lib/tokens";
7
+
8
+ export const getTwoFactorStatus = async () => {
9
+ const session = await auth();
10
+ if (!session?.user?.email) return { error: "Unauthorized" };
11
+
12
+ await dbConnect();
13
+ const user = await User.findOne({ email: session.user.email });
14
+ return { isEnabled: user?.isTwoFactorEnabled };
15
+ };
16
+
17
+ export const enableTwoFactor = async () => {
18
+ const session = await auth();
19
+ if (!session?.user?.email) return { error: "Unauthorized" };
20
+
21
+ await dbConnect();
22
+ const user = await User.findOne({ email: session.user.email });
23
+
24
+ if (!user) return { error: "User not found" };
25
+
26
+ const { secret, qrCodeUrl } = await generateTwoFactorSecret(user.email);
27
+
28
+ user.twoFactorSecret = secret;
29
+ await user.save();
30
+
31
+ return { secret, qrCodeUrl };
32
+ };
33
+
34
+ export const confirmTwoFactor = async (token: string) => {
35
+ const session = await auth();
36
+ if (!session?.user?.email) return { error: "Unauthorized" };
37
+
38
+ await dbConnect();
39
+ const user = await User.findOne({ email: session.user.email });
40
+
41
+ if (!user || !user.twoFactorSecret) return { error: "User not found or 2FA not initiated" };
42
+
43
+ const isValid = verifyTwoFactorToken(token, user.twoFactorSecret);
44
+
45
+ if (!isValid) return { error: "Invalid token" };
46
+
47
+ user.isTwoFactorEnabled = true;
48
+ await user.save();
49
+
50
+ return { success: true };
51
+ };
52
+
53
+ export const disableTwoFactor = async () => {
54
+ const session = await auth();
55
+ if (!session?.user?.email) return { error: "Unauthorized" };
56
+
57
+ await dbConnect();
58
+ const user = await User.findOne({ email: session.user.email });
59
+
60
+ if (!user) return { error: "User not found" };
61
+
62
+ user.isTwoFactorEnabled = false;
63
+ user.twoFactorSecret = undefined;
64
+ await user.save();
65
+
66
+ return { success: true };
67
+ };
app/(auth)/login/page.tsx ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { login } from "@/actions/login";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Input } from "@/components/ui/input";
8
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
9
+ import Link from "next/link";
10
+
11
+ export default function LoginPage() {
12
+ const [email, setEmail] = useState("");
13
+ const [password, setPassword] = useState("");
14
+ const [token, setToken] = useState("");
15
+ const [showTwoFactor, setShowTwoFactor] = useState(false);
16
+ const [error, setError] = useState("");
17
+ const router = useRouter();
18
+
19
+ const handleSubmit = async (e: React.FormEvent) => {
20
+ e.preventDefault();
21
+ setError("");
22
+
23
+ try {
24
+ const res = await login({
25
+ email,
26
+ password,
27
+ token: showTwoFactor ? token : undefined,
28
+ });
29
+
30
+ if (res?.requires2FA) {
31
+ setShowTwoFactor(true);
32
+ } else if (res?.error) {
33
+ setError(res.error);
34
+ } else {
35
+ router.push("/settings");
36
+ router.refresh();
37
+ }
38
+ } catch (err) {
39
+ setError("Something went wrong");
40
+ }
41
+ };
42
+
43
+ return (
44
+ <div className="flex items-center justify-center min-h-screen bg-slate-950">
45
+ <Card className="w-[350px] bg-slate-900 border-slate-800 text-slate-100">
46
+ <CardHeader>
47
+ <CardTitle>Login</CardTitle>
48
+ </CardHeader>
49
+ <CardContent>
50
+ <form onSubmit={handleSubmit} className="space-y-4">
51
+ {!showTwoFactor && (
52
+ <>
53
+ <div className="space-y-2">
54
+ <label htmlFor="email">Email</label>
55
+ <Input
56
+ id="email"
57
+ type="email"
58
+ value={email}
59
+ onChange={(e) => setEmail(e.target.value)}
60
+ required
61
+ className="bg-slate-950 border-slate-800"
62
+ />
63
+ </div>
64
+ <div className="space-y-2">
65
+ <label htmlFor="password">Password</label>
66
+ <Input
67
+ id="password"
68
+ type="password"
69
+ value={password}
70
+ onChange={(e) => setPassword(e.target.value)}
71
+ required
72
+ className="bg-slate-950 border-slate-800"
73
+ />
74
+ </div>
75
+ </>
76
+ )}
77
+
78
+ {showTwoFactor && (
79
+ <div className="space-y-2">
80
+ <label htmlFor="token">2FA Code</label>
81
+ <Input
82
+ id="token"
83
+ type="text"
84
+ value={token}
85
+ onChange={(e) => setToken(e.target.value)}
86
+ required
87
+ placeholder="123456"
88
+ className="bg-slate-950 border-slate-800"
89
+ />
90
+ </div>
91
+ )}
92
+
93
+ {error && <p className="text-red-500 text-sm">{error}</p>}
94
+
95
+ <Button type="submit" className="w-full bg-emerald-600 hover:bg-emerald-700">
96
+ {showTwoFactor ? "Verify" : "Login"}
97
+ </Button>
98
+ </form>
99
+ <div className="mt-4 text-center text-sm">
100
+ <Link href="/register" className="text-emerald-500 hover:underline">
101
+ Don't have an account? Register
102
+ </Link>
103
+ </div>
104
+ </CardContent>
105
+ </Card>
106
+ </div>
107
+ );
108
+ }
app/(auth)/register/page.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
8
+ import { registerUser } from "@/actions/register";
9
+ import Link from "next/link";
10
+
11
+ export default function RegisterPage() {
12
+ const [error, setError] = useState("");
13
+ const router = useRouter();
14
+
15
+ const handleSubmit = async (formData: FormData) => {
16
+ const res = await registerUser(formData);
17
+
18
+ if (res.error) {
19
+ setError(res.error);
20
+ } else {
21
+ router.push("/login");
22
+ }
23
+ };
24
+
25
+ return (
26
+ <div className="flex items-center justify-center min-h-screen bg-slate-950">
27
+ <Card className="w-[350px] bg-slate-900 border-slate-800 text-slate-100">
28
+ <CardHeader>
29
+ <CardTitle>Create Account</CardTitle>
30
+ </CardHeader>
31
+ <CardContent>
32
+ <form action={handleSubmit} className="space-y-4">
33
+ <div className="space-y-2">
34
+ <label htmlFor="email">Email</label>
35
+ <Input
36
+ id="email"
37
+ name="email"
38
+ type="email"
39
+ required
40
+ className="bg-slate-950 border-slate-800"
41
+ />
42
+ </div>
43
+ <div className="space-y-2">
44
+ <label htmlFor="password">Password</label>
45
+ <Input
46
+ id="password"
47
+ name="password"
48
+ type="password"
49
+ required
50
+ className="bg-slate-950 border-slate-800"
51
+ />
52
+ </div>
53
+ {error && <p className="text-red-500 text-sm">{error}</p>}
54
+ <Button type="submit" className="w-full bg-emerald-600 hover:bg-emerald-700">
55
+ Register
56
+ </Button>
57
+ </form>
58
+ <div className="mt-4 text-center text-sm">
59
+ <Link href="/login" className="text-emerald-500 hover:underline">
60
+ Already have an account? Login
61
+ </Link>
62
+ </div>
63
+ </CardContent>
64
+ </Card>
65
+ </div>
66
+ );
67
+ }
app/api/auth/[...nextauth]/route.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ import { handlers } from "@/auth" // Referring to the auth.ts we just created
2
+ export const { GET, POST } = handlers
app/api/files/[id]/route.ts ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { readFile, unlink } from "fs/promises";
3
+ import { join } from "path";
4
+ import { existsSync } from "fs";
5
+
6
+ const UPLOAD_DIR = join(process.cwd(), "uploads");
7
+
8
+ export async function GET(
9
+ request: NextRequest,
10
+ { params }: { params: Promise<{ id: string }> }
11
+ ) {
12
+ try {
13
+ const { id } = await params;
14
+ const fileId = id;
15
+ const filePath = join(UPLOAD_DIR, fileId);
16
+
17
+ if (!existsSync(filePath)) {
18
+ return NextResponse.json(
19
+ { error: "File not found" },
20
+ { status: 404 }
21
+ );
22
+ }
23
+
24
+ const fileBuffer = await readFile(filePath);
25
+
26
+ return new NextResponse(fileBuffer, {
27
+ headers: {
28
+ "Content-Type": "application/octet-stream",
29
+ "Content-Disposition": "attachment",
30
+ },
31
+ });
32
+ } catch (error) {
33
+ console.error("Download error:", error);
34
+ return NextResponse.json(
35
+ { error: "Download failed" },
36
+ { status: 500 }
37
+ );
38
+ }
39
+ }
40
+
41
+ export async function DELETE(
42
+ request: NextRequest,
43
+ { params }: { params: Promise<{ id: string }> }
44
+ ) {
45
+ try {
46
+ const { id } = await params;
47
+ const fileId = id;
48
+ const filePath = join(UPLOAD_DIR, fileId);
49
+
50
+ if (existsSync(filePath)) {
51
+ await unlink(filePath);
52
+ }
53
+
54
+ return NextResponse.json({ success: true });
55
+ } catch (error) {
56
+ console.error("Delete error:", error);
57
+ return NextResponse.json(
58
+ { error: "Delete failed" },
59
+ { status: 500 }
60
+ );
61
+ }
62
+ }
app/api/upload/route.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { writeFile, mkdir } from "fs/promises";
3
+ import { join } from "path";
4
+ import { existsSync } from "fs";
5
+
6
+ const UPLOAD_DIR = join(process.cwd(), "uploads");
7
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
8
+
9
+ // Ensure upload directory exists
10
+ async function ensureUploadDir() {
11
+ if (!existsSync(UPLOAD_DIR)) {
12
+ await mkdir(UPLOAD_DIR, { recursive: true });
13
+ }
14
+ }
15
+
16
+ export async function POST(request: NextRequest) {
17
+ try {
18
+ const formData = await request.formData();
19
+ const file = formData.get("file") as File;
20
+
21
+ if (!file) {
22
+ return NextResponse.json(
23
+ { error: "No file provided" },
24
+ { status: 400 }
25
+ );
26
+ }
27
+
28
+ // Check file size
29
+ if (file.size > MAX_FILE_SIZE) {
30
+ return NextResponse.json(
31
+ { error: "File too large. Maximum size is 10MB" },
32
+ { status: 400 }
33
+ );
34
+ }
35
+
36
+ // Ensure upload directory exists
37
+ await ensureUploadDir();
38
+
39
+ // Generate unique file ID
40
+ const fileId = crypto.randomUUID();
41
+ const bytes = await file.arrayBuffer();
42
+ const buffer = Buffer.from(bytes);
43
+
44
+ // Save encrypted file
45
+ const filePath = join(UPLOAD_DIR, fileId);
46
+ await writeFile(filePath, buffer);
47
+
48
+ return NextResponse.json({
49
+ fileId,
50
+ size: file.size,
51
+ name: file.name,
52
+ type: file.type,
53
+ });
54
+ } catch (error) {
55
+ console.error("Upload error:", error);
56
+ return NextResponse.json(
57
+ { error: "Upload failed" },
58
+ { status: 500 }
59
+ );
60
+ }
61
+ }
app/favicon.ico ADDED
app/globals.css ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ @custom-variant dark (&:is(.dark *));
4
+
5
+ @theme inline {
6
+ --color-background: var(--background);
7
+ --color-foreground: var(--foreground);
8
+ --font-sans: var(--font-geist-sans);
9
+ --font-mono: var(--font-geist-mono);
10
+ --color-sidebar-ring: var(--sidebar-ring);
11
+ --color-sidebar-border: var(--sidebar-border);
12
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
13
+ --color-sidebar-accent: var(--sidebar-accent);
14
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
15
+ --color-sidebar-primary: var(--sidebar-primary);
16
+ --color-sidebar-foreground: var(--sidebar-foreground);
17
+ --color-sidebar: var(--sidebar);
18
+ --color-chart-5: var(--chart-5);
19
+ --color-chart-4: var(--chart-4);
20
+ --color-chart-3: var(--chart-3);
21
+ --color-chart-2: var(--chart-2);
22
+ --color-chart-1: var(--chart-1);
23
+ --color-ring: var(--ring);
24
+ --color-input: var(--input);
25
+ --color-border: var(--border);
26
+ --color-destructive: var(--destructive);
27
+ --color-accent-foreground: var(--accent-foreground);
28
+ --color-accent: var(--accent);
29
+ --color-muted-foreground: var(--muted-foreground);
30
+ --color-muted: var(--muted);
31
+ --color-secondary-foreground: var(--secondary-foreground);
32
+ --color-secondary: var(--secondary);
33
+ --color-primary-foreground: var(--primary-foreground);
34
+ --color-primary: var(--primary);
35
+ --color-popover-foreground: var(--popover-foreground);
36
+ --color-popover: var(--popover);
37
+ --color-card-foreground: var(--card-foreground);
38
+ --color-card: var(--card);
39
+ --radius-sm: calc(var(--radius) - 4px);
40
+ --radius-md: calc(var(--radius) - 2px);
41
+ --radius-lg: var(--radius);
42
+ --radius-xl: calc(var(--radius) + 4px);
43
+
44
+ --animate-fade-in: fade-in 0.5s ease-out;
45
+ --animate-fade-in-up: fade-in-up 0.5s ease-out;
46
+ --animate-pulse-slow: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
47
+
48
+ @keyframes fade-in {
49
+ 0% { opacity: 0; }
50
+ 100% { opacity: 1; }
51
+ }
52
+
53
+ @keyframes fade-in-up {
54
+ 0% { opacity: 0; transform: translateY(10px); }
55
+ 100% { opacity: 1; transform: translateY(0); }
56
+ }
57
+ }
58
+
59
+ :root {
60
+ --radius: 0.625rem;
61
+ --background: oklch(1 0 0);
62
+ --foreground: oklch(0.145 0 0);
63
+ --card: oklch(1 0 0);
64
+ --card-foreground: oklch(0.145 0 0);
65
+ --popover: oklch(1 0 0);
66
+ --popover-foreground: oklch(0.145 0 0);
67
+ --primary: oklch(0.205 0 0);
68
+ --primary-foreground: oklch(0.985 0 0);
69
+ --secondary: oklch(0.97 0 0);
70
+ --secondary-foreground: oklch(0.205 0 0);
71
+ --muted: oklch(0.97 0 0);
72
+ --muted-foreground: oklch(0.556 0 0);
73
+ --accent: oklch(0.97 0 0);
74
+ --accent-foreground: oklch(0.205 0 0);
75
+ --destructive: oklch(0.577 0.245 27.325);
76
+ --border: oklch(0.922 0 0);
77
+ --input: oklch(0.922 0 0);
78
+ --ring: oklch(0.708 0 0);
79
+ --chart-1: oklch(0.646 0.222 41.116);
80
+ --chart-2: oklch(0.6 0.118 184.704);
81
+ --chart-3: oklch(0.398 0.07 227.392);
82
+ --chart-4: oklch(0.828 0.189 84.429);
83
+ --chart-5: oklch(0.769 0.188 70.08);
84
+ --sidebar: oklch(0.985 0 0);
85
+ --sidebar-foreground: oklch(0.145 0 0);
86
+ --sidebar-primary: oklch(0.205 0 0);
87
+ --sidebar-primary-foreground: oklch(0.985 0 0);
88
+ --sidebar-accent: oklch(0.97 0 0);
89
+ --sidebar-accent-foreground: oklch(0.205 0 0);
90
+ --sidebar-border: oklch(0.922 0 0);
91
+ --sidebar-ring: oklch(0.708 0 0);
92
+ }
93
+
94
+ .dark {
95
+ --background: oklch(0.12 0.01 240); /* Slightly richer dark blue-grey */
96
+ --foreground: oklch(0.985 0 0);
97
+ --card: oklch(0.18 0.02 240);
98
+ --card-foreground: oklch(0.985 0 0);
99
+ --popover: oklch(0.18 0.02 240);
100
+ --popover-foreground: oklch(0.985 0 0);
101
+ --primary: oklch(0.65 0.2 150); /* Emerald-ish */
102
+ --primary-foreground: oklch(0.145 0 0);
103
+ --secondary: oklch(0.25 0.02 240);
104
+ --secondary-foreground: oklch(0.985 0 0);
105
+ --muted: oklch(0.25 0.02 240);
106
+ --muted-foreground: oklch(0.7 0 0);
107
+ --accent: oklch(0.25 0.02 240);
108
+ --accent-foreground: oklch(0.985 0 0);
109
+ --destructive: oklch(0.6 0.2 20);
110
+ --border: oklch(0.3 0.02 240);
111
+ --input: oklch(0.3 0.02 240);
112
+ --ring: oklch(0.65 0.2 150);
113
+ --chart-1: oklch(0.488 0.243 264.376);
114
+ --chart-2: oklch(0.696 0.17 162.48);
115
+ --chart-3: oklch(0.769 0.188 70.08);
116
+ --chart-4: oklch(0.627 0.265 303.9);
117
+ --chart-5: oklch(0.645 0.246 16.439);
118
+ --sidebar: oklch(0.18 0.02 240);
119
+ --sidebar-foreground: oklch(0.985 0 0);
120
+ --sidebar-primary: oklch(0.65 0.2 150);
121
+ --sidebar-primary-foreground: oklch(0.985 0 0);
122
+ --sidebar-accent: oklch(0.25 0.02 240);
123
+ --sidebar-accent-foreground: oklch(0.985 0 0);
124
+ --sidebar-border: oklch(0.3 0.02 240);
125
+ --sidebar-ring: oklch(0.65 0.2 150);
126
+ }
127
+
128
+ @layer base {
129
+ * {
130
+ @apply border-border outline-ring/50;
131
+ }
132
+ body {
133
+ @apply bg-background text-foreground antialiased selection:bg-emerald-500/30 selection:text-emerald-200;
134
+ }
135
+ }
136
+
137
+ /* Custom Scrollbar */
138
+ ::-webkit-scrollbar {
139
+ width: 8px;
140
+ height: 8px;
141
+ }
142
+
143
+ ::-webkit-scrollbar-track {
144
+ background: transparent;
145
+ }
146
+
147
+ ::-webkit-scrollbar-thumb {
148
+ @apply bg-muted-foreground/20 rounded-full hover:bg-muted-foreground/40 transition-colors;
149
+ }
150
+
151
+ /* Glassmorphism Utilities */
152
+ .glass {
153
+ @apply bg-background/60 backdrop-blur-md border border-border/50;
154
+ }
155
+
156
+ .glass-card {
157
+ @apply bg-card/40 backdrop-blur-sm border border-white/10 shadow-xl;
158
+ }
app/layout.tsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import "./globals.css";
4
+ import { Providers } from "@/components/providers";
5
+ import { ThemeProvider } from "@/components/theme-provider";
6
+
7
+ const geistSans = Geist({
8
+ variable: "--font-geist-sans",
9
+ subsets: ["latin"],
10
+ });
11
+
12
+ const geistMono = Geist_Mono({
13
+ variable: "--font-geist-mono",
14
+ subsets: ["latin"],
15
+ });
16
+
17
+ export const metadata: Metadata = {
18
+ title: "Create Next App",
19
+ description: "Generated by create next app",
20
+ };
21
+
22
+ export default function RootLayout({
23
+ children,
24
+ }: Readonly<{
25
+ children: React.ReactNode;
26
+ }>) {
27
+ return (
28
+ <html lang="en" suppressHydrationWarning>
29
+ <body
30
+ className={`${geistSans.variable} ${geistMono.variable} antialiased`}
31
+ >
32
+ <ThemeProvider
33
+ attribute="class"
34
+ defaultTheme="dark"
35
+ forcedTheme="dark"
36
+ enableSystem={false}
37
+ disableTransitionOnChange
38
+ >
39
+ <Providers>{children}</Providers>
40
+ </ThemeProvider>
41
+ </body>
42
+ </html>
43
+ );
44
+ }
app/page.tsx ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Card, CardContent } from "@/components/ui/card";
8
+ import { Lock, Shield, Zap, Eye, Users, MessageSquare, ArrowRight, CheckCircle2, X } from "lucide-react";
9
+
10
+ export default function Home() {
11
+ const router = useRouter();
12
+ const [roomCode, setRoomCode] = useState("");
13
+ const [nickname, setNickname] = useState("");
14
+ const [userLimit, setUserLimit] = useState("10");
15
+ const [password, setPassword] = useState("");
16
+ const [showJoinModal, setShowJoinModal] = useState(false);
17
+ const [mounted, setMounted] = useState(false);
18
+
19
+ useEffect(() => {
20
+ setMounted(true);
21
+ }, []);
22
+
23
+ const createRoom = async () => {
24
+ if (!nickname.trim()) {
25
+ alert("Please enter a nickname");
26
+ return;
27
+ }
28
+ if (password) {
29
+ sessionStorage.setItem("temp_room_password", password);
30
+ }
31
+ const code = Math.random().toString(36).substring(2, 8).toUpperCase();
32
+ router.push(`/room/${code}?nickname=${encodeURIComponent(nickname)}&limit=${userLimit}`);
33
+ };
34
+
35
+ const joinRoom = () => {
36
+ if (!nickname.trim()) {
37
+ alert("Please enter a nickname");
38
+ return;
39
+ }
40
+ if (roomCode.trim()) {
41
+ if (password) {
42
+ sessionStorage.setItem("temp_room_password", password);
43
+ }
44
+ router.push(`/room/${roomCode.toUpperCase()}?nickname=${encodeURIComponent(nickname)}`);
45
+ }
46
+ };
47
+
48
+ if (!mounted) return null;
49
+
50
+ return (
51
+ <div className="min-h-screen bg-background relative overflow-hidden selection:bg-emerald-500/30">
52
+ {/* Background Gradients */}
53
+ <div className="fixed inset-0 z-0 pointer-events-none">
54
+ <div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-emerald-500/10 rounded-full blur-[120px] animate-pulse-slow" />
55
+ <div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-blue-600/10 rounded-full blur-[120px] animate-pulse-slow" style={{ animationDelay: '1.5s' }} />
56
+ </div>
57
+
58
+ {/* Navbar */}
59
+ <nav className="container mx-auto px-6 py-6 relative z-10">
60
+ <div className="flex items-center justify-between">
61
+ <div className="flex items-center gap-3">
62
+ <div className="relative w-10 h-10 bg-gradient-to-br from-emerald-400 to-cyan-400 rounded-xl flex items-center justify-center shadow-lg shadow-emerald-500/20">
63
+ <Lock className="w-6 h-6 text-slate-950" />
64
+ </div>
65
+ <span className="text-2xl font-bold bg-gradient-to-r from-emerald-400 to-cyan-400 bg-clip-text text-transparent">
66
+ WhisperNet
67
+ </span>
68
+ </div>
69
+ <div className="flex gap-4">
70
+ <Button variant="ghost" className="text-muted-foreground hover:text-foreground">Login</Button>
71
+ <Button className="bg-emerald-600 hover:bg-emerald-700 text-white shadow-lg shadow-emerald-500/20">Sign Up</Button>
72
+ </div>
73
+ </div>
74
+ </nav>
75
+
76
+ <main className="container mx-auto px-6 relative z-10">
77
+ {/* Hero Content */}
78
+ <div className="pt-20 pb-24 text-center">
79
+ <div className="inline-flex items-center gap-2 bg-emerald-500/10 border border-emerald-500/20 rounded-full px-4 py-2 mb-8 animate-fade-in-up">
80
+ <Shield className="w-4 h-4 text-emerald-400" />
81
+ <span className="text-sm text-emerald-400 font-medium">End-to-End Encrypted</span>
82
+ </div>
83
+
84
+ <h1 className="text-6xl md:text-7xl font-bold text-foreground mb-6 leading-tight tracking-tight animate-fade-in-up" style={{ animationDelay: '0.1s' }}>
85
+ Private Conversations,
86
+ <br />
87
+ <span className="bg-gradient-to-r from-emerald-400 via-cyan-400 to-blue-400 bg-clip-text text-transparent">
88
+ Zero Traces
89
+ </span>
90
+ </h1>
91
+
92
+ <p className="text-xl text-muted-foreground mb-12 max-w-2xl mx-auto animate-fade-in-up" style={{ animationDelay: '0.2s' }}>
93
+ Secure, ephemeral chat rooms with military-grade encryption.
94
+ No registration, no history, no compromises.
95
+ </p>
96
+
97
+ {/* CTA Buttons */}
98
+ <div className="flex flex-col sm:flex-row gap-4 justify-center mb-16 animate-fade-in-up" style={{ animationDelay: '0.3s' }}>
99
+ <Button
100
+ onClick={() => setShowJoinModal(true)}
101
+ className="h-14 px-8 text-lg bg-gradient-to-r from-emerald-500 to-cyan-500 hover:from-emerald-600 hover:to-cyan-600 text-white font-semibold shadow-lg shadow-emerald-500/25 hover:shadow-emerald-500/40 transition-all hover:scale-105"
102
+ >
103
+ Get Started
104
+ <ArrowRight className="ml-2 h-5 w-5" />
105
+ </Button>
106
+ <Button
107
+ variant="outline"
108
+ className="h-14 px-8 text-lg border-border bg-background/50 hover:bg-accent hover:text-accent-foreground backdrop-blur-sm transition-all"
109
+ >
110
+ Learn More
111
+ </Button>
112
+ </div>
113
+
114
+ {/* Feature Cards */}
115
+ <div className="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto mb-24 animate-fade-in-up" style={{ animationDelay: '0.4s' }}>
116
+ {[
117
+ { icon: Lock, title: "E2E Encryption", desc: "Every message encrypted with unique keys. Nobody can intercept your conversations." },
118
+ { icon: Eye, title: "No Tracking", desc: "Zero data collection. No accounts required. Your privacy is absolute." },
119
+ { icon: Zap, title: "Instant Setup", desc: "Create or join rooms in seconds. No downloads, no hassle." }
120
+ ].map((feature, i) => (
121
+ <Card key={i} className="glass-card border-white/5 hover:border-emerald-500/30 transition-all duration-300 group hover:-translate-y-1">
122
+ <CardContent className="p-6">
123
+ <div className="w-12 h-12 bg-gradient-to-br from-emerald-500/20 to-cyan-500/20 rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300">
124
+ <feature.icon className="w-6 h-6 text-emerald-400" />
125
+ </div>
126
+ <h3 className="text-xl font-bold text-foreground mb-2">{feature.title}</h3>
127
+ <p className="text-muted-foreground leading-relaxed">
128
+ {feature.desc}
129
+ </p>
130
+ </CardContent>
131
+ </Card>
132
+ ))}
133
+ </div>
134
+
135
+ {/* Features List */}
136
+ <div className="max-w-4xl mx-auto mb-24">
137
+ <h2 className="text-3xl font-bold text-foreground mb-12">
138
+ Everything you need for secure communication
139
+ </h2>
140
+ <div className="grid md:grid-cols-2 gap-6 text-left">
141
+ {[
142
+ { icon: MessageSquare, title: "Real-time Messaging", desc: "Instant message delivery with read receipts" },
143
+ { icon: Users, title: "Group Chats", desc: "Secure rooms for multiple participants" },
144
+ { icon: Shield, title: "Perfect Forward Secrecy", desc: "New keys for every session" },
145
+ { icon: CheckCircle2, title: "Typing Indicators", desc: "Know when others are responding" },
146
+ ].map((feature, i) => (
147
+ <div key={i} className="flex gap-4 items-start p-4 rounded-xl bg-card/30 border border-border/50 hover:bg-card/50 transition-colors">
148
+ <div className="w-10 h-10 bg-emerald-500/10 rounded-lg flex items-center justify-center flex-shrink-0">
149
+ <feature.icon className="w-5 h-5 text-emerald-400" />
150
+ </div>
151
+ <div>
152
+ <h3 className="text-lg font-semibold text-foreground mb-1">{feature.title}</h3>
153
+ <p className="text-muted-foreground text-sm">{feature.desc}</p>
154
+ </div>
155
+ </div>
156
+ ))}
157
+ </div>
158
+ </div>
159
+ </div>
160
+ </main>
161
+
162
+ {/* Footer */}
163
+ <footer className="border-t border-border/50 py-8 relative z-10 bg-background/50 backdrop-blur-sm">
164
+ <div className="container mx-auto px-6 text-center text-muted-foreground text-sm">
165
+ <p>© 2025 WhisperNet. Secure by design.</p>
166
+ </div>
167
+ </footer>
168
+
169
+ {/* Join Modal */}
170
+ {showJoinModal && (
171
+ <div className="fixed inset-0 bg-black/60 backdrop-blur-md flex items-center justify-center p-4 z-50 animate-fade-in" onClick={() => setShowJoinModal(false)}>
172
+ <Card className="w-full max-w-md bg-card border-border shadow-2xl animate-fade-in-up" onClick={(e) => e.stopPropagation()}>
173
+ <div className="absolute right-4 top-4">
174
+ <Button variant="ghost" size="icon" onClick={() => setShowJoinModal(false)} className="h-8 w-8 rounded-full hover:bg-muted">
175
+ <X className="w-4 h-4" />
176
+ </Button>
177
+ </div>
178
+ <CardContent className="p-8 space-y-6">
179
+ <div className="text-center">
180
+ <div className="w-12 h-12 bg-emerald-500/10 rounded-xl flex items-center justify-center mx-auto mb-4">
181
+ <Users className="w-6 h-6 text-emerald-500" />
182
+ </div>
183
+ <h2 className="text-2xl font-bold text-foreground mb-2">Join WhisperNet</h2>
184
+ <p className="text-muted-foreground text-sm">Enter a nickname to get started</p>
185
+ </div>
186
+
187
+ <div className="space-y-4">
188
+ <div className="space-y-2">
189
+ <label className="text-sm font-medium text-foreground">Your Nickname</label>
190
+ <Input
191
+ placeholder="Enter your display name"
192
+ value={nickname}
193
+ onChange={(e) => setNickname(e.target.value)}
194
+ className="bg-muted/50 border-border focus:ring-emerald-500 h-11"
195
+ autoFocus
196
+ />
197
+ </div>
198
+
199
+ <div className="space-y-2">
200
+ <label className="text-sm font-medium text-foreground">Room Password (Optional)</label>
201
+ <Input
202
+ type="password"
203
+ placeholder="Set a password for the room"
204
+ value={password}
205
+ onChange={(e) => setPassword(e.target.value)}
206
+ className="bg-muted/50 border-border focus:ring-emerald-500 h-11"
207
+ />
208
+ </div>
209
+
210
+ <div className="pt-4 border-t border-border">
211
+ <label className="text-sm font-medium text-foreground mb-2 block">Create Room</label>
212
+ <div className="flex gap-3">
213
+ <Input
214
+ type="number"
215
+ placeholder="Max"
216
+ value={userLimit}
217
+ onChange={(e) => setUserLimit(e.target.value)}
218
+ className="w-24 bg-muted/50 border-border focus:ring-emerald-500 h-11"
219
+ min={2}
220
+ max={50}
221
+ />
222
+ <Button
223
+ onClick={createRoom}
224
+ className="flex-1 h-11 bg-emerald-600 hover:bg-emerald-700 text-white font-semibold shadow-lg shadow-emerald-500/20"
225
+ >
226
+ Create Room
227
+ </Button>
228
+ </div>
229
+ </div>
230
+
231
+ <div className="relative py-2">
232
+ <div className="absolute inset-0 flex items-center">
233
+ <span className="w-full border-t border-border" />
234
+ </div>
235
+ <div className="relative flex justify-center text-xs uppercase">
236
+ <span className="bg-card px-2 text-muted-foreground">Or join existing</span>
237
+ </div>
238
+ </div>
239
+
240
+ <div className="space-y-2">
241
+ <label className="text-sm font-medium text-foreground">Join Room</label>
242
+ <div className="flex gap-3">
243
+ <Input
244
+ placeholder="Enter Room Code"
245
+ value={roomCode}
246
+ onChange={(e) => setRoomCode(e.target.value)}
247
+ className="bg-muted/50 border-border focus:ring-emerald-500 h-11"
248
+ />
249
+ <Button
250
+ onClick={joinRoom}
251
+ className="h-11 bg-secondary hover:bg-secondary/80 text-secondary-foreground"
252
+ >
253
+ Join
254
+ </Button>
255
+ </div>
256
+ </div>
257
+ </div>
258
+ </CardContent>
259
+ </Card>
260
+ </div>
261
+ )}
262
+ </div>
263
+ );
264
+ }
app/room/[roomId]/page.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ChatRoom from "@/components/chat/ChatRoom";
2
+
3
+ interface PageProps {
4
+ params: Promise<{ roomId: string }>;
5
+ }
6
+
7
+ export default async function RoomPage({ params }: PageProps) {
8
+ const { roomId } = await params;
9
+
10
+ return (
11
+ <div className="min-h-screen bg-slate-950">
12
+ <ChatRoom roomId={roomId} />
13
+ </div>
14
+ );
15
+ }
app/settings/page.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Input } from "@/components/ui/input";
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import { enableTwoFactor, confirmTwoFactor, disableTwoFactor, getTwoFactorStatus } from "@/actions/two-factor";
8
+ import { useSession, signOut } from "next-auth/react";
9
+ import Image from "next/image";
10
+
11
+ export default function SettingsPage() {
12
+ const { data: session } = useSession();
13
+ const [isEnabled, setIsEnabled] = useState(false);
14
+ const [qrCode, setQrCode] = useState("");
15
+ const [token, setToken] = useState("");
16
+ const [step, setStep] = useState<"idle" | "verify">("idle");
17
+ const [msg, setMsg] = useState("");
18
+
19
+ useEffect(() => {
20
+ getTwoFactorStatus().then((res) => {
21
+ if (res.isEnabled) setIsEnabled(true);
22
+ });
23
+ }, []);
24
+
25
+ const handleEnable = async () => {
26
+ const res = await enableTwoFactor();
27
+ if (res.qrCodeUrl) {
28
+ setQrCode(res.qrCodeUrl);
29
+ setStep("verify");
30
+ }
31
+ };
32
+
33
+ const handleVerify = async () => {
34
+ const res = await confirmTwoFactor(token);
35
+ if (res.success) {
36
+ setIsEnabled(true);
37
+ setStep("idle");
38
+ setQrCode("");
39
+ setMsg("2FA Enabled Successfully!");
40
+ } else {
41
+ setMsg("Invalid Token");
42
+ }
43
+ };
44
+
45
+ const handleDisable = async () => {
46
+ await disableTwoFactor();
47
+ setIsEnabled(false);
48
+ setMsg("2FA Disabled");
49
+ };
50
+
51
+ if (!session) return <div className="p-10 text-white">Loading...</div>;
52
+
53
+ return (
54
+ <div className="flex flex-col items-center justify-center min-h-screen bg-slate-950 text-slate-100">
55
+ <Card className="w-[400px] bg-slate-900 border-slate-800 text-slate-100">
56
+ <CardHeader>
57
+ <CardTitle>Settings</CardTitle>
58
+ </CardHeader>
59
+ <CardContent className="space-y-4">
60
+ <div className="flex justify-between items-center">
61
+ <span>Email:</span>
62
+ <span className="text-slate-400">{session.user?.email}</span>
63
+ </div>
64
+
65
+ <div className="border-t border-slate-800 pt-4">
66
+ <h3 className="text-lg font-semibold mb-2">Two-Factor Authentication</h3>
67
+ {isEnabled ? (
68
+ <div className="space-y-2">
69
+ <p className="text-emerald-500">✅ Enabled</p>
70
+ <Button onClick={handleDisable} variant="destructive" className="w-full">
71
+ Disable 2FA
72
+ </Button>
73
+ </div>
74
+ ) : (
75
+ <div className="space-y-2">
76
+ <p className="text-slate-400">Secure your account with 2FA.</p>
77
+ {step === "idle" && (
78
+ <Button onClick={handleEnable} className="w-full bg-emerald-600 hover:bg-emerald-700">
79
+ Enable 2FA
80
+ </Button>
81
+ )}
82
+ </div>
83
+ )}
84
+
85
+ {step === "verify" && qrCode && (
86
+ <div className="mt-4 space-y-4">
87
+ <div className="flex justify-center bg-white p-2 rounded">
88
+ <Image src={qrCode} alt="QR Code" width={150} height={150} />
89
+ </div>
90
+ <p className="text-xs text-center text-slate-400">Scan with Google Authenticator</p>
91
+ <Input
92
+ value={token}
93
+ onChange={(e) => setToken(e.target.value)}
94
+ placeholder="Enter 6-digit code"
95
+ className="bg-slate-950 border-slate-800"
96
+ />
97
+ <Button onClick={handleVerify} className="w-full">
98
+ Verify & Activate
99
+ </Button>
100
+ </div>
101
+ )}
102
+
103
+ {msg && <p className="text-center text-sm mt-2">{msg}</p>}
104
+ </div>
105
+
106
+ <div className="border-t border-slate-800 pt-4">
107
+ <Button onClick={() => signOut()} variant="outline" className="w-full border-slate-700 text-slate-300 hover:bg-slate-800">
108
+ Sign Out
109
+ </Button>
110
+ </div>
111
+ </CardContent>
112
+ </Card>
113
+ </div>
114
+ );
115
+ }
auth.ts ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import NextAuth, { CredentialsSignin } from "next-auth";
2
+ import Credentials from "next-auth/providers/credentials";
3
+ import { compare } from "bcryptjs";
4
+ import User from "@/models/User";
5
+ import dbConnect from "@/lib/db";
6
+ import { verifyTwoFactorToken } from "@/lib/tokens";
7
+
8
+ class TwoFactorRequired extends CredentialsSignin {
9
+ code = "2FA_REQUIRED";
10
+ }
11
+
12
+ class InvalidToken extends CredentialsSignin {
13
+ code = "INVALID_2FA_TOKEN";
14
+ }
15
+
16
+ export const { handlers, signIn, signOut, auth } = NextAuth({
17
+ providers: [
18
+ Credentials({
19
+ credentials: {
20
+ email: { label: "Email", type: "email" },
21
+ password: { label: "Password", type: "password" },
22
+ token: { label: "2FA Token", type: "text" },
23
+ },
24
+ authorize: async (credentials) => {
25
+ await dbConnect();
26
+
27
+ const email = credentials.email as string;
28
+ const password = credentials.password as string;
29
+ const token = credentials.token as string | undefined;
30
+
31
+ if (!email || !password) {
32
+ throw new Error("Missing credentials");
33
+ }
34
+
35
+ const user = await User.findOne({ email });
36
+
37
+ if (!user || !user.password) {
38
+ throw new Error("Invalid credentials");
39
+ }
40
+
41
+ const isPasswordValid = await compare(password, user.password);
42
+
43
+ if (!isPasswordValid) {
44
+ throw new Error("Invalid credentials");
45
+ }
46
+
47
+ if (user.isTwoFactorEnabled) {
48
+ if (!token) {
49
+ throw new TwoFactorRequired();
50
+ }
51
+
52
+ const isTokenValid = verifyTwoFactorToken(token, user.twoFactorSecret!);
53
+
54
+ if (!isTokenValid) {
55
+ throw new InvalidToken();
56
+ }
57
+ }
58
+
59
+ return {
60
+ id: user._id.toString(),
61
+ email: user.email,
62
+ image: user.image,
63
+ };
64
+ },
65
+ }),
66
+ ],
67
+ pages: {
68
+ signIn: "/login",
69
+ },
70
+ callbacks: {
71
+ async session({ session, token }) {
72
+ if (token.sub && session.user) {
73
+ session.user.id = token.sub;
74
+ }
75
+ return session;
76
+ },
77
+ async jwt({ token }) {
78
+ return token;
79
+ },
80
+ },
81
+ session: { strategy: "jwt" },
82
+ });
bun.lock ADDED
The diff for this file is too large to render. See raw diff
 
components.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "aliases": {
15
+ "components": "@/components",
16
+ "utils": "@/lib/utils",
17
+ "ui": "@/components/ui",
18
+ "lib": "@/lib",
19
+ "hooks": "@/hooks"
20
+ },
21
+ "registries": {}
22
+ }
components/chat/CallInterface.tsx ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Mic, MicOff, Video, VideoOff, PhoneOff } from "lucide-react";
6
+ import { Card } from "@/components/ui/card";
7
+
8
+ interface CallInterfaceProps {
9
+ localStream: MediaStream | null;
10
+ remoteStreams: Map<string, MediaStream>;
11
+ onLeave: () => void;
12
+ nicknames: Map<string, string>;
13
+ }
14
+
15
+ export default function CallInterface({ localStream, remoteStreams, onLeave, nicknames }: CallInterfaceProps) {
16
+ const localVideoRef = useRef<HTMLVideoElement>(null);
17
+ const [isMuted, setIsMuted] = useState(false);
18
+ const [isVideoOff, setIsVideoOff] = useState(false);
19
+
20
+ useEffect(() => {
21
+ if (localVideoRef.current && localStream) {
22
+ localVideoRef.current.srcObject = localStream;
23
+ }
24
+ }, [localStream]);
25
+
26
+ const toggleMute = () => {
27
+ if (localStream) {
28
+ localStream.getAudioTracks().forEach(track => track.enabled = !track.enabled);
29
+ setIsMuted(!isMuted);
30
+ }
31
+ };
32
+
33
+ const toggleVideo = () => {
34
+ if (localStream) {
35
+ localStream.getVideoTracks().forEach(track => track.enabled = !track.enabled);
36
+ setIsVideoOff(!isVideoOff);
37
+ }
38
+ };
39
+
40
+ return (
41
+ <div className="fixed inset-0 bg-background/95 z-50 flex flex-col p-4">
42
+ <div className="flex-1 grid grid-cols-2 md:grid-cols-3 gap-4 p-4 overflow-y-auto">
43
+ {/* Local Video */}
44
+ <Card className="relative overflow-hidden bg-black/50 aspect-video flex items-center justify-center">
45
+ <video
46
+ ref={localVideoRef}
47
+ autoPlay
48
+ muted
49
+ playsInline
50
+ className={`w-full h-full object-cover ${isVideoOff ? "hidden" : ""}`}
51
+ />
52
+ {isVideoOff && (
53
+ <div className="absolute inset-0 flex items-center justify-center text-muted-foreground">
54
+ Video Off
55
+ </div>
56
+ )}
57
+ <div className="absolute bottom-2 left-2 bg-black/50 px-2 py-1 rounded text-xs text-white">
58
+ You {isMuted && "(Muted)"}
59
+ </div>
60
+ </Card>
61
+
62
+ {/* Remote Videos */}
63
+ {Array.from(remoteStreams.entries()).map(([socketId, stream]) => (
64
+ <RemoteVideo
65
+ key={socketId}
66
+ stream={stream}
67
+ nickname={nicknames.get(socketId) || `User ${socketId.slice(0, 4)}`}
68
+ />
69
+ ))}
70
+ </div>
71
+
72
+ {/* Controls */}
73
+ <div className="h-20 flex items-center justify-center gap-4 bg-card border-t border-border rounded-t-xl">
74
+ <Button
75
+ variant={isMuted ? "destructive" : "secondary"}
76
+ size="icon"
77
+ className="rounded-full h-12 w-12"
78
+ onClick={toggleMute}
79
+ >
80
+ {isMuted ? <MicOff className="w-5 h-5" /> : <Mic className="w-5 h-5" />}
81
+ </Button>
82
+ <Button
83
+ variant="destructive"
84
+ size="icon"
85
+ className="rounded-full h-14 w-14"
86
+ onClick={onLeave}
87
+ >
88
+ <PhoneOff className="w-6 h-6" />
89
+ </Button>
90
+ <Button
91
+ variant={isVideoOff ? "destructive" : "secondary"}
92
+ size="icon"
93
+ className="rounded-full h-12 w-12"
94
+ onClick={toggleVideo}
95
+ >
96
+ {isVideoOff ? <VideoOff className="w-5 h-5" /> : <Video className="w-5 h-5" />}
97
+ </Button>
98
+ </div>
99
+ </div>
100
+ );
101
+ }
102
+
103
+ function RemoteVideo({ stream, nickname }: { stream: MediaStream; nickname: string }) {
104
+ const videoRef = useRef<HTMLVideoElement>(null);
105
+
106
+ useEffect(() => {
107
+ if (videoRef.current) {
108
+ videoRef.current.srcObject = stream;
109
+ }
110
+ }, [stream]);
111
+
112
+ return (
113
+ <Card className="relative overflow-hidden bg-black/50 aspect-video flex items-center justify-center">
114
+ <video
115
+ ref={videoRef}
116
+ autoPlay
117
+ playsInline
118
+ className="w-full h-full object-cover"
119
+ />
120
+ <div className="absolute bottom-2 left-2 bg-black/50 px-2 py-1 rounded text-xs text-white">
121
+ {nickname}
122
+ </div>
123
+ </Card>
124
+ );
125
+ }
components/chat/ChatRoom.tsx ADDED
@@ -0,0 +1,1017 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useState, useRef } from "react";
4
+ import { socket } from "@/lib/socket";
5
+ import {
6
+ generateKeyPair,
7
+ exportKey,
8
+ importKey,
9
+ encryptMessage,
10
+ decryptMessage,
11
+ generateSymKey,
12
+ encryptSymMessage,
13
+ decryptSymMessage,
14
+ exportSymKey,
15
+ importSymKey,
16
+ encryptFile,
17
+ decryptFile,
18
+ } from "@/lib/crypto";
19
+ import { Button } from "@/components/ui/button";
20
+ import { Input } from "@/components/ui/input";
21
+ import { ScrollArea } from "@/components/ui/scroll-area";
22
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
23
+ import { Send, User, Users, Lock, Check, CheckCheck, Smile, Paperclip, FileIcon, Download, Image as ImageIcon, Settings, Phone, Mic, MicOff, UserX, X } from "lucide-react";
24
+ import { useSearchParams, useRouter } from "next/navigation";
25
+ import dynamic from "next/dynamic";
26
+ import ReactMarkdown from "react-markdown";
27
+ import remarkGfm from "remark-gfm";
28
+ import { ThemeToggle } from "@/components/theme-toggle";
29
+ import { useTheme } from "next-themes";
30
+ import { useWebRTC } from "@/hooks/useWebRTC";
31
+ import CallInterface from "./CallInterface";
32
+
33
+ const EmojiPicker = dynamic(() => import("emoji-picker-react"), { ssr: false });
34
+
35
+ interface Message {
36
+ id: string;
37
+ senderId: string;
38
+ content: string;
39
+ timestamp: number;
40
+ status?: "sending" | "sent" | "delivered" | "read";
41
+ type?: "text" | "file";
42
+ file?: {
43
+ id: string;
44
+ name: string;
45
+ size: number;
46
+ mimeType: string;
47
+ url?: string;
48
+ };
49
+ encryptedKey?: string;
50
+ reactions?: Record<string, string[]>; // emoji -> [senderIds]
51
+ }
52
+
53
+ interface ChatRoomProps {
54
+ roomId: string;
55
+ }
56
+
57
+ export default function ChatRoom({ roomId }: ChatRoomProps) {
58
+ const [messages, setMessages] = useState<Message[]>([]);
59
+ const [inputMessage, setInputMessage] = useState("");
60
+ const [isConnected, setIsConnected] = useState(false);
61
+ const [participantCount, setParticipantCount] = useState(1);
62
+ const [nicknames, setNicknames] = useState<Map<string, string>>(new Map());
63
+ const [typingUsers, setTypingUsers] = useState<Set<string>>(new Set());
64
+ const [showEmojiPicker, setShowEmojiPicker] = useState(false);
65
+ const [isUploading, setIsUploading] = useState(false);
66
+ const [activeReactionMessageId, setActiveReactionMessageId] = useState<string | null>(null);
67
+
68
+ // Advanced Controls State
69
+ const [isCreator, setIsCreator] = useState(false);
70
+ const [isMuted, setIsMuted] = useState(false);
71
+ const [showPasswordModal, setShowPasswordModal] = useState(false);
72
+ const [passwordInput, setPasswordInput] = useState("");
73
+ const [showSettingsModal, setShowSettingsModal] = useState(false);
74
+ const [showParticipantsModal, setShowParticipantsModal] = useState(false);
75
+ const [participants, setParticipants] = useState<{ socketId: string; userId: string; nickname?: string; isMuted?: boolean }[]>([]);
76
+ const [newLimit, setNewLimit] = useState("");
77
+ const [newPassword, setNewPassword] = useState("");
78
+
79
+ const searchParams = useSearchParams();
80
+ const router = useRouter();
81
+ const nickname = searchParams.get("nickname") || "Anonymous";
82
+ const userLimit = searchParams.get("limit") ? parseInt(searchParams.get("limit")!) : undefined;
83
+ const { theme } = useTheme();
84
+ const { localStream, remoteStreams, joinCall, leaveCall } = useWebRTC(roomId);
85
+
86
+ const myKeys = useRef<{ public: CryptoKey; private: CryptoKey } | null>(null);
87
+ const otherUsersKeys = useRef<Map<string, CryptoKey>>(new Map());
88
+ const messagesEndRef = useRef<HTMLDivElement>(null);
89
+ const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
90
+ const inputRef = useRef<HTMLInputElement>(null);
91
+ const fileInputRef = useRef<HTMLInputElement>(null);
92
+
93
+ const scrollToBottom = () => {
94
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
95
+ };
96
+
97
+ useEffect(() => {
98
+ scrollToBottom();
99
+ }, [messages]);
100
+
101
+ useEffect(() => {
102
+ const init = async () => {
103
+ let userId = sessionStorage.getItem("userId");
104
+ if (!userId) {
105
+ userId = crypto.randomUUID();
106
+ sessionStorage.setItem("userId", userId);
107
+ }
108
+
109
+ const keys = await generateKeyPair();
110
+ myKeys.current = { public: keys.publicKey, private: keys.privateKey };
111
+
112
+ socket.auth = { userId };
113
+ socket.connect();
114
+
115
+ const storedPassword = sessionStorage.getItem("temp_room_password");
116
+ const password = storedPassword || searchParams.get("password");
117
+ if (storedPassword) sessionStorage.removeItem("temp_room_password");
118
+
119
+ socket.emit("join-room", { roomId, nickname, userLimit, password });
120
+ setIsConnected(true);
121
+
122
+ socket.on("error", (err: string) => {
123
+ if (err === "Password required" || err === "Invalid password") {
124
+ setShowPasswordModal(true);
125
+ if (err === "Invalid password") alert("Invalid password");
126
+ } else if (err === "You are muted") {
127
+ alert(err); // Just alert, don't redirect
128
+ } else {
129
+ alert(err);
130
+ router.push("/");
131
+ }
132
+ });
133
+
134
+ socket.on("room-role", (data: { role: string }) => {
135
+ if (data.role === "creator") setIsCreator(true);
136
+ });
137
+
138
+ socket.on("kicked", (msg: string) => {
139
+ alert(msg);
140
+ router.push("/");
141
+ });
142
+
143
+ socket.on("muted", (msg: string) => {
144
+ setIsMuted(true);
145
+ alert(msg);
146
+ });
147
+
148
+ socket.on("unmuted", (msg: string) => {
149
+ setIsMuted(false);
150
+ alert(msg);
151
+ });
152
+
153
+ socket.on("room-participants", (data: { socketId: string; userId: string; nickname?: string; isMuted: boolean }[]) => {
154
+ setParticipants(data);
155
+ const newMap = new Map(nicknames);
156
+ data.forEach(p => {
157
+ if (p.nickname) newMap.set(p.socketId, p.nickname);
158
+ });
159
+ setNicknames(newMap);
160
+ });
161
+
162
+ socket.on("user-muted", ({ userId }: { userId: string }) => {
163
+ setParticipants(prev => prev.map(p => p.userId === userId ? { ...p, isMuted: true } : p));
164
+ if (userId === sessionStorage.getItem("userId")) setIsMuted(true);
165
+ });
166
+
167
+ socket.on("user-unmuted", ({ userId }: { userId: string }) => {
168
+ setParticipants(prev => prev.map(p => p.userId === userId ? { ...p, isMuted: false } : p));
169
+ if (userId === sessionStorage.getItem("userId")) setIsMuted(false);
170
+ });
171
+
172
+ socket.on("room-info", (data: { count: number }) => {
173
+ setParticipantCount(data.count);
174
+ });
175
+
176
+ socket.on("user-joined", async (data: { socketId: string; userId: string; nickname?: string }) => {
177
+ if (data.nickname) {
178
+ setNicknames(prev => new Map(prev).set(data.socketId, data.nickname!));
179
+ }
180
+ setParticipants(prev => {
181
+ if (prev.find(p => p.socketId === data.socketId)) return prev;
182
+ return [...prev, { socketId: data.socketId, userId: data.userId, nickname: data.nickname }];
183
+ });
184
+
185
+ if (myKeys.current) {
186
+ const exportedPub = await exportKey(myKeys.current.public);
187
+ socket.emit("signal", {
188
+ target: data.socketId,
189
+ signal: { type: "offer-key", key: exportedPub },
190
+ });
191
+ }
192
+ });
193
+
194
+ socket.on("user-left", (data: { socketId: string; userId: string }) => {
195
+ otherUsersKeys.current.delete(data.socketId);
196
+ setParticipants(prev => prev.filter(p => p.socketId !== data.socketId));
197
+ });
198
+
199
+ socket.on("signal", async (data: { sender: string; signal: any }) => {
200
+ const { sender, signal } = data;
201
+ if (signal.type === "offer-key") {
202
+ const importedKey = await importKey(signal.key, ["encrypt"]);
203
+ otherUsersKeys.current.set(sender, importedKey);
204
+ if (myKeys.current) {
205
+ const exportedPub = await exportKey(myKeys.current.public);
206
+ socket.emit("signal", {
207
+ target: sender,
208
+ signal: { type: "answer-key", key: exportedPub },
209
+ });
210
+ }
211
+ } else if (signal.type === "answer-key") {
212
+ const importedKey = await importKey(signal.key, ["encrypt"]);
213
+ otherUsersKeys.current.set(sender, importedKey);
214
+ }
215
+ });
216
+
217
+ socket.on("message-status", (data: { messageId: string; status: "delivered" | "read"; originalSenderId: string }) => {
218
+ if (data.originalSenderId === socket.id) {
219
+ setMessages((prev) =>
220
+ prev.map((msg) => {
221
+ if (msg.id === data.messageId) {
222
+ if (msg.status === "read") return msg;
223
+ if (msg.status === "delivered" && data.status === "delivered") return msg;
224
+ return { ...msg, status: data.status };
225
+ }
226
+ return msg;
227
+ })
228
+ );
229
+ }
230
+ });
231
+
232
+ socket.on("receive-message", async (data: { senderId: string; payload: any; messageId: string; roomId: string; type?: string }) => {
233
+ const { senderId, payload, messageId, type } = data;
234
+
235
+ socket.emit("message-delivered", {
236
+ roomId: data.roomId,
237
+ messageId,
238
+ senderId,
239
+ recipientId: socket.id
240
+ });
241
+
242
+ try {
243
+ const myEncryptedKey = payload.keys[socket.id || ""];
244
+ if (!myEncryptedKey) return;
245
+
246
+ if (!myKeys.current) return;
247
+ const aesKeyRaw = await decryptMessage(myKeys.current.private, myEncryptedKey);
248
+ const aesKey = await importSymKey(aesKeyRaw);
249
+
250
+ let content = "";
251
+ let fileData = undefined;
252
+
253
+ if (type === "file") {
254
+ content = "[FILE]";
255
+ fileData = payload.file;
256
+ } else {
257
+ content = await decryptSymMessage(aesKey, payload.content);
258
+ }
259
+
260
+ setMessages((prev) => [
261
+ ...prev,
262
+ {
263
+ id: messageId || crypto.randomUUID(),
264
+ senderId,
265
+ content,
266
+ timestamp: Date.now(),
267
+ type: (type as "text" | "file") || "text",
268
+ file: fileData,
269
+ encryptedKey: myEncryptedKey
270
+ },
271
+ ]);
272
+
273
+ socket.emit("message-read", {
274
+ roomId,
275
+ messageId,
276
+ senderId,
277
+ recipientId: socket.id
278
+ });
279
+ } catch (err) {
280
+ console.error("Failed to decrypt message:", err);
281
+ }
282
+ });
283
+
284
+ socket.on("user-typing", ({ socketId, nickname }: { socketId: string; nickname: string }) => {
285
+ setTypingUsers(prev => new Set(prev).add(socketId));
286
+ setNicknames(prev => new Map(prev).set(socketId, nickname));
287
+ });
288
+
289
+ socket.on("user-stopped-typing", ({ socketId }: { socketId: string }) => {
290
+ setTypingUsers(prev => {
291
+ const newSet = new Set(prev);
292
+ newSet.delete(socketId);
293
+ return newSet;
294
+ });
295
+ });
296
+
297
+ socket.on("message-reaction-update", (data: { messageId: string; reaction: string; senderId: string }) => {
298
+ setMessages(prev => prev.map(msg => {
299
+ if (msg.id === data.messageId) {
300
+ const newReactions = { ...(msg.reactions || {}) };
301
+
302
+ // Check if user is toggling the same reaction
303
+ const currentUsers = newReactions[data.reaction] || [];
304
+ if (currentUsers.includes(data.senderId)) {
305
+ // Remove it (Toggle off)
306
+ newReactions[data.reaction] = currentUsers.filter(id => id !== data.senderId);
307
+ if (newReactions[data.reaction].length === 0) delete newReactions[data.reaction];
308
+ } else {
309
+ // Remove user from ALL other reactions first (Single reaction limit)
310
+ Object.keys(newReactions).forEach(key => {
311
+ const users = newReactions[key] || [];
312
+ if (users.includes(data.senderId)) {
313
+ newReactions[key] = users.filter(id => id !== data.senderId);
314
+ if (newReactions[key].length === 0) delete newReactions[key];
315
+ }
316
+ });
317
+
318
+ // Add new reaction
319
+ newReactions[data.reaction] = [...(newReactions[data.reaction] || []), data.senderId];
320
+ }
321
+ return { ...msg, reactions: newReactions };
322
+ }
323
+ return msg;
324
+ }));
325
+ });
326
+ };
327
+
328
+ init();
329
+
330
+ return () => {
331
+ socket.off("error");
332
+ socket.off("room-info");
333
+ socket.off("user-joined");
334
+ socket.off("user-left");
335
+ socket.off("signal");
336
+ socket.off("receive-message");
337
+ socket.off("message-status");
338
+ socket.off("user-typing");
339
+ socket.off("user-stopped-typing");
340
+ socket.off("room-role");
341
+ socket.off("kicked");
342
+ socket.off("muted");
343
+ socket.off("unmuted");
344
+ socket.off("room-participants");
345
+ socket.off("message-reaction-update");
346
+ socket.disconnect();
347
+ };
348
+ }, [roomId, nickname, userLimit, router, searchParams]);
349
+
350
+ const sendMessage = async () => {
351
+ if (!inputMessage.trim() || !myKeys.current) return;
352
+
353
+ if (typingTimeoutRef.current) {
354
+ clearTimeout(typingTimeoutRef.current);
355
+ }
356
+ socket.emit("typing-stop", { roomId });
357
+
358
+ try {
359
+ const aesKey = await generateSymKey();
360
+ const encryptedContent = await encryptSymMessage(aesKey, inputMessage);
361
+ const rawAesKey = await exportSymKey(aesKey);
362
+ const keysMap: Record<string, string> = {};
363
+
364
+ for (const [userId, pubKey] of otherUsersKeys.current.entries()) {
365
+ const encryptedAesKey = await encryptMessage(pubKey, rawAesKey);
366
+ keysMap[userId] = encryptedAesKey;
367
+ }
368
+
369
+ const messageId = crypto.randomUUID();
370
+
371
+ socket.emit("send-message", {
372
+ roomId,
373
+ payload: {
374
+ content: encryptedContent,
375
+ keys: keysMap,
376
+ },
377
+ senderId: socket.id,
378
+ messageId,
379
+ type: "text"
380
+ });
381
+
382
+ setMessages((prev) => [
383
+ ...prev,
384
+ {
385
+ id: messageId,
386
+ senderId: "me",
387
+ content: inputMessage,
388
+ timestamp: Date.now(),
389
+ status: "sent",
390
+ type: "text"
391
+ },
392
+ ]);
393
+
394
+ setInputMessage("");
395
+ } catch (err) {
396
+ console.error("Failed to send message:", err);
397
+ }
398
+ };
399
+
400
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
401
+ setInputMessage(e.target.value);
402
+ socket.emit("typing-start", { roomId });
403
+ if (typingTimeoutRef.current) {
404
+ clearTimeout(typingTimeoutRef.current);
405
+ }
406
+ typingTimeoutRef.current = setTimeout(() => {
407
+ socket.emit("typing-stop", { roomId });
408
+ }, 2000);
409
+ };
410
+
411
+ const handleEmojiClick = (emojiData: any) => {
412
+ const emoji = emojiData.emoji;
413
+ const input = inputRef.current;
414
+ if (input) {
415
+ const start = input.selectionStart || 0;
416
+ const end = input.selectionEnd || 0;
417
+ const newValue = inputMessage.substring(0, start) + emoji + inputMessage.substring(end);
418
+ setInputMessage(newValue);
419
+ setTimeout(() => {
420
+ input.focus();
421
+ input.setSelectionRange(start + emoji.length, start + emoji.length);
422
+ }, 0);
423
+ } else {
424
+ setInputMessage(inputMessage + emoji);
425
+ }
426
+ setShowEmojiPicker(false);
427
+ };
428
+
429
+ const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
430
+ const file = e.target.files?.[0];
431
+ if (!file || !myKeys.current) return;
432
+ if (file.size > 10 * 1024 * 1024) {
433
+ alert("File size must be less than 10MB");
434
+ return;
435
+ }
436
+ setIsUploading(true);
437
+ try {
438
+ const fileAesKey = await generateSymKey();
439
+ const fileBuffer = await file.arrayBuffer();
440
+ const encryptedFileContent = await encryptFile(fileAesKey, fileBuffer);
441
+ const encryptedBlob = new Blob([encryptedFileContent], { type: "application/octet-stream" });
442
+ const formData = new FormData();
443
+ formData.append("file", encryptedBlob, file.name);
444
+ const response = await fetch("/api/upload", { method: "POST", body: formData });
445
+ if (!response.ok) throw new Error("Upload failed");
446
+ const fileData = await response.json();
447
+ const rawFileAesKey = await exportSymKey(fileAesKey);
448
+ const keysMap: Record<string, string> = {};
449
+ for (const [userId, pubKey] of otherUsersKeys.current.entries()) {
450
+ const encryptedKey = await encryptMessage(pubKey, rawFileAesKey);
451
+ keysMap[userId] = encryptedKey;
452
+ }
453
+ if (myKeys.current && socket.id) {
454
+ const myEncryptedKey = await encryptMessage(myKeys.current.public, rawFileAesKey);
455
+ keysMap[socket.id] = myEncryptedKey;
456
+ }
457
+ const messageId = crypto.randomUUID();
458
+ socket.emit("send-message", {
459
+ roomId,
460
+ payload: {
461
+ content: "[FILE]",
462
+ keys: keysMap,
463
+ file: {
464
+ id: fileData.fileId,
465
+ name: file.name,
466
+ size: file.size,
467
+ mimeType: file.type,
468
+ }
469
+ },
470
+ senderId: socket.id,
471
+ messageId,
472
+ type: "file"
473
+ });
474
+ setMessages((prev) => [
475
+ ...prev,
476
+ {
477
+ id: messageId,
478
+ senderId: "me",
479
+ content: "[FILE]",
480
+ timestamp: Date.now(),
481
+ status: "sent",
482
+ type: "file",
483
+ file: {
484
+ id: fileData.fileId,
485
+ name: file.name,
486
+ size: file.size,
487
+ mimeType: file.type,
488
+ },
489
+ encryptedKey: socket.id ? keysMap[socket.id] : undefined
490
+ },
491
+ ]);
492
+ if (fileInputRef.current) fileInputRef.current.value = "";
493
+ } catch (error) {
494
+ console.error("File upload error:", error);
495
+ alert("Failed to upload file");
496
+ } finally {
497
+ setIsUploading(false);
498
+ }
499
+ };
500
+
501
+ const getTypingIndicator = () => {
502
+ if (typingUsers.size === 0) return null;
503
+ const typingNames = Array.from(typingUsers).map(socketId =>
504
+ nicknames.get(socketId) || `User ${socketId.slice(0, 4)}`
505
+ );
506
+ if (typingNames.length === 1) return `${typingNames[0]} is typing...`;
507
+ if (typingNames.length === 2) return `${typingNames[0]} and ${typingNames[1]} are typing...`;
508
+ return `${typingNames.length} people are typing...`;
509
+ };
510
+
511
+ const handleDownload = async (fileId: string, fileName: string, encryptedKey: string) => {
512
+ try {
513
+ const response = await fetch(`/api/files/${fileId}`);
514
+ if (!response.ok) throw new Error("Download failed");
515
+ const encryptedBlob = await response.blob();
516
+ const encryptedBuffer = await encryptedBlob.arrayBuffer();
517
+ if (!myKeys.current) return;
518
+ const aesKeyRaw = await decryptMessage(myKeys.current.private, encryptedKey);
519
+ const aesKey = await importSymKey(aesKeyRaw);
520
+ const decryptedBuffer = await decryptFile(aesKey, encryptedBuffer);
521
+ const blob = new Blob([decryptedBuffer]);
522
+ const url = URL.createObjectURL(blob);
523
+ const a = document.createElement("a");
524
+ a.href = url;
525
+ a.download = fileName;
526
+ document.body.appendChild(a);
527
+ a.click();
528
+ document.body.removeChild(a);
529
+ URL.revokeObjectURL(url);
530
+ } catch (error) {
531
+ console.error("Download error:", error);
532
+ alert("Failed to download file");
533
+ }
534
+ };
535
+
536
+ const handleReaction = (messageId: string, emoji: string) => {
537
+ if (!socket.id) return;
538
+
539
+ // Optimistic update
540
+ setMessages(prev => prev.map(msg => {
541
+ if (msg.id === messageId) {
542
+ const newReactions = { ...(msg.reactions || {}) };
543
+ const users = newReactions[emoji] || [];
544
+
545
+ // Check if user is toggling the same reaction
546
+ if (users.includes(socket.id!)) {
547
+ newReactions[emoji] = users.filter(id => id !== socket.id);
548
+ if (newReactions[emoji].length === 0) delete newReactions[emoji];
549
+ } else {
550
+ // Remove user from ALL other reactions first (Single reaction limit)
551
+ Object.keys(newReactions).forEach(key => {
552
+ const rUsers = newReactions[key] || [];
553
+ if (rUsers.includes(socket.id!)) {
554
+ newReactions[key] = rUsers.filter(id => id !== socket.id!);
555
+ if (newReactions[key].length === 0) delete newReactions[key];
556
+ }
557
+ });
558
+
559
+ // Add new reaction
560
+ newReactions[emoji] = [...users, socket.id!];
561
+ }
562
+ return { ...msg, reactions: newReactions };
563
+ }
564
+ return msg;
565
+ }));
566
+
567
+ socket.emit("message-reaction", {
568
+ roomId,
569
+ messageId,
570
+ reaction: emoji,
571
+ senderId: socket.id
572
+ });
573
+ setActiveReactionMessageId(null);
574
+ };
575
+
576
+ const COMMON_REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "😡"];
577
+
578
+ return (
579
+ <div className="flex flex-col h-screen max-w-5xl mx-auto p-2 md:p-4">
580
+ <Card className="flex-1 !py-0 !px-0 flex flex-col bg-background/50 backdrop-blur-sm border-border/50 shadow-2xl overflow-hidden">
581
+ <CardHeader className="border-b border-border/40 py-4 px-6 flex flex-row items-center justify-between bg-background/40 backdrop-blur-md sticky top-0 z-10">
582
+ <div className="flex items-center gap-3">
583
+ <div className="bg-gradient-to-br from-emerald-500 to-cyan-500 p-2.5 rounded-xl shadow-lg shadow-emerald-500/20">
584
+ <Lock className="w-5 h-5 text-white" />
585
+ </div>
586
+ <div>
587
+ <CardTitle className="text-lg flex items-center gap-2 font-bold tracking-tight">
588
+ Room: <span className="font-mono text-emerald-500">{roomId}</span>
589
+ </CardTitle>
590
+ <p className="text-xs text-muted-foreground flex items-center gap-1">
591
+ <span className={`w-1.5 h-1.5 rounded-full ${isConnected ? "bg-emerald-500 animate-pulse" : "bg-red-500"}`} />
592
+ {isConnected ? "Encrypted Connection Active" : "Disconnected"}
593
+ </p>
594
+ </div>
595
+ </div>
596
+ <div className="flex items-center gap-2">
597
+ <div className="flex items-center bg-muted/50 rounded-full p-1 border border-border/50">
598
+ <Button variant="ghost" size="sm" onClick={() => setShowParticipantsModal(true)} className="text-muted-foreground hover:text-foreground rounded-full h-8 px-3">
599
+ <Users className="w-4 h-4 mr-2" />
600
+ <span>{participantCount}</span>
601
+ </Button>
602
+ {isCreator && (
603
+ <Button variant="ghost" size="icon" onClick={() => setShowSettingsModal(true)} className="text-muted-foreground hover:text-foreground rounded-full h-8 w-8">
604
+ <Settings className="w-4 h-4" />
605
+ </Button>
606
+ )}
607
+ </div>
608
+ <div className="h-6 w-px bg-border/50 mx-1" />
609
+ <Button
610
+ variant="ghost"
611
+ size="icon"
612
+ onClick={joinCall}
613
+ className="text-muted-foreground hover:text-emerald-500 hover:bg-emerald-500/10 rounded-full transition-all"
614
+ title="Start Call"
615
+ >
616
+ <Phone className="w-5 h-5" />
617
+ </Button>
618
+ <ThemeToggle />
619
+ </div>
620
+ </CardHeader>
621
+
622
+ {localStream && (
623
+ <CallInterface
624
+ localStream={localStream}
625
+ remoteStreams={remoteStreams}
626
+ onLeave={leaveCall}
627
+ nicknames={nicknames}
628
+ />
629
+ )}
630
+
631
+ <CardContent className="flex-1 overflow-hidden p-0 relative">
632
+ <ScrollArea className="h-full px-4 py-2">
633
+ <div className="flex flex-col gap-4">
634
+ {messages.map((msg) => {
635
+ const isMe = msg.senderId === "me";
636
+ const senderName = isMe ? "Me" : (nicknames.get(msg.senderId) || `User ${msg.senderId.slice(0, 4)}`);
637
+
638
+ return (
639
+ <div
640
+ key={msg.id}
641
+ className={`flex ${isMe ? "justify-end" : "justify-start"} animate-fade-in-up`}
642
+ >
643
+ {!isMe && (
644
+ <div className="flex flex-col items-start mr-2">
645
+ <div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-indigo-500 flex items-center justify-center text-white text-xs font-bold shadow-sm">
646
+ {senderName.charAt(0).toUpperCase()}
647
+ </div>
648
+ </div>
649
+ )}
650
+ <div className="relative group max-w-[80%]">
651
+ {!isMe && (
652
+ <span className="text-[10px] text-muted-foreground ml-1 mb-1 block">
653
+ {senderName}
654
+ </span>
655
+ )}
656
+ <div
657
+ className={`rounded-2xl px-4 py-3 relative shadow-md transition-all ${isMe
658
+ ? "bg-gradient-to-br from-emerald-500 to-emerald-600 text-white rounded-tr-sm"
659
+ : "bg-card border border-border/50 text-foreground rounded-tl-sm hover:border-emerald-500/30"
660
+ }`}
661
+ >
662
+ {/* Reaction Button (Visible on Hover) */}
663
+ <button
664
+ onClick={() => setActiveReactionMessageId(activeReactionMessageId === msg.id ? null : msg.id)}
665
+ className={`absolute ${isMe ? "-left-8" : "-right-8"} top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-all p-1.5 hover:bg-background/20 rounded-full backdrop-blur-sm`}
666
+ >
667
+ <Smile className="w-4 h-4 text-muted-foreground" />
668
+ </button>
669
+
670
+ {/* Reaction Picker Popover */}
671
+ {activeReactionMessageId === msg.id && (
672
+ <div className={`absolute ${isMe ? "right-full mr-2" : "left-full ml-2"} top-1/2 -translate-y-1/2 bg-popover/90 backdrop-blur-md border border-border rounded-full shadow-xl p-1.5 flex gap-1 z-50 animate-fade-in`}>
673
+ {COMMON_REACTIONS.map(emoji => (
674
+ <button
675
+ key={emoji}
676
+ onClick={() => handleReaction(msg.id, emoji)}
677
+ className="hover:bg-muted p-1.5 rounded-full text-lg transition-transform hover:scale-110"
678
+ >
679
+ {emoji}
680
+ </button>
681
+ ))}
682
+ </div>
683
+ )}
684
+
685
+ {msg.type === "file" && msg.file ? (
686
+ <div className="flex items-center gap-3">
687
+ <div className={`p-2.5 rounded-xl ${isMe ? "bg-black/20" : "bg-muted"}`}>
688
+ <FileIcon className="w-6 h-6" />
689
+ </div>
690
+ <div className="flex flex-col overflow-hidden">
691
+ <span className="text-sm font-medium truncate max-w-[150px]">{msg.file.name}</span>
692
+ <span className="text-xs opacity-70">{(msg.file.size / 1024).toFixed(1)} KB</span>
693
+ </div>
694
+ {!isMe && msg.encryptedKey && (
695
+ <Button
696
+ variant="ghost"
697
+ size="icon"
698
+ className="h-8 w-8 hover:bg-black/10 rounded-full"
699
+ onClick={() => handleDownload(msg.file!.id, msg.file!.name, msg.encryptedKey!)}
700
+ >
701
+ <Download className="w-4 h-4" />
702
+ </Button>
703
+ )}
704
+ </div>
705
+ ) : (
706
+ <div className={`markdown-content text-sm leading-relaxed break-words break-all whitespace-pre-wrap ${isMe ? 'text-white' : 'text-foreground'}`}>
707
+ <ReactMarkdown
708
+ remarkPlugins={[remarkGfm]}
709
+ components={{
710
+ p: ({ node, ...props }) => <p className="mb-1 last:mb-0" {...props} />,
711
+ a: ({ node, ...props }) => <a className="underline hover:opacity-80 transition-opacity" target="_blank" rel="noopener noreferrer" {...props} />,
712
+ code: ({ node, className, children, ...props }: any) => {
713
+ const match = /language-(\w+)/.exec(className || '')
714
+ return !match ? (
715
+ <code className={`rounded px-1 py-0.5 font-mono text-xs ${isMe ? "bg-black/20" : "bg-muted"}`} {...props}>
716
+ {children}
717
+ </code>
718
+ ) : (
719
+ <code className={`block rounded p-2 font-mono text-xs overflow-x-auto my-1 ${isMe ? "bg-black/20" : "bg-muted"}`} {...props}>
720
+ {children}
721
+ </code>
722
+ )
723
+ },
724
+ ul: ({ node, ...props }) => <ul className="list-disc list-inside mb-1" {...props} />,
725
+ ol: ({ node, ...props }) => <ol className="list-decimal list-inside mb-1" {...props} />,
726
+ blockquote: ({ node, ...props }) => <blockquote className="border-l-2 border-current pl-2 italic my-1 opacity-80" {...props} />,
727
+ }}
728
+ >
729
+ {msg.content}
730
+ </ReactMarkdown>
731
+ </div>
732
+ )}
733
+
734
+ {/* Reactions Display */}
735
+ {msg.reactions && Object.keys(msg.reactions).length > 0 && (
736
+ <div className={`absolute -bottom-3 ${isMe ? 'right-0' : 'left-0'} flex items-center gap-0.5 bg-background/80 backdrop-blur-md border border-border/50 rounded-full px-1.5 py-0.5 shadow-sm z-10`}>
737
+ {Object.entries(msg.reactions).map(([emoji, users]) => (
738
+ <button
739
+ key={emoji}
740
+ onClick={() => handleReaction(msg.id, emoji)}
741
+ className={`text-[10px] min-w-[20px] h-[16px] flex items-center justify-center rounded-full transition-all hover:scale-110 ${users.includes(socket.id!) ? 'bg-emerald-500/20 text-emerald-500' : 'hover:bg-muted/50'}`}
742
+ title={users.map(u => nicknames.get(u) || u).join(", ")}
743
+ >
744
+ <span className="mr-0.5">{emoji}</span>
745
+ <span className="font-medium opacity-80">{users.length}</span>
746
+ </button>
747
+ ))}
748
+ </div>
749
+ )}
750
+ <span className="text-[10px] opacity-60 block mt-1 flex items-center justify-end gap-1">
751
+ {new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
752
+ {isMe && (
753
+ <span>
754
+ {msg.status === "sending" && <Check className="w-3 h-3 opacity-70" />}
755
+ {msg.status === "sent" && <Check className="w-3 h-3 opacity-70" />}
756
+ {msg.status === "delivered" && <CheckCheck className="w-3 h-3 opacity-70" />}
757
+ {msg.status === "read" && <CheckCheck className="w-3 h-3 text-blue-200" />}
758
+ </span>
759
+ )}
760
+ </span>
761
+ </div>
762
+ </div>
763
+ </div>
764
+ );
765
+ })}
766
+ <div ref={messagesEndRef} />
767
+ </div>
768
+ </ScrollArea>
769
+ </CardContent>
770
+
771
+ <div className="p-3 border-t border-border/40 bg-background/40 backdrop-blur-md">
772
+ {typingUsers.size > 0 && (
773
+ <div className="text-xs text-emerald-500 mb-2 italic animate-pulse flex items-center gap-1 px-2">
774
+ <span className="w-1 h-1 rounded-full bg-emerald-500 animate-bounce" />
775
+ <span className="w-1 h-1 rounded-full bg-emerald-500 animate-bounce delay-75" />
776
+ <span className="w-1 h-1 rounded-full bg-emerald-500 animate-bounce delay-150" />
777
+ {getTypingIndicator()}
778
+ </div>
779
+ )}
780
+ <div className="relative">
781
+ {showEmojiPicker && (
782
+ <div className="absolute bottom-full right-0 mb-4 z-50 animate-fade-in-up">
783
+ <EmojiPicker
784
+ onEmojiClick={handleEmojiClick}
785
+ theme={"dark" as any}
786
+ lazyLoadEmojis={true}
787
+ style={{ boxShadow: '0 10px 40px rgba(0,0,0,0.5)', border: '1px solid var(--border)', backgroundColor: '#0f172a' }}
788
+ />
789
+ </div>
790
+ )}
791
+ <form
792
+ onSubmit={(e) => {
793
+ e.preventDefault();
794
+ sendMessage();
795
+ }}
796
+ className="relative flex items-center gap-2 bg-muted/30 rounded-3xl border border-border/50 focus-within:border-emerald-500/50 focus-within:ring-1 focus-within:ring-emerald-500/20 transition-all p-1.5"
797
+ >
798
+ <input
799
+ type="file"
800
+ ref={fileInputRef}
801
+ onChange={handleFileSelect}
802
+ className="hidden"
803
+ />
804
+
805
+ <Button
806
+ type="button"
807
+ size="icon"
808
+ variant="ghost"
809
+ onClick={() => fileInputRef.current?.click()}
810
+ className="text-muted-foreground hover:text-emerald-500 hover:bg-emerald-500/10 rounded-full h-9 w-9 transition-colors shrink-0"
811
+ disabled={isUploading}
812
+ >
813
+ {isUploading ? (
814
+ <div className="w-4 h-4 border-2 border-muted-foreground border-t-emerald-500 rounded-full animate-spin" />
815
+ ) : (
816
+ <Paperclip className="w-5 h-5" />
817
+ )}
818
+ </Button>
819
+
820
+ <Input
821
+ ref={inputRef}
822
+ value={inputMessage}
823
+ onChange={handleInputChange}
824
+ maxLength={1000}
825
+ placeholder="Type a secure message..."
826
+ className="flex-1 bg-transparent border-none text-foreground focus-visible:ring-0 px-2 py-3 h-auto shadow-none placeholder:text-muted-foreground/70"
827
+ />
828
+
829
+ {inputMessage.length > 0 && (
830
+ <span className={`text-[10px] font-mono mr-2 ${inputMessage.length > 900 ? "text-red-500 font-bold" : "text-muted-foreground/50"}`}>
831
+ {inputMessage.length}/1000
832
+ </span>
833
+ )}
834
+
835
+ <div className="flex items-center gap-1 pr-1">
836
+ <Button
837
+ type="button"
838
+ size="icon"
839
+ variant="ghost"
840
+ onClick={() => setShowEmojiPicker(!showEmojiPicker)}
841
+ className={`h-9 w-9 rounded-full transition-colors ${showEmojiPicker ? 'text-emerald-500 bg-emerald-500/10' : 'text-muted-foreground hover:text-emerald-500 hover:bg-emerald-500/10'}`}
842
+ >
843
+ <Smile className="w-5 h-5" />
844
+ </Button>
845
+
846
+ <Button
847
+ type="submit"
848
+ size="icon"
849
+ className="bg-emerald-600 hover:bg-emerald-700 text-white rounded-full h-9 w-9 shadow-lg shadow-emerald-500/20 transition-transform hover:scale-105 shrink-0"
850
+ disabled={!isConnected}
851
+ >
852
+ <Send className="w-4 h-4 ml-0.5" />
853
+ </Button>
854
+ </div>
855
+ </form>
856
+ </div>
857
+ </div>
858
+ </Card>
859
+
860
+ {/* Password Modal */}
861
+ {
862
+ showPasswordModal && (
863
+ <div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50">
864
+ <Card className="w-full max-w-sm bg-slate-900 border-slate-800">
865
+ <CardHeader>
866
+ <CardTitle className="text-white">Password Required</CardTitle>
867
+ </CardHeader>
868
+ <CardContent className="space-y-4">
869
+ <Input
870
+ type="password"
871
+ placeholder="Enter room password"
872
+ value={passwordInput}
873
+ onChange={(e) => setPasswordInput(e.target.value)}
874
+ className="bg-slate-950 border-slate-800 text-white"
875
+ />
876
+ <Button
877
+ onClick={() => {
878
+ socket.emit("join-room", { roomId, nickname, userLimit, password: passwordInput });
879
+ setShowPasswordModal(false);
880
+ }}
881
+ className="w-full bg-emerald-600 hover:bg-emerald-700"
882
+ >
883
+ Join Room
884
+ </Button>
885
+ </CardContent>
886
+ </Card>
887
+ </div>
888
+ )
889
+ }
890
+
891
+ {/* Settings Modal */}
892
+ {
893
+ showSettingsModal && (
894
+ <div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50" onClick={() => setShowSettingsModal(false)}>
895
+ <Card className="w-full max-w-md bg-slate-900 border-slate-800" onClick={e => e.stopPropagation()}>
896
+ <CardHeader className="flex flex-row items-center justify-between">
897
+ <CardTitle className="text-white">Room Settings</CardTitle>
898
+ <Button variant="ghost" size="icon" onClick={() => setShowSettingsModal(false)}>
899
+ <X className="w-4 h-4 text-slate-400" />
900
+ </Button>
901
+ </CardHeader>
902
+ <CardContent className="space-y-4">
903
+ <div className="space-y-2">
904
+ <label className="text-sm text-slate-400">User Limit</label>
905
+ <Input
906
+ type="number"
907
+ placeholder="Max users"
908
+ value={newLimit}
909
+ onChange={(e) => setNewLimit(e.target.value)}
910
+ className="bg-slate-950 border-slate-800 text-white"
911
+ />
912
+ </div>
913
+ <div className="space-y-2">
914
+ <label className="text-sm text-slate-400">Update Password (leave empty to remove)</label>
915
+ <Input
916
+ type="password"
917
+ placeholder="New password"
918
+ value={newPassword}
919
+ onChange={(e) => setNewPassword(e.target.value)}
920
+ className="bg-slate-950 border-slate-800 text-white"
921
+ />
922
+ </div>
923
+ <Button
924
+ onClick={() => {
925
+ socket.emit("update-room-settings", {
926
+ roomId,
927
+ limit: newLimit ? parseInt(newLimit) : undefined,
928
+ password: newPassword
929
+ });
930
+ setShowSettingsModal(false);
931
+ setNewPassword("");
932
+ setNewLimit("");
933
+ }}
934
+ className="w-full bg-emerald-600 hover:bg-emerald-700"
935
+ >
936
+ Save Changes
937
+ </Button>
938
+ </CardContent>
939
+ </Card>
940
+ </div>
941
+ )
942
+ }
943
+
944
+ {/* Participants Modal */}
945
+ {
946
+ showParticipantsModal && (
947
+ <div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50" onClick={() => setShowParticipantsModal(false)}>
948
+ <Card className="w-full max-w-md bg-slate-900 border-slate-800 max-h-[80vh] flex flex-col" onClick={e => e.stopPropagation()}>
949
+ <CardHeader className="flex flex-row items-center justify-between border-b border-slate-800">
950
+ <CardTitle className="text-white">Participants ({participants.length})</CardTitle>
951
+ <Button variant="ghost" size="icon" onClick={() => setShowParticipantsModal(false)}>
952
+ <X className="w-4 h-4 text-slate-400" />
953
+ </Button>
954
+ </CardHeader>
955
+ <CardContent className="p-0 overflow-hidden flex-1">
956
+ <ScrollArea className="h-full max-h-[60vh]">
957
+ <div className="p-4 space-y-2">
958
+ {participants.map((p) => (
959
+ <div key={p.socketId} className="flex items-center justify-between p-2 rounded bg-slate-950/50 border border-slate-800">
960
+ <div className="flex items-center gap-3">
961
+ <div className="w-8 h-8 rounded-full bg-emerald-500/10 flex items-center justify-center">
962
+ <User className="w-4 h-4 text-emerald-400" />
963
+ </div>
964
+ <div>
965
+ <p className="text-sm font-medium text-white">
966
+ {p.nickname || "Anonymous"}
967
+ {p.socketId === socket.id && " (You)"}
968
+ </p>
969
+ <p className="text-xs text-slate-500">ID: {p.userId.slice(0, 8)}...</p>
970
+ </div>
971
+ </div>
972
+ {isCreator && p.socketId !== socket.id && (
973
+ <div className="flex items-center gap-1">
974
+ {p.isMuted ? (
975
+ <Button
976
+ variant="ghost"
977
+ size="icon"
978
+ className="h-8 w-8 text-red-400 hover:text-red-300"
979
+ onClick={() => socket.emit("unmute-user", { roomId, targetUserId: p.userId })}
980
+ title="Unmute"
981
+ >
982
+ <MicOff className="w-4 h-4" />
983
+ </Button>
984
+ ) : (
985
+ <Button
986
+ variant="ghost"
987
+ size="icon"
988
+ className="h-8 w-8 text-slate-400 hover:text-yellow-400"
989
+ onClick={() => socket.emit("mute-user", { roomId, targetUserId: p.userId })}
990
+ title="Mute"
991
+ >
992
+ <Mic className="w-4 h-4" />
993
+ </Button>
994
+ )}
995
+ <Button
996
+ variant="ghost"
997
+ size="icon"
998
+ className="h-8 w-8 text-slate-400 hover:text-red-400"
999
+ onClick={() => socket.emit("kick-user", { roomId, targetUserId: p.userId })}
1000
+ title="Kick"
1001
+ >
1002
+ <UserX className="w-4 h-4" />
1003
+ </Button>
1004
+ </div>
1005
+ )}
1006
+ </div>
1007
+ ))}
1008
+ </div>
1009
+ </ScrollArea>
1010
+ </CardContent>
1011
+ </Card>
1012
+ </div>
1013
+ )
1014
+ }
1015
+ </div >
1016
+ );
1017
+ }
components/providers.tsx ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { SessionProvider } from "next-auth/react";
4
+
5
+ export function Providers({ children }: { children: React.ReactNode }) {
6
+ return <SessionProvider>{children}</SessionProvider>;
7
+ }
components/theme-provider.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ThemeProvider as NextThemesProvider } from "next-themes"
5
+
6
+ export function ThemeProvider({
7
+ children,
8
+ ...props
9
+ }: React.ComponentProps<typeof NextThemesProvider>) {
10
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>
11
+ }
components/theme-toggle.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Moon, Sun } from "lucide-react"
5
+ import { useTheme } from "next-themes"
6
+
7
+ import { Button } from "@/components/ui/button"
8
+
9
+ export function ThemeToggle() {
10
+ const { setTheme, theme } = useTheme()
11
+
12
+ return (
13
+ <Button
14
+ variant="ghost"
15
+ size="icon"
16
+ onClick={() => setTheme(theme === "light" ? "dark" : "light")}
17
+ className="rounded-full"
18
+ >
19
+ <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
20
+ <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
21
+ <span className="sr-only">Toggle theme</span>
22
+ </Button>
23
+ )
24
+ }
components/ui/button.tsx ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost:
20
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
+ icon: "size-9",
28
+ "icon-sm": "size-8",
29
+ "icon-lg": "size-10",
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: "default",
34
+ size: "default",
35
+ },
36
+ }
37
+ )
38
+
39
+ function Button({
40
+ className,
41
+ variant,
42
+ size,
43
+ asChild = false,
44
+ ...props
45
+ }: React.ComponentProps<"button"> &
46
+ VariantProps<typeof buttonVariants> & {
47
+ asChild?: boolean
48
+ }) {
49
+ const Comp = asChild ? Slot : "button"
50
+
51
+ return (
52
+ <Comp
53
+ data-slot="button"
54
+ className={cn(buttonVariants({ variant, size, className }))}
55
+ {...props}
56
+ />
57
+ )
58
+ }
59
+
60
+ export { Button, buttonVariants }
components/ui/card.tsx ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6", className)}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardHeader,
87
+ CardFooter,
88
+ CardTitle,
89
+ CardAction,
90
+ CardDescription,
91
+ CardContent,
92
+ }
components/ui/input.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
13
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ export { Input }
components/ui/scroll-area.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function ScrollArea({
9
+ className,
10
+ children,
11
+ ...props
12
+ }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
13
+ return (
14
+ <ScrollAreaPrimitive.Root
15
+ data-slot="scroll-area"
16
+ className={cn("relative", className)}
17
+ {...props}
18
+ >
19
+ <ScrollAreaPrimitive.Viewport
20
+ data-slot="scroll-area-viewport"
21
+ className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
22
+ >
23
+ {children}
24
+ </ScrollAreaPrimitive.Viewport>
25
+ <ScrollBar />
26
+ <ScrollAreaPrimitive.Corner />
27
+ </ScrollAreaPrimitive.Root>
28
+ )
29
+ }
30
+
31
+ function ScrollBar({
32
+ className,
33
+ orientation = "vertical",
34
+ ...props
35
+ }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
36
+ return (
37
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
38
+ data-slot="scroll-area-scrollbar"
39
+ orientation={orientation}
40
+ className={cn(
41
+ "flex touch-none p-px transition-colors select-none",
42
+ orientation === "vertical" &&
43
+ "h-full w-2.5 border-l border-l-transparent",
44
+ orientation === "horizontal" &&
45
+ "h-2.5 flex-col border-t border-t-transparent",
46
+ className
47
+ )}
48
+ {...props}
49
+ >
50
+ <ScrollAreaPrimitive.ScrollAreaThumb
51
+ data-slot="scroll-area-thumb"
52
+ className="bg-border relative flex-1 rounded-full"
53
+ />
54
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
55
+ )
56
+ }
57
+
58
+ export { ScrollArea, ScrollBar }
docs/details.md ADDED
@@ -0,0 +1,1362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ Project Synopsis
4
+
5
+ TITLE OF THE PROJECT:
6
+
7
+ End-to-End Encrypted Chat Room Web
8
+
9
+ Application
10
+
11
+ CONTENTS OF SYNOPSIS
12
+
13
+
14
+
15
+ &nbsp; Title of the Project........................................................................................................................................................... Catalog
16
+
17
+ &nbsp; Introduction and Objectives of the Project.........................................................................................................
18
+
19
+ &nbsp; 2.1 Introduction...........................................................................................................................................................
20
+
21
+ &nbsp; 2.2 Objectives...............................................................................................................................................................
22
+
23
+ &nbsp; Project Category...............................................................................................................................................................
24
+
25
+ &nbsp; Analysis................................................................................................................................................................................
26
+
27
+ &nbsp; 4.1 User Flow Diagram (Activity Diagram)....................................................................................................
28
+
29
+ &nbsp; 4.2 Context Level DFD (Level 0)..........................................................................................................................
30
+
31
+ &nbsp; 4.3 Level 1 DFD............................................................................................................................................................
32
+
33
+ &nbsp; 4.4 Level 2 DFDs.......................................................................................................................................................
34
+
35
+ &nbsp; 4.4.1 Level 2 DFD for Process 1.0: Room Creation \& Access Control.............................................
36
+
37
+ &nbsp; 4.4.2 Level 2 DFD for Message Exchange (Involving P3.0 and P4.0)..............................................
38
+
39
+ &nbsp; 4.5 Entity Relationship Diagram (ERD) / Database Design (for Temporary Room Data)...
40
+
41
+ &nbsp; Complete System Structure.....................................................................................................................................
42
+
43
+ &nbsp; 5.1 Number of Modules and their Description..........................................................................................
44
+
45
+ &nbsp; 5.2 Data Structures..................................................................................................................................................
46
+
47
+ &nbsp; 5.3 Process Logic of Each Module....................................................................................................................
48
+
49
+ &nbsp; 5.4 Testing Process to be Used..........................................................................................................................
50
+
51
+ &nbsp; 5.5 Reports Generation.........................................................................................................................................
52
+
53
+ &nbsp; Tools / Platform, Hardware and Software Requirement Specifications............................................
54
+
55
+ &nbsp; 6.1 Software Requirements................................................................................................................................
56
+
57
+ &nbsp; 6.2 Hardware Requirements..............................................................................................................................
58
+
59
+ &nbsp; Industry/Client Affiliation........................................................................................................................................
60
+
61
+ &nbsp; Future Scope and Further Enhancement of the Project.............................................................................
62
+
63
+ &nbsp; Limitations of the Project (Initial Version)......................................................................................................
64
+
65
+ &nbsp; Security and Validation Checks...........................................................................................................................
66
+
67
+ &nbsp; Bibliography (References).....................................................................................................................................
68
+
69
+
70
+
71
+ 1\. Title of the Project........................................................................................................................................................... Catalog
72
+
73
+
74
+
75
+ End-to-End Encrypted Chat Room Web Application
76
+
77
+ 2\. Introduction and Objectives of the Project.........................................................................................................
78
+
79
+ 2.1 Introduction...........................................................................................................................................................
80
+
81
+
82
+
83
+ Today’s digital communication landscape is dominated by concerns about privacy and data
84
+
85
+ security. Most popular messaging platforms store our conversations on their servers, making
86
+
87
+ them vulnerable to data breaches, government surveillance, and unauthorized access. This
88
+
89
+ creates a real need for truly private communication tools.
90
+
91
+
92
+
93
+ My project addresses this problem by developing a web-basedchat application that prioritizes
94
+
95
+ user privacy through three key features:
96
+
97
+
98
+
99
+ ► End-to-End Encryption (E2EE): Messages are encrypted on the sender’s device and
100
+
101
+ can only be decrypted by the intended recipients. Even the server hosting the
102
+
103
+ application cannot read the message content - it simply actsas a relay for encrypted
104
+
105
+ data..
106
+
107
+
108
+
109
+ ► Ephemerality (Temporary Nature): Unlike traditional chat apps that store your
110
+
111
+ conversation history forever, this application is designed to be ephemeral. Messages
112
+
113
+ aren’t saved anywhere - not on servers, not in databases. When you close the chat room,
114
+
115
+ everything disappears.
116
+
117
+
118
+
119
+ ► Room-Based, Code-Driven Access: Instead of requiring user accounts or friend lists,
120
+
121
+ people can create temporary chat rooms with unique access codes. Share the code with
122
+
123
+ whoever you want to chat with, and they can join instantly.
124
+
125
+
126
+
127
+ The technical foundation includes Next.js and Tailwind CSSfor a modern, responsive user
128
+
129
+ interface, Socket.io for real-time messaging capabilities, and MongoDB for managing temporary
130
+
131
+ room information (but never storing actual messages). The goal is creating a platform where
132
+
133
+ people can have genuinely private conversations without worrying about their data being
134
+
135
+ stored or accessed by others.
136
+
137
+ 2.2 Objectives...............................................................................................................................................................
138
+
139
+
140
+
141
+ The principal objectives for this project are:
142
+
143
+
144
+
145
+ &nbsp; Develop a Secure Chat Platform: Create a web application where users can make
146
+
147
+ &nbsp; temporary chat rooms using unique access codes, without needing to create accounts or
148
+
149
+ &nbsp; provide personal information.
150
+
151
+
152
+
153
+ &nbsp; Implement Strong E2EE: Use proven cryptographic methods to ensure that only the
154
+
155
+ &nbsp; people in a conversation can read the messages. This includes secure key generation
156
+
157
+ &nbsp; and exchange between users.
158
+
159
+ &nbsp; Guarantee Message Ephemerality: To design the system so that chat messages are not
160
+
161
+ &nbsp; stored persistently. Message history will be volatile, existing only for the duration of an
162
+
163
+ &nbsp; active chat room session on the client-side.
164
+
165
+ &nbsp; Enable Real-Time Interaction: Use WebSocket technology through Socket.io to make
166
+
167
+ &nbsp; sure messages are delivered instantly between users in the same chat room.
168
+
169
+ &nbsp; Secure Room Access Control: To implement a system for generating unique, non-
170
+
171
+ &nbsp; guessable room codes and to allow controlled entry based on these codes, including
172
+
173
+ &nbsp; options for setting user limits per room.
174
+
175
+ &nbsp; Create an Intuitive User Interface: To build a user-friendly, responsive frontend that
176
+
177
+ &nbsp; simplifies the process of room creation, joining, and secure messaging.
178
+
179
+ &nbsp; Minimize Data Retention: To ensure the backend system only manages essential,
180
+
181
+ &nbsp; transient session data for active rooms (e.g., room IDs, participant session identifiers for
182
+
183
+ &nbsp; routing), without storing E2EE private keys or any decrypted message content.
184
+
185
+ &nbsp; Implement Effective Chat Destruction: When a chat room ends or everyone leaves,
186
+
187
+ &nbsp; automatically delete all related temporary data from the server and clear message
188
+
189
+ &nbsp; displays from users’ browsers.
190
+
191
+
192
+
193
+ 3\. Project Category...............................................................................................................................................................
194
+
195
+
196
+
197
+ This project spans multiple areas of computer science and software development:
198
+
199
+
200
+
201
+ ► Networking Application: It fundamentally relies on network protocols for real-time,
202
+
203
+ multi-user communication, primarily leveraging WebSockets through Socket.io.
204
+
205
+
206
+
207
+ ► Web Application: The user interface and core functionality will be deliveredas a web-
208
+
209
+ based application, accessible via standard browsers, using Next.js for the frontend and
210
+
211
+ Node.js for the backend.
212
+
213
+
214
+
215
+ ► Security Application: Privacy and security are central to the project, involving
216
+
217
+ cryptographic techniques like end-to-end encryption and principles of data
218
+
219
+ minimization and temporary storage.
220
+
221
+
222
+
223
+ ► Real-Time Systems: The chat functionality requires immediate message processing
224
+
225
+ and delivery to create a smooth conversation experience forusers.
226
+
227
+
228
+
229
+ While the project uses MongoDB as a database, it’s specifically for temporary session
230
+
231
+ management rather than permanent data storage, which distinguishes it from typical RDBMS-
232
+
233
+ centric or data-intensive applications.
234
+
235
+ 4\. Analysis................................................................................................................................................................................
236
+
237
+
238
+
239
+ This section outlines the system’s operational flow, data interactions, and structural design
240
+
241
+ through various diagrams.
242
+
243
+ 4.1 User Flow Diagram (Activity Diagram)....................................................................................................
244
+
245
+
246
+
247
+ This diagram depicts the typical sequence of actions and decisions a user makes while
248
+
249
+ interacting with the application, from creating/joining a room to participating in a chat and
250
+
251
+ exiting.
252
+
253
+
254
+
255
+ Description of User Flow Diagram: The diagram illustrates the user journey: one user
256
+
257
+ (Creator) initiates a room, obtains a unique code, and shares it. Another user (Joiner) uses this
258
+
259
+ code to enter. Inside the room, a secure key exchange establishes E2EE. Encrypted messages
260
+
261
+ are then exchanged, relayed by the server but only readable by clients. Upon exit, local data is
262
+
263
+ cleared, and the server eventually purges room metadata if all users depart, ensuring
264
+
265
+ ephemerality.
266
+
267
+ 4.2 Context Level DFD (Level 0)..........................................................................................................................
268
+
269
+
270
+
271
+ This diagram provides a high-level overview of the system asa single process, showing its
272
+
273
+ inputs from and outputs to the external entity (User).
274
+
275
+
276
+
277
+ Description of Context Level DFD: Users interact with the “ E2E Chat Room System” by
278
+
279
+ providing inputs like room creation requests, room joiningrequests, and message content
280
+
281
+ (which gets encrypted before leaving their device). The system responds with outputs
282
+
283
+ including room access confirmation, encrypted messages from other users, and status
284
+
285
+ notifications. The main purpose is facilitating secure, temporary communication between users.
286
+
287
+ 4.3 Level 1 DFD............................................................................................................................................................
288
+
289
+
290
+
291
+ This diagram decomposes the system into its major functional processes, data stores, and the
292
+
293
+ data flows between them.
294
+
295
+
296
+
297
+ Description of Level 1 DFD:
298
+
299
+
300
+
301
+ ► P1.0 Room Creation \& Access Control: Manages requests to create new rooms,
302
+
303
+ validates attempts to join existing rooms, and controls access based on room codes and
304
+
305
+ user limits. Interacts withD1to store and retrieve room status.
306
+
307
+ ► P2.0 User Session Mgt. \& Signaling: Handles individual user connections to rooms,
308
+
309
+ manages their session lifecycle (join/leave), and crucially, facilitates the exchange of
310
+
311
+ signaling messages required for clients to perform E2EE keyestablishment.
312
+
313
+ ► P3.0 Client-Side E2E Encryption/Decryption: This process resides entirely on the
314
+
315
+ client’s device. It encrypts outgoing messages before transmission and decrypts
316
+
317
+ incoming messages after reception, using keys unknown to the server.
318
+
319
+ ► P4.0 Server-Side Real-time Encrypted Message Relay: The backend component that
320
+
321
+ receives encrypted messages from one user and forwards themto other authenticated
322
+
323
+ users in the same room. It can’t read the message content.
324
+
325
+ ► D1: Temporary Active Room Metadata Store: A MongoDB database that holds
326
+
327
+ temporary information about active rooms (like room codes,user limits, and lists of
328
+
329
+ current participants). This data gets deleted when rooms become inactive.
330
+
331
+
332
+
333
+ 4.4 Level 2 DFDs.......................................................................................................................................................
334
+
335
+
336
+
337
+ These diagrams provide a more detailed breakdown of selected processes from the Level 1
338
+
339
+ DFD.
340
+
341
+ 4.4.1 Level 2 DFD for Process 1.0: Room Creation \& Access Control.............................................
342
+
343
+ 4.4.2 Level 2 DFD for Message Exchange (Involving P3.0 and P4.0)..............................................
344
+
345
+
346
+
347
+ This diagram details the flow of a message from sender to receiver, highlighting client-side
348
+
349
+ encryption/decryption and server-side relay.
350
+
351
+ 4.5 Entity Relationship Diagram (ERD) / Database Design (for Temporary Room Data)...
352
+
353
+
354
+
355
+ This ERD conceptualizes the entities and relationships forthe temporary metadata stored by
356
+
357
+ the server (e.g., in MongoDB) to manage active chat room sessions. No message content is
358
+
359
+ stored.
360
+
361
+
362
+
363
+ Conceptual MongoDB Document Structure (for anActiveRoomscollection):
364
+
365
+
366
+
367
+ This example illustrates how an active room’s metadata might be structured in a MongoDB
368
+
369
+ document.
370
+
371
+
372
+
373
+ {
374
+
375
+ "\_id": "", // Auto-generated by MongoDB
376
+
377
+ "roomCode": "A7B3C9", // Unique , application-generated room identifier
378
+
379
+ "userLimit": 25 ,
380
+
381
+ "participantSessions": \[ // Array of active participant session details
382
+
383
+ { "sessionId": "socket\_io\_session\_id\_1", "joinedAt": "ISODate(...)"},
384
+
385
+ { "sessionId": "socket\_io\_session\_id\_2", "joinedAt": "ISODate(...)"}
386
+
387
+ ],
388
+
389
+ "createdAt": "ISODate(...)",
390
+
391
+ "lastActivityAt": "ISODate(...)" // Updated on new message or join/leave
392
+
393
+ }
394
+
395
+
396
+
397
+ This structure would be queried and updated by the backend (Process 1.0 and 2.0) to manage
398
+
399
+ room access and message routing.
400
+
401
+ 5\. Complete System Structure.....................................................................................................................................
402
+
403
+ 5.1 Number of Modules and their Description..........................................................................................
404
+
405
+
406
+
407
+ The application’s architecture is modular to promote clarity, maintainability, and testability.
408
+
409
+ Key modules include:
410
+
411
+
412
+
413
+ &nbsp; Frontend User Interface (UI) Module (Next.js, Tailwind CSS):
414
+
415
+ &nbsp; Description: This module handles everything users see and interact with.It
416
+
417
+ &nbsp; renders all the pages (home page, room creation forms, chat interface), manages
418
+
419
+ &nbsp; user interactions, displays decrypted messages, and maintains the visual state of
420
+
421
+ &nbsp; the application.
422
+
423
+ &nbsp; Client-Side End-to-End Encryption (E2EE) Module (Web Crypto API):
424
+
425
+ &nbsp; Description: Operating entirely within each user’s browser, this is the security
426
+
427
+ &nbsp; heart of the system. It generates encryption keys, handles secure key exchange
428
+
429
+ &nbsp; with other users in the room, encrypts outgoing messages, and decrypts
430
+
431
+ &nbsp; incoming messages. Importantly, all private keys stay on the user’s device and
432
+
433
+ &nbsp; never get sent to the server.
434
+
435
+ &nbsp; Client-Side Real-Time Communication Module (Socket.io Client):
436
+
437
+ &nbsp; Description: This module manages the persistent connection between the user’s
438
+
439
+ &nbsp; browser and the server. It handles sending encrypted messages to the server and
440
+
441
+ &nbsp; receiving relayed encrypted messages and setup signals from the server.
442
+
443
+ &nbsp; Backend Room Management \& Signaling Server Module (Node.js, Socket.io):
444
+
445
+ &nbsp; Description: The server-side component that coordinates chat room operations.
446
+
447
+ &nbsp; It processes room creation requests, validates room joining attempts, manages
448
+
449
+ &nbsp; active room lifecycles, helps facilitate encrypted key exchange between clients,
450
+
451
+ &nbsp; and relays encrypted messages. Crucially, it never has access to actual message
452
+
453
+ &nbsp; content or users’ private keys.
454
+
455
+ &nbsp; Temporary Room Metadata Storage Module (MongoDB Driver \& Logic):
456
+
457
+ &nbsp; Description: This backend module provides the connection and operationsfor
458
+
459
+ &nbsp; interacting with MongoDB, which stores temporary information about active
460
+
461
+ &nbsp; chat rooms (room codes, user limits, participant lists) butnever stores messages
462
+
463
+ &nbsp; or encryption keys. CRUD operations (Create, Read, Update,Delete) on
464
+
465
+ &nbsp; temporary room metadata, ensuring data is available for room management and
466
+
467
+ &nbsp; cleared upon room inactivity.
468
+
469
+
470
+
471
+ 5.2 Data Structures..................................................................................................................................................
472
+
473
+
474
+
475
+ The application will utilize various data structures, bothclient-side and server-side, to manage
476
+
477
+ state and information flow:
478
+
479
+
480
+
481
+ ► Client-Side Structures:
482
+
483
+
484
+
485
+ &nbsp; currentRoomDetails: An object holding details of the room the client is currently
486
+
487
+ &nbsp; in (e.g.,{ roomCode: string, userLimit: number, participantNicknames:
488
+
489
+ &nbsp; string\[] }).
490
+
491
+ &nbsp; displayedMessages: An array of message objects shown in the chat interface,
492
+
493
+ &nbsp; including sender name, decrypted content, and timestamp. This gets cleared
494
+
495
+ &nbsp; when leaving the room.
496
+
497
+ &nbsp; sessionEncryptionContext: Securely holds the cryptographic keys for the
498
+
499
+ &nbsp; current chat session. This exists only in memory and gets cleared when the
500
+
501
+ &nbsp; session ends.
502
+
503
+ &nbsp; roomParticipantsInfo: A Map or array storing temporary information about
504
+
505
+ &nbsp; other participants in the room, potentially including their public key fragments if
506
+
507
+ &nbsp; needed during the key exchange phase.
508
+
509
+
510
+
511
+ ► Server-Side Structures (Node.js/Socket.io - In-Memory for active sessions, often
512
+
513
+ synced/backed by DB):
514
+
515
+
516
+
517
+ &nbsp; activeRoomsData: A JavaScript Map where keys are room codes and values
518
+
519
+ &nbsp; contain room details like user limits, sets of connected user IDs, and activity
520
+
521
+ &nbsp; timestamps.
522
+
523
+
524
+
525
+ ► MongoDB Document Structure (forActiveRoomscollection):
526
+
527
+
528
+
529
+ &nbsp; Fields includeroomCode,userLimit, an array ofparticipantSessions(each
530
+
531
+ &nbsp; withsessionId,joinedAt),createdAt,lastActivityAt.
532
+
533
+
534
+
535
+ ► Network Message Formats (Conceptual):
536
+
537
+
538
+
539
+ &nbsp; Encrypted Chat Message (Client <-> Server): A object like{ roomCode:
540
+
541
+ &nbsp; string, encryptedPayload: string (Base64 encoded ciphertext +
542
+
543
+ &nbsp; IV/nonce), senderSessionId?: string }.
544
+
545
+ &nbsp; Signaling Message (Client <-> Server, for E2EE key exchange): A object like
546
+
547
+ &nbsp; { roomCode: string, signalType: string (e.g., 'offer', 'answer',
548
+
549
+ &nbsp; 'candidate'), signalPayload: object, targetSessionId?: string }. The
550
+
551
+ &nbsp; signalPayloadstructure depends on the chosen key exchange protocol.
552
+
553
+
554
+
555
+ 5.3 Process Logic of Each Module....................................................................................................................
556
+
557
+
558
+
559
+ This section describes the core logic flow for each module.
560
+
561
+
562
+
563
+ &nbsp; Frontend UI Module:
564
+
565
+ &nbsp; Room Creation: User clicks create → UI prompts for optional user limit → sends
566
+
567
+ &nbsp; request to communication module → receives room code from server → displays
568
+
569
+ &nbsp; code and navigates to chat interface.
570
+
571
+ &nbsp; Room Joining: User inputsroomCode-> UI triggers “join room” action with code
572
+
573
+ &nbsp; to Communication Client. On success/failure from server, UInavigates to chat or
574
+
575
+ &nbsp; displays error.
576
+
577
+ &nbsp; Message Display: Receives decrypted message object (from E2EE Module) ->
578
+
579
+ &nbsp; appends to chat view with appropriate styling (sender, timestamp).
580
+
581
+ &nbsp; Sending Message: User types message -> UI captures input -> passes plaintext
582
+
583
+ &nbsp; to E2EE Module for encryption.
584
+
585
+ &nbsp; Leaving Room: User clicks “leave” or closes tab -> UI triggers “leave room”
586
+
587
+ &nbsp; action to Communication Client -> clears local message display and any session-
588
+
589
+ &nbsp; specific E2EE keys.
590
+
591
+ &nbsp; Client-Side E2EE Module:
592
+
593
+ &nbsp; Initialization (on room entry): Generates necessary cryptographic material.
594
+
595
+ &nbsp; Key Exchange: Sends public key information to other users through the server,
596
+
597
+ &nbsp; receives their public keys, computes shared secrets, derives symmetric session
598
+
599
+ &nbsp; keys.
600
+
601
+ &nbsp; Message Encryption: Takes plaintext from UI → uses session key to encrypt
602
+
603
+ &nbsp; with unique nonce/IV → returns ciphertext to communication module.
604
+
605
+ &nbsp; Message Decryption: Takes ciphertext from communication module → uses
606
+
607
+ &nbsp; session key to decrypt → returns plaintext to UI module.
608
+
609
+ &nbsp; Client-Side Real-Time Communication Module:
610
+
611
+ &nbsp; Connection Management: Establishes and maintains WebSocket connection to
612
+
613
+ &nbsp; Socket.io server upon entering a room context. Handles reconnect logic if
614
+
615
+ &nbsp; necessary.
616
+
617
+ &nbsp; Event Emission: Sends structured events to server:
618
+
619
+ &nbsp; ► create\_room\_request (with user limit)
620
+
621
+ &nbsp; ► join\_room\_request (with roomCode)
622
+
623
+ &nbsp; ► encrypted\_message\_to\_server (with roomCode, encryptedPayload)
624
+
625
+
626
+
627
+ ► key\_exchange\_signal\_to\_server (with roomCode, signalType,
628
+
629
+ signalPayload, targetSessionId if applicable)
630
+
631
+ ► leave\_room\_notification
632
+
633
+
634
+
635
+ &nbsp; Event Listening: Handles events from server:
636
+
637
+ &nbsp; ► room\_created\_success (with roomCode)
638
+
639
+ &nbsp; ► join\_room\_status (success or error message)
640
+
641
+ &nbsp; ► new\_encrypted\_message\_from\_server (passes encryptedPayload to E2EE
642
+
643
+ &nbsp; Module)
644
+
645
+ &nbsp; ► key\_exchange\_signal\_from\_server (passes signalPayloadto E2E Module)
646
+
647
+ &nbsp; ► user\_joined\_room\_notification, user\_left\_room\_notification (for UI
648
+
649
+ &nbsp; updates)
650
+
651
+
652
+
653
+ &nbsp; Backend Room Management \& Signaling Server Module:
654
+
655
+
656
+
657
+ &nbsp; Oncreate\_room\_request: Generates unique room code → creates database
658
+
659
+ &nbsp; entry → adds creator to Socket.io room → confirms creation touser.
660
+
661
+ &nbsp; Onjoin\_room\_request: Validates room code and capacity → adds user to
662
+
663
+ &nbsp; Socket.io room and database → notifies user and others in room.
664
+
665
+ &nbsp; Onencrypted\_message\_to\_server: Receives encrypted message → broadcasts
666
+
667
+ &nbsp; to other users in same room without decrypting\_.\_
668
+
669
+ &nbsp; Onkey\_exchange\_signal\_to\_server Receives setup signals → forwards to
670
+
671
+ &nbsp; appropriate recipients without interpreting content.
672
+
673
+ &nbsp; On clientdisconnectorleave\_room\_notification: Removes user from room
674
+
675
+ &nbsp; → updates database → notifies remaining users → deletes roomif empty.
676
+
677
+
678
+
679
+ &nbsp; Temporary Room Metadata Storage Module:
680
+
681
+
682
+
683
+ &nbsp; createRoom(details): Inserts a new document into theActiveRoomsMongoDB
684
+
685
+ &nbsp; collection.
686
+
687
+ &nbsp; findRoomByCode(roomCode): Retrieves a room document.
688
+
689
+ &nbsp; addParticipantToRoom(roomCode, sessionId): Updates the specified room
690
+
691
+ &nbsp; document to add a participant.
692
+
693
+ &nbsp; removeParticipantFromRoom(roomCode, sessionId): Updates room document
694
+
695
+ &nbsp; to remove a participant.
696
+
697
+ &nbsp; deleteRoom(roomCode): Deletes a room document.
698
+
699
+ &nbsp; getParticipantCount(roomCode): Returns current number of participants.
700
+
701
+
702
+
703
+ 5.4 Testing Process to be Used..........................................................................................................................
704
+
705
+
706
+
707
+ A multi-layered testing strategy will be implemented to ensure application quality, security,
708
+
709
+ and reliability:
710
+
711
+
712
+
713
+ &nbsp; Unit Testing:
714
+
715
+ &nbsp; Focus on individual functions and components in isolation.
716
+
717
+ &nbsp; Client-Side: Test encryption/decryption functions with known test vectors, test
718
+
719
+ &nbsp; React components for proper rendering and state management.
720
+
721
+ &nbsp; Server-Side: Test individual Socket.io event handlers and helper functions with
722
+
723
+ &nbsp; mocked dependencies.
724
+
725
+ &nbsp; Integration Testing:
726
+
727
+ &nbsp; Verify interactions between different modules.
728
+
729
+ &nbsp; Client-Server: Test the complete flow of Socket.io events between client and
730
+
731
+ &nbsp; server for room creation, joining, message relay, and signaling.
732
+
733
+ &nbsp; Module Interactions: Test Frontend UI <-> E2EE Module, Backend Server <->
734
+
735
+ &nbsp; MongoDB Storage Module.
736
+
737
+ &nbsp; End-to-End (E2E) Testing:
738
+
739
+ &nbsp; Simulate real user scenarios from start to finish using browser automation tools
740
+
741
+ &nbsp; (e.g., Cypress, Playwright).
742
+
743
+ &nbsp; Key Scenarios: User A creates a room, User B joins; both exchange multiple
744
+
745
+ &nbsp; encrypted messages; one user leaves, then the other; attempts to join full/invalid
746
+
747
+ &nbsp; rooms. Verify message display and ephemerality.
748
+
749
+ &nbsp; Security Testing:
750
+
751
+ &nbsp; E2EE Verification: Manually inspect network traffic using browser developer
752
+
753
+ &nbsp; tools to confirm all transmitted data is properly encrypted.
754
+
755
+ &nbsp; Vulnerability Assessment: Check for common web security issues and assess
756
+
757
+ &nbsp; room code generation strength.
758
+
759
+ &nbsp; Logical Flaw Detection: Review logic for key exchange and session
760
+
761
+ &nbsp; management for potential weaknesses.
762
+
763
+ &nbsp; Usability Testing:
764
+
765
+ &nbsp; Gather qualitative feedback from a small group of test users regarding the
766
+
767
+ &nbsp; application’s ease of use, clarity of instructions, and overall user experience.
768
+
769
+
770
+
771
+ Primary Testing Tools:
772
+
773
+
774
+
775
+ ► Jest and React Testing Library (for frontend unit/integration)
776
+
777
+ ► Jest or Mocha/Chai (for backend unit/integration)
778
+
779
+ ► Cypress or Playwright (for E2E tests)
780
+
781
+ ► Browser Developer Tools
782
+
783
+
784
+
785
+ 5.5 Reports Generation.........................................................................................................................................
786
+
787
+
788
+
789
+ Given the application’s core principles of ephemerality and privacy, traditional report
790
+
791
+ generation is intentionally minimal and avoids storing sensitive user data:
792
+
793
+
794
+
795
+ &nbsp; Client-Side Debugging Logs (Developer-Enabled):
796
+
797
+ &nbsp; Content: Timestamps of significant client-side events (e.g., “Roomjoined: XYZ”,
798
+
799
+ &nbsp; “Key exchange step 1 complete”, “Message encrypted”, “Error: Decryption
800
+
801
+ &nbsp; failed”). Strictly no message content or cryptographic key material will be
802
+
803
+ &nbsp; logged.
804
+
805
+ &nbsp; Purpose: For developers or advanced users to diagnose local issues related to
806
+
807
+ &nbsp; connectivity, E2EE setup, or UI errors.
808
+
809
+ &nbsp; Generation: Implemented viaconsole.logor a lightweight client-side logging
810
+
811
+ &nbsp; utility, typically enabled via a browser console command ora debug flag in
812
+
813
+ &nbsp; development builds.
814
+
815
+ &nbsp; Server-Side Operational Logs (Anonymized ):
816
+
817
+ &nbsp; Content: Event timestamps, server operations (room created, user joined,
818
+
819
+ &nbsp; message relayed), anonymized room identifiers, error codes and stack traces,
820
+
821
+ &nbsp; aggregate metrics (active connections, active rooms).
822
+
823
+ &nbsp; Purpose: For system administrators/developers to monitor server health,
824
+
825
+ &nbsp; performance, identify bottlenecks, track error rates, anddebug server-side
826
+
827
+ &nbsp; operational issues.
828
+
829
+ &nbsp; Generation: Using a robust logging library (e.g., Winston, Pino) in the Node.js
830
+
831
+ &nbsp; backend, with configurable log levels.
832
+
833
+ &nbsp; Ephemeral Session “Report” (User Interface):
834
+
835
+ &nbsp; Content: The dynamically rendered chat messages displayed within the user’s
836
+
837
+ &nbsp; active browser session.
838
+
839
+ &nbsp; Purpose: This is the primary “report” visible to the user – their live conversation.
840
+
841
+ &nbsp; Generation: This “report” is the application’s core user interface. It is ephemeral
842
+
843
+ &nbsp; by design; when the user leaves the room or closes the browsertab/window,
844
+
845
+ &nbsp; this displayed information is cleared from their client.
846
+
847
+
848
+
849
+ No persistent reports containing chat message content, user identities (beyond
850
+
851
+ temporary session identifiers), or detailed user activity logs will be generated or stored
852
+
853
+ by the server. The system prioritizes not collecting data that doesn’t absolutely need to be
854
+
855
+ collected.
856
+
857
+ 6\. Tools / Platform, Hardware and Software Requirement Specifications............................................
858
+
859
+ 6.1 Software Requirements................................................................................................................................
860
+
861
+
862
+
863
+ ► Frontend Development:
864
+
865
+
866
+
867
+ &nbsp; Programming Languages: JavaScript (ES6+), TypeScript
868
+
869
+ &nbsp; Core Framework/Library: Next.js (React-based framework)
870
+
871
+ &nbsp; Styling: Tailwind CSS
872
+
873
+ &nbsp; Client-Side Cryptography: Web Crypto API (built into modern browsers)
874
+
875
+ &nbsp; Real-Time Client Communication: Socket.io Client library
876
+
877
+ &nbsp; Package Management: bun (Node Package Manager)
878
+
879
+ &nbsp; Version Control System: Git
880
+
881
+
882
+
883
+ ► Backend Development:
884
+
885
+
886
+
887
+ &nbsp; Runtime Environment: Node.js
888
+
889
+ &nbsp; Real-Time Server Framework: Socket.io
890
+
891
+ &nbsp; Programming Languages: JavaScript (ES6+), TypeScript
892
+
893
+ &nbsp; Database: MongoDB (for temporary room metadata)
894
+
895
+ &nbsp; Package Management: bun
896
+
897
+
898
+
899
+ ► Development Environment:
900
+
901
+
902
+
903
+ &nbsp; Operating System: Windows 11
904
+
905
+ &nbsp; Code Editor/IDE: Visual Studio Code
906
+
907
+ &nbsp; Web Browsers (for development \& testing): Latest stable versions of Google
908
+
909
+ &nbsp; Chrome, Mozilla Firefox, Microsoft Edge, Safari.
910
+
911
+ &nbsp; Terminal/Command Line Interface: For running scripts, Gitcommands, etc.
912
+
913
+
914
+
915
+ ► Deployment Environment (Server-Side):
916
+
917
+
918
+
919
+ &nbsp; Operating System: Linux-based OS (e.g., Ubuntu Server) isstandard for Node.js.
920
+
921
+ &nbsp; Process Manager (for Node.js application): PM2, or systemd.
922
+
923
+ &nbsp; Database Server: MongoDB instance.
924
+
925
+ &nbsp; Cloud Platform: Vercel (ideal for Next.js).
926
+
927
+
928
+
929
+ ► User Environment (Client-Side):
930
+
931
+
932
+
933
+ &nbsp; Operating System: Any modern OS capable of running currentweb browsers
934
+
935
+ &nbsp; (Windows, macOS, Linux, Android, iOS).
936
+
937
+ &nbsp; Web Browser: Latest stable versions of Google Chrome, Mozilla Firefox,
938
+
939
+ &nbsp; Microsoft Edge, Safari, with full support for WebSockets and the Web Crypto API.
940
+
941
+
942
+
943
+ 6.2 Hardware Requirements..............................................................................................................................
944
+
945
+
946
+
947
+ ► Development Machine:
948
+
949
+
950
+
951
+ &nbsp; Processor: Multi-core processor AMD Ryzen 5
952
+
953
+ &nbsp; RAM: Minimum 8 GB.
954
+
955
+ &nbsp; Storage: 100 GB free disk space SSD.
956
+
957
+ &nbsp; Network: Broadband internet connection.
958
+
959
+
960
+
961
+ ► Server Machine (for Deployment - indicative for a small to moderate load):
962
+
963
+
964
+
965
+ &nbsp; Processor: 1-2+ vCPUs (AWS t3.small/medium equivalent).
966
+
967
+ &nbsp; RAM: 2-8 GB (depends on concurrent user load - Socket.io canbe memory-
968
+
969
+ &nbsp; intensive).
970
+
971
+ &nbsp; Storage: 20 GB - 50 GB+ SSD (for OS, application, logs, and MongoDB data if co-
972
+
973
+ &nbsp; hosted).
974
+
975
+ &nbsp; Network: Reliable connection with sufficient bandwidth for real-time WebSocket
976
+
977
+ &nbsp; traffic.
978
+
979
+
980
+
981
+ ► User Machine (Client-Side):
982
+
983
+
984
+
985
+ &nbsp; Standard Requirements: Any desktop, laptop, tablet, or smartphone from the last 5-
986
+
987
+ &nbsp; years
988
+
989
+ &nbsp; Processor: Any modern CPU capable of handling JavaScript execution forencryption
990
+
991
+ &nbsp; without significant delay
992
+
993
+ &nbsp; RAM: 2 GB minimum (more recommended for better browser performance)
994
+
995
+ &nbsp; Network: Stable internet connection (WiFi, Ethernet, or reliable mobile data)
996
+
997
+
998
+
999
+ 7\. Industry/Client Affiliation........................................................................................................................................
1000
+
1001
+ No.
1002
+
1003
+
1004
+
1005
+ This “ End-to-End Encrypted Chat Room Application” is beingdeveloped purely as an academic
1006
+
1007
+ project. It is intended to fulfill educational requirements and explore concepts in secure web
1008
+
1009
+ application development. It is not commissioned by, associated with, or undertaken for any
1010
+
1011
+ specific industry, client, or commercial organization.
1012
+
1013
+ 8\. Future Scope and Further Enhancement of the Project.............................................................................
1014
+
1015
+
1016
+
1017
+ While the initial version focuses on core secure chat functionality, several enhancements could
1018
+
1019
+ be added in future iterations:
1020
+
1021
+
1022
+
1023
+ &nbsp; Advanced Room Controls:
1024
+
1025
+ &nbsp; Room Passwords: Add optional password protection in addition to room codes.
1026
+
1027
+ &nbsp; Moderation Tools: Give room creators basic moderation tools (mute or remove
1028
+
1029
+ &nbsp; disruptive users).
1030
+
1031
+ &nbsp; Customizable User Limits: Allow room creators to adjust user limits during
1032
+
1033
+ &nbsp; active sessions
1034
+
1035
+ &nbsp; .
1036
+
1037
+ &nbsp; Rich Media \& Interaction (E2EE):
1038
+
1039
+ &nbsp; Encrypted File Sharing: Allow secure file transfers within chat rooms.
1040
+
1041
+ &nbsp; Markdown/Rich Text Formatting: Support for basic message formatting to
1042
+
1043
+ &nbsp; improve readability and expression.
1044
+
1045
+ &nbsp; Emoji Reactions: Allow users to react to messages with emojis.
1046
+
1047
+ &nbsp; E2EE Voice/Video Calls: Integration of WebRTC for encrypted peer-to-peer
1048
+
1049
+ &nbsp; voice and video calls
1050
+
1051
+ &nbsp; .
1052
+
1053
+ &nbsp; User Experience Improvements:
1054
+
1055
+ &nbsp; Typing Indicators: Securely implement indicators to show when other users are
1056
+
1057
+ &nbsp; typing.
1058
+
1059
+ &nbsp; Read Receipts (Optional \& E2EE): A privacy-conscious implementation of
1060
+
1061
+ &nbsp; message read receipts.
1062
+
1063
+ &nbsp; UI Themes \& Personalization: Allow users to choose different visual themes.
1064
+
1065
+ &nbsp; Improved Notification System: More refined in-app notifications.
1066
+
1067
+ &nbsp; Advanced Security Features:
1068
+
1069
+ &nbsp; Key Verification Mechanisms: Allow users to verify each other’s identities
1070
+
1071
+ &nbsp; through safety numbers or QR code scanning.
1072
+
1073
+ &nbsp; Formal Security Audit: Engage professional security reviewers to assess the
1074
+
1075
+ &nbsp; cryptographic implementation.
1076
+
1077
+ &nbsp; Scalability and Performance:
1078
+
1079
+ &nbsp; Horizontal Scaling for Socket.io: mplement Redis adapter for Socket.io to
1080
+
1081
+ &nbsp; handle more concurrent users across multiple server instances.
1082
+
1083
+ &nbsp; Optimized Message Broadcasting: More efficient message delivery
1084
+
1085
+ &nbsp; mechanisms for very large rooms (if user limits are increased).
1086
+
1087
+
1088
+
1089
+ These potential enhancements would progressively build upon the foundational secure and
1090
+
1091
+ ephemeral chat system, adding value and utility.
1092
+
1093
+ 9\. Limitations of the Project (Initial Version)......................................................................................................
1094
+
1095
+
1096
+
1097
+ The initial development phase will focus on delivering corefunctionality, and as such, certain
1098
+
1099
+ limitations will exist:
1100
+
1101
+
1102
+
1103
+ &nbsp; No User Accounts: The application operates without traditional registration or login
1104
+
1105
+ &nbsp; systems. Users remain anonymous within each chat session.
1106
+
1107
+ &nbsp; No Message History: All conversations are temporary. Messages aren’t saved
1108
+
1109
+ &nbsp; anywhere and disappear when rooms close or users leave.
1110
+
1111
+ &nbsp; Text Messages Only: Initial version supports only text-based communication. File
1112
+
1113
+ &nbsp; sharing, voice messages, or video calls aren’t included yet.
1114
+
1115
+ &nbsp; Basic Key Exchange: While end-to-end encryption is implemented, advanced features
1116
+
1117
+ &nbsp; like Perfect Forward Secrecy or explicit key fingerprint verification aren’t included
1118
+
1119
+ &nbsp; initially.
1120
+
1121
+ &nbsp; Room Code Security: Room security relies primarily on keeping the generated codes
1122
+
1123
+ &nbsp; secret. While codes are designed to be hard to guess, additional security measures
1124
+
1125
+ &nbsp; against code compromise aren’t a primary focus initially.
1126
+
1127
+ &nbsp; Single Server Focus: The backend architecture is optimized for single server
1128
+
1129
+ &nbsp; deployment. Horizontal scaling strategies are consideredfuture enhancements.
1130
+
1131
+ &nbsp; No Offline Support: Users must be actively connected to send or receive messages.
1132
+
1133
+ &nbsp; There’s no message queuing for offline users.
1134
+
1135
+ &nbsp; Trust in Client Code: The effectiveness of encryption depends on the integrity of
1136
+
1137
+ &nbsp; JavaScript code running in users’ browsers. Users must trust that this code correctly
1138
+
1139
+ &nbsp; implements encryption and doesn’t compromise security.
1140
+
1141
+ &nbsp; Limited Room Management: Initial version doesn’t include features for room creators
1142
+
1143
+ &nbsp; to manage participants (like kicking or banning users).
1144
+
1145
+ &nbsp; Modern Browser Dependency: Requires recent browser versions with WebSocket and
1146
+
1147
+ &nbsp; Web Crypto API support, which may limit compatibility with very old browsers.
1148
+
1149
+
1150
+
1151
+ These limitations help keep the project scope manageable while ensuring robust
1152
+
1153
+ implementation of core security and communication features.
1154
+
1155
+ 10\. Security and Validation Checks...........................................................................................................................
1156
+
1157
+
1158
+
1159
+ Security is a foundational requirement of this application. The following checks, principles, and
1160
+
1161
+ validations will be integral to its design and implementation:
1162
+
1163
+
1164
+
1165
+ &nbsp; End-to-End Encryption (E2EE) Implementation:
1166
+
1167
+ &nbsp; Core: All user-to-user message content will be encrypted on the sender’s client
1168
+
1169
+ &nbsp; device and decrypted only on the recipient(s)’ client device(s).
1170
+
1171
+ &nbsp; Algorithms: ndustry-standard cryptography using AES-256-GCM for message
1172
+
1173
+ &nbsp; encryption and secure key exchange protocols like Diffie-Hellman.
1174
+
1175
+ &nbsp; Key Management: ll private keys and session keys are generated and stored
1176
+
1177
+ &nbsp; only on client devices - never transmitted to or stored by theserver.
1178
+
1179
+ &nbsp; Message and Data Ephemerality:
1180
+
1181
+ &nbsp; No Server-Side Message Storage: The server will not store any chat message
1182
+
1183
+ &nbsp; content, either in plaintext or ciphertext form, in any database or persistent logs.
1184
+
1185
+ &nbsp; Client-Side Data Clearing: When users leave rooms, their browsers
1186
+
1187
+ &nbsp; automatically clear displayed messages and encryption keys from active Java
1188
+
1189
+ &nbsp; memory.
1190
+
1191
+ &nbsp; Temporary Room Metadata Purging: Server-side metadata related to active
1192
+
1193
+ &nbsp; chat rooms (e.g., room ID, list of active participant sessions) will be actively
1194
+
1195
+ &nbsp; deleted from the temporary store (MongoDB/memory) once a room becomes
1196
+
1197
+ &nbsp; empty or after a defined period of inactivity.
1198
+
1199
+ &nbsp; Secure Room Access and Control:
1200
+
1201
+ &nbsp; Unique and Complex Room Codes: Chat rooms will be accessed via unique,
1202
+
1203
+ &nbsp; randomly generated codes of sufficient complexity to make guessing impractical.
1204
+
1205
+ &nbsp; Server-Side Validation: The backend server will rigorously validate room codes
1206
+
1207
+ &nbsp; and enforce any defined user limits before granting a user access to a chat room.
1208
+
1209
+ &nbsp; Rate Limiting (Consideration): Basic rate limiting on attempts to join rooms
1210
+
1211
+ &nbsp; may be implemented on the server-side to mitigate brute-force attacks on room
1212
+
1213
+ &nbsp; codes.
1214
+
1215
+ &nbsp; Input Validation (Client-Side and Server-Side):
1216
+
1217
+
1218
+
1219
+ &nbsp; Data Sanitization: All user input is validated and sanitized on both client and
1220
+
1221
+ &nbsp; server sides to prevent injection attacks.
1222
+
1223
+ &nbsp; Socket.io Event Payload Validation: Socket.io event payloads are validated to
1224
+
1225
+ &nbsp; ensure they conform to expected formats.
1226
+
1227
+
1228
+
1229
+ &nbsp; Transport Layer Security (TLS/SSL):
1230
+
1231
+
1232
+
1233
+ &nbsp; All communication between the client’s browser and the webserver (serving the
1234
+
1235
+ &nbsp; Next.js application) and the Socket.io server will be enforced over HTTPS and
1236
+
1237
+ &nbsp; WSS (Secure WebSockets) respectively. This protects the already E2E-encrypted
1238
+
1239
+ &nbsp; message payloads and critical signaling messages while they are in transit
1240
+
1241
+ &nbsp; to/from the server.
1242
+
1243
+
1244
+
1245
+ &nbsp; Protection Against Common Web Vulnerabilities:
1246
+
1247
+
1248
+
1249
+ &nbsp; Cross-Site Scripting (XSS): Although message content is E2E encrypted, any
1250
+
1251
+ &nbsp; user-generated input that might be displayed directly by the UI (e.g., user-chosen
1252
+
1253
+ &nbsp; temporary nicknames, if implemented) will be properly escaped/sanitized by the
1254
+
1255
+ &nbsp; frontend framework (Next.js/React) to prevent XSS.
1256
+
1257
+ &nbsp; Secure Headers: Implement appropriate HTTP security headers (e.g., Content
1258
+
1259
+ &nbsp; Security Policy, X-Content-Type-Options).
1260
+
1261
+
1262
+
1263
+ &nbsp; Secure Code Practices:
1264
+
1265
+
1266
+
1267
+ &nbsp; Dependency Management: Regularly update all third-party libraries and
1268
+
1269
+ &nbsp; dependencies (both frontend and backend) to patch known vulnerabilities, using
1270
+
1271
+ &nbsp; tools likenpm audit.
1272
+
1273
+ &nbsp; Principle of Least Privilege: Server-side processes will operate with the
1274
+
1275
+ &nbsp; minimum necessary permissions.
1276
+
1277
+
1278
+
1279
+ &nbsp; Signaling Channel Security:
1280
+
1281
+
1282
+
1283
+ &nbsp; Ensure that signaling messages (used for E2EE key exchangesetup) are relayed
1284
+
1285
+ &nbsp; correctly only to the intended participants within a specific room and are
1286
+
1287
+ &nbsp; protected in transit by WSS.
1288
+
1289
+
1290
+
1291
+ Validation Approach:
1292
+
1293
+
1294
+
1295
+ ► Conduct regular code reviews with a focus on security implementation details.
1296
+
1297
+ ► Perform manual security testing, including attempts to bypass E2EE and inspecting
1298
+
1299
+ network traffic.
1300
+
1301
+ ► Utilize browser developer tools for examining client-sidedata handling and storage.
1302
+
1303
+ ► Write automated tests specifically targeting the encryption/decryption logic and key
1304
+
1305
+ exchange process.
1306
+
1307
+
1308
+
1309
+ 11\. Bibliography (References).....................................................................................................................................
1310
+
1311
+
1312
+
1313
+ Cryptography and Security:
1314
+
1315
+
1316
+
1317
+ &nbsp; OWASP Foundation. (2021). OWASP Top Ten Web Application Security Risks. Retrieved
1318
+
1319
+ &nbsp; fromowasp.org/www-project-top-ten/
1320
+
1321
+ &nbsp; Mozilla Developer Network (MDN). Web Crypto API. Retrieved from
1322
+
1323
+ &nbsp; developer.mozilla.org/en-US/docs/Web/API/Web\_Crypto\_API
1324
+
1325
+
1326
+
1327
+ Web Technologies and Development:
1328
+
1329
+
1330
+
1331
+ &nbsp; Next.js Official Documentation. Vercel. Retrieved fromnextjs.org/docs
1332
+
1333
+ &nbsp; Socket.IO Official Documentation. Retrieved fromsocket.io/docs/
1334
+
1335
+ &nbsp; Tailwind CSS Official Documentation. Tailwind Labs. Retrieved from
1336
+
1337
+ &nbsp; tailwindcss.com/docs
1338
+
1339
+
1340
+
1341
+ Powered by TCPDF (www.tcpdf.org)
1342
+
1343
+
1344
+
1345
+ Index of comments
1346
+
1347
+
1348
+
1349
+ 1.1 Implement asymmetric encryption (e.g., RSA) for secure key exchange between users.
1350
+
1351
+ Integrate message integrity checks using digital signatures or hash verification (e.g., SHA-256).
1352
+
1353
+ Add support for multimedia (images/files) sharing with encrypted transmission and storage.
1354
+
1355
+ Enable user authentication with two-factor authentication (2FA) for enhanced account security.
1356
+
1357
+ Include real-time message delivery status (sent, delivered, seen) with WebSocket-based communication.
1358
+
1359
+
1360
+
1361
+
1362
+
eslint.config.mjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
hooks/useWebRTC.ts ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useRef, useCallback } from "react";
2
+ import { socket } from "@/lib/socket";
3
+
4
+ const STUN_SERVERS = {
5
+ iceServers: [
6
+ { urls: "stun:stun.l.google.com:19302" },
7
+ { urls: "stun:stun1.l.google.com:19302" },
8
+ ],
9
+ };
10
+
11
+ export function useWebRTC(roomId: string) {
12
+ const [localStream, setLocalStream] = useState<MediaStream | null>(null);
13
+ const [remoteStreams, setRemoteStreams] = useState<Map<string, MediaStream>>(new Map());
14
+ const peersRef = useRef<Map<string, RTCPeerConnection>>(new Map());
15
+
16
+ const createPeer = useCallback((targetSocketId: string, initiator: boolean, stream: MediaStream) => {
17
+ const peer = new RTCPeerConnection(STUN_SERVERS);
18
+ peersRef.current.set(targetSocketId, peer);
19
+
20
+ // Add local tracks
21
+ stream.getTracks().forEach(track => peer.addTrack(track, stream));
22
+
23
+ // Handle ICE candidates
24
+ peer.onicecandidate = (event) => {
25
+ if (event.candidate) {
26
+ socket.emit("signal", {
27
+ target: targetSocketId,
28
+ signal: { type: "ice-candidate", candidate: event.candidate }
29
+ });
30
+ }
31
+ };
32
+
33
+ // Handle remote stream
34
+ peer.ontrack = (event) => {
35
+ console.log("Received remote track from:", targetSocketId);
36
+ setRemoteStreams(prev => {
37
+ const newMap = new Map(prev);
38
+ newMap.set(targetSocketId, event.streams[0]);
39
+ return newMap;
40
+ });
41
+ };
42
+
43
+ // Create Offer if initiator
44
+ if (initiator) {
45
+ peer.createOffer()
46
+ .then(offer => peer.setLocalDescription(offer))
47
+ .then(() => {
48
+ socket.emit("signal", {
49
+ target: targetSocketId,
50
+ signal: { type: "offer", sdp: peer.localDescription }
51
+ });
52
+ })
53
+ .catch(err => console.error("Error creating offer:", err));
54
+ }
55
+
56
+ return peer;
57
+ }, []);
58
+
59
+ const joinCall = useCallback(async () => {
60
+ try {
61
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
62
+ setLocalStream(stream);
63
+ socket.emit("join-call", { roomId });
64
+ return stream;
65
+ } catch (err) {
66
+ console.error("Error accessing media devices:", err);
67
+ alert("Could not access camera/microphone");
68
+ return null;
69
+ }
70
+ }, [roomId]);
71
+
72
+ const leaveCall = useCallback(() => {
73
+ // Stop local stream
74
+ if (localStream) {
75
+ localStream.getTracks().forEach(track => track.stop());
76
+ setLocalStream(null);
77
+ }
78
+
79
+ // Close all peers
80
+ peersRef.current.forEach(peer => peer.close());
81
+ peersRef.current.clear();
82
+ setRemoteStreams(new Map());
83
+
84
+ socket.emit("leave-call", { roomId });
85
+ }, [localStream, roomId]);
86
+
87
+ useEffect(() => {
88
+ if (!localStream) return;
89
+
90
+ const handleUserConnected = ({ socketId }: { socketId: string }) => {
91
+ console.log("User connected to call:", socketId);
92
+ // Initiate connection to new user
93
+ createPeer(socketId, true, localStream);
94
+ };
95
+
96
+ const handleUserDisconnected = ({ socketId }: { socketId: string }) => {
97
+ console.log("User disconnected from call:", socketId);
98
+ if (peersRef.current.has(socketId)) {
99
+ peersRef.current.get(socketId)!.close();
100
+ peersRef.current.delete(socketId);
101
+ }
102
+ setRemoteStreams(prev => {
103
+ const newMap = new Map(prev);
104
+ newMap.delete(socketId);
105
+ return newMap;
106
+ });
107
+ };
108
+
109
+ const handleSignal = async (data: { sender: string; signal: any }) => {
110
+ const { sender, signal } = data;
111
+
112
+ // Ignore key exchange signals (offer-key, answer-key)
113
+ if (signal.type === "offer-key" || signal.type === "answer-key") return;
114
+
115
+ let peer = peersRef.current.get(sender);
116
+
117
+ if (!peer) {
118
+ // If receiving offer, create peer (not initiator)
119
+ if (signal.type === "offer") {
120
+ peer = createPeer(sender, false, localStream);
121
+ } else {
122
+ console.warn("Received signal for unknown peer:", sender);
123
+ return;
124
+ }
125
+ }
126
+
127
+ try {
128
+ if (signal.type === "offer") {
129
+ await peer.setRemoteDescription(new RTCSessionDescription(signal.sdp));
130
+ const answer = await peer.createAnswer();
131
+ await peer.setLocalDescription(answer);
132
+ socket.emit("signal", {
133
+ target: sender,
134
+ signal: { type: "answer", sdp: peer.localDescription }
135
+ });
136
+ } else if (signal.type === "answer") {
137
+ await peer.setRemoteDescription(new RTCSessionDescription(signal.sdp));
138
+ } else if (signal.type === "ice-candidate") {
139
+ await peer.addIceCandidate(new RTCIceCandidate(signal.candidate));
140
+ }
141
+ } catch (err) {
142
+ console.error("Error handling signal:", err);
143
+ }
144
+ };
145
+
146
+ socket.on("user-connected-to-call", handleUserConnected);
147
+ socket.on("user-disconnected-from-call", handleUserDisconnected);
148
+ socket.on("signal", handleSignal);
149
+
150
+ return () => {
151
+ socket.off("user-connected-to-call", handleUserConnected);
152
+ socket.off("user-disconnected-from-call", handleUserDisconnected);
153
+ socket.off("signal", handleSignal);
154
+ };
155
+ }, [localStream, createPeer]);
156
+
157
+ return { localStream, remoteStreams, joinCall, leaveCall };
158
+ }
lib/crypto.test.ts ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ generateKeyPair,
4
+ exportKey,
5
+ importKey,
6
+ encryptMessage,
7
+ decryptMessage,
8
+ generateSymKey,
9
+ encryptSymMessage,
10
+ decryptSymMessage,
11
+ exportSymKey,
12
+ importSymKey
13
+ } from "./crypto";
14
+
15
+ describe("Crypto Logic", () => {
16
+ test("should generate key pair", async () => {
17
+ const keyPair = await generateKeyPair();
18
+ expect(keyPair.publicKey).toBeDefined();
19
+ expect(keyPair.privateKey).toBeDefined();
20
+ });
21
+
22
+ test("should export and import keys", async () => {
23
+ const keyPair = await generateKeyPair();
24
+ const exportedPublic = await exportKey(keyPair.publicKey);
25
+ const importedPublic = await importKey(exportedPublic, ["encrypt"]);
26
+ expect(importedPublic).toBeDefined();
27
+ });
28
+
29
+ test("should encrypt and decrypt message", async () => {
30
+ const keyPair = await generateKeyPair();
31
+ const message = "Hello, World!";
32
+ const encrypted = await encryptMessage(keyPair.publicKey, message);
33
+ const decrypted = await decryptMessage(keyPair.privateKey, encrypted);
34
+ expect(decrypted).toBe(message);
35
+ });
36
+
37
+ test("should encrypt and decrypt symmetric message", async () => {
38
+ const key = await generateSymKey();
39
+ const message = "Secret Message";
40
+ const encrypted = await encryptSymMessage(key, message);
41
+ const decrypted = await decryptSymMessage(key, encrypted);
42
+ expect(decrypted).toBe(message);
43
+ });
44
+
45
+ test("should export and import symmetric keys", async () => {
46
+ const key = await generateSymKey();
47
+ const exported = await exportSymKey(key);
48
+ const imported = await importSymKey(exported);
49
+ expect(imported).toBeDefined();
50
+
51
+ // Verify imported key works
52
+ const message = "Test Key Import";
53
+ const encrypted = await encryptSymMessage(imported, message);
54
+ const decrypted = await decryptSymMessage(key, encrypted); // Decrypt with original key
55
+ expect(decrypted).toBe(message);
56
+ });
57
+ });
lib/crypto.ts ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export async function generateKeyPair() {
2
+ return await crypto.subtle.generateKey(
3
+ {
4
+ name: "RSA-OAEP",
5
+ modulusLength: 2048,
6
+ publicExponent: new Uint8Array([1, 0, 1]),
7
+ hash: "SHA-256",
8
+ },
9
+ true,
10
+ ["encrypt", "decrypt"]
11
+ );
12
+ }
13
+
14
+ export async function exportKey(key: CryptoKey) {
15
+ const exported = await crypto.subtle.exportKey("jwk", key);
16
+ return exported;
17
+ }
18
+
19
+ export async function importKey(jwk: JsonWebKey, usage: KeyUsage[]) {
20
+ return await crypto.subtle.importKey(
21
+ "jwk",
22
+ jwk,
23
+ {
24
+ name: "RSA-OAEP",
25
+ hash: "SHA-256",
26
+ },
27
+ true,
28
+ usage
29
+ );
30
+ }
31
+
32
+ export async function encryptMessage(publicKey: CryptoKey, message: string) {
33
+ const encoder = new TextEncoder();
34
+ const data = encoder.encode(message);
35
+ const encrypted = await crypto.subtle.encrypt(
36
+ {
37
+ name: "RSA-OAEP",
38
+ },
39
+ publicKey,
40
+ data
41
+ );
42
+ return arrayBufferToBase64(encrypted);
43
+ }
44
+
45
+ export async function decryptMessage(privateKey: CryptoKey, encryptedMessage: string) {
46
+ const data = base64ToArrayBuffer(encryptedMessage);
47
+ const decrypted = await crypto.subtle.decrypt(
48
+ {
49
+ name: "RSA-OAEP",
50
+ },
51
+ privateKey,
52
+ data
53
+ );
54
+ const decoder = new TextDecoder();
55
+ return decoder.decode(decrypted);
56
+ }
57
+
58
+ function arrayBufferToBase64(buffer: ArrayBuffer) {
59
+ let binary = "";
60
+ const bytes = new Uint8Array(buffer);
61
+ const len = bytes.byteLength;
62
+ for (let i = 0; i < len; i++) {
63
+ binary += String.fromCharCode(bytes[i]);
64
+ }
65
+ return btoa(binary);
66
+ }
67
+
68
+ function base64ToArrayBuffer(base64: string) {
69
+ const binaryString = atob(base64);
70
+ const bytes = new Uint8Array(binaryString.length);
71
+ for (let i = 0; i < binaryString.length; i++) {
72
+ bytes[i] = binaryString.charCodeAt(i);
73
+ }
74
+ return bytes.buffer;
75
+ }
76
+
77
+ // ========== AES-GCM Functions for Hybrid Encryption ==========
78
+
79
+ export async function generateSymKey() {
80
+ return await crypto.subtle.generateKey(
81
+ {
82
+ name: "AES-GCM",
83
+ length: 256,
84
+ },
85
+ true,
86
+ ["encrypt", "decrypt"]
87
+ );
88
+ }
89
+
90
+ export async function encryptSymMessage(key: CryptoKey, message: string) {
91
+ const encoder = new TextEncoder();
92
+ const data = encoder.encode(message);
93
+ const iv = crypto.getRandomValues(new Uint8Array(12));
94
+ const encrypted = await crypto.subtle.encrypt(
95
+ {
96
+ name: "AES-GCM",
97
+ iv: iv,
98
+ },
99
+ key,
100
+ data
101
+ );
102
+
103
+ // Combine IV and ciphertext
104
+ const combined = new Uint8Array(iv.length + encrypted.byteLength);
105
+ combined.set(iv);
106
+ combined.set(new Uint8Array(encrypted), iv.length);
107
+
108
+ return arrayBufferToBase64(combined.buffer);
109
+ }
110
+
111
+ export async function decryptSymMessage(key: CryptoKey, encryptedMessage: string) {
112
+ const combined = base64ToArrayBuffer(encryptedMessage);
113
+ const combinedArray = new Uint8Array(combined);
114
+ const iv = combinedArray.slice(0, 12);
115
+ const data = combinedArray.slice(12);
116
+
117
+ const decrypted = await crypto.subtle.decrypt(
118
+ {
119
+ name: "AES-GCM",
120
+ iv: iv,
121
+ },
122
+ key,
123
+ data
124
+ );
125
+
126
+ const decoder = new TextDecoder();
127
+ return decoder.decode(decrypted);
128
+ }
129
+
130
+ export async function exportSymKey(key: CryptoKey) {
131
+ const exported = await crypto.subtle.exportKey("raw", key);
132
+ return arrayBufferToBase64(exported);
133
+ }
134
+
135
+ export async function importSymKey(rawKey: string) {
136
+ const buffer = base64ToArrayBuffer(rawKey);
137
+ return await crypto.subtle.importKey(
138
+ "raw",
139
+ buffer,
140
+ {
141
+ name: "AES-GCM",
142
+ },
143
+ true,
144
+ ["encrypt", "decrypt"]
145
+ );
146
+ }
147
+
148
+ // ========== FILE ENCRYPTION FUNCTIONS ==========
149
+
150
+ /**
151
+ * Encrypt a file (ArrayBuffer) with AES-GCM
152
+ */
153
+ export async function encryptFile(aesKey: CryptoKey, fileData: ArrayBuffer): Promise<ArrayBuffer> {
154
+ const iv = crypto.getRandomValues(new Uint8Array(12));
155
+ const encrypted = await crypto.subtle.encrypt(
156
+ {
157
+ name: "AES-GCM",
158
+ iv,
159
+ },
160
+ aesKey,
161
+ fileData
162
+ );
163
+
164
+ // Combine IV + encrypted data
165
+ const combined = new Uint8Array(iv.length + encrypted.byteLength);
166
+ combined.set(iv, 0);
167
+ combined.set(new Uint8Array(encrypted), iv.length);
168
+
169
+ return combined.buffer;
170
+ }
171
+
172
+ /**
173
+ * Decrypt a file (ArrayBuffer) with AES-GCM
174
+ */
175
+ export async function decryptFile(aesKey: CryptoKey, encryptedData: ArrayBuffer): Promise<ArrayBuffer> {
176
+ const combined = new Uint8Array(encryptedData);
177
+ const iv = combined.slice(0, 12);
178
+ const data = combined.slice(12);
179
+
180
+ const decrypted = await crypto.subtle.decrypt(
181
+ {
182
+ name: "AES-GCM",
183
+ iv: iv,
184
+ },
185
+ aesKey,
186
+ data
187
+ );
188
+
189
+ return decrypted;
190
+ }
lib/db.ts ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mongoose from 'mongoose';
2
+
3
+ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/secrate_chat';
4
+
5
+ if (!MONGODB_URI) {
6
+ throw new Error(
7
+ 'Please define the MONGODB_URI environment variable inside .env.local'
8
+ );
9
+ }
10
+
11
+ /**
12
+ * Global is used here to maintain a cached connection across hot reloads
13
+ * in development. This prevents connections growing exponentially
14
+ * during API Route usage.
15
+ */
16
+ let cached = (global as any).mongoose;
17
+
18
+ if (!cached) {
19
+ cached = (global as any).mongoose = { conn: null, promise: null };
20
+ }
21
+
22
+ async function dbConnect() {
23
+ if (cached.conn) {
24
+ return cached.conn;
25
+ }
26
+
27
+ if (!cached.promise) {
28
+ const opts = {
29
+ bufferCommands: false,
30
+ };
31
+
32
+ cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
33
+ return mongoose;
34
+ });
35
+ }
36
+ try {
37
+ cached.conn = await cached.promise;
38
+ } catch (e) {
39
+ cached.promise = null;
40
+ throw e;
41
+ }
42
+
43
+ return cached.conn;
44
+ }
45
+
46
+ export default dbConnect;
lib/socket.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { io } from "socket.io-client";
2
+
3
+ // "undefined" means the URL will be computed from the `window.location` object
4
+ const URL = process.env.NEXT_PUBLIC_SOCKET_URL || "http://localhost:8000";
5
+
6
+ export const socket = io(URL, {
7
+ autoConnect: false,
8
+ });
lib/tokens.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { authenticator } from "otplib";
2
+ import QRCode from "qrcode";
3
+
4
+ export const generateTwoFactorSecret = async (email: string) => {
5
+ const secret = authenticator.generateSecret();
6
+ const otpauth = authenticator.keyuri(email, "SecureChatApp", secret);
7
+ const qrCodeUrl = await QRCode.toDataURL(otpauth);
8
+
9
+ return { secret, qrCodeUrl };
10
+ };
11
+
12
+ export const verifyTwoFactorToken = (token: string, secret: string) => {
13
+ authenticator.options = { window: 1 };
14
+ return authenticator.verify({ token, secret });
15
+ };
lib/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
models/Room.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mongoose, { Schema, Document } from 'mongoose';
2
+
3
+ export interface IRoom extends Document {
4
+ roomId: string;
5
+ createdAt: Date;
6
+ expiresAt: Date;
7
+ }
8
+
9
+ const RoomSchema: Schema = new Schema({
10
+ roomId: { type: String, required: true, unique: true },
11
+ userLimit: { type: Number, default: 10 }, // Default limit
12
+ participants: [
13
+ {
14
+ userId: String,
15
+ nickname: String,
16
+ joinedAt: { type: Date, default: Date.now },
17
+ },
18
+ ],
19
+ createdAt: { type: Date, default: Date.now },
20
+ expiresAt: { type: Date, index: { expires: "1h" } }, // Auto-delete after 1 hour of inactivity (or custom logic)
21
+ });
22
+
23
+ export default mongoose.models.Room || mongoose.model<IRoom>('Room', RoomSchema);
models/User.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mongoose, { Schema, Document } from "mongoose";
2
+
3
+ export interface IUser extends Document {
4
+ email: string;
5
+ password?: string;
6
+ image?: string;
7
+ twoFactorSecret?: string;
8
+ isTwoFactorEnabled: boolean;
9
+ createdAt: Date;
10
+ }
11
+
12
+ const UserSchema: Schema = new Schema({
13
+ email: { type: String, required: true, unique: true },
14
+ password: { type: String, required: false }, // Optional for OAuth, but we are using Credentials
15
+ image: { type: String },
16
+ twoFactorSecret: { type: String },
17
+ isTwoFactorEnabled: { type: Boolean, default: false },
18
+ createdAt: { type: Date, default: Date.now },
19
+ });
20
+
21
+ export default mongoose.models.User || mongoose.model<IUser>("User", UserSchema);
next-env.d.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ import "./.next/types/routes.d.ts";
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
next.config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ };
6
+
7
+ export default nextConfig;
package.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "secrate_chat",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "bun server.ts",
7
+ "build": "next build",
8
+ "start": "NODE_ENV=production bun server.ts",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "@radix-ui/react-scroll-area": "^1.2.10",
13
+ "@radix-ui/react-slot": "^1.2.4",
14
+ "bcryptjs": "^3.0.3",
15
+ "class-variance-authority": "^0.7.1",
16
+ "clsx": "^2.1.1",
17
+ "emoji-picker-react": "^4.15.2",
18
+ "lucide-react": "^0.554.0",
19
+ "mongoose": "^9.0.0",
20
+ "next": "16.0.4",
21
+ "next-auth": "^5.0.0-beta.30",
22
+ "next-themes": "^0.4.6",
23
+ "otplib": "^12.0.1",
24
+ "qrcode": "^1.5.4",
25
+ "react": "19.2.0",
26
+ "react-dom": "19.2.0",
27
+ "react-markdown": "^10.1.0",
28
+ "remark-gfm": "^4.0.1",
29
+ "socket.io": "^4.8.1",
30
+ "socket.io-client": "^4.8.1",
31
+ "tailwind-merge": "^3.4.0",
32
+ "tailwindcss-animate": "^1.0.7"
33
+ },
34
+ "devDependencies": {
35
+ "@tailwindcss/postcss": "^4",
36
+ "@types/bcryptjs": "^3.0.0",
37
+ "@types/node": "^20",
38
+ "@types/qrcode": "^1.5.6",
39
+ "@types/react": "^19",
40
+ "@types/react-dom": "^19",
41
+ "eslint": "^9",
42
+ "eslint-config-next": "16.0.4",
43
+ "tailwindcss": "^4",
44
+ "tw-animate-css": "^1.4.0",
45
+ "typescript": "^5"
46
+ }
47
+ }
postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
public/file.svg ADDED
public/globe.svg ADDED
public/next.svg ADDED
public/vercel.svg ADDED