feat(invite-system): implement user invite functionality with registration mode control
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m24s
Build & Push Docker Image / build (release) Successful in 6m25s

This commit is contained in:
2026-03-01 12:53:45 +01:00
parent 8c39275615
commit df4666bb63
15 changed files with 516 additions and 38 deletions

View File

@@ -112,7 +112,27 @@ const router = Router();
// POST /api/auth/register
router.post('/register', registerLimiter, async (req, res) => {
try {
const { username, display_name, email, password } = req.body;
const { username, display_name, email, password, invite_token } = req.body;
// Check registration mode
const db = getDb();
const regModeSetting = await db.get("SELECT value FROM settings WHERE key = 'registration_mode'");
const registrationMode = regModeSetting?.value || 'open';
let validatedInvite = null;
if (registrationMode === 'invite') {
if (!invite_token) {
return res.status(403).json({ error: 'Registration is currently invite-only. You need an invitation link to register.' });
}
// Validate the invite token
validatedInvite = await db.get(
'SELECT * FROM user_invites WHERE token = ? AND used_at IS NULL AND expires_at > CURRENT_TIMESTAMP',
[invite_token]
);
if (!validatedInvite) {
return res.status(403).json({ error: 'Invalid or expired invitation link.' });
}
}
if (!username || !display_name || !email || !password) {
return res.status(400).json({ error: 'All fields are required' });
@@ -138,7 +158,6 @@ router.post('/register', registerLimiter, async (req, res) => {
return res.status(400).json({ error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters long` });
}
const db = getDb();
const existing = await db.get('SELECT id FROM users WHERE email = ?', [email]);
if (existing) {
return res.status(409).json({ error: 'Email is already in use' });
@@ -161,6 +180,14 @@ router.post('/register', registerLimiter, async (req, res) => {
[username, display_name, email.toLowerCase(), hash, verificationToken, expires]
);
// Mark invite as used if applicable
if (validatedInvite) {
const newUser = await db.get('SELECT id FROM users WHERE email = ?', [email.toLowerCase()]);
if (newUser) {
await db.run('UPDATE user_invites SET used_by = ?, used_at = CURRENT_TIMESTAMP WHERE id = ?', [newUser.id, validatedInvite.id]);
}
}
// Build verification URL
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
const verifyUrl = `${baseUrl}/verify-email?token=${verificationToken}`;
@@ -189,6 +216,11 @@ router.post('/register', registerLimiter, async (req, res) => {
[username, display_name, email.toLowerCase(), hash]
);
// Mark invite as used if applicable
if (validatedInvite) {
await db.run('UPDATE user_invites SET used_by = ?, used_at = CURRENT_TIMESTAMP WHERE id = ?', [result.lastInsertRowid, validatedInvite.id]);
}
const token = generateToken(result.lastInsertRowid);
const user = await db.get('SELECT id, name, display_name, email, role, theme, language, avatar_color, avatar_image, email_verified FROM users WHERE id = ?', [result.lastInsertRowid]);