T30+T31: add dapp is_new_version bool field, default false (V21, §I.getDapps)

This commit is contained in:
proitlab committed 2026-08-19 18:48:49 +07:00
1 parent 01ee7ec733
commit 7ae6b8f4f8
5 files changed
+15 -1

No files matched your search

+5 -1
View File
@@ -15,6 +15,7 @@ FastAPI + SQLite server mirroring idrs-api live endpoints — `GET /api/getToken
- version rule: `acceptableVersion` ≤ `latestVersion` ! enforced (else reject)
- `is_live` admin-owned by default; optional verifier gated by env `IDRS_VERIFY_TOKENS` (default off): when enabled, token live iff `iconUrl` reachable — local URL (host = `IDRS_PUBLIC_URL`) → file exists under `icons/token/`; external URL → HTTP HEAD/GET 2xx (~3s timeout, follow redirects); empty iconUrl → not live; flips after 2 consecutive failed passes, auto-recovers; interval env `IDRS_VERIFY_INTERVAL` (default 600s); external hosts must be reachable from the server. Same rules for dapp `link` reachability, **enabled by default** (disable via `IDRS_VERIFY_DAPPS=0/false/off`): strict 2xx, whitespace-stripped, empty → not live
- dapps: `Dapp` table seeded once from repo-root `dapps.json` (empty DB only); at seed, mirror each dapp `image` to `icons/dapps/` + rewrite `image` = `{IDRS_PUBLIC_URL}/img/dapps/<file>`; download gate: accept `image/*` only (sniff magic for `octet-stream`; `text/html`/other → skip), filename = sanitized URL basename stem + content-type ext, collision → `-N` suffix; download/validation failure → keep original URL, continue (seed never hard-fails); admin panel can upload a new image (any `image/*`, ≤2MB) → rewrites URL, or edit URL directly
- dapp `is_new_version`: non-nullable Boolean, default false; admin-editable (form + details); surfaced in `/api/getDapps` as `is_new_version:bool`; existing DBs migrated additively via startup `ALTER TABLE dapps ADD COLUMN is_new_version BOOLEAN NOT NULL DEFAULT 0` guarded by column-existence check (⊥ data loss; all existing rows → false); ⊥ reseed
- dapp counter: public POST endpoint increments `counter` by 1; **auth-gated** by shared secret env `IDRS_SECRET` sent as `X-Api-Key` header (constant-time compare; unset → fail-closed 401); `id` query param; atomic `UPDATE ... SET counter=counter+1`; unknown id → 404
- 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; startup runs idempotent additive `ALTER TABLE dapps ADD COLUMN is_live_override BOOLEAN NOT NULL DEFAULT 0` (guarded by `PRAGMA table_info`) — columns added, no data rewritten; ⊥ reseed if DB non-empty
@@ -24,7 +25,7 @@ FastAPI + SQLite server mirroring idrs-api live endpoints — `GET /api/getToken
## §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 `/api/getDapps` → 200 `{dappList:[{id:int, name, description, link, image, is_live:bool, counter:int, category_id:int|null, is_available_indonesia:bool|null, created_at, updated_at}]}` (id ASC)
api: GET `/api/getDapps` → 200 `{dappList:[{id:int, name, description, link, image, is_live:bool, counter:int, category_id:int|null, is_available_indonesia:bool|null, is_new_version:bool, created_at, updated_at}]}` (id ASC)
api: POST `/api/increaseDappCounter?id=<int>` header `X-Api-Key: <IDRS_SECRET>` → 200 `{id:int, counter:int}` | 401 bad/missing secret (or secret unset) | 404 unknown id
api: GET `/img/dapps/<filename>` + `/public/img/dapps/<filename>` → 200 image (mime from file) | 404 unknown (traversal-guarded)
api: GET `/img/<filename>` → 200 image/png | 404 unknown
@@ -56,6 +57,7 @@ V17: `is_live` admin-owned by default (editable in form, writes honored). Option
V18: `Dapp` seeded from `dapps.json` (empty DB only); seed-mirrored image URLs = `{IDRS_PUBLIC_URL}/img/dapps/<file>`; download gate accepts `image/*` only (sniff `octet-stream`; `text/html` → keep original URL); admin-uploaded image = any `image/*` ≤2MB (magic-sniffed), rewrites URL; `id` never writable from form (⊥ injectable); `created_at` set on create, `updated_at` on every change
V19: dapp-link verifier — **enabled by default**; disabled when `IDRS_VERIFY_DAPPS` is `0/false/off`: dapp live iff `link` reachable (whitespace-stripped; empty → false; HTTP HEAD/GET **strict 2xx**, `Accept: text/html`, 3s timeout, follow redirects); flips after 2 consecutive failed passes, recovers next success; **admin-set `is_live=false` is sticky: dapp sets `is_live_override=True`, and the verifier never flips an overridden dapp (in either direction)**
V20: dapp counter increment — POST only, `id` required, secret-gated (`IDRS_SECRET`, `X-Api-Key` header, constant-time compare; unset → always 401); `+1` atomic update; returns new counter; unknown id → 404; ⊥ decrement/reset via this endpoint
V21: `Dapp.is_new_version` — non-nullable Boolean, default false; surfaced as `is_new_version:bool` in `/api/getDapps`; admin-editable (form + `column_details_list`); existing DBs auto-migrated via additive startup `ALTER TABLE dapps ADD COLUMN is_new_version BOOLEAN NOT NULL DEFAULT 0` guarded by column-existence check (mirrors `is_live_override`; all pre-existing rows → false); ⊥ reseed
## §T — Tasks
id|status|task|cites
@@ -88,6 +90,8 @@ T26|x|`Dapp` table + seed from `dapps.json` (mirror images to `icons/dapps/`, re
T27|x|dapp-link verifier (`DappVerifier` in `app/verify.py`): strict 2xx on `link`, whitespace-stripped, 2-pass grace + recover, **enabled by default** (`IDRS_VERIFY_DAPPS=0/false/off` disables; admin-owned then), skips dapps with `is_live_override=True` (sticky admin-off); tokens opt-in via `IDRS_VERIFY_TOKENS`; verifier refactored to base class + `TokenVerifier`/`DappVerifier`; env `IDRS_VERIFY_ENABLED` renamed → `IDRS_VERIFY_TOKENS`|V17,V19
T28|x|`is_live_override` column on `Dapp` (additive startup ALTER TABLE); `DappAdmin.on_model_change` sets it `True` on admin-set `false` (create, or edit only when `is_live` changes), never injected; verifier skips overridden dapps|V19
T29|x|`POST /api/increaseDappCounter?id=<int>` (header `X-Api-Key` = `IDRS_SECRET`, fail-closed, constant-time; atomic `counter+1`; 200/401/404); `db.increment_dapp_counter` with SQLite `RETURNING`|V20
T30|x|add `is_new_version` Boolean column to `Dapp` model (default False, nullable=False) + additive startup `ALTER TABLE dapps ADD COLUMN is_new_version BOOLEAN NOT NULL DEFAULT 0` guarded by column-existence check in `db._ensure_migrations` (mirrors `is_live_override` handling)|V21,§C
T31|x|expose `is_new_version:bool` in `/api/getDapps` (`main.py` `get_dapps` returns `d.is_new_version`) + `DappForm` `BooleanField` + `DappAdmin` `column_list`/`column_details_list`; seeded rows default false via DB default (dapps.json has no such key)|V21,§I
## §B — Bug log
id|date|cause|fix
+3
View File
@@ -181,6 +181,7 @@ class DappForm(Form):
choices=[("", "—"), ("true", "Yes"), ("false", "No")],
default="",
)
is_new_version = BooleanField("New Version")
class DappAdmin(ModelView, model=Dapp):
@@ -195,6 +196,7 @@ class DappAdmin(ModelView, model=Dapp):
"counter",
"category_id",
"is_available_indonesia",
"is_new_version",
]
column_labels = {
"is_live": "Live",
@@ -211,6 +213,7 @@ class DappAdmin(ModelView, model=Dapp):
"counter",
"category_id",
"is_available_indonesia",
"is_new_version",
"created_at",
"updated_at",
]
+5
View File
@@ -60,6 +60,11 @@ def _ensure_migrations(engine):
with engine.begin() as conn:
conn.execute(text("ALTER TABLE dapps ADD COLUMN is_live_override BOOLEAN NOT NULL DEFAULT 0"))
if "is_new_version" not in cols:
from sqlalchemy import text
with engine.begin() as conn:
conn.execute(text("ALTER TABLE dapps ADD COLUMN is_new_version BOOLEAN NOT NULL DEFAULT 0"))
def init_db():
+1
View File
@@ -108,6 +108,7 @@ def get_dapps():
"counter": d.counter,
"category_id": d.category_id,
"is_available_indonesia": d.is_available_indonesia,
"is_new_version": bool(d.is_new_version),
"created_at": d.created_at,
"updated_at": d.updated_at,
}
+1
View File
@@ -64,6 +64,7 @@ class Dapp(Base):
counter = Column("counter", Integer, nullable=False, default=0)
category_id = Column("category_id", Integer, nullable=True)
is_available_indonesia = Column("is_available_indonesia", Boolean, nullable=True)
is_new_version = Column("is_new_version", Boolean, nullable=False, default=False)
created_at = Column("created_at", String, nullable=True)
updated_at = Column("updated_at", String, nullable=True)
is_live_override = Column("is_live_override", Boolean, nullable=False, default=False)