Let people register with a personal email and an invite code
The invite gate was unreachable. PIG shares its identity provider with another application, and self-registration there is deliberately switched off — so somebody with a personal address and a valid invite code could never obtain a token, and therefore could never reach the endpoint that accepts the code. The gate was real and nothing could ever arrive at it. Opening self-registration on the provider would have opened it for the neighbouring application too, which is precisely why it was closed. So PIG now mints the account itself through the provider's admin API, and only after the invite validates. The provider stays shut; the invite becomes the actual gate. Order is deliberate: validate the invite, create the auth account, create the profile, consume the invite. If the profile write fails the auth account is deleted again — otherwise someone could sign in with no profile and no way to obtain one, because their invite would look spent. An administrator's address does NOT bypass this. The bypass in /api/signup exists to bootstrap the first admin from an account that already exists; here an account is created from nothing, and an ungated version of that is simply an open registration endpoint. Registration signs the person in on success rather than returning them to a login form to retype the password they entered ten seconds earlier. An address that already exists in the provider but has no PIG profile is detected and pointed at sign-in, since /api/signup handles that case properly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+21
-1
@@ -42,6 +42,7 @@ import type { Config } from './lib/config';
|
||||
import { AuthError, createAuthenticator, type Principal } from './lib/auth';
|
||||
import { CapacityService } from './services/capacity';
|
||||
import { createSignupRoute } from './routes/signup';
|
||||
import { createRegisterRoute } from './routes/register';
|
||||
|
||||
type Env = { Variables: { principal: Principal } };
|
||||
|
||||
@@ -69,6 +70,14 @@ export function createApp(config: Config, db: Database) {
|
||||
*/
|
||||
app.route('/', createSignupRoute(config, db));
|
||||
|
||||
/*
|
||||
* Registration. Also before the auth middleware, and necessarily so: the
|
||||
* caller has no account yet, so there is no token to present. The invite
|
||||
* code is the only gate, which is why it is validated before anything is
|
||||
* created anywhere.
|
||||
*/
|
||||
app.route('/', createRegisterRoute(config, db));
|
||||
|
||||
/** Liveness. Unauthenticated by design so a load balancer can reach it. */
|
||||
app.get('/api/health', (c) => c.json({ ok: true, service: 'pig', version: '0.1.0' }));
|
||||
|
||||
@@ -83,6 +92,9 @@ export function createApp(config: Config, db: Database) {
|
||||
supabaseAnonKey: config.SUPABASE_ANON_KEY ?? null,
|
||||
authDisabled: !config.SUPABASE_URL,
|
||||
inviteRequired: Boolean(config.PIG_INVITE_CODE),
|
||||
// Whether someone with an invite can create an account outright, or must
|
||||
// be provisioned by an administrator first.
|
||||
canSelfRegister: Boolean(config.SUPABASE_URL && config.SUPABASE_SERVICE_KEY),
|
||||
accents: ACCENTS.map((a) => ({ key: a.key, label: a.label })),
|
||||
teams: TEAMS,
|
||||
}),
|
||||
@@ -91,7 +103,15 @@ export function createApp(config: Config, db: Database) {
|
||||
// Everything below requires a principal.
|
||||
app.use('/api/*', async (c, next) => {
|
||||
const path = new URL(c.req.url).pathname;
|
||||
if (path === '/api/health' || path === '/api/config' || path === '/api/signup') {
|
||||
// Public by necessity: health for load balancers, config for the front
|
||||
// end before sign-in, and the two join routes for people who are not yet
|
||||
// members. Each verifies whatever it needs itself.
|
||||
if (
|
||||
path === '/api/health' ||
|
||||
path === '/api/config' ||
|
||||
path === '/api/signup' ||
|
||||
path === '/api/register'
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user