Clean up: fix run scripts, remove 190+ debug print statements from 22 files, keep only FCM debugPrints for active notification work

This commit is contained in:
Patrick Britton 2026-02-06 14:13:03 -06:00
parent b14e1fbfa3
commit 46566f394b
33 changed files with 880 additions and 318 deletions

542
admin/docs/ADMIN_SYSTEM.md Normal file
View file

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

45
admin/fix_service.sh Normal file
View file

@ -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

28
admin/setup_nginx.sh Normal file
View file

@ -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 ---"

78
admin/setup_port3002.sh Normal file
View file

@ -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 ==="

104
admin/setup_server.sh Normal file
View file

@ -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 ==="

16
admin/sojorn-admin.nginx Normal file
View file

@ -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;
}
}

View file

@ -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

View file

@ -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 {
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
}
Get-Content $path | ForEach-Object {
$line = $_.Trim()
if ($line.Length -eq 0) { return }
if ($line.StartsWith('#')) { return }
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)
}
$values[$key] = $value
$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

View file

@ -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
}

View file

@ -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
}

View file

@ -87,7 +87,6 @@ class QuipUploadNotifier extends Notifier<QuipUploadState> {
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<QuipUploadState> {
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
}

View file

@ -87,7 +87,6 @@ class _QuipRepairScreenState extends ConsumerState<QuipRepairScreen> {
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<QuipRepairScreen> {
}
} catch (e) {
print('Repair error details: $e'); // Log for debugging
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Repair Failed: $e')));
}

View file

@ -70,7 +70,6 @@ class _SignInScreenState extends ConsumerState<SignInScreen> {
});
}
} catch (e) {
debugPrint('[Biometrics] Failed to check support: $e');
}
}

View file

@ -120,7 +120,6 @@ class _DiscoverScreenState extends ConsumerState<DiscoverScreen> {
isLoadingDiscover = false;
});
} catch (e) {
debugPrint('Error loading discover data: $e');
if (mounted) {
setState(() => isLoadingDiscover = false);
}
@ -137,7 +136,6 @@ class _DiscoverScreenState extends ConsumerState<DiscoverScreen> {
.toList();
});
} catch (e) {
debugPrint('Error loading recent searches: $e');
}
}
@ -156,7 +154,6 @@ class _DiscoverScreenState extends ConsumerState<DiscoverScreen> {
if (mounted) setState(() {});
} catch (e) {
debugPrint('Error saving recent search: $e');
}
}
@ -168,7 +165,6 @@ class _DiscoverScreenState extends ConsumerState<DiscoverScreen> {
recentSearches = [];
});
} catch (e) {
debugPrint('Error clearing recent searches: $e');
}
}

View file

@ -130,7 +130,6 @@ class _BlockedUsersScreenState extends ConsumerState<BlockedUsersScreen> {
await apiService.callGoApi('/users/block_by_handle', method: 'POST', body: {'handle': handle});
count++;
} catch (e) {
debugPrint('Failed to block $handle: $e');
}
}

View file

@ -103,7 +103,6 @@ class _QuipRecorderScreenState extends State<QuipRecorderScreen>
if (mounted) setState(() => _isInitializing = false);
} catch (e) {
debugPrint('Camera init error: $e');
}
}
@ -154,7 +153,6 @@ class _QuipRecorderScreenState extends State<QuipRecorderScreen>
}
});
} catch (e) {
debugPrint("Start Error: $e");
}
}
@ -175,7 +173,6 @@ class _QuipRecorderScreenState extends State<QuipRecorderScreen>
// NO Auto-Navigation. Just stop.
} catch (e) {
debugPrint("Stop Error: $e");
}
}

View file

@ -249,29 +249,22 @@ class _QuipsFeedScreenState extends ConsumerState<QuipsFeedScreen>
// 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<String, dynamic>);
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');
}
}
}

View file

@ -105,7 +105,6 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
.toList();
});
} catch (e) {
print('Error loading recent searches: $e');
}
}
@ -124,7 +123,6 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
if (mounted) setState(() {});
} catch (e) {
print('Error saving recent search: $e');
}
}
@ -136,7 +134,6 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
recentSearches = [];
});
} catch (e) {
print('Error clearing recent searches: $e');
}
}
@ -171,15 +168,12 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
});
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<SearchScreen> {
isLoading = false;
});
} catch (e) {
print('[SearchScreen] Search error: $e');
if (!mounted || requestId != _searchEpoch) return;
setState(() {
isLoading = false;

View file

@ -40,7 +40,6 @@ class _SecureChatFullScreenState extends State<SecureChatFullScreen> {
// 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<SecureChatFullScreen> {
});
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<SecureChatFullScreen> {
// 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<SecureChatFullScreen> {
Future<void> _loadConversations() async {
try {
print('[UI] Loading conversations...');
final conversations = await _chatService.getConversations();
if (mounted) {
@ -88,10 +83,8 @@ class _SecureChatFullScreenState extends State<SecureChatFullScreen> {
_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();

View file

@ -56,7 +56,6 @@ class _SecureChatScreenState extends State<SecureChatScreen>
@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;

View file

@ -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<Post> {
final activeAd = ad ?? fallbackAd;
if (activeAd == null) {
debugPrint('AdIntegrationService: no ad available to interleave');
return [...this];
}

View file

@ -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: []);

View file

@ -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<String, dynamic>;
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<String, dynamic>;
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());
}
}

View file

@ -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<String, dynamic>? keyData;
@ -48,7 +47,6 @@ class LocalKeyBackupService {
// 1b. Export messages if requested
List<Map<String, dynamic>>? 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<Map<String, dynamic>>();
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<String> saveBackupToDevice(Map<String, dynamic> 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<Map<String, dynamic>> 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<String, dynamic>;
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<void> uploadToCloud({
required Map<String, dynamic> 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) {

View file

@ -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 <String>[];
}
}
@ -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;
}
}

View file

@ -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<void> forceResetBrokenKeys() async {
print('[CHAT] Force resetting broken keys to fix 208-bit bug');
await _e2ee.forceResetBrokenKeys();
}
// Manual key upload for testing
Future<void> 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, () => <String>{}).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,

View file

@ -77,7 +77,6 @@ class SimpleE2EEService {
// DO NOT add debug flags here - use resetAllKeys() method for intentional resets
Future<void> 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<void> 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<void> 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<void> _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<void> _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<void> 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<void> _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<void> _publishKeys(List<int> 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<bool> _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<void> importAllKeys(Map<String, dynamic> 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;
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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,

View file

@ -133,7 +133,6 @@ class _ReactionPickerState extends State<ReactionPicker> with SingleTickerProvid
});
}
} catch (e) {
print('[REACTIONS] Error scanning assets: $e');
// Fallback
if (mounted) {
setState(() {

View file

@ -58,7 +58,6 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
});
}
} catch (e) {
print('Error initializing video player: $e');
}
}

View file

@ -64,7 +64,6 @@ class _VideoPlayerWithCommentsState extends State<VideoPlayerWithComments> {
setState(() {});
} catch (e) {
print('Error initializing video: $e');
}
}