Add profile creation — close the gap between signing in and being a member
Deploying and then trying to actually use it surfaced a dead end: /api/signup was exempted from auth but never implemented, so a real person could sign in, receive 403 needs_profile, and have nowhere to go. Authentication worked; joining did not. The route is mounted before the auth middleware, because requiring membership to reach the route that grants membership is circular. It verifies the token itself and then requires one of two things: - A valid invite code. Stored hashed, optionally pinned to an address, optionally expiring, consumed on redemption with the redeemer recorded. - Presence in PIG_ADMIN_EMAILS. The bootstrap path, which exists because on a fresh deployment nobody can issue an invite since nobody can sign in to issue one. The bootstrap path is narrow by construction: the address must be listed in server-side configuration AND match the verified email claim on the token. Admin rights are never read from the request body, so a crafted payload cannot grant them. Two behaviours worth noting. A row that was invited but never signed into is claimed rather than rejected, binding it to the identity that just proved ownership of the address. And a resubmitted form returns the existing user instead of erroring, because a double-tap should be harmless. The front end now treats needs_profile as a step in the flow rather than an error, showing a team picker. Sending someone back to a login screen they have already completed is a loop with no exit. Also: the seed no longer creates the dev@localhost admin row under NODE_ENV=production. It was unreachable (no auth subject, so nobody can sign in as it), but an admin-flagged placeholder in a real deployment is a trap. Verified: rejects a missing token, rejects an invalid body, claims a pre-existing row, and is idempotent on resubmission. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -321,8 +321,12 @@ async function seed() {
|
||||
// Only when the table is empty. With authentication disabled in development
|
||||
// the API adopts the first user it finds, so creating one unconditionally
|
||||
// could hand a local session to the wrong identity.
|
||||
//
|
||||
// Skipped entirely in production: a platform-admin row with no auth subject
|
||||
// is unreachable (nobody can sign in as it), but leaving an admin-flagged
|
||||
// placeholder in a real deployment is untidy at best and a trap at worst.
|
||||
const existing = await db.select({ id: users.id }).from(users).limit(1);
|
||||
if (existing.length === 0) {
|
||||
if (existing.length === 0 && process.env.NODE_ENV !== 'production') {
|
||||
const [devUser] = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
@@ -343,6 +347,11 @@ async function seed() {
|
||||
}
|
||||
console.log(' Development user created (dev@localhost), on all three teams.');
|
||||
}
|
||||
} else if (existing.length === 0) {
|
||||
console.log(
|
||||
' Skipped the development user (NODE_ENV=production). Sign in and create a ' +
|
||||
'profile; an address in PIG_ADMIN_EMAILS bootstraps the first admin.',
|
||||
);
|
||||
}
|
||||
|
||||
console.log('\nUnresolved names, recorded rather than invented:');
|
||||
|
||||
Reference in New Issue
Block a user