Docker hardening + first-run fixes
This commit is contained in:
+9
-4
@@ -30,8 +30,8 @@ WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Install tools needed for port detection (netcat)
|
||||
RUN apk add --no-cache netcat-openbsd
|
||||
# Install tools needed for port detection and healthchecks
|
||||
RUN apk add --no-cache netcat-openbsd wget
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
@@ -41,6 +41,11 @@ RUN adduser --system --uid 1001 nextjs
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/drizzle ./drizzle
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/drizzle.config.ts ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/src/db/schema.ts ./src/db/schema.ts
|
||||
|
||||
# Copy and set up entrypoint script
|
||||
COPY --chown=nextjs:nodejs scripts/docker-entrypoint.sh /usr/local/bin/
|
||||
@@ -59,9 +64,9 @@ EXPOSE 3000-3020
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
# Health check (will use the actual PORT at runtime)
|
||||
# Health check (reads dynamic port file when PORT=auto)
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget -q --spider http://localhost:${PORT:-3000}/api/health || exit 1
|
||||
CMD sh -c 'PORT_FILE=/var/run/synapsis/port; PORT=3000; if [ -f "$PORT_FILE" ]; then PORT=$(cat "$PORT_FILE"); fi; case "$PORT" in ""|*[!0-9]*) PORT=3000;; esac; wget -q --spider "http://127.0.0.1:${PORT}/api/health" || exit 1'
|
||||
|
||||
# Use entrypoint script for port detection
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Your node. Your network. Your identity.**
|
||||
|
||||
Synapsis is an open-source, federated social network built for the decentralized web. Run your own node, own your identity with DIDs, and communicate across the Swarm network with end-to-end encryption. No corporations. No lock-in. Just you and the network.
|
||||
Synapsis is an open-source, federated social network built for the decentralized web. Run your own node, own your identity with DIDs, and communicate across the Swarm network with plain text server-stored messaging. No corporations. No lock-in. Just you and the network.
|
||||
|
||||
---
|
||||
|
||||
@@ -15,19 +15,22 @@ Run your personal Synapsis node with a single command:
|
||||
mkdir -p /opt/synapsis && cd /opt/synapsis
|
||||
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker-compose.yml
|
||||
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/Caddyfile
|
||||
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/caddy-entrypoint.sh
|
||||
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/.env.example
|
||||
cp .env.example .env
|
||||
|
||||
# 2. Edit .env with your domain and secrets (takes 2 minutes)
|
||||
nano .env
|
||||
# Use a host-only domain (no scheme), e.g. synapsis.example.com
|
||||
|
||||
# 3. Start your node
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Done! Your node is live at `https://your-domain.com` with automatic SSL. No build step. No dependencies. No fuss.
|
||||
Database migrations run automatically on startup and during updates.
|
||||
|
||||
**Updating:**
|
||||
**Updating (migrations run automatically):**
|
||||
```bash
|
||||
docker compose pull && docker compose up -d
|
||||
```
|
||||
@@ -39,7 +42,7 @@ For detailed Docker setup, see [docker/README.md](docker/README.md).
|
||||
## ✨ Features
|
||||
|
||||
- **🌐 Swarm Network** — Native peer-to-peer network with automatic node discovery and gossip protocol
|
||||
- **💬 Swarm Chat** — End-to-end encrypted messaging across the entire network
|
||||
- **💬 Swarm Chat** — Direct messaging across the entire network
|
||||
- **🔐 Decentralized Identity (DIDs)** — Cryptographic identity you truly own, portable between nodes
|
||||
- **🤖 AI Bots** — Create AI-powered bot accounts with custom personalities
|
||||
- **🎨 Modern UI** — Clean, responsive interface inspired by Vercel's design system
|
||||
@@ -83,7 +86,7 @@ Synapsis operates on the **Swarm** — a native peer-to-peer network designed sp
|
||||
|
||||
- **Gossip Protocol** — Nodes discover each other automatically and exchange information
|
||||
- **Swarm Timeline** — Aggregated feed of posts from across all Synapsis nodes
|
||||
- **Swarm Chat** — End-to-end encrypted direct messaging between users on any Synapsis node
|
||||
- **Swarm Chat** — Direct messaging between users on any Synapsis node
|
||||
- **Handle Registry** — Distributed directory of user handles across the swarm
|
||||
- **Instant Interactions** — Likes, reposts, follows, and mentions delivered in real-time
|
||||
|
||||
@@ -94,7 +97,7 @@ Synapsis operates on the **Swarm** — a native peer-to-peer network designed sp
|
||||
| **Identity** | Server-bound (`@user@server`) | DID-based (cryptographic, portable) |
|
||||
| **Account Migration** | Limited (followers don't auto-migrate) | **Full** — DID-based migration with auto-follow |
|
||||
| **Cryptographic Signing** | HTTP Signatures only | Full post signing with user keys |
|
||||
| **Direct Messages** | Posts with limited visibility | True E2E encrypted chat |
|
||||
| **Direct Messages** | Posts with limited visibility | Direct messaging between users |
|
||||
| **Network Discovery** | Manual server discovery | Automatic gossip protocol |
|
||||
| **AI Bots** | Not supported | Native bot framework with LLM integration |
|
||||
| **Interactions** | Queue-based, delayed | Instant delivery via Swarm |
|
||||
@@ -108,7 +111,7 @@ Want to hack on Synapsis? Here's how to run it locally:
|
||||
### Prerequisites
|
||||
- Node.js 20+
|
||||
- PostgreSQL 15+
|
||||
- S3-compatible storage (or MinIO for local dev)
|
||||
- S3-compatible storage (AWS S3, Cloudflare R2, Backblaze B2, Wasabi, or Contabo)
|
||||
|
||||
### Local Setup
|
||||
|
||||
|
||||
+6
-3
@@ -6,7 +6,7 @@
|
||||
# mkdir -p /opt/synapsis && cd /opt/synapsis
|
||||
# curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker-compose.yml
|
||||
# curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/Caddyfile
|
||||
# curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/scripts/caddy-entrypoint.sh
|
||||
# curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/caddy-entrypoint.sh
|
||||
# curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/.env.example
|
||||
# cp .env.example .env
|
||||
# 2. Configure (edit with your domain and secrets): nano .env
|
||||
@@ -61,7 +61,10 @@ services:
|
||||
AUTH_SECRET: ${AUTH_SECRET}
|
||||
|
||||
# Domain configuration
|
||||
NEXT_PUBLIC_NODE_DOMAIN: ${DOMAIN:-localhost}
|
||||
DOMAIN: ${DOMAIN:-localhost}
|
||||
NEXT_PUBLIC_NODE_DOMAIN: ${NEXT_PUBLIC_NODE_DOMAIN:-}
|
||||
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-}
|
||||
ALLOW_LOCALHOST: ${ALLOW_LOCALHOST:-}
|
||||
|
||||
# Admin emails
|
||||
ADMIN_EMAILS: ${ADMIN_EMAILS}
|
||||
@@ -89,7 +92,7 @@ services:
|
||||
condition: service_healthy
|
||||
# Increased start period to allow migrations to run
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/api/health"]
|
||||
test: ["CMD-SHELL", "PORT=$(cat /var/run/synapsis/port 2>/dev/null || echo 3000); case \"$PORT\" in ''|*[!0-9]*) PORT=3000;; esac; wget -q --spider http://127.0.0.1:${PORT}/api/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
+10
-1
@@ -7,9 +7,15 @@
|
||||
# REQUIRED: Core Settings
|
||||
# ===========================================
|
||||
|
||||
# Your domain name (e.g., synapsis.example.com)
|
||||
# Your domain name (no scheme, no path). Example: synapsis.example.com
|
||||
DOMAIN=your-domain.com
|
||||
|
||||
# Optional: Override node domain (host only, defaults to DOMAIN)
|
||||
# NEXT_PUBLIC_NODE_DOMAIN=your-domain.com
|
||||
|
||||
# Optional: Public app URL for background jobs (auto-derived from node domain if unset)
|
||||
# NEXT_PUBLIC_APP_URL=https://your-domain.com
|
||||
|
||||
# Admin email addresses (comma-separated for multiple)
|
||||
ADMIN_EMAILS=admin@your-domain.com
|
||||
|
||||
@@ -32,5 +38,8 @@ DB_NAME=synapsis
|
||||
# Or set a specific port: PORT=3000
|
||||
PORT=auto
|
||||
|
||||
# Allow localhost domains in production (set only for local testing)
|
||||
# ALLOW_LOCALHOST=1
|
||||
|
||||
# Maximum AI bots per user (default: 5)
|
||||
# BOT_MAX_PER_USER=5
|
||||
|
||||
+3
-4
@@ -43,7 +43,7 @@ RUN npm run build
|
||||
# Stage 3: Production Runner
|
||||
# ============================================
|
||||
FROM node:22-alpine AS runner
|
||||
RUN apk add --no-cache libc6-compat openssl netcat-openbsd
|
||||
RUN apk add --no-cache libc6-compat openssl netcat-openbsd wget
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -87,10 +87,9 @@ USER nextjs
|
||||
EXPOSE 3000-3020
|
||||
|
||||
# Health check - longer start period to allow migrations to run
|
||||
# Uses PORT environment variable (set by entrypoint)
|
||||
# Reads dynamic port file when PORT=auto
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=5 \
|
||||
CMD wget -q --spider http://127.0.0.1:${PORT:-3000}/api/health || exit 1
|
||||
CMD sh -c 'PORT_FILE=/var/run/synapsis/port; PORT=3000; if [ -f "$PORT_FILE" ]; then PORT=$(cat "$PORT_FILE"); fi; case "$PORT" in ""|*[!0-9]*) PORT=3000;; esac; wget -q --spider "http://127.0.0.1:${PORT}/api/health" || exit 1'
|
||||
|
||||
# Set the entrypoint
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
|
||||
|
||||
+8
-2
@@ -10,6 +10,7 @@ Production Docker deployment using pre-built images from GitHub Container Regist
|
||||
mkdir -p /opt/synapsis && cd /opt/synapsis
|
||||
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker-compose.yml
|
||||
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/Caddyfile
|
||||
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/caddy-entrypoint.sh
|
||||
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/.env.example
|
||||
cp .env.example .env
|
||||
nano .env # Add your domain and secrets
|
||||
@@ -39,7 +40,7 @@ newgrp docker
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
Edit `.env` and set these required values:
|
||||
Edit `.env` and set these required values (domain should be host only, no scheme or path):
|
||||
|
||||
| Variable | What to put |
|
||||
|----------|-------------|
|
||||
@@ -48,13 +49,18 @@ Edit `.env` and set these required values:
|
||||
| `AUTH_SECRET` | Run: `openssl rand -hex 32` |
|
||||
| `ADMIN_EMAILS` | Your email address |
|
||||
|
||||
Optional (advanced):
|
||||
- `NEXT_PUBLIC_NODE_DOMAIN` to override the node domain (defaults to `DOMAIN`)
|
||||
- `NEXT_PUBLIC_APP_URL` to override the public app URL used by background jobs (auto-derived from the node domain)
|
||||
- `ALLOW_LOCALHOST=1` to allow `localhost` in production containers for local testing
|
||||
|
||||
**Port Configuration:**
|
||||
- `PORT=auto` (default) — Automatically finds an available port between 3000-3020
|
||||
- `PORT=3000` — Use a specific port instead
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Updates
|
||||
## 🔄 Updates (migrations run automatically)
|
||||
|
||||
```bash
|
||||
cd /opt/synapsis
|
||||
|
||||
@@ -7,6 +7,24 @@ set -e
|
||||
SHARED_PORT_FILE="/var/run/synapsis/port"
|
||||
DEFAULT_PORT=3000
|
||||
|
||||
# Normalize DOMAIN (strip scheme/path) to avoid invalid Caddyfile hostnames
|
||||
sanitize_domain() {
|
||||
echo "$1" | sed -E 's#^[a-zA-Z]+://##; s#/.*$##'
|
||||
}
|
||||
|
||||
if [ -n "$DOMAIN" ]; then
|
||||
CLEAN_DOMAIN=$(sanitize_domain "$DOMAIN")
|
||||
if [ "$CLEAN_DOMAIN" != "$DOMAIN" ]; then
|
||||
export DOMAIN="$CLEAN_DOMAIN"
|
||||
echo "🌐 DOMAIN normalized to $DOMAIN"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$DOMAIN" ]; then
|
||||
echo "❌ DOMAIN is not set. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Wait for the port file to exist (with timeout)
|
||||
echo "⏳ Waiting for Synapsis app to announce its port..."
|
||||
TIMEOUT=60
|
||||
|
||||
@@ -57,7 +57,10 @@ services:
|
||||
AUTH_SECRET: ${AUTH_SECRET}
|
||||
|
||||
# Domain configuration
|
||||
NEXT_PUBLIC_NODE_DOMAIN: ${DOMAIN:-localhost}
|
||||
DOMAIN: ${DOMAIN:-localhost}
|
||||
NEXT_PUBLIC_NODE_DOMAIN: ${NEXT_PUBLIC_NODE_DOMAIN:-}
|
||||
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-}
|
||||
ALLOW_LOCALHOST: ${ALLOW_LOCALHOST:-}
|
||||
|
||||
# Admin emails
|
||||
ADMIN_EMAILS: ${ADMIN_EMAILS}
|
||||
@@ -89,7 +92,7 @@ services:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"]
|
||||
test: ["CMD-SHELL", "PORT=$(cat /var/run/synapsis/port 2>/dev/null || echo 3000); case \"$PORT\" in ''|*[!0-9]*) PORT=3000;; esac; wget -q --spider http://127.0.0.1:${PORT}/api/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
@@ -82,14 +82,79 @@ echo " Database URL: ${DATABASE_URL%%:*}://***@***"
|
||||
echo " Port: $PORT"
|
||||
echo "========================================"
|
||||
|
||||
# Ensure DATABASE_URL is set
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
echo "❌ DATABASE_URL is not set. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Normalize domain inputs (strip scheme/path) to avoid localhost/protocol issues
|
||||
sanitize_domain() {
|
||||
echo "$1" | sed -E 's#^[a-zA-Z]+://##; s#/.*$##'
|
||||
}
|
||||
|
||||
# Normalize DOMAIN if provided
|
||||
if [ -n "$DOMAIN" ]; then
|
||||
CLEAN_DOMAIN=$(sanitize_domain "$DOMAIN")
|
||||
if [ "$CLEAN_DOMAIN" != "$DOMAIN" ]; then
|
||||
export DOMAIN="$CLEAN_DOMAIN"
|
||||
echo "🌐 DOMAIN normalized to $DOMAIN"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Normalize NEXT_PUBLIC_NODE_DOMAIN if provided
|
||||
if [ -n "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
|
||||
CLEAN_NODE_DOMAIN=$(sanitize_domain "$NEXT_PUBLIC_NODE_DOMAIN")
|
||||
if [ "$CLEAN_NODE_DOMAIN" != "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
|
||||
export NEXT_PUBLIC_NODE_DOMAIN="$CLEAN_NODE_DOMAIN"
|
||||
echo "🌐 NEXT_PUBLIC_NODE_DOMAIN normalized to $NEXT_PUBLIC_NODE_DOMAIN"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure NEXT_PUBLIC_NODE_DOMAIN is set (fallback to DOMAIN)
|
||||
if [ -z "$NEXT_PUBLIC_NODE_DOMAIN" ] && [ -n "$DOMAIN" ]; then
|
||||
export NEXT_PUBLIC_NODE_DOMAIN="$DOMAIN"
|
||||
echo "🌐 NEXT_PUBLIC_NODE_DOMAIN set to $NEXT_PUBLIC_NODE_DOMAIN"
|
||||
fi
|
||||
|
||||
# Fail fast if running in production with localhost domain
|
||||
if [ "$NODE_ENV" = "production" ]; then
|
||||
case "$NEXT_PUBLIC_NODE_DOMAIN" in
|
||||
""|localhost|localhost:*|127.0.0.1|127.0.0.1:*)
|
||||
if [ -z "$ALLOW_LOCALHOST" ]; then
|
||||
echo "❌ NEXT_PUBLIC_NODE_DOMAIN is set to localhost in production."
|
||||
echo " Set DOMAIN to your public domain in .env, or set ALLOW_LOCALHOST=1 to bypass."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Ensure NEXT_PUBLIC_APP_URL is set for background jobs
|
||||
if [ -z "$NEXT_PUBLIC_APP_URL" ] && [ -n "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
|
||||
case "$NEXT_PUBLIC_NODE_DOMAIN" in
|
||||
http://*|https://*)
|
||||
NEXT_PUBLIC_APP_URL="$NEXT_PUBLIC_NODE_DOMAIN"
|
||||
;;
|
||||
localhost*|127.0.0.1*)
|
||||
NEXT_PUBLIC_APP_URL="http://$NEXT_PUBLIC_NODE_DOMAIN"
|
||||
;;
|
||||
*)
|
||||
NEXT_PUBLIC_APP_URL="https://$NEXT_PUBLIC_NODE_DOMAIN"
|
||||
;;
|
||||
esac
|
||||
export NEXT_PUBLIC_APP_URL
|
||||
echo "🌐 NEXT_PUBLIC_APP_URL set to $NEXT_PUBLIC_APP_URL"
|
||||
fi
|
||||
|
||||
# Function to wait for database
|
||||
wait_for_db() {
|
||||
echo ""
|
||||
echo "⏳ Waiting for PostgreSQL..."
|
||||
|
||||
# Extract host and port from DATABASE_URL
|
||||
DB_HOST=$(echo "$DATABASE_URL" | sed -n 's/.*@\([^:]*\):.*/\1/p')
|
||||
DB_PORT=$(echo "$DATABASE_URL" | sed -n 's/.*:\([0-9]*\)\/.*/\1/p')
|
||||
DB_HOST=$(echo "$DATABASE_URL" | sed -n 's#.*@\([^/:]*\).*#\1#p')
|
||||
DB_PORT=$(echo "$DATABASE_URL" | sed -n 's#.*:\([0-9][0-9]*\)/.*#\1#p')
|
||||
DB_HOST=${DB_HOST:-postgres}
|
||||
DB_PORT=${DB_PORT:-5432}
|
||||
|
||||
@@ -119,11 +184,12 @@ run_migrations() {
|
||||
|
||||
# Run migrations using npm script
|
||||
echo " Executing: npm run db:push"
|
||||
npm run db:push 2>&1 || {
|
||||
echo "⚠️ Migration command exited with error (may be already up to date)"
|
||||
}
|
||||
|
||||
echo "✅ Migration step complete"
|
||||
if npm run db:push 2>&1; then
|
||||
echo "✅ Migration step complete"
|
||||
else
|
||||
echo "❌ Migration failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Wait for database
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Migration: Add storage_public_base_url column to users table
|
||||
ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "storage_public_base_url" text;
|
||||
@@ -92,6 +92,13 @@
|
||||
"when": 1769586699065,
|
||||
"tag": "0012_quick_mockingbird",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 13,
|
||||
"version": "7",
|
||||
"when": 1769638500000,
|
||||
"tag": "0013_add_storage_public_base_url",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
# Synapsis Docker Entrypoint Script
|
||||
# Handles automatic port detection when PORT=auto
|
||||
# Handles database migrations, port detection, and application startup
|
||||
|
||||
set -e
|
||||
|
||||
@@ -66,6 +66,136 @@ echo "📝 Port $PORT written to $SHARED_PORT_FILE"
|
||||
# Export HOSTNAME for Next.js
|
||||
export HOSTNAME="0.0.0.0"
|
||||
|
||||
# ============================================
|
||||
# Database Migrations
|
||||
# ============================================
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " Synapsis - Starting Application"
|
||||
echo "========================================"
|
||||
echo " Time: $(date)"
|
||||
echo " Working Dir: $(pwd)"
|
||||
echo " Database URL: ${DATABASE_URL%%:*}://***@***"
|
||||
echo " Port: $PORT"
|
||||
echo "========================================"
|
||||
|
||||
# Ensure DATABASE_URL is set
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
echo "❌ DATABASE_URL is not set. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Normalize domain inputs (strip scheme/path) to avoid localhost/protocol issues
|
||||
sanitize_domain() {
|
||||
echo "$1" | sed -E 's#^[a-zA-Z]+://##; s#/.*$##'
|
||||
}
|
||||
|
||||
# Normalize DOMAIN if provided
|
||||
if [ -n "$DOMAIN" ]; then
|
||||
CLEAN_DOMAIN=$(sanitize_domain "$DOMAIN")
|
||||
if [ "$CLEAN_DOMAIN" != "$DOMAIN" ]; then
|
||||
export DOMAIN="$CLEAN_DOMAIN"
|
||||
echo "🌐 DOMAIN normalized to $DOMAIN"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Normalize NEXT_PUBLIC_NODE_DOMAIN if provided
|
||||
if [ -n "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
|
||||
CLEAN_NODE_DOMAIN=$(sanitize_domain "$NEXT_PUBLIC_NODE_DOMAIN")
|
||||
if [ "$CLEAN_NODE_DOMAIN" != "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
|
||||
export NEXT_PUBLIC_NODE_DOMAIN="$CLEAN_NODE_DOMAIN"
|
||||
echo "🌐 NEXT_PUBLIC_NODE_DOMAIN normalized to $NEXT_PUBLIC_NODE_DOMAIN"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure NEXT_PUBLIC_NODE_DOMAIN is set (fallback to DOMAIN)
|
||||
if [ -z "$NEXT_PUBLIC_NODE_DOMAIN" ] && [ -n "$DOMAIN" ]; then
|
||||
export NEXT_PUBLIC_NODE_DOMAIN="$DOMAIN"
|
||||
echo "🌐 NEXT_PUBLIC_NODE_DOMAIN set to $NEXT_PUBLIC_NODE_DOMAIN"
|
||||
fi
|
||||
|
||||
# Fail fast if running in production with localhost domain
|
||||
if [ "$NODE_ENV" = "production" ]; then
|
||||
case "$NEXT_PUBLIC_NODE_DOMAIN" in
|
||||
""|localhost|localhost:*|127.0.0.1|127.0.0.1:*)
|
||||
if [ -z "$ALLOW_LOCALHOST" ]; then
|
||||
echo "❌ NEXT_PUBLIC_NODE_DOMAIN is set to localhost in production."
|
||||
echo " Set DOMAIN to your public domain in .env, or set ALLOW_LOCALHOST=1 to bypass."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Ensure NEXT_PUBLIC_APP_URL is set for background jobs
|
||||
if [ -z "$NEXT_PUBLIC_APP_URL" ] && [ -n "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
|
||||
case "$NEXT_PUBLIC_NODE_DOMAIN" in
|
||||
http://*|https://*)
|
||||
NEXT_PUBLIC_APP_URL="$NEXT_PUBLIC_NODE_DOMAIN"
|
||||
;;
|
||||
localhost*|127.0.0.1*)
|
||||
NEXT_PUBLIC_APP_URL="http://$NEXT_PUBLIC_NODE_DOMAIN"
|
||||
;;
|
||||
*)
|
||||
NEXT_PUBLIC_APP_URL="https://$NEXT_PUBLIC_NODE_DOMAIN"
|
||||
;;
|
||||
esac
|
||||
export NEXT_PUBLIC_APP_URL
|
||||
echo "🌐 NEXT_PUBLIC_APP_URL set to $NEXT_PUBLIC_APP_URL"
|
||||
fi
|
||||
|
||||
# Function to wait for database
|
||||
wait_for_db() {
|
||||
echo ""
|
||||
echo "⏳ Waiting for PostgreSQL..."
|
||||
|
||||
# Extract host and port from DATABASE_URL
|
||||
DB_HOST=$(echo "$DATABASE_URL" | sed -n 's#.*@\([^/:]*\).*#\1#p')
|
||||
DB_PORT=$(echo "$DATABASE_URL" | sed -n 's#.*:\([0-9][0-9]*\)/.*#\1#p')
|
||||
DB_HOST=${DB_HOST:-postgres}
|
||||
DB_PORT=${DB_PORT:-5432}
|
||||
|
||||
max_retries=30
|
||||
retry_count=0
|
||||
|
||||
while ! nc -z "$DB_HOST" "$DB_PORT" 2>/dev/null; do
|
||||
retry_count=$((retry_count + 1))
|
||||
if [ $retry_count -ge $max_retries ]; then
|
||||
echo "❌ Failed to connect to database after $max_retries attempts"
|
||||
exit 1
|
||||
fi
|
||||
echo " Attempt $retry_count/$max_retries - waiting for $DB_HOST:$DB_PORT..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "✅ Database is ready!"
|
||||
}
|
||||
|
||||
# Function to run database migrations
|
||||
run_migrations() {
|
||||
echo ""
|
||||
echo "🔄 Running database migrations..."
|
||||
echo " Current directory: $(pwd)"
|
||||
echo " Drizzle directory contents:"
|
||||
ls -la drizzle/ 2>/dev/null || echo " (drizzle dir not found or empty)"
|
||||
|
||||
# Run migrations using npm script
|
||||
echo " Executing: npm run db:push"
|
||||
if npm run db:push 2>&1; then
|
||||
echo "✅ Migration step complete"
|
||||
else
|
||||
echo "❌ Migration failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Wait for database
|
||||
wait_for_db
|
||||
|
||||
# Run migrations
|
||||
run_migrations
|
||||
|
||||
echo "🚀 Starting Synapsis on port $PORT..."
|
||||
echo ""
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db, users } from '@/db';
|
||||
import { db, users, isDbAvailable } from '@/db';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
if (!isDbAvailable()) {
|
||||
return NextResponse.json(
|
||||
{ available: false, error: 'Database not configured' },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const handle = searchParams.get('handle')?.toLowerCase().trim();
|
||||
|
||||
@@ -15,9 +22,21 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.json({ available: false, error: 'Invalid characters' });
|
||||
}
|
||||
|
||||
const existingUser = await db.query.users.findFirst({
|
||||
where: eq(users.handle, handle),
|
||||
});
|
||||
let existingUser = null;
|
||||
try {
|
||||
existingUser = await db.query.users.findFirst({
|
||||
where: eq(users.handle, handle),
|
||||
});
|
||||
} catch (err: any) {
|
||||
// Handle fresh installs where the users table isn't created yet.
|
||||
if (err?.code === '42P01' || /relation .*users.* does not exist/i.test(err?.message || '')) {
|
||||
return NextResponse.json(
|
||||
{ available: true, handle, warning: 'Database not initialized' },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
available: !existingUser,
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db, nodes } from '@/db';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export async function GET() {
|
||||
function getRequestBaseUrl(req: NextRequest, fallbackDomain: string): string {
|
||||
const forwardedHost = req.headers.get('x-forwarded-host');
|
||||
const forwardedProto = req.headers.get('x-forwarded-proto');
|
||||
const host = forwardedHost?.split(',')[0]?.trim() || req.headers.get('host');
|
||||
const protocol =
|
||||
forwardedProto?.split(',')[0]?.trim() ||
|
||||
(host && host.includes('localhost') ? 'http' : 'https');
|
||||
|
||||
if (host) {
|
||||
return `${protocol}://${host}`;
|
||||
}
|
||||
|
||||
return `https://${fallbackDomain}`;
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
if (!db) {
|
||||
// Redirect to default favicon
|
||||
return NextResponse.redirect(new URL('/favicon.png', process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'));
|
||||
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
const baseUrl = getRequestBaseUrl(req, domain);
|
||||
return NextResponse.redirect(new URL('/favicon.png', baseUrl));
|
||||
}
|
||||
|
||||
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
@@ -17,14 +34,20 @@ export async function GET() {
|
||||
|
||||
if (node?.faviconUrl) {
|
||||
// Redirect to custom favicon
|
||||
return NextResponse.redirect(node.faviconUrl);
|
||||
const baseUrl = getRequestBaseUrl(req, domain);
|
||||
const target = node.faviconUrl.startsWith('/')
|
||||
? new URL(node.faviconUrl, baseUrl)
|
||||
: node.faviconUrl;
|
||||
return NextResponse.redirect(target);
|
||||
}
|
||||
|
||||
// Redirect to default favicon
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || `https://${domain}`;
|
||||
const baseUrl = getRequestBaseUrl(req, domain);
|
||||
return NextResponse.redirect(new URL('/favicon.png', baseUrl));
|
||||
} catch (error) {
|
||||
console.error('Favicon error:', error);
|
||||
return NextResponse.redirect(new URL('/favicon.png', process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'));
|
||||
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
const baseUrl = getRequestBaseUrl(req, domain);
|
||||
return NextResponse.redirect(new URL('/favicon.png', baseUrl));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function POST(req: NextRequest) {
|
||||
accessKeyId: process.env.STORAGE_ACCESS_KEY || '',
|
||||
secretAccessKey: process.env.STORAGE_SECRET_KEY || '',
|
||||
},
|
||||
forcePathStyle: true, // Needed for many S3-compatible providers (MinIO, etc.)
|
||||
forcePathStyle: true, // Needed for many S3-compatible providers
|
||||
});
|
||||
|
||||
const bucket = process.env.STORAGE_BUCKET || 'synapsis';
|
||||
|
||||
@@ -577,8 +577,6 @@ export default function LoginPage() {
|
||||
<option value="wasabi">Wasabi</option>
|
||||
<option value="contabo">Contabo S3</option>
|
||||
<option value="s3">AWS S3</option>
|
||||
<option value="minio">MinIO / Self-hosted</option>
|
||||
<option value="other">Other S3-compatible</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -614,8 +612,8 @@ export default function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Endpoint URL - only show for providers that need it (R2, B2, Contabo, MinIO, Other) */}
|
||||
{(storageProvider === 'r2' || storageProvider === 'b2' || storageProvider === 'contabo' || storageProvider === 'minio' || storageProvider === 'other') && (
|
||||
{/* Endpoint URL - only show for providers that need it (R2, B2, Contabo) */}
|
||||
{(storageProvider === 'r2' || storageProvider === 'b2' || storageProvider === 'contabo') && (
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
|
||||
Endpoint URL
|
||||
@@ -635,8 +633,8 @@ export default function LoginPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Public Base URL - only show for providers that need it (R2, B2, Contabo, Other) */}
|
||||
{(storageProvider === 'r2' || storageProvider === 'b2' || storageProvider === 'contabo' || storageProvider === 'other') && (
|
||||
{/* Public Base URL - only show for providers that need it (R2, B2, Contabo) */}
|
||||
{(storageProvider === 'r2' || storageProvider === 'b2' || storageProvider === 'contabo') && (
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
|
||||
Public Base URL
|
||||
|
||||
+3
-3
@@ -65,7 +65,7 @@ export const users = pgTable('users', {
|
||||
movedFrom: text('moved_from'), // Old actor URL if this account migrated here
|
||||
migratedAt: timestamp('migrated_at'), // When the migration occurred
|
||||
// User-owned S3-compatible storage - required for new users
|
||||
storageProvider: text('storage_provider'), // 's3', 'r2', 'b2', 'wasabi', 'contabo', etc
|
||||
storageProvider: text('storage_provider'), // 's3', 'r2', 'b2', 'wasabi', 'contabo'
|
||||
storageEndpoint: text('storage_endpoint'), // S3 endpoint URL (optional for AWS)
|
||||
storagePublicBaseUrl: text('storage_public_base_url'), // Public URL for viewing files (required for R2, B2, Contabo)
|
||||
storageRegion: text('storage_region'), // Region (e.g., 'us-east-1')
|
||||
@@ -919,8 +919,8 @@ export const chatConversationsRelations = relations(chatConversations, ({ one, m
|
||||
|
||||
/**
|
||||
* Individual chat messages within conversations.
|
||||
* Messages are encrypted end-to-end using recipient's public key.
|
||||
* Sender also gets a copy encrypted with their own public key.
|
||||
* Messages are stored as plain text on the server.
|
||||
* Both sender and recipient can view the message content.
|
||||
*/
|
||||
export const chatMessages = pgTable('chat_messages', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||
|
||||
interface RuntimeConfig {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* User-Owned S3-Compatible Storage Utilities
|
||||
*
|
||||
* Supports AWS S3, Cloudflare R2, Backblaze B2, Wasabi, MinIO, etc.
|
||||
* Supports AWS S3, Cloudflare R2, Backblaze B2, Wasabi, and Contabo.
|
||||
*/
|
||||
|
||||
import { S3Client, PutObjectCommand, HeadBucketCommand } from '@aws-sdk/client-s3';
|
||||
import { decryptPrivateKey, deserializeEncryptedKey } from '@/lib/crypto/private-key';
|
||||
|
||||
export type StorageProvider = 's3' | 'r2' | 'b2' | 'wasabi' | 'minio' | 'other';
|
||||
export type StorageProvider = 's3' | 'r2' | 'b2' | 'wasabi' | 'contabo';
|
||||
|
||||
interface S3Credentials {
|
||||
endpoint?: string;
|
||||
@@ -105,7 +105,7 @@ export async function uploadToUserStorage(
|
||||
if (publicBaseUrl) {
|
||||
url = `${publicBaseUrl.replace(/\/$/, '')}/${key}`;
|
||||
} else if (endpoint) {
|
||||
// Custom endpoint (MinIO, etc)
|
||||
// Custom endpoint (R2, B2, Contabo)
|
||||
url = `${endpoint}/${bucket}/${key}`;
|
||||
} else {
|
||||
// AWS S3 standard URL
|
||||
|
||||
Reference in New Issue
Block a user