diff --git a/Dockerfile b/Dockerfile index 2983bfc..16a74e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index 756c4f1..349e38f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 393c408..2fb0978 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/docker/.env.example b/docker/.env.example index f96d984..2a02910 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -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 diff --git a/docker/Dockerfile b/docker/Dockerfile index cbd2547..955c510 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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"] - diff --git a/docker/README.md b/docker/README.md index 8534bce..63ffb0c 100644 --- a/docker/README.md +++ b/docker/README.md @@ -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 diff --git a/docker/caddy-entrypoint.sh b/docker/caddy-entrypoint.sh index dc99f1b..6d236ed 100755 --- a/docker/caddy-entrypoint.sh +++ b/docker/caddy-entrypoint.sh @@ -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 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 29b1774..4f67fae 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -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 diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index b46cb8c..1597a31 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -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 diff --git a/drizzle/0013_add_storage_public_base_url.sql b/drizzle/0013_add_storage_public_base_url.sql new file mode 100644 index 0000000..9c1daff --- /dev/null +++ b/drizzle/0013_add_storage_public_base_url.sql @@ -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; diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index aa75922..166cea6 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -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 } ] } \ No newline at end of file diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh index b0b8656..0d9122d 100755 --- a/scripts/docker-entrypoint.sh +++ b/scripts/docker-entrypoint.sh @@ -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 "" diff --git a/src/app/api/auth/check-handle/route.ts b/src/app/api/auth/check-handle/route.ts index 0cbb0b6..1166d44 100644 --- a/src/app/api/auth/check-handle/route.ts +++ b/src/app/api/auth/check-handle/route.ts @@ -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, diff --git a/src/app/api/favicon/route.ts b/src/app/api/favicon/route.ts index c0cc0a3..82c64d1 100644 --- a/src/app/api/favicon/route.ts +++ b/src/app/api/favicon/route.ts @@ -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)); } } diff --git a/src/app/api/uploads/route.ts b/src/app/api/uploads/route.ts index 4f67aff..bfd105f 100644 --- a/src/app/api/uploads/route.ts +++ b/src/app/api/uploads/route.ts @@ -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'; diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 9f9999c..5423b0e 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -577,8 +577,6 @@ export default function LoginPage() { - - @@ -614,8 +612,8 @@ export default function LoginPage() { - {/* 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') && (
)} - {/* 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') && (