# CHANGELOG — TikTok Manager

> **Generated file.** Source of truth is `RULEFOLDER/changes/*.md` (one file per change).
> Rebuild with `python RULEFOLDER/tools/build_changelog.py`. Do not edit by hand.

6 changes. Newest last. IDs: legacy `CHG-NNN` (before 2026-09-24) and `CHG-YYYYMMDD-DEV-NN` (see `CONTRIBUTING.md`).

## Index

| # | ID | Date | Author | Type | Scope | Title | Security | API |
|---|----|------|--------|------|-------|-------|----------|-----|
| 1 | CHG-20261013-RG-01 | 2026-10-13 | RG | feature | backend, api, web-ui, infra | Initial application: TikTok accounts manager | designed against the sibling projects' findings registers — tokens Fernet-encrypted at rest with rotated refresh tokens persisted, constant-time API key compare, CSRF on every GUI mutation, POST-only state changes, explicit-account mutations, session-only login with no fallback, server-named uploads with finally cleanup, safe error messages everywhere (including the scheduler's recorded errors), startup guards; the remaining gaps are recorded as TT-S-01..09 in SECURITY.md §4 | GET /api/v1/health, GET /api/v1/accounts, GET /api/v1/accounts/{id}, POST /api/v1/accounts/{id}/refresh, DELETE /api/v1/accounts/{id}, GET /api/v1/videos, POST /api/v1/posts, POST /api/v1/posts/media, GET /api/v1/posts/{publish_id}/status, GET /api/v1/scheduled, POST /api/v1/scheduled, POST /api/v1/scheduled/media, PATCH /api/v1/scheduled/{id}, POST /api/v1/scheduled/{id}/retry, DELETE /api/v1/scheduled/{id}, GET /api/v1/insights/account, GET /api/v1/insights/videos |
| 2 | CHG-20261013-RG-02 | 2026-10-13 | RG | chore | project-wide, docs | Governance: rules folder, change fragments, hooks | none — documentation, tooling and git hooks only; no application code touched. SECURITY.md records this project's own findings register (TT-S-01..09) with the sibling projects' satisfied-at-birth lessons | — |
| 3 | CHG-20261014-RG-01 | 2026-10-14 | RG | bugfix | backend | Login: send PKCE code challenge (TikTok refused authorize without it) | improves — the authorization code is now bound to a per-flow PKCE verifier kept server-side in the session, so an intercepted code cannot be redeemed without it | n/a |
| 4 | CHG-20261014-RG-02 | 2026-10-14 | RG | bugfix | backend, web-ui, api, docs | Drafts: upload through the inbox endpoint with video.upload; drafts become the default publish path | none — same guards; a post mode is now refused unless the account granted its scope | POST /api/v1/posts, POST /api/v1/posts/media, POST /api/v1/scheduled, POST /api/v1/scheduled/media |
| 5 | CHG-20261014-RG-03 | 2026-10-14 | RG | infra | infra, docs | Port 9506, loopback-only binding, and the server deployment guide for ttok.thinkera.academy | improves — the port is published on 127.0.0.1 only (Apache is the single public entry), and the session cookie is Secure + SameSite=Lax in production | n/a |
| 6 | CHG-20261014-RG-04 | 2026-10-14 | RG | security | backend, web-ui, docs | Sign-in allowlist: only listed TikTok accounts can connect | improves — once the app is public, only TikTok accounts listed in TIKTOK_ALLOWED_ACCOUNTS can connect; production refuses to start without the list | n/a |

---

## CHG-20261013-RG-01 — Initial application: TikTok accounts manager
**Date:** 2026-10-13  
**Author:** RG  
**Type:** feature  
**Scope:** backend, api, web-ui, infra  
**Design Patterns:** Factory, Blueprint, Decorator, Facade, Adapter, DTO, Active Record, Template Method, Service Layer, Worker loop  
**Files:** `.env.example`, `.gitignore`, `Dockerfile`, `docker-compose.yml`, `entrypoint.sh`, `healthcheck.py`, `pytest.ini`, `requirements.txt`, `requirements-dev.txt`, `README.md`, `DOC/API.md`, `app/__init__.py`, `app/main.py`, `app/config.py`, `app/db_wait.py`, `app/models.py`, `app/auth.py`, `app/tiktok_api.py`, `app/scheduler.py`, `app/services/__init__.py`, `app/services/errors.py`, `app/services/uploads.py`, `app/services/posts.py`, `app/services/scheduled.py`, `app/routes/__init__.py`, `app/routes/auth_routes.py`, `app/routes/account_routes.py`, `app/routes/post_routes.py`, `app/routes/insights_routes.py`, `app/routes/api_routes.py`, `app/templates/base.html`, `app/templates/login.html`, `app/templates/csrf_error.html`, `app/templates/dashboard.html`, `app/templates/accounts.html`, `app/templates/posts.html`, `app/templates/post_new.html`, `app/templates/scheduled_posts.html`, `app/templates/insights.html`, `app/static/css/style.css`, `migrations/alembic.ini`, `migrations/env.py`, `migrations/script.py.mako`, `migrations/README`, `migrations/versions/0001_initial.py`, `tests/conftest.py`, `tests/test_config.py`, `tests/test_token_encryption.py`, `tests/services/test_posts.py`, `tests/services/test_uploads.py`, `tests/services/test_scheduled.py`, `tests/routes/test_api_accounts.py`, `tests/routes/test_api_posts.py`, `tests/routes/test_gui_posts.py`, `tests/routes/test_gui_csrf.py`  
**GUI:** GET /, GET /login, GET /auth/start, GET /auth/callback, POST /logout, GET /dashboard, GET /accounts, GET /accounts/add, POST /accounts/{id}/activate, POST /accounts/{id}/refresh, POST /accounts/{id}/delete, GET /posts, GET /posts/new, POST /posts/new, GET /posts/scheduled, POST /scheduled/{id}/reschedule, POST /scheduled/{id}/retry, POST /scheduled/{id}/delete, GET /insights  
**API:** GET /api/v1/health, GET /api/v1/accounts, GET /api/v1/accounts/{id}, POST /api/v1/accounts/{id}/refresh, DELETE /api/v1/accounts/{id}, GET /api/v1/videos, POST /api/v1/posts, POST /api/v1/posts/media, GET /api/v1/posts/{publish_id}/status, GET /api/v1/scheduled, POST /api/v1/scheduled, POST /api/v1/scheduled/media, PATCH /api/v1/scheduled/{id}, POST /api/v1/scheduled/{id}/retry, DELETE /api/v1/scheduled/{id}, GET /api/v1/insights/account, GET /api/v1/insights/videos  
**Security impact:** designed against the sibling projects' findings registers — tokens Fernet-encrypted at rest with rotated refresh tokens persisted, constant-time API key compare, CSRF on every GUI mutation, POST-only state changes, explicit-account mutations, session-only login with no fallback, server-named uploads with finally cleanup, safe error messages everywhere (including the scheduler's recorded errors), startup guards; the remaining gaps are recorded as TT-S-01..09 in SECURITY.md §4  

## What
The complete first version of TikTok Manager: a Dockerized Flask application that manages TikTok
accounts through the official APIs. A web GUI (dashboard, accounts, videos, new post, scheduled,
insights) and a REST API (`/api/v1`, 17 endpoints, JSON envelope, `X-API-Key`) — both over the
same use cases — let the owner connect TikTok accounts (TikTok Login with automatic 24-hour
token refresh and rotated-refresh-token persistence), publish videos (direct post or draft, file
upload in 32 MB chunks or public URL pull), **schedule posts for any future time** (app-side:
a dedicated `tiktok-scheduler` container publishes due posts, ADR-005), reschedule/retry/delete
scheduled posts, and follow up on performance (published videos with views/likes/comments/
shares, account totals, top videos). Documented end to end in `README.md` (with TikTok's honest
caveats) and `DOC/API.md`.

## Why
The publishing workflow for Thinkera's TikTok content was manual. The sibling YouTube Manager
and Facebook Manager already proved the pattern — one Flask app, GUI + API over shared use
cases, Docker deployment, the same governance — so the owner asked for the same capability for
TikTok ("بنفس الطريقة تمام").

## How
- **Same architecture as the siblings, with their lessons built in from day one** (see
  `## Security review`): app factory, one blueprint per route module, a Flask-free/DB-free
  facade (`tiktok_api.py`, plain `requests` against `open.tiktokapis.com`), a service layer
  (ADR-002) so GUI, API **and the scheduler** share one validation, Fernet-encrypted tokens in
  the models, Alembic migrations on both engines (ADR-004).
- **App-side scheduling (ADR-005).** TikTok's Content Posting API has no scheduled publish, so
  a `scheduled_posts` table (status machine pending → publishing → published/failed) holds the
  posts, videos wait in the persistent `MEDIA_FOLDER`, and a second compose service
  (`tiktok-scheduler`, same image, `python -m app.scheduler`, user 1000:1000, no ports) runs a
  plain loop over `services.scheduled.run_due_posts()`. Rejected: a thread inside gunicorn
  (2 workers ⇒ double posting); rejected: APScheduler/Celery (a plain loop needs no new
  dependency and the tick is idempotent).
- **Token lifecycle.** Access tokens live 24 h and TikTok rotates the refresh token on every
  refresh; `auth.ensure_fresh_token()` is the single refresh point (used by every adapter and
  by the scheduler before each publish) and persists the rotated tokens in the same transaction.
- **Publishing in one place.** `services.posts.publish_now()` owns the validation (caption ≤
  2,200 chars, privacy allow-list, direct/draft mode, video xor URL) and the temp-file
  lifecycle (`finally` cleanup); it uploads (init + chunked PUT) or pulls by URL, then polls
  the async processing status ~90 s for a verdict.
- **Naive-UTC timestamps.** `expires_at` / `scheduled_at` are stored naive UTC on both engines;
  user input is parsed by `parse_scheduled_time` (aware) and stripped at the storage boundary.
  This was forced by a real test failure (SQLite round-trips naive; mixing naive and aware
  raises TypeError) and is now a convention (`CODING-CONVENTIONS.md` §4.5).
- **Error safety at the edge.** The facade raises `TikTokAPIError` with fixed client-safe
  messages (token invalid, scope missing, unaudited-app privacy rule, rate limit/daily cap);
  the scheduler records the same safe string in `scheduled_posts.error` — the UI shows exactly
  what the API would return.

## API impact
All 17 endpoints are new, documented in `DOC/API.md` (§6–§16) and registered in
`FEATURE-REGISTRY.md`. Envelope: `{"success", "data", "error"}`. Errors: `UNAUTHORIZED`,
`API_NOT_CONFIGURED`, `NO_ACCOUNT`, `NOT_FOUND`, `EXPLICIT_ACCOUNT_REQUIRED`, `INVALID_INPUT`,
`NO_FILE`, `FORBIDDEN`, `RATE_LIMITED`, `UPSTREAM_ERROR` — all with fixed safe messages.
Mutations require an explicit account; reads may fall back to the first connected one.

## Security review
- Inputs validated (type/length/allow-list/path)?           yes — service boundary: caption ≤ 2,200 chars, privacy allow-list, mode allow-list, video xor URL, http(s) URL shape, future scheduled_at, video extension allow-list (mp4/mov/webm), uploads under server uuid names with realpath confinement
- Every new route has the right guard?                      yes — `@login_required` (session-only, no first-account fallback) on every GUI route; `@api_key_required` (constant-time compare) on every API route
- Any GET that changes state?                               no — activate/refresh/delete/logout/reschedule/retry/delete-scheduled are all POST
- Secrets, tokens, personal data touched/logged/returned?    tokens (access + rotated refresh) are Fernet-encrypted at rest; never logged, never in any response; `TIKTOK_CLIENT_SECRET` is config, not a column; TikTok error payloads logged server-side only; the scheduler records only client-safe error strings
- New dependency? pip-audit clean?                           yes — `pip-audit -r requirements.txt`: "No known vulnerabilities found" (pins inherited from the audited FacebookManager baseline); the scheduler deliberately added no dependency
- Mutating API endpoint requires explicit account?             yes — `resolve_account(require_explicit=True)`; tests assert `EXPLICIT_ACCOUNT_REQUIRED`
- Error messages safe (code + message, no exception text)?  yes — `TikTokAPIError` carries fixed strings; tests assert the exception text does not leak; no adapter formats an exception into a client message
- Audit log written for mutations?                          info log lines only (structured audit records are TT-S-04, open)

## Verification
- `.venv\Scripts\python -m pytest` → **60 passed** (config/token/service/route/CSRF suites; no
  test reaches TikTok — every API call is faked; the scheduler use case is tested end to end:
  due posts publish, files cleaned up, failures recorded with safe errors).
- `.venv\Scripts\python -m pip_audit -r requirements.txt` → **No known vulnerabilities found**.
- `docker compose build --no-cache && docker compose up -d` → both containers built; the web
  container serves TLS via the mkcert certificate on the host port from `.env` (5002; 5000 and
  5001 are taken by the sibling projects) and reports healthy; the scheduler container waits for
  the web container to be healthy (`depends_on: condition: service_healthy`) before starting
  its loop — the first deploy without that gate raced the migration and logged one
  self-healing "no such table" tick, which is what the gate now prevents — and its healthcheck
  is disabled (it runs no web server; liveness is observable via `docker logs`).
- Live checks: `GET https://localhost:5002/login` → 200 over TLS; `GET /dashboard` without a
  session → 302 to `/login` (no first-account fallback); `GET /api/v1/health` without a key →
  401, with the key → 200 envelope; `GET /api/v1/accounts` with the key → 200 `[]` (connecting
  requires the real TikTok app credentials in `.env` and the TikTok consent screen, which no
  test or script can drive).
- Not run: the suite on PostgreSQL (`DB_ENGINE=postgres`) — the initial schema migration is
  engine-neutral (batch mode) but the CI container run is still to be set up.

---

## CHG-20261013-RG-02 — Governance: rules folder, change fragments, hooks
**Date:** 2026-10-13  
**Author:** RG  
**Type:** chore  
**Scope:** project-wide, docs  
**Files:** `AGENTS.md`, `.githooks/pre-commit`, `.githooks/commit-msg`, `RULEFOLDER/README.md`, `RULEFOLDER/TEAM.md`, `RULEFOLDER/ARCHITECTURE.md`, `RULEFOLDER/FEATURE-REGISTRY.md`, `RULEFOLDER/CODING-CONVENTIONS.md`, `RULEFOLDER/SECURITY.md`, `RULEFOLDER/API-PARITY.md`, `RULEFOLDER/CONTRIBUTING.md`, `RULEFOLDER/CHANGELOG.md`, `RULEFOLDER/changes/_TEMPLATE.md`, `RULEFOLDER/decisions/ADR-001-change-fragments-and-ids.md`, `RULEFOLDER/decisions/ADR-002-service-layer.md`, `RULEFOLDER/decisions/ADR-003-gui-api-parity-as-a-hard-rule.md`, `RULEFOLDER/decisions/ADR-004-database-engine-selection.md`, `RULEFOLDER/decisions/ADR-005-app-side-scheduling.md`, `RULEFOLDER/tools/new_change.py`, `RULEFOLDER/tools/build_changelog.py`, `RULEFOLDER/tools/check_rules.py`, `RULEFOLDER/tools/_fragments.py`  
**Security impact:** none — documentation, tooling and git hooks only; no application code touched. SECURITY.md records this project's own findings register (TT-S-01..09) with the sibling projects' satisfied-at-birth lessons  

## What
The full governance system of the sibling projects, ported to this repository: `RULEFOLDER/`
(map, architecture, feature registry, coding conventions, security rules and findings,
API parity, contributing workflow, team table, generated changelog), five ADRs (the four
ported ones plus a new **ADR-005 — app-side scheduling**, the TikTok-specific architectural
decision), the three tools (`new_change.py`, `build_changelog.py`, `check_rules.py` — copied
verbatim, they are project-agnostic), the change-fragment template, and the git hooks
(pre-commit + commit-msg) with `core.hooksPath` configured. `AGENTS.md` becomes the entry point.
The initial application (CHG-20261013-RG-01) is recorded as the first fragment, so the
changelog starts complete.

## Why
The owner runs all three publishers under one workflow: every change recorded, every GUI
capability with an API twin, security reviewed per change, IDs that cannot collide between
people or parallel AI sessions. Porting the proven system — tools verbatim, prose adapted to
this codebase — costs one change and removes all future argument about process.

## How
- **Tools copied verbatim** from FacebookManager (only the generated changelog's title string
  was adapted): they locate the project relative to their own path and reference `DOC/API.md`
  and `app/routes/`, which exist here.
- **Prose rewritten against this codebase**: `ARCHITECTURE.md` documents the two-container
  shape and the scheduling/token-refresh flows; `FEATURE-REGISTRY.md` lists the 19 real
  features with their real routes — `check_rules.py --parity` verifies it against the code and
  `DOC/API.md` (and caught one real gap while writing it: the GUI's retry button had no API
  twin, so `POST /api/v1/scheduled/{id}/retry` was added to keep parity structural);
  `SECURITY.md` keeps the mandatory rules and opens this project's own findings register
  (TT-S-01..09, including the two-writer SQLite note).
- **ADR-005 is new**, not ported: TikTok's Content Posting API has no scheduled publish, and
  the decision to schedule app-side (table + persistent media folder + a dedicated one-writer
  scheduler container, no scheduler library) is exactly the kind of decision an ADR exists for.
- **Hooks**: `.githooks/pre-commit` runs `check_rules.py --staged`; `.githooks/commit-msg`
  enforces the `CHG-YYYYMMDD-DEV-NN:` first line. Installed with
  `git config core.hooksPath .githooks` (the repository also needed a one-time
  `safe.directory` exception — this drive does not record ownership).

## API impact
One endpoint was added to keep parity structural (see `## How`):
`POST /api/v1/scheduled/{id}/retry` — re-queues a failed scheduled post; documented in
`DOC/API.md` §11.5 and registered in `FEATURE-REGISTRY.md`. No other endpoint changed.
`check_rules.py --parity` passes: every GUI route in code is registered with an API twin or a
reasoned browser exception, and every API endpoint in code is documented in `DOC/API.md`.

## Security review
- Inputs validated (type/length/allow-list/path)?           not applicable for the docs/tooling; the one new endpoint reuses the existing guards and service function
- Every new route has the right guard?                       yes — `POST /api/v1/scheduled/{id}/retry` is behind `@api_key_required` and requires an explicit account
- Any GET that changes state?                                no
- Secrets, tokens, personal data touched/logged/returned?    no — the fragments and rules contain no secrets; `SECURITY.md` §4 records the open findings honestly
- New dependency? pip-audit clean?                           n/a — the tools use only the Python standard library
- Mutating API endpoint requires explicit account?           yes — the retry endpoint (tested)
- Error messages safe (code + message, no exception text)?   yes — fixed messages
- Audit log written for mutations?                           n/a

## Verification
- `python RULEFOLDER\tools\build_changelog.py` → `CHANGELOG.md` rebuilt from 2 fragments.
- `python RULEFOLDER\tools\check_rules.py` → **All rule checks passed** (fragments valid, parity
  clean, changelog fresh).
- `.venv\Scripts\python -m pytest` → 61 passed (60 from CHG-20261013-RG-01 plus the new retry
  endpoint test).
- `git config core.hooksPath` → `.githooks`; `git status` works after the `safe.directory`
  exception.

---

## CHG-20261014-RG-01 — Login: send PKCE code challenge (TikTok refused authorize without it)
**Date:** 2026-10-14  
**Author:** RG  
**Type:** bugfix  
**Scope:** backend  
**Files:** `app/auth.py`, `app/tiktok_api.py`, `tests/test_auth_pkce.py`  
**Parity exception:** Login is a browser-only OAuth flow; it has no API twin by design (API-PARITY browser exception for auth routes).  
**Security impact:** improves — the authorization code is now bound to a per-flow PKCE verifier kept server-side in the session, so an intercepted code cannot be redeemed without it  

## What
"Continue with TikTok" now reaches TikTok's consent screen. Before, TikTok answered with "Something
went wrong … code_challenge" and no account could ever be connected.

## Why
**Problem:** TikTok's authorize endpoint rejected the request with `error_type=code_challenge`
(seen 14 Oct 2026 with the configured client key). The app sent no PKCE parameters.
**Fix:** every login flow generates a fresh code verifier, sends its challenge with
`code_challenge_method=S256`, and sends the verifier back on the token exchange.

## How
`auth.pkce_pair()` returns a 64-character verifier from `secrets.token_urlsafe(48)` (only
unreserved characters) and its challenge. TikTok documents the challenge as the **hex** SHA-256 of
the verifier ("You must use hex encoding of SHA256"), not RFC 7636's base64url, so the code follows
TikTok. The verifier lives in the Flask session next to `oauth_state` and is popped once on the
callback; a callback without it is refused like a state mismatch. `tiktok_api.exchange_code` takes
an optional `code_verifier` and stays Flask-free.

## API impact
None. Login is browser-only (see `parity_exception`).

## Security review
- Input validated? yes — the callback still requires code + matching state, and now also a verifier from the same session
- Auth guard present on every new route? no new route
- State change on GET? no change: `/auth/callback` is the OAuth redirect target, as before
- Secrets, tokens or personal data touched, logged or returned? the verifier is kept in the signed session only; it is never logged, never put in a URL (a test asserts the redirect URL does not contain it)
- New dependency? no (`hashlib`, `secrets` from the standard library)

## Verification
- `.venv\Scripts\python -m pytest` → 71 passed (4 new PKCE tests).
- Manual, before the fix: `/auth/start` → TikTok page "correct the following: code_challenge".
- Manual check after the fix: see `## Verification` of CHG-20261014-RG-02 (the consent screen is reached once the container is rebuilt).

---

## CHG-20261014-RG-02 — Drafts: upload through the inbox endpoint with video.upload; drafts become the default publish path
**Date:** 2026-10-14  
**Author:** RG  
**Type:** bugfix  
**Scope:** backend, web-ui, api, docs  
**Files:** `app/tiktok_api.py`, `app/config.py`, `app/services/posts.py`, `app/services/scheduled.py`, `app/routes/post_routes.py`, `app/routes/api_routes.py`, `app/templates/post_new.html`, `DOC/API.md`, `README.md`, `.env.example`, `RULEFOLDER/FEATURE-REGISTRY.md`, `tests/conftest.py`, `tests/services/test_posts.py`, `tests/services/test_tiktok_api_init.py`, `tests/routes/test_gui_posts.py`, `tests/test_token_encryption.py`  
**GUI:** GET /posts/new, POST /posts/new  
**API:** POST /api/v1/posts, POST /api/v1/posts/media, POST /api/v1/scheduled, POST /api/v1/scheduled/media  
**Security impact:** none — same guards; a post mode is now refused unless the account granted its scope  
**BREAKING CHANGE**  

## What
Publishing now sends the video to the creator's TikTok inbox as a draft by default. TikTok notifies
the creator, who taps the notification in the TikTok app to caption and post it. The New Post page
explains this step, shows a preview of the chosen file, and only offers Direct post when the account
granted `video.publish`. The default scopes are now `user.info.basic,user.info.stats,video.list,video.upload`.

## Why
**Problem:** "draft" mode called the Direct Post endpoint (`/v2/post/publish/video/init/`) without
`post_info`, so it never produced a draft; drafts belong to `/v2/post/publish/inbox/video/init/`
with the `video.upload` scope, which the app did not request.
**Product reason for the new default:** a draft needs only `video.upload` and no Direct Post
audit, so it works for every connected creator from day one; unaudited Direct Post is limited to
private (SELF_ONLY) videos. Direct post stays available for accounts that grant `video.publish`.

## How
`tiktok_api._init_body()` chooses the endpoint and body per mode: a draft sends `source_info` only;
a direct post keeps `post_info` (privacy, caption). `services.posts.validate_post_fields()` takes the
account's granted scopes and refuses a mode whose scope (`MODE_SCOPES`) is missing, so the GUI,
the API and scheduling share one rule (ADR-002). Mode `draft` is the default everywhere.
Out of scope: Direct Post's audit UX (creator_info query, no-default privacy, interaction toggles,
commercial-content disclosure); required before the app applies for the Direct Post audit.

## API impact
`POST /api/v1/posts`, `/posts/media`, `/scheduled`, `/scheduled/media`: `mode` now defaults to
`draft` (breaking for a client that relied on the old `direct` default); a mode whose scope the
account did not grant returns `400 INVALID_INPUT`. `DOC/API.md` §9.1 and §13 updated.

## Security review
- Input validated? yes — mode allow-list unchanged, plus the granted-scope check
- Auth guard present on every new route? no new route; existing guards unchanged
- State change on GET? no
- Secrets, tokens or personal data touched, logged or returned? no
- New dependency? no

## Verification
- `.venv\Scripts\python -m pytest` → 71 passed (new: inbox endpoint for file and URL drafts, direct
  keeps post_info, scope gate, draft-first form, draft flash).
- `python RULEFOLDER\tools\build_changelog.py` and `python RULEFOLDER\tools\check_rules.py` → "CHANGELOG.md rebuilt from 4 fragments", "All rule checks passed".

---

## CHG-20261014-RG-03 — Port 9506, loopback-only binding, and the server deployment guide for ttok.thinkera.academy
**Date:** 2026-10-14  
**Author:** RG  
**Type:** infra  
**Scope:** infra, docs  
**Files:** `docker-compose.yml`, `app/config.py`, `.env.example`, `README.md`, `AGENTS.md`, `DOC/API.md`, `DOC/DEPLOY.md`, `tests/conftest.py`  
**Parity exception:** Infrastructure and documentation only; no GUI or API behaviour changed.  
**Security impact:** improves — the port is published on 127.0.0.1 only (Apache is the single public entry), and the session cookie is Secure + SameSite=Lax in production  
**BREAKING CHANGE**  

## What
TikTok Manager now listens on port 9506 instead of 5002, on the loopback interface only, on the
laptop and on the server. `DOC/DEPLOY.md` describes the production deployment at
`/var/www/private/thinkera.academy/ttok.thinkera.academy` behind Apache at `https://ttok.thinkera.academy`.

## Why
The owner chose port 9506 for the server and the same port locally (14 Oct 2026), and a public
HTTPS domain is needed because TikTok's app review requires the demo to show the app on the
verified website domain, not on localhost.

## How
The compose file publishes `127.0.0.1:${PORT:-9506}:5000`, following the Publishers rule that a
tool holding account tokens binds to loopback. The redirect URI default, `.env.example`, tests and
docs move to 9506. In production Apache terminates TLS and proxies to the container over plain
HTTP; the session cookie gets `Secure` (production only, so the HTTP test client keeps working)
and `SameSite=Lax`, which still sends it on TikTok's top-level redirect back to `/auth/callback`.

## API impact
Base URL in the docs is now `https://localhost:9506/api/v1` locally and
`https://ttok.thinkera.academy/api/v1` in production; no endpoint changed.

## Security review
- Input validated? not affected
- Auth guard present on every new route? no new route
- State change on GET? no
- Secrets, tokens or personal data touched, logged or returned? no; DEPLOY.md requires fresh SECRET_KEY, API_KEY and TOKEN_ENCRYPTION_KEY on the server
- New dependency? no

## Verification
- `.venv\Scripts\python -m pytest` → 75 passed.
- `docker compose down` + `up -d --build`: `docker ps` shows `127.0.0.1:9506->5000/tcp`; `https://localhost:9506/login` → 200; `https://localhost:5002/login` → no answer; migration `0001_initial -> 0002_account_union_id` ran on start.
- `python RULEFOLDER/tools/build_changelog.py` → rebuilt from 6 fragments; `python RULEFOLDER/tools/check_rules.py` → All rule checks passed.

---

## CHG-20261014-RG-04 — Sign-in allowlist: only listed TikTok accounts can connect
**Date:** 2026-10-14  
**Author:** RG  
**Type:** security  
**Scope:** backend, web-ui, docs  
**Files:** `app/config.py`, `app/__init__.py`, `app/auth.py`, `app/models.py`, `migrations/versions/0002_account_union_id.py`, `.env.example`, `DOC/DEPLOY.md`, `tests/test_allowlist.py`  
**GUI:** GET /auth/callback  
**Parity exception:** Sign-in is a browser-only OAuth flow; the REST API is already limited to holders of API_KEY and to accounts that are connected.  
**Security impact:** improves — once the app is public, only TikTok accounts listed in TIKTOK_ALLOWED_ACCOUNTS can connect; production refuses to start without the list  
**BREAKING CHANGE**  

## What
Only the TikTok accounts listed in `TIKTOK_ALLOWED_ACCOUNTS` (open_id or union_id, comma-separated)
can sign in. Any other account is refused at the callback, nothing of it is stored, and the login
page shows its ID so the administrator can add it. A session whose account was removed from the
list is closed on the next page. `*` lets any account sign in.

## Why
The owner will run the app publicly at `ttok.thinkera.academy` but limited to specific accounts
(14 Oct 2026). Without a list, any TikTok user could connect once the app is approved.

## How
`auth.account_allowed()` checks the list; `save_account_from_login()` calls it after the token
exchange and profile fetch and before any database write; `@login_required` re-checks the active
account. `union_id` (same across the Sandbox and Production apps of one developer, unlike open_id)
is stored on the account by migration `0002_account_union_id`. `check_access_config()` makes the
app refuse to start in production with an empty list (fail closed); in development an empty list
stays open so local work needs no setup.

## API impact
None (see `parity_exception`). `GET /api/v1/accounts` lists only connected accounts, which passed
the allowlist when they connected.

## Security review
- Input validated? the list comes from the server's `.env` only; values are compared as exact strings
- Auth guard present on every new route? no new route; `@login_required` now also enforces the list
- State change on GET? `/auth/callback` (the OAuth redirect target) refuses before writing anything
- Secrets, tokens or personal data touched, logged or returned? a refused user sees their own ID only; the log line carries no ID or token
- New dependency? no

## Verification
- `.venv\Scripts\python -m pytest` → 75 passed (4 new: listed union_id signs in and is stored, unlisted account refused with its ID and not stored, removed account's session closed, production refuses an empty list).

---
