{"record":{"id":"844eb44a7e664b0c","repo":"toeverything/AFFiNE","slug":"action-forbidden-844eb4","errorCode":"action_forbidden","errorMessage":"First user already created","messagePattern":"First user already created","errorType":"exception","errorClass":"ActionForbidden","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/core/selfhost/controller.ts","lineNumber":42,"sourceCode":"@Controller('/api/setup')\nexport class CustomSetupController {\n  constructor(\n    private readonly config: Config,\n    private readonly models: Models,\n    private readonly sessionIssuer: SessionIssuer,\n    private readonly mutex: Mutex,\n    private readonly server: ServerService\n  ) {}\n\n  @Public()\n  @Post('/create-admin-user')\n  async createAdmin(\n    @Req() req: Request,\n    @Res() res: Response,\n    @Body() input: CreateUserInput\n  ) {\n    if (await this.server.initialized()) {\n      throw new ActionForbidden('First user already created');\n    }\n\n    validators.assertValidEmail(input.email);\n\n    if (!input.password) {\n      throw new PasswordRequired();\n    }\n\n    validators.assertValidPassword(\n      input.password,\n      this.config.auth.passwordRequirements\n    );\n\n    await using lock = await this.mutex.acquire('createFirstAdmin');\n\n    if (!lock) {\n      throw new InternalServerError();\n    }","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/selfhost/controller.ts#L24-L60","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Skip the call once the server reports initialized — query ServerService or GET the setup status endpoint first.","If a fresh setup is genuinely required, wipe the database/user table so initialized() returns false.","Guard the provisioning script with an idempotency check that polls initialized() before POSTing."],"exampleFix":"// before\nawait fetch('/api/setup/create-admin-user', { method: 'POST', body });\n\n// after\nconst { initialized } = await fetch('/api/setup/status').then(r => r.json());\nif (initialized) {\n  throw new Error('Instance already initialized');\n}\nawait fetch('/api/setup/create-admin-user', { method: 'POST', body });","handlingStrategy":"validation","validationCode":"const { initialized } = await fetch('/api/setup/status').then(r => r.json());\nif (initialized) {\n  // route to sign-in instead of setup\n  return navigate('/sign-in');\n}","typeGuard":"// n/a","tryCatchPattern":"try {\n  await createAdmin(input);\n} catch (e) {\n  if (e?.code === 'action_forbidden' && /already created/.test(e.message)) {\n    // instance already set up — go to login\n    return navigate('/sign-in');\n  }\n  throw e;\n}","preventionTips":["Poll the setup-status endpoint once on first launch before offering the setup form.","Make provisioning scripts idempotent by gating on initialized().","Document the one-shot nature of the setup endpoint for operators."],"tags":["selfhost","setup","nestjs","configuration"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}