Skip to content

fix(dashboard): seed default operator, fix infinite /setup redirect - #119

Merged
AbirAbbas merged 4 commits into
mainfrom
fix/operator-default-account
Jun 15, 2026
Merged

fix(dashboard): seed default operator, fix infinite /setup redirect#119
AbirAbbas merged 4 commits into
mainfrom
fix/operator-default-account

Conversation

@AbirAbbas

Copy link
Copy Markdown
Contributor

Problem

Signing into the operator dashboard infinitely redirected back to the setup screen, and the console offered a public "Create operator account" signup that doesn't make sense for an operator console.

Root cause of the redirect: operator status lives in a separate suite_operators table, populated only as a side-effect of the first better-auth signup — inside one all-or-nothing transaction that also wrote suite_users + suite_memberships (the latter depends on a default-tenant row existing). Any partial failure rolled the whole thing back, so suite_operators stayed empty, operatorCount() stayed 0 forever, and /login, /, and everything else bounced to /setup.

Approach

Replace the fragile signup-bootstrap with a seeded default operator account, so there's never an operator-less state:

  1. Seed a default operator on first boot (lib/bootstrap-operator.ts, run from a Next.js instrumentation.ts hook). Credentials are configurable via AF_STACK_DEFAULT_OPERATOR_EMAIL / _PASSWORD / _NAME and documented in the README + .env.example. The seed is idempotent (gated on an empty suite_operators table — never re-runs, never clobbers a changed password), login-compatible (hashes with better-auth/crypto, writes the credential account row), and resilient to boot ordering (retries until the migrated auth tables exist; never crashes the process).
  2. Harden the mirror hook — the user.create hook's three mirror steps now run independently/best-effort, so a partial failure can never undo operator creation again. Also set emailAndPassword.disableSignUp to close public self-provisioning.
  3. Remove the first-run wizard — delete the /setup and /signup pages, the count===0 → /setup redirects, the allow-list entries, and the login-form "Sign up" link. The admin layout relies on requireOperator() alone.

Default credentials

Field Default
Email operator@af-stack.local
Password changeme123

Documented in the README with change-password guidance and an AF_STACK_DEFAULT_OPERATOR_DISABLED opt-out. Scope was confirmed with the requester as "fix login + default account now"; an in-dashboard "add more operators" UI is intentionally left as a follow-up.

Validation contract

  • Fresh boot (no operators) → exactly one operator exists after the dashboard starts; operatorCount() == 1.
  • /login always shows the sign-in form (never redirects to /setup).
  • Sign-in with the default credentials lands on / with no redirect loop.
  • Seed is idempotent across restarts; a changed password is never reset.
  • Credentials are env-configurable and documented.
  • Public self-signup is gone (pages removed + disableSignUp).
  • Mirror-hook partial failure can't leave the deployment operator-less.

Testing

  • tsc --noEmit clean; eslint clean on all changed files; prettier --check clean.
  • Built on a fresh worktree off latest origin/main (unrelated local changes excluded).
  • Not yet exercised end-to-end against a live docker compose up — worth a manual smoke (boot fresh stack, sign in with the default account).

Note (separate concern, flagged by the requester)

The customer app and operator dashboard share the same better-auth user/account tables, so one credential authenticates against both — operator access is gated only at authorization (suite_operators). Tracking that separately (see follow-up issue).

🤖 Generated with Claude Code

AbirAbbas and others added 4 commits June 15, 2026 14:09
The user-create hook mirrored a new better-auth user into suite_users,
suite_memberships, and suite_operators inside one all-or-nothing
transaction. Any partial failure (e.g. the default-tenant row not
existing when suite_memberships is written) rolled back the
suite_operators insert too — leaving the deployment with ZERO operators.
operatorCount() then stays 0 forever and every route bounces to /setup:
the infinite redirect.

Split the three mirror steps so each runs independently and best-effort:
a failure in one is logged but can never undo operator creation.

Also set emailAndPassword.disableSignUp so the operator console no longer
exposes public self-provisioning. The first operator is now seeded at
boot (next commit); sign-in, magic-link, OAuth, and SSO stay enabled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes the setup chicken-and-egg: a default operator (configurable via
AF_STACK_DEFAULT_OPERATOR_EMAIL / _PASSWORD / _NAME) is seeded the first
time the dashboard boots, so the console is usable immediately with no
signup wizard. Credentials are documented in the README and .env.example.

The seed runs in a Next.js instrumentation hook and is:
- idempotent: gated on an empty suite_operators table, so it never
  re-runs or clobbers a changed password / a curated operator set;
- compatible with better-auth login: hashes the password with
  better-auth/crypto and writes the credential account row directly
  (providerId='credential', accountId=<user id>);
- resilient to boot ordering: migrations run in the runtime container,
  which the dashboard only depends_on as service_started, so the seed
  retries until the auth tables exist and never crashes the process.

Set AF_STACK_DEFAULT_OPERATOR_DISABLED=true to skip seeding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ogin

With a default operator seeded at boot there is no operator-less state to
divert to a wizard, and the operator console no longer offers public
self-signup. Remove the /setup and /signup pages, the count===0 → /setup
redirects in the login page and admin layout, the /setup and /signup
entries from the middleware allow-list, and the "Sign up" link on the
login form. The admin layout now relies on requireOperator() alone, which
sends anyone without a valid operator session to /login — no loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an "Operator login" section with the default credentials, the
override env vars, the change-password guidance, and the opt-out flag.
Update the identity row to reflect the seeded default operator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AbirAbbas

Copy link
Copy Markdown
Contributor Author

The shared customer/operator identity concern noted above is now tracked in #120.

@AbirAbbas
AbirAbbas merged commit 7eb191c into main Jun 15, 2026
3 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant