diff --git a/admin/docs/ADMIN_SYSTEM.md b/admin/docs/ADMIN_SYSTEM.md new file mode 100644 index 0000000..04225ef --- /dev/null +++ b/admin/docs/ADMIN_SYSTEM.md @@ -0,0 +1,542 @@ +# Sojorn Admin Panel — Comprehensive System Documentation + +> Last updated: February 6, 2026 + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Architecture](#architecture) +3. [Authentication & Security](#authentication--security) +4. [Server Deployment](#server-deployment) +5. [Frontend (Next.js)](#frontend-nextjs) +6. [Backend API Routes](#backend-api-routes) +7. [Database Schema](#database-schema) +8. [Feature Reference](#feature-reference) +9. [Environment Variables](#environment-variables) +10. [Troubleshooting](#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 + +```sql +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`**: +```ini +[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`**: +```nginx +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 + +```bash +# 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**: `active` ↔ `suspended` ↔ `banned` ↔ `deactivated` (requires reason) +- **Change role**: `user` ↔ `moderator` ↔ `admin` +- **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: +```sql +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 + +```bash +# 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: +```bash +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. diff --git a/admin/fix_service.sh b/admin/fix_service.sh new file mode 100644 index 0000000..7d6b493 --- /dev/null +++ b/admin/fix_service.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Fix and restart the sojorn-admin service +# Run as: sudo bash /opt/sojorn/admin/fix_service.sh + +# 1. Remove old service completely +systemctl stop sojorn-admin 2>/dev/null +systemctl disable sojorn-admin 2>/dev/null + +# 2. Kill ANY process on port 3001 +fuser -k 3001/tcp 2>/dev/null +sleep 2 +# Double check +fuser -k 3001/tcp 2>/dev/null +sleep 1 + +# 3. Write fresh service file with Restart=on-failure +cat > /etc/systemd/system/sojorn-admin.service <<'SVCEOF' +[Unit] +Description=Sojorn Admin Panel +After=network.target sojorn-api.service + +[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 3001 +Restart=on-failure +RestartSec=15 +StartLimitIntervalSec=60 +StartLimitBurst=3 +Environment=NODE_ENV=production +Environment=NEXT_PUBLIC_API_URL=https://api.sojorn.net + +[Install] +WantedBy=multi-user.target +SVCEOF + +# 4. Reload and start +systemctl daemon-reload +systemctl enable sojorn-admin +systemctl start sojorn-admin + +sleep 4 +systemctl status sojorn-admin --no-pager diff --git a/admin/setup_nginx.sh b/admin/setup_nginx.sh new file mode 100644 index 0000000..ed997d7 --- /dev/null +++ b/admin/setup_nginx.sh @@ -0,0 +1,28 @@ +#!/bin/bash +cat > /tmp/sojorn-admin.conf << 'EOF' +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; + } +} +EOF + +sudo cp /tmp/sojorn-admin.conf /etc/nginx/sites-available/sojorn-admin +sudo ln -sf /etc/nginx/sites-available/sojorn-admin /etc/nginx/sites-enabled/sojorn-admin +sudo nginx -t && sudo systemctl reload nginx +echo "--- Nginx status ---" +sudo systemctl status nginx --no-pager | head -5 +echo "--- Testing certbot ---" +sudo certbot --nginx -d admin.sojorn.net --non-interactive --agree-tos --redirect -m patrick@mp.ls +echo "--- Done ---" diff --git a/admin/setup_port3002.sh b/admin/setup_port3002.sh new file mode 100644 index 0000000..4b86ad6 --- /dev/null +++ b/admin/setup_port3002.sh @@ -0,0 +1,78 @@ +#!/bin/bash +set -e + +echo "=== Setting up Sojorn Admin on port 3002 ===" + +# Stop old service if running +systemctl stop sojorn-admin 2>/dev/null || true +systemctl disable sojorn-admin 2>/dev/null || true + +# Write service file for port 3002 +cat > /etc/systemd/system/sojorn-admin.service <<'SVCEOF' +[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 +SVCEOF + +systemctl daemon-reload +systemctl enable sojorn-admin +systemctl start sojorn-admin + +echo "Waiting 5s for startup..." +sleep 5 + +echo "" +echo "=== Service status ===" +systemctl status sojorn-admin --no-pager + +echo "" +echo "=== Port check ===" +ss -tlnp | grep 3002 + +echo "" +echo "=== Setting up Nginx ===" + +cat > /etc/nginx/sites-available/sojorn-admin <<'NGXEOF' +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; + } +} +NGXEOF + +if [ ! -L /etc/nginx/sites-enabled/sojorn-admin ]; then + ln -s /etc/nginx/sites-available/sojorn-admin /etc/nginx/sites-enabled/ +fi + +nginx -t && systemctl reload nginx +echo "Nginx configured for admin.sojorn.net -> port 3002" + +echo "" +echo "=== DONE ===" diff --git a/admin/setup_server.sh b/admin/setup_server.sh new file mode 100644 index 0000000..91315d4 --- /dev/null +++ b/admin/setup_server.sh @@ -0,0 +1,104 @@ +#!/bin/bash +set -e + +echo "=== Cleaning up all node processes on port 3001 ===" + +# Stop and fully remove any existing service +systemctl stop sojorn-admin 2>/dev/null || true +systemctl disable sojorn-admin 2>/dev/null || true + +# Kill ALL node processes related to the admin panel +pkill -9 -f "next start --port 3001" 2>/dev/null || true +pkill -9 -f "dist/server/entry.mjs" 2>/dev/null || true +sleep 2 + +# Double-kill anything left on port 3001 +fuser -k 3001/tcp 2>/dev/null || true +sleep 2 + +# Triple check +STILL_RUNNING=$(fuser 3001/tcp 2>/dev/null || true) +if [ -n "$STILL_RUNNING" ]; then + echo "Force killing PIDs: $STILL_RUNNING" + kill -9 $STILL_RUNNING 2>/dev/null || true + sleep 2 +fi + +echo "Port 3001 status:" +ss -tlnp | grep 3001 || echo "PORT IS FREE" + +echo "" +echo "=== Writing systemd service ===" + +cat > /etc/systemd/system/sojorn-admin.service <<'SVCEOF' +[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 3001 +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 +SVCEOF + +systemctl daemon-reload +systemctl enable sojorn-admin +systemctl start sojorn-admin + +echo "Waiting 5s for startup..." +sleep 5 + +echo "" +echo "=== Service status ===" +systemctl status sojorn-admin --no-pager + +echo "" +echo "=== Port check ===" +ss -tlnp | grep 3001 + +echo "" +echo "=== Setting up Nginx ===" + +cat > /etc/nginx/sites-available/sojorn-admin <<'NGXEOF' +server { + listen 80; + server_name admin.sojorn.net; + + location / { + proxy_pass http://127.0.0.1:3001; + 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; + } +} +NGXEOF + +if [ ! -L /etc/nginx/sites-enabled/sojorn-admin ]; then + ln -s /etc/nginx/sites-available/sojorn-admin /etc/nginx/sites-enabled/ +fi + +nginx -t && systemctl reload nginx +echo "Nginx configured and reloaded" + +echo "" +echo "=== Checking Go API service ===" +systemctl status sojorn-api --no-pager || true + +echo "" +echo "=== DONE ===" diff --git a/admin/sojorn-admin.nginx b/admin/sojorn-admin.nginx new file mode 100644 index 0000000..41d4454 --- /dev/null +++ b/admin/sojorn-admin.nginx @@ -0,0 +1,16 @@ +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; + } +} diff --git a/go-backend/scripts/add_jailed_status.sql b/go-backend/scripts/add_jailed_status.sql new file mode 100644 index 0000000..dfa8cdf --- /dev/null +++ b/go-backend/scripts/add_jailed_status.sql @@ -0,0 +1,14 @@ +-- Check if posts.status is an enum or text and add 'jailed' if needed +DO $$ +BEGIN + -- Check if post_status enum exists + IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'post_status') THEN + -- Add 'jailed' to post_status enum + BEGIN + ALTER TYPE post_status ADD VALUE IF NOT EXISTS 'jailed'; + EXCEPTION WHEN duplicate_object THEN NULL; + END; + END IF; +END $$; + +-- If posts.status is just text, no enum changes needed - 'jailed' will just work diff --git a/run_dev.ps1 b/run_dev.ps1 index 706edad..ca3c8cf 100644 --- a/run_dev.ps1 +++ b/run_dev.ps1 @@ -1,59 +1,55 @@ +# run_dev.ps1 — Run Sojorn on the default connected device (mobile/emulator) +# Usage: .\run_dev.ps1 [-EnvPath .env] param( [string]$EnvPath = (Join-Path $PSScriptRoot ".env") ) -if (-not (Test-Path $EnvPath)) { - Write-Error "Env file not found: ${EnvPath}" - exit 1 -} - -$values = @{} -Get-Content $EnvPath | ForEach-Object { - $line = $_.Trim() - if ($line.Length -eq 0) { return } - if ($line.StartsWith('#')) { return } - - $parts = $line -split '=', 2 - if ($parts.Count -lt 2) { return } - - $key = $parts[0].Trim() - $value = $parts[1].Trim() - - if ($value.StartsWith('"') -and $value.EndsWith('"')) { - $value = $value.Substring(1, $value.Length - 2) +function Parse-Env($path) { + $vals = @{} + if (-not (Test-Path $path)) { + Write-Host "No .env file found at ${path}. Using defaults." -ForegroundColor Yellow + $vals['API_BASE_URL'] = 'https://api.sojorn.net/api/v1' + return $vals } - - $values[$key] = $value + Get-Content $path | ForEach-Object { + $line = $_.Trim() + if ($line.Length -eq 0 -or $line.StartsWith('#')) { return } + $parts = $line -split '=', 2 + if ($parts.Count -lt 2) { return } + $key = $parts[0].Trim() + $value = $parts[1].Trim() + if ($value.StartsWith('"') -and $value.EndsWith('"')) { + $value = $value.Substring(1, $value.Length - 2) + } + $vals[$key] = $value + } + return $vals } -$required = @('API_BASE_URL') -$missing = $required | Where-Object { - -not $values.ContainsKey($_) -or [string]::IsNullOrWhiteSpace($values[$_]) -} +$values = Parse-Env $EnvPath -if ($missing.Count -gt 0) { - Write-Error "Missing required keys in ${EnvPath}: $($missing -join ', ')" - exit 1 -} - -$defineArgs = @( - "--dart-define=API_BASE_URL=$($values['API_BASE_URL'])" -) - -$optionalDefines = @( - 'FIREBASE_WEB_VAPID_KEY', - 'TURNSTILE_SITE_KEY' -) - -foreach ($opt in $optionalDefines) { - if ($values.ContainsKey($opt) -and -not [string]::IsNullOrWhiteSpace($values[$opt])) { - $defineArgs += "--dart-define=$opt=$($values[$opt])" +$defineArgs = @() +$keysOfInterest = @('API_BASE_URL', 'FIREBASE_WEB_VAPID_KEY', 'TURNSTILE_SITE_KEY') +foreach ($k in $keysOfInterest) { + if ($values.ContainsKey($k) -and -not [string]::IsNullOrWhiteSpace($values[$k])) { + $defineArgs += "--dart-define=$k=$($values[$k])" } } +if (-not $values.ContainsKey('API_BASE_URL') -or [string]::IsNullOrWhiteSpace($values['API_BASE_URL'])) { + $currentApi = 'https://api.sojorn.net/api/v1' + $defineArgs += "--dart-define=API_BASE_URL=$currentApi" +} else { + $currentApi = $values['API_BASE_URL'] +} + +Write-Host "Launching Sojorn (device)..." -ForegroundColor Cyan +Write-Host "API: $currentApi" + Push-Location (Join-Path $PSScriptRoot "sojorn_app") try { - flutter run @defineArgs @Args + Write-Host "Running: flutter run $($defineArgs -join ' ')" -ForegroundColor DarkGray + & flutter run @defineArgs @Args } finally { Pop-Location diff --git a/run_web.ps1 b/run_web.ps1 index e0e4960..b2406b4 100644 --- a/run_web.ps1 +++ b/run_web.ps1 @@ -1,15 +1,14 @@ +# run_web.ps1 — Run Sojorn in Edge browser +# Usage: .\run_web.ps1 [-Port 8001] [-EnvPath .env] param( [string]$EnvPath = (Join-Path $PSScriptRoot ".env"), - [int]$Port = 8001, - [string]$Renderer = "auto", # Options: auto, canvaskit, html - [switch]$NoWasmDryRun + [int]$Port = 8001 ) function Parse-Env($path) { $vals = @{} if (-not (Test-Path $path)) { Write-Host "No .env file found at ${path}. Using defaults." -ForegroundColor Yellow - # Set default API_BASE_URL since no .env exists $vals['API_BASE_URL'] = 'https://api.sojorn.net/api/v1' return $vals } @@ -30,7 +29,6 @@ function Parse-Env($path) { $values = Parse-Env $EnvPath -# Collect dart-defines we actually use on web. $defineArgs = @() $keysOfInterest = @('API_BASE_URL', 'FIREBASE_WEB_VAPID_KEY', 'TURNSTILE_SITE_KEY') foreach ($k in $keysOfInterest) { @@ -39,47 +37,21 @@ foreach ($k in $keysOfInterest) { } } -# Ensure API_BASE_URL is set if (-not $values.ContainsKey('API_BASE_URL') -or [string]::IsNullOrWhiteSpace($values['API_BASE_URL'])) { $currentApi = 'https://api.sojorn.net/api/v1' $defineArgs += "--dart-define=API_BASE_URL=$currentApi" -} -else { +} else { $currentApi = $values['API_BASE_URL'] - # Always ensure we're using the HTTPS endpoint - if ($currentApi.StartsWith('http://api.sojorn.net:8080')) { - $currentApi = $currentApi.Replace('http://api.sojorn.net:8080', 'https://api.sojorn.net') - $defineArgs = $defineArgs | Where-Object { -not ($_ -like '--dart-define=API_BASE_URL=*') } - $defineArgs += "--dart-define=API_BASE_URL=$currentApi" - } - elseif ($currentApi.StartsWith('http://localhost:')) { - # For local development, keep localhost but warn - Write-Host "Using local API: $currentApi" -ForegroundColor Yellow - } } -Write-Host "Launching Sojorn Web..." -ForegroundColor Cyan -Write-Host "Port: $Port" -Write-Host "Renderer: $Renderer" -Write-Host "API: $currentApi" +Write-Host "Launching Sojorn Web (Edge)..." -ForegroundColor Cyan +Write-Host "Port: $Port | API: $currentApi" Push-Location (Join-Path $PSScriptRoot -ChildPath "sojorn_app") try { - $cmdArgs = @( - 'run', - '-d', - 'edge', - '--web-hostname', - 'localhost', - '--web-port', - "$Port" - ) + $cmdArgs = @('run', '-d', 'edge', '--web-hostname', 'localhost', '--web-port', "$Port") $cmdArgs += $defineArgs - if ($NoWasmDryRun) { - $cmdArgs += '--no-wasm-dry-run' - } - Write-Host "Running: flutter $($cmdArgs -join ' ')" -ForegroundColor DarkGray & flutter $cmdArgs } diff --git a/run_web_chrome.ps1 b/run_web_chrome.ps1 index 9a31a5a..1e8002b 100644 --- a/run_web_chrome.ps1 +++ b/run_web_chrome.ps1 @@ -1,15 +1,14 @@ +# run_web_chrome.ps1 — Run Sojorn in Chrome browser +# Usage: .\run_web_chrome.ps1 [-Port 8002] [-EnvPath .env] param( [string]$EnvPath = (Join-Path $PSScriptRoot ".env"), - [int]$Port = 8002, - [string]$Renderer = "auto", # Options: auto, canvaskit, html - [switch]$NoWasmDryRun + [int]$Port = 8002 ) function Parse-Env($path) { $vals = @{} if (-not (Test-Path $path)) { Write-Host "No .env file found at ${path}. Using defaults." -ForegroundColor Yellow - # Set default API_BASE_URL since no .env exists $vals['API_BASE_URL'] = 'https://api.sojorn.net/api/v1' return $vals } @@ -30,7 +29,6 @@ function Parse-Env($path) { $values = Parse-Env $EnvPath -# Collect dart-defines we actually use on web. $defineArgs = @() $keysOfInterest = @('API_BASE_URL', 'FIREBASE_WEB_VAPID_KEY', 'TURNSTILE_SITE_KEY') foreach ($k in $keysOfInterest) { @@ -39,47 +37,21 @@ foreach ($k in $keysOfInterest) { } } -# Ensure API_BASE_URL is set if (-not $values.ContainsKey('API_BASE_URL') -or [string]::IsNullOrWhiteSpace($values['API_BASE_URL'])) { $currentApi = 'https://api.sojorn.net/api/v1' $defineArgs += "--dart-define=API_BASE_URL=$currentApi" -} -else { +} else { $currentApi = $values['API_BASE_URL'] - # Always ensure we're using the HTTPS endpoint - if ($currentApi.StartsWith('http://api.sojorn.net:8080')) { - $currentApi = $currentApi.Replace('http://api.sojorn.net:8080', 'https://api.sojorn.net') - $defineArgs = $defineArgs | Where-Object { -not ($_ -like '--dart-define=API_BASE_URL=*') } - $defineArgs += "--dart-define=API_BASE_URL=$currentApi" - } - elseif ($currentApi.StartsWith('http://localhost:')) { - # For local development, keep localhost but warn - Write-Host "Using local API: $currentApi" -ForegroundColor Yellow - } } Write-Host "Launching Sojorn Web (Chrome)..." -ForegroundColor Cyan -Write-Host "Port: $Port" -Write-Host "Renderer: $Renderer" -Write-Host "API: $currentApi" +Write-Host "Port: $Port | API: $currentApi" Push-Location (Join-Path $PSScriptRoot -ChildPath "sojorn_app") try { - $cmdArgs = @( - 'run', - '-d', - 'chrome', - '--web-hostname', - 'localhost', - '--web-port', - "$Port" - ) + $cmdArgs = @('run', '-d', 'chrome', '--web-hostname', 'localhost', '--web-port', "$Port") $cmdArgs += $defineArgs - if ($NoWasmDryRun) { - $cmdArgs += '--no-wasm-dry-run' - } - Write-Host "Running: flutter $($cmdArgs -join ' ')" -ForegroundColor DarkGray & flutter $cmdArgs } diff --git a/sojorn_app/lib/providers/quip_upload_provider.dart b/sojorn_app/lib/providers/quip_upload_provider.dart index 3374f8c..2fe8634 100644 --- a/sojorn_app/lib/providers/quip_upload_provider.dart +++ b/sojorn_app/lib/providers/quip_upload_provider.dart @@ -87,7 +87,6 @@ class QuipUploadNotifier extends Notifier { videoFile, onProgress: (p) => state = state.copyWith(progress: 0.1 + (p * 0.4)), ); - print('Video uploaded successfully: $videoUrl'); state = state.copyWith(progress: 0.5); @@ -99,9 +98,7 @@ class QuipUploadNotifier extends Notifier { thumbnail, onProgress: (p) => state = state.copyWith(progress: 0.5 + (p * 0.3)), ); - print('Thumbnail uploaded: $thumbnailUrl'); } catch (e) { - print('Thumbnail upload failed: $e'); // Continue without thumbnail - video is more important } diff --git a/sojorn_app/lib/screens/admin/quip_repair_screen.dart b/sojorn_app/lib/screens/admin/quip_repair_screen.dart index 5fa8e9c..3fdf548 100644 --- a/sojorn_app/lib/screens/admin/quip_repair_screen.dart +++ b/sojorn_app/lib/screens/admin/quip_repair_screen.dart @@ -87,7 +87,6 @@ class _QuipRepairScreenState extends ConsumerState { if (!ReturnCode.isSuccess(returnCode)) { final logs = await session.getAllLogsAsString(); // Print in chunks if it's too long for some logcats - debugPrint("FFmpeg Full Log:\n$logs"); // Extract the last error message from logs if possible String errorDetail = "FFmpeg failed (Code: $returnCode)"; @@ -114,7 +113,6 @@ class _QuipRepairScreenState extends ConsumerState { } } catch (e) { - print('Repair error details: $e'); // Log for debugging if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Repair Failed: $e'))); } diff --git a/sojorn_app/lib/screens/auth/sign_in_screen.dart b/sojorn_app/lib/screens/auth/sign_in_screen.dart index 8b9600b..d0e42da 100644 --- a/sojorn_app/lib/screens/auth/sign_in_screen.dart +++ b/sojorn_app/lib/screens/auth/sign_in_screen.dart @@ -70,7 +70,6 @@ class _SignInScreenState extends ConsumerState { }); } } catch (e) { - debugPrint('[Biometrics] Failed to check support: $e'); } } diff --git a/sojorn_app/lib/screens/discover/discover_screen.dart b/sojorn_app/lib/screens/discover/discover_screen.dart index 3d5e72d..1d81a6a 100644 --- a/sojorn_app/lib/screens/discover/discover_screen.dart +++ b/sojorn_app/lib/screens/discover/discover_screen.dart @@ -120,7 +120,6 @@ class _DiscoverScreenState extends ConsumerState { isLoadingDiscover = false; }); } catch (e) { - debugPrint('Error loading discover data: $e'); if (mounted) { setState(() => isLoadingDiscover = false); } @@ -137,7 +136,6 @@ class _DiscoverScreenState extends ConsumerState { .toList(); }); } catch (e) { - debugPrint('Error loading recent searches: $e'); } } @@ -156,7 +154,6 @@ class _DiscoverScreenState extends ConsumerState { if (mounted) setState(() {}); } catch (e) { - debugPrint('Error saving recent search: $e'); } } @@ -168,7 +165,6 @@ class _DiscoverScreenState extends ConsumerState { recentSearches = []; }); } catch (e) { - debugPrint('Error clearing recent searches: $e'); } } diff --git a/sojorn_app/lib/screens/profile/blocked_users_screen.dart b/sojorn_app/lib/screens/profile/blocked_users_screen.dart index eebcbad..f2aece6 100644 --- a/sojorn_app/lib/screens/profile/blocked_users_screen.dart +++ b/sojorn_app/lib/screens/profile/blocked_users_screen.dart @@ -130,7 +130,6 @@ class _BlockedUsersScreenState extends ConsumerState { await apiService.callGoApi('/users/block_by_handle', method: 'POST', body: {'handle': handle}); count++; } catch (e) { - debugPrint('Failed to block $handle: $e'); } } diff --git a/sojorn_app/lib/screens/quips/create/quip_recorder_screen.dart b/sojorn_app/lib/screens/quips/create/quip_recorder_screen.dart index cbe9b0b..fe63813 100644 --- a/sojorn_app/lib/screens/quips/create/quip_recorder_screen.dart +++ b/sojorn_app/lib/screens/quips/create/quip_recorder_screen.dart @@ -103,7 +103,6 @@ class _QuipRecorderScreenState extends State if (mounted) setState(() => _isInitializing = false); } catch (e) { - debugPrint('Camera init error: $e'); } } @@ -154,7 +153,6 @@ class _QuipRecorderScreenState extends State } }); } catch (e) { - debugPrint("Start Error: $e"); } } @@ -175,7 +173,6 @@ class _QuipRecorderScreenState extends State // NO Auto-Navigation. Just stop. } catch (e) { - debugPrint("Stop Error: $e"); } } @@ -382,4 +379,4 @@ class _QuipRecorderScreenState extends State ), ); } -} \ No newline at end of file +} diff --git a/sojorn_app/lib/screens/quips/feed/quips_feed_screen.dart b/sojorn_app/lib/screens/quips/feed/quips_feed_screen.dart index 0fcd652..da6221e 100644 --- a/sojorn_app/lib/screens/quips/feed/quips_feed_screen.dart +++ b/sojorn_app/lib/screens/quips/feed/quips_feed_screen.dart @@ -249,29 +249,22 @@ class _QuipsFeedScreenState extends ConsumerState // If we have an initialPostId, ensure it's at the top // If we have an initialPostId, ensure it's at the top if (refresh && widget.initialPostId != null) { - print('[Quips] Handling initialPostId: ${widget.initialPostId}'); final existingIndex = items.indexWhere((q) => q.id == widget.initialPostId); if (existingIndex != -1) { - print('[Quips] Found initialPostId in feed at index $existingIndex, moving to top'); final initial = items.removeAt(existingIndex); items.insert(0, initial); } else { - print('[Quips] initialPostId NOT in feed, fetching specifically...'); try { final postData = await api.callGoApi('/posts/${widget.initialPostId}', method: 'GET'); if (postData['post'] != null) { final quip = Quip.fromMap(postData['post'] as Map); if (quip.videoUrl.isNotEmpty) { - print('[Quips] Successfully fetched initial quip: ${quip.videoUrl}'); items.insert(0, quip); } else { - print('[Quips] Fetched post is not a video: ${quip.videoUrl}'); } } else { - print('[Quips] No post found for initialPostId: ${widget.initialPostId}'); } } catch (e) { - print('Initial quip fetch error: $e'); } } } diff --git a/sojorn_app/lib/screens/search/search_screen.dart b/sojorn_app/lib/screens/search/search_screen.dart index 262b2a0..a321aa0 100644 --- a/sojorn_app/lib/screens/search/search_screen.dart +++ b/sojorn_app/lib/screens/search/search_screen.dart @@ -105,7 +105,6 @@ class _SearchScreenState extends ConsumerState { .toList(); }); } catch (e) { - print('Error loading recent searches: $e'); } } @@ -124,7 +123,6 @@ class _SearchScreenState extends ConsumerState { if (mounted) setState(() {}); } catch (e) { - print('Error saving recent search: $e'); } } @@ -136,7 +134,6 @@ class _SearchScreenState extends ConsumerState { recentSearches = []; }); } catch (e) { - print('Error clearing recent searches: $e'); } } @@ -171,15 +168,12 @@ class _SearchScreenState extends ConsumerState { }); try { - print('[SearchScreen] Requesting search for: "$normalizedQuery"'); final apiService = ref.read(apiServiceProvider); final searchResults = await apiService.search(normalizedQuery); if (!mounted || requestId != _searchEpoch) { - print('[SearchScreen] Request $requestId discarded (stale)'); return; } - print('[SearchScreen] Results received. Users: ${searchResults.users.length}, Tags: ${searchResults.tags.length}, Posts: ${searchResults.posts.length}'); if (searchResults.users.isNotEmpty) { await saveRecentSearch(RecentSearch( @@ -203,7 +197,6 @@ class _SearchScreenState extends ConsumerState { isLoading = false; }); } catch (e) { - print('[SearchScreen] Search error: $e'); if (!mounted || requestId != _searchEpoch) return; setState(() { isLoading = false; diff --git a/sojorn_app/lib/screens/secure_chat/secure_chat_full_screen.dart b/sojorn_app/lib/screens/secure_chat/secure_chat_full_screen.dart index 958742b..d1a1c98 100644 --- a/sojorn_app/lib/screens/secure_chat/secure_chat_full_screen.dart +++ b/sojorn_app/lib/screens/secure_chat/secure_chat_full_screen.dart @@ -40,7 +40,6 @@ class _SecureChatFullScreenState extends State { // Also trigger immediate reload on conversation changes _chatService.conversationListChanges.listen((_) { if (mounted) { - print('[UI] Conversation list changed in full screen - stream will update'); setState(() {}); // Force rebuild to get latest from stream } }); @@ -56,7 +55,6 @@ class _SecureChatFullScreenState extends State { }); try { - print('[UI] Initializing secure chat full screen...'); // Initialize chat service with key generation if needed await _chatService.initialize(generateIfMissing: true); @@ -64,9 +62,7 @@ class _SecureChatFullScreenState extends State { // Load conversations await _loadConversations(); - print('[UI] Secure chat full screen initialized successfully'); } catch (e) { - print('[UI] Failed to initialize secure chat: $e'); setState(() { _error = e.toString(); }); @@ -80,7 +76,6 @@ class _SecureChatFullScreenState extends State { Future _loadConversations() async { try { - print('[UI] Loading conversations...'); final conversations = await _chatService.getConversations(); if (mounted) { @@ -88,10 +83,8 @@ class _SecureChatFullScreenState extends State { _conversations = conversations; _error = null; }); - print('[UI] Loaded ${conversations.length} conversations'); } } catch (e) { - print('[UI] Failed to load conversations: $e'); if (mounted) { setState(() { _error = e.toString(); diff --git a/sojorn_app/lib/screens/secure_chat/secure_chat_screen.dart b/sojorn_app/lib/screens/secure_chat/secure_chat_screen.dart index 0d72de9..2b06f33 100644 --- a/sojorn_app/lib/screens/secure_chat/secure_chat_screen.dart +++ b/sojorn_app/lib/screens/secure_chat/secure_chat_screen.dart @@ -56,7 +56,6 @@ class _SecureChatScreenState extends State @override void initState() { super.initState(); - print('[DEBUG] SecureChatScreen initState - UPLOAD BUTTON SHOULD BE VISIBLE'); WidgetsBinding.instance.addObserver(this); _messageStream = _chatService.getMessagesStream(widget.conversation.id); NotificationService.instance.activeConversationId = widget.conversation.id; diff --git a/sojorn_app/lib/services/ad_integration_service.dart b/sojorn_app/lib/services/ad_integration_service.dart index d179745..4866c91 100644 --- a/sojorn_app/lib/services/ad_integration_service.dart +++ b/sojorn_app/lib/services/ad_integration_service.dart @@ -33,18 +33,14 @@ class AdIntegrationService { return ad; } if (_currentAd != null && _currentAd!.matchesCategory(categoryId)) { - debugPrint('AdIntegrationService: using cached ad for $categoryId'); return _currentAd; } - debugPrint( 'AdIntegrationService: no sponsored post available for $categoryId'); return null; } catch (e) { if (_currentAd != null && _currentAd!.matchesCategory(categoryId)) { - debugPrint('AdIntegrationService: fetch failed, using cached ad: $e'); return _currentAd; } - debugPrint('AdIntegrationService: fetch failed, no fallback ad: $e'); return null; } } @@ -97,7 +93,6 @@ extension ListAdExtension on List { final activeAd = ad ?? fallbackAd; if (activeAd == null) { - debugPrint('AdIntegrationService: no ad available to interleave'); return [...this]; } diff --git a/sojorn_app/lib/services/api_service.dart b/sojorn_app/lib/services/api_service.dart index 9db39fe..b71540d 100644 --- a/sojorn_app/lib/services/api_service.dart +++ b/sojorn_app/lib/services/api_service.dart @@ -580,8 +580,6 @@ class ApiService { } if (kDebugMode) { - print('[Post] Publishing: body=$body, video=$videoUrl, thumb=$thumbnailUrl'); - print('[Post] Sanitized: video=$sanitizedVideoUrl, thumb=$sanitizedThumbnailUrl'); } final data = await _callGoApi( @@ -974,9 +972,6 @@ class ApiService { return SearchResults.fromJson(data); } catch (e, stack) { if (kDebugMode) { - print('[API] Search failed for query: "$query"'); - print('Error: $e'); - print('Stack: $stack'); } // Return empty results on error return SearchResults(users: [], tags: [], posts: []); diff --git a/sojorn_app/lib/services/image_upload_service.dart b/sojorn_app/lib/services/image_upload_service.dart index 49ff31d..9d36d20 100644 --- a/sojorn_app/lib/services/image_upload_service.dart +++ b/sojorn_app/lib/services/image_upload_service.dart @@ -103,7 +103,6 @@ class ImageUploadService { request.fields['fileName'] = videoFile.path.split('/').last; onProgress?.call(0.1); - print('Starting streamed video upload for: ${videoFile.path} ($fileLength bytes)'); try { final streamedResponse = await request.send(); @@ -112,7 +111,6 @@ class ImageUploadService { onProgress?.call(1.0); if (response.statusCode != 200) { - print('Upload error: ${response.body}'); final errorData = jsonDecode(response.body) as Map; throw UploadException(errorData['message'] ?? 'Upload failed'); } @@ -180,7 +178,6 @@ class ImageUploadService { onProgress?.call(0.2); - print('Starting direct upload for: $fileName (${fileBytes.length} bytes)'); return _uploadBytes( fileBytes: fileBytes, @@ -224,7 +221,6 @@ class ImageUploadService { } onProgress?.call(0.2); - print('Starting direct upload for: $safeName (${fileBytes.length} bytes)'); return _uploadBytes( fileBytes: fileBytes, @@ -469,14 +465,12 @@ class ImageUploadService { onProgress?.call(0.3); - print('Uploading image via R2 bridge...'); final streamedResponse = await request.send(); final response = await http.Response.fromStream(streamedResponse); onProgress?.call(0.9); if (response.statusCode != 200) { - print('Upload error: ${response.body}'); final errorData = jsonDecode(response.body) as Map; final errorMsg = errorData['error'] ?? 'Unknown error'; throw UploadException('Upload failed: $errorMsg'); @@ -486,14 +480,11 @@ class ImageUploadService { final signedUrl = responseData['signedUrl'] ?? responseData['signed_url']; final publicUrl = (signedUrl ?? responseData['publicUrl']) as String; - print('Upload successful! Public URL: $publicUrl'); onProgress?.call(1.0); // FORCE FIX: Ensure custom domain is used even if backend returns raw R2 URL return _fixR2Url(publicUrl); } catch (e, stack) { - print('Upload Service Error: $e'); - print('Stack trace: $stack'); throw UploadException(e.toString()); } } diff --git a/sojorn_app/lib/services/local_key_backup_service.dart b/sojorn_app/lib/services/local_key_backup_service.dart index c0f9c6c..fb8a1cb 100644 --- a/sojorn_app/lib/services/local_key_backup_service.dart +++ b/sojorn_app/lib/services/local_key_backup_service.dart @@ -37,7 +37,6 @@ class LocalKeyBackupService { bool includeMessages = true, }) async { try { - print('[BACKUP] Creating encrypted backup (keys: $includeKeys, msgs: $includeMessages)...'); // 1. Export keys (if requested) Map? keyData; @@ -48,7 +47,6 @@ class LocalKeyBackupService { // 1b. Export messages if requested List>? messageData; if (includeMessages) { - print('[BACKUP] Exporting messages...'); final messages = await LocalMessageStore.instance.getAllMessageRecords(); messageData = messages.map((m) => { 'conversationId': m.conversationId, @@ -61,7 +59,6 @@ class LocalKeyBackupService { 'readAt': m.readAt?.toIso8601String(), 'expiresAt': m.expiresAt?.toIso8601String(), }).toList(); - print('[BACKUP] Exported ${messages.length} messages'); } final payloadData = { @@ -107,11 +104,9 @@ class LocalKeyBackupService { }, }; - print('[BACKUP] Backup created successfully (${secretBox.cipherText.length} bytes)'); return backup; } catch (e) { - print('[BACKUP] Failed to create backup: $e'); rethrow; } } @@ -123,7 +118,6 @@ class LocalKeyBackupService { required SimpleE2EEService e2eeService, }) async { try { - print('[BACKUP] Restoring from backup...'); // 1. Validate backup format _validateBackupFormat(backup); @@ -157,7 +151,6 @@ class LocalKeyBackupService { int restoredMessages = 0; if (payloadData is Map && payloadData.containsKey('messages')) { final messages = (payloadData['messages'] as List).cast>(); - print('[BACKUP] Restoring ${messages.length} messages...'); for (final m in messages) { await LocalMessageStore.instance.saveMessageRecord(LocalMessageRecord( @@ -175,7 +168,6 @@ class LocalKeyBackupService { restoredMessages = messages.length; } - print('[BACKUP] Backup restored successfully'); return { 'success': true, 'restored_keys': keyData != null ? (keyData['keys']?.length ?? 0) : 0, @@ -184,7 +176,6 @@ class LocalKeyBackupService { }; } catch (e) { - print('[BACKUP] Failed to restore backup: $e'); if (e is ArgumentError && e.message.contains('MAC')) { throw Exception('Invalid password or corrupted backup file'); } @@ -195,7 +186,6 @@ class LocalKeyBackupService { /// Save backup to device file static Future saveBackupToDevice(Map backup) async { try { - print('[BACKUP] Saving backup to device...'); // Web implementation - download file if (kIsWeb) { @@ -215,7 +205,6 @@ class LocalKeyBackupService { html.document.body?.children.remove(anchor); html.Url.revokeObjectUrl(url); - print('[BACKUP] Backup downloaded to browser: $fileName'); return fileName; } @@ -245,11 +234,9 @@ class LocalKeyBackupService { final backupJson = const JsonEncoder.withIndent(' ').convert(backup); await file.writeAsString(backupJson); - print('[BACKUP] Backup saved to: ${file.path}'); return file.path; } catch (e) { - print('[BACKUP] Failed to save backup: $e'); rethrow; } } @@ -257,7 +244,6 @@ class LocalKeyBackupService { /// Load backup from device file static Future> loadBackupFromDevice() async { try { - print('[BACKUP] Loading backup from device...'); // Web implementation - file upload if (kIsWeb) { @@ -308,7 +294,6 @@ class LocalKeyBackupService { // Wait for file to be processed final backup = await completer.future; - print('[BACKUP] Backup loaded from browser upload'); return backup; } @@ -336,11 +321,9 @@ class LocalKeyBackupService { final content = await file.readAsString(); final backup = jsonDecode(content) as Map; - print('[BACKUP] Backup loaded from: ${file.path}'); return backup; } catch (e) { - print('[BACKUP] Failed to load backup: $e'); rethrow; } } @@ -407,7 +390,6 @@ class LocalKeyBackupService { static Future uploadToCloud({ required Map backup, }) async { - print('[BACKUP] Uploading to cloud...'); // Get device name String deviceName = 'Unknown Device'; @@ -432,7 +414,6 @@ class LocalKeyBackupService { deviceName: deviceName, version: 1, // Currently hardcoded version ); - print('[BACKUP] Upload successful'); } /// Restore from cloud backup @@ -441,7 +422,6 @@ class LocalKeyBackupService { required SimpleE2EEService e2eeService, String? backupId, }) async { - print('[BACKUP] Downloading from cloud...'); final backupData = await ApiService.instance.downloadBackup(backupId); if (backupData == null) { diff --git a/sojorn_app/lib/services/local_message_store.dart b/sojorn_app/lib/services/local_message_store.dart index 8327e6b..081517e 100644 --- a/sojorn_app/lib/services/local_message_store.dart +++ b/sojorn_app/lib/services/local_message_store.dart @@ -75,7 +75,6 @@ class LocalMessageStore { return _cachedKey!; } } catch (e) { - print('[LOCAL_STORE] Warning: Could not read encryption key: $e'); } final bytes = _generateRandomBytes(32); @@ -91,7 +90,6 @@ class LocalMessageStore { return _cachedKey!; } catch (e) { if (attempt == _maxRetries - 1) { - print( '[LOCAL_STORE] Failed to store encryption key after $_maxRetries attempts: $e'); } await Future.delayed(_retryDelay * (attempt + 1)); @@ -189,7 +187,6 @@ class LocalMessageStore { success = true; break; } catch (e) { - print( '[LOCAL_STORE] Save attempt ${attempt + 1}/$_maxRetries failed for $messageId: $e'); if (attempt < _maxRetries - 1) { await Future.delayed(_retryDelay * (attempt + 1)); @@ -243,7 +240,6 @@ class LocalMessageStore { if (version >= 2 && storedHash != null) { final computedHash = _computeHash(plaintext); if (computedHash != storedHash) { - print('[LOCAL_STORE] Integrity check failed for $messageId'); return pending?.plaintext; } } @@ -251,11 +247,9 @@ class LocalMessageStore { return plaintext; } catch (e) { if (e.toString().contains('SecretBoxAuthenticationError')) { - print('[LOCAL_STORE] MAC mismatch for $messageId - record is corrupt or key changed. Deleting.'); await _messageBox!.delete(messageId); return pending?.plaintext; } - print( '[LOCAL_STORE] Read attempt ${attempt + 1}/$_maxRetries failed for $messageId: $e'); if (attempt < _maxRetries - 1) { await Future.delayed(_retryDelay * (attempt + 1)); @@ -304,7 +298,6 @@ class LocalMessageStore { } } } catch (e) { - print('[LOCAL_STORE] Batch load failed for conversation $conversationId: $e'); } // Include any pending saves from WAL @@ -360,7 +353,6 @@ class LocalMessageStore { } } } catch (e) { - print('[LOCAL_STORE] Batch record load failed for $conversationId: $e'); } // Include any pending saves from WAL @@ -396,7 +388,6 @@ class LocalMessageStore { results.addAll(messages); } } catch (e) { - print('[LOCAL_STORE] Failed to get all messages: $e'); } return results; } @@ -427,7 +418,6 @@ class LocalMessageStore { try { return await _getConversationIndex(conversationId); } catch (e) { - print('[LOCAL_STORE] Failed to get message IDs for $conversationId: $e'); return []; } } @@ -463,7 +453,6 @@ class LocalMessageStore { return true; } catch (e) { - print( '[LOCAL_STORE] Delete attempt ${attempt + 1}/$_maxRetries failed for $messageId: $e'); if (attempt < _maxRetries - 1) { await Future.delayed(_retryDelay * (attempt + 1)); @@ -508,7 +497,6 @@ class LocalMessageStore { return true; } catch (e) { - print('[LOCAL_STORE] Failed to delete conversation $conversationId: $e'); return false; } } @@ -531,7 +519,6 @@ class LocalMessageStore { expiresAt: pending.expiresAt, ); if (success) { - print('[LOCAL_STORE] Flushed pending write for ${entry.key}'); } } } @@ -620,7 +607,6 @@ class LocalMessageStore { if (version >= 2 && storedHash != null) { final computedHash = _computeHash(plaintext); if (computedHash != storedHash) { - print('[LOCAL_STORE] Integrity check failed for $messageId'); return null; } } @@ -648,11 +634,9 @@ class LocalMessageStore { ); } catch (e) { if (e.toString().contains('SecretBoxAuthenticationError')) { - print('[LOCAL_STORE] MAC mismatch for record $messageId - deleting corrupt record.'); _messageBox!.delete(messageId); return null; } - print('[LOCAL_STORE] Failed to parse record $messageId: $e'); return null; } } diff --git a/sojorn_app/lib/services/secure_chat_service.dart b/sojorn_app/lib/services/secure_chat_service.dart index f26e042..2175e86 100644 --- a/sojorn_app/lib/services/secure_chat_service.dart +++ b/sojorn_app/lib/services/secure_chat_service.dart @@ -73,22 +73,18 @@ class SecureChatService { }; try { _wsChannel!.sink.add(jsonEncode(keyRecoveryEvent)); - print('[WS] Key recovery event broadcasted for user: $userId'); } catch (e) { - print('[WS] Failed to broadcast key recovery: $e'); } } } // Force reset to fix 208-bit key bug Future forceResetBrokenKeys() async { - print('[CHAT] Force resetting broken keys to fix 208-bit bug'); await _e2ee.forceResetBrokenKeys(); } // Manual key upload for testing Future uploadKeysManually() async { - print('[CHAT] Manual key upload requested'); await _e2ee.uploadKeysManually(); } @@ -110,7 +106,6 @@ class SecureChatService { final wsUrl = Uri.parse(ApiConfig.baseUrl) .replace(scheme: ApiConfig.baseUrl.startsWith('https') ? 'wss' : 'ws', path: '/ws', queryParameters: {'token': token}); - print('[WS] Connecting to $wsUrl'); _isReconnecting = true; try { @@ -130,7 +125,6 @@ class SecureChatService { return; // Silently ignore } - print('[WS] Received: ${jsonEncode(data)}'); if (type == 'new_message') { final payload = data['payload']; @@ -143,7 +137,6 @@ class SecureChatService { final messageId = payload['message_id']; final conversationId = payload['conversation_id']; if (messageId != null && conversationId != null) { - print('[WS] IMMEDIATE DELETE: $messageId'); _locallyDeletedMessageIds.add(messageId); unawaited(_localStore.deleteMessage(messageId)); _processedMessageIds[conversationId]?.remove(messageId); @@ -156,7 +149,6 @@ class SecureChatService { final payload = data['payload']; final conversationId = payload['conversation_id']; if (conversationId != null) { - print('[WS] CONVERSATION DELETED: $conversationId'); unawaited(_localStore.deleteConversation(conversationId)); _processedMessageIds.remove(conversationId); _localControllers[conversationId]?.close(); @@ -169,7 +161,6 @@ class SecureChatService { final userId = payload['user_id']; final currentUserId = _auth.currentUser?.id; if (userId != null && currentUserId != null && userId == currentUserId) { - print('[WS] KEY RECOVERY EVENT RECEIVED - triggering local recovery'); unawaited(_e2ee.initiateKeyRecovery(currentUserId)); } } else if (data['type'] == 'pong') { @@ -177,20 +168,16 @@ class SecureChatService { _lastHeartbeat = DateTime.now(); } } catch (e) { - print('[WS] Parse error: $e'); } } }, onError: (e) { - print('[WS] ERROR - IMMEDIATE RECONNECT: $e'); _cleanup(); Future.delayed(const Duration(seconds: 1), connectRealtime); }, onDone: () { - print('[WS] DISCONNECTED - IMMEDIATE RECONNECT'); _cleanup(); Future.delayed(const Duration(seconds: 1), connectRealtime); }); } catch (e) { - print('[WS] Connection failed: $e'); _isReconnecting = false; Future.delayed(const Duration(seconds: 2), connectRealtime); } @@ -299,7 +286,6 @@ class SecureChatService { unawaited(_emitLocal(conversationId)); return msg; } catch (e) { - print('[CHAT] Failed to send message: $e'); return null; } } @@ -341,11 +327,8 @@ class SecureChatService { if (forEveryone) { unawaited(_api.deleteMessage(messageId).then((success) { if (!success) { - print('[CHAT] Server delete failed - message already removed locally'); } }).catchError((e) { - print('[CHAT] Error deleting message from server: $e'); - print('[CHAT] Server delete error: $e'); })); } @@ -359,11 +342,9 @@ class SecureChatService { final messages = await _localStore.getMessagesForConversation(conversationId); if (messages.isEmpty) { - print('[CHAT] Conversation $conversationId is empty - deleting'); await deleteConversation(conversationId, fullDelete: true); } } catch (e) { - print('[CHAT] Error checking empty conversation: $e'); } } @@ -371,7 +352,6 @@ class SecureChatService { String conversationId, { bool fullDelete = false, }) async { - print('[CHAT] Deleting conversation: $conversationId (fullDelete: $fullDelete)'); // Clear local state IMMEDIATELY _processedMessageIds.remove(conversationId); @@ -391,12 +371,9 @@ class SecureChatService { if (fullDelete) { unawaited(_api.deleteConversation(conversationId).then((success) { if (success) { - print('[CHAT] Conversation permanently deleted from server'); } else { - print('[CHAT] Server delete failed - already removed locally'); } }).catchError((e) { - print('[CHAT] Error deleting conversation from server: $e'); })); } return DeleteResult(success: true); @@ -430,7 +407,6 @@ class SecureChatService { final rows = await _api.getConversationMessages(conversationId); await _ingestRemoteSnapshot(conversationId, rows); } catch (e) { - print('[SYNC] Failed to sync $conversationId: $e'); } } @@ -440,7 +416,6 @@ class SecureChatService { final rows = await _api.getConversationMessages(conversationId, limit: limit); await _ingestRemoteSnapshot(conversationId, rows); } catch (e) { - print('[SYNC] Failed to hydrate $conversationId: $e'); } } @@ -449,7 +424,6 @@ class SecureChatService { await _e2ee.initialize(); if (!_e2ee.isReady) { - print('[CHAT] Not ready to decrypt.'); return; } @@ -511,7 +485,6 @@ class SecureChatService { _processedMessageIds.putIfAbsent(conversationId, () => {}).add(msg.id); } catch (e) { - print('[DECRYPT] Failed for ${msg.id}: $e'); if (e.toString().contains('Invalid Key') || e.toString().contains('MAC')) { await _localStore.saveMessage( conversationId: conversationId, diff --git a/sojorn_app/lib/services/simple_e2ee_service.dart b/sojorn_app/lib/services/simple_e2ee_service.dart index 9a9a871..f883d8c 100644 --- a/sojorn_app/lib/services/simple_e2ee_service.dart +++ b/sojorn_app/lib/services/simple_e2ee_service.dart @@ -77,7 +77,6 @@ class SimpleE2EEService { // DO NOT add debug flags here - use resetAllKeys() method for intentional resets Future resetAllKeys() async { - print('[E2EE] RESETTING ALL KEYS - fixing MAC errors'); // Clear all storage await _storage.deleteAll(); @@ -91,12 +90,10 @@ class SimpleE2EEService { // Generate fresh identity await generateNewIdentity(); - print('[E2EE] Keys reset complete - fresh identity generated'); } // Force reset to fix 208-bit key bug Future forceResetBrokenKeys() async { - print('[E2EE] FORCE RESET - Clearing all broken 208-bit keys'); // Clear ALL storage completely await _storage.deleteAll(); @@ -112,7 +109,6 @@ class SimpleE2EEService { // Clear session cache _sessionCache.clear(); - print('[E2EE] All keys cleared - generating fresh 256-bit keys'); // Generate fresh identity with proper key lengths await generateNewIdentity(); @@ -120,20 +116,16 @@ class SimpleE2EEService { // Verify the new keys are proper length if (_identityDhKeyPair != null) { final publicKey = await _identityDhKeyPair!.extractPublicKey(); - print('[E2EE] New identity DH key: ${publicKey.bytes.length} bytes'); } if (_identitySigningKeyPair != null) { final publicKey = await _identitySigningKeyPair!.extractPublicKey(); - print('[E2EE] New identity signing key: ${publicKey.bytes.length} bytes'); } - print('[E2EE] FORCE RESET complete - all keys now 256-bit'); } // Manual key upload for testing Future uploadKeysManually() async { - print('[E2EE] Manual key upload requested'); if (!isReady) { throw Exception('Keys not ready - generate keys first'); @@ -154,7 +146,6 @@ class SimpleE2EEService { } await _publishKeys(spkSignature); - print('[E2EE] Manual key upload completed'); } // Check if keys exist on backend @@ -167,21 +158,17 @@ class SimpleE2EEService { // If we get a successful response with key data, keys exist if (response.containsKey('identity_key')) { - print('[E2EE] Keys found on backend'); return true; } else { - print('[E2EE] Keys not found on backend'); return false; } } catch (e) { - print('[E2EE] Error checking backend keys: $e'); return false; } } // Upload existing keys to backend Future _uploadExistingKeys() async { - print('[E2EE] Uploading existing keys to backend...'); if (!isReady) { throw Exception('Keys not ready for upload'); @@ -196,7 +183,6 @@ class SimpleE2EEService { final spkSignature = signature.bytes; await _publishKeys(spkSignature); - print('[E2EE] Existing keys uploaded to backend'); } Future _doInitialize(String userId) async { @@ -208,50 +194,41 @@ class SimpleE2EEService { try { final loaded = await _loadKeysFromLocal(userId); if (loaded) { - print('[E2EE] Keys loaded from local storage.'); // Test if keys are working by attempting a simple encrypt/decrypt if (await _testKeyCompatibility()) { // Check if keys exist on backend, upload if not if (await _checkKeysExistOnBackend()) { final backendValid = await _validateBackendKeyBundle(userId); if (!backendValid) { - print('[E2EE] Backend key bundle failed signature verification - reuploading'); await _uploadExistingKeys(); return; } - print('[E2EE] Keys exist on backend - ready'); return; } else { - print('[E2EE] Keys missing on backend - uploading now'); await _uploadExistingKeys(); return; } } else { - print('[E2EE] Keys failed compatibility test - initiating recovery'); await initiateKeyRecovery(userId); return; } } } catch (e) { - print('[E2EE] Error loading keys: $e. Regenerating.'); } // 2. Try Cloud Restore final restored = await _restoreFromCloud(userId); if (restored) { - print('[E2EE] Keys restored from cloud backup.'); // Test restored keys if (await _testKeyCompatibility()) { return; } else { - print('[E2EE] Restored keys failed compatibility test - initiating recovery'); await initiateKeyRecovery(userId); return; } } // 3. Generate New - print('[E2EE] No keys found. Generating new Identity.'); await generateNewIdentity(); } @@ -274,7 +251,6 @@ class SimpleE2EEService { // Verify key length if (testKeyBytes.length != 32) { - print('[E2EE] ERROR: Test key is ${testKeyBytes.length} bytes, expected 32'); return false; } @@ -290,10 +266,8 @@ class SimpleE2EEService { ); final result = utf8.decode(decrypted) == testMessage; - print('[E2EE] Local encryption test: $result (key length: ${testKeyBytes.length} bytes)'); return result; } catch (e) { - print('[E2EE] Key compatibility test failed: $e'); } return false; } @@ -337,14 +311,12 @@ class SimpleE2EEService { return verified; } catch (e) { - print('[E2EE] Backend key bundle validation failed: $e'); return false; } } // Smart key recovery that preserves messages when possible Future initiateKeyRecovery(String userId) async { - print('[E2EE] Starting smart key recovery...'); // Try to preserve existing messages by backing up encrypted content final messageBackup = await _backupEncryptedMessages(); @@ -354,12 +326,10 @@ class SimpleE2EEService { // Restore message backup with new keys if possible if (messageBackup > 0) { - print('[E2EE] Attempting to preserve $messageBackup messages with new keys'); // Note: Messages encrypted with old keys will show as "encrypted with old keys" // but new messages will work perfectly } - print('[E2EE] Key recovery complete - new messages will encrypt/decrypt properly'); } // Backup encrypted messages to preserve them during key recovery @@ -367,10 +337,8 @@ class SimpleE2EEService { try { // This would integrate with local message store to count/preserve messages // For now, just log that we're attempting preservation - print('[E2EE] Backing up encrypted messages for preservation...'); return 0; // Return count of backed up messages } catch (e) { - print('[E2EE] Error backing up messages: $e'); return 0; } } @@ -379,7 +347,6 @@ class SimpleE2EEService { final userId = _auth.currentUser?.id; if (userId == null) return; - print('[E2EE] Generating X3DH Key Bundle...'); // 1. Identity Key Pair (DH) _identityDhKeyPair = await _dhAlgo.newKeyPair(); @@ -421,13 +388,11 @@ class SimpleE2EEService { if (!_auth.isAuthenticated) throw Exception('Not authenticated'); await initialize(); - print('[ENCRYPT] Fetching key bundle for recipient: $recipientId'); // 1. Fetch Bundle final bundle = await ApiService(AuthService.instance).getKeyBundle(recipientId); // DEBUG: Validate Bundle - print('[ENCRYPT] Bundle keys: ${bundle.keys.toList()}'); // Handle both formats: // Flat (from getKeyBundle normalization): { "identity_key_public": "...", "signed_prekey_public": "...", "signed_prekey_signature": "..." } @@ -468,8 +433,6 @@ class SimpleE2EEService { otkId = bundle['one_time_prekey_id']; } - print('[ENCRYPT] IK: $ikField'); - print('[ENCRYPT] SPK: $spkField'); if (ikField == null || ikField.isEmpty) { throw Exception('Recipient identity_key not found in bundle. Structure: $bundle'); @@ -522,7 +485,6 @@ class SimpleE2EEService { if (!isVerified) { throw Exception('E2EE SECURITY ALERT: Recipient Signed PreKey signature verification failed!'); } - print('[E2EE] SPK signature verified successfully.'); final theirIk = SimplePublicKey(theirIkDhBytes, type: KeyPairType.x25519); final theirSpk = SimplePublicKey(theirSpkBytes, type: KeyPairType.x25519); @@ -626,9 +588,7 @@ class SimpleE2EEService { final matchingOtk = _oneTimePreKeys![otkId]; final dh4 = await _dhAlgo.sharedSecretKey(keyPair: matchingOtk, remotePublicKey: senderEk); dhBytes.addAll(await dh4.extractBytes()); - print('[DECRYPT] Used OTK with key_id: $otkId'); } else { - print('[DECRYPT] WARNING: OTK key_id $otkId out of range (have ${_oneTimePreKeys!.length} OTKs)'); } } @@ -639,7 +599,6 @@ class SimpleE2EEService { // Decryption successful - plaintext not logged for security return plaintext; } catch (e) { - print('[DECRYPT] Failed: $e'); if (e.toString().contains('MAC') || e.toString().contains('SecretBoxAuthenticationError')) { // Automatic key recovery on MAC errors _handleMacError(); @@ -661,11 +620,9 @@ class SimpleE2EEService { _macErrorCount++; _lastMacErrorTime = DateTime.now(); - print('[E2EE] MAC error #$_macErrorCount detected'); // If we get multiple MAC errors in quick succession, trigger recovery if (_macErrorCount >= _maxMacErrors) { - print('[E2EE] Multiple MAC errors detected - triggering automatic key recovery'); _triggerAutomaticRecovery(); _macErrorCount = 0; // Reset counter } @@ -675,10 +632,8 @@ class SimpleE2EEService { final userId = _auth.currentUser?.id; if (userId == null) return; - print('[E2EE] AUTOMATIC KEY RECOVERY TRIGGERED'); // Show user-friendly notification - print('[E2EE] User notification: "Fixing encryption issues..."'); // Initiate smart recovery await initiateKeyRecovery(userId); @@ -686,27 +641,22 @@ class SimpleE2EEService { // Broadcast key recovery event to all user's devices _broadcastKeyRecovery(userId); - print('[E2EE] Automatic recovery complete - new messages will work properly'); } void _broadcastKeyRecovery(String userId) { // Broadcast key recovery event to all user's devices via WebSocket _chatService?.broadcastKeyRecovery(userId); - print('[E2EE] Broadcasting key recovery to all devices for user: $userId'); } // Delete used OTK from server to prevent reuse Future _deleteUsedOTK(int keyId) async { try { await _api.callGoApi('/keys/otk/$keyId', method: 'DELETE'); - print('[E2EE] Deleted used OTK #$keyId from server'); } catch (e) { final message = e.toString(); if (message.contains('route not found') || message.contains('404')) { - print('[E2EE] OTK delete endpoint missing on backend; skipping cleanup for #$keyId'); return; } - print('[E2EE] Error deleting OTK #$keyId: $e'); } } @@ -721,7 +671,6 @@ class SimpleE2EEService { } Future _publishKeys(List spkSignature) async { - print('[E2EE] Publishing key bundle to backend...'); try { final skPublic = await _identitySigningKeyPair!.extractPublicKey(); @@ -740,7 +689,6 @@ class SimpleE2EEService { }); } - print('[E2EE] Uploading ${otks.length} OTKs to backend'); // Verify signature is not all zeros before upload final allZeros = spkSignature.every((b) => b == 0); @@ -757,9 +705,7 @@ class SimpleE2EEService { oneTimePrekeys: otks, ); - print('[E2EE] Key bundle uploaded successfully to backend'); } catch (e) { - print('[E2EE] FAILED to upload key bundle: $e'); rethrow; } } @@ -784,27 +730,21 @@ class SimpleE2EEService { if (kIsWeb) { final prefs = await SharedPreferences.getInstance(); await prefs.setString('e2ee_keys_$userId', data); - print('[E2EE] Keys also saved to SharedPreferences (web fallback)'); } } Future _loadKeysFromLocal(String userId) async { - print('[E2EE] Attempting to load keys for user: $userId'); // Try FlutterSecureStorage first var data = await _storage.read(key: 'e2ee_keys_$userId'); - print('[E2EE] FlutterSecureStorage read result: ${data != null ? "found (${data.length} chars)" : "null"}'); // Fallback to SharedPreferences on web if secure storage fails if (data == null && kIsWeb) { - print('[E2EE] Trying SharedPreferences fallback for web...'); final prefs = await SharedPreferences.getInstance(); data = prefs.getString('e2ee_keys_$userId'); - print('[E2EE] SharedPreferences read result: ${data != null ? "found (${data.length} chars)" : "null"}'); } if (data == null) { - print('[E2EE] No keys found in any storage'); return false; } @@ -829,7 +769,6 @@ class SimpleE2EEService { for (final otkSeed in map['otks']) { _oneTimePreKeys!.add(await _dhAlgo.newKeyPairFromSeed(base64Decode(otkSeed))); } - print('[E2EE] Loaded ${_oneTimePreKeys!.length} OTKs from local storage'); } return isReady; @@ -914,7 +853,6 @@ class SimpleE2EEService { return isReady; } catch (e) { - print('Restore failed: $e'); return false; } } @@ -925,7 +863,6 @@ class SimpleE2EEService { } try { - print('[E2EE] Exporting all keys for backup...'); final identityDhPublic = await _identityDhKeyPair!.extractPublicKey(); final identitySigningPublic = await _identitySigningKeyPair!.extractPublicKey(); @@ -968,18 +905,15 @@ class SimpleE2EEService { }, }; - print('[E2EE] Keys exported successfully'); return exportData; } catch (e) { - print('[E2EE] Failed to export keys: $e'); rethrow; } } Future importAllKeys(Map backupData) async { try { - print('[E2EE] Importing keys from backup...'); if (!backupData.containsKey('keys')) { throw ArgumentError('Invalid backup format: missing keys'); @@ -989,18 +923,15 @@ class SimpleE2EEService { // 1. Restore Identity Keys if (keys.containsKey('identity_dh_private')) { - print('[E2EE] Restoring Identity DH key...'); _identityDhKeyPair = await _dhAlgo.newKeyPairFromSeed(base64Decode(keys['identity_dh_private'])); } if (keys.containsKey('identity_signing_private')) { - print('[E2EE] Restoring Identity Signing key...'); _identitySigningKeyPair = await _signingAlgo.newKeyPairFromSeed(base64Decode(keys['identity_signing_private'])); } // 2. Restore Signed PreKey if (keys.containsKey('signed_prekey_private')) { - print('[E2EE] Restoring Signed PreKey...'); _signedPreKey = await _dhAlgo.newKeyPairFromSeed(base64Decode(keys['signed_prekey_private'])); } @@ -1014,7 +945,6 @@ class SimpleE2EEService { } } _oneTimePreKeys = importedOTKs; - print('[E2EE] Restored ${_oneTimePreKeys!.length} OTKs'); } // 4. Set User Context from metadata @@ -1032,7 +962,6 @@ class SimpleE2EEService { // 5. Persist and Synchronize if (_initializedForUserId != null) { - print('[E2EE] Persisting restored keys to local storage...'); await _saveKeysToLocal(_initializedForUserId!); // Republish to server to ensure backend is synchronized @@ -1047,10 +976,8 @@ class SimpleE2EEService { } } - print('[E2EE] Backup restoration complete. Old messages can now be decrypted.'); } catch (e) { - print('[E2EE] CRITICAL: Failed to import keys: $e'); rethrow; } } diff --git a/sojorn_app/lib/services/sync_manager.dart b/sojorn_app/lib/services/sync_manager.dart index 1e9b270..e780ac2 100644 --- a/sojorn_app/lib/services/sync_manager.dart +++ b/sojorn_app/lib/services/sync_manager.dart @@ -104,11 +104,9 @@ class SyncManager with WidgetsBindingObserver { if (!_authService.isAuthenticated || _syncInProgress) return; _syncInProgress = true; try { - print('[SYNC] Triggered by: $reason'); await _e2ee.initialize(); if (!_e2ee.isReady) { - print('[SYNC] Keys not ready, aborting sync.'); return; } @@ -116,9 +114,7 @@ class SyncManager with WidgetsBindingObserver { await _secureChatService.syncAllConversations(force: true); _lastSyncAt = DateTime.now(); - print('[SYNC] Sync complete.'); } catch (e) { - print('[SYNC] Global sync failed ($reason): $e'); } finally { _syncInProgress = false; } @@ -130,7 +126,6 @@ class SyncManager with WidgetsBindingObserver { try { await _e2ee.initialize(); if (!_e2ee.isReady) { - print('[SYNC] Identity not ready; skipping hydration.'); return; } @@ -139,13 +134,11 @@ class SyncManager with WidgetsBindingObserver { final isEmpty = (await _localStore.getMessageIdsForConversation(conv.id)).isEmpty; if (isEmpty) { - print('[SYNC] Hydrating empty conversation: ${conv.id}'); await _secureChatService.fetchAndDecryptHistory(conv.id, limit: 50); } await _secureChatService.startLiveListener(conv.id); } } catch (e) { - print('[SYNC] History load failed: $e'); } finally { _hydrating = false; } diff --git a/sojorn_app/lib/services/video_stitching_service.dart b/sojorn_app/lib/services/video_stitching_service.dart index 1366cdf..cf157a2 100644 --- a/sojorn_app/lib/services/video_stitching_service.dart +++ b/sojorn_app/lib/services/video_stitching_service.dart @@ -41,16 +41,13 @@ class VideoStitchingService { if (ReturnCode.isSuccess(returnCode)) { return outputFile; } else { - print("Stitching failed with return code: $returnCode"); // Fallback: return the last segment or first one to at least save something? // For strict correctness, return null or throw. // Let's print logs. final logs = await session.getOutput(); - print("FFmpeg Logs: $logs"); return null; } } catch (e) { - print("Stitching Error: $e"); return null; } } diff --git a/sojorn_app/lib/widgets/post/post_media.dart b/sojorn_app/lib/widgets/post/post_media.dart index 19322dd..bf153e5 100644 --- a/sojorn_app/lib/widgets/post/post_media.dart +++ b/sojorn_app/lib/widgets/post/post_media.dart @@ -68,7 +68,6 @@ class PostMedia extends StatelessWidget { onTap: isVideo ? () { final url = '${AppRoutes.quips}?postId=${post!.id}'; - print('[PostMedia] Navigating to quips: $url'); context.go(url); } : onTap, diff --git a/sojorn_app/lib/widgets/reactions/reaction_picker.dart b/sojorn_app/lib/widgets/reactions/reaction_picker.dart index 40e9d58..0bbf7a8 100644 --- a/sojorn_app/lib/widgets/reactions/reaction_picker.dart +++ b/sojorn_app/lib/widgets/reactions/reaction_picker.dart @@ -133,7 +133,6 @@ class _ReactionPickerState extends State with SingleTickerProvid }); } } catch (e) { - print('[REACTIONS] Error scanning assets: $e'); // Fallback if (mounted) { setState(() { diff --git a/sojorn_app/lib/widgets/video_player_widget.dart b/sojorn_app/lib/widgets/video_player_widget.dart index 8720e25..e13c16c 100644 --- a/sojorn_app/lib/widgets/video_player_widget.dart +++ b/sojorn_app/lib/widgets/video_player_widget.dart @@ -58,7 +58,6 @@ class _VideoPlayerWidgetState extends State { }); } } catch (e) { - print('Error initializing video player: $e'); } } diff --git a/sojorn_app/lib/widgets/video_player_with_comments.dart b/sojorn_app/lib/widgets/video_player_with_comments.dart index dc0d863..dd72c65 100644 --- a/sojorn_app/lib/widgets/video_player_with_comments.dart +++ b/sojorn_app/lib/widgets/video_player_with_comments.dart @@ -64,7 +64,6 @@ class _VideoPlayerWithCommentsState extends State { setState(() {}); } catch (e) { - print('Error initializing video: $e'); } }