first commit
This commit is contained in:
10
Backend/db.js
Normal file
10
Backend/db.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const { Pool } = require('pg');
|
||||
require('dotenv').config();
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
query: (text, params) => pool.query(text, params),
|
||||
};
|
||||
1313
Backend/package-lock.json
generated
Normal file
1313
Backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
Backend/package.json
Normal file
20
Backend/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.2.1",
|
||||
"pg": "^8.16.3",
|
||||
"redis": "^5.10.0",
|
||||
"socket.io": "^4.8.3"
|
||||
}
|
||||
}
|
||||
15
Backend/redis.js
Normal file
15
Backend/redis.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const { createClient } = require('redis');
|
||||
require('dotenv').config();
|
||||
|
||||
const client = createClient({
|
||||
url: process.env.REDIS_URL
|
||||
});
|
||||
|
||||
client.on('error', (err) => console.log('Redis Client Error', err));
|
||||
|
||||
(async () => {
|
||||
await client.connect();
|
||||
console.log('Redis connected');
|
||||
})();
|
||||
|
||||
module.exports = client;
|
||||
80
Backend/routes/auth.js
Normal file
80
Backend/routes/auth.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// Helper to generate fake salt for user privacy
|
||||
function generateFakeSalt(username) {
|
||||
return crypto.createHmac('sha256', 'SERVER_SECRET_KEY') // In prod, use env var
|
||||
.update(username)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
router.post('/register', async (req, res) => {
|
||||
const { username, salt, encryptedMK, hak, publicKey, signingKey, encryptedPrivateKeys } = req.body;
|
||||
try {
|
||||
const result = await db.query(
|
||||
`INSERT INTO users (username, client_salt, encrypted_master_key, hashed_auth_key, public_identity_key, public_signing_key, encrypted_private_keys)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
|
||||
[username, salt, encryptedMK, hak, publicKey, signingKey, encryptedPrivateKeys]
|
||||
);
|
||||
res.json({ success: true, userId: result.rows[0].id });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (err.code === '23505') { // Unique violation
|
||||
res.status(400).json({ error: 'Username taken' });
|
||||
} else {
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/login/salt', async (req, res) => {
|
||||
const { username } = req.body;
|
||||
try {
|
||||
const result = await db.query('SELECT client_salt FROM users WHERE username = $1', [username]);
|
||||
if (result.rows.length > 0) {
|
||||
res.json({ salt: result.rows[0].client_salt });
|
||||
} else {
|
||||
// Return fake salt to prevent enumeration
|
||||
res.json({ salt: generateFakeSalt(username) });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/login/verify', async (req, res) => {
|
||||
const { username, dak } = req.body;
|
||||
|
||||
try {
|
||||
const result = await db.query(
|
||||
'SELECT hashed_auth_key, encrypted_master_key, encrypted_private_keys FROM users WHERE username = $1',
|
||||
[username]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
const hashedDAK = crypto.createHash('sha256').update(dak).digest('hex');
|
||||
|
||||
if (hashedDAK === user.hashed_auth_key) {
|
||||
res.json({
|
||||
success: true,
|
||||
userId: user.id,
|
||||
encryptedMK: user.encrypted_master_key,
|
||||
encryptedPrivateKeys: user.encrypted_private_keys
|
||||
});
|
||||
} else {
|
||||
res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
15
Backend/routes/channels.js
Normal file
15
Backend/routes/channels.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const result = await db.query('SELECT * FROM channels ORDER BY name ASC');
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
46
Backend/schema.sql
Normal file
46
Backend/schema.sql
Normal file
@@ -0,0 +1,46 @@
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
client_salt TEXT NOT NULL,
|
||||
encrypted_master_key TEXT NOT NULL,
|
||||
hashed_auth_key TEXT NOT NULL,
|
||||
public_identity_key TEXT NOT NULL,
|
||||
public_signing_key TEXT NOT NULL,
|
||||
encrypted_private_keys TEXT NOT NULL, -- Added this column
|
||||
is_admin BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channels (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
type TEXT DEFAULT 'text',
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
permissions JSONB
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_keys (
|
||||
channel_id UUID NOT NULL,
|
||||
user_id UUID NOT NULL,
|
||||
encrypted_key_bundle TEXT NOT NULL,
|
||||
key_version INTEGER DEFAULT 1,
|
||||
PRIMARY KEY (channel_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID NOT NULL,
|
||||
sender_id UUID NOT NULL,
|
||||
ciphertext TEXT NOT NULL,
|
||||
nonce TEXT NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
key_version INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
31
Backend/scripts/init-db.js
Normal file
31
Backend/scripts/init-db.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const db = require('../db');
|
||||
|
||||
async function initDb() {
|
||||
try {
|
||||
const schemaPath = path.join(__dirname, '../schema.sql');
|
||||
const schemaSql = fs.readFileSync(schemaPath, 'utf8');
|
||||
|
||||
console.log('Applying schema...');
|
||||
await db.query(schemaSql);
|
||||
|
||||
// Seed Channels
|
||||
const channels = ['general', 'random'];
|
||||
for (const name of channels) {
|
||||
await db.query(
|
||||
`INSERT INTO channels (name) VALUES ($1) ON CONFLICT (name) DO NOTHING`,
|
||||
[name]
|
||||
);
|
||||
}
|
||||
console.log('Channels seeded.');
|
||||
|
||||
console.log('Schema applied successfully.');
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('Error applying schema:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
initDb();
|
||||
97
Backend/server.js
Normal file
97
Backend/server.js
Normal file
@@ -0,0 +1,97 @@
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const { Server } = require('socket.io');
|
||||
const cors = require('cors');
|
||||
require('dotenv').config();
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server, {
|
||||
cors: {
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST']
|
||||
}
|
||||
});
|
||||
|
||||
const authRoutes = require('./routes/auth');
|
||||
const channelRoutes = require('./routes/channels');
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/channels', channelRoutes);
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send('Secure Chat Backend Running');
|
||||
});
|
||||
|
||||
const redisClient = require('./redis');
|
||||
const db = require('./db');
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log('User connected:', socket.id);
|
||||
|
||||
socket.on('join_channel', async (channelId) => {
|
||||
socket.join(channelId);
|
||||
console.log(`User ${socket.id} joined channel ${channelId}`);
|
||||
// Load recent messages
|
||||
try {
|
||||
const result = await db.query(
|
||||
`SELECT m.*, u.username
|
||||
FROM messages m
|
||||
JOIN users u ON m.sender_id = u.id
|
||||
WHERE m.channel_id = $1
|
||||
ORDER BY m.created_at DESC LIMIT 50`,
|
||||
[channelId]
|
||||
);
|
||||
socket.emit('recent_messages', result.rows.reverse());
|
||||
} catch (err) {
|
||||
console.error('Error fetching messages:', err);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('send_message', async (data) => {
|
||||
// data: { channelId, senderId, ciphertext, nonce, signature, keyVersion }
|
||||
const { channelId, senderId, ciphertext, nonce, signature, keyVersion } = data;
|
||||
|
||||
try {
|
||||
// Store in DB
|
||||
const result = await db.query(
|
||||
`INSERT INTO messages (channel_id, sender_id, ciphertext, nonce, signature, key_version)
|
||||
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, created_at`,
|
||||
[channelId, senderId, ciphertext, nonce, signature, keyVersion]
|
||||
);
|
||||
|
||||
const message = {
|
||||
id: result.rows[0].id,
|
||||
created_at: result.rows[0].created_at,
|
||||
...data
|
||||
};
|
||||
|
||||
// Get username for display
|
||||
const userRes = await db.query('SELECT username FROM users WHERE id = $1', [senderId]);
|
||||
if (userRes.rows.length > 0) {
|
||||
message.username = userRes.rows[0].username;
|
||||
}
|
||||
|
||||
// Broadcast to channel
|
||||
io.to(channelId).emit('new_message', message);
|
||||
} catch (err) {
|
||||
console.error('Error saving message:', err);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('typing', (data) => {
|
||||
socket.to(data.channelId).emit('user_typing', { username: data.username });
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('User disconnected:', socket.id);
|
||||
});
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user