Skip to content

Commit 923f021

Browse files
authored
feat: v7.2.0 — Share tokens, thumbnails, download restrictions, settings
Share tokens, on-demand thumbnails, download restrictions, app settings, and audit improvements. - Share tokens: Link-shareable auth scoped to specific chats with PBKDF2 hashing, expiry, revocation, use tracking, and immediate session invalidation via source_token_id - Download restrictions: no_download flag on viewer accounts and share tokens blocks explicit downloads/exports while preserving inline media - On-demand thumbnails: WebP thumbnail generation with decompression bomb protection and path traversal guards - App settings: Key-value store with admin CRUD endpoints - Audit log improvements: Prefix-match action filtering, token auth event tracking - Session persistence: no_download and source_token_id survive container restarts - Migration 010: viewer_tokens, app_settings tables + no_download/source_token_id columns PR #89
1 parent 150f1cf commit 923f021

16 files changed

Lines changed: 1761 additions & 61 deletions

Dockerfile.viewer

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ WORKDIR /app
66
# Copy requirements first for better caching
77
COPY requirements-viewer.txt .
88

9-
# Install Python dependencies (no gcc needed - viewer has no native extensions)
9+
# Install system libraries for Pillow (thumbnail generation)
10+
RUN apt-get update && apt-get install -y --no-install-recommends \
11+
libjpeg62-turbo libwebp7 libwebpmux3 zlib1g \
12+
&& rm -rf /var/lib/apt/lists/*
13+
14+
# Install Python dependencies
1015
RUN pip install --no-cache-dir -r requirements-viewer.txt
1116

1217
# Copy only the necessary application code for the viewer

SECURITY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
| Version | Supported |
66
| ------- | ------------------ |
7+
| 7.x.x | :white_check_mark: |
78
| 6.x.x | :white_check_mark: |
89
| 5.x.x | :x: |
910
| < 5.0 | :x: |
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""Add viewer tokens, app settings, session fields, and no_download columns (v7.2.0).
2+
3+
Creates:
4+
1. viewer_tokens table for share-token authentication
5+
2. app_settings table for cross-container configuration
6+
3. no_download column on viewer_accounts
7+
4. no_download + source_token_id columns on viewer_sessions
8+
9+
Revision ID: 010
10+
Revises: 009
11+
Create Date: 2026-03-10
12+
13+
"""
14+
15+
from collections.abc import Sequence
16+
17+
import sqlalchemy as sa
18+
19+
from alembic import op
20+
21+
revision: str = "010"
22+
down_revision: str | None = "009"
23+
branch_labels: str | Sequence[str] | None = None
24+
depends_on: str | Sequence[str] | None = None
25+
26+
27+
def upgrade() -> None:
28+
conn = op.get_bind()
29+
inspector = sa.inspect(conn)
30+
existing_tables = set(inspector.get_table_names())
31+
32+
# -- viewer_tokens table --
33+
if "viewer_tokens" not in existing_tables:
34+
op.create_table(
35+
"viewer_tokens",
36+
sa.Column("id", sa.Integer(), nullable=False),
37+
sa.Column("label", sa.String(255), nullable=True),
38+
sa.Column("token_hash", sa.String(128), nullable=False, unique=True),
39+
sa.Column("token_salt", sa.String(64), nullable=False),
40+
sa.Column("created_by", sa.String(255), nullable=False),
41+
sa.Column("allowed_chat_ids", sa.Text(), nullable=False),
42+
sa.Column("is_revoked", sa.Integer(), server_default="0"),
43+
sa.Column("no_download", sa.Integer(), server_default="0"),
44+
sa.Column("expires_at", sa.DateTime(), nullable=True),
45+
sa.Column("last_used_at", sa.DateTime(), nullable=True),
46+
sa.Column("use_count", sa.Integer(), server_default="0"),
47+
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now()),
48+
sa.PrimaryKeyConstraint("id"),
49+
)
50+
op.create_index("idx_viewer_tokens_created_by", "viewer_tokens", ["created_by"])
51+
op.create_index("idx_viewer_tokens_is_revoked", "viewer_tokens", ["is_revoked"])
52+
else:
53+
existing_indexes = {idx["name"] for idx in inspector.get_indexes("viewer_tokens")}
54+
if "idx_viewer_tokens_created_by" not in existing_indexes:
55+
op.create_index("idx_viewer_tokens_created_by", "viewer_tokens", ["created_by"])
56+
if "idx_viewer_tokens_is_revoked" not in existing_indexes:
57+
op.create_index("idx_viewer_tokens_is_revoked", "viewer_tokens", ["is_revoked"])
58+
59+
# -- app_settings table --
60+
if "app_settings" not in existing_tables:
61+
op.create_table(
62+
"app_settings",
63+
sa.Column("key", sa.String(255), nullable=False),
64+
sa.Column("value", sa.Text(), nullable=False),
65+
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now()),
66+
sa.PrimaryKeyConstraint("key"),
67+
)
68+
69+
# -- no_download column on viewer_accounts --
70+
existing_va_cols = {c["name"] for c in inspector.get_columns("viewer_accounts")}
71+
if "no_download" not in existing_va_cols:
72+
op.add_column("viewer_accounts", sa.Column("no_download", sa.Integer(), server_default="0", nullable=True))
73+
74+
# -- no_download and source_token_id columns on viewer_sessions --
75+
if "viewer_sessions" in existing_tables:
76+
existing_vs_cols = {c["name"] for c in inspector.get_columns("viewer_sessions")}
77+
if "no_download" not in existing_vs_cols:
78+
op.add_column("viewer_sessions", sa.Column("no_download", sa.Integer(), server_default="0", nullable=True))
79+
if "source_token_id" not in existing_vs_cols:
80+
op.add_column("viewer_sessions", sa.Column("source_token_id", sa.Integer(), nullable=True))
81+
existing_vs_indexes = {idx["name"] for idx in inspector.get_indexes("viewer_sessions")}
82+
if "idx_viewer_sessions_source_token" not in existing_vs_indexes:
83+
op.create_index("idx_viewer_sessions_source_token", "viewer_sessions", ["source_token_id"])
84+
85+
86+
def downgrade() -> None:
87+
op.drop_index("idx_viewer_sessions_source_token", table_name="viewer_sessions")
88+
op.drop_column("viewer_sessions", "source_token_id")
89+
op.drop_column("viewer_sessions", "no_download")
90+
op.drop_column("viewer_accounts", "no_download")
91+
op.drop_table("app_settings")
92+
op.drop_index("idx_viewer_tokens_is_revoked", table_name="viewer_tokens")
93+
op.drop_index("idx_viewer_tokens_created_by", table_name="viewer_tokens")
94+
op.drop_table("viewer_tokens")

docs/CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,41 @@ For upgrade instructions, see [Upgrading](#upgrading) at the bottom.
66

77
## [Unreleased]
88

9+
## [7.2.0] - 2026-03-10
10+
11+
### Added
12+
13+
- **Share tokens** — Admins can create link-shareable tokens scoped to specific chats. Recipients authenticate via token without needing an account. Tokens support expiry dates, revocation, and use tracking
14+
- **Download restrictions**`no_download` flag on both viewer accounts and share tokens. Restricted users can still view media inline but cannot explicitly download files or export chat history. Download buttons hidden in the UI for restricted users
15+
- **On-demand thumbnails** — WebP thumbnail generation at whitelisted sizes (200px, 400px) with disk caching under `{media_root}/.thumbs/`. Includes Pillow decompression bomb protection and path traversal guards
16+
- **App settings** — Key-value `app_settings` table for cross-container configuration, with admin CRUD endpoints
17+
- **Audit log improvements** — Action-based filtering in admin panel (prefix match for suffixed events like `viewer_updated:username`), token auth events tracked (`token_auth_success`, `token_auth_failed`, `token_created`, etc.)
18+
- **Admin chat picker metadata** — Chat picker now returns `username`, `first_name`, `last_name` for better display
19+
- **Token management UI** — New "Share Tokens" tab in admin panel with create, revoke, and delete controls. Plaintext token shown once at creation with copy button
20+
- **Token login UI** — Login page has a "Share Token" tab for token-based authentication
21+
22+
### Security
23+
24+
- **Token revocation enforced on active sessions** — Revoking, deleting, or changing scope/permissions of a share token immediately invalidates all sessions created from that token. Sessions track `source_token_id` for precise invalidation
25+
- **Session persistence includes restrictions**`no_download` and `source_token_id` are now persisted in `viewer_sessions` table, surviving container restarts. Previously `no_download` was lost after restart, silently granting download access
26+
- **Export endpoint respects no_download** — The `GET /api/chats/{chat_id}/export` endpoint now returns 403 for restricted users
27+
28+
### Fixed
29+
30+
- **Create viewer passes all flags**`is_active` and `no_download` from the admin form are now correctly passed through to `create_viewer_account()`. Previously both flags were silently ignored on creation
31+
- **Token expiry timezone handling** — Frontend now converts local datetime to UTC ISO before sending to the backend, fixing early/late expiry for non-UTC admins
32+
- **Audit filter matches suffixed actions** — Filter now uses prefix matching so "viewer_updated" catches "viewer_updated:username"
33+
- **Migration stamping checks all artifacts** — Entrypoint now checks `viewer_tokens`, `app_settings`, AND `viewer_accounts.no_download` before stamping migration 010 as complete
34+
35+
### Changed
36+
37+
- **Migration 010** — Consolidated idempotent migration creates `viewer_tokens`, `app_settings` tables and adds `no_download` column to `viewer_accounts`. Also adds `no_download` and `source_token_id` columns to `viewer_sessions`
38+
- **Entrypoint stamping** — Updated both PostgreSQL and SQLite stamping blocks to detect all migration 010 artifacts
39+
- **Dockerfile.viewer** — Added Pillow system dependencies (libjpeg, libwebp) for thumbnail generation
40+
- **Version declarations**`pyproject.toml` and `src/__init__.py` both set to 7.2.0
41+
- **SECURITY.md** — Added 7.x.x as a supported version
42+
- **pyproject.toml** — Added `viewer` optional dependency group for Pillow
43+
944
## [7.1.3] - 2026-03-05
1045

1146
### Fixed

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "telegram-archive"
7-
version = "7.1.3"
7+
version = "7.2.0"
88
description = "Automated Telegram backup with Docker. Performs incremental backups of messages and media on a configurable schedule."
99
readme = "README.md"
1010
requires-python = ">=3.14"
@@ -43,6 +43,9 @@ dependencies = [
4343
]
4444

4545
[project.optional-dependencies]
46+
viewer = [
47+
"Pillow>=10.0.0", # v7.2.0: thumbnail generation
48+
]
4649
dev = [
4750
"pytest>=7.0",
4851
"pytest-asyncio>=0.21.0",

requirements-viewer.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ greenlet>=3.1.0
2121
pywebpush>=2.0.0
2222
py-vapid>=1.9.0
2323

24+
# Image processing for thumbnails (v7.2.0+)
25+
Pillow>=10.0.0
26+
2427

2528

2629

scripts/entrypoint.sh

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,30 @@ if has_tables and not has_alembic:
8181
CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)
8282
);
8383
\"\"\")
84+
# Check all artifacts from migration 010: viewer_tokens, app_settings, viewer_accounts.no_download
85+
cur.execute(\"\"\"
86+
SELECT EXISTS (
87+
SELECT FROM information_schema.tables
88+
WHERE table_name = 'viewer_tokens'
89+
);
90+
\"\"\")
91+
has_010_tokens = cur.fetchone()[0]
92+
cur.execute(\"\"\"
93+
SELECT EXISTS (
94+
SELECT FROM information_schema.tables
95+
WHERE table_name = 'app_settings'
96+
);
97+
\"\"\")
98+
has_010_settings = cur.fetchone()[0]
99+
cur.execute(\"\"\"
100+
SELECT EXISTS (
101+
SELECT FROM information_schema.columns
102+
WHERE table_name = 'viewer_accounts' AND column_name = 'no_download'
103+
);
104+
\"\"\")
105+
has_010_no_download = cur.fetchone()[0]
106+
has_010_all = has_010_tokens and has_010_settings and has_010_no_download
107+
84108
# Check if viewer_sessions table exists (added in migration 009)
85109
cur.execute(\"\"\"
86110
SELECT EXISTS (
@@ -145,7 +169,9 @@ if has_tables and not has_alembic:
145169
has_push_subs = cur.fetchone()[0]
146170
147171
# Determine which version to stamp based on existing schema
148-
if has_009_table:
172+
if has_010_all:
173+
stamp_version = '010'
174+
elif has_009_table:
149175
stamp_version = '009'
150176
elif has_008_column:
151177
stamp_version = '008'
@@ -212,6 +238,16 @@ if has_tables and not has_alembic:
212238
)
213239
''')
214240
241+
# Check all artifacts from migration 010: viewer_tokens, app_settings, viewer_accounts.no_download
242+
cur.execute(\"SELECT name FROM sqlite_master WHERE type='table' AND name='viewer_tokens'\")
243+
has_010_tokens = cur.fetchone() is not None
244+
cur.execute(\"SELECT name FROM sqlite_master WHERE type='table' AND name='app_settings'\")
245+
has_010_settings = cur.fetchone() is not None
246+
cur.execute(\"PRAGMA table_info(viewer_accounts)\")
247+
va_columns = {row[1] for row in cur.fetchall()}
248+
has_010_no_download = 'no_download' in va_columns
249+
has_010_all = has_010_tokens and has_010_settings and has_010_no_download
250+
215251
# Check if viewer_sessions table exists (added in migration 009)
216252
cur.execute(\"SELECT name FROM sqlite_master WHERE type='table' AND name='viewer_sessions'\")
217253
has_009_table = cur.fetchone() is not None
@@ -243,7 +279,9 @@ if has_tables and not has_alembic:
243279
has_push_subs = cur.fetchone() is not None
244280
245281
# Determine which version to stamp based on existing schema
246-
if has_009_table:
282+
if has_010_all:
283+
stamp_version = '010'
284+
elif has_009_table:
247285
stamp_version = '009'
248286
elif has_008_column:
249287
stamp_version = '008'

src/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Telegram Backup Automation - Main Package
33
"""
44

5-
__version__ = "7.0.3"
5+
__version__ = "7.2.0"

src/db/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from .base import DatabaseManager, close_database, get_db_manager, init_database
3535
from .migrate import migrate_sqlite_to_postgres, verify_migration
3636
from .models import (
37+
AppSettings,
3738
Base,
3839
Chat,
3940
ChatFolder,
@@ -48,6 +49,7 @@
4849
ViewerAccount,
4950
ViewerAuditLog,
5051
ViewerSession,
52+
ViewerToken,
5153
)
5254

5355
__all__ = [
@@ -66,6 +68,8 @@
6668
"ViewerAccount",
6769
"ViewerAuditLog",
6870
"ViewerSession",
71+
"ViewerToken",
72+
"AppSettings",
6973
# Database management
7074
"DatabaseManager",
7175
"init_database",

0 commit comments

Comments
 (0)