toeverything/AFFiNE · error · ActionForbidden

action_forbidden

action_forbidden

Error message

First user already created

What it means

Thrown by CustomSetupController.createAdmin when ServerService.initialized() returns true, i.e. the self-host instance already has its first admin user. The /api/setup/create-admin-user endpoint is meant to run exactly once; all subsequent calls are forbidden regardless of authentication.

Source

Thrown at packages/backend/server/src/core/selfhost/controller.ts:42

@Controller('/api/setup')
export class CustomSetupController {
  constructor(
    private readonly config: Config,
    private readonly models: Models,
    private readonly sessionIssuer: SessionIssuer,
    private readonly mutex: Mutex,
    private readonly server: ServerService
  ) {}

  @Public()
  @Post('/create-admin-user')
  async createAdmin(
    @Req() req: Request,
    @Res() res: Response,
    @Body() input: CreateUserInput
  ) {
    if (await this.server.initialized()) {
      throw new ActionForbidden('First user already created');
    }

    validators.assertValidEmail(input.email);

    if (!input.password) {
      throw new PasswordRequired();
    }

    validators.assertValidPassword(
      input.password,
      this.config.auth.passwordRequirements
    );

    await using lock = await this.mutex.acquire('createFirstAdmin');

    if (!lock) {
      throw new InternalServerError();
    }

View on GitHub (pinned to 26c515e050)

Solutions

  1. Skip the call once the server reports initialized — query ServerService or GET the setup status endpoint first.
  2. If a fresh setup is genuinely required, wipe the database/user table so initialized() returns false.
  3. Guard the provisioning script with an idempotency check that polls initialized() before POSTing.

Example fix

// before
await fetch('/api/setup/create-admin-user', { method: 'POST', body });

// after
const { initialized } = await fetch('/api/setup/status').then(r => r.json());
if (initialized) {
  throw new Error('Instance already initialized');
}
await fetch('/api/setup/create-admin-user', { method: 'POST', body });
Defensive patterns

Strategy: validation

Validate before calling

const { initialized } = await fetch('/api/setup/status').then(r => r.json());
if (initialized) {
  // route to sign-in instead of setup
  return navigate('/sign-in');
}

Type guard

// n/a

Try / catch

try {
  await createAdmin(input);
} catch (e) {
  if (e?.code === 'action_forbidden' && /already created/.test(e.message)) {
    // instance already set up — go to login
    return navigate('/sign-in');
  }
  throw e;
}

Prevention

When it happens

Trigger: POST /api/setup/create-admin-user after the first successful setup; an automation script re-running setup; the setup UI being replayed in a browser tab.

Common situations: Operator forgot the instance was already initialized; restoring a DB snapshot into a fresh container and re-running setup; misconfigured init container hitting the endpoint twice.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/844eb44a7e664b0c. Report an issue: GitHub.