Merge branch 'dev' into 592-migrate-frontend-applications-to-v2-backend-and-database
This commit is contained in:
@@ -21,16 +21,18 @@ What was validated live against the deployed stack:
|
||||
- client sign-in
|
||||
- staff auth bootstrap
|
||||
- client dashboard, billing, coverage, hubs, vendors, managers, team members, orders, and reports
|
||||
- client hub and order write flows
|
||||
- client coverage incident feed for geofence and override review
|
||||
- client hub, order, coverage review, device token, and late-worker cancellation flows
|
||||
- client invoice approve and dispute
|
||||
- staff dashboard, availability, payments, shifts, profile sections, documents, certificates, attire, bank accounts, benefits, and time card
|
||||
- staff availability, profile, tax form, bank account, shift apply, shift accept, clock-in, clock-out, and swap request
|
||||
- staff availability, profile, tax form, bank account, shift apply, shift accept, push token registration, clock-in, clock-out, location stream upload, and swap request
|
||||
- direct file upload helpers and verification job creation through the unified host
|
||||
- client and staff sign-out
|
||||
|
||||
The live validation command is:
|
||||
|
||||
```bash
|
||||
export FIREBASE_WEB_API_KEY="$(gcloud secrets versions access latest --secret=firebase-web-api-key --project=krow-workforce-dev)"
|
||||
source ~/.nvm/nvm.sh
|
||||
nvm use 23.5.0
|
||||
node backend/unified-api/scripts/live-smoke-v2-unified.mjs
|
||||
@@ -76,7 +78,38 @@ All routes return the same error envelope:
|
||||
}
|
||||
```
|
||||
|
||||
## 4) Route model
|
||||
## 4) Attendance policy and monitoring
|
||||
|
||||
V2 now supports an explicit attendance proof policy:
|
||||
|
||||
- `NFC_REQUIRED`
|
||||
- `GEO_REQUIRED`
|
||||
- `EITHER`
|
||||
|
||||
The effective policy is resolved as:
|
||||
|
||||
1. shift override if present
|
||||
2. hub default if present
|
||||
3. fallback to `EITHER`
|
||||
|
||||
For geofence-heavy staff flows, frontend should read the policy from:
|
||||
|
||||
- `GET /staff/clock-in/shifts/today`
|
||||
- `GET /staff/shifts/:shiftId`
|
||||
- `GET /client/hubs`
|
||||
|
||||
Important operational rules:
|
||||
|
||||
- outside-geofence clock-ins can be accepted only when override is enabled and a written reason is provided
|
||||
- NFC mismatches are rejected and are not overrideable
|
||||
- attendance proof logs are durable in SQL and raw object storage
|
||||
- device push tokens are durable in SQL and can be registered separately for client and staff apps
|
||||
- background location streams are stored as raw batch payloads in the private v2 bucket and summarized in SQL for query speed
|
||||
- incident review lives on `GET /client/coverage/incidents`
|
||||
- confirmed late-worker recovery is exposed on `POST /client/coverage/late-workers/:assignmentId/cancel`
|
||||
- queued alerts are written to `notification_outbox`, dispatched by the private Cloud Run worker service `krow-notification-worker-v2`, and recorded in `notification_deliveries`
|
||||
|
||||
## 5) Route model
|
||||
|
||||
Frontend sees one base URL and one route shape:
|
||||
|
||||
@@ -94,7 +127,7 @@ Internally, the gateway still forwards to:
|
||||
| writes and workflow actions | `command-api-v2` |
|
||||
| reads and mobile read models | `query-api-v2` |
|
||||
|
||||
## 5) Frontend integration rule
|
||||
## 6) Frontend integration rule
|
||||
|
||||
Use the unified routes first.
|
||||
|
||||
@@ -106,8 +139,9 @@ Do not build new frontend work on:
|
||||
|
||||
Those routes still exist for backend/internal compatibility, but mobile/frontend migration should target the unified surface documented in [Unified API](./unified-api.md).
|
||||
|
||||
## 6) Docs
|
||||
## 7) Docs
|
||||
|
||||
- [Authentication](./authentication.md)
|
||||
- [Unified API](./unified-api.md)
|
||||
- [Core API](./core-api.md)
|
||||
- [Command API](./command-api.md)
|
||||
|
||||
343
docs/BACKEND/API_GUIDES/V2/authentication.md
Normal file
343
docs/BACKEND/API_GUIDES/V2/authentication.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# V2 Authentication Guide
|
||||
|
||||
This document is the source of truth for V2 authentication.
|
||||
|
||||
Base URL:
|
||||
|
||||
```env
|
||||
API_V2_BASE_URL=https://krow-api-v2-933560802882.us-central1.run.app
|
||||
```
|
||||
|
||||
## 1) What is implemented
|
||||
|
||||
### Client app
|
||||
|
||||
Client authentication is implemented through backend endpoints:
|
||||
|
||||
- `POST /auth/client/sign-in`
|
||||
- `POST /auth/client/sign-up`
|
||||
- `POST /auth/client/sign-out`
|
||||
- `GET /auth/session`
|
||||
|
||||
The backend signs the user in with Firebase Identity Toolkit, validates the user against the V2 database, and returns the full auth envelope.
|
||||
|
||||
### Staff app
|
||||
|
||||
Staff authentication is implemented, but it is a hybrid flow.
|
||||
|
||||
Routes:
|
||||
|
||||
- `POST /auth/staff/phone/start`
|
||||
- `POST /auth/staff/phone/verify`
|
||||
- `POST /auth/staff/sign-out`
|
||||
- `GET /auth/session`
|
||||
|
||||
Important:
|
||||
|
||||
- the default mobile path is **not** a fully backend-managed OTP flow
|
||||
- the usual mobile path uses the Firebase Auth SDK on-device for phone verification
|
||||
- after the device gets a Firebase `idToken`, frontend sends that token to `POST /auth/staff/phone/verify`
|
||||
|
||||
So if someone expects `POST /auth/staff/phone/start` to always send the SMS and always return `sessionInfo`, that expectation is wrong for the current implementation
|
||||
|
||||
## 2) Auth refresh
|
||||
|
||||
There is currently **no** backend `/auth/refresh` endpoint.
|
||||
|
||||
That is intentional for now.
|
||||
|
||||
Current refresh model:
|
||||
|
||||
- frontend keeps Firebase Auth local session state
|
||||
- frontend lets the Firebase SDK refresh the ID token
|
||||
- frontend sends the latest Firebase ID token in:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <firebase-id-token>
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
- `authStateChanges()` / `idTokenChanges()` listeners
|
||||
- `currentUser.getIdToken()`
|
||||
- `currentUser.getIdToken(true)` only when a forced refresh is actually needed
|
||||
|
||||
`GET /auth/session` is **not** a refresh endpoint.
|
||||
|
||||
It is a context endpoint used to:
|
||||
|
||||
- hydrate role/tenant/business/staff context
|
||||
- validate that the signed-in Firebase user is allowed in this app
|
||||
|
||||
## 3) Client auth flow
|
||||
|
||||
### Client sign-in
|
||||
|
||||
Request:
|
||||
|
||||
```http
|
||||
POST /auth/client/sign-in
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "legendary.owner+v2@krowd.com",
|
||||
"password": "Demo2026!"
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"sessionToken": "firebase-id-token",
|
||||
"refreshToken": "firebase-refresh-token",
|
||||
"expiresInSeconds": 3600,
|
||||
"user": {
|
||||
"id": "user-uuid",
|
||||
"email": "legendary.owner+v2@krowd.com",
|
||||
"displayName": "Legendary Owner",
|
||||
"phone": null
|
||||
},
|
||||
"tenant": {
|
||||
"tenantId": "tenant-uuid",
|
||||
"tenantName": "Legendary Event Staffing and Entertainment"
|
||||
},
|
||||
"business": {
|
||||
"businessId": "business-uuid",
|
||||
"businessName": "Google Mountain View Cafes"
|
||||
},
|
||||
"requestId": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
Frontend behavior:
|
||||
|
||||
1. Call `POST /auth/client/sign-in`
|
||||
2. If success, sign in locally with Firebase Auth SDK using the same email/password
|
||||
3. Use Firebase SDK token refresh for later API calls
|
||||
4. Use `GET /auth/session` when role/session hydration is needed on app boot
|
||||
|
||||
### Client sign-up
|
||||
|
||||
Request:
|
||||
|
||||
```http
|
||||
POST /auth/client/sign-up
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"companyName": "Legendary Event Staffing and Entertainment",
|
||||
"email": "legendary.owner+v2@krowd.com",
|
||||
"password": "Demo2026!"
|
||||
}
|
||||
```
|
||||
|
||||
What it does:
|
||||
|
||||
- creates Firebase Auth account
|
||||
- creates V2 user
|
||||
- creates tenant
|
||||
- creates business
|
||||
- creates tenant membership
|
||||
- creates business membership
|
||||
|
||||
Frontend behavior after success:
|
||||
|
||||
1. call `POST /auth/client/sign-up`
|
||||
2. sign in locally with Firebase Auth SDK using the same email/password
|
||||
3. use Firebase SDK for later token refresh
|
||||
|
||||
## 4) Staff auth flow
|
||||
|
||||
## Step 1: start phone auth
|
||||
|
||||
Request:
|
||||
|
||||
```http
|
||||
POST /auth/staff/phone/start
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"phoneNumber": "+15551234567"
|
||||
}
|
||||
```
|
||||
|
||||
Possible response A:
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "CLIENT_FIREBASE_SDK",
|
||||
"provider": "firebase-phone-auth",
|
||||
"phoneNumber": "+15551234567",
|
||||
"nextStep": "Complete phone verification in the mobile client, then call /auth/staff/phone/verify with the Firebase idToken.",
|
||||
"requestId": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
This is the normal mobile path when frontend does **not** send recaptcha or integrity tokens.
|
||||
|
||||
Possible response B:
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "IDENTITY_TOOLKIT_SMS",
|
||||
"phoneNumber": "+15551234567",
|
||||
"sessionInfo": "firebase-session-info",
|
||||
"requestId": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
This is the server-managed SMS path.
|
||||
|
||||
## Step 2A: normal mobile path (`CLIENT_FIREBASE_SDK`)
|
||||
|
||||
Frontend must do this on-device:
|
||||
|
||||
1. call `FirebaseAuth.verifyPhoneNumber(...)`
|
||||
2. collect the `verificationId`
|
||||
3. collect the OTP code from the user
|
||||
4. create a Firebase phone credential
|
||||
5. call `signInWithCredential(...)`
|
||||
6. get Firebase `idToken`
|
||||
7. call `POST /auth/staff/phone/verify` with that `idToken`
|
||||
|
||||
Request:
|
||||
|
||||
```http
|
||||
POST /auth/staff/phone/verify
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "sign-in",
|
||||
"idToken": "firebase-id-token-from-device"
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"sessionToken": "firebase-id-token-from-device",
|
||||
"refreshToken": null,
|
||||
"expiresInSeconds": 3600,
|
||||
"user": {
|
||||
"id": "user-uuid",
|
||||
"phone": "+15551234567"
|
||||
},
|
||||
"staff": {
|
||||
"staffId": "staff-uuid"
|
||||
},
|
||||
"requiresProfileSetup": false,
|
||||
"requestId": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
Important:
|
||||
|
||||
- `refreshToken` is expected to be `null` in this path
|
||||
- refresh remains owned by Firebase Auth SDK on the device
|
||||
|
||||
## Step 2B: server SMS path (`IDENTITY_TOOLKIT_SMS`)
|
||||
|
||||
If `start` returned `sessionInfo`, frontend can call:
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "sign-in",
|
||||
"sessionInfo": "firebase-session-info",
|
||||
"code": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
The backend exchanges `sessionInfo + code` with Identity Toolkit and returns the hydrated auth envelope.
|
||||
|
||||
## 5) Sign-out
|
||||
|
||||
Routes:
|
||||
|
||||
- `POST /auth/sign-out`
|
||||
- `POST /auth/client/sign-out`
|
||||
- `POST /auth/staff/sign-out`
|
||||
|
||||
All sign-out routes require:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <firebase-id-token>
|
||||
```
|
||||
|
||||
What sign-out does:
|
||||
|
||||
- revokes backend-side Firebase sessions for that user
|
||||
- frontend should still clear local Firebase Auth state with `FirebaseAuth.instance.signOut()`
|
||||
|
||||
## 6) Session endpoint
|
||||
|
||||
Route:
|
||||
|
||||
- `GET /auth/session`
|
||||
|
||||
Headers:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <firebase-id-token>
|
||||
```
|
||||
|
||||
Use it for:
|
||||
|
||||
- app startup hydration
|
||||
- role validation
|
||||
- deciding whether this app should allow the current signed-in user
|
||||
|
||||
Do not use it as:
|
||||
|
||||
- a refresh endpoint
|
||||
- a login endpoint
|
||||
|
||||
## 7) Error contract
|
||||
|
||||
All auth routes use the standard V2 error envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "STRING_CODE",
|
||||
"message": "Human readable message",
|
||||
"details": {},
|
||||
"requestId": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
Common auth failures:
|
||||
|
||||
- `UNAUTHENTICATED`
|
||||
- `FORBIDDEN`
|
||||
- `VALIDATION_ERROR`
|
||||
- `AUTH_PROVIDER_ERROR`
|
||||
|
||||
## 8) Troubleshooting
|
||||
|
||||
### Staff sign-in does not work, but endpoints are reachable
|
||||
|
||||
The most likely causes are:
|
||||
|
||||
1. frontend expected `POST /auth/staff/phone/start` to always return `sessionInfo`
|
||||
2. frontend did not complete Firebase phone verification on-device
|
||||
3. frontend called `POST /auth/staff/phone/verify` without a valid Firebase `idToken`
|
||||
4. frontend phone-auth setup in Firebase mobile config is incomplete
|
||||
|
||||
### `POST /auth/staff/phone/start` returns `CLIENT_FIREBASE_SDK`
|
||||
|
||||
That is expected for the normal mobile flow when no recaptcha or integrity tokens are sent.
|
||||
|
||||
### There is no `/auth/refresh`
|
||||
|
||||
That is also expected right now.
|
||||
|
||||
Refresh is handled by Firebase Auth SDK on the client.
|
||||
@@ -16,6 +16,7 @@ That includes:
|
||||
- staff dashboard, availability, payments, shifts, profile sections, documents, attire, certificates, bank accounts, benefits, privacy, and frequently asked questions
|
||||
- staff availability, tax forms, emergency contacts, bank account, shift decision, clock-in/out, and swap write flows
|
||||
- upload and verification flows for profile photo, government document, attire, and certificates
|
||||
- attendance policy enforcement, geofence incident review, background location-stream ingest, and queued manager alerts
|
||||
|
||||
## What was validated live
|
||||
|
||||
@@ -41,5 +42,6 @@ The remaining items are not blockers for current mobile frontend migration.
|
||||
They are follow-up items:
|
||||
|
||||
- extend the same unified pattern to new screens added after the current mobile specification
|
||||
- add stronger observability and contract automation around the unified route surface
|
||||
- add stronger contract automation around the unified route surface
|
||||
- add a device-token registry and dispatch worker on top of `notification_outbox`
|
||||
- keep refining reporting and financial read models as product scope expands
|
||||
|
||||
@@ -8,6 +8,8 @@ The gateway keeps backend services separate internally, but frontend should trea
|
||||
|
||||
## 1) Auth routes
|
||||
|
||||
Full auth behavior, including staff phone flow and refresh rules, is documented in [Authentication](./authentication.md).
|
||||
|
||||
### Client auth
|
||||
|
||||
- `POST /auth/client/sign-in`
|
||||
@@ -41,6 +43,7 @@ The gateway keeps backend services separate internally, but frontend should trea
|
||||
- `GET /client/coverage`
|
||||
- `GET /client/coverage/stats`
|
||||
- `GET /client/coverage/core-team`
|
||||
- `GET /client/coverage/incidents`
|
||||
- `GET /client/hubs`
|
||||
- `GET /client/cost-centers`
|
||||
- `GET /client/vendors`
|
||||
@@ -59,6 +62,8 @@ The gateway keeps backend services separate internally, but frontend should trea
|
||||
|
||||
### Client writes
|
||||
|
||||
- `POST /client/devices/push-tokens`
|
||||
- `DELETE /client/devices/push-tokens`
|
||||
- `POST /client/orders/one-time`
|
||||
- `POST /client/orders/recurring`
|
||||
- `POST /client/orders/permanent`
|
||||
@@ -112,8 +117,11 @@ The gateway keeps backend services separate internally, but frontend should trea
|
||||
### Staff writes
|
||||
|
||||
- `POST /staff/profile/setup`
|
||||
- `POST /staff/devices/push-tokens`
|
||||
- `DELETE /staff/devices/push-tokens`
|
||||
- `POST /staff/clock-in`
|
||||
- `POST /staff/clock-out`
|
||||
- `POST /staff/location-streams`
|
||||
- `PUT /staff/availability`
|
||||
- `POST /staff/availability/quick-set`
|
||||
- `POST /staff/shifts/:shiftId/apply`
|
||||
@@ -159,7 +167,119 @@ These are exposed as direct unified aliases even though they are backed by `core
|
||||
- `roleId` on `POST /staff/shifts/:shiftId/apply` is the concrete `shift_roles.id` for that shift, not the catalog role definition id.
|
||||
- `accountType` on `POST /staff/profile/bank-accounts` accepts either lowercase or uppercase and is normalized by the backend.
|
||||
- File upload routes return a storage path plus a signed URL. Frontend uploads the file directly to storage using that URL.
|
||||
- Verification routes are durable in the v2 backend and were validated live through document, attire, and certificate upload flows.
|
||||
- Verification upload and review routes are live and were validated through document, attire, and certificate flows. Do not rely on long-lived verification history durability until the dedicated persistence slice is landed in `core-api-v2`.
|
||||
- Attendance policy is explicit. Reads now expose `clockInMode` and `allowClockInOverride`.
|
||||
- `clockInMode` values are:
|
||||
- `NFC_REQUIRED`
|
||||
- `GEO_REQUIRED`
|
||||
- `EITHER`
|
||||
- For `POST /staff/clock-in` and `POST /staff/clock-out`:
|
||||
- send `nfcTagId` when clocking with NFC
|
||||
- send `latitude`, `longitude`, and `accuracyMeters` when clocking with geolocation
|
||||
- send `proofNonce` and `proofTimestamp` for attendance-proof logging; these are most important on NFC paths
|
||||
- send `attestationProvider` and `attestationToken` only when the device has a real attestation result to forward
|
||||
- send `overrideReason` only when the worker is bypassing a geofence failure and the shift/hub allows overrides
|
||||
- `POST /staff/location-streams` is for the background tracking loop after a worker is already clocked in.
|
||||
- `GET /client/coverage/incidents` is the review feed for geofence breaches, missing-location batches, and clock-in overrides.
|
||||
- `POST /client/coverage/late-workers/:assignmentId/cancel` is the client-side recovery action when lateness is confirmed by incident evidence or elapsed grace time.
|
||||
- Raw location stream payloads are stored in the private v2 bucket; SQL only stores the summary and incident index.
|
||||
- Push delivery is backed by:
|
||||
- SQL token registry in `device_push_tokens`
|
||||
- durable queue in `notification_outbox`
|
||||
- per-attempt delivery records in `notification_deliveries`
|
||||
- private Cloud Run worker service `krow-notification-worker-v2`
|
||||
- Cloud Scheduler job `krow-notification-dispatch-v2`
|
||||
|
||||
### Push token request example
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": "FCM",
|
||||
"platform": "IOS",
|
||||
"pushToken": "expo-or-fcm-device-token",
|
||||
"deviceId": "iphone-15-pro-max",
|
||||
"appVersion": "2.0.0",
|
||||
"appBuild": "2000",
|
||||
"locale": "en-US",
|
||||
"timezone": "America/Los_Angeles"
|
||||
}
|
||||
```
|
||||
|
||||
Push-token delete requests may send `tokenId` or `pushToken` either:
|
||||
|
||||
- as JSON in the request body
|
||||
- or as query params on the `DELETE` URL
|
||||
|
||||
Using query params is safer when the client stack or proxy is inconsistent about forwarding `DELETE` bodies.
|
||||
|
||||
### Clock-in request example
|
||||
|
||||
```json
|
||||
{
|
||||
"shiftId": "uuid",
|
||||
"sourceType": "GEO",
|
||||
"deviceId": "iphone-15-pro",
|
||||
"latitude": 37.4221,
|
||||
"longitude": -122.0841,
|
||||
"accuracyMeters": 12,
|
||||
"proofNonce": "nonce-generated-on-device",
|
||||
"proofTimestamp": "2026-03-16T09:00:00.000Z",
|
||||
"overrideReason": "Parking garage entrance is outside the marked hub geofence",
|
||||
"capturedAt": "2026-03-16T09:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Location-stream batch example
|
||||
|
||||
```json
|
||||
{
|
||||
"shiftId": "uuid",
|
||||
"sourceType": "GEO",
|
||||
"deviceId": "iphone-15-pro",
|
||||
"points": [
|
||||
{
|
||||
"capturedAt": "2026-03-16T09:15:00.000Z",
|
||||
"latitude": 37.4221,
|
||||
"longitude": -122.0841,
|
||||
"accuracyMeters": 12
|
||||
},
|
||||
{
|
||||
"capturedAt": "2026-03-16T09:30:00.000Z",
|
||||
"latitude": 37.4301,
|
||||
"longitude": -122.0761,
|
||||
"accuracyMeters": 20
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"source": "background-workmanager"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Coverage incidents response shape
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"incidentId": "uuid",
|
||||
"assignmentId": "uuid",
|
||||
"shiftId": "uuid",
|
||||
"staffName": "Ana Barista",
|
||||
"incidentType": "OUTSIDE_GEOFENCE",
|
||||
"severity": "CRITICAL",
|
||||
"status": "OPEN",
|
||||
"clockInMode": "GEO_REQUIRED",
|
||||
"overrideReason": null,
|
||||
"message": "Worker drifted outside hub geofence during active monitoring",
|
||||
"distanceToClockPointMeters": 910,
|
||||
"withinGeofence": false,
|
||||
"occurredAt": "2026-03-16T09:30:00.000Z"
|
||||
}
|
||||
],
|
||||
"requestId": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
## 6) Why this shape
|
||||
|
||||
|
||||
Reference in New Issue
Block a user