diff --git a/src/server/accounts.js b/src/server/accounts.js index adbeeb2..043ca4e 100644 --- a/src/server/accounts.js +++ b/src/server/accounts.js @@ -1,6 +1,6 @@ // Accounts, passwords, browser sessions and API tokens. // -// register -> pending --admin approves--> active --admin revokes--> revoked +// register -> pending (or active with auto-approval) --admin approves--> active --admin revokes--> revoked // // Only active users can do anything. Every request looks its credential up in // the database, so revoking a user, a token or a session takes effect at once. @@ -66,6 +66,19 @@ export class Accounts { this.store = store; } + registrationSettings() { + return { autoApprove: this.store.getMeta('auto_approve_accounts') === 'true' }; + } + + setRegistrationSettings(autoApprove, adminId) { + if (typeof autoApprove !== 'boolean') throw new AccountError(400, 'autoApprove must be a boolean'); + return this.store.tx(() => { + this.store.setMeta('auto_approve_accounts', autoApprove); + this.store.event('registration_settings', adminId, { autoApprove }); + return this.registrationSettings(); + }); + } + // --- self service async register(name, password) { @@ -73,9 +86,15 @@ export class Accounts { checkPassword(password); if (this.byName(name)) throw new AccountError(409, 'username is taken'); const hash = await hashPassword(password); - this.store.run(`INSERT INTO users (name, password_hash, role, status, created_at) VALUES (?, ?, 'contributor', 'pending', ?)`, name, hash, now()); - this.store.event('register', null, { name }); - return { name, status: 'pending' }; + return this.store.tx(() => { + const { autoApprove } = this.registrationSettings(); + const status = autoApprove ? 'active' : 'pending'; + const createdAt = now(); + this.store.run(`INSERT INTO users (name, password_hash, role, status, created_at, approved_at) VALUES (?, ?, 'contributor', ?, ?, ?)`, name, hash, status, createdAt, autoApprove ? createdAt : null); + this.store.event('register', null, { name }); + if (autoApprove) this.store.event('user_approved', null, { name, automatic: true }); + return { name, status }; + }); } async login(name, password) { diff --git a/src/server/app.js b/src/server/app.js index 5f07b2e..8afea5d 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -982,6 +982,15 @@ export function createApp(ctx) { }); return { authors, limits: LIMITS }; }); + router.on('GET', '/api/admin/registration', ({ req }) => { + auth(req, { role: 'admin', session: true }); + return accounts.registrationSettings(); + }); + router.on('POST', '/api/admin/registration', async ({ req }) => { + const admin = auth(req, { role: 'admin', session: true }); + const body = await readJson(req, 1024); + return accountCall(() => accounts.setRegistrationSettings(body.autoApprove, admin.id)); + }); router.on('GET', '/api/admin/users', ({ req, query }) => { auth(req, { role: 'admin', session: true }); return { roles: ROLES, users: accounts.listUsers(query.get('status') ?? undefined), work: claimedWork(), reviewers: reviewerStats(store) }; diff --git a/src/server/db.js b/src/server/db.js index c064755..5a5d3a1 100644 --- a/src/server/db.js +++ b/src/server/db.js @@ -4,9 +4,9 @@ import { DatabaseSync } from 'node:sqlite'; import crypto from 'node:crypto'; import { formatSymbolId } from '../shared/annotations.js'; -// Accounts. People register with a password and stay 'pending' until an admin -// approves them. Approved users create API tokens for their agents; the browser -// uses short-lived sessions. Both are stored as SHA-256 hashes only. +// Accounts. Registrations stay 'pending' until approved unless auto-approval is on. +// Approved users create API tokens for their agents; the browser uses short-lived +// sessions. Both are stored as SHA-256 hashes only. const USERS_TABLES = ` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, diff --git a/src/server/web/app.js b/src/server/web/app.js index f417584..d1da6c6 100644 --- a/src/server/web/app.js +++ b/src/server/web/app.js @@ -226,9 +226,11 @@ $('login-form').addEventListener('submit', async (e) => { $('register-form').addEventListener('submit', async (e) => { e.preventDefault(); try { - await post('/api/register', formData(e.target)); + const registration = await post('/api/register', formData(e.target)); e.target.reset(); - $('register-msg').textContent = 'Registered. You can log in once an admin approves your account.'; + $('register-msg').textContent = registration.status === 'active' + ? 'Registered. Your account is approved; you can log in now.' + : 'Registered. You can log in once an admin approves your account.'; } catch (err) { $('register-msg').textContent = err.message; } @@ -859,11 +861,32 @@ async function adminAction(name, action, body) { return null; } } +$('auto-approve').addEventListener('change', async (e) => { + const toggle = e.target; + const previous = !toggle.checked; + toggle.disabled = true; + $('registration-msg').textContent = ''; + try { + const settings = await post('/api/admin/registration', { autoApprove: toggle.checked }); + toggle.checked = settings.autoApprove; + $('registration-msg').textContent = settings.autoApprove + ? 'Auto approval is on. New accounts can log in immediately.' + : 'Auto approval is off. New accounts need manual approval.'; + } catch (err) { + toggle.checked = previous; + $('registration-msg').textContent = err.message; + } finally { + toggle.disabled = false; + } +}); + let adminData = null; async function loadAdmin() { - const [users, authors] = await Promise.all([api('/api/admin/users'), api('/api/admin/authors').catch(() => ({ authors: [] }))]); + const [users, authors, registration] = await Promise.all([api('/api/admin/users'), api('/api/admin/authors').catch(() => ({ authors: [] })), api('/api/admin/registration')]); adminData = users; + $('auto-approve').checked = registration.autoApprove; + $('auto-approve').disabled = false; renderAdmin(); renderAuthors(authors.authors); } diff --git a/src/server/web/index.html b/src/server/web/index.html index 5445011..373431b 100644 --- a/src/server/web/index.html +++ b/src/server/web/index.html @@ -6,7 +6,7 @@
An admin checks every new account. Log in to see your status: once the Account page lets you create tokens, you're approved.
When auto approval is on, you can log in immediately after registering. Otherwise, wait for an admin to approve your account. Once the Account page lets you create tokens, you're approved.
On your Account page, give the token a label (for example "laptop") and press Create token. The page then shows your personal install line with the token already filled in. Copy it; it's shown only once.
When on, new accounts can log in immediately. Existing pending accounts still need manual approval.
+ @@ -383,6 +389,6 @@Clank Together · source is open: git clone https://clank.rodecomp.online/git/clank/game-decomp.git