Compare commits

..

3 Commits

Author SHA1 Message Date
jackos1998 fd43bede6e Update AGENTS.md with app token details 2026-06-14 15:23:36 +01:00
jackos1998 3e6ef7a908 Expand PT-booking docs in api.md
Flesh out `PersonalTrainings/Bookings` (past+future in one list, client-side
`startDate` filtering, per-field notes, `timestamp` delta-sync) and the
`PersonalTrainingsTypes` / `Instructors/Instructors` enrichment endpoints.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 03:06:37 +01:00
jackos1998 07804e8ae8 Expand AGENTS.md: checks, brand assets, decompiled-APK note
Add a Checks section (`py_compile` + `nix build`), note the `brand/` assets
and their source, broaden the backtick-quoting rule, refine the token-expiry
guidance, and explain using decompiled APK output if provided.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 02:42:32 +01:00
2 changed files with 85 additions and 16 deletions
+37 -5
View File
@@ -76,8 +76,39 @@ When dumping flows, redact `Authorization` / `Cookie` headers and the login body
Some questions the capture can't answer (token lifecycle, the white-label ID,
error codes) need the app itself. If you're pointed at **decompiled APK output**
(e.g. apktool `smali/`), grep it there — but it's R8-obfuscated: class names are
mangled and library types (e.g. OkHttp) may be shrunk/repackaged, so a missing
grep hit is not proof of absence. No such decompilation lives in this repo.
mangled and library types (e.g. OkHttp, `androidx.security.crypto`) may be
shrunk/repackaged, so a missing grep hit is not proof of absence. No such
decompilation lives in this repo (the user can supply a local apktool dump on
request; app package `com.perfectgym.perfectgymgo2.westwoodclub`). The original
ELPassion source package names survive obfuscation under
`smali/com/elpassion/perfectgym/`, which is the useful entry point.
### How the app stores the bearer token
Confirmed from the decompilation (relevant because it answers the token-lifecycle
question and shows there's no second auth secret to capture):
- The login response DTO (`AccountAuthorizationGoApiDto`) carries `token`,
`tokenType`, `authorizationHeader`, and a **nullable `expireTime`**.
`DtoMapperKt.asAuthorizeResponse` keeps **only the bare `token`** string;
`tokenType`/`authorizationHeader`/`expireTime` are discarded. The app
reconstructs `Authorization: bearer <token>` itself per request.
- The token is **persisted in `EncryptedSharedPreferences`** (androidx
security-crypto, R8-repackaged to `l3.*`; AES-256-GCM master key in the Android
Keystore). The store class is `f6/o` (interface `f6/p`); the backing file is
named `wevgebvre` and values are Moshi-JSON-encoded. Token key: **`"token"`**
(writer `f6/o.h(String)`, reader `f6/o.r()`).
- **Load path:** at DI-graph construction the provider (`androidx/room/v0`) calls
`f6/p.r()` and passes the stored token into the `appmodel/s0` AppModel
constructor as its initial value, which seeds the reactive `tokenS`
(`Optional<String>`) stream — so the app comes up already authenticated.
Login writes the new token back via `f6/p.h(...)` (dispatcher `z4/c`).
- **Legacy + migration:** older builds kept the same `"token"` key in the
*plaintext* default `SharedPreferences` (`f6/b0`, via
`PreferenceManager.getDefaultSharedPreferences`). `PerfectGymApplication` runs a
one-time migration gated by an `isMigrated` flag: copy from `f6/b0` into the
encrypted `f6/o`, then `clear()` + `deleteSharedPreferences()` the plaintext
file. So the token is no longer recoverable in cleartext on current installs.
## API essentials
@@ -85,9 +116,10 @@ Full detail in `api.md`. Quick reference:
- Responses are wrapped `{ "data": ..., "errors": ... }`; `errors` is `null` on success.
- **Auth:** `POST /v1/Authorize/LogInWithEmail` (white-label ID goes in the body)
→ reuse the returned `bearer <token>` as the `Authorization` header. The token
most likely doesn't expire (no refresh token in the response, app appears to
store no credentials); on `401`/`403` get a fresh one. See `api.md`.
→ reuse the returned `bearer <token>` as the `Authorization` header. Treat the
token as long-lived: there's no refresh token, the app persists only the token
(not the email/password) and discards the response's `expireTime`, so refetch
reactively on `401`/`403`. See `api.md` and the token-storage notes above.
- Authenticated endpoints need **only** the `Authorization` header — the `X-Go-*`
headers and app `User-Agent` the app sends are not required (verified against
the clubs endpoint).
+48 -11
View File
@@ -228,17 +228,19 @@ overrides and was empty in the capture.
`GET /v1/PersonalTrainings/Bookings?timestamp=0`
The account's PT sessions. `Classes/BookingsV2` (same shape, class bookings) was
empty in the capture. `instructorId` maps to `Instructors/Instructors`;
`clubId` to the club list.
The account's own PT sessions**past and future in one list**, newest-booked
last. The endpoint does not filter by date; to surface *upcoming* bookings,
filter client-side on `startDate > now` (and typically `not isCanceled` /
`not isCompleted`). `Classes/BookingsV2` (same shape, for class bookings) was
empty in the capture.
```json
{
"name": "1st Consultation",
"startDate": "2026-04-20T08:45:00+01:00",
"endDate": "2026-04-20T09:15:00+01:00",
"name": "New - 4th Program- Review",
"startDate": "2026-05-26T08:30:00+01:00",
"endDate": "2026-05-26T09:00:00+01:00",
"isCanceled": false,
"isCompleted": false,
"isCompleted": true,
"instructorId": 0,
"clubId": 962,
"personalTrainingTypeId": 0,
@@ -248,6 +250,23 @@ empty in the capture. `instructorId` maps to `Instructors/Instructors`;
}
```
Fields:
- `name` — already human-readable and self-contained (e.g.
`"New - 1st Consultation"`, `"New - 4th Program- Review"`). A "next PT booking"
sensor needs **only this endpoint** — the lookups below are enrichment.
- `startDate` / `endDate` — ISO-8601 **with** offset (`+01:00`); the duration is
implied (no separate field on the booking).
- `isCanceled` / `isCompleted` — booleans. A future session has both `false`.
- `instructorId``Instructors/Instructors`; `personalTrainingTypeId`
`PersonalTrainings/PersonalTrainingsTypes`; `clubId` → the club list. Note the
booking's own `name` does **not** match the type's `name`.
**Delta sync:** like other catalogue endpoints, passing the largest `timestamp`
seen in a previous response (instead of `0`) returns only rows changed since —
`data: []` when nothing changed. A simple poller can ignore this and always send
`timestamp=0` to get the full list each time.
### Membership contract
`GET /v1/RemoteAccounts/Contracts?timestamp=0`
@@ -415,7 +434,10 @@ classes. All empty in capture.
### Instructors
`GET /v1/Instructors/Instructors?timestamp=0` — instructor directory:
`GET /v1/Instructors/Instructors?timestamp=0` — instructor directory. Large list
(hundreds of rows), mostly `isActive: false` and/or `isDeleted: true` legacy
staff; `position` is a department label (`Swim`, `Tennis`, `Sales`, …). Only
needed to resolve a booking's `instructorId` to a name.
```json
{
@@ -428,7 +450,8 @@ classes. All empty in capture.
"photoUrl": null,
"description": null,
"companyId": 251,
"id": 0
"id": 0,
"isDeleted": false
}
```
@@ -437,8 +460,22 @@ classes. All empty in capture.
capture).
`GET /v1/PersonalTrainings/PersonalTrainingsTypes?timestamp=0` — PT session-type
catalogue (`name`, `duration`, `productId`). `personalTrainingTypeId` on a PT
booking points here.
catalogue; `personalTrainingTypeId` on a PT booking points here. This is where the
session `duration` lives (the booking itself carries only start/end). `name`
ranges over paid sessions (`"60 min PT session"`, `"45 min PT session FREE"`),
consultations/reviews, and non-session blocks (`"Lunch 30 min"`, `"Shower 15
min"`). Many rows are `isDeleted: true` legacy types.
```json
{
"name": "60 min PT session",
"duration": "01:00",
"productId": 105674,
"companyId": 251,
"id": 0,
"isDeleted": false
}
```
### Products & pricing