77 lines
7.7 KiB
Markdown
77 lines
7.7 KiB
Markdown
# SPEC — IDRS API server (Python FastAPI)
|
|
|
|
## §G — Goal
|
|
FastAPI + SQLite server mirroring idrs-api live endpoints — `GET /api/getTokenInfo` + `GET /api/version/android` + icon files. Admin panel (sqladmin, login-protected) to enter/edit data → served as JSON API. Seed from existing `tokenList.json` + `versionAndroid.json` on first run.
|
|
|
|
## §C — Constraints
|
|
- Python 3, FastAPI + SQLAlchemy + sqlite (WAL) + `sqladmin` admin panel (Bootstrap UI, built-in login) — venv `./venv`, deps: `fastapi`, `uvicorn`, `sqlalchemy`, `sqladmin`, `jinja2`, `python-multipart` (upload); install in venv, ⊥ global
|
|
- endpoints match live paths verbatim: `/api/getTokenInfo`, `/api/version/android`, `/img/<filename>`; admin panel at `/admin` (login-gated)
|
|
- admin CRUD via `sqladmin` ModelAdmin: token (10 fields: idfToken, position, typeBlockchain, name, symbol, contractAddr, decimals, iconUrl, filename, is_live) + version record (idVersion, appName, acceptableVersion, latestVersion); ⊖ hand-rolled admin.html + old CRUD routes
|
|
- auth: single admin via env `ADMIN_USERNAME` + `ADMIN_PASSWORD`; session-signing secret auto-generated on first run + persisted locally (survives restart); creds unset → fail closed (∀ login refused + warning logged, ⊥ default/empty password)
|
|
- icon upload PNG → stored under `token/` subdir → served `/img/token/<filename>`; upload sets `iconUrl` = `{IDRS_PUBLIC_URL}/img/token/<filename>`; `iconUrl` otherwise free-form (external URL allowed)
|
|
- cleaned types OK: `decimals` int, `is_live` bool — byte drift from live API (string-typed there) accepted; shape + field names kept verbatim
|
|
- seed once on first run (empty DB) from repo-root `tokenList.json` (23 tokens) + `versionAndroid.json`; ⊥ reseed if DB non-empty
|
|
- `idfToken` unique; response ordered `position` ASC
|
|
- version rule: `acceptableVersion` ≤ `latestVersion` ! enforced (else reject)
|
|
- out of scope: multi-user table, roles, 2FA, password reset, on-chain calls, audit trail, change history
|
|
- data survival: existing `idrs.db` file + schema kept (migration-free); SQLAlchemy reads the same file
|
|
- run: `./venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000` + `run.sh` wrapper
|
|
|
|
## §I — Interfaces
|
|
api: GET `/api/getTokenInfo` → 200 `{tokenList:[{idfToken:int, position:int, typeBlockchain:string, name:string, symbol:string, contractAddr:string, decimals:int, iconUrl:string, filename:string, is_live:bool}]}` (position ASC)
|
|
api: GET `/api/version/android` → 200 `{idVersion:int, appName:string, acceptableVersion:int, latestVersion:int}`
|
|
api: GET `/img/<filename>` → 200 image/png | 404 unknown
|
|
web: GET `/admin` → sqladmin panel (login-gated; login page when unauthenticated)
|
|
web: GET `/admin/login` → login form; POST → session cookie on success
|
|
web: GET `/admin/logout` → end session
|
|
web: POST `/admin/token/<id>/icon` → upload PNG (multipart) → sets iconUrl `/img/<filename>` (custom panel view/action)
|
|
env: `ADMIN_USERNAME`, `ADMIN_PASSWORD` (login); `IDRS_SESSION_SECRET` (session signing, auto-gen + persist on first run)
|
|
file: `tokenList.json`, `versionAndroid.json` → seed source (repo root); read-only after seed
|
|
|
|
## §V — Invariants
|
|
V1: ∀ token → `idfToken` unique; `position` unique; response ordered `position` ASC
|
|
V2: response shape fixed: `getTokenInfo` = `{tokenList:[...]}`, `version/android` = flat object; field names verbatim from live API
|
|
V3: `decimals` int, `is_live` bool in DB + serialized (cleaned types; drift from live string-typing accepted)
|
|
V4: `acceptableVersion` ≤ `latestVersion`; update violating rule → reject
|
|
V5: seed runs once: empty DB → import `tokenList.json` + `versionAndroid.json`; DB non-empty → ⊥ reseed
|
|
V6: ∀ upload → PNG, filename sanitized (basename, safe chars), written under `token/` subdir of icons dir; duplicate filename → overwrite (idempotent)
|
|
V7: ∀ token → `iconUrl` = `{IDRS_PUBLIC_URL}/img/token/<filename>` after upload; token without upload → keep entered URL (external ok)
|
|
V8: token delete → row removed; response excludes it on next GET
|
|
V9: ∀ admin view → auth check before render; unauthenticated → redirect `/admin/login`; wrong creds → reject; logout ends session
|
|
V10: `ADMIN_USERNAME`/`ADMIN_PASSWORD` unset → ∀ login refused + warning logged; ⊥ default/empty credential
|
|
V11: public API endpoints (`/api/getTokenInfo`, `/api/version/android`, `/img/...`) ⊥ auth; response shapes unchanged (V2,V3)
|
|
V12: same `idrs.db` file + schema used by raw-sqlite AND SQLAlchemy (no migration); empty DB → seed still runs
|
|
V13: version table single row (`id=1`) maintained — panel ⊥ creates duplicates
|
|
V14: ∀ upload → PNG verified by magic bytes `\x89PNG\r\n\x1a\n` (⊥ trust `content-type` header), extension forced `.png`, size ≤ 512KB; `/img/<filename>` served with `image/png` (⊥ `guess_type`)
|
|
V15: token edit/create form upload → `on_model_change` sets `filename` + `iconUrl={IDRS_PUBLIC_URL}/img/token/<filename>` (base env var, default `https://idrs.databisnis.id`, trailing `/` stripped) before persist; no new file → keep existing (⊥ clear)
|
|
V16: `idfToken` auto-increment only — not in form, never written from form data (⊥ injectable); `typeBlockchain` fixed `"Vexanium"` — not in form, always forced on write (⊥ injectable); `position` editable, default = next idfToken value
|
|
|
|
## §T — Tasks
|
|
id|status|task|cites
|
|
T1|x|venv + deps (`fastapi`, `uvicorn`, `jinja2`, `python-multipart`)|§C
|
|
T2|x|sqlite schema + WAL + seed from `tokenList.json`/`versionAndroid.json`|V5
|
|
T3|x|GET `/api/getTokenInfo` (position ASC, cleaned types)|V1,V2,V3
|
|
T4|x|GET `/api/version/android`|V2,V3
|
|
T5|x|icon upload + `/img/<filename>` serving|V6,V7
|
|
T6|x|`/admin` token list + CRUD|V1,V3,V8
|
|
T7|x|`/admin` version edit + rule check|V4
|
|
T8|x|`run.sh` + README quickstart (seed, run, admin)|§C
|
|
T9|x|deps `sqlalchemy` + `sqladmin`; SQLAlchemy models over existing `idrs.db`|V12
|
|
T10|x|auth backend (env creds, fail-closed) + persisted session secret|V9,V10
|
|
T11|x|ModelAdmin Token + Version (single-row guard)|V1,V4,V13
|
|
T12|x|port seed to SQLAlchemy; public endpoints keep shapes on same DB|V11,V12
|
|
T13|x|icon upload in panel (custom view/action); drop admin.html + old CRUD routes|V6,V7
|
|
T14|x|`run.sh`/`.env` + README update (creds, secret, login)|§C,V10
|
|
T15|x|custom WTForms form on TokenAdmin (FileField `icon`) + `on_model_change` file handling|V15
|
|
T16|x|remove IconUploadView BaseView + upload.html + `/admin/upload`|V9
|
|
T17|x|PNG magic-byte + extension + size validation; force `image/png` on `/img`|V14
|
|
T18|x|`IDRS_PUBLIC_URL` env base for upload iconUrl (default domain); one-time backfill of relative `/img/...` iconUrl|V15
|
|
T19|x|form: hide `idfToken` (auto) + `typeBlockchain` (fixed Vexanium); `position` default = next id, editable; empty position → auto-default|V16
|
|
T20|x|Dockerfile (python:3.12-slim, uvicorn single worker, non-root) + .dockerignore; DB (`/app/data`) and icons (`/app/app/static/icons`) volumes persist|§C
|
|
T21|x|`tokenList.json`: replace seed iconUrl domain `idrs.kriptoteknologi.io` → `idrs.databisnis.id` (14 rows; existing DBs need manual update)|V5
|
|
T22|x|icon upload → `token/` subdir + `/img/token/<filename>` route (flat route removed); migrate `ayam-logo.png` into `token/`, backfill token1 iconUrl|V15
|
|
|
|
## §B — Bug log
|
|
id|date|cause|fix
|
|
B1|2026-08-16|seed `tokenList.json`: token CHIP `"filename":null` → `t.get("filename","")` returns None (key exists) → NOT NULL constraint failed|seed null-coalesces `t.get("filename") or ""`, `t.get("iconUrl") or ""` (V5)
|
|
B2|2026-08-16|icon upload checks only client `content-type` header, saves any filename, `/img` serves mime via `guess_type` → uploaded `x.html` served as `text/html` on API origin (stored XSS)|V14: magic-bytes PNG check + `.png` extension + size cap + forced `image/png` content-type on `/img` |