---
title: Authentication and Users
description: Log in, understand the HS256 JWT claims, enforce role checks, and manage users through the admin endpoints.
---

You authenticate every request with a bearer token from `POST /auth/login`. The token logic lives in `apps/server/src/core/auth/token.ts`, the middleware and role checks live in `apps/server/src/core/auth/middleware.ts`, and the login routes live in `apps/server/src/features/auth/route.ts`.

### How login works

You send an email and password to the public login endpoint. It is rate limited to 5 attempts per 60 seconds per client by the `loginRateLimit` middleware in `apps/server/src/core/auth/middleware.ts`, backed by the per address counters in `apps/server/src/core/auth/login-attempts.ts`. A failed attempt stays in the window while a successful one removes its own entry.

```bash
curl -X POST http://localhost:8000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@demo.pharmaflow","password":"Demo@1234"}'
```

You get back `access_token`, `token_type`, `user_id`, `role`, `branch_id`, `branch_name`, and `full_name`. Every login writes a `LOGIN` row to the audit log through `apps/server/src/features/auth/service.ts` with the caller IP, and updates `last_login_at` on the user.

### The JWT shape

Tokens are HS256 JWTs signed with `JWT_SECRET`, which must be at least 32 characters. The default lifetime is 480 minutes, configurable with `JWT_EXPIRE_MINUTES` and enforced in `apps/server/src/core/auth/token.ts`. The token carries an `org_id` claim that selects the tenant, and `authMiddleware` never trusts an org from a header.

```json
{
	"sub": "user-uuid",
	"role": "admin",
	"branch_id": "branch-uuid",
	"email": "admin@demo.pharmaflow",
	"org_id": "default"
}
```

You send the token on every protected call:

```bash
curl http://localhost:8000/auth/me \
  -H "Authorization: Bearer $TOKEN"
```

### How authMiddleware protects you

`authMiddleware` in `apps/server/src/core/auth/middleware.ts` verifies the token, then re-reads the user row on every request. If the account is missing or `is_active` is not true, you get a 401 even with a valid token. It also overwrites the token `role` and `branch_id` with the fresh database values, so a demoted user loses access immediately, and it applies the tenant search path from the token `org_id` through `apps/server/src/core/database/tenancy.ts`.

### How requireRoles works

`requireRoles(...allowed)` in `apps/server/src/core/auth/middleware.ts` runs after `authMiddleware`. The `admin` role always passes, every other role must be in the allowed list or you get a 403. `requireAdmin` is `requireRoles()` with an empty list, so only admins pass.

```ts
userRoutes.get("/", requireAdmin, async (context) => { ... });
syncRoutes.post("/push", requireRoles("admin", "branch_manager", "pharmacist", "cashier"), async (context) => { ... });
```

### Managing users

All user routes live in `apps/server/src/features/users/route.ts` behind `authMiddleware`, and the business rules live in `apps/server/src/features/users/service.ts`. Admins list everyone with role and active filters, and the list defaults to active users only.

```bash
curl "http://localhost:8000/users?page=1&page_size=20&role=pharmacist&is_active=true" \
  -H "Authorization: Bearer $TOKEN"
```

You fetch one user when you are that user or an admin. `GET /users/:userId` returns 403 otherwise and 404 for unknown ids.

### Creating and updating users

Only admins create users with `POST /users`. The role must be `admin` or `pharmacist`, the branch must exist and be active, the email must be unique and valid, and the password must be at least 8 characters. Creation emits a `users` sync change and logs a `CREATE` audit row, both in `apps/server/src/features/users/service.ts`.

```bash
curl -X POST http://localhost:8000/users \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email":"new@demo.pharmaflow","password":"Secret123","full_name":"New Pharmacist","role":"pharmacist","branch_id":"BRANCH_ID"}'
```

You update with `PUT /users/:userId` under a field allowlist. You may change your own `full_name` and `phone` only, while admins may also change `role`, `branch_id`, and `is_active`. Passwords never go through this endpoint. Use `POST /auth/change-password` for your own password with `current_password` plus a `new_password` of 8 or more characters, and `POST /users/:userId/reset-password` for the admin reset with matching `new_password` and `confirm_password`.

### Next steps

Read how branches scope your data in /platform/branches, query who changed what in /platform/audit-log, and walk the full counter flow in /guides/selling-end-to-end.
