add icon token subdir, Docker, SPEC/AGENTS, initial commit

This commit is contained in:
proitlab committed 2026-08-16 20:13:42 +07:00
commit 73eeab7f63
17 files changed
+622

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
venv/
__pycache__/
*.pyc
.env
.session_secret
idrs.db
idrs.db-wal
idrs.db-shm
app/static/icons/
.git/
README.md
SPEC.md
+5
View File
@@ -0,0 +1,5 @@
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me
# IDRS_SESSION_SECRET=auto-generated on first run (stored in .session_secret)
# IDRS_DB_PATH=
# IDRS_PUBLIC_URL=https://idrs.databisnis.id (base URL for uploaded icon URLs)
+9
View File
@@ -0,0 +1,9 @@
venv/
__pycache__/
*.pyc
idrs.db
idrs.db-wal
idrs.db-shm
.session_secret
.env
app/static/icons/*
+39
View File
@@ -0,0 +1,39 @@
# AGENTS.md — IDRS API server
Project conventions and operational gotchas for agents working in this repo.
## Layout
- `app/main.py` — FastAPI app: public endpoints `GET /api/getTokenInfo`, `GET /api/version/android`, `GET /img/token/<filename>`; mounts `/static`; startup calls `db.init_db()`.
- `app/admin.py` — sqladmin panel: `TokenAdmin` (custom WTForms form, `on_model_change`), `VersionAdmin`, `create_admin(app)`. Upload icons saved under `app/static/icons/token/` with `iconUrl = {IDRS_PUBLIC_URL}/img/token/<filename>`.
- `app/models.py` — SQLAlchemy `Token`/`Version`, engine, `SessionLocal`, `ICON_DIR`, `DB_PATH` (env `IDRS_DB_PATH`).
- `app/db.py` — seed from `tokenList.json`/`versionAndroid.json` (once, empty DB only).
- `app/auth.py` — `AdminAuthBackend`, fail-closed creds.
- `tokenList.json` / `versionAndroid.json` — seed source (read-only after seed).
## Environment / secrets
- `.env` (gitignored) → `ADMIN_USERNAME`, `ADMIN_PASSWORD`, `IDRS_PUBLIC_URL` (default `https://idrs.databisnis.id`). Test creds `admin`/`testpass123` — change before exposure.
- `.session_secret` (gitignored, auto-generated 0600) if `IDRS_SESSION_SECRET` unset.
- NEVER bake `.env` into the Docker image; pass via `--env-file` / `-e`.
- Untracked, never commit: `.env`, `.session_secret`, `idrs.db*`, `venv/`, `app/static/icons/*`.
## Run (local)
- `./run.sh` loads `.env` then `uvicorn app.main:app --host 127.0.0.1 --port 8000`.
- Start detached: `setsid nohup ./run.sh < /dev/null > /tmp/opencode/idrs.log 2>&1 & disown` (plain `&` hangs).
- Sanity-import: `./venv/bin/python -c "from app import main; print('IMPORT_OK')"`.
## Docker
- `docker build -t idrs-api .`
- Run with volumes: `-v idrs-data:/app/data -v idrs-icons:/app/app/static/icons`, env via `--env-file .env`.
- MUST use a single uvicorn worker (in-memory starlette sessions → `--workers >1` breaks login).
- DB + uploaded icons persist in the volumes across container recreate; code changes need an image rebuild.
## Verification
- curl login: `POST /admin/login` with `-c/-b` cookie jar; then curl admin endpoints / uploads with the jar.
- Assert `/img/token/<f>` returns `image/png`; HTML/oversize uploads rejected (400).
## Operation gotchas
- NEVER `pkill -f uvicorn` broadly — a separate user process runs uvicorn on port 7000. Kill only the port-8000 PID (`ps -ef | grep "uvicorn app.main:app --host 127.0.0.1 --port 8000"`).
- Local server binds 127.0.0.1:8000. Host ports 8080/8100 are taken by other services.
## Spec
- Canonical invariants and task status live in `SPEC.md` (§V invariants, §T tasks, §B bug log). Keep it updated when behavior changes; check §V before modifying.
+23
View File
@@ -0,0 +1,23 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
IDRS_DB_PATH=/app/data/idrs.db
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
COPY tokenList.json versionAndroid.json ./
RUN mkdir -p /app/data /app/app/static/icons \
&& useradd --system --uid 1000 --create-home idrs \
&& chown -R idrs:idrs /app
USER idrs
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
+31
View File
@@ -0,0 +1,31 @@
# IDRS API
Python (FastAPI + SQLite) server that mirrors the live idrs-api endpoints, with a login-protected admin panel to manage the data.
- `GET /api/getTokenInfo` → token list (position ASC, `decimals` int, `is_live` bool)
- `GET /api/version/android` → version record
- `GET /img/<filename>` → uploaded token icons
- `/admin` → sqladmin panel (login-protected): manage tokens, version, upload icons
## Setup
```
python3.12 -m venv venv
./venv/bin/pip install -r requirements.txt
cp .env.example .env # set ADMIN_USERNAME / ADMIN_PASSWORD
```
## Run
```
./run.sh # http://127.0.0.1:8000 (admin at /admin)
```
`run.sh` loads `.env`. First run seeds the DB from `tokenList.json` and `versionAndroid.json` (idrs.db is created here; override path via `IDRS_DB_PATH`).
## Notes
- **Auth:** single admin, credentials from `ADMIN_USERNAME`/`ADMIN_PASSWORD` env. If they're unset, login is refused and a warning is logged (fail closed). Session secret is auto-generated on first run and stored in `.session_secret`. The public API endpoints stay unauthenticated.
- **Version rule:** `acceptableVersion` ≤ `latestVersion` is enforced in the panel; below `acceptableVersion` the Android app force-updates, below `latestVersion` it prompts.
- **Icons:** upload PNGs in the admin panel (Icons page); `iconUrl` auto-points to `/img/<filename>`.
- No on-chain integration, no audit trail — data is entered manually.
+77
View File
@@ -0,0 +1,77 @@
# 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`
View File
Whitespace-only changes.
+170
View File
@@ -0,0 +1,170 @@
import os
from pathlib import Path
from sqladmin import Admin, ModelView
from starlette.datastructures import UploadFile
from starlette.requests import Request
from wtforms import BooleanField, FileField, Form, IntegerField, StringField
from wtforms.validators import DataRequired
from sqlalchemy import text
from app.auth import AdminAuthBackend, get_session_secret
from app.models import ICON_DIR, SessionLocal, Token, Version
BASE_DIR = Path(__file__).resolve().parent.parent
TEMPLATES_DIR = str(BASE_DIR / "app" / "templates")
MAX_ICON_BYTES = 512 * 1024
PNG_MAGIC = b"\x89PNG\r\n\x1a\n"
PUBLIC_BASE_URL = os.environ.get("IDRS_PUBLIC_URL", "https://idrs.databisnis.id").rstrip("/")
def _is_authenticated(request: Request) -> bool:
return bool(request.session.get("auth"))
def _sanitize_png_name(name: str) -> str:
base = Path(name).name
if base in ("", ".", ".."):
return "icon.png"
if not base.lower().endswith(".png"):
base = f"{Path(base).stem or 'icon'}.png"
return base
async def _extract_icon(raw) -> bytes:
if isinstance(raw, UploadFile):
return await raw.read(MAX_ICON_BYTES + 1)
stream = raw.stream if hasattr(raw, "stream") else raw
data = stream.read(MAX_ICON_BYTES + 1)
return data if isinstance(data, bytes) else bytes(data)
def _next_token_id() -> int:
with SessionLocal() as session:
return int(session.execute(text("SELECT COALESCE(MAX(idfToken), 0) + 1 FROM tokens")).scalar())
class NullableIntegerField(IntegerField):
def process_formdata(self, valuelist):
if valuelist and valuelist[0].strip() == "":
self.data = None
return
super().process_formdata(valuelist)
class TokenForm(Form):
position = NullableIntegerField("Position", default=_next_token_id)
name = StringField("Name", validators=[DataRequired()])
symbol = StringField("Symbol", validators=[DataRequired()])
contractAddr = StringField("Contract", validators=[DataRequired()])
decimals = IntegerField("Decimals", validators=[DataRequired()])
iconUrl = StringField("Icon URL")
icon = FileField("Icon PNG")
is_live = BooleanField("Live")
class TokenAdmin(ModelView, model=Token):
name = "Tokens"
icon = "fa-solid fa-coins"
form = TokenForm
column_list = [
"position",
"name",
"symbol",
"typeBlockchain",
"contractAddr",
"decimals",
"is_live",
"iconUrl",
]
column_labels = {
"idfToken": "ID",
"position": "Position",
"typeBlockchain": "Blockchain",
"contractAddr": "Contract",
"decimals": "Decimals",
"is_live": "Live",
}
column_details_list = [
"idfToken",
"position",
"typeBlockchain",
"name",
"symbol",
"contractAddr",
"decimals",
"iconUrl",
"filename",
"is_live",
]
column_searchable_list = ["name", "symbol", "contractAddr"]
column_default_sort = [("position", True)]
page_size = 25
def is_accessible(self, request: Request) -> bool:
return _is_authenticated(request)
async def on_model_change(self, data, model, is_created, request):
data.pop("idfToken", None)
data["typeBlockchain"] = "Vexanium"
if data.get("position") in (None, ""):
data["position"] = _next_token_id()
raw = data.get("icon")
data.pop("icon", None)
if not raw:
return
content = await _extract_icon(raw)
if not content:
return
if len(content) > MAX_ICON_BYTES:
raise Exception(f"Icon must be PNG ≤ {MAX_ICON_BYTES // 1024}KB")
if not content.startswith(PNG_MAGIC):
raise Exception("Icon must be a real PNG file")
filename = _sanitize_png_name(getattr(raw, "filename", "") or "icon.png")
token_dir = ICON_DIR / "token"
token_dir.mkdir(parents=True, exist_ok=True)
(token_dir / filename).write_bytes(content)
data["filename"] = filename
data["iconUrl"] = f"{PUBLIC_BASE_URL}/img/token/{filename}"
class VersionAdmin(ModelView, model=Version):
name = "Android Version"
icon = "fa-solid fa-mobile-screen"
can_create = False
can_delete = False
column_list = ["idVersion", "appName", "acceptableVersion", "latestVersion"]
column_labels = {
"idVersion": "ID Version",
"appName": "App",
"acceptableVersion": "Acceptable",
"latestVersion": "Latest",
}
column_details_list = ["idVersion", "appName", "acceptableVersion", "latestVersion"]
form_columns = ["idVersion", "appName", "acceptableVersion", "latestVersion"]
def is_accessible(self, request: Request) -> bool:
return _is_authenticated(request)
async def on_model_change(self, data, model, is_created, request):
acceptable = int(data.get("acceptableVersion", model.acceptableVersion))
latest = int(data.get("latestVersion", model.latestVersion))
if acceptable > latest:
raise Exception("acceptableVersion must be <= latestVersion")
def create_admin(app):
auth_backend = AdminAuthBackend(get_session_secret())
admin = Admin(
app,
session_maker=SessionLocal,
title="IDRS Admin",
base_url="/admin",
templates_dir=TEMPLATES_DIR,
authentication_backend=auth_backend,
)
admin.add_view(TokenAdmin)
admin.add_view(VersionAdmin)
return admin
+57
View File
@@ -0,0 +1,57 @@
import logging
import os
import secrets
from pathlib import Path
from sqladmin.authentication import AuthenticationBackend
from starlette.requests import Request
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_FILE = BASE_DIR / ".session_secret"
logger = logging.getLogger("idrs.auth")
def get_admin_credentials():
user = os.environ.get("ADMIN_USERNAME", "")
password = os.environ.get("ADMIN_PASSWORD", "")
return user, password
def get_session_secret():
secret = os.environ.get("IDRS_SESSION_SECRET", "")
if not secret:
if SECRET_FILE.exists():
secret = SECRET_FILE.read_text(encoding="utf-8").strip()
else:
secret = secrets.token_urlsafe(48)
SECRET_FILE.write_text(secret, encoding="utf-8")
os.chmod(SECRET_FILE, 0o600)
return secret
def credentials_configured() -> bool:
user, password = get_admin_credentials()
return bool(user and password)
class AdminAuthBackend(AuthenticationBackend):
async def authenticate(self, request: Request) -> bool:
return bool(request.session.get("auth"))
async def login(self, request: Request) -> bool:
user, password = get_admin_credentials()
if not (user and password):
logger.warning("ADMIN_USERNAME/ADMIN_PASSWORD not set — login refused (fail closed)")
return False
form = await request.form()
if secrets.compare_digest(form.get("username", ""), user) and secrets.compare_digest(
form.get("password", ""), password
):
request.session["auth"] = True
return True
return False
async def logout(self, request: Request) -> bool:
request.session.clear()
return True
+71
View File
@@ -0,0 +1,71 @@
import json
from pathlib import Path
from app.models import (
SessionLocal,
Token,
Version,
Base,
engine,
)
BASE_DIR = Path(__file__).resolve().parent.parent
SEED_TOKEN = BASE_DIR / "tokenList.json"
SEED_VERSION = BASE_DIR / "versionAndroid.json"
def init_db():
Base.metadata.create_all(engine)
seed()
def seed():
session = SessionLocal()
try:
if session.query(Token).count() == 0 and SEED_TOKEN.exists():
data = json.loads(SEED_TOKEN.read_text(encoding="utf-8"))
for t in data["tokenList"]:
session.add(
Token(
idfToken=int(t["idfToken"]),
position=int(t["position"]),
typeBlockchain=t["typeBlockchain"],
name=t["name"],
symbol=t["symbol"],
contractAddr=t["contractAddr"],
decimals=int(t["decimals"]),
iconUrl=t.get("iconUrl") or "",
filename=t.get("filename") or "",
is_live=t.get("is_live") in (1, "1", "true", True),
)
)
if session.query(Version).count() == 0 and SEED_VERSION.exists():
v = json.loads(SEED_VERSION.read_text(encoding="utf-8"))
session.add(
Version(
id=1,
idVersion=int(v["idVersion"]),
appName=v["appName"],
acceptableVersion=int(v["acceptableVersion"]),
latestVersion=int(v["latestVersion"]),
)
)
session.commit()
finally:
session.close()
def list_tokens():
session = SessionLocal()
try:
return session.query(Token).order_by(Token.position.asc()).all()
finally:
session.close()
def get_version():
session = SessionLocal()
try:
return session.query(Version).filter(Version.id == 1).first()
finally:
session.close()
+62
View File
@@ -0,0 +1,62 @@
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from app import db
from app.admin import create_admin
BASE_DIR = Path(__file__).resolve().parent
ICON_DIR = BASE_DIR / "static" / "icons"
app = FastAPI(title="IDRS API")
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
create_admin(app)
@app.get("/api/getTokenInfo")
def get_token_info():
tokens = []
for t in db.list_tokens():
tokens.append(
{
"idfToken": t.idfToken,
"position": t.position,
"typeBlockchain": t.typeBlockchain,
"name": t.name,
"symbol": t.symbol,
"contractAddr": t.contractAddr,
"decimals": t.decimals,
"iconUrl": t.iconUrl,
"filename": t.filename,
"is_live": bool(t.is_live),
}
)
return {"tokenList": tokens}
@app.get("/api/version/android")
def get_version_android():
v = db.get_version()
return {
"idVersion": v.idVersion,
"appName": v.appName,
"acceptableVersion": v.acceptableVersion,
"latestVersion": v.latestVersion,
}
@app.get("/img/token/{filename}")
def get_icon(filename: str):
token_dir = ICON_DIR / "token"
path = (token_dir / filename).resolve()
if not path.is_file() or token_dir.resolve() not in path.parents:
raise HTTPException(status_code=404, detail="not found")
return FileResponse(path, media_type="image/png")
@app.on_event("startup")
def startup():
db.init_db()
+51
View File
@@ -0,0 +1,51 @@
import os
from pathlib import Path
from sqlalchemy import Boolean, Column, Integer, String, create_engine, event
from sqlalchemy.orm import declarative_base, sessionmaker
BASE_DIR = Path(__file__).resolve().parent.parent
DB_PATH = Path(os.environ.get("IDRS_DB_PATH", BASE_DIR / "idrs.db"))
ICON_DIR = BASE_DIR / "app" / "static" / "icons"
engine = create_engine(
f"sqlite:///{DB_PATH}",
connect_args={"check_same_thread": False},
pool_pre_ping=True,
)
@event.listens_for(engine, "connect")
def _set_wal(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA journal_mode=WAL")
cursor.close()
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
Base = declarative_base()
class Token(Base):
__tablename__ = "tokens"
idfToken = Column("idfToken", Integer, primary_key=True)
position = Column("position", Integer, unique=True, nullable=False)
typeBlockchain = Column("typeBlockchain", String, nullable=False)
name = Column("name", String, nullable=False)
symbol = Column("symbol", String, nullable=False)
contractAddr = Column("contractAddr", String, nullable=False)
decimals = Column("decimals", Integer, nullable=False)
iconUrl = Column("iconUrl", String, nullable=False, default="")
filename = Column("filename", String, nullable=False, default="")
is_live = Column("is_live", Boolean, nullable=False, default=True)
class Version(Base):
__tablename__ = "version"
id = Column(Integer, primary_key=True)
idVersion = Column("idVersion", Integer, nullable=False)
appName = Column("appName", String, nullable=False)
acceptableVersion = Column("acceptableVersion", Integer, nullable=False)
latestVersion = Column("latestVersion", Integer, nullable=False)
+7
View File
@@ -0,0 +1,7 @@
fastapi
uvicorn
sqlalchemy
sqladmin
jinja2
python-multipart
itsdangerous
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
cd "$(dirname "$0")"
if [ -f .env ]; then
set -a; . ./.env; set +a
fi
exec ./venv/bin/uvicorn app.main:app --host "${IDRS_HOST:-127.0.0.1}" --port "${IDRS_PORT:-8000}"
+1
View File
@@ -0,0 +1 @@
{"tokenList":[{"idfToken":1,"position":1,"typeBlockchain":"Vexanium","name":"IDRS","symbol":"IDRS","contractAddr":"theidrstoken","decimals":"4","iconUrl":"https://idrs.databisnis.id/public/img/token/idrs.png","filename":"idrs.png","is_live":"1"},{"idfToken":2,"position":2,"typeBlockchain":"Vexanium","name":"VEX","symbol":"VEX","contractAddr":"vex.token","decimals":"4","iconUrl":"https://idrs.databisnis.id/public/img/token/vex.png","filename":"vex.png","is_live":"1"},{"idfToken":3,"position":3,"typeBlockchain":"Vexanium","name":"VYN","symbol":"VYN","contractAddr":"vyndaoutoken","decimals":"4","iconUrl":"https://idrs.databisnis.id/public/img/token/vyn.png","filename":"vyn.png","is_live":"1"},{"idfToken":4,"position":4,"typeBlockchain":"Vexanium","name":"DJV","symbol":"DJV","contractAddr":"djvtokenvexa","decimals":"10","iconUrl":"https://idrs.databisnis.id/public/img/token/djv.png","filename":"djv.png","is_live":"1"},{"idfToken":5,"position":5,"typeBlockchain":"Vexanium","name":"Labirin","symbol":"LBN","contractAddr":"tokenlabirin","decimals":"4","iconUrl":"https://idrs.databisnis.id/public/img/token/lbn.png","filename":"lbn.png","is_live":"1"},{"idfToken":6,"position":6,"typeBlockchain":"Vexanium","name":"DICE","symbol":"DICE","contractAddr":"vexdicetoken","decimals":"4","iconUrl":"https://idrs.databisnis.id/public/img/token/dice.png","filename":"dice.png","is_live":"1"},{"idfToken":7,"position":7,"typeBlockchain":"Vexanium","name":"FLY","symbol":"FLY","contractAddr":"flydicetoken","decimals":"4","iconUrl":"https://idrs.databisnis.id/public/img/token/fly.png","filename":"fly.png","is_live":"1"},{"idfToken":8,"position":8,"typeBlockchain":"Vexanium","name":"IDRT","symbol":"IDRT","contractAddr":"idrtidrtidrt","decimals":"4","iconUrl":"https://idrs.databisnis.id/public/img/token/idrt.png","filename":"idrt.png","is_live":"1"},{"idfToken":9,"position":9,"typeBlockchain":"Vexanium","name":"BASO","symbol":"BASO","contractAddr":"vexbasotoken","decimals":"10","iconUrl":"https://idrs.databisnis.id/public/img/token/baso.png","filename":"baso.png","is_live":"1"},{"idfToken":10,"position":10,"typeBlockchain":"Vexanium","name":"BATIK","symbol":"BATIK","contractAddr":"batikutokens","decimals":"10","iconUrl":"https://idrs.databisnis.id/public/img/token/batik.png","filename":"batik.png","is_live":"1"},{"idfToken":11,"position":11,"typeBlockchain":"Vexanium","name":"KUJANG","symbol":"KUJANG","contractAddr":"kujangtokenn","decimals":"4","iconUrl":"https://idrs.databisnis.id/public/img/token/kujang.png","filename":"kujang.png","is_live":"1"},{"idfToken":12,"position":12,"typeBlockchain":"Vexanium","name":"BNKRI","symbol":"BNKRI","contractAddr":"bnkriiotoken","decimals":"10","iconUrl":"https://idrs.databisnis.id/public/img/token/bnkri.png","filename":"bnkri.png","is_live":"1"},{"idfToken":13,"position":13,"typeBlockchain":"Vexanium","name":"TRAFI","symbol":"TRAFI","contractAddr":"trafiiotoken","decimals":"10","iconUrl":"https://idrs.databisnis.id/public/img/token/trafi.png","filename":"trafi.png","is_live":"1"},{"idfToken":14,"position":14,"typeBlockchain":"Vexanium","name":"SATE","symbol":"SATE","contractAddr":"satevextoken","decimals":"8","iconUrl":"https://idrs.databisnis.id/public/img/token/sate.png","filename":"sate.png","is_live":"1"},{"idfToken":15,"position":15,"typeBlockchain":"Vexanium","name":"SALAD","symbol":"SALAD","contractAddr":"saladswaptkn","decimals":"8","iconUrl":"https://pbs.twimg.com/profile_images/1410076131105988610/R-qUSgDA.jpg","filename":"R-qUSgDA.jpg","is_live":"1"},{"idfToken":16,"position":16,"typeBlockchain":"Vexanium","name":"VOLKS","symbol":"VOLKS","contractAddr":"volksvolksus","decimals":"10","iconUrl":"https://i.ibb.co/S3cfHG4/volks.jpg","filename":"volks.png","is_live":"1"},{"idfToken":17,"position":17,"typeBlockchain":"Vexanium","name":"BTV","symbol":"BTV","contractAddr":"bitvexatoken","decimals":"8","iconUrl":"https://i.ibb.co/xfj6tLm/bitvexatoken.png","filename":"bitvexatoken.png","is_live":"1"},{"idfToken":18,"position":18,"typeBlockchain":"Vexanium","name":"VX Stable","symbol":"VX","contractAddr":"vexwrap.exy","decimals":"4","iconUrl":"https://i.ibb.co/V9qDf1R/vexwrap-exy.png","filename":"vexwrap-exy.png","is_live":"1"},{"idfToken":19,"position":19,"typeBlockchain":"Vexanium","name":"ECASH","symbol":"ECASH","contractAddr":"ecash.token","decimals":"8","iconUrl":"http://dapp.vexjabar.org/ecash.png","filename":"ecash.png","is_live":"1"},{"idfToken":20,"position":20,"typeBlockchain":"Vexanium","name":"CHIP","symbol":"CHIP","contractAddr":"bitchip","decimals":"2","iconUrl":"https://i.ibb.co/rxfKdR0/20221127-000316.png","filename":null,"is_live":"1"},{"idfToken":21,"position":21,"typeBlockchain":"Vexanium","name":"HASH","symbol":"HASH","contractAddr":"bithash","decimals":"2","iconUrl":"https://i.ibb.co/QbMzbwV/bithash.png","filename":"bithash.png","is_live":"1"},{"idfToken":22,"position":22,"typeBlockchain":"Vexanium","name":"MDR","symbol":"MDR","contractAddr":"maduratokens","decimals":"5","iconLine truncated
+1
View File
@@ -0,0 +1 @@
{"idVersion":1,"appName":"idrs","acceptableVersion":143,"latestVersion":143}