Skip to content

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_role is ORG_OWNER or ORG_ADMIN, every workspace in the org is returned, each synthetically stamped role: "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, workspaces is a UNION of direct workspace_members rows and team_workspace_roles (via team_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