feat(invite-system): implement user invite functionality with registration mode control
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { Router } from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { getDb } from '../config/database.js';
|
||||
import { authenticateToken, requireAdmin } from '../middleware/auth.js';
|
||||
import { isMailerConfigured, sendInviteEmail } from '../config/mailer.js';
|
||||
import { log } from '../config/logger.js';
|
||||
|
||||
const EMAIL_RE = /^[^\s@]{1,64}@[^\s@]{1,253}\.[^\s@]{2,}$/;
|
||||
@@ -164,4 +166,98 @@ router.put('/users/:id/password', authenticateToken, requireAdmin, async (req, r
|
||||
}
|
||||
});
|
||||
|
||||
// ── User Invite System ─────────────────────────────────────────────────────
|
||||
|
||||
// POST /api/admin/invites - Create and send an invite
|
||||
router.post('/invites', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
if (!email || !EMAIL_RE.test(email)) {
|
||||
return res.status(400).json({ error: 'A valid email address is required' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Check if user with this email already exists
|
||||
const existing = await db.get('SELECT id FROM users WHERE email = ?', [email.toLowerCase()]);
|
||||
if (existing) {
|
||||
return res.status(409).json({ error: 'A user with this email already exists' });
|
||||
}
|
||||
|
||||
// Check if there's already a pending invite for this email
|
||||
const existingInvite = await db.get(
|
||||
'SELECT id FROM user_invites WHERE email = ? AND used_at IS NULL AND expires_at > CURRENT_TIMESTAMP',
|
||||
[email.toLowerCase()]
|
||||
);
|
||||
if (existingInvite) {
|
||||
return res.status(409).json({ error: 'There is already a pending invite for this email' });
|
||||
}
|
||||
|
||||
const token = uuidv4();
|
||||
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // 7 days
|
||||
|
||||
await db.run(
|
||||
'INSERT INTO user_invites (token, email, created_by, expires_at) VALUES (?, ?, ?, ?)',
|
||||
[token, email.toLowerCase(), req.user.id, expiresAt]
|
||||
);
|
||||
|
||||
// Send invite email if SMTP is configured
|
||||
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const inviteUrl = `${baseUrl}/register?invite=${token}`;
|
||||
|
||||
// Load app name
|
||||
const brandingSetting = await db.get("SELECT value FROM settings WHERE key = 'app_name'");
|
||||
const appName = brandingSetting?.value || 'Redlight';
|
||||
|
||||
if (isMailerConfigured()) {
|
||||
try {
|
||||
await sendInviteEmail(email.toLowerCase(), inviteUrl, appName);
|
||||
} catch (mailErr) {
|
||||
log.admin.warn(`Invite email failed (non-fatal): ${mailErr.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json({ invite: { token, email: email.toLowerCase(), expiresAt, inviteUrl } });
|
||||
} catch (err) {
|
||||
log.admin.error(`Create invite error: ${err.message}`);
|
||||
res.status(500).json({ error: 'Invite could not be created' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/admin/invites - List all invites
|
||||
router.get('/invites', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const invites = await db.all(`
|
||||
SELECT ui.id, ui.token, ui.email, ui.expires_at, ui.created_at, ui.used_at,
|
||||
creator.name as created_by_name,
|
||||
used_user.name as used_by_name
|
||||
FROM user_invites ui
|
||||
LEFT JOIN users creator ON creator.id = ui.created_by
|
||||
LEFT JOIN users used_user ON used_user.id = ui.used_by
|
||||
ORDER BY ui.created_at DESC
|
||||
`);
|
||||
res.json({ invites });
|
||||
} catch (err) {
|
||||
log.admin.error(`List invites error: ${err.message}`);
|
||||
res.status(500).json({ error: 'Invites could not be loaded' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/admin/invites/:id - Delete an invite
|
||||
router.delete('/invites/:id', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const invite = await db.get('SELECT id FROM user_invites WHERE id = ?', [req.params.id]);
|
||||
if (!invite) {
|
||||
return res.status(404).json({ error: 'Invite not found' });
|
||||
}
|
||||
await db.run('DELETE FROM user_invites WHERE id = ?', [req.params.id]);
|
||||
res.json({ message: 'Invite deleted' });
|
||||
} catch (err) {
|
||||
log.admin.error(`Delete invite error: ${err.message}`);
|
||||
res.status(500).json({ error: 'Invite could not be deleted' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user