feat: Add a large collection of emoji and other frontend assets, including a sound file, and a backend package.json.

This commit is contained in:
Bryan1029384756
2026-01-06 17:58:56 -06:00
parent f531301863
commit abedd78893
3795 changed files with 10981 additions and 229 deletions

View File

@@ -11,14 +11,81 @@ function generateFakeSalt(username) {
}
router.post('/register', async (req, res) => {
const { username, salt, encryptedMK, hak, publicKey, signingKey, encryptedPrivateKeys } = req.body;
const { username, salt, encryptedMK, hak, publicKey, signingKey, encryptedPrivateKeys, inviteCode } = 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 });
// Step 1: Enforce Invite (unless first user)
const userCountRes = await db.query('SELECT count(*) FROM users');
const userCount = parseInt(userCountRes.rows[0].count);
if (userCount > 0) {
if (!inviteCode) {
return res.status(403).json({ error: 'Invite code required' });
}
// Check Invite validity
const inviteRes = await db.query('SELECT * FROM invites WHERE code = $1', [inviteCode]);
if (inviteRes.rows.length === 0) {
return res.status(403).json({ error: 'Invalid invite code' });
}
var invite = inviteRes.rows[0];
// Check Expiration
if (invite.expires_at && new Date() > new Date(invite.expires_at)) {
return res.status(410).json({ error: 'Invite expired' });
}
// Check Usage Limits
if (invite.max_uses !== null && invite.uses >= invite.max_uses) {
return res.status(410).json({ error: 'Invite max uses reached' });
}
}
// START TRANSACTION - To ensure invite usage and user creation are atomic
await db.query('BEGIN');
try {
// Update Invite Usage (only if enforced)
if (userCount > 0) {
await db.query('UPDATE invites SET uses = uses + 1 WHERE code = $1', [inviteCode]);
}
// Create User
// Create User
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]
);
const newUserId = result.rows[0].id;
// Assign Roles
// 1. @everyone (Always)
await db.query(`
INSERT INTO user_roles (user_id, role_id)
SELECT $1, id FROM roles WHERE name = '@everyone'
`, [newUserId]);
// 2. Owner (If first user or if admin logic allows)
if (userCount === 0) {
await db.query(`
INSERT INTO user_roles (user_id, role_id)
SELECT $1, id FROM roles WHERE name = 'Owner'
`, [newUserId]);
// Also set is_admin = true for legacy support
await db.query('UPDATE users SET is_admin = TRUE WHERE id = $1', [newUserId]);
}
await db.query('COMMIT');
res.json({ success: true, userId: result.rows[0].id });
} catch (txErr) {
await db.query('ROLLBACK');
throw txErr;
}
} catch (err) {
console.error(err);
if (err.code === '23505') { // Unique violation
@@ -50,7 +117,7 @@ router.post('/login/verify', async (req, res) => {
try {
const result = await db.query(
'SELECT id, hashed_auth_key, encrypted_master_key, encrypted_private_keys FROM users WHERE username = $1',
'SELECT id, hashed_auth_key, encrypted_master_key, encrypted_private_keys, public_identity_key FROM users WHERE username = $1',
[username]
);
@@ -66,7 +133,8 @@ router.post('/login/verify', async (req, res) => {
success: true,
userId: user.id,
encryptedMK: user.encrypted_master_key,
encryptedPrivateKeys: user.encrypted_private_keys
encryptedPrivateKeys: user.encrypted_private_keys,
publicKey: user.public_identity_key // Return Public Key
});
} else {
res.status(401).json({ error: 'Invalid credentials' });
@@ -77,4 +145,14 @@ router.post('/login/verify', async (req, res) => {
}
});
router.get('/users/public-keys', async (req, res) => {
try {
const result = await db.query('SELECT id, public_identity_key FROM users');
res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;

View File

@@ -12,4 +12,149 @@ router.get('/', async (req, res) => {
}
});
// Create New Channel
router.post('/create', async (req, res) => {
console.log('Creates Channel Body:', req.body);
const { name, type } = req.body;
if (!name) return res.status(400).json({ error: 'Channel name required' });
try {
const result = await db.query(
'INSERT INTO channels (name, type) VALUES ($1, $2) RETURNING *',
[name, type || 'text']
);
const newChannel = result.rows[0];
// DO NOT emit 'new_channel' here. Wait until keys are uploaded.
res.json({ id: newChannel.id });
} catch (err) {
console.error('Error creating channel:', err);
if (err.code === '23505') {
res.status(400).json({ error: 'Channel already exists' });
} else {
res.status(500).json({ error: 'Server error' });
}
}
});
// Notify Channel Creation (Called AFTER keys are uploaded)
router.post('/:id/notify', async (req, res) => {
const { id } = req.params;
try {
const result = await db.query('SELECT * FROM channels WHERE id = $1', [id]);
if (result.rows.length === 0) return res.status(404).json({ error: 'Channel not found' });
const channel = result.rows[0];
if (req.io) req.io.emit('new_channel', channel); // Emit NOW
res.json({ success: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Upload Channel Keys (for self or others)
// Upload Channel Keys (for self or others) - Supports Batch
router.post('/keys', async (req, res) => {
// Check if body is array
const keysToUpload = Array.isArray(req.body) ? req.body : [req.body];
if (keysToUpload.length === 0) return res.json({ success: true });
try {
await db.query('BEGIN');
for (const keyData of keysToUpload) {
const { channelId, userId, encryptedKeyBundle, keyVersion } = keyData;
if (!channelId || !userId || !encryptedKeyBundle) {
continue;
}
await db.query(
`INSERT INTO channel_keys (channel_id, user_id, encrypted_key_bundle, key_version)
VALUES ($1, $2, $3, $4)
ON CONFLICT (channel_id, user_id) DO UPDATE
SET encrypted_key_bundle = EXCLUDED.encrypted_key_bundle,
key_version = EXCLUDED.key_version`,
[channelId, userId, encryptedKeyBundle, keyVersion || 1]
);
}
await db.query('COMMIT');
res.json({ success: true, count: keysToUpload.length });
} catch (err) {
await db.query('ROLLBACK');
console.error('Error uploading channel keys:', err);
res.status(500).json({ error: 'Server error' });
}
});
// Get User's Channel Keys
router.get('/keys/:userId', async (req, res) => {
const { userId } = req.params;
try {
const result = await db.query(
'SELECT channel_id, encrypted_key_bundle, key_version FROM channel_keys WHERE user_id = $1',
[userId]
);
res.json(result.rows);
} catch (err) {
console.error('Error fetching channel keys:', err);
res.status(500).json({ error: 'Server error' });
}
});
// Update Channel Name
router.put('/:id', async (req, res) => {
const { id } = req.params;
const { name } = req.body;
if (!name) return res.status(400).json({ error: 'Name required' });
try {
const result = await db.query(
'UPDATE channels SET name = $1 WHERE id = $2 RETURNING *',
[name, id]
);
if (result.rows.length === 0) return res.status(404).json({ error: 'Channel not found' });
const updatedChannel = result.rows[0];
if (req.io) req.io.emit('channel_renamed', updatedChannel);
res.json(updatedChannel);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Delete Channel
router.delete('/:id', async (req, res) => {
const { id } = req.params;
try {
await db.query('BEGIN');
// Manual Cascade (since we didn't set FK CASCADE in schema for these yet)
await db.query('DELETE FROM messages WHERE channel_id = $1', [id]);
await db.query('DELETE FROM channel_keys WHERE channel_id = $1', [id]);
const result = await db.query('DELETE FROM channels WHERE id = $1 RETURNING *', [id]);
if (result.rows.length === 0) {
await db.query('ROLLBACK');
return res.status(404).json({ error: 'Channel not found' });
}
await db.query('COMMIT');
if (req.io) req.io.emit('channel_deleted', id);
res.json({ success: true, deletedId: id });
} catch (err) {
await db.query('ROLLBACK');
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;

75
Backend/routes/invites.js Normal file
View File

@@ -0,0 +1,75 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
// Create a new invite
router.post('/create', async (req, res) => {
const { code, encryptedPayload, createdBy, maxUses, expiresAt, keyVersion } = req.body;
if (!code || !encryptedPayload || !createdBy || !keyVersion) {
return res.status(400).json({ error: 'Missing required fields' });
}
try {
await db.query(
`INSERT INTO invites (code, encrypted_payload, created_by, max_uses, expires_at, key_version)
VALUES ($1, $2, $3, $4, $5, $6)`,
[code, encryptedPayload, createdBy, maxUses || null, expiresAt || null, keyVersion]
);
res.json({ success: true });
} catch (err) {
console.error('Error creating invite:', err);
res.status(500).json({ error: 'Server error' });
}
});
// Fetch an invite (and validate it)
router.get('/:code', async (req, res) => {
const { code } = req.params;
try {
const result = await db.query('SELECT * FROM invites WHERE code = $1', [code]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Invite not found' });
}
const invite = result.rows[0];
// Check Expiration
if (invite.expires_at && new Date() > new Date(invite.expires_at)) {
return res.status(410).json({ error: 'Invite expired' });
}
// Check Usage Limits
if (invite.max_uses !== null && invite.uses >= invite.max_uses) {
return res.status(410).json({ error: 'Invite max uses reached' });
}
// Increment Uses
await db.query('UPDATE invites SET uses = uses + 1 WHERE code = $1', [code]);
res.json({
encryptedPayload: invite.encrypted_payload,
keyVersion: invite.key_version
});
} catch (err) {
console.error('Error fetching invite:', err);
res.status(500).json({ error: 'Server error' });
}
});
// Delete an invite (Revoke) -> Triggers client-side key rotation policy warning?
// The client should call this, then rotate keys.
router.delete('/:code', async (req, res) => {
const { code } = req.params;
try {
await db.query('DELETE FROM invites WHERE code = $1', [code]);
res.json({ success: true });
} catch (err) {
console.error('Error deleting invite:', err);
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;

213
Backend/routes/roles.js Normal file
View File

@@ -0,0 +1,213 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
// Middleware to check for permissions (simplified for now)
// In a real app, you'd check if req.user has the permission
// Middleware to check for permissions
const checkPermission = (requiredPerm) => {
return async (req, res, next) => {
const userId = req.headers['x-user-id'];
if (!userId) {
return res.status(401).json({ error: 'Unauthorized: No User ID' });
}
try {
// Fetch all roles for user and aggregate permissions
const result = await db.query(`
SELECT r.permissions
FROM user_roles ur
JOIN roles r ON ur.role_id = r.id
WHERE ur.user_id = $1
`, [userId]);
let hasPermission = false;
// Check if ANY of the user's roles has the permission
for (const row of result.rows) {
const perms = row.permissions || {};
// If Manage Roles is true, or if checking for something else and they have it
if (perms[requiredPerm] === true) {
hasPermission = true;
break;
}
// Implicit Admin/Owner check: if they have 'manage_roles' and we are checking something lower?
// For "Owner" role, we seeded it with specific true values.
// But let's check for 'administrator' equivalent if we had it.
// For now, implicit check: if we need 'manage_roles', look for it.
}
if (!hasPermission) {
return res.status(403).json({ error: `Forbidden: Missing ${requiredPerm}` });
}
next();
} catch (err) {
console.error('Permission check failed:', err);
return res.status(500).json({ error: 'Internal Server Error' });
}
};
};
// GET /api/roles/permissions - Get current user's permissions
router.get('/permissions', async (req, res) => {
const userId = req.headers['x-user-id'];
if (!userId) return res.json({}); // No perms if no ID
try {
const result = await db.query(`
SELECT r.permissions
FROM user_roles ur
JOIN roles r ON ur.role_id = r.id
WHERE ur.user_id = $1
`, [userId]);
const finalPerms = {
manage_channels: false,
manage_roles: false,
create_invite: false,
embed_links: false,
attach_files: false
};
for (const row of result.rows) {
const p = row.permissions || {};
if (p.manage_channels) finalPerms.manage_channels = true;
if (p.manage_roles) finalPerms.manage_roles = true;
if (p.create_invite) finalPerms.create_invite = true;
if (p.embed_links) finalPerms.embed_links = true;
if (p.attach_files) finalPerms.attach_files = true;
}
res.json(finalPerms);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// GET /api/roles - List all roles
router.get('/', async (req, res) => {
try {
const result = await db.query('SELECT * FROM roles ORDER BY position DESC, id ASC');
res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// POST /api/roles - Create new role
router.post('/', checkPermission('manage_roles'), async (req, res) => {
const { name, color, permissions, position, is_hoist } = req.body;
try {
const result = await db.query(
'INSERT INTO roles (name, color, permissions, position, is_hoist) VALUES ($1, $2, $3, $4, $5) RETURNING *',
[name || 'new role', color || '#99aab5', permissions || {}, position || 0, is_hoist || false]
);
res.json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// PUT /api/roles/:id - Update role
router.put('/:id', checkPermission('manage_roles'), async (req, res) => {
const { id } = req.params;
const { name, color, permissions, position, is_hoist } = req.body;
// Dynamic update query
const fields = [];
const values = [];
let idx = 1;
if (name !== undefined) { fields.push(`name = $${idx++}`); values.push(name); }
if (color !== undefined) { fields.push(`color = $${idx++}`); values.push(color); }
if (permissions !== undefined) { fields.push(`permissions = $${idx++}`); values.push(permissions); }
if (position !== undefined) { fields.push(`position = $${idx++}`); values.push(position); }
if (is_hoist !== undefined) { fields.push(`is_hoist = $${idx++}`); values.push(is_hoist); }
if (fields.length === 0) return res.json({ success: true }); // Nothing to update
values.push(id);
const query = `UPDATE roles SET ${fields.join(', ')} WHERE id = $${idx} RETURNING *`;
try {
const result = await db.query(query, values);
if (result.rows.length === 0) return res.status(404).json({ error: 'Role not found' });
res.json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// DELETE /api/roles/:id - Delete role
router.delete('/:id', checkPermission('manage_roles'), async (req, res) => {
const { id } = req.params;
try {
// user_roles cascade handled by DB
const result = await db.query('DELETE FROM roles WHERE id = $1 RETURNING *', [id]);
if (result.rows.length === 0) return res.status(404).json({ error: 'Role not found' });
res.json({ success: true, deletedId: id });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// GET /api/roles/members - List members with roles
// (This is effectively GET /api/users but enriched with roles)
router.get('/members', async (req, res) => {
try {
const result = await db.query(`
SELECT u.id, u.username, u.public_identity_key,
json_agg(r.*) FILTER (WHERE r.id IS NOT NULL) as roles
FROM users u
LEFT JOIN user_roles ur ON u.id = ur.user_id
LEFT JOIN roles r ON ur.role_id = r.id
GROUP BY u.id
`);
res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// POST /api/roles/:id/assign - Assign role to user
router.post('/:id/assign', checkPermission('manage_roles'), async (req, res) => {
const { id } = req.params; // role id
const { userId } = req.body;
try {
await db.query(
'INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2) ON CONFLICT DO NOTHING',
[userId, id]
);
res.json({ success: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// POST /api/roles/:id/remove - Remove role from user
router.post('/:id/remove', checkPermission('manage_roles'), async (req, res) => {
const { id } = req.params; // role id
const { userId } = req.body;
try {
await db.query(
'DELETE FROM user_roles WHERE user_id = $1 AND role_id = $2',
[userId, id]
);
res.json({ success: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;

41
Backend/routes/upload.js Normal file
View File

@@ -0,0 +1,41 @@
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const router = express.Router();
// Ensure uploads directory exists
const uploadDir = path.join(__dirname, '../uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
// Configure Multer Storage
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadDir);
},
filename: (req, file, cb) => {
// Generate unique filename: timestamp-random.ext
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
cb(null, uniqueSuffix + ext);
}
});
const upload = multer({ storage: storage });
// POST /api/upload
router.post('/', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Return the URL to access the file
// Assumes server serves 'uploads' folder at '/uploads'
const fileUrl = `/uploads/${req.file.filename}`;
res.json({ url: fileUrl, filename: req.file.filename });
});
module.exports = router;

64
Backend/routes/voice.js Normal file
View File

@@ -0,0 +1,64 @@
const express = require('express');
const router = express.Router();
const { AccessToken } = require('livekit-server-sdk');
const db = require('../db');
// Middleware to check permissions?
// For now, simpler: assuming user is logged in (via x-user-id header check in frontend)
// Real implementation should use the checkPermission middleware or verify session
router.post('/token', async (req, res) => {
const { channelId } = req.body;
const userId = req.headers['x-user-id']; // Sent by frontend
// Default fallback if no user (should rely on auth middleware ideally)
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
// 1. Get Username (for display)
const userRes = await db.query('SELECT username FROM users WHERE id = $1', [userId]);
if (userRes.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
const username = userRes.rows[0].username;
// 2. Get Channel Name (Optional, for room name check)
// Ensure channel exists and is of type 'voice'
const channelRes = await db.query('SELECT id, type FROM channels WHERE id = $1', [channelId]);
if (channelRes.rows.length === 0) {
return res.status(404).json({ error: 'Channel not found' });
}
if (channelRes.rows[0].type !== 'voice') {
return res.status(400).json({ error: 'Not a voice channel' });
}
// 3. Generate Token
// API Key/Secret from env
const apiKey = process.env.LIVEKIT_API_KEY || 'devkey';
const apiSecret = process.env.LIVEKIT_API_SECRET || 'secret';
const at = new AccessToken(apiKey, apiSecret, {
identity: userId,
name: username,
});
at.addGrant({
roomJoin: true,
room: channelId,
canPublish: true,
canSubscribe: true,
});
const token = await at.toJwt();
res.json({ token });
} catch (err) {
console.error('Error creating voice token:', err);
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;