Login Flow & Role Management Guide
Audience: End Users, Frontend Developers, Backend Engineers
Last Updated: 2026-06-06
Related: Multi-Tenant Authentication & Session Management, Tenant, Users & Roles Configuration
Overview
After users authenticate (see parent: Multi-Tenant Authentication), the system must:
- Extract roles from the user's
usr_rolesdatabase field (JSON structure) - Validate roles exist in the
app_rolestable - Resolve role codes for feature access control
- Handle multiple roles — allow user to select primary role if > 1 assigned
This document covers the end-to-end login flow, role parsing, and how roles are used for feature access.
Part 1: User Login (End-User Manual)
Step 1: Access Login Page
-
Open browser and navigate to tenant subdomain
Example:https://acme-corp.axiom-genesis.com/ -
Nginx redirects to
/auth/login -
Login page loads with:
- Tenant logo (from
app_tenants.logo_urlvia/api/v1/auth/tenant-config) - Tenant branding (colors, fonts)
- Email field
- Password field
- Tenant logo (from
Step 2: Submit Credentials
- Enter registered email address
- Enter password
- Click Sign In button
Behind the scenes:
- Frontend encrypts tenant ID from nginx header
- POST
/api/v1/auth/signinwith{email_id, password} - Backend validates credentials
Step 3a: Single Role (Auto-Login)
If user has only 1 role assigned:
- Login succeeds
- Frontend stores JWT token in Redux
- Auto-redirects to
/workspace - No role selection prompt needed
Example:
User: alice@acme.com
Assigned roles: [ADMIN]
Result: Logged in as ADMIN automatically
Step 3b: Multiple Roles (Role Selection)
If user has 2 or more roles assigned:
-
Shows role selection page
Example options:- ☐ Admin
- ☐ Manager
- ☐ Analyst
-
User selects their active role
(or system auto-selects "DEFAULT" role if marked) -
Confirm & Continue
-
Redirects to
/workspacewith selected role active
Example:
User: bob@acme.com
Assigned roles: [ADMIN, MANAGER, USER]
DEFAULT role: MANAGER
Result: Role selection page shown; MANAGER pre-selected
Step 4: Enter Workspace
- Token is valid ✓
- Tenant context set ✓
- User role active ✓
- Redirects to workspace dashboard
Part 2: Developer Guide — Login Flow & Role Parsing
Architecture Diagram
┌──────────────────────────────────────┐
│ User Submits Login Form │
│ Email: alice@acme.com │
│ Password: **** │
└─────────────┬──────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Frontend: useSignin() Hook │
│ ├─ Call: authService.signin(email, password, tenant_id) │
│ └─ Returns: { token, user_id, tenant_id, roles } │
└─────────────┬────────────────────────────────── ────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Middleware: POST /api/auth/signin │
│ ├─ Middleware 1: validateTenant │
│ │ └─ Decrypt x-tenant-id header │
│ ├─ Middleware 2: validateInput │
│ │ └─ Check email/password format │
│ └─ Controller: authController.signin() │
└─────────────┬──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Backend: Credential Validation │
│ ├─ Query: SELECT * FROM app_users │
│ │ WHERE usr_email = ? AND usr_tenant_id = ? AND deleted=0 │
│ ├─ Extract: usr_roles (JSON) │
│ │ { │
│ │ "roles": [ │
│ │ {role_id:12, role_code:"ADMIN", role_name:"..."}, │
│ │ {role_id:13, role_code:"MANAGER", role_name:"..."} │
│ │ ], │
│ │ "DEFAULT": "ADMIN" │
│ │ } │
│ ├─ Verify: password hash matches │
│ └─ Result: User object with usr_roles JSON │
└─────────────┬──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Backend: JWT Token Generation │
│ ├─ Payload: {user_id, tenant_id, role: usr_roles_JSON} │
│ ├─ Sign: JWT.sign(payload, JWT_SECRET, {expiresIn: '24h'}) │
│ └─ Return: {token, user_id, tenant_id, expires_in} │
└─────────────┬─────────────────────── ───────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Frontend: JWT Decode & Redux Dispatch │
│ ├─ Decode: jwtDecode(token) │
│ ├─ Extract: decoded.role (the usr_roles JSON) │
│ ├─ Parse: JSON.parse(decoded.role) if string │
│ ├─ Dispatch Redux: │
│ │ ├─ setRoles(rolesArray) │
│ │ ├─ setDefaultRole(decoded.DEFAULT) │
│ │ ├─ setSelectedRole(first role or DEFAULT) │
│ │ └─ setToken(token) │
│ └─ Return: { token, user_id, tenant_id, roles } │
└─────────────┬──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Frontend: Login Page Role Check │
│ ├─ Receive response.roles from useSignin() │
│ ├─ If roles.length === 0: │
│ │ └─ Show toast: "No roles assigned" │
│ ├─ If roles.length === 1: │
│ │ └─ Auto-select role, redirect to /workspace │
│ └─ If roles.length > 1: │
│ └─ Redirect to /auth/select-role (pick primary role) │
└─────────────┬──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Authenticated API Calls │
│ ├─ All requests include: │
│ │ ├─ Authorization: Bearer {jwt_token} │
│ │ └─ x-tenant-id: {encrypted_tenant_id} │
│ ├─ Middleware: verify-token validates JWT │
│ ├─ Extract: req.user.tenant_id, req.user.user_id │
│ ├─ Fetch: app_users.usr_roles from DB (fresh copy) │
│ ├─ Parse: usr_roles → extract role_id values │
│ ├─ Enrich: Query app_roles to get role_code │
│ └─ Result: req.user.roles = [{role_id, role_code, ...}] │
└──────────────────────────────────────────────────────────────┘
Frontend: useSignin Hook
File: axiom-genesis-frontend/src/hooks/useSignin.ts
export function useSignin(): UseSigninReturn {
const dispatch = useAppDispatch();
const signin = useCallback(async (data: SigninRequest): Promise<SigninResponse> => {
try {
// 1. Call backend API
const response = await authService.signin(data);
// authService.signin() does JWT decoding and Redux dispatch internally
// 2. Extract token
const token = response.data.token;
// 3. Store in Redux (via authService already done)
dispatch(setToken(token));
dispatch(setUserId(String(response.data.user_id)));
dispatch(setTenantId(String(response.data.tenant_id)));
// 4. Fetch user details (optional, may fail and that's OK)
let userData = null;
try {
userData = await userService.getUserById(response.data.user_id);
dispatch(setUser(userData));
} catch (userFetchError) {
// User fetch failed but auth is still valid
}
// 5. Get roles from Redux store (just set by authService.signin())
// CRITICAL: Read store state directly, not from stale selector
const rolesFromStore = store.getState().auth.roles || [];
// 6. Return response with roles
return {
...response.data,
user: userData || undefined,
roles: rolesFromStore, // ← This is what login page checks
};
} catch (err) {
// Handle error...
}
}, [dispatch]);
return { signin, loading, error, reset, user, roles };
}
Key Points:
- Don't use stale selector closures; call
store.getState()after dispatch - Return
rolesarray for login page to check role count - User data fetch is optional (may fail gracefully)
Backend: Role Parsing & Enrichment
File: axiom-genesis-middleware/middleware/verify-token.middleware.js
async function fetchUserWithRolesAndPermissions(userId, tenantId) {
try {
// 1. Query app_users for usr_roles JSON
const user = await db.query(`
SELECT usr_id, usr_email, usr_roles, ...
FROM app_users
WHERE usr_id = @userId AND usr_tenant_id = @tenantId
`);
if (!user) return null;
// 2. Parse usr_roles JSON
let roles = [];
if (user.usr_roles) {
try {
// Parse as JSON (could be string or object depending on DB type)
const rawRoles = user.usr_roles;
const rolesData = typeof rawRoles === 'string'
? JSON.parse(rawRoles)
: rawRoles;
// Handle new format: { roles: [{role_id, role_code, role_name}], DEFAULT: "..." }
if (rolesData.roles && Array.isArray(rolesData.roles)) {
roles = rolesData.roles.map((r) => ({
role_id: r.role_id || r.rol_id,
role_code: r.role_code || r.rol_code,
role_name: r.role_name || r.rol_name,
}));
}
// Handle legacy format: array of role IDs
else if (Array.isArray(rolesData)) {
roles = rolesData.map((r) => ({ role_id: r }));
}
} catch (e) {
// Parse failed, treat as single role ID
roles = [{ role_id: user.usr_roles }];
}
}
// 3. Enrich roles from app_roles table
if (roles.length > 0) {
const roleIds = roles.map(r => r.role_id).filter(Boolean);
if (roleIds.length > 0) {
// Query app_roles to get role_code and role_name
const rolesFromDb = await db.query(`
SELECT rol_id, rol_code, rol_description
FROM app_roles
WHERE rol_id IN (${roleIds.join(',')})
AND rol_tenant_id = @tenantId
`);
// Create lookup map
const roleCodeMap = {};
rolesFromDb.forEach(r => {
roleCodeMap[r.rol_id] = r.rol_code;
});
// Enrich roles with code from DB
roles = roles.map(r => ({
...r,
role_code: roleCodeMap[r.role_id] || null
}));
}
}
// 4. Attach to req.user for downstream use
return {
user: user,
roles: roles.length > 0
? roles
: [{ role_id: 1, role_code: 'DEFAULT', role_name: 'Default User' }]
};
} catch (error) {
throw new Error(`Failed to fetch user data: ${error.message}`);
}
}
Key Points:
- Parse
usr_rolesas JSON with both string and object support - Handle both new format
{roles:[...], DEFAULT:"..."}and legacy format - Enrich with DB lookup to get
role_code(role_id is numeric, but code is semantic) - Return default role if none found
Database: usr_roles Structure
Column: app_users.usr_roles (JSON)
Structure:
{
"roles": [
{
"role_id": 12,
"role_code": "ADMIN",
"role_name": "System Administrator"
},
{
"role_id": 13,
"role_code": "MANAGER",
"role_name": "Manager"
}
],
"groups": [
"IT_ADMIN",
"FINANCE"
],
"DEFAULT": "ADMIN"
}
Assigning Roles (SQL Example):
-- Update user's roles
UPDATE app_users
SET usr_roles = JSON_BUILD_OBJECT(
'roles', CAST('[
{"role_id":12, "role_code":"ADMIN", "role_name":"System Administrator"},
{"role_id":13, "role_code":"MANAGER", "role_name":"Manager"}
]' AS JSON),
'groups', CAST('["IT_ADMIN"]' AS JSON),
'DEFAULT', 'ADMIN'
)
WHERE usr_id = 5 AND usr_tenant_id = 7;
Part 3: Role-Based Feature Access
Role Navigation Access
Roles control which features users can access via app_roles.rol_navigation_access:
// Admin role
{
"mode": "allowlist",
"navCodes": ["DOCUMENT_STUDIO", "SETTINGS", "WORKFLOW_DESIGNER"]
}
// Manager role
{
"mode": "allowlist",
"navCodes": ["DATAVIEWER", "WORKFLOW_DESIGNER"]
}
// User role
{
"mode": "allowlist",
"navCodes": ["DATAVIEWER"]
}
Frontend Implementation:
// After login, check if user's role has access
const userRoles = useAppSelector(state => state.auth.roles);
const selectedRole = useAppSelector(state => state.auth.selectedRole);
function canAccessFeature(featureCode: string): boolean {
const roleNav = selectedRole?.rol_navigation_access;
if (!roleNav) return false; // No access if no role navigation config
if (roleNav.mode === 'allowlist') {
return roleNav.navCodes?.includes(featureCode) ?? false;
} else if (roleNav.mode === 'denylist') {
return !roleNav.navCodes?.includes(featureCode) ?? true;
}
return false;
}
// Usage
if (canAccessFeature('DOCUMENT_STUDIO')) {
// Show Document Studio menu item
} else {
// Hide menu item
}
Part 4: Troubleshooting
Issue: "No roles assigned" Toast on Login
Symptoms:
- User logs in successfully
- Token is generated
- But login page shows: "No roles assigned to this user. Proceeding with limited access..."
Root Causes (Fixed in Commit 78ab10b):
-
Stale Redux closure —
useSignin()reads pre-login empty roles- Fix: Use
store.getState().auth.rolesinstead of selector
- Fix: Use
-
Wrong array indexing — Login page uses
response.user?.[0]?.usr_roles- Fix: Use
response.roles(already resolved)
- Fix: Use
-
Broken usr_roles parser — Middleware parser only checks for
[start- Fix: Properly parse
{roles:[...], DEFAULT:"..."}format
- Fix: Properly parse
Debug Steps:
// 1. Check Redux state after login
import { store } from '@/store';
const state = store.getState();
console.log('Roles in Redux:', state.auth.roles); // Should be array, not empty
// 2. Check JWT payload
import { jwtDecode } from 'jwt-decode';
const decoded = jwtDecode(token);
console.log('JWT role field:', decoded.role); // Should be {roles:[...], DEFAULT:"..."}
// 3. Check response from signin
const response = await useSignin();
console.log('Response roles:', response.roles); // Should be non-empty array
// 4. Check database
SELECT usr_id, usr_roles FROM app_users WHERE usr_id = 5;
-- usr_roles should be: {"roles":[{role_id:12,...}],...}
If Issue Persists:
- Verify
app_users.usr_rolesis not NULL - Confirm parser in
verify-token.middleware.jshandles object format (lines 94-108) - Check middleware logs for parse errors
- Ensure
app_rolestable has matchingrol_idrecords
Issue: 401 Unauthorized After Login
Symptoms:
- Login succeeds
- Redirect to workspace
- First API call returns 401 Unauthorized
Root Causes:
- Token not stored in Redux
- Token not included in Authorization header
- Token expired (unlikely immediately after login)
Debug Steps:
// 1. Check if token is stored
const token = useAppSelector(state => state.auth.token);
console.log('Token in Redux:', token); // Should be non-null string
// 2. Check if header is sent
// Open DevTools → Network tab
// Click any API request
// Headers → Authorization should be: "Bearer {token}"
// 3. Check if middleware validates token
// Backend logs should show: "Token validated for user {user_id}"
// 4. Check token expiration
const decoded = jwtDecode(token);
const now = Math.floor(Date.now() / 1000);
console.log('Token expires at:', new Date(decoded.exp * 1000));
console.log('Token expired?:', decoded.exp < now);
Issue: User Can Access Features They Shouldn't
Symptoms:
- User with "USER" role can view "SETTINGS" menu
- Should only have "DATAVIEWER" access
Root Cause:
- Role's
rol_navigation_accessnot configured - Frontend not checking feature access
Fix:
-- Update User role navigation access
UPDATE app_roles
SET rol_navigation_access = JSON_BUILD_OBJECT(
'mode', 'allowlist',
'navCodes', CAST('["DATAVIEWER"]' AS JSON)
)
WHERE rol_code = 'USER' AND rol_tenant_id = 7;
// Ensure frontend checks access
if (!canAccessFeature('SETTINGS')) {
return <Navigate to="/workspace" />; // Redirect if no access
}
Part 5: Testing Login & Roles
Manual Test Flow
# 1. Start services
cd axiom-genesis-middleware && npm run dev
cd axiom-genesis-frontend && npm run dev
# 2. Navigate to login (subdomain important for x-tenant-id injection)
# http://localhost:3000/auth/login
# Note: If not using nginx, manually inject x-tenant-id or modify validateTenant
# 3. Log in with test user
# Email: alice@acme.com
# Password: (from your DB seeding script)
# 4. Verify response
# Check DevTools Network tab:
# POST /api/auth/signin
# Response: {token, user_id, tenant_id, expires_in}
# 5. Verify roles loaded
# Check DevTools Console:
# store.getState().auth.roles // Should be [{role_id, role_code, ...}]
# 6. Verify redirect
# Should redirect to /workspace (or /auth/select-role if multiple roles)
Automated Test (Jest Example)
describe('Login Flow', () => {
it('should parse roles from JWT and dispatch to Redux', async () => {
// Mock response from signin
const mockResponse = {
data: {
token: 'eyJhbGc...', // JWT with role field
user_id: '5',
tenant_id: '7',
}
};
// Mock authService.signin
jest.spyOn(authService, 'signin').mockResolvedValue(mockResponse);
// Call hook
const { signin } = useSignin();
const result = await signin({
email_id: 'alice@acme.com',
password: 'test',
tenant_id: '7',
});
// Assert roles returned
expect(result.roles).toEqual(
expect.arrayContaining([
expect.objectContaining({
role_code: 'ADMIN',
role_name: 'System Administrator',
})
])
);
});
});
Summary
Login & Roles Flow:
- Login — User submits email + password
- Backend validation — Check credentials, extract
usr_rolesJSON - JWT generation — Encode
usr_rolesin token - Frontend parsing — Decode JWT, parse roles, dispatch to Redux
- Role selection — If 1 role → auto-select; if >1 → show picker
- Feature gating — Check
rol_navigation_accessfor menu items - Subsequent requests — Include JWT for authenticated API calls
- Role enrichment — Middleware re-fetches
usr_roleson each request, enriches withrole_code
Key Files:
| File | Purpose |
|---|---|
Frontend: src/hooks/useSignin.ts | Login hook; returns roles |
Frontend: src/app/auth/login/page.tsx | Login page; checks role count |
Frontend: src/services/auth.service.ts | JWT decode; Redux dispatch |
Middleware: middleware/verify-token.middleware.js | Role parser; role enrichment |
Middleware: services/auth.service.js | Credential validation; JWT generation |
Database: app_users.usr_roles | Role assignment storage |
Database: app_roles.rol_navigation_access | Feature access control |
Related Fixes:
- [[fix_login_no_roles_assigned]] — 3 bugs causing "No roles assigned" toast (commits 78ab10b, 6f2ef47)
See Also
- Multi-Tenant Authentication & Session Management — Subdomain, encryption, JWT architecture
- Tenant, Users & Roles Configuration — Complete DB schema