---
id: CHG-20261013-RG-01
title: Initial application: TikTok accounts manager
date: 2026-10-13
author: RG
type: feature
scope: [backend, api, web-ui, infra]
status: done
breaking: false
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_routes:
  - 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_endpoints:
  - 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
parity_exception: ""
api_doc_updated: yes
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"
tests:
  - tests/test_config.py (4 tests: production guards, non-production minting)
  - tests/test_token_encryption.py (4 tests: encryption at rest, no tokens in to_dict, expiry flag, second account)
  - tests/services/test_posts.py (7 tests: validation rules, publish paths, cleanup, processing failure, schedule parsing)
  - tests/services/test_uploads.py (4 tests: server names, confinement, allow-list, size cap)
  - tests/services/test_scheduled.py (7 tests: create, future-time validation, due-post publish + file cleanup, future posts untouched, safe failure errors, reschedule/retry, delete + file removal)
  - tests/routes/test_api_accounts.py (6 tests: 401, list/get/refresh/delete, health)
  - tests/routes/test_api_posts.py (13 tests: 401s, NO_ACCOUNT, EXPLICIT_ACCOUNT_REQUIRED, publish happy paths, validation, safe error mapping, media cleanup, status, scheduled CRUD + retry, insights)
  - tests/routes/test_gui_posts.py (10 tests: login guard, dashboard, videos page, publish now, schedule, validation flash, scheduled page, delete route, insights)
  - tests/routes/test_gui_csrf.py (4 tests: mutations rejected without a token, accepted with one, logout POST-only)
---

## 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.
