sojorn/admin/docs/ADMIN_SYSTEM.md

21 KiB

Sojorn Admin Panel — Comprehensive System Documentation

Last updated: February 6, 2026


Table of Contents

  1. Overview
  2. Architecture
  3. Authentication & Security
  4. Server Deployment
  5. Frontend (Next.js)
  6. Backend API Routes
  7. Database Schema
  8. Feature Reference
  9. Environment Variables
  10. Troubleshooting

Overview

The Sojorn Admin Panel is an internal tool for platform administrators to manage users, moderate content, review appeals, configure the feed algorithm, and monitor system health. It is a standalone Next.js 14 application that communicates with the existing Sojorn Go backend via a dedicated set of admin API endpoints.

Key characteristics:

  • Separate frontend deployment from the main Flutter app
  • Role-based access — only users with role = 'admin' in the profiles table can log in
  • All admin actions are logged to the audit_log table
  • Invisible Cloudflare Turnstile bot protection on login
  • JWT authentication with 24-hour token expiry

Architecture

┌─────────────────────┐     HTTPS      ┌──────────────────────┐
│  Browser            │ ◄────────────► │  Nginx               │
│  admin.sojorn.net   │                │  (reverse proxy)     │
└─────────────────────┘                └──────┬───────────────┘
                                              │
                           ┌──────────────────┼──────────────────┐
                           │ port 3002        │ port 8080        │
                           ▼                  ▼                  │
                  ┌─────────────────┐ ┌─────────────────┐       │
                  │ Next.js 14      │ │ Go Backend      │       │
                  │ (sojorn-admin)  │ │ (sojorn-api)    │       │
                  │ SSR + Static    │ │ Gin framework   │       │
                  └─────────────────┘ └────────┬────────┘       │
                                               │                │
                                      ┌────────▼────────┐       │
                                      │ PostgreSQL      │       │
                                      │ sojorn database │       │
                                      └─────────────────┘       │
                                                                │
                                      ┌─────────────────┐       │
                                      │ Cloudflare R2   │───────┘
                                      │ (media storage) │
                                      └─────────────────┘

Tech Stack

Component Technology
Frontend Next.js 14, TypeScript, TailwindCSS, Recharts, Lucide icons
Backend Go 1.21+, Gin framework, pgx (PostgreSQL driver)
Database PostgreSQL 15+
Process management systemd
Reverse proxy Nginx
Bot protection Cloudflare Turnstile (invisible mode)
Auth JWT (HS256), bcrypt password hashing

Authentication & Security

Login Flow

  1. User enters email + password on /login
  2. Cloudflare Turnstile invisible widget generates a token in the background
  3. Frontend sends POST /api/v1/admin/login with { email, password, turnstile_token }
  4. Backend verifies Turnstile token with Cloudflare API
  5. Backend checks users table for valid credentials (bcrypt)
  6. Backend verifies account status is active
  7. Backend checks profiles.role = 'admin'
  8. Returns JWT (access_token) with 24-hour expiry + user profile data
  9. Token stored in localStorage as admin_token

Middleware Chain (Protected Routes)

All /api/v1/admin/* routes (except /login) pass through:

  1. AuthMiddleware — Validates JWT, extracts user_id from sub claim, sets it in Gin context
  2. AdminMiddleware — Queries profiles.role for the authenticated user, rejects non-admin users with 403

Security Measures

  • Invisible Turnstile — Blocks automated login attacks without user friction
  • Graceful degradation — If TURNSTILE_SECRET is empty, verification is skipped (dev mode)
  • bcrypt — Passwords hashed with bcrypt (default cost)
  • JWT expiry — Admin tokens expire after 24 hours (vs 7 days for regular users)
  • Auto-logout — Frontend redirects to /login on any 401 response
  • Audit logging — Status changes, post deletions, and moderation actions are logged

Granting Admin Access

UPDATE profiles SET role = 'admin' WHERE handle = 'your_handle';

Valid roles: user, moderator, admin


Server Deployment

Services

Service systemd unit Port Binary/Entry
Go API sojorn-api 8080 /opt/sojorn/bin/api
Admin Frontend sojorn-admin 3002 node .../next start --port 3002

File Locations on Server

/opt/sojorn/
├── .env                          # Shared environment variables
├── bin/
│   └── api                       # Compiled Go binary
├── go-backend/                   # Go source code
│   ├── cmd/api/main.go
│   ├── internal/
│   │   ├── handlers/admin_handler.go
│   │   ├── middleware/admin.go
│   │   └── ...
│   └── .env                      # Symlink or copy of /opt/sojorn/.env
├── admin/                        # Next.js admin frontend
│   ├── .env.local                # Frontend env vars
│   ├── .next/                    # Build output
│   ├── node_modules/
│   ├── src/
│   │   ├── app/                  # Page routes
│   │   └── lib/                  # API client, auth context
│   └── package.json
└── firebase-service-account.json

systemd Service Files

/etc/systemd/system/sojorn-admin.service:

[Unit]
Description=Sojorn Admin Panel
After=network.target

[Service]
Type=simple
User=patrick
Group=patrick
WorkingDirectory=/opt/sojorn/admin
ExecStart=/usr/bin/node /opt/sojorn/admin/node_modules/next/dist/bin/next start --port 3002
Restart=on-failure
RestartSec=30
StartLimitIntervalSec=120
StartLimitBurst=3
Environment=NODE_ENV=production
Environment=NEXT_PUBLIC_API_URL=https://api.sojorn.net

[Install]
WantedBy=multi-user.target

Nginx Configuration

/etc/nginx/sites-available/sojorn-admin:

server {
    listen 80;
    server_name admin.sojorn.net;

    location / {
        proxy_pass http://127.0.0.1:3002;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}

Common Operations

# Rebuild and deploy Go backend
cd /opt/sojorn/go-backend
go build -ldflags='-s -w' -o /opt/sojorn/bin/api ./cmd/api/main.go
sudo systemctl restart sojorn-api

# Rebuild and deploy admin frontend
cd /opt/sojorn/admin
npx next build
sudo systemctl restart sojorn-admin

# View logs
sudo journalctl -u sojorn-admin -f
sudo journalctl -u sojorn-api -f

# Check service status
sudo systemctl status sojorn-admin
sudo systemctl status sojorn-api

# SSL certificate (after DNS A record is pointed)
sudo certbot --nginx -d admin.sojorn.net

Frontend (Next.js)

Pages

Route File Description
/ app/page.tsx Dashboard with stats cards and growth charts
/login app/login/page.tsx Admin login with invisible Turnstile
/users app/users/page.tsx User list with search, filter by status/role
/users/[id] app/users/[id]/page.tsx User detail — profile, stats, admin actions
/posts app/posts/page.tsx Post list with search, filter by status
/posts/[id] app/posts/[id]/page.tsx Post detail — content, media, moderation flags, admin actions
/moderation app/moderation/page.tsx Moderation queue — AI-flagged content pending review
/appeals app/appeals/page.tsx User appeals against violations
/reports app/reports/page.tsx User-submitted reports
/algorithm app/algorithm/page.tsx Feed algorithm & moderation threshold tuning
/categories app/categories/page.tsx Category management — create, edit, toggle sensitive
/system app/system/page.tsx System health, DB stats, audit log
/settings app/settings/page.tsx Admin session info, API URL override

Key Libraries

Library Source
api.ts src/lib/api.ts — Singleton API client with JWT token management
auth.tsx src/lib/auth.tsx — React context provider for auth state

API Client (src/lib/api.ts)

The ApiClient class provides typed methods for every admin API endpoint. Key behaviors:

  • Token management — Stored in localStorage as admin_token, attached as Bearer header
  • Auto-logout — Any 401 response clears token and redirects to /login
  • Base URL — Configurable via NEXT_PUBLIC_API_URL environment variable
  • Error handling — Throws Error with server-provided error message

Auth Context (src/lib/auth.tsx)

  • Wraps app in AuthProvider
  • On mount, validates existing token by calling getDashboardStats()
  • Provides login(), logout(), isAuthenticated, isLoading, user

Backend API Routes

All routes prefixed with /api/v1/admin/.

Authentication (no middleware)

Method Path Handler Description
POST /login AdminLogin Email/password login with Turnstile verification

Dashboard (requires auth + admin)

Method Path Handler Description
GET /dashboard GetDashboardStats User/post/moderation/appeal/report counts
GET /growth?days=30 GetGrowthStats Daily user & post creation counts for charts

User Management

Method Path Handler Description
GET /users?limit=50&offset=0&search=&status=&role= ListUsers Paginated user list with filters
GET /users/:id GetUser Full user profile with follower/post/violation counts
PATCH /users/:id/status UpdateUserStatus Set status: active, suspended, banned, deactivated
PATCH /users/:id/role UpdateUserRole Set role: user, moderator, admin
PATCH /users/:id/verification UpdateUserVerification Toggle is_official and is_verified flags
POST /users/:id/reset-strikes ResetUserStrikes Reset violation strike counter to 0

Post Management

Method Path Handler Description
GET /posts?limit=50&offset=0&search=&status=&author_id= ListPosts Paginated post list with filters
GET /posts/:id GetPost Full post detail with moderation flags
PATCH /posts/:id/status UpdatePostStatus Set status: active, flagged, removed
DELETE /posts/:id DeletePost Soft-delete (sets deleted_at + status removed)

Moderation Queue

Method Path Handler Description
GET /moderation?limit=50&offset=0&status=pending GetModerationQueue AI-flagged content awaiting review
PATCH /moderation/:id/review ReviewModerationFlag Actions: approve, dismiss, remove_content, ban_user

Appeals

Method Path Handler Description
GET /appeals?limit=50&offset=0&status=pending ListAppeals User appeals with violation details
PATCH /appeals/:id/review ReviewAppeal Decision: approved or rejected, optional content restore

Reports

Method Path Handler Description
GET /reports?limit=50&offset=0&status=pending ListReports User-submitted reports
PATCH /reports/:id UpdateReportStatus Set status: reviewed, dismissed, actioned

Algorithm & Feed Config

Method Path Handler Description
GET /algorithm GetAlgorithmConfig All key-value config pairs
PUT /algorithm UpdateAlgorithmConfig Upsert a config { key, value }

Categories

Method Path Handler Description
GET /categories ListCategories All content categories
POST /categories CreateCategory Create { slug, name, description?, is_sensitive? }
PATCH /categories/:id UpdateCategory Update name, description, or sensitive flag

System

Method Path Handler Description
GET /health GetSystemHealth DB ping, latency, connection pool stats, DB size
GET /audit-log?limit=50&offset=0 GetAuditLog Admin action history with actor handles

Database Schema

Tables Created by Admin Migration

algorithm_config — Key-value store for feed and moderation tuning:

Column Type Description
key TEXT PRIMARY KEY Config identifier
value TEXT NOT NULL Config value
description TEXT Human-readable description
updated_at TIMESTAMPTZ Last modification time

Default seed values:

Key Default Description
feed_recency_weight 0.4 Weight for post recency in feed ranking
feed_engagement_weight 0.3 Weight for engagement metrics
feed_harmony_weight 0.2 Weight for author harmony/trust score
feed_diversity_weight 0.1 Weight for content diversity
moderation_auto_flag_threshold 0.7 AI score threshold for auto-flagging
moderation_auto_remove_threshold 0.95 AI score threshold for auto-removal
moderation_greed_keyword_threshold 0.7 Spam/greed detection threshold
feed_max_posts_per_author 3 Max posts from same author per feed page
feed_boost_mutual_follow 1.5 Boost multiplier for mutual follows
feed_beacon_boost 1.2 Boost multiplier for beacon posts

audit_log — Admin action history:

Column Type Description
id UUID PRIMARY KEY Unique entry ID
actor_id UUID Admin who performed the action
action TEXT NOT NULL Action type (e.g., post_status_change, admin_delete_post)
target_type TEXT NOT NULL Entity type: user, post, comment, appeal, report, config
target_id UUID ID of the affected entity
details TEXT JSON string with action-specific metadata
created_at TIMESTAMPTZ Timestamp

Columns Ensured by Migration

The migration ensures these columns exist (added if missing):

Table Column Type Default
profiles role TEXT 'user'
profiles is_verified BOOLEAN FALSE
profiles is_private BOOLEAN FALSE
users status TEXT 'active'
users last_login TIMESTAMPTZ NULL

Pre-existing Tables Used by Admin

Table Admin Usage
users Login validation, status management, growth stats
profiles Role checks, user details, verification flags
posts Content listing, status changes, deletion
comments Moderation flag targets, appeal content restoration
moderation_flags Moderation queue — AI-generated flags with scores
user_violations Violation records linked to moderation flags
user_appeals Appeal records linked to violations
user_status_history Log of admin-initiated status changes
reports User-submitted reports of other users/content
categories Content categories managed by admins
follows Follower/following counts on user detail page

Feature Reference

Dashboard

Displays real-time aggregate stats:

  • Users: total, active, suspended, banned, new today
  • Posts: total, active, flagged, removed, new today
  • Moderation: pending flags, reviewed flags
  • Appeals: pending, approved, rejected
  • Reports: pending count
  • Growth charts: Daily user & post registrations (configurable 7/30/90 day window)

Moderation Workflow

  1. AI flagging — Posts/comments are automatically analyzed by the ModerationService using OpenAI + Google Vision
  2. Three Poisons Score — Content is scored on Hate, Greed, Delusion dimensions
  3. Auto-flag — Content exceeding moderation_auto_flag_threshold is flagged for review
  4. Admin review — Admin sees flagged content in the moderation queue with scores
  5. Actions available:
    • Approve — Content is fine, dismiss the flag
    • Dismiss — Same as approve (flag was a false positive)
    • Remove content — Soft-delete the post/comment
    • Ban user — Ban the author and action the flag

Appeal Workflow

  1. User receives a violation (triggered by moderation)
  2. User submits an appeal with reason and context
  3. Appeal appears in admin panel with violation details, AI scores, and original content
  4. Admin reviews and decides:
    • Approve — Optionally restore the removed content
    • Reject — Violation stands, include written reasoning (min 5 chars)

User Management Actions

  • Change status: activesuspendedbanneddeactivated (requires reason)
  • Change role: usermoderatoradmin
  • Toggle verification: is_official and is_verified badges
  • Reset strikes: Clear the violation counter

Environment Variables

Frontend (/opt/sojorn/admin/.env.local)

Variable Value Description
NEXT_PUBLIC_API_URL https://api.sojorn.net Go backend base URL
NEXT_PUBLIC_TURNSTILE_SITE_KEY 0x4AAAAAAC... Cloudflare Turnstile site key (invisible mode)

Backend (/opt/sojorn/.env)

The admin system uses these existing environment variables:

Variable Description
DATABASE_URL PostgreSQL connection string
JWT_SECRET Secret for signing/verifying JWT tokens
TURNSTILE_SECRET Cloudflare Turnstile server-side verification key
PORT API server port (default: 8080)

Troubleshooting

Admin login returns "Admin access required"

The user's profile doesn't have role = 'admin'. Fix:

UPDATE profiles SET role = 'admin' WHERE handle = 'your_handle';

Admin login returns "Invalid credentials"

  • Verify the email matches what's in the users table
  • Password is validated against encrypted_password via bcrypt
  • Check the user's status is active (not pending, suspended, or banned)

Admin login returns "Security verification failed"

Cloudflare Turnstile rejected the request. Possible causes:

  • TURNSTILE_SECRET in backend .env doesn't match the site key in frontend .env.local
  • The Turnstile widget hostname doesn't include admin.sojorn.net in Cloudflare dashboard
  • Bot or automated request without a valid Turnstile token

sojorn-admin service won't start

# Check logs
sudo journalctl -u sojorn-admin -n 50 --no-pager

# Check if port 3002 is in use
ss -tlnp | grep 3002

# If port is taken, find and kill the process
sudo fuser -k 3002/tcp
sudo systemctl restart sojorn-admin

sojorn-api panics on startup

Check for duplicate route registrations in cmd/api/main.go. The Go backend will panic if two routes resolve to the same path (e.g., legacy routes conflicting with admin group routes).

Frontend shows "Loading..." indefinitely

  • Check browser console for network errors
  • Verify NEXT_PUBLIC_API_URL in .env.local is correct and reachable
  • Ensure the Go API is running: sudo systemctl status sojorn-api
  • Check CORS — the backend must allow the admin domain in CORS_ORIGINS

Database migration errors

Run the migration manually:

export PGPASSWORD=your_db_password
psql -U postgres -h localhost -d sojorn -f /opt/sojorn/go-backend/internal/database/migrations/20260206000001_admin_panel_tables.up.sql

PM2 conflicts

Port 3001 is used by another PM2-managed site on this server. The admin panel uses port 3002 to avoid conflicts. Do not change it to 3001.