Admin step B: stats endpoint + /admin dashboard
- users.is_admin (+ migration); admin = is_admin OR email in GOODNEWS_ADMIN_EMAILS (normalized). is_admin exposed on /api/auth/me. Server-authorized GET /api/admin/stats (403 for non-admins). - queries.admin_stats: visitors (today/7d/30d), returning vs one-and-done, top opened articles, popular groupings + topics (derived from article_id at query time), share breakdown, daily opens/visits trend — all aggregate, no PII. - /admin page (gated, redirects non-admins): stat cards, CSS bar lists, a daily trend; "Admin dashboard" link on /account for admins. 129 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -137,6 +137,7 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT,
|
||||
avatar_url TEXT,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -259,10 +260,12 @@ def _migrate(conn: sqlite3.Connection) -> None:
|
||||
if column not in score_cols:
|
||||
conn.execute(f"ALTER TABLE article_scores ADD COLUMN {column} TEXT")
|
||||
|
||||
# users.avatar_url added for Google profile pictures.
|
||||
# users.avatar_url (Google pictures) + is_admin (admin dashboard) added later.
|
||||
user_tbl = {row["name"] for row in conn.execute("PRAGMA table_info(users)")}
|
||||
if user_tbl and "avatar_url" not in user_tbl:
|
||||
conn.execute("ALTER TABLE users ADD COLUMN avatar_url TEXT")
|
||||
if user_tbl and "is_admin" not in user_tbl:
|
||||
conn.execute("ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0")
|
||||
|
||||
article_cols = {row["name"] for row in conn.execute("PRAGMA table_info(articles)")}
|
||||
if "duplicate_of" not in article_cols:
|
||||
|
||||
Reference in New Issue
Block a user