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¶
- Click Add user.
- Enter email, name, password, and role.
- Optionally enable
can_run_processorcan_create_override. - Optionally restrict to specific segments.
- 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¶
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:
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:
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¶
SuperAdmin Login Flow¶
When a superAdmin logs in:
POST /api/auth/loginwith email + password (noaccount_id).- Backend detects the superAdmin role and returns
{ "status": "select_account" }. - The login UI shows an account picker.
- The superAdmin selects a tenant and logs in again with
account_id. - 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:
verify_superadmin(email, password)checks the master DB hash.- If the password matches the tenant hash but not the master hash,
verify_superadmin()returnsNone. - The code falls through to the tenant user login, which succeeds and issues a JWT with
role: 'admin'. - The frontend sets
isSuperAdmin = falseand 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:
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.