diff --git a/migrations/1780100000000_registration-invites.js b/migrations/1780100000000_registration-invites.js new file mode 100644 index 00000000..f29f5ab6 --- /dev/null +++ b/migrations/1780100000000_registration-invites.js @@ -0,0 +1,38 @@ +// Invite-only registration. +// +// registration_enabled is a single on/off switch: open to anyone, or closed to +// everyone. This adds the middle setting an operator actually wants — open to +// people you invited. A code is single-use, expires, and can be revoked or +// deleted without touching the account it created. +// +// The code is stored hashed. An invite grants account creation, so a leaked +// settings dump or database backup should not hand someone a working code, the +// same reason password reset tokens are not stored in the clear. + +exports.up = pgm => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS registration_invites ( + id SERIAL PRIMARY KEY, + code_hash TEXT NOT NULL UNIQUE, + -- The last few characters, so the list can show which code a row is + -- without being able to reconstruct it. + code_hint TEXT NOT NULL, + note TEXT NOT NULL DEFAULT '', + created_by INTEGER REFERENCES users(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ NOT NULL, + -- Set when used. The row is kept so an admin can see who used which code. + used_at TIMESTAMPTZ, + used_by INTEGER REFERENCES users(id) ON DELETE SET NULL, + -- Set when revoked. Separate from deletion: a revoked code stays visible. + revoked_at TIMESTAMPTZ, + revoked_by INTEGER REFERENCES users(id) ON DELETE SET NULL + ); + CREATE INDEX IF NOT EXISTS idx_registration_invites_hash ON registration_invites(code_hash); + CREATE INDEX IF NOT EXISTS idx_registration_invites_expires ON registration_invites(expires_at); + `); +}; + +exports.down = pgm => { + pgm.sql('DROP TABLE IF EXISTS registration_invites;'); +}; diff --git a/public/components/admin.html b/public/components/admin.html index 15e526fe..3d0f3ccb 100644 --- a/public/components/admin.html +++ b/public/components/admin.html @@ -373,6 +373,40 @@ + +
+
+

Registration Invitations

+
+
+
+ Invite only +
+ +

Sits between open and closed registration. With registration disabled entirely, nobody can register even with a code.

+
+
+ +
+ +
+ + + +
+
+

The code is shown once, here. Only its hash is stored, so it cannot be read again afterwards.

+
+ +
+
+
+
diff --git a/public/index.html b/public/index.html index bc3c0613..f7a3bfa5 100644 --- a/public/index.html +++ b/public/index.html @@ -111,6 +111,11 @@
+ +