tweaked ui and updated invite command

This commit is contained in:
2025-10-04 10:27:45 -04:00
parent 834e77a93e
commit 053ffe51f7
9 changed files with 496 additions and 343 deletions

22
backend/.env.example Normal file
View File

@@ -0,0 +1,22 @@
# Example backend .env
# Set the host/interface to bind to (for Tailscale use your 100.x.y.z address)
HOST=0.0.0.0
PORT=3002
# Optional: fully-qualified base URLs
BACKEND_BASE=http://100.x.y.z:3002
FRONTEND_BASE=http://100.x.y.z:3000
# CORS origin (frontend origin) - set to frontend base for tighter security
CORS_ORIGIN=http://100.x.y.z:3000
# Optional invite delete protection
INVITE_API_KEY=replace-with-a-secret
# Discord credentials
DISCORD_CLIENT_ID=your_client_id
DISCORD_CLIENT_SECRET=your_client_secret
DISCORD_BOT_TOKEN=your_bot_token
# Encryption key for backend db.json
ENCRYPTION_KEY=pick-a-long-random-string

View File

@@ -4,17 +4,68 @@ const cors = require('cors');
const app = express();
const port = process.env.PORT || 3001;
const host = process.env.HOST || '0.0.0.0';
const corsOrigin = process.env.CORS_ORIGIN || null; // e.g. 'http://example.com' or '*' or 'http://127.0.0.1:3000'
app.use(cors());
// Convenience base URLs (override if you want fully-qualified URLs)
const BACKEND_BASE = process.env.BACKEND_BASE || `http://${host}:${port}`;
const FRONTEND_BASE = process.env.FRONTEND_BASE || 'http://localhost:3000';
if (corsOrigin) {
app.use(cors({ origin: corsOrigin }));
} else {
app.use(cors());
}
app.use(express.json());
const axios = require('axios');
const crypto = require('crypto');
// Invite token helpers: short-lived HMAC-signed token so frontend can authorize invite deletes
const INVITE_TOKEN_TTL_MS = 5 * 60 * 1000; // 5 minutes
const inviteTokenSecret = process.env.INVITE_TOKEN_SECRET || process.env.ENCRYPTION_KEY || 'fallback-invite-secret';
function generateInviteToken(guildId) {
const payload = JSON.stringify({ gid: guildId, iat: Date.now() });
const payloadB64 = Buffer.from(payload).toString('base64url');
const h = crypto.createHmac('sha256', inviteTokenSecret).update(payloadB64).digest('base64url');
return `${payloadB64}.${h}`;
}
function verifyInviteToken(token) {
try {
if (!token) return null;
const parts = token.split('.');
if (parts.length !== 2) return null;
const [payloadB64, sig] = parts;
const expected = crypto.createHmac('sha256', inviteTokenSecret).update(payloadB64).digest('base64url');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) return null;
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf8'));
if (!payload || !payload.gid || !payload.iat) return null;
if (Date.now() - payload.iat > INVITE_TOKEN_TTL_MS) return null;
return payload;
} catch (e) {
return null;
}
}
app.get('/auth/discord', (req, res) => {
const url = `https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&redirect_uri=${encodeURIComponent('http://localhost:3002/auth/discord/callback')}&response_type=code&scope=identify%20guilds`;
const redirectUri = `${BACKEND_BASE}/auth/discord/callback`;
const url = `https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=identify%20guilds`;
res.redirect(url);
});
// Provide a short-lived invite token for frontend actions (delete). Not a replacement for proper auth
app.get('/api/servers/:guildId/invite-token', (req, res) => {
const { guildId } = req.params;
try {
const token = generateInviteToken(guildId);
res.json({ token });
} catch (err) {
res.status(500).json({ success: false, message: 'Failed to generate token' });
}
});
app.get('/auth/discord/callback', async (req, res) => {
const code = req.query.code;
if (!code) {
@@ -27,7 +78,7 @@ app.get('/auth/discord/callback', async (req, res) => {
params.append('client_secret', process.env.DISCORD_CLIENT_SECRET);
params.append('grant_type', 'authorization_code');
params.append('code', code);
params.append('redirect_uri', 'http://localhost:3002/auth/discord/callback');
params.append('redirect_uri', `${BACKEND_BASE}/auth/discord/callback`);
const response = await axios.post('https://discord.com/api/oauth2/token', params, {
headers: {
@@ -55,7 +106,7 @@ app.get('/auth/discord/callback', async (req, res) => {
const db = readDb();
user.theme = db.users && db.users[user.id] ? db.users[user.id].theme : 'light';
const guilds = adminGuilds;
res.redirect(`http://localhost:3000/dashboard?user=${encodeURIComponent(JSON.stringify(user))}&guilds=${encodeURIComponent(JSON.stringify(guilds))}`);
res.redirect(`${FRONTEND_BASE}/dashboard?user=${encodeURIComponent(JSON.stringify(user))}&guilds=${encodeURIComponent(JSON.stringify(guilds))}`);
} catch (error) {
console.error('Error during Discord OAuth2 callback:', error);
res.status(500).send('Internal Server Error');
@@ -371,6 +422,12 @@ app.post('/api/servers/:guildId/invites', async (req, res) => {
});
app.delete('/api/servers/:guildId/invites/:code', async (req, res) => {
// Require a short-lived invite token issued by /api/servers/:guildId/invite-token
const providedToken = req.headers['x-invite-token'];
const payload = verifyInviteToken(providedToken);
if (!payload || payload.gid !== req.params.guildId) {
return res.status(401).json({ success: false, message: 'Unauthorized: missing or invalid invite token' });
}
try {
const { guildId, code } = req.params;
const db = readDb();
@@ -404,6 +461,6 @@ const bot = require('../discord-bot');
bot.login();
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
app.listen(port, host, () => {
console.log(`Server is running on ${host}:${port}`);
});