diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx
index e0f60cf..d083339 100644
--- a/client/src/pages/LoginPage.tsx
+++ b/client/src/pages/LoginPage.tsx
@@ -496,7 +496,7 @@ export default function LoginPage(): React.ReactElement {
{error}
)}
- {t('common.or')}
- {
}
}, AUTH_CODE_CLEANUP);
-const pendingStates = new Map();
+const pendingStates = new Map();
setInterval(() => {
const now = Date.now();
@@ -124,8 +124,9 @@ router.get('/login', async (req: Request, res: Response) => {
const proto = (req.headers['x-forwarded-proto'] as string) || req.protocol;
const host = (req.headers['x-forwarded-host'] as string) || req.headers.host;
const redirectUri = `${proto}://${host}/api/auth/oidc/callback`;
+ const inviteToken = req.query.invite as string | undefined;
- pendingStates.set(state, { createdAt: Date.now(), redirectUri });
+ pendingStates.set(state, { createdAt: Date.now(), redirectUri, inviteToken });
const params = new URLSearchParams({
response_type: 'code',
@@ -222,7 +223,16 @@ router.get('/callback', async (req: Request, res: Response) => {
const userCount = (db.prepare('SELECT COUNT(*) as count FROM users').get() as { count: number }).count;
const isFirstUser = userCount === 0;
- if (!isFirstUser) {
+ let validInvite: any = null;
+ if (pending.inviteToken) {
+ validInvite = db.prepare('SELECT * FROM invite_tokens WHERE token = ?').get(pending.inviteToken);
+ if (validInvite) {
+ if (validInvite.max_uses > 0 && validInvite.used_count >= validInvite.max_uses) validInvite = null;
+ if (validInvite?.expires_at && new Date(validInvite.expires_at) < new Date()) validInvite = null;
+ }
+ }
+
+ if (!isFirstUser && !validInvite) {
const setting = db.prepare("SELECT value FROM app_settings WHERE key = 'allow_registration'").get() as { value: string } | undefined;
if (setting?.value === 'false') {
return res.redirect(frontendUrl('/login?oidc_error=registration_disabled'));
@@ -242,6 +252,15 @@ router.get('/callback', async (req: Request, res: Response) => {
'INSERT INTO users (username, email, password_hash, role, oidc_sub, oidc_issuer) VALUES (?, ?, ?, ?, ?, ?)'
).run(username, email, hash, role, sub, config.issuer);
+ if (validInvite) {
+ const updated = db.prepare(
+ 'UPDATE invite_tokens SET used_count = used_count + 1 WHERE id = ? AND (max_uses = 0 OR used_count < max_uses)'
+ ).run(validInvite.id);
+ if (updated.changes === 0) {
+ console.warn(`[OIDC] Invite token ${pending.inviteToken?.slice(0, 8)}... exceeded max_uses (race condition)`);
+ }
+ }
+
user = { id: Number(result.lastInsertRowid), username, email, role } as User;
}