Skip to content

User Management


Roles

Role Description
superadmin Full access to all tenant accounts. Can provision tenants, manage superAdmins.
admin Full access within their tenant. Can create users, run pipelines, edit all data.
user Regular access. Permissions are controlled by the flags below.

Additional Permission Flags (for user role)

Flag Description
can_run_process Allows the user to trigger pipeline steps via Process Runner
can_create_override Allows the user to lock forecast methods and set per-SKU overrides
allowed_segments List of segment IDs the user can view (empty = all)
allowed_segments_edit List of segment IDs the user can edit (empty = all)

Managing Users via the UI

Navigate to Users (/users) — available to admin and superAdmin roles.

Create a user

  1. Click Add user.
  2. Enter email, name, password, and role.
  3. Optionally enable can_run_process or can_create_override.
  4. Optionally restrict to specific segments.
  5. Save.

Edit a user

Click the edit icon on any user row. The same fields are editable.

Reset password

Admins can set a new password via the edit form. Users can change their own password via POST /api/auth/change-password.


Managing Users via the API

Create

POST /api/auth/users
Authorization: Bearer <admin-token>
Content-Type: application/json

{
  "email": "analyst@acme.com",
  "name": "Jane Smith",
  "password": "securepassword123",
  "role": "user",
  "can_run_process": true,
  "can_create_override": false,
  "allowed_segments": [],
  "allowed_segments_edit": []
}

List

GET /api/auth/users
Authorization: Bearer <admin-token>

Update

PUT /api/auth/users/{user_id}
Authorization: Bearer <admin-token>
Content-Type: application/json

{
  "name": "Jane Smith-Jones",
  "can_run_process": false
}

OAuth Integration

Microsoft (Azure AD / Entra ID)

Configure in .env:

MICROSOFT_CLIENT_ID=your-app-registration-client-id
MICROSOFT_TENANT_ID=your-tenant-id

Users logging in with Microsoft are automatically provisioned in the DB with role = 'user' on first login if they are in the allowed domain. Upgrade their role via the Users page after first login.

Google

Configure in .env:

GOOGLE_CLIENT_ID=your-google-oauth-client-id

Users must register from an allowed domain. Same auto-provisioning behaviour as Microsoft.


SuperAdmin Management

SuperAdmins are stored in master.superadmins (not in any tenant DB).

Create a superAdmin via API

POST /api/admin/superadmins
Authorization: Bearer <superadmin-token>
Content-Type: application/json

{
  "email": "root@system.com",
  "name": "System Administrator",
  "password": "supersecret"
}

List superAdmins

GET /api/admin/superadmins
Authorization: Bearer <superadmin-token>

SuperAdmin Login Flow

When a superAdmin logs in:

  1. POST /api/auth/login with email + password (no account_id).
  2. Backend detects the superAdmin role and returns { "status": "select_account" }.
  3. The login UI shows an account picker.
  4. The superAdmin selects a tenant and logs in again with account_id.
  5. The backend issues a JWT scoped to that tenant.

The superAdmin can switch tenants at any time via the account switcher in the sidebar (gear icon shows available accounts).


Troubleshooting: SuperAdmin Password Divergence

Symptom

A user who is registered in master.superadmins logs in but does not see the Tenant (🏢) link in the sidebar. The login completes successfully, but they receive role: 'admin' instead of role: 'superadmin'.

Cause

SuperAdmin credentials are stored in master.superadmins (master database), while tenant user credentials are stored in scenario.sys_users (tenant database). These are independent credential stores — they are not kept in sync automatically.

When the same email exists in both tables with different bcrypt hashes, the login flow in auth.py works as follows:

  1. verify_superadmin(email, password) checks the master DB hash.
  2. If the password matches the tenant hash but not the master hash, verify_superadmin() returns None.
  3. The code falls through to the tenant user login, which succeeds and issues a JWT with role: 'admin'.
  4. The frontend sets isSuperAdmin = false and hides the Tenant nav link.

This typically happens when: - A superAdmin changes their tenant password via the UI (POST /api/auth/change-password), which only updates scenario.sys_users, not master.superadmins. - A superAdmin is created with a different password than their tenant user account.

Detection

As of the 2026-06-14 fix, the API logs a warning when a superadmin email exists in master.superadmins but the password did not match:

WARNING: Login: email user@example.com exists in master.superadmins but password did not match —
falling through to tenant login. The superadmin password may have diverged from the tenant user password.

Fix

Sync the superadmin password hash to match the tenant user hash. Run against the forecastai_master database:

-- Step 1: Get the tenant user's password hash
-- Connect to the tenant DB (e.g. thebicycle) and run:
SELECT hashed_password FROM scenario.sys_users WHERE email = 'user@example.com';

-- Step 2: Update the master superadmin record
-- Connect to forecastai_master and run:
UPDATE master.superadmins
SET hashed_password = '<hash-from-step-1>'
WHERE email = 'user@example.com';

Or use the provided sync script:

psql -U postgres -d forecastai_master -f files/scripts/sync_superadmin_passwords.sql

Prevention

When changing a superAdmin's password, always update both stores:

-- Update tenant DB
UPDATE scenario.sys_users SET hashed_password = '<new-hash>' WHERE email = 'user@example.com';

-- Update master DB
UPDATE master.superadmins SET hashed_password = '<new-hash>' WHERE email = 'user@example.com';

The POST /api/auth/change-password endpoint only updates the tenant DB. For superAdmins, also update master.superadmins separately.


Segment-Based Access Control

To restrict a user to specific segments:

PUT /api/auth/users/{user_id}
Authorization: Bearer <admin-token>
Content-Type: application/json

{
  "allowed_segments": [3, 7, 12],
  "allowed_segments_edit": [3]
}
  • allowed_segments: [3, 7, 12] — user can view series that belong to segments 3, 7, or 12.
  • allowed_segments_edit: [3] — user can only create overrides or adjustments for series in segment 3.
  • allowed_segments: [] — user can view all segments (unrestricted).

The backend enforces this on every API call via _user_has_segment_access() in files/api/auth.py.