Profile and settings
Background
On Shukah the app changes the profile and the push-notification switch with one call,
PUT /maeda/client/{client_id}, and tells Shukah about the phone with
POST /maeda/client/add-device-id. The auth service has three routes for this: one for the profile,
one for the push switch, and a device-id route. Three things need care: the push switch needs its own
call because the profile route ignores push_enabled, the profile route refuses a changed phone, and
the device-id route is a stub that stores nothing.
Feature
All three calls are part of the signed-in client's account screens. In the app snapshot of 2026-02-26 they happen here:
| Call | Where in the app |
|---|---|
| Update profile | On the Personal info screen, when the client taps save. One call carries client_id, contact_phone, first_name, last_name, email_address, sex, receive_offers and birth_date (as YYYY-MM-DD). The phone field is shown but disabled, and its value is the phone the app loaded with the profile, so the app sends back the phone it received. |
| Update setting | On the Settings screen, when the client flips the push-notification switch. The app sends client_id and push_enabled (0 or 1). On Shukah this goes through the same route as the profile. |
| Add device id | At app start, right after the profile check (verify-token), when the client is logged in and the call has not been made on this install yet. It sends the device's unique id once. After a 200 answer the app records that it has sent it and never sends it again. |
The first two calls need the Locate JWT (see "Getting a test token" on the module overview) and answer with the client's profile, the same 13 fields as Verify token, after the change. The device-id call needs no token.
PUT /api/v1/clients/:client_id
PUT /maeda/client/:client_id✅ Replaced- 🔴 Shukah token gets 401 "Unauthorization"; send a Locate JWT (from exchange-token; keep the Shukah token)
- 🟡 push_enabled is ignored on this route (the app still sends it here: user.dto.ts:100-101); see update-setting
- 🟡 contact_phone must equal the stored phone, with "+", else 400; Shukah's changed-phone rule is untested
- 🟡 Bad input is 400 {status,message,stack} on Locate, 422 [{field,message}] on Shukah; the app loses field
- 🟡 The edit stays in Locate and is not pushed to Shukah (update-profile/index.js has no push)
Request
A partial update: only the keys that are in the body change. The service returns the first problem it
finds and checks in this order: client_id, contact_phone, first_name, last_name,
email_address, birth_date.
| Field | In | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
Authorization | header | string | yes | Bearer <locate-jwt>. A Shukah token gets 401. | The Locate JWT. |
client_id | path | integer | yes | Must equal the client id in the token, else 400 client_id does not match the authenticated client. | The caller's own id. |
client_id | body | integer or numeric string | no | If sent, it must also equal the token's client id, with the same 400. null fails. | The caller's own id again, as the app sends it. |
contact_phone | body | string | no | Cannot be changed. If sent, the value with whitespace removed must equal the stored phone exactly, including the leading + (966500000000 does not match +966500000000), else 400 contact_phone cannot be changed. It is never written. | The client's phone. |
first_name, last_name | body | string or null | no | null or a string of at most 100 characters after trimming, else 400 first_name is invalid or last_name is invalid. Stored trimmed. null clears it, and "" is stored as "". | Names. |
email_address | body | string or null | no | Any string or null: no format check and no length limit, else 400 email_address is invalid. Stored trimmed. | Email. |
birth_date | body | string or null | no | A value that JavaScript new Date() can read, stored as the UTC date YYYY-MM-DD. null clears it. An unreadable value, including "", gives 400 birth_date is invalid. | Birth date. The app sends YYYY-MM-DD. |
sex | body | integer | no | Not validated. 1 is male and 2 is female. Anything else, including 0, 9 and null, is stored as unknown and read back as 0. | Gender. |
receive_offers | body | 0, 1 or boolean | no | Not validated. Only 1, "1" and true mean on. Anything else means off. | Whether the client receives offers. |
push_enabled and any other key | body | — | no | Ignored. The push switch has its own route. | — |
A request used for the capture below, which writes the client's current value back:
{
"client_id": 356259,
"receive_offers": 0
}
Response
200 with the 13-field profile after the update, with no wrapper. The fields and their rules are the
table under Verify token.
{
"client_id": 356259,
"odoo_id": "1342170",
"contact_phone": "+966500000000",
"status": "active",
"email_address": "user@example.com",
"first_name": "Test",
"last_name": "User",
"sex": 1,
"birth_date": "1990-01-01",
"receive_offers": 0,
"push_enabled": 0,
"is_food_truck_user": 1,
"is_cod_disabled": 0
}
Captured on dev, 2026-10-01 (client 356259, Task 9 live write, the current value written back); personal fields replaced.
Errors
| HTTP | Body | When |
|---|---|---|
400 | {"status":400,"message":"birth_date is invalid"} | birth_date cannot be read as a date. The messages for the other fields have the same shape: client_id does not match the authenticated client, contact_phone cannot be changed, first_name is invalid, last_name is invalid, email_address is invalid. Only the first failing message is returned. On dev the body also has a stack field (see below). |
401 | {"status":401,"message":"Unauthorization"} | No token, a token that is not a Locate JWT, or an inactive client. This comes before any check of the body. |
500 | On dev: {"status":500,"message":"<raw error text>","stack":"<stack trace>"}. In production: {"status":500,"message":"Internal server error"}. | An unexpected failure. Not captured. On dev the message is the raw error text and a stack field is added; Internal server error appears only in production (UAT and production: Not documented yet). |
The birth_date 400 was captured on dev on 2026-10-01, and again on 2026-10-02 with
"birth_date":"not-a-date". The other 400 messages are from the code and the service doc, and
were not captured. The 401 body was captured on dev on 2026-10-01 with no token. On dev every 400
body also carries a stack field with a server stack trace as text, for example
{"status":400,"message":"birth_date is invalid","stack":"<stack trace>"} (captured 2026-10-02). The
samples in the table leave it out.
Try it
The first call writes to dev, so send the client's current value, as the capture did. The second call is rejected and writes nothing.
export AUTH_BASE_URL=https://dev.authentication.locate.sa
export TOKEN='<locate-jwt>' # see "Getting a test token" on the module overview
export CLIENT_ID='<client-id-in-the-token>'
curl -s -X PUT "$AUTH_BASE_URL/api/v1/clients/$CLIENT_ID" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "{\"client_id\":$CLIENT_ID,\"receive_offers\":0}"
curl -s -X PUT "$AUTH_BASE_URL/api/v1/clients/$CLIENT_ID" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"birth_date":"not-a-date"}'
Expected: the first call gives 200 and the profile shown under Response. The second gives 400 with
birth_date is invalid.
Differences from Shukah
| Aspect | Shukah | Locate | Impact on the app |
|---|---|---|---|
| URL | PUT $SHUKAH_BASE_URL/maeda/client/{client_id} | PUT $AUTH_BASE_URL/api/v1/clients/{client_id} | Change the base URL and the path. |
| Token | Shukah opaque token. | Locate JWT only. | Get the JWT with exchange-token first, and keep the Shukah token for order-v2. Do not move login to Locate yet. |
push_enabled | The same route also takes push_enabled, and the app sends it here for the push switch. | Ignored. The call still answers 200 with the unchanged profile. | Move the push switch to the setting call below, or it silently does nothing (code reading, not captured). |
contact_phone | Whether Shukah accepts a changed phone: Not documented yet. | Must equal the stored phone exactly, else 400. | None while the app sends back the phone it loaded from Locate. Do not reformat it. |
| Validation and errors | 422 with an array of field errors: [{"field":"birth_date","message":"The birth date field must match the format Y-m-d."}] for "birth_date":"not-a-date" (captured on dev, 2026-10-02). Shukah's other rules: Not documented yet. | 400 with {"status":400,"message":"..."} (plus stack on dev), the rules in the Request table. | The status changes from 422 to 400, and the field name is gone. The app shows message. Check the wording on the Personal info screen. |
| Success body | The 13-field profile (captured on dev, 2026-10-02, with the request above). | The 13-field profile (captured on dev, 2026-10-01 and 2026-10-02). | None: the same 13 keys, types and values for client 356259 on 2026-10-02. |
sex | The app's type allows 9 for "not applicable". | 9 is stored as unknown and read back as 0. | A client whose Shukah profile has 9 reads as unknown after the switch. The Personal info form offers only male and female. |
| Copy to Shukah | Not applicable. | The edit stays in Locate and is not copied to Shukah. | Anything that still reads the profile from Shukah keeps the old values. |
PUT /api/v1/clients/setting
PUT /maeda/client/:client_id✅ Replaced- 🔴 Shukah token gets 401 "Unauthorization"; send a Locate JWT (from exchange-token; keep the Shukah token)
- 🔴 The app flips push through PUT client/:id; Locate ignores push_enabled there, so call PUT /clients/setting
- 🟡 client_id is required in the body and must equal the token's; a missing push_enabled is 400
- 🟡 The change stays in Locate and is not pushed to Shukah
- 🟡 The pair wrote the current value (push_enabled 0), so Shukah applying push_enabled here is unproven
Request
| Field | In | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
Authorization | header | string | yes | Bearer <locate-jwt>. A Shukah token gets 401. | The Locate JWT. |
client_id | body | integer or numeric string | yes | Must equal the client id in the token. Missing or different gives 400 client_id does not match the authenticated client. | The caller's own id. There is no id in the path. |
push_enabled | body | 0, 1 or boolean | yes | The key must be present, else 400 push_enabled is required. Only 1, "1" and true switch it on. Everything else switches it off, including null, "true" and 2. | Push notifications on or off. |
client_id is checked first. Only push_enabled is supported on this route today.
A request used for the capture below, which writes the current value back:
{
"client_id": 356259,
"push_enabled": 0
}
Response
200 with the 13-field profile after the update, with no wrapper, the same as under
Verify token. push_enabled shows the new value.
{
"client_id": 356259,
"odoo_id": "1342170",
"contact_phone": "+966500000000",
"status": "active",
"email_address": "user@example.com",
"first_name": "Test",
"last_name": "User",
"sex": 1,
"birth_date": "1990-01-01",
"receive_offers": 0,
"push_enabled": 0,
"is_food_truck_user": 1,
"is_cod_disabled": 0
}
Captured on dev, 2026-10-01 (client 356259, Task 9 live write, the current value written back); personal fields replaced.
Errors
| HTTP | Body | When |
|---|---|---|
400 | {"status":400,"message":"client_id does not match the authenticated client"} | client_id is missing or is not the token's client. This includes a request with no JSON body, which the service reads as {}. Captured with client_id set to 1. |
400 | {"status":400,"message":"push_enabled is required"} | The push_enabled key is absent and client_id is correct. Not captured. |
401 | {"status":401,"message":"Unauthorization"} | No token, a token that is not a Locate JWT, or an inactive client. |
500 | On dev: {"status":500,"message":"<raw error text>","stack":"<stack trace>"}. In production: {"status":500,"message":"Internal server error"}. | An unexpected failure. Not captured. On dev the message is the raw error text and a stack field is added; Internal server error appears only in production (UAT and production: Not documented yet). |
The client_id 400 and the 401 were captured on dev on 2026-10-01 (the 401 with no token). The
push_enabled 400 is from the code and the service doc. On dev the 400 body also carries a
stack field, which this page leaves out.
Try it
The first call writes to dev, so send the client's current value. The second call is rejected and writes nothing.
export AUTH_BASE_URL=https://dev.authentication.locate.sa
export TOKEN='<locate-jwt>' # see "Getting a test token" on the module overview
export CLIENT_ID='<client-id-in-the-token>'
curl -s -X PUT "$AUTH_BASE_URL/api/v1/clients/setting" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "{\"client_id\":$CLIENT_ID,\"push_enabled\":0}"
curl -s -X PUT "$AUTH_BASE_URL/api/v1/clients/setting" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"client_id":1,"push_enabled":0}'
Expected: the first call gives 200 and the profile. The second gives 400 with
client_id does not match the authenticated client.
Differences from Shukah
| Aspect | Shukah | Locate | Impact on the app |
|---|---|---|---|
| URL | PUT $SHUKAH_BASE_URL/maeda/client/{client_id}, the same route as the profile update. The body is {client_id, push_enabled}. | PUT $AUTH_BASE_URL/api/v1/clients/setting, with no id in the path. | The app's push switch calls the profile update today. Point it at this route, or Locate ignores the change. |
| Token | Shukah opaque token. | Locate JWT only. | Get the JWT with exchange-token first, and keep the Shukah token for order-v2. Do not move login to Locate yet. |
client_id | Sent in the path and the body. Whether Shukah checks it against the token: Not documented yet. | Must be in the body and must equal the token's client. | None: the app already sends client_id in the body. |
| Validation and errors | Not documented yet. | 400 with {"status":400,"message":"..."} for a wrong client_id or a missing push_enabled. | The app already sends both keys, so these should not occur. |
| Success body | The 13-field profile with push_enabled 0 (captured on dev, 2026-10-02, body {"client_id":356259,"push_enabled":0}, the current value written back). | The same 13-field profile (captured on dev, 2026-10-02). | The app reads push_enabled from the answer to set the switch. The capture wrote the current value, so it does not prove that Shukah applies the change on this route. |
| Copy to Shukah | Not applicable. | The change stays in Locate and is not copied to Shukah. | Shukah keeps the old value. |
POST /api/v1/clients/add-device-id
POST /maeda/client/add-device-id✅ Replaced- 🟡 Locate stores nothing; Shukah saved a device row (id 1074); the app marks the id as sent anyway
- 🟡 Locate echoes fixed id 920 and client_id 356315 for everyone; Shukah returns the caller's client_id
- 🟡 Locate needs no token (no guard, routes/client.js:23); the app can keep sending its token
Request
No token is needed: a token or any other header is ignored.
| Field | In | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
device_id | body | string | no | Not validated. It is echoed back when present. | The device's unique id. |
| Any other body key, query key or header | body, query, header | — | no | Ignored, for example language. | — |
{
"device_id": "00000000-0000-4000-8000-000000000000"
}
Response
200 with a device record. It is a stub, so two of the three values are fixed:
| Field | Type | Nullable | Format / constraints | Description |
|---|---|---|---|---|
id | integer | no | Always 920, for every caller. | A stand-in device record id. |
client_id | integer | no | Always 356315, for every caller. It is not the caller's id. | A stand-in client id. |
device_id | string | no | The device_id from the request. When the request has none, a fixed sample id 46AE56EF-29BA-40AE-B436-AF22B119FE1E. | The device id, echoed. |
{
"id": 920,
"client_id": 356315,
"device_id": "00000000-0000-4000-8000-000000000000"
}
Captured on dev, 2026-10-01 (Task 9 live call, no token; the request is the one above). Nothing is stored by this call.
Errors
None are defined. There is no 401, because the route does not check the token. A request with
no JSON body is read as an empty body and still answers 200 (the empty-body call was captured on
2026-10-01).
Try it
export AUTH_BASE_URL=https://dev.authentication.locate.sa
curl -s -X POST "$AUTH_BASE_URL/api/v1/clients/add-device-id" -H "Content-Type: application/json" -d '{"device_id":"00000000-0000-4000-8000-000000000000"}'
Expected: 200 and the record shown under Response. It is safe to call: it writes nothing.
Differences from Shukah
| Aspect | Shukah | Locate | Impact on the app |
|---|---|---|---|
| URL | POST $SHUKAH_BASE_URL/maeda/client/add-device-id | POST $AUTH_BASE_URL/api/v1/clients/add-device-id | Change the base URL and the path. |
| What it does | Stores a new device row for the caller on every call (captured on dev, 2026-10-02: row 1074). No API removes it. | Nothing: no storage, no link to the client, no token check. | After the switch no device is registered by this call. If something on Shukah depends on the device record, it stops: Not documented yet. |
| Answer | {"id":1074,"client_id":356259,"device_id":"<the id sent>"}: the new row's id and the caller's id (captured on dev, 2026-10-02). The app ignores the answer. | {id: 920, client_id: 356315, device_id} for everyone. | None for the app. |
| Token | The app sends its Shukah token. Whether Shukah requires it: Not documented yet. | Not needed, not checked. | None. |
| Retry | The app records that it sent the id after a 200 answer. | The stub always answers 200. | The app will mark the device as sent and never try again, although nothing was stored. |
Watch out
- The profile route ignores
push_enabledand still answers200with the old value, so the push switch must callPUT /api/v1/clients/settinginstead. contact_phonemust equal the stored phone exactly, including the leading+, or the call fails with400.add-device-idalways answers200and stores nothing, so the app marks the device as sent although no device is registered.
Details (for backend)
Only dev was checked: host https://dev.authentication.locate.sa, prefix /api/v1. The UAT and
production hosts and prefix: Not documented yet. Error bodies on dev carry a stack field (captured
on 2026-10-01 on the profile and setting 400s), so dev does not run with NODE_ENV=production; the
handler adds stack only outside production (src/middlewares/error-handler.js:12-35). The same handler makes an unexpected 500 carry the raw error text and a stack on dev, and Internal server error only in production (src/middlewares/error-handler.js:28-33).
Route order and guard
PUT /clients/setting is registered before PUT /clients/:client_id, so setting can never be read
as a client id. Both run the client guard, so the token rules are those of
Verify token. The device-id route has only the async wrapper and no
guard (src/routes/client.js:23-26).
Profile update
- The checks are express-validator chains run after the guard. The handler throws the first
message, so only one
400message comes back (src/controllers/client/update-profile/validate.js:12-31,src/controllers/client/update-profile/index.js:12-15). check('client_id')looks in the body, cookies, headers, params and query, so a mismatchingclient_idin the query string or a header also fails with the same message (offline check, 2026-10-01: the real validator chains were run against about 20 crafted requests).contact_phoneis compared afternormalizePhone, which removes whitespace only. Anullvalue fails, and the value is never written (src/controllers/client/update-profile/validate.js:20-23,src/controllers/client/update-profile/parse-payload.js:8-9,src/sync/lib/casts.js:40-41).- Names and email are stored trimmed.
nullis stored asnulland""as""(src/controllers/client/update-profile/parse-payload.js:5,11-13). birth_datepasses when it isnullortoBirthDateStringgives a date. That function returnsnullfor an empty value and for an unreadable one, and the date it keeps is the UTC date (src/sync/lib/casts.js:2-11,src/controllers/client/update-profile/validate.js:27-30,src/controllers/client/update-profile/parse-payload.js:14-16).sexgoes throughmapGender:1or"1"isMALE,2or"2"isFEMALE, everything else isUNKNOWN, and the response mapsUNKNOWNto0.receive_offersgoes throughtoBool(src/sync/lib/casts.js:1,14-18,src/controllers/client/update-profile/parse-payload.js:17-18,src/controllers/client/setting/mapper.js:14-18,34).- The handler changes the client the guard loaded, sets
updated_atand saves. The save clears the cached client (src/controllers/client/update-profile/index.js:17-20,src/models/client.js:158-161). There is no push to Shukah (docs/plans/2026-07-16-shukah-update-profile-api-local.md:42,140). - The profile route does not read
push_enabled(docs/plans/2026-07-16-shukah-update-profile-api-local.md:39,src/controllers/client/update-profile/parse-payload.js:10-19).
Setting update
The handler needs client_id to equal the token's client, then push_enabled not to be undefined,
sets push_enabled with toBool and saves. It changes nothing else and does not push to Shukah
(src/controllers/client/setting/index.js:9-24, src/sync/lib/casts.js:1,
docs/plans/2026-07-10-shukah-client-setting-api-local.md:100-106). A request with no JSON body
arrives as {}, which gives the client_id message first.
Device id stub
The handler returns {id: 920, client_id: 356315, device_id: req.body?.device_id ?? '<sample uuid>'}
and does nothing else. The query and headers are ignored
(src/controllers/client/add-device-id/index.js:1-10,
docs/plans/2026-07-15-shukah-add-device-id-api-local.md:15-16,65-69). A unit test pins it
(src/controllers/client/__tests__/add-device-id.test.js:11-35).
Where the docs and the code disagree (code wins)
- The 2026-07-16 design doc gives the Shukah update path as
PUT /api/v1/clients/{client_id}(docs/plans/2026-07-16-shukah-update-profile-api-local.md:14-19). The app callsPUT /maeda/client/{client_id}. - The auth service's route history shows a Shukah
PUT /maeda/api/settingroute that the Locate service replaced (commitd837492). The app never calls it: the push switch goes through the profile route. - The profile call's
sex,receive_offersandpush_enabledbehaviour on Shukah is not in any repo: Not documented yet. - 2026-10-02: this page had no Shukah success body for update profile and update setting, and no
Shukah error body. They are captured now: the same 13-field profile on both sides, and a
422array of field errors on Shukah where Locate answers400. - 2026-10-02: this page wrote Locate's
400body as{"status":400,"message":"..."}. On dev it also carriesstack, a server stack trace (src/middlewares/error-handler.js:33, added wheneverNODE_ENVis notproduction). - 2026-10-02: this page said what Shukah stores for add-device-id was not documented, and its answer not confirmed. Shukah stores a new device row per call and answers with its id and the caller's id.
Sources
locate-authentication-service@6757aa4:src/routes/client.js:23-26,src/controllers/client/update-profile/validate.js:4-31,src/controllers/client/update-profile/index.js:11-23,src/controllers/client/update-profile/parse-payload.js:5-19,src/controllers/client/setting/index.js:9-24,src/controllers/client/setting/mapper.js:14-38,src/controllers/client/add-device-id/index.js:1-10,src/controllers/client/__tests__/add-device-id.test.js:11-35,src/sync/lib/casts.js:1-18,40-41,src/models/client.js:158-161,src/middlewares/error-handler.js:12-35locate-authentication-service@6757aa4docs:docs/api/client-auth-and-address-book.md§10 and §11 (lines 412-528; last changed 2026-07-27),docs/plans/2026-07-16-shukah-update-profile-api-local.md:14-19,36-44,138-140,docs/plans/2026-07-10-shukah-client-setting-api-local.md:100-106,docs/plans/2026-07-15-shukah-add-device-id-api-local.md:12-18,63-69- Mobile snapshot
mobile/locate-client@e960c3a(branchdevelopment, 2026-02-26; the app's own code, not Locate):src/entities/user/model/api/services/user.service.ts:20-27,68-74,src/entities/user/model/api/dto/user.dto.ts:58-121,165-171,src/entities/user/model/types/user.server.ts:3-14,src/entities/user/model/constants/user.server.ts:1-6,src/entities/user/model/services/updateUser/updateUser.ts:13-42,src/features/UpdateProfileForm/model/services/updateProfile.ts:80-91,src/features/UpdateProfileForm/model/services/initUpdateProfileForm.ts:13-23,src/features/UpdateProfileForm/ui/UpdateProfileForm/UpdateProfileForm.tsx:42-45,73,155-160,platforms/mobile/products/locate/screens/home/PersonalInfo/ui/PersonalInfo/PersonalInfo.tsx:51,src/features/PushNotificationSettings/model/services/togglePushNotificationEnable.ts:22-26,src/features/PushNotificationSettings/ui/PushNotificationSettings/PushNotificationSettings.tsx:42-50,platforms/mobile/products/locate/screens/home/Settings/ui/Settings/Settings.tsx:37,src/entities/user/model/services/sendDeviceIdentificationData/sendDeviceIdentificationData.ts:18-30,src/entities/user/model/services/init/init.ts:21-23 - Dev captures, 2026-10-02 (parity audit):
update-profile--shukah,update-profile--locate-jwt,update-profile-bad--shukah,update-profile-bad--locate-jwt,update-setting--shukah,update-setting--locate-jwt,add-device-id--shukah,add-device-id--locate-jwt - Dev captures, 2026-10-01:
locate-update-profile-unchanged,locate-update-profile-bad-birth-date,locate-setting-unchanged,locate-setting-mismatched-client,locate-add-device-id,probe-locate-update-profile,probe-locate-setting,probe-locate-add-device-id