Identity Domain
Two identity models — this page only covered one until 2026-09-20
domain/identity in CoreAPI has two independent slices, same pattern as
the tenancy domain's "two tenancy models" note.
Everything below this note is the ticketing-side slice
(domain/identity/routes/routes_ticketing.go — HS256, Admins/Customers,
/auth/*). The native slice
(domain/identity/routes/routes.go, /api/v1/identity/*, RS256) had zero
documentation anywhere until the section immediately below was added —
despite being referenced by name from organization.md,
tellyid/tellyid.md, and the root CLAUDE.md (§3) as the endpoint every
frontend calls right after login to learn its own org/workspaces/permissions.
Native Identity (/api/v1/identity)
The RS256 counterpart to everything below — this is what every platform frontend (the consoles, SpatioViewAdmin, SpatioViewMobile) actually calls after login, not the ticketing HS256 endpoints further down this page.
Handles:
-
GET /api/v1/identity/users/me— the only route in this slice today
GET /api/v1/identity/users/me
Requires a valid RS256 access token (ProtectedRS256). Reads the caller's
OrgClaim from the token, then enriches it with org name, workspaces, and
per-role permissions from CoreAPI's own DB. This is the single source of
truth every frontend uses to build its own nav/permission model — see
CLAUDE.md §3 for the login flow that produces the token this reads.
Response 200
{
"user_id": "<uuid>",
"email": "alice@acme.io",
"username": "alice",
"first_name": "Alice",
"last_name": "Ng",
"platform_role": "PLATFORM_OWNER",
"platform_permissions": ["platform:org:manage", "..."],
"org": {
"org_id": "<uuid>",
"org_name": "Acme",
"org_slug": "acme",
"org_role": "ORG_ADMIN",
"org_permissions": ["org:users:manage", "..."],
"workspaces": [
{
"workspace_id": "<uuid>",
"workspace_name": "Default",
"app_ids": ["ticketing"],
"role": "WORKSPACE_ADMIN",
"permissions": ["workspace:general-settings:manage", "..."]
}
]
}
}
org is a single nullable object, not an array — one token, one org (see
CLAUDE.md §3). Two behaviors worth knowing before reading the code:
- If the caller's
org_roleisORG_OWNERorORG_ADMIN, every workspace in the org is returned, each synthetically stampedrole: "WORKSPACE_ADMIN"regardless of actual membership — org owners/admins implicitly see themselves as admin of every workspace, not just ones they're a member of. - For everyone else,
workspacesis aUNIONof directworkspace_membersrows andteam_workspace_roles(viateam_members), deduplicated by workspace with the highest-ranked role winning when both exist (WORKSPACE_ADMIN > WORKSPACE_MEMBER > VIEWER).
Manages authentication and user identity for the ticketing platform. Covers two user types — Admins (staff with portal access) and Customers (end users who buy tickets). Issues HS256 JWTs stored in a token table. Tokens are revoked on logout.
Handles:
- Login — admin and customer, returns JWT pair
- Token refresh — reissue access + refresh tokens
- Logout — revoke current token
- Token validation — verify token integrity
- Customer registration — self-service signup
- Password reset — email-based for both user types
- Admin profile — view and update own profile, change password
- Admin management — SYSADMIN creates, updates, disables admins
- Customer profile — view and update own profile, change password
- Customer management — paginated list for admin staff
Architecture
graph LR
AdminFrontend["Admin Portal"]
CustomerApp["Customer App"]
CoreAPI["CoreAPI (Identity)"]
DB["Ticketing DB\n(admin, customer, token)"]
Email["Email Service"]
AdminFrontend -->|POST /auth/login?userType=admin| CoreAPI
CustomerApp -->|POST /auth/login?userType=customer| CoreAPI
CoreAPI --> DB
CoreAPI -->|reset password email| Email
Data Model
erDiagram
Admin {
uint adminId
string username
string fullName
string role
string email
string contactNo
bool isDisabled
}
Customer {
string custId
string email
string identificationNo
string fullName
string contactNo
bool isDisabled
}
Token {
uint tokenId
string userId
string userType
string accessToken
string refreshToken
string ipAddress
string userAgent
}
CustomerLog {
uint custLogId
string custId
string type
string title
string message
string date
}
AuditLog {
uint auditLogId
string userId
string authorityLevel
string logType
string logAction
}
Admin ||--o{ Token : "has tokens"
Customer ||--o{ Token : "has tokens"
Customer ||--o{ CustomerLog : "has logs"
Roles
| Role | Scope | Capabilities |
|---|---|---|
SYSADMIN |
Platform | Full access including admin management |
ADMIN |
Platform | Manage tickets, orders, customers |
MEMBER |
Platform | Read-only admin access |
CUSTOMER |
End user | Manage own profile and orders |
Authentication
Tokens are HS256 JWTs. Pass in Authorization: Bearer <token> for protected routes.
The userId claim inside the token is the admin's username or customer's custId.
Endpoints
Auth (/auth)
POST /auth/login
Login for admin or customer.
Request
{
"username": "admin@example.com",
"password": "s3cur3p@ss",
"userType": "admin"
}
userType defaults to "admin" if omitted. For customers, pass "customer".
Response 200
{
"success": true,
"data": {
"accessToken": "<HS256 JWT>",
"refreshToken": "<HS256 JWT>",
"role": "ADMIN",
"userId": "admin@example.com",
"fullName": "Ahmad Admin"
}
}
| Status | Meaning |
|---|---|
400 |
Missing fields or invalid user type |
401 |
Wrong credentials |
POST /auth/refresh-token
Refresh the token pair. Pass the current refresh token in Authorization: Bearer <refresh_token>.
Response 200 — same shape as login.
GET /auth/logout
Revoke the current access token. Requires Authorization: Bearer <access_token>.
Response 200
{ "success": true, "data": { "success": true } }
GET /auth/validate
Validate token integrity. Requires Authorization: Bearer <access_token>.
Response 200
{ "success": true, "data": { "valid": true } }
POST /auth/customer/create
Self-registration — creates a new customer account.
Request
{
"email": "user@example.com",
"password": "s3cur3p@ss",
"identificationNo": "900101-14-1234",
"fullName": "Ahmad Bin Ali",
"contactNo": "+60123456789"
}
Response 201
{
"success": true,
"data": {
"custId": "CUST-001",
"email": "user@example.com",
"fullName": "Ahmad Bin Ali",
"identificationNo": "900101-14-1234",
"isDisabled": false,
"createdAt": "2026-06-01T10:00:00Z"
}
}
| Status | Meaning |
|---|---|
409 |
Email already registered |
POST /auth/customer/reset-password
Trigger a password reset email for a customer.
Request
{ "email": "user@example.com" }
Response 200 — always returns success (security measure — no email existence disclosure).
POST /auth/admin/reset-password
Trigger a password reset email for an admin.
Request
{ "email": "admin@example.com" }
Response 200 — always returns success.
Admin Profile (/api/admin)
All require auth + role ADMIN, MEMBER, or SYSADMIN.
GET /api/admin/profile
Get the authenticated admin's own profile.
Response 200
{
"success": true,
"data": {
"admin": {
"adminId": 1,
"username": "jsmith",
"fullName": "John Smith",
"email": "jsmith@example.com",
"contactNo": "+60123456789",
"role": "ADMIN"
}
}
}
PUT /api/admin/profile
Update own profile fields.
Request
{
"fullName": "John Smith Jr.",
"email": "jsmith2@example.com",
"contactNo": "+60123456788"
}
PUT /api/admin/password
Change own password.
Request
{
"currentPassword": "old_pass",
"newPassword": "new_pass"
}
Admin Management (/api/admin/management)
All require SYSADMIN role.
GET /api/admin/management
List all admin accounts.
POST /api/admin/management
Create a new admin account.
Request
{
"username": "newadmin",
"password": "s3cur3p@ss",
"fullName": "New Admin",
"email": "newadmin@example.com",
"contactNo": "+60123456780",
"role": "MEMBER"
}
| Status | Meaning |
|---|---|
409 |
Username already exists |
PUT /api/admin/management
Update an admin account (by admin ID in body).
DELETE /api/admin/management
Delete an admin (by admin ID in body).
Customer Profile (/api/customer)
GET /api/customer/profile
Get customer profile by ?custId=<id>. Public — no auth required.
PUT /api/customer/profile
Update own customer profile. Requires CUSTOMER role.
Request
{
"fullName": "Ahmad Bin Ali Updated",
"contactNo": "+60123456789"
}
PUT /api/customer/password
Change own password. Requires CUSTOMER role.
Request
{
"currentPassword": "old_pass",
"newPassword": "new_pass"
}
GET /api/customer/management
Paginated customer list. Requires ADMIN, SYSADMIN, or MEMBER role.
Query params: page, limit, query