This commit is contained in:
@@ -1,18 +0,0 @@
|
|||||||
.git
|
|
||||||
.github
|
|
||||||
.next
|
|
||||||
.vercel
|
|
||||||
node_modules
|
|
||||||
coverage
|
|
||||||
site-work
|
|
||||||
*.log
|
|
||||||
.DS_Store
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
+7
-3
@@ -2,7 +2,11 @@
|
|||||||
# Copy to .env and adjust values for your machine.
|
# Copy to .env and adjust values for your machine.
|
||||||
|
|
||||||
# Required: local database
|
# Required: local database
|
||||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/synapsis
|
# Embedded Turso database file. Relative paths resolve from the app directory.
|
||||||
|
DATABASE_PATH=./data/synapsis.db
|
||||||
|
|
||||||
|
# Synapsis' standard local bind port. Override it if this port is already in use.
|
||||||
|
PORT=43821
|
||||||
|
|
||||||
# Required: used for sessions, node key encryption, and bot API key encryption
|
# Required: used for sessions, node key encryption, and bot API key encryption
|
||||||
AUTH_SECRET=replace-with-a-long-random-secret
|
AUTH_SECRET=replace-with-a-long-random-secret
|
||||||
@@ -11,8 +15,8 @@ AUTH_SECRET=replace-with-a-long-random-secret
|
|||||||
ADMIN_EMAILS=admin@example.com
|
ADMIN_EMAILS=admin@example.com
|
||||||
|
|
||||||
# Recommended: local node identity
|
# Recommended: local node identity
|
||||||
NEXT_PUBLIC_NODE_DOMAIN=localhost:3000
|
NEXT_PUBLIC_NODE_DOMAIN=localhost:43821
|
||||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
NEXT_PUBLIC_APP_URL=http://localhost:43821
|
||||||
|
|
||||||
# Optional: node metadata shown before a node record exists in the database
|
# Optional: node metadata shown before a node record exists in the database
|
||||||
# NEXT_PUBLIC_NODE_NAME=Synapsis Local
|
# NEXT_PUBLIC_NODE_NAME=Synapsis Local
|
||||||
|
|||||||
@@ -36,21 +36,12 @@ jobs:
|
|||||||
src/lib/bots/mentionHandler.property.test.ts
|
src/lib/bots/mentionHandler.property.test.ts
|
||||||
src/lib/bots/scheduler.test.ts
|
src/lib/bots/scheduler.test.ts
|
||||||
|
|
||||||
|
- name: Embedded database smoke test
|
||||||
|
run: DATABASE_PATH=/tmp/synapsis-ci.db npm run db:migrate
|
||||||
|
|
||||||
- name: Production build
|
- name: Production build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
env:
|
||||||
docker-validation:
|
AUTH_SECRET: ci-test-secret
|
||||||
runs-on: ubuntu-latest
|
DATABASE_PATH: /tmp/synapsis-ci.db
|
||||||
|
NEXT_PUBLIC_NODE_DOMAIN: localhost:43821
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Validate production compose config
|
|
||||||
run: docker compose --env-file docker/.env.example config > /tmp/synapsis-compose.txt
|
|
||||||
|
|
||||||
- name: Validate source-build compose config
|
|
||||||
run: docker compose -f docker/docker-compose.yml --env-file docker/.env.example config > /tmp/synapsis-build-compose.txt
|
|
||||||
|
|
||||||
- name: Build application image
|
|
||||||
run: docker build -f docker/Dockerfile .
|
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
name: Build and Push Docker Image to GHCR
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tag:
|
|
||||||
description: 'Optional explicit version tag (for example 2026.03.09.10). Leave blank to auto-increment.'
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
# Also build on version tags
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: ghcr.io
|
|
||||||
IMAGE_NAME: gnosyslabs/synapsis
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Log in to Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY }}
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
env:
|
|
||||||
IMAGE_REPO: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
||||||
PLATFORMS: linux/amd64,linux/arm64
|
|
||||||
PRUNE_BUILD_CACHE: '0'
|
|
||||||
APP_VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || '' }}
|
|
||||||
EXTRA_TAGS: ${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || '' }}
|
|
||||||
run: |
|
|
||||||
if [ -n "$EXTRA_TAGS" ]; then
|
|
||||||
EXTRA_TAGS="${EXTRA_TAGS#v}"
|
|
||||||
export EXTRA_TAGS
|
|
||||||
if [ -z "$APP_VERSION" ]; then
|
|
||||||
APP_VERSION="$EXTRA_TAGS"
|
|
||||||
export APP_VERSION
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
chmod +x ./scripts/docker-publish.sh
|
|
||||||
./scripts/docker-publish.sh
|
|
||||||
@@ -47,6 +47,12 @@ next-env.d.ts
|
|||||||
# uploads
|
# uploads
|
||||||
/public/uploads/
|
/public/uploads/
|
||||||
|
|
||||||
|
# embedded Turso / SQLite data
|
||||||
|
/data/
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
.kiro/
|
.kiro/
|
||||||
|
|||||||
@@ -1,247 +0,0 @@
|
|||||||
# Quick Start with Docker
|
|
||||||
|
|
||||||
Get your Synapsis node running in under 10 minutes with Docker. This is the **recommended and easiest** way to self-host.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- A Linux server with a domain pointed to it
|
|
||||||
- A domain name pointed to your server
|
|
||||||
|
|
||||||
## Quick Start (5 Minutes)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Bootstrap the deployment directory
|
|
||||||
curl -fsSL https://synapsis.social/install.sh | bash
|
|
||||||
|
|
||||||
# 2. Review the generated config
|
|
||||||
nano /opt/synapsis/.env
|
|
||||||
|
|
||||||
# 3. Start your node
|
|
||||||
cd /opt/synapsis
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Done! Your node is live at `https://your-domain.com` with automatic SSL.
|
|
||||||
|
|
||||||
## Existing nginx / reverse proxy host
|
|
||||||
|
|
||||||
If your server already has nginx, Traefik, or another reverse proxy using `80/443`, use proxyless mode instead:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://synapsis.social/install.sh | PROXY=none bash
|
|
||||||
nano /opt/synapsis/.env
|
|
||||||
cd /opt/synapsis
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
In `PROXY=none` mode:
|
|
||||||
|
|
||||||
- Synapsis skips the bundled Caddy service
|
|
||||||
- the app binds to `127.0.0.1:${PORT}`
|
|
||||||
- your existing reverse proxy should forward traffic to that localhost port
|
|
||||||
|
|
||||||
Example nginx upstream:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
location / {
|
|
||||||
proxy_pass http://127.0.0.1:3000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
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_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Detailed Setup
|
|
||||||
|
|
||||||
### 1. Bootstrap the Deployment Directory
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://synapsis.social/install.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
If Docker is missing, the installer will install it for you on supported Linux hosts.
|
|
||||||
|
|
||||||
This downloads:
|
|
||||||
|
|
||||||
- `docker-compose.yml`
|
|
||||||
- `Caddyfile`
|
|
||||||
- `caddy-entrypoint.sh`
|
|
||||||
- `.env.example`
|
|
||||||
- `.env`
|
|
||||||
|
|
||||||
The installer also generates `AUTH_SECRET` and `DB_PASSWORD` if `openssl` is available.
|
|
||||||
|
|
||||||
### 2. Configure Environment
|
|
||||||
|
|
||||||
Edit `/opt/synapsis/.env` with your settings:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nano /opt/synapsis/.env
|
|
||||||
```
|
|
||||||
|
|
||||||
**Required variables:**
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Your domain
|
|
||||||
DOMAIN=your-domain.com
|
|
||||||
|
|
||||||
# Admin email(s)
|
|
||||||
ADMIN_EMAILS=you@example.com
|
|
||||||
|
|
||||||
# Generate with: openssl rand -hex 32
|
|
||||||
AUTH_SECRET=your-secret-key-here
|
|
||||||
|
|
||||||
# Database password
|
|
||||||
DB_PASSWORD=your-secure-password
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also set these before running the installer to prefill `.env`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DOMAIN=your-domain.com
|
|
||||||
ADMIN_EMAILS=you@example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
That’s it. Save and exit.
|
|
||||||
|
|
||||||
### 3. Start Synapsis
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/synapsis
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
This starts:
|
|
||||||
- **PostgreSQL** database
|
|
||||||
- **Synapsis** app (pre-built image from GitHub Container Registry)
|
|
||||||
- **Caddy** reverse proxy with automatic SSL
|
|
||||||
|
|
||||||
Visit `https://your-domain.com` to create your admin account.
|
|
||||||
|
|
||||||
## Storage Model
|
|
||||||
|
|
||||||
### Node installation
|
|
||||||
|
|
||||||
The node itself only needs Docker, PostgreSQL, and a domain to start.
|
|
||||||
|
|
||||||
### User media storage
|
|
||||||
|
|
||||||
At the moment, new user registration requires each account to provide **its own S3-compatible storage credentials** for media uploads. That requirement is enforced in the app today, so plan onboarding accordingly.
|
|
||||||
|
|
||||||
## Configuration Reference
|
|
||||||
|
|
||||||
### Core Variables
|
|
||||||
|
|
||||||
| Variable | Required | Description |
|
|
||||||
|----------|----------|-------------|
|
|
||||||
| `DOMAIN` | Yes | Your domain name |
|
|
||||||
| `AUTH_SECRET` | Yes | Generate with `openssl rand -hex 32` |
|
|
||||||
| `ADMIN_EMAILS` | Yes | Comma-separated admin emails |
|
|
||||||
| `DB_PASSWORD` | Yes | Database password |
|
|
||||||
|
|
||||||
### Optional Variables
|
|
||||||
|
|
||||||
| Variable | Default | Description |
|
|
||||||
|----------|---------|-------------|
|
|
||||||
| `BOT_MAX_PER_USER` | 5 | Max AI bots per user |
|
|
||||||
| `PORT` | auto | Application port. Default installs use `auto` to scan 3000-3020. In `PROXY=none` mode this is the localhost port your reverse proxy should target. |
|
|
||||||
| `NEXT_PUBLIC_NODE_DOMAIN` | `DOMAIN` | Override node domain if needed |
|
|
||||||
| `NEXT_PUBLIC_APP_URL` | Derived from domain | Public URL used by background jobs |
|
|
||||||
|
|
||||||
## Updating Synapsis
|
|
||||||
|
|
||||||
Updating is a single command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/synapsis
|
|
||||||
docker compose pull
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
This will:
|
|
||||||
- Pull the latest pre-built image from GitHub Container Registry
|
|
||||||
- Recreate containers if needed
|
|
||||||
- Run database migrations automatically
|
|
||||||
- Keep your data volumes intact
|
|
||||||
|
|
||||||
### Check Update Status
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# See running containers
|
|
||||||
docker compose ps
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
docker compose logs -f app
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Container won't start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check logs
|
|
||||||
docker compose logs app
|
|
||||||
|
|
||||||
# Common issues:
|
|
||||||
# - Missing required env vars
|
|
||||||
# - Port 3000 already in use
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database connection errors
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check database container
|
|
||||||
docker compose logs postgres
|
|
||||||
|
|
||||||
# Verify env vars are loaded
|
|
||||||
docker compose exec app env | grep DATABASE
|
|
||||||
```
|
|
||||||
|
|
||||||
### Port already in use
|
|
||||||
|
|
||||||
If `80` or `443` is already in use, your server already has another reverse proxy bound there. Use `PROXY=none` instead of the default Caddy install:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://synapsis.social/install.sh | PROXY=none bash
|
|
||||||
```
|
|
||||||
|
|
||||||
For the application port itself, Synapsis uses `PORT=auto` by default, which automatically finds an available port between 3000-3020. If you need to use a specific port:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Edit .env and set a specific port
|
|
||||||
PORT=3001
|
|
||||||
|
|
||||||
# Restart
|
|
||||||
docker compose down
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reset everything (data loss!)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose down -v # Remove containers AND volumes
|
|
||||||
docker compose up -d # Start fresh
|
|
||||||
```
|
|
||||||
|
|
||||||
### View all logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# All services
|
|
||||||
docker compose logs -f
|
|
||||||
|
|
||||||
# Just the app
|
|
||||||
docker compose logs -f app
|
|
||||||
|
|
||||||
# Just the database
|
|
||||||
docker compose logs -f postgres
|
|
||||||
```
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
- [Configure your node settings](/user-guide)
|
|
||||||
- [Learn about the Swarm](/swarm)
|
|
||||||
- [Learn how Synapsis differs from traditional federation](/user-guide/synapsis-difference)
|
|
||||||
-72
@@ -1,72 +0,0 @@
|
|||||||
# Synapsis Docker Image
|
|
||||||
# Multi-stage build for production
|
|
||||||
|
|
||||||
# Stage 1: Dependencies
|
|
||||||
FROM node:20-alpine AS deps
|
|
||||||
RUN apk add --no-cache libc6-compat
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy package files
|
|
||||||
COPY package.json package-lock.json* ./
|
|
||||||
RUN npm ci --only=production
|
|
||||||
|
|
||||||
# Stage 2: Builder
|
|
||||||
FROM node:20-alpine AS builder
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy deps from previous stage
|
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build the application
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Stage 3: Runner
|
|
||||||
FROM node:20-alpine AS runner
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
|
||||||
|
|
||||||
# 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
|
|
||||||
RUN adduser --system --uid 1001 nextjs
|
|
||||||
|
|
||||||
# Copy necessary files
|
|
||||||
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/
|
|
||||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
|
||||||
|
|
||||||
# Create shared directory for port file (must be writable by nextjs)
|
|
||||||
RUN mkdir -p /var/run/synapsis && chown nextjs:nodejs /var/run/synapsis
|
|
||||||
|
|
||||||
# Switch to non-root user
|
|
||||||
USER nextjs
|
|
||||||
|
|
||||||
# Expose port range for auto-detection
|
|
||||||
EXPOSE 3000-3020
|
|
||||||
|
|
||||||
# Set environment variables (can be overridden at runtime)
|
|
||||||
ENV PORT=3000
|
|
||||||
ENV HOSTNAME="0.0.0.0"
|
|
||||||
|
|
||||||
# Health check (reads dynamic port file when PORT=auto)
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
|
||||||
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"]
|
|
||||||
+14
-135
@@ -1,159 +1,38 @@
|
|||||||
# How To Work On Synapsis
|
# Working on Synapsis
|
||||||
This is the practical workflow for developing Synapsis without turning every bugfix into a full Docker release.
|
|
||||||
|
|
||||||
## Basic Rule
|
Synapsis uses an embedded Turso database and runs as a native Node.js process. Docker and a local PostgreSQL server are not part of the development or deployment workflow.
|
||||||
Use three different loops for three different jobs:
|
|
||||||
1. `npm run dev` for normal feature work and bugfixes
|
|
||||||
2. local Docker source builds when you need container parity
|
|
||||||
3. GHCR publish only when you actually want the server to update
|
|
||||||
|
|
||||||
Do **not** rebuild and push a Docker image for every tiny fix. Batch fixes together, verify them locally, then publish when the server needs the new version.
|
## Local loop
|
||||||
|
|
||||||
## 1. Normal Local Development
|
|
||||||
Use this for most day-to-day work.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
npm run db:push
|
npm run db:migrate
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Useful verification commands:
|
Before handing off a change:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run type-check
|
npm run type-check
|
||||||
npm run build
|
|
||||||
npm test
|
npm test
|
||||||
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
Use this loop when:
|
When the schema changes, edit `src/db/schema.ts`, generate a migration with `npm run db:generate`, and verify it against a fresh database:
|
||||||
- changing UI
|
|
||||||
- fixing API logic
|
|
||||||
- working on auth, feed logic, posting, bots, or settings
|
|
||||||
- you do not specifically need to test the Docker runtime
|
|
||||||
|
|
||||||
## 2. Local Docker Parity Test
|
|
||||||
Use this when you want to know whether the app still works inside the actual container setup. This compose file builds from your local source tree:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd docker
|
DATABASE_PATH=/tmp/synapsis-test.db npm run db:migrate
|
||||||
cp .env.example .env
|
|
||||||
docker compose up --build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
That uses [docker/docker-compose.yml](/Users/christopher/Dev/Synapsis/Synapsis/docker/docker-compose.yml) and [docker/Dockerfile](/Users/christopher/Dev/Synapsis/Synapsis/docker/Dockerfile).
|
## VPS workflow
|
||||||
|
|
||||||
Use this loop when:
|
Production runs through `synapsis.service`, listening on `127.0.0.1:43821`. Reverse-proxy configuration belongs to the host operator.
|
||||||
- Docker-specific startup behavior matters
|
|
||||||
- you changed the Dockerfile or entrypoint
|
|
||||||
- you changed env handling, healthchecks, migrations, ports, or install flow
|
|
||||||
|
|
||||||
## 3. Production Image Publish
|
|
||||||
Use this only when you want the server or end users to pull a new image. The production install uses [docker-compose.yml](/Users/christopher/Dev/Synapsis/Synapsis/docker-compose.yml) and `ghcr.io/gnosyslabs/synapsis:latest`.
|
|
||||||
|
|
||||||
### First-time GHCR auth on this machine
|
|
||||||
```bash
|
|
||||||
gh auth refresh -h github.com -s read:packages -s write:packages
|
|
||||||
gh auth token | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin
|
|
||||||
```
|
|
||||||
|
|
||||||
### Publish the image
|
|
||||||
Push code first:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git push origin main
|
sudo /opt/synapsis/deploy/update.sh
|
||||||
|
sudo systemctl status synapsis
|
||||||
|
sudo journalctl -u synapsis -f
|
||||||
```
|
```
|
||||||
|
|
||||||
Then build and push the multi-arch image:
|
The updater makes a timestamped database backup, pulls with `--ff-only`, installs dependencies, runs migrations, builds, and restarts the service.
|
||||||
|
|
||||||
```bash
|
|
||||||
BUILDER=colima ./scripts/docker-publish.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
That publishes:
|
|
||||||
- `ghcr.io/gnosyslabs/synapsis:latest`
|
|
||||||
- `ghcr.io/gnosyslabs/synapsis:<YYYY.MM.DD.N>`
|
|
||||||
- `ghcr.io/gnosyslabs/synapsis:<short-sha>`
|
|
||||||
|
|
||||||
If you are not on a Mac/Colima setup, set `BUILDER` to your buildx builder or leave it empty to use the default builder.
|
|
||||||
|
|
||||||
To force an explicit version instead of auto-incrementing:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
APP_VERSION=2026.03.09.10 BUILDER=colima ./scripts/docker-publish.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Update The Server
|
|
||||||
Once a new image is published, update the server with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/synapsis
|
|
||||||
docker compose pull
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Useful checks:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose ps
|
|
||||||
docker compose logs -f app
|
|
||||||
docker compose images
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Which Compose File Is Which
|
|
||||||
There are two main Docker compose paths in this repo.
|
|
||||||
|
|
||||||
### Local source-build compose
|
|
||||||
File: [docker/docker-compose.yml](/Users/christopher/Dev/Synapsis/Synapsis/docker/docker-compose.yml)
|
|
||||||
|
|
||||||
Purpose:
|
|
||||||
- local Docker testing
|
|
||||||
- builds from your current working tree
|
|
||||||
- no GHCR push required
|
|
||||||
|
|
||||||
### Production install compose
|
|
||||||
File: [docker-compose.yml](/Users/christopher/Dev/Synapsis/Synapsis/docker-compose.yml)
|
|
||||||
|
|
||||||
Purpose:
|
|
||||||
- end-user install
|
|
||||||
- server deployment
|
|
||||||
- uses `ghcr.io/gnosyslabs/synapsis:latest`
|
|
||||||
|
|
||||||
Do not confuse them.
|
|
||||||
|
|
||||||
## 6. Recommended Workflow
|
|
||||||
This is the default path that makes the most sense for Synapsis:
|
|
||||||
1. Make code changes locally
|
|
||||||
2. Run `npm run type-check`
|
|
||||||
3. Run `npm run build`
|
|
||||||
4. If Docker behavior matters, run `cd docker && docker compose up --build`
|
|
||||||
5. Keep stacking fixes until the server actually needs them
|
|
||||||
6. Commit and push
|
|
||||||
7. Build and push the GHCR image
|
|
||||||
8. Pull and restart on the server
|
|
||||||
|
|
||||||
## 7. When To Publish A New Docker Image
|
|
||||||
Publish when:
|
|
||||||
- you want the fix on the real server
|
|
||||||
- you changed install/runtime/container behavior
|
|
||||||
- you finished a coherent batch of fixes
|
|
||||||
|
|
||||||
Do not publish just because:
|
|
||||||
- one small UI bug was fixed locally
|
|
||||||
- one small API bug was fixed and not needed on the server yet
|
|
||||||
|
|
||||||
## 8. Current Install Reality
|
|
||||||
For clean servers, the normal install path is:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://synapsis.social/install.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
For servers that already run nginx or another reverse proxy on `80/443`, use:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://synapsis.social/install.sh | PROXY=none bash
|
|
||||||
```
|
|
||||||
|
|
||||||
In `PROXY=none` mode, Synapsis binds to `127.0.0.1:${PORT}` and your existing reverse proxy should point there.
|
|
||||||
|
|||||||
@@ -2,164 +2,85 @@
|
|||||||
|
|
||||||
**Your node. Your network. Your identity.**
|
**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 plain text server-stored messaging. No corporations. No lock-in. Just you and the network.
|
Synapsis is an open-source federated social network built around sovereign DIDs, portable user-owned media storage, and the Swarm network.
|
||||||
|
|
||||||
---
|
## Run a node on a VPS
|
||||||
|
|
||||||
## 🚀 Deploy Your Own Node (5 Minutes)
|
Synapsis runs directly under systemd. It does not require Docker, PostgreSQL, or a bundled reverse proxy.
|
||||||
|
|
||||||
Run your personal Synapsis node with a single command:
|
Prerequisites:
|
||||||
|
|
||||||
|
- A Linux VPS with systemd
|
||||||
|
- Node.js 20 or newer
|
||||||
|
- Git, npm, and OpenSSL
|
||||||
|
- Your own nginx, Caddy, Traefik, or other reverse proxy
|
||||||
|
|
||||||
|
Install from a checkout:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Bootstrap the deployment directory
|
sudo bash deploy/install.sh
|
||||||
curl -fsSL https://synapsis.social/install.sh | bash
|
sudo nano /etc/synapsis.env
|
||||||
|
sudo bash /opt/synapsis/deploy/update.sh
|
||||||
# 2. Edit /opt/synapsis/.env with your domain and admin email
|
|
||||||
nano /opt/synapsis/.env
|
|
||||||
|
|
||||||
# 3. Start your node
|
|
||||||
cd /opt/synapsis
|
|
||||||
docker compose up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Done! Your node is live at `https://your-domain.com` with automatic SSL. No build step. No dependencies. No fuss.
|
The service binds only to `127.0.0.1:43821`. Point your reverse proxy at that address and terminate TLS there. `PORT` can be overridden in `/etc/synapsis.env` if needed.
|
||||||
Database migrations run automatically on startup and during updates.
|
|
||||||
|
|
||||||
Set `DOMAIN` to the canonical host you want Synapsis to live on. If you also create a `www` DNS record, the bundled Caddy setup will redirect `www` to that canonical domain automatically.
|
The installer creates:
|
||||||
|
|
||||||
If your server already has nginx or another reverse proxy using `80/443`, use the advanced mode instead:
|
- Application checkout: `/opt/synapsis`
|
||||||
|
- Environment file: `/etc/synapsis.env`
|
||||||
|
- Embedded Turso database: `/var/lib/synapsis/synapsis.db`
|
||||||
|
- Service: `synapsis.service`
|
||||||
|
|
||||||
|
Useful commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://synapsis.social/install.sh | PROXY=none bash
|
sudo systemctl status synapsis
|
||||||
|
sudo journalctl -u synapsis -f
|
||||||
|
sudo /opt/synapsis/deploy/update.sh
|
||||||
|
sudo /opt/synapsis/deploy/uninstall.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
**Updating (migrations run automatically):**
|
Uninstalling preserves the database and environment by default. Pass `--purge-data` only when you intentionally want to remove both.
|
||||||
```bash
|
|
||||||
curl -fsSL https://synapsis.social/update.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
**Full uninstall:**
|
## Storage and account portability
|
||||||
```bash
|
|
||||||
curl -fsSL https://synapsis.social/uninstall.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
For detailed Docker setup, see [docker/README.md](docker/README.md).
|
The node database is a local embedded Turso/SQLite file. Media remains in each user's S3-compatible bucket so exported accounts can retain portable media URLs and move between Synapsis nodes without requiring the old node to transfer a shared upload directory.
|
||||||
|
|
||||||
---
|
Supported S3-compatible providers include AWS S3, Cloudflare R2, Backblaze B2, Wasabi, and Contabo. Synapsis stores each user's credentials encrypted with `AUTH_SECRET`.
|
||||||
|
|
||||||
## ✨ Features
|
## Development
|
||||||
|
|
||||||
- **🌐 Swarm Network** — Native peer-to-peer network with automatic node discovery and gossip protocol
|
|
||||||
- **💬 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
|
|
||||||
- **🖼️ Rich Media** — Image uploads, media galleries, and S3-compatible storage
|
|
||||||
- **🛡️ Built-in Moderation** — Admin dashboard for user management and content moderation
|
|
||||||
- **📱 Auto Port Detection** — Runs on the first available port (3000-3020) automatically
|
|
||||||
- **📊 Curated Feeds** — Smart algorithms highlighting engaging content across the swarm
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📖 Documentation
|
|
||||||
|
|
||||||
- **[Docker Deployment Guide](docker/README.md)** — Complete production deployment instructions
|
|
||||||
- **[User Guide](/guide)** — Learn how Synapsis works (visit after installing)
|
|
||||||
- **[API Documentation](https://docs.synapsis.social)** — Developer reference
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ Architecture
|
|
||||||
|
|
||||||
Synapsis differs from traditional social networks by prioritizing **sovereign identity** and **native peer-to-peer communication**.
|
|
||||||
|
|
||||||
### 🔐 Decentralized Identity (DIDs)
|
|
||||||
|
|
||||||
Unlike centralized platforms where your identity is a row in a database owned by a corporation, Synapsis uses a cryptographic identity system:
|
|
||||||
|
|
||||||
| Concept | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| **DID** | A unique, cryptographically-generated identifier (`did:key:...`) assigned to every user. This is your true identity that exists independently of any server. |
|
|
||||||
| **Handle** | A human-readable username (`@alice`) that points to your DID. Think of it like a domain name pointing to an IP address. |
|
|
||||||
| **Key Pair** | Every account has a public/private key pair. Your private key proves you are you; your public key lets others verify your identity. |
|
|
||||||
|
|
||||||
**Why this matters:**
|
|
||||||
- **Ownership** — Your identity is cryptographically yours, not controlled by a company
|
|
||||||
- **Authenticity** — Every post is signed with your private key, proving it came from you
|
|
||||||
- **True Portability** — Move your account between nodes without losing followers
|
|
||||||
|
|
||||||
### 🌐 The Swarm Network
|
|
||||||
|
|
||||||
Synapsis operates on the **Swarm** — a native peer-to-peer network designed specifically for Synapsis nodes:
|
|
||||||
|
|
||||||
- **Gossip Protocol** — Nodes discover each other automatically and exchange information
|
|
||||||
- **Swarm Timeline** — Aggregated feed of posts from across all Synapsis nodes
|
|
||||||
- **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
|
|
||||||
|
|
||||||
### 🆚 Synapsis vs. Traditional Federation
|
|
||||||
|
|
||||||
| Feature | Traditional Federation | Synapsis |
|
|
||||||
|---------|------------------------|----------|
|
|
||||||
| **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 | 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 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Development
|
|
||||||
|
|
||||||
Want to hack on Synapsis? Here's how to run it locally:
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- Node.js 20+
|
|
||||||
- PostgreSQL 15+
|
|
||||||
- S3-compatible storage (AWS S3, Cloudflare R2, Backblaze B2, Wasabi, or Contabo)
|
|
||||||
|
|
||||||
### Local Setup
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Clone the repository
|
git clone https://githop.xyz/GnosysLabs/Synapsis.git
|
||||||
git clone https://github.com/GnosysLabs/Synapsis.git
|
cd Synapsis
|
||||||
cd synapsis
|
|
||||||
|
|
||||||
# 2. Install dependencies
|
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# 3. Configure environment
|
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# Edit .env with your local database and storage settings
|
npm run db:migrate
|
||||||
|
|
||||||
# 4. Set up the database
|
|
||||||
npm run db:push
|
|
||||||
|
|
||||||
# 5. Run the development server
|
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Visit `http://localhost:3000` and register with an email listed in `ADMIN_EMAILS`. Local setup no longer uses a dedicated `/install` route.
|
The development database defaults to `./data/synapsis.db`. No separate database server is needed.
|
||||||
|
|
||||||
### Tech Stack
|
Common commands:
|
||||||
|
|
||||||
- **Framework** — [Next.js 15+](https://nextjs.org/) (App Router)
|
```bash
|
||||||
- **Database** — PostgreSQL with [Drizzle ORM](https://orm.drizzle.team/)
|
npm run type-check
|
||||||
- **Styling** — Tailwind CSS v4 & custom Vercel-like design system
|
npm test
|
||||||
- **Authentication** — Auth.js (NextAuth)
|
npm run db:generate
|
||||||
- **Type Safety** — TypeScript
|
npm run db:migrate
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
---
|
## Architecture
|
||||||
|
|
||||||
## 📜 License
|
- **Framework:** Next.js 16 and React 19
|
||||||
|
- **Database:** embedded Turso with Drizzle ORM's relational-query v2 API
|
||||||
|
- **Identity:** DIDs and per-user signing keys
|
||||||
|
- **Media:** user-owned S3-compatible storage
|
||||||
|
- **Federation:** Synapsis Swarm discovery and signed interactions
|
||||||
|
- **Deployment:** native Node.js process managed by systemd
|
||||||
|
|
||||||
Licensed under the **Apache 2.0 License**. See [LICENSE](LICENSE) for details.
|
## License
|
||||||
|
|
||||||
---
|
Licensed under the [Apache License 2.0](LICENSE).
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<strong>Run your node. Join the swarm. Own your social.</strong>
|
|
||||||
</p>
|
|
||||||
|
|||||||
Executable
+65
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_DIR="${APP_DIR:-/opt/synapsis}"
|
||||||
|
DATA_DIR="${DATA_DIR:-/var/lib/synapsis}"
|
||||||
|
ENV_FILE="${ENV_FILE:-/etc/synapsis.env}"
|
||||||
|
REPO_URL="${REPO_URL:-https://githop.xyz/GnosysLabs/Synapsis.git}"
|
||||||
|
BRANCH="${BRANCH:-main}"
|
||||||
|
|
||||||
|
if [[ ${EUID} -ne 0 ]]; then
|
||||||
|
echo "Run this installer as root." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for command in git node npm openssl runuser systemctl; do
|
||||||
|
command -v "$command" >/dev/null || { echo "Missing required command: $command" >&2; exit 1; }
|
||||||
|
done
|
||||||
|
|
||||||
|
node_major="$(node -p 'process.versions.node.split(`.`)[0]')"
|
||||||
|
if (( node_major < 20 )); then
|
||||||
|
echo "Synapsis requires Node.js 20 or newer." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! id synapsis >/dev/null 2>&1; then
|
||||||
|
useradd --system --home-dir "$DATA_DIR" --create-home --shell /usr/sbin/nologin synapsis
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -d -o synapsis -g synapsis -m 0750 "$DATA_DIR"
|
||||||
|
|
||||||
|
if [[ -e "$APP_DIR" ]]; then
|
||||||
|
echo "$APP_DIR already exists; use deploy/update.sh for an existing installation." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git clone --branch "$BRANCH" --single-branch "$REPO_URL" "$APP_DIR"
|
||||||
|
chown -R synapsis:synapsis "$APP_DIR"
|
||||||
|
|
||||||
|
if [[ ! -e "$ENV_FILE" ]]; then
|
||||||
|
auth_secret="$(openssl rand -base64 48 | tr -d '\n')"
|
||||||
|
port="43821"
|
||||||
|
install -m 0600 -o root -g synapsis /dev/null "$ENV_FILE"
|
||||||
|
{
|
||||||
|
echo "DATABASE_PATH=$DATA_DIR/synapsis.db"
|
||||||
|
echo "PORT=$port"
|
||||||
|
echo "AUTH_SECRET=$auth_secret"
|
||||||
|
echo "ADMIN_EMAILS=admin@example.com"
|
||||||
|
echo "NEXT_PUBLIC_NODE_DOMAIN=localhost:$port"
|
||||||
|
} > "$ENV_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -a
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
runuser -u synapsis -- npm --prefix "$APP_DIR" ci
|
||||||
|
runuser -u synapsis -- npm --prefix "$APP_DIR" run db:migrate
|
||||||
|
runuser -u synapsis -- npm --prefix "$APP_DIR" run build
|
||||||
|
|
||||||
|
install -m 0644 "$APP_DIR/deploy/synapsis.service" /etc/systemd/system/synapsis.service
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now synapsis
|
||||||
|
|
||||||
|
echo "Synapsis is listening on http://127.0.0.1:${PORT}"
|
||||||
|
echo "Edit $ENV_FILE, run $APP_DIR/deploy/update.sh, then configure your own reverse proxy."
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Synapsis node
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=synapsis
|
||||||
|
Group=synapsis
|
||||||
|
WorkingDirectory=/opt/synapsis
|
||||||
|
EnvironmentFile=/etc/synapsis.env
|
||||||
|
Environment=NODE_ENV=production
|
||||||
|
ExecStart=/usr/bin/env npm start
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/var/lib/synapsis /opt/synapsis/.next
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_DIR="${APP_DIR:-/opt/synapsis}"
|
||||||
|
DATA_DIR="${DATA_DIR:-/var/lib/synapsis}"
|
||||||
|
ENV_FILE="${ENV_FILE:-/etc/synapsis.env}"
|
||||||
|
|
||||||
|
if [[ ${EUID} -ne 0 ]]; then
|
||||||
|
echo "Run this uninstaller as root." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl disable --now synapsis 2>/dev/null || true
|
||||||
|
rm -f /etc/systemd/system/synapsis.service
|
||||||
|
systemctl daemon-reload
|
||||||
|
rm -rf "$APP_DIR"
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--purge-data" ]]; then
|
||||||
|
rm -rf "$DATA_DIR"
|
||||||
|
rm -f "$ENV_FILE"
|
||||||
|
userdel synapsis 2>/dev/null || true
|
||||||
|
echo "Synapsis and its database were removed."
|
||||||
|
else
|
||||||
|
echo "Synapsis was removed; $DATA_DIR and $ENV_FILE were preserved."
|
||||||
|
fi
|
||||||
Executable
+43
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_DIR="${APP_DIR:-/opt/synapsis}"
|
||||||
|
DATA_DIR="${DATA_DIR:-/var/lib/synapsis}"
|
||||||
|
ENV_FILE="${ENV_FILE:-/etc/synapsis.env}"
|
||||||
|
|
||||||
|
if [[ ${EUID} -ne 0 ]]; then
|
||||||
|
echo "Run this updater as root." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -d "$APP_DIR/.git" ]] || { echo "No Synapsis checkout found at $APP_DIR" >&2; exit 1; }
|
||||||
|
[[ -f "$ENV_FILE" ]] || { echo "Missing $ENV_FILE" >&2; exit 1; }
|
||||||
|
|
||||||
|
set -a
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
systemctl stop synapsis
|
||||||
|
trap 'systemctl start synapsis' EXIT
|
||||||
|
|
||||||
|
if [[ -f "${DATABASE_PATH:-$DATA_DIR/synapsis.db}" ]]; then
|
||||||
|
database_file="${DATABASE_PATH:-$DATA_DIR/synapsis.db}"
|
||||||
|
backup_dir="$DATA_DIR/backups/$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
install -d -o synapsis -g synapsis -m 0750 "$backup_dir"
|
||||||
|
cp "$database_file" "$backup_dir/synapsis.db"
|
||||||
|
for suffix in -wal -shm; do
|
||||||
|
if [[ -f "${database_file}${suffix}" ]]; then
|
||||||
|
cp "${database_file}${suffix}" "$backup_dir/synapsis.db${suffix}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
chown -R synapsis:synapsis "$backup_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
runuser -u synapsis -- git -C "$APP_DIR" pull --ff-only
|
||||||
|
runuser -u synapsis -- npm --prefix "$APP_DIR" ci
|
||||||
|
runuser -u synapsis -- npm --prefix "$APP_DIR" run db:migrate
|
||||||
|
runuser -u synapsis -- npm --prefix "$APP_DIR" run build
|
||||||
|
|
||||||
|
systemctl start synapsis
|
||||||
|
trap - EXIT
|
||||||
|
echo "Synapsis updated successfully."
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# Synapsis - Docker Compose (Production, External Reverse Proxy)
|
|
||||||
# Uses pre-built image from GitHub Container Registry
|
|
||||||
#
|
|
||||||
# Intended for hosts that already run nginx/Traefik/Caddy on 80/443.
|
|
||||||
# Synapsis binds only to localhost so your existing proxy can forward to it.
|
|
||||||
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
container_name: synapsis-db
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: ${DB_USER:-synapsis}
|
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
|
|
||||||
POSTGRES_DB: ${DB_NAME:-synapsis}
|
|
||||||
PGDATA: /var/lib/postgresql/data/pgdata
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
networks:
|
|
||||||
- synapsis-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-synapsis} -d ${DB_NAME:-synapsis}"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 1G
|
|
||||||
reservations:
|
|
||||||
memory: 256M
|
|
||||||
|
|
||||||
app:
|
|
||||||
image: ghcr.io/gnosyslabs/synapsis:latest
|
|
||||||
container_name: synapsis-app
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:${PORT:-3000}:${PORT:-3000}"
|
|
||||||
environment:
|
|
||||||
DATABASE_URL: postgresql://${DB_USER:-synapsis}:${DB_PASSWORD:-changeme}@postgres:5432/${DB_NAME:-synapsis}
|
|
||||||
AUTH_SECRET: ${AUTH_SECRET}
|
|
||||||
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}
|
|
||||||
BOT_MAX_PER_USER: ${BOT_MAX_PER_USER:-5}
|
|
||||||
HOST_UPDATER_SOCKET: ${HOST_UPDATER_SOCKET:-/var/run/synapsis-updater/updater.sock}
|
|
||||||
HOST_UPDATER_TOKEN: ${HOST_UPDATER_TOKEN:-}
|
|
||||||
PORT: ${PORT:-3000}
|
|
||||||
NODE_ENV: production
|
|
||||||
volumes:
|
|
||||||
- uploads_data:/app/uploads
|
|
||||||
- /var/run/synapsis-updater:/var/run/synapsis-updater
|
|
||||||
networks:
|
|
||||||
- synapsis-network
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:${PORT:-3000}/api/health || exit 1"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 60s
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 1G
|
|
||||||
reservations:
|
|
||||||
memory: 256M
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
driver: local
|
|
||||||
uploads_data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
networks:
|
|
||||||
synapsis-network:
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
# Synapsis - Docker Compose (Production)
|
|
||||||
# Uses pre-built image from GitHub Container Registry
|
|
||||||
#
|
|
||||||
# Quick Start:
|
|
||||||
# 1. Bootstrap the deployment directory:
|
|
||||||
# curl -fsSL https://synapsis.social/install.sh | bash
|
|
||||||
# 2. Configure (edit with your domain and admin email): nano /opt/synapsis/.env
|
|
||||||
# 3. Start your node: cd /opt/synapsis && docker compose up -d
|
|
||||||
#
|
|
||||||
# Services:
|
|
||||||
# - Synapsis Next.js application (from GHCR)
|
|
||||||
# - PostgreSQL database (persistent storage)
|
|
||||||
# - Caddy reverse proxy with automatic SSL
|
|
||||||
|
|
||||||
services:
|
|
||||||
# ============================================
|
|
||||||
# PostgreSQL Database
|
|
||||||
# ============================================
|
|
||||||
postgres:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
container_name: synapsis-db
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: ${DB_USER:-synapsis}
|
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
|
|
||||||
POSTGRES_DB: ${DB_NAME:-synapsis}
|
|
||||||
PGDATA: /var/lib/postgresql/data/pgdata
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
networks:
|
|
||||||
- synapsis-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-synapsis} -d ${DB_NAME:-synapsis}"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 1G
|
|
||||||
reservations:
|
|
||||||
memory: 256M
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Synapsis Application (from GHCR)
|
|
||||||
# ============================================
|
|
||||||
app:
|
|
||||||
image: ghcr.io/gnosyslabs/synapsis:latest
|
|
||||||
container_name: synapsis-app
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
# Database connection
|
|
||||||
DATABASE_URL: postgresql://${DB_USER:-synapsis}:${DB_PASSWORD:-changeme}@postgres:5432/${DB_NAME:-synapsis}
|
|
||||||
|
|
||||||
# Authentication
|
|
||||||
AUTH_SECRET: ${AUTH_SECRET}
|
|
||||||
|
|
||||||
# Domain configuration
|
|
||||||
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}
|
|
||||||
|
|
||||||
# Optional settings
|
|
||||||
BOT_MAX_PER_USER: ${BOT_MAX_PER_USER:-5}
|
|
||||||
HOST_UPDATER_SOCKET: ${HOST_UPDATER_SOCKET:-/var/run/synapsis-updater/updater.sock}
|
|
||||||
HOST_UPDATER_TOKEN: ${HOST_UPDATER_TOKEN:-}
|
|
||||||
|
|
||||||
# Port configuration (auto or specific port)
|
|
||||||
PORT: ${PORT:-3000}
|
|
||||||
|
|
||||||
# Node environment
|
|
||||||
NODE_ENV: production
|
|
||||||
volumes:
|
|
||||||
# Uploads directory (if using local file storage)
|
|
||||||
- uploads_data:/app/uploads
|
|
||||||
# Shared volume for port coordination with Caddy
|
|
||||||
- port_data:/var/run/synapsis
|
|
||||||
- /var/run/synapsis-updater:/var/run/synapsis-updater
|
|
||||||
networks:
|
|
||||||
- synapsis-network
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
# Increased start period to allow migrations to run
|
|
||||||
healthcheck:
|
|
||||||
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
|
|
||||||
start_period: 60s
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 1G
|
|
||||||
reservations:
|
|
||||||
memory: 256M
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Caddy Reverse Proxy (Automatic SSL)
|
|
||||||
# ============================================
|
|
||||||
caddy:
|
|
||||||
image: caddy:2-alpine
|
|
||||||
container_name: synapsis-caddy
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
volumes:
|
|
||||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
||||||
- ./caddy-entrypoint.sh:/usr/local/bin/caddy-entrypoint.sh:ro
|
|
||||||
- caddy_data:/data
|
|
||||||
- caddy_config:/config
|
|
||||||
# Shared volume for port coordination with app
|
|
||||||
- port_data:/var/run/synapsis
|
|
||||||
networks:
|
|
||||||
- synapsis-network
|
|
||||||
depends_on:
|
|
||||||
app:
|
|
||||||
condition: service_healthy
|
|
||||||
environment:
|
|
||||||
- DOMAIN=${DOMAIN:-localhost}
|
|
||||||
# Use custom entrypoint to read dynamic port from app
|
|
||||||
entrypoint: ["/usr/local/bin/caddy-entrypoint.sh"]
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Persistent Volumes
|
|
||||||
# ============================================
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
driver: local
|
|
||||||
uploads_data:
|
|
||||||
driver: local
|
|
||||||
caddy_data:
|
|
||||||
driver: local
|
|
||||||
caddy_config:
|
|
||||||
driver: local
|
|
||||||
port_data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Networks
|
|
||||||
# ============================================
|
|
||||||
networks:
|
|
||||||
synapsis-network:
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
# Dependencies
|
|
||||||
node_modules
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
pnpm-lock.yaml
|
|
||||||
|
|
||||||
# Build outputs
|
|
||||||
.next
|
|
||||||
out
|
|
||||||
dist
|
|
||||||
build
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Environment files (these are passed via docker-compose)
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
.env.dev
|
|
||||||
.env.test
|
|
||||||
.env.production
|
|
||||||
|
|
||||||
# Version control
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
.gitattributes
|
|
||||||
|
|
||||||
# IDE and editor files
|
|
||||||
.vscode
|
|
||||||
.idea
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Testing
|
|
||||||
coverage
|
|
||||||
.nyc_output
|
|
||||||
*.test.ts
|
|
||||||
*.test.tsx
|
|
||||||
*.spec.ts
|
|
||||||
*.spec.tsx
|
|
||||||
tests
|
|
||||||
test
|
|
||||||
__tests__
|
|
||||||
|
|
||||||
# Documentation (not needed in production image)
|
|
||||||
*.md
|
|
||||||
!README.md
|
|
||||||
LICENSE
|
|
||||||
CHANGELOG.md
|
|
||||||
CONTRIBUTING.md
|
|
||||||
|
|
||||||
# Docker files (avoid recursive copy)
|
|
||||||
docker
|
|
||||||
docker-compose*.yml
|
|
||||||
Dockerfile*
|
|
||||||
.dockerignore
|
|
||||||
|
|
||||||
# CI/CD
|
|
||||||
.github
|
|
||||||
.gitlab-ci.yml
|
|
||||||
.travis.yml
|
|
||||||
azure-pipelines.yml
|
|
||||||
Jenkinsfile
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Misc
|
|
||||||
.cache
|
|
||||||
temp
|
|
||||||
tmp
|
|
||||||
.tmp
|
|
||||||
uploads/*
|
|
||||||
!uploads/.gitkeep
|
|
||||||
|
|
||||||
# Large binary files
|
|
||||||
*.mp4
|
|
||||||
*.mov
|
|
||||||
*.avi
|
|
||||||
*.mkv
|
|
||||||
*.pdf
|
|
||||||
*.zip
|
|
||||||
*.tar.gz
|
|
||||||
*.rar
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# ===========================================
|
|
||||||
# Synapsis Environment Configuration
|
|
||||||
# ===========================================
|
|
||||||
# Copy this file to .env and configure your values
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# REQUIRED: Core Settings
|
|
||||||
# ===========================================
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Authentication secret - generate with: openssl rand -hex 32
|
|
||||||
AUTH_SECRET=your-secret-key-here-minimum-32-characters
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# REQUIRED: Database
|
|
||||||
# ===========================================
|
|
||||||
|
|
||||||
DB_USER=synapsis
|
|
||||||
DB_PASSWORD=your-secure-password-here
|
|
||||||
DB_NAME=synapsis
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# OPTIONAL: General Settings
|
|
||||||
# ===========================================
|
|
||||||
|
|
||||||
# Application port.
|
|
||||||
# Default install (with bundled Caddy): PORT=auto scans 3000-3020 automatically.
|
|
||||||
# Existing reverse proxy mode (PROXY=none): use a fixed localhost port, e.g. PORT=3000.
|
|
||||||
PORT=auto
|
|
||||||
|
|
||||||
# Host updater integration for the admin panel.
|
|
||||||
# These are generated by the installer for Docker installs.
|
|
||||||
# HOST_UPDATER_SOCKET=/var/run/synapsis-updater/updater.sock
|
|
||||||
# HOST_UPDATER_TOKEN=
|
|
||||||
|
|
||||||
# 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
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# Caddyfile for Synapsis
|
|
||||||
# Automatic HTTPS via Let's Encrypt
|
|
||||||
|
|
||||||
www.{$DOMAIN} {
|
|
||||||
redir https://{$DOMAIN}{uri} permanent
|
|
||||||
}
|
|
||||||
|
|
||||||
{$DOMAIN} {
|
|
||||||
# Reverse proxy to Synapsis app (port is set via APP_PORT env var)
|
|
||||||
reverse_proxy app:{$APP_PORT:3000}
|
|
||||||
|
|
||||||
# Enable compression
|
|
||||||
encode gzip
|
|
||||||
|
|
||||||
# Security headers
|
|
||||||
header {
|
|
||||||
# Enable HSTS
|
|
||||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
|
||||||
# Prevent clickjacking
|
|
||||||
X-Frame-Options "SAMEORIGIN"
|
|
||||||
# Prevent MIME type sniffing
|
|
||||||
X-Content-Type-Options "nosniff"
|
|
||||||
# XSS protection
|
|
||||||
X-XSS-Protection "1; mode=block"
|
|
||||||
# Referrer policy
|
|
||||||
Referrer-Policy "strict-origin-when-cross-origin"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
log {
|
|
||||||
output file /data/access.log
|
|
||||||
format json
|
|
||||||
}
|
|
||||||
|
|
||||||
# Increase max body size for uploads (20MB)
|
|
||||||
request_body {
|
|
||||||
max_size 20MB
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Health check endpoint for Docker
|
|
||||||
:8080 {
|
|
||||||
respond /health "healthy" 200
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
# Synapsis - Multi-stage Production Dockerfile
|
|
||||||
# This Dockerfile creates a production-ready image that:
|
|
||||||
# - Keeps source files immutable (prevents accidental modifications)
|
|
||||||
# - Optimizes build size with multi-stage approach
|
|
||||||
# - Runs as non-root user for security
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Stage 1: Dependencies
|
|
||||||
# ============================================
|
|
||||||
FROM node:22-alpine AS deps
|
|
||||||
RUN apk add --no-cache libc6-compat openssl
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy package files for efficient layer caching
|
|
||||||
COPY package.json package-lock.json* ./
|
|
||||||
|
|
||||||
# Install all dependencies (production + dev all in dependencies now)
|
|
||||||
RUN npm ci --ignore-scripts && npm cache clean --force
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Stage 2: Builder
|
|
||||||
# ============================================
|
|
||||||
FROM node:22-alpine AS builder
|
|
||||||
RUN apk add --no-cache libc6-compat openssl
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy dependencies from deps stage
|
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
|
||||||
|
|
||||||
# Copy all source files
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Set environment variables for build
|
|
||||||
ARG APP_VERSION=dev
|
|
||||||
ARG APP_COMMIT=unknown
|
|
||||||
ARG APP_BUILD_DATE=
|
|
||||||
ARG APP_GITHUB_URL=
|
|
||||||
ARG APP_IMAGE_REPO=ghcr.io/gnosyslabs/synapsis
|
|
||||||
ARG APP_SOURCE_REPO=https://github.com/GnosysLabs/Synapsis
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
|
||||||
ENV APP_VERSION=${APP_VERSION}
|
|
||||||
ENV APP_COMMIT=${APP_COMMIT}
|
|
||||||
ENV APP_BUILD_DATE=${APP_BUILD_DATE}
|
|
||||||
ENV APP_GITHUB_URL=${APP_GITHUB_URL}
|
|
||||||
ENV APP_IMAGE_REPO=${APP_IMAGE_REPO}
|
|
||||||
ENV APP_SOURCE_REPO=${APP_SOURCE_REPO}
|
|
||||||
|
|
||||||
# Build the Next.js application
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Stage 3: Production Dependencies
|
|
||||||
# ============================================
|
|
||||||
FROM deps AS prod-deps
|
|
||||||
RUN npm prune --omit=dev && npm cache clean --force
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Stage 4: Production Runner
|
|
||||||
# ============================================
|
|
||||||
FROM node:22-alpine AS runner
|
|
||||||
RUN apk add --no-cache libc6-compat openssl netcat-openbsd wget
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ARG APP_VERSION=dev
|
|
||||||
ARG APP_COMMIT=unknown
|
|
||||||
ARG APP_BUILD_DATE=
|
|
||||||
ARG APP_GITHUB_URL=
|
|
||||||
ARG APP_IMAGE_REPO=ghcr.io/gnosyslabs/synapsis
|
|
||||||
ARG APP_SOURCE_REPO=https://github.com/GnosysLabs/Synapsis
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.source="${APP_SOURCE_REPO}"
|
|
||||||
LABEL org.opencontainers.image.description="Synapsis self-hosted social node"
|
|
||||||
LABEL org.opencontainers.image.version="${APP_VERSION}"
|
|
||||||
LABEL org.opencontainers.image.revision="${APP_COMMIT}"
|
|
||||||
LABEL org.opencontainers.image.created="${APP_BUILD_DATE}"
|
|
||||||
|
|
||||||
# Create non-root user for security
|
|
||||||
RUN addgroup --system --gid 1001 nodejs && \
|
|
||||||
adduser --system --uid 1001 nextjs
|
|
||||||
|
|
||||||
# Create shared directory for port file (must be writable by nextjs)
|
|
||||||
RUN mkdir -p /var/run/synapsis && chown nextjs:nodejs /var/run/synapsis
|
|
||||||
|
|
||||||
# Set production environment
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
|
||||||
ENV PORT=3000
|
|
||||||
ENV HOSTNAME="0.0.0.0"
|
|
||||||
ENV APP_VERSION=${APP_VERSION}
|
|
||||||
ENV APP_COMMIT=${APP_COMMIT}
|
|
||||||
ENV APP_BUILD_DATE=${APP_BUILD_DATE}
|
|
||||||
ENV APP_GITHUB_URL=${APP_GITHUB_URL}
|
|
||||||
ENV APP_IMAGE_REPO=${APP_IMAGE_REPO}
|
|
||||||
ENV APP_SOURCE_REPO=${APP_SOURCE_REPO}
|
|
||||||
|
|
||||||
# Copy public assets
|
|
||||||
COPY --from=builder /app/public ./public
|
|
||||||
|
|
||||||
# Copy Next.js build output
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
||||||
|
|
||||||
# Copy pruned runtime dependencies
|
|
||||||
COPY --from=prod-deps --chown=nextjs:nodejs /app/node_modules ./node_modules
|
|
||||||
|
|
||||||
# Copy drizzle migrations, config, and schema (needed for migrations)
|
|
||||||
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/package.json ./
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/src/db/schema.ts ./src/db/schema.ts
|
|
||||||
|
|
||||||
# Copy entrypoint script
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/docker/docker-entrypoint.sh ./
|
|
||||||
RUN chmod +x ./docker-entrypoint.sh
|
|
||||||
|
|
||||||
# Switch to non-root user
|
|
||||||
USER nextjs
|
|
||||||
|
|
||||||
# Expose port range for auto-detection
|
|
||||||
EXPOSE 3000-3020
|
|
||||||
|
|
||||||
# Health check - longer start period to allow migrations to run
|
|
||||||
# Reads dynamic port file when PORT=auto
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=5 \
|
|
||||||
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"]
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
# Synapsis Docker Deployment
|
|
||||||
|
|
||||||
Production Docker deployment using pre-built images from GitHub Container Registry.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
|
||||||
|
|
||||||
This is the default install path for a fresh VPS where Synapsis should manage its own HTTPS with Caddy.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://synapsis.social/install.sh | bash
|
|
||||||
nano /opt/synapsis/.env # Add your domain and admin email
|
|
||||||
cd /opt/synapsis
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Your node is live at `https://your-domain.com` with automatic SSL.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 What You Need
|
|
||||||
|
|
||||||
| Requirement | Details |
|
|
||||||
|-------------|---------|
|
|
||||||
| **Server** | 2GB RAM, 2 CPU cores, 20GB SSD (minimum) |
|
|
||||||
| **Domain** | A domain or subdomain pointing to your server |
|
|
||||||
| **Docker** | Installed automatically by `install.sh` when missing on supported Linux hosts |
|
|
||||||
| **Ports** | `80` and `443` must be free for the default Caddy install |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ Configuration
|
|
||||||
|
|
||||||
Edit `.env` and set these required values (domain should be host only, no scheme or path):
|
|
||||||
|
|
||||||
| Variable | What to put |
|
|
||||||
|----------|-------------|
|
|
||||||
| `DOMAIN` | Your domain (e.g., `synapsis.example.com`) |
|
|
||||||
| `DB_PASSWORD` | Strong password for PostgreSQL |
|
|
||||||
| `AUTH_SECRET` | Run: `openssl rand -hex 32` |
|
|
||||||
| `ADMIN_EMAILS` | Your email address |
|
|
||||||
|
|
||||||
Use the bare/canonical host in `DOMAIN`. Example: set `DOMAIN=synapsis.example.com`, not `www.synapsis.example.com`.
|
|
||||||
If you also want `www.synapsis.example.com` to work, create a DNS record for `www` pointing to the same server. The bundled Caddy config will redirect `www` to the canonical `DOMAIN`.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Advanced: Existing nginx/Traefik/Caddy Host
|
|
||||||
|
|
||||||
If your server already runs a reverse proxy on `80/443`, use the advanced mode:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://synapsis.social/install.sh | PROXY=none bash
|
|
||||||
nano /opt/synapsis/.env
|
|
||||||
cd /opt/synapsis
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
This mode:
|
|
||||||
- skips the bundled Caddy service
|
|
||||||
- binds Synapsis to `127.0.0.1:${PORT:-3000}`
|
|
||||||
- expects your existing reverse proxy to forward traffic there
|
|
||||||
|
|
||||||
In `PROXY=none` mode, `PORT` is the localhost port your reverse proxy should target. The installer automatically changes `PORT=auto` to `PORT=3000` unless you override it.
|
|
||||||
|
|
||||||
Example nginx site:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
server {
|
|
||||||
server_name node.example.com;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://127.0.0.1:3000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
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_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 Updates (migrations run automatically)
|
|
||||||
|
|
||||||
Use the updater for existing installs. It preserves `.env`, refreshes the installed compose/proxy files, pulls the latest published image, and restarts the stack.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://synapsis.social/update.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
If the host was installed with `PROXY=none`, the updater detects that and keeps using the proxyless compose file automatically.
|
|
||||||
|
|
||||||
Manual update still works if you only want to restart with the already-installed files:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/synapsis
|
|
||||||
docker compose pull
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🗑️ Full Uninstall
|
|
||||||
|
|
||||||
To remove Synapsis completely from a host and start over:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://synapsis.social/uninstall.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
The uninstaller destroys the Synapsis containers, volumes, network, cached Synapsis images, and `/opt/synapsis`. It requires typing `DELETE` unless you set `FORCE=1`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Common Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# View logs
|
|
||||||
docker compose logs -f app
|
|
||||||
|
|
||||||
# Restart services
|
|
||||||
docker compose restart app
|
|
||||||
|
|
||||||
# Stop everything
|
|
||||||
docker compose down
|
|
||||||
|
|
||||||
# Database backup
|
|
||||||
docker compose exec postgres pg_dump -U synapsis synapsis > backup.sql
|
|
||||||
|
|
||||||
# Access database
|
|
||||||
docker compose exec postgres psql -U synapsis -d synapsis
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 Troubleshooting
|
|
||||||
|
|
||||||
### Container won't start
|
|
||||||
```bash
|
|
||||||
docker compose config # Validate config
|
|
||||||
docker compose logs app --tail=50 # Check errors
|
|
||||||
```
|
|
||||||
|
|
||||||
### Port already in use
|
|
||||||
If the installer says `80` or `443` is already in use, another reverse proxy is already bound there.
|
|
||||||
|
|
||||||
Use a fresh VPS for the default Caddy install, or rerun the installer in advanced mode:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://synapsis.social/install.sh | PROXY=none bash
|
|
||||||
```
|
|
||||||
|
|
||||||
For the application port itself, `PORT=auto` (default) automatically finds an available port. If you set a specific port that's taken:
|
|
||||||
```bash
|
|
||||||
# Check what's using the port
|
|
||||||
sudo netstat -tlnp | grep :3000
|
|
||||||
|
|
||||||
# Choose a different fixed port in /opt/synapsis/.env
|
|
||||||
# Example: PORT=3013
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database connection failed
|
|
||||||
```bash
|
|
||||||
# Check database health
|
|
||||||
docker compose ps
|
|
||||||
|
|
||||||
# Verify environment variables loaded
|
|
||||||
docker compose exec app env | grep DATABASE
|
|
||||||
```
|
|
||||||
|
|
||||||
### SSL certificate issues
|
|
||||||
```bash
|
|
||||||
# Check Caddy logs
|
|
||||||
docker compose logs caddy
|
|
||||||
|
|
||||||
# Test Caddy config
|
|
||||||
docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
|
|
||||||
```
|
|
||||||
|
|
||||||
### Image pull fails
|
|
||||||
```bash
|
|
||||||
# Verify image exists
|
|
||||||
docker pull ghcr.io/gnosyslabs/synapsis:latest
|
|
||||||
|
|
||||||
# Check the published package tags in GitHub Container Registry
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💾 Backup Strategy
|
|
||||||
|
|
||||||
Create `/opt/synapsis/backup.sh`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
BACKUP_DIR="/var/backups/synapsis"
|
|
||||||
DATE=$(date +%Y%m%d_%H%M%S)
|
|
||||||
mkdir -p $BACKUP_DIR
|
|
||||||
|
|
||||||
# Database backup
|
|
||||||
docker compose exec -T postgres pg_dump -U synapsis synapsis > "$BACKUP_DIR/db_$DATE.sql"
|
|
||||||
|
|
||||||
echo "✅ Backup complete: $DATE"
|
|
||||||
```
|
|
||||||
|
|
||||||
Schedule daily backups:
|
|
||||||
```bash
|
|
||||||
chmod +x /opt/synapsis/backup.sh
|
|
||||||
echo "0 2 * * * /opt/synapsis/backup.sh" | sudo crontab -
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ Building from Source
|
|
||||||
|
|
||||||
To build locally instead of using pre-built images:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/GnosysLabs/Synapsis.git
|
|
||||||
cd synapsis/docker
|
|
||||||
docker compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
To publish a stamped GHCR image with embedded app version, commit, and build date:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /path/to/Synapsis
|
|
||||||
./scripts/docker-publish.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
That script automatically publishes:
|
|
||||||
- `ghcr.io/gnosyslabs/synapsis:latest`
|
|
||||||
- `ghcr.io/gnosyslabs/synapsis:<YYYY.MM.DD.N>`
|
|
||||||
- `ghcr.io/gnosyslabs/synapsis:<short-sha>`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For full documentation, visit [docs.synapsis.social](https://docs.synapsis.social)
|
|
||||||
Binary file not shown.
@@ -1,58 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Caddy Entrypoint Script for Synapsis
|
|
||||||
# Reads the dynamic port from the shared file and starts Caddy
|
|
||||||
|
|
||||||
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
|
|
||||||
ELAPSED=0
|
|
||||||
|
|
||||||
while [ ! -f "$SHARED_PORT_FILE" ] && [ $ELAPSED -lt $TIMEOUT ]; do
|
|
||||||
sleep 1
|
|
||||||
ELAPSED=$((ELAPSED + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -f "$SHARED_PORT_FILE" ]; then
|
|
||||||
APP_PORT=$(cat "$SHARED_PORT_FILE")
|
|
||||||
echo "✅ Found Synapsis app on port: $APP_PORT"
|
|
||||||
else
|
|
||||||
echo "⚠️ Port file not found after ${TIMEOUT}s, using default port: $DEFAULT_PORT"
|
|
||||||
APP_PORT=$DEFAULT_PORT
|
|
||||||
fi
|
|
||||||
|
|
||||||
export APP_PORT
|
|
||||||
|
|
||||||
# Validate that APP_PORT is a number
|
|
||||||
if ! echo "$APP_PORT" | grep -qE '^[0-9]+$'; then
|
|
||||||
echo "❌ Invalid port number: $APP_PORT"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "🌐 Starting Caddy reverse proxy to app:$APP_PORT..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Start Caddy with the environment variable set
|
|
||||||
exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
# Synapsis - Docker Compose
|
|
||||||
# One-command deployment: docker-compose up -d
|
|
||||||
#
|
|
||||||
# Services:
|
|
||||||
# - Synapsis Next.js application
|
|
||||||
# - PostgreSQL database (persistent storage)
|
|
||||||
# - Caddy reverse proxy with automatic SSL
|
|
||||||
#
|
|
||||||
# All state is stored in Docker volumes - source code is immutable in the image
|
|
||||||
|
|
||||||
services:
|
|
||||||
# ============================================
|
|
||||||
# PostgreSQL Database
|
|
||||||
# ============================================
|
|
||||||
postgres:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
container_name: synapsis-db
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: ${DB_USER:-synapsis}
|
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
|
|
||||||
POSTGRES_DB: ${DB_NAME:-synapsis}
|
|
||||||
PGDATA: /var/lib/postgresql/data/pgdata
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
networks:
|
|
||||||
- synapsis-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-synapsis} -d ${DB_NAME:-synapsis}"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 1G
|
|
||||||
reservations:
|
|
||||||
memory: 256M
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Synapsis Application
|
|
||||||
# ============================================
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: ..
|
|
||||||
dockerfile: docker/Dockerfile
|
|
||||||
image: synapsis:latest
|
|
||||||
container_name: synapsis-app
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
# Database connection
|
|
||||||
DATABASE_URL: postgresql://${DB_USER:-synapsis}:${DB_PASSWORD:-changeme}@postgres:5432/${DB_NAME:-synapsis}
|
|
||||||
|
|
||||||
# Authentication
|
|
||||||
AUTH_SECRET: ${AUTH_SECRET}
|
|
||||||
|
|
||||||
# Domain configuration
|
|
||||||
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}
|
|
||||||
|
|
||||||
# Optional settings
|
|
||||||
BOT_MAX_PER_USER: ${BOT_MAX_PER_USER:-5}
|
|
||||||
|
|
||||||
# Port configuration (auto or specific port)
|
|
||||||
PORT: ${PORT:-3000}
|
|
||||||
|
|
||||||
# Node environment
|
|
||||||
NODE_ENV: production
|
|
||||||
volumes:
|
|
||||||
# Uploads directory (if using local file storage)
|
|
||||||
- uploads_data:/app/uploads
|
|
||||||
# Shared volume for port coordination with Caddy
|
|
||||||
- port_data:/var/run/synapsis
|
|
||||||
networks:
|
|
||||||
- synapsis-network
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
|
||||||
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
|
|
||||||
start_period: 40s
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 1G
|
|
||||||
reservations:
|
|
||||||
memory: 256M
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Caddy Reverse Proxy (Automatic SSL)
|
|
||||||
# ============================================
|
|
||||||
caddy:
|
|
||||||
image: caddy:2-alpine
|
|
||||||
container_name: synapsis-caddy
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
volumes:
|
|
||||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
||||||
- ./caddy-entrypoint.sh:/usr/local/bin/caddy-entrypoint.sh:ro
|
|
||||||
- caddy_data:/data
|
|
||||||
- caddy_config:/config
|
|
||||||
# Shared volume for port coordination with app
|
|
||||||
- port_data:/var/run/synapsis
|
|
||||||
networks:
|
|
||||||
- synapsis-network
|
|
||||||
depends_on:
|
|
||||||
app:
|
|
||||||
condition: service_healthy
|
|
||||||
environment:
|
|
||||||
- DOMAIN=${DOMAIN:-localhost}
|
|
||||||
# Use custom entrypoint to read dynamic port from app
|
|
||||||
entrypoint: ["/usr/local/bin/caddy-entrypoint.sh"]
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Persistent Volumes
|
|
||||||
# ============================================
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
driver: local
|
|
||||||
uploads_data:
|
|
||||||
driver: local
|
|
||||||
caddy_data:
|
|
||||||
driver: local
|
|
||||||
caddy_config:
|
|
||||||
driver: local
|
|
||||||
port_data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Networks
|
|
||||||
# ============================================
|
|
||||||
networks:
|
|
||||||
synapsis-network:
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Synapsis Docker Entrypoint Script
|
|
||||||
# Handles database migrations, port detection, and application startup
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Port Detection Configuration
|
|
||||||
# ============================================
|
|
||||||
PORT_START=${PORT_START:-3000}
|
|
||||||
PORT_END=${PORT_END:-3020}
|
|
||||||
|
|
||||||
# Function to check if a port is available
|
|
||||||
check_port_available() {
|
|
||||||
local port=$1
|
|
||||||
if ! nc -z localhost "$port" 2>/dev/null; then
|
|
||||||
return 0 # Port is available
|
|
||||||
else
|
|
||||||
return 1 # Port is in use
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to find first available port in range
|
|
||||||
find_available_port() {
|
|
||||||
local start=$1
|
|
||||||
local end=$2
|
|
||||||
|
|
||||||
for port in $(seq "$start" "$end"); do
|
|
||||||
if check_port_available "$port"; then
|
|
||||||
echo "$port"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "ERROR: No available ports found in range $start-$end" >&2
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Handle PORT=auto
|
|
||||||
if [ "${PORT}" = "auto" ]; then
|
|
||||||
echo "🔍 PORT=auto detected, scanning for available port in range ${PORT_START}-${PORT_END}..."
|
|
||||||
|
|
||||||
DETECTED_PORT=$(find_available_port "$PORT_START" "$PORT_END")
|
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "❌ Failed to find an available port. Exiting."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export PORT="$DETECTED_PORT"
|
|
||||||
echo "✅ Using automatically detected port: $PORT"
|
|
||||||
else
|
|
||||||
echo "📡 Using configured port: $PORT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure PORT is set
|
|
||||||
if [ -z "$PORT" ]; then
|
|
||||||
echo "⚠️ PORT not set, defaulting to 3000"
|
|
||||||
export PORT=3000
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Write port to shared file for Caddy to read
|
|
||||||
SHARED_PORT_FILE="/var/run/synapsis/port"
|
|
||||||
mkdir -p "$(dirname "$SHARED_PORT_FILE")"
|
|
||||||
echo "$PORT" > "$SHARED_PORT_FILE"
|
|
||||||
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
|
|
||||||
|
|
||||||
# Final startup message
|
|
||||||
echo ""
|
|
||||||
echo "========================================"
|
|
||||||
echo " 🚀 Starting Synapsis Server"
|
|
||||||
echo "========================================"
|
|
||||||
echo " Environment: $NODE_ENV"
|
|
||||||
echo " Port: $PORT"
|
|
||||||
echo " Node Version: $(node --version)"
|
|
||||||
echo "========================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Start the application
|
|
||||||
exec node server.js
|
|
||||||
@@ -1,357 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import signal
|
|
||||||
import socketserver
|
|
||||||
import subprocess
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from http import HTTPStatus
|
|
||||||
from http.server import BaseHTTPRequestHandler
|
|
||||||
|
|
||||||
SOCKET_PATH = os.environ.get("HOST_UPDATER_SOCKET", "/var/run/synapsis-updater/updater.sock")
|
|
||||||
TOKEN = os.environ.get("HOST_UPDATER_TOKEN", "")
|
|
||||||
STATUS_FILE = os.environ.get("HOST_UPDATER_STATUS_FILE", "/opt/synapsis/updater-status.json")
|
|
||||||
CONFIG_FILE = os.environ.get("HOST_UPDATER_CONFIG_FILE", "/opt/synapsis/updater-config.json")
|
|
||||||
LOG_FILE = os.environ.get("HOST_UPDATER_LOG_FILE", "/opt/synapsis/updater.log")
|
|
||||||
UPDATE_SCRIPT = os.environ.get("HOST_UPDATER_SCRIPT", "/opt/synapsis/update-local.sh")
|
|
||||||
INSTALL_DIR = os.environ.get("INSTALL_DIR", "/opt/synapsis")
|
|
||||||
AUTO_UPDATE_INTERVAL_MINUTES = max(5, int(os.environ.get("HOST_UPDATER_INTERVAL_MINUTES", "30")))
|
|
||||||
|
|
||||||
status_lock = threading.RLock()
|
|
||||||
current_process = None
|
|
||||||
last_auto_trigger_at = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def now_iso():
|
|
||||||
return datetime.now(timezone.utc).isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_parent_dirs():
|
|
||||||
os.makedirs(os.path.dirname(SOCKET_PATH), exist_ok=True)
|
|
||||||
os.makedirs(os.path.dirname(STATUS_FILE), exist_ok=True)
|
|
||||||
os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
|
|
||||||
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
def default_config():
|
|
||||||
return {
|
|
||||||
"autoUpdateEnabled": True,
|
|
||||||
"intervalMinutes": AUTO_UPDATE_INTERVAL_MINUTES,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def load_config():
|
|
||||||
if not os.path.exists(CONFIG_FILE):
|
|
||||||
config = default_config()
|
|
||||||
save_config(config)
|
|
||||||
return config
|
|
||||||
|
|
||||||
with open(CONFIG_FILE, "r", encoding="utf-8") as handle:
|
|
||||||
stored = json.load(handle)
|
|
||||||
|
|
||||||
config = default_config()
|
|
||||||
config.update({
|
|
||||||
"autoUpdateEnabled": bool(stored.get("autoUpdateEnabled", True)),
|
|
||||||
"intervalMinutes": max(5, int(stored.get("intervalMinutes", AUTO_UPDATE_INTERVAL_MINUTES))),
|
|
||||||
})
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def save_config(config):
|
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as handle:
|
|
||||||
json.dump(config, handle, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
def update_config(**changes):
|
|
||||||
config = load_config()
|
|
||||||
config.update(changes)
|
|
||||||
config["autoUpdateEnabled"] = bool(config.get("autoUpdateEnabled", True))
|
|
||||||
config["intervalMinutes"] = max(5, int(config.get("intervalMinutes", AUTO_UPDATE_INTERVAL_MINUTES)))
|
|
||||||
save_config(config)
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def load_status():
|
|
||||||
if not os.path.exists(STATUS_FILE):
|
|
||||||
return {
|
|
||||||
"available": True,
|
|
||||||
"status": "idle",
|
|
||||||
"message": "Ready to update.",
|
|
||||||
"lastStartedAt": None,
|
|
||||||
"lastFinishedAt": None,
|
|
||||||
"lastExitCode": None,
|
|
||||||
"lastError": None,
|
|
||||||
"pid": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
with open(STATUS_FILE, "r", encoding="utf-8") as handle:
|
|
||||||
return json.load(handle)
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_status_on_startup():
|
|
||||||
status = load_status()
|
|
||||||
|
|
||||||
if status.get("status") == "error" and status.get("lastExitCode") == -15:
|
|
||||||
status.update({
|
|
||||||
"status": "success",
|
|
||||||
"message": "Synapsis update completed.",
|
|
||||||
"lastExitCode": 0,
|
|
||||||
"lastError": None,
|
|
||||||
"pid": None,
|
|
||||||
})
|
|
||||||
save_status(status)
|
|
||||||
|
|
||||||
if status.get("status") == "updating":
|
|
||||||
status.update({
|
|
||||||
"status": "idle",
|
|
||||||
"message": "Ready to update.",
|
|
||||||
"lastError": None,
|
|
||||||
"pid": None,
|
|
||||||
})
|
|
||||||
save_status(status)
|
|
||||||
|
|
||||||
|
|
||||||
def save_status(status):
|
|
||||||
with open(STATUS_FILE, "w", encoding="utf-8") as handle:
|
|
||||||
json.dump(status, handle, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
def update_status(**changes):
|
|
||||||
with status_lock:
|
|
||||||
status = load_status()
|
|
||||||
status.update(changes)
|
|
||||||
save_status(status)
|
|
||||||
return status
|
|
||||||
|
|
||||||
|
|
||||||
def get_status_payload():
|
|
||||||
status = load_status()
|
|
||||||
status["config"] = load_config()
|
|
||||||
return status
|
|
||||||
|
|
||||||
|
|
||||||
def is_authorized(headers):
|
|
||||||
expected = f"Bearer {TOKEN}"
|
|
||||||
return bool(TOKEN) and headers.get("Authorization", "") == expected
|
|
||||||
|
|
||||||
|
|
||||||
def watch_process(process):
|
|
||||||
global current_process
|
|
||||||
exit_code = process.wait()
|
|
||||||
with status_lock:
|
|
||||||
current_process = None
|
|
||||||
update_status(
|
|
||||||
status="success" if exit_code == 0 else "error",
|
|
||||||
message="Synapsis update completed." if exit_code == 0 else "Synapsis update failed.",
|
|
||||||
lastFinishedAt=now_iso(),
|
|
||||||
lastExitCode=exit_code,
|
|
||||||
lastError=None if exit_code == 0 else f"Updater exited with code {exit_code}",
|
|
||||||
pid=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def start_update_process(trigger="manual"):
|
|
||||||
global current_process
|
|
||||||
|
|
||||||
# Give the app enough time to return the 202 response before the container restarts.
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
with status_lock:
|
|
||||||
log_handle = open(LOG_FILE, "a", encoding="utf-8")
|
|
||||||
current_process = subprocess.Popen(
|
|
||||||
[UPDATE_SCRIPT],
|
|
||||||
cwd=INSTALL_DIR,
|
|
||||||
stdout=log_handle,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
start_new_session=True,
|
|
||||||
env={
|
|
||||||
**os.environ,
|
|
||||||
"INSTALL_DIR": INSTALL_DIR,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
update_status(pid=current_process.pid, trigger=trigger)
|
|
||||||
|
|
||||||
thread = threading.Thread(target=watch_process, args=(current_process,), daemon=True)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
|
|
||||||
def schedule_update(trigger="manual"):
|
|
||||||
with status_lock:
|
|
||||||
if current_process and current_process.poll() is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
update_status(
|
|
||||||
status="updating",
|
|
||||||
message="Synapsis update scheduled." if trigger == "manual" else "Automatic Synapsis update scheduled.",
|
|
||||||
lastStartedAt=now_iso(),
|
|
||||||
lastFinishedAt=None,
|
|
||||||
lastExitCode=None,
|
|
||||||
lastError=None,
|
|
||||||
pid=None,
|
|
||||||
trigger=trigger,
|
|
||||||
)
|
|
||||||
|
|
||||||
thread = threading.Thread(target=start_update_process, args=(trigger,), daemon=True)
|
|
||||||
thread.start()
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def auto_update_loop():
|
|
||||||
global last_auto_trigger_at
|
|
||||||
while True:
|
|
||||||
time.sleep(60)
|
|
||||||
try:
|
|
||||||
config = load_config()
|
|
||||||
if not config.get("autoUpdateEnabled", True):
|
|
||||||
continue
|
|
||||||
|
|
||||||
interval_seconds = max(300, int(config.get("intervalMinutes", AUTO_UPDATE_INTERVAL_MINUTES)) * 60)
|
|
||||||
now = time.time()
|
|
||||||
|
|
||||||
with status_lock:
|
|
||||||
if current_process and current_process.poll() is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
last_reference = last_auto_trigger_at
|
|
||||||
status = load_status()
|
|
||||||
if not last_reference:
|
|
||||||
last_started = status.get("lastStartedAt")
|
|
||||||
if last_started:
|
|
||||||
try:
|
|
||||||
last_reference = datetime.fromisoformat(last_started).timestamp()
|
|
||||||
except Exception:
|
|
||||||
last_reference = 0.0
|
|
||||||
|
|
||||||
if last_reference and (now - last_reference) < interval_seconds:
|
|
||||||
continue
|
|
||||||
|
|
||||||
last_auto_trigger_at = now
|
|
||||||
|
|
||||||
schedule_update(trigger="auto")
|
|
||||||
except Exception as error:
|
|
||||||
update_status(lastError=f"Auto-update scheduler error: {error}")
|
|
||||||
|
|
||||||
|
|
||||||
class ThreadedUnixServer(socketserver.ThreadingMixIn, socketserver.UnixStreamServer):
|
|
||||||
daemon_threads = True
|
|
||||||
|
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
|
||||||
server_version = "SynapsisHostUpdater/1.0"
|
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
|
||||||
return
|
|
||||||
|
|
||||||
def send_json(self, status_code, payload):
|
|
||||||
body = json.dumps(payload).encode("utf-8")
|
|
||||||
self.send_response(status_code)
|
|
||||||
self.send_header("Content-Type", "application/json")
|
|
||||||
self.send_header("Content-Length", str(len(body)))
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(body)
|
|
||||||
|
|
||||||
def do_GET(self):
|
|
||||||
if self.path == "/health":
|
|
||||||
self.send_json(HTTPStatus.OK, {"ok": True})
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.path == "/status":
|
|
||||||
if not is_authorized(self.headers):
|
|
||||||
self.send_json(HTTPStatus.UNAUTHORIZED, {"error": "Unauthorized"})
|
|
||||||
return
|
|
||||||
self.send_json(HTTPStatus.OK, get_status_payload())
|
|
||||||
return
|
|
||||||
|
|
||||||
self.send_json(HTTPStatus.NOT_FOUND, {"error": "Not found"})
|
|
||||||
|
|
||||||
def do_POST(self):
|
|
||||||
global current_process
|
|
||||||
|
|
||||||
if self.path != "/update":
|
|
||||||
self.send_json(HTTPStatus.NOT_FOUND, {"error": "Not found"})
|
|
||||||
return
|
|
||||||
|
|
||||||
if not is_authorized(self.headers):
|
|
||||||
self.send_json(HTTPStatus.UNAUTHORIZED, {"error": "Unauthorized"})
|
|
||||||
return
|
|
||||||
|
|
||||||
if not schedule_update(trigger="manual"):
|
|
||||||
self.send_json(
|
|
||||||
HTTPStatus.CONFLICT,
|
|
||||||
{
|
|
||||||
"ok": False,
|
|
||||||
"status": "updating",
|
|
||||||
"message": "An update is already running.",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.send_json(
|
|
||||||
HTTPStatus.ACCEPTED,
|
|
||||||
{
|
|
||||||
"ok": True,
|
|
||||||
"status": "updating",
|
|
||||||
"message": "Synapsis update scheduled. The node will restart shortly.",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def do_PATCH(self):
|
|
||||||
if self.path != "/config":
|
|
||||||
self.send_json(HTTPStatus.NOT_FOUND, {"error": "Not found"})
|
|
||||||
return
|
|
||||||
|
|
||||||
if not is_authorized(self.headers):
|
|
||||||
self.send_json(HTTPStatus.UNAUTHORIZED, {"error": "Unauthorized"})
|
|
||||||
return
|
|
||||||
|
|
||||||
content_length = int(self.headers.get("Content-Length", "0") or "0")
|
|
||||||
raw_body = self.rfile.read(content_length) if content_length > 0 else b"{}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
payload = json.loads(raw_body.decode("utf-8") or "{}")
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
self.send_json(HTTPStatus.BAD_REQUEST, {"error": "Invalid JSON"})
|
|
||||||
return
|
|
||||||
|
|
||||||
if "autoUpdateEnabled" not in payload:
|
|
||||||
self.send_json(HTTPStatus.BAD_REQUEST, {"error": "autoUpdateEnabled is required"})
|
|
||||||
return
|
|
||||||
|
|
||||||
config = update_config(autoUpdateEnabled=payload.get("autoUpdateEnabled"))
|
|
||||||
self.send_json(HTTPStatus.OK, {"ok": True, "config": config})
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_socket(*_args):
|
|
||||||
try:
|
|
||||||
if os.path.exists(SOCKET_PATH):
|
|
||||||
os.remove(SOCKET_PATH)
|
|
||||||
finally:
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
ensure_parent_dirs()
|
|
||||||
|
|
||||||
if os.path.exists(SOCKET_PATH):
|
|
||||||
os.remove(SOCKET_PATH)
|
|
||||||
|
|
||||||
normalize_status_on_startup()
|
|
||||||
load_config()
|
|
||||||
|
|
||||||
signal.signal(signal.SIGTERM, cleanup_socket)
|
|
||||||
signal.signal(signal.SIGINT, cleanup_socket)
|
|
||||||
|
|
||||||
update_status(available=True)
|
|
||||||
threading.Thread(target=auto_update_loop, daemon=True).start()
|
|
||||||
|
|
||||||
with ThreadedUnixServer(SOCKET_PATH, Handler) as server:
|
|
||||||
os.chmod(SOCKET_PATH, 0o666)
|
|
||||||
server.serve_forever()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
REPO="${REPO:-GnosysLabs/Synapsis}"
|
|
||||||
REF="${REF:-main}"
|
|
||||||
INSTALL_DIR="${1:-${INSTALL_DIR:-/opt/synapsis}}"
|
|
||||||
PUBLIC_INSTALL_URL="${PUBLIC_INSTALL_URL:-https://synapsis.social/install.sh}"
|
|
||||||
PROXY="${PROXY:-caddy}"
|
|
||||||
HOST_UPDATER_SOCKET_PATH="${HOST_UPDATER_SOCKET_PATH:-/var/run/synapsis-updater/updater.sock}"
|
|
||||||
|
|
||||||
require_command() {
|
|
||||||
if ! command -v "$1" >/dev/null 2>&1; then
|
|
||||||
echo "❌ Required command not found: $1" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
download_file() {
|
|
||||||
source_path="$1"
|
|
||||||
target_path="$2"
|
|
||||||
echo "⬇️ Downloading ${source_path}"
|
|
||||||
curl -fsSL "${RAW_BASE}/${source_path}" -o "${target_path}"
|
|
||||||
}
|
|
||||||
|
|
||||||
normalize_proxy_mode() {
|
|
||||||
case "$1" in
|
|
||||||
caddy|none)
|
|
||||||
printf '%s\n' "$1"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "❌ Unsupported PROXY mode: $1" >&2
|
|
||||||
echo " Supported values: caddy, none" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
is_port_in_use() {
|
|
||||||
port="$1"
|
|
||||||
|
|
||||||
if command -v ss >/dev/null 2>&1; then
|
|
||||||
ss -tulpn 2>/dev/null | grep -q ":${port} "
|
|
||||||
return $?
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v lsof >/dev/null 2>&1; then
|
|
||||||
lsof -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1
|
|
||||||
return $?
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v netstat >/dev/null 2>&1; then
|
|
||||||
netstat -tulpn 2>/dev/null | grep -q ":${port} "
|
|
||||||
return $?
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_caddy_ports_available() {
|
|
||||||
for port in 80 443; do
|
|
||||||
if is_port_in_use "$port"; then
|
|
||||||
echo "❌ Port ${port} is already in use on this host." >&2
|
|
||||||
echo " The default Synapsis install includes Caddy and needs ports 80/443." >&2
|
|
||||||
echo " If this is a fresh VPS, free those ports and rerun the installer." >&2
|
|
||||||
echo " If this host already runs nginx or another reverse proxy, rerun with:" >&2
|
|
||||||
echo " curl -fsSL ${PUBLIC_INSTALL_URL} | PROXY=none bash" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
set_env_value() {
|
|
||||||
file="$1"
|
|
||||||
key="$2"
|
|
||||||
value="$3"
|
|
||||||
|
|
||||||
escaped_value=$(printf '%s\n' "$value" | sed -e 's/[\/&]/\\&/g')
|
|
||||||
sed -i.bak -E "s|^${key}=.*$|${key}=${escaped_value}|" "$file"
|
|
||||||
rm -f "${file}.bak"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_env_value() {
|
|
||||||
file="$1"
|
|
||||||
key="$2"
|
|
||||||
value="$3"
|
|
||||||
|
|
||||||
if grep -q "^${key}=" "$file" 2>/dev/null; then
|
|
||||||
set_env_value "$file" "$key" "$value"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s=%s\n' "$key" "$value" >> "$file"
|
|
||||||
}
|
|
||||||
|
|
||||||
get_env_value() {
|
|
||||||
file="$1"
|
|
||||||
key="$2"
|
|
||||||
|
|
||||||
if [ ! -f "$file" ]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
sed -n -E "s/^${key}=(.*)$/\\1/p" "$file" | head -n 1
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_db_password() {
|
|
||||||
openssl rand -base64 24 | tr -d '\n' | tr '/+' '_-' | cut -c1-32
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_token() {
|
|
||||||
if command -v openssl >/dev/null 2>&1; then
|
|
||||||
openssl rand -hex 32
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
date +%s | sha256sum | awk '{print $1}'
|
|
||||||
}
|
|
||||||
|
|
||||||
find_available_port() {
|
|
||||||
start="${1:-3000}"
|
|
||||||
end="${2:-3020}"
|
|
||||||
|
|
||||||
port="$start"
|
|
||||||
while [ "$port" -le "$end" ]; do
|
|
||||||
if ! is_port_in_use "$port"; then
|
|
||||||
printf '%s\n' "$port"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
port=$((port + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "❌ No available localhost port found in range ${start}-${end} for PROXY=none." >&2
|
|
||||||
echo " Set PORT explicitly when rerunning the installer if you want a different range." >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_proxyless_port() {
|
|
||||||
if [ -n "${PORT:-}" ] && [ "${PORT}" != "auto" ]; then
|
|
||||||
printf '%s\n' "${PORT}"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
find_available_port 3000 3020
|
|
||||||
}
|
|
||||||
|
|
||||||
install_docker_if_needed() {
|
|
||||||
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
|
||||||
echo "🐳 Docker is already installed"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
|
||||||
echo "❌ Docker is not installed and this installer is not running as root." >&2
|
|
||||||
echo " Re-run it as root or with sudo so Synapsis can install Docker for you." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "🐳 Docker not found, installing Docker"
|
|
||||||
curl -fsSL https://get.docker.com | sh
|
|
||||||
|
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
|
||||||
systemctl enable --now docker >/dev/null 2>&1 || true
|
|
||||||
elif command -v service >/dev/null 2>&1; then
|
|
||||||
service docker start >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
|
||||||
echo "❌ Docker installation did not finish successfully." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! docker compose version >/dev/null 2>&1; then
|
|
||||||
echo "⚠️ Docker was installed, but 'docker compose' is not available yet." >&2
|
|
||||||
echo " Install the Docker Compose plugin before starting Synapsis." >&2
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
install_python3_if_needed() {
|
|
||||||
if command -v python3 >/dev/null 2>&1; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v apt-get >/dev/null 2>&1; then
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y python3
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v apk >/dev/null 2>&1; then
|
|
||||||
apk add --no-cache python3
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v dnf >/dev/null 2>&1; then
|
|
||||||
dnf install -y python3
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v yum >/dev/null 2>&1; then
|
|
||||||
yum install -y python3
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "⚠️ Could not install python3 automatically. Host updater will be unavailable." >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
install_host_updater() {
|
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
|
||||||
echo "⚠️ Host updater installation requires root. Skipping one-click updater setup."
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
download_file "docker/host-updater.py" "${INSTALL_DIR}/host-updater.py"
|
|
||||||
download_file "docker/update-local.sh" "${INSTALL_DIR}/update-local.sh"
|
|
||||||
chmod 755 "${INSTALL_DIR}/host-updater.py" "${INSTALL_DIR}/update-local.sh"
|
|
||||||
|
|
||||||
mkdir -p "$(dirname "${HOST_UPDATER_SOCKET_PATH}")"
|
|
||||||
|
|
||||||
updater_token="$(get_env_value "${INSTALL_DIR}/.env" "HOST_UPDATER_TOKEN" || true)"
|
|
||||||
if [ -z "${updater_token}" ]; then
|
|
||||||
updater_token="$(generate_token)"
|
|
||||||
ensure_env_value "${INSTALL_DIR}/.env" "HOST_UPDATER_TOKEN" "${updater_token}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ensure_env_value "${INSTALL_DIR}/.env" "HOST_UPDATER_SOCKET" "${HOST_UPDATER_SOCKET_PATH}"
|
|
||||||
|
|
||||||
if ! command -v systemctl >/dev/null 2>&1; then
|
|
||||||
echo "⚠️ systemd not found. Admin one-click updates will be unavailable on this host."
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
install_python3_if_needed
|
|
||||||
if ! command -v python3 >/dev/null 2>&1; then
|
|
||||||
echo "⚠️ python3 is unavailable. Admin one-click updates will be unavailable."
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat > "${INSTALL_DIR}/updater.env" <<EOF
|
|
||||||
HOST_UPDATER_TOKEN=${updater_token}
|
|
||||||
REPO=${REPO}
|
|
||||||
REF=${REF}
|
|
||||||
EOF
|
|
||||||
chmod 600 "${INSTALL_DIR}/updater.env"
|
|
||||||
|
|
||||||
cat > /etc/systemd/system/synapsis-updater.service <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=Synapsis Host Updater
|
|
||||||
After=network-online.target docker.service
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=root
|
|
||||||
Group=root
|
|
||||||
WorkingDirectory=${INSTALL_DIR}
|
|
||||||
Environment=INSTALL_DIR=${INSTALL_DIR}
|
|
||||||
Environment=HOST_UPDATER_SOCKET=${HOST_UPDATER_SOCKET_PATH}
|
|
||||||
Environment=HOST_UPDATER_STATUS_FILE=${INSTALL_DIR}/updater-status.json
|
|
||||||
Environment=HOST_UPDATER_LOG_FILE=${INSTALL_DIR}/updater.log
|
|
||||||
Environment=HOST_UPDATER_SCRIPT=${INSTALL_DIR}/update-local.sh
|
|
||||||
EnvironmentFile=${INSTALL_DIR}/updater.env
|
|
||||||
ExecStart=/usr/bin/env python3 ${INSTALL_DIR}/host-updater.py
|
|
||||||
Restart=always
|
|
||||||
RestartSec=3
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable --now synapsis-updater.service >/dev/null 2>&1 || true
|
|
||||||
systemctl restart synapsis-updater.service >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
|
|
||||||
require_command curl
|
|
||||||
require_command chmod
|
|
||||||
require_command mkdir
|
|
||||||
require_command cp
|
|
||||||
|
|
||||||
RAW_BASE="https://raw.githubusercontent.com/${REPO}/${REF}"
|
|
||||||
PROXY="$(normalize_proxy_mode "${PROXY}")"
|
|
||||||
PROXYLESS_PORT="$(resolve_proxyless_port)"
|
|
||||||
|
|
||||||
echo "========================================"
|
|
||||||
echo " Synapsis Docker Installer"
|
|
||||||
echo "========================================"
|
|
||||||
echo " Repo: ${REPO}"
|
|
||||||
echo " Ref: ${REF}"
|
|
||||||
echo " Install dir: ${INSTALL_DIR}"
|
|
||||||
echo " Proxy mode: ${PROXY}"
|
|
||||||
echo "========================================"
|
|
||||||
|
|
||||||
install_docker_if_needed
|
|
||||||
mkdir -p "${INSTALL_DIR}"
|
|
||||||
|
|
||||||
download_file "docker/.env.example" "${INSTALL_DIR}/.env.example"
|
|
||||||
case "${PROXY}" in
|
|
||||||
caddy)
|
|
||||||
ensure_caddy_ports_available
|
|
||||||
download_file "docker-compose.yml" "${INSTALL_DIR}/docker-compose.yml"
|
|
||||||
download_file "docker/Caddyfile" "${INSTALL_DIR}/Caddyfile"
|
|
||||||
download_file "docker/caddy-entrypoint.sh" "${INSTALL_DIR}/caddy-entrypoint.sh"
|
|
||||||
chmod 755 "${INSTALL_DIR}/caddy-entrypoint.sh"
|
|
||||||
rm -f "${INSTALL_DIR}/docker-compose.proxyless.yml"
|
|
||||||
;;
|
|
||||||
none)
|
|
||||||
download_file "docker-compose.proxyless.yml" "${INSTALL_DIR}/docker-compose.yml"
|
|
||||||
rm -f "${INSTALL_DIR}/Caddyfile" "${INSTALL_DIR}/caddy-entrypoint.sh"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ ! -f "${INSTALL_DIR}/.env" ]; then
|
|
||||||
cp "${INSTALL_DIR}/.env.example" "${INSTALL_DIR}/.env"
|
|
||||||
echo "📝 Created ${INSTALL_DIR}/.env"
|
|
||||||
|
|
||||||
if command -v openssl >/dev/null 2>&1; then
|
|
||||||
set_env_value "${INSTALL_DIR}/.env" "AUTH_SECRET" "$(openssl rand -hex 32)"
|
|
||||||
set_env_value "${INSTALL_DIR}/.env" "DB_PASSWORD" "$(generate_db_password)"
|
|
||||||
echo "🔐 Generated AUTH_SECRET and DB_PASSWORD"
|
|
||||||
else
|
|
||||||
echo "⚠️ openssl not found, leaving placeholder secrets in .env"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${DOMAIN:-}" ]; then
|
|
||||||
set_env_value "${INSTALL_DIR}/.env" "DOMAIN" "${DOMAIN}"
|
|
||||||
echo "🌐 Set DOMAIN=${DOMAIN}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${ADMIN_EMAILS:-}" ]; then
|
|
||||||
set_env_value "${INSTALL_DIR}/.env" "ADMIN_EMAILS" "${ADMIN_EMAILS}"
|
|
||||||
echo "📧 Set ADMIN_EMAILS=${ADMIN_EMAILS}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${PROXY}" = "none" ]; then
|
|
||||||
set_env_value "${INSTALL_DIR}/.env" "PORT" "${PROXYLESS_PORT}"
|
|
||||||
echo "📡 Set PORT=${PROXYLESS_PORT} for proxyless mode"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "📝 Existing ${INSTALL_DIR}/.env found, leaving it unchanged"
|
|
||||||
|
|
||||||
if [ "${PROXY}" = "none" ]; then
|
|
||||||
current_port="$(get_env_value "${INSTALL_DIR}/.env" "PORT" || true)"
|
|
||||||
if [ -z "${current_port}" ] || [ "${current_port}" = "auto" ]; then
|
|
||||||
set_env_value "${INSTALL_DIR}/.env" "PORT" "${PROXYLESS_PORT}"
|
|
||||||
echo "📡 Updated PORT=${PROXYLESS_PORT} for proxyless mode"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
install_host_updater
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Next steps:"
|
|
||||||
echo " 1. Review ${INSTALL_DIR}/.env"
|
|
||||||
if [ "${PROXY}" = "caddy" ]; then
|
|
||||||
echo " 2. Start Synapsis:"
|
|
||||||
echo " cd ${INSTALL_DIR} && docker compose up -d"
|
|
||||||
else
|
|
||||||
echo " 2. Start Synapsis:"
|
|
||||||
echo " cd ${INSTALL_DIR} && docker compose up -d"
|
|
||||||
echo " 3. Configure your existing reverse proxy to forward to:"
|
|
||||||
echo " http://127.0.0.1:${PROXYLESS_PORT}"
|
|
||||||
echo " (change PORT in ${INSTALL_DIR}/.env if you want a different localhost port)"
|
|
||||||
fi
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# HTTP to HTTPS redirect (run this first without SSL)
|
|
||||||
# After SSL certificates are obtained, use the SSL version below
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name _; # Accept any server name (change to your domain for production)
|
|
||||||
|
|
||||||
# Health check endpoint for Docker
|
|
||||||
location /nginx-health {
|
|
||||||
access_log off;
|
|
||||||
return 200 "healthy\n";
|
|
||||||
add_header Content-Type text/plain;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Proxy all requests to the Next.js app
|
|
||||||
location / {
|
|
||||||
proxy_pass http://app:3000;
|
|
||||||
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;
|
|
||||||
proxy_read_timeout 86400;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Static files caching
|
|
||||||
location /_next/static {
|
|
||||||
proxy_pass http://app:3000;
|
|
||||||
proxy_cache_valid 200 365d;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
}
|
|
||||||
|
|
||||||
# API rate limiting
|
|
||||||
location /api/ {
|
|
||||||
limit_req zone=api burst=20 nodelay;
|
|
||||||
limit_conn addr 10;
|
|
||||||
|
|
||||||
proxy_pass http://app:3000;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Auth endpoints - stricter rate limiting
|
|
||||||
location /api/auth/ {
|
|
||||||
limit_req zone=auth burst=5 nodelay;
|
|
||||||
limit_conn addr 5;
|
|
||||||
|
|
||||||
proxy_pass http://app:3000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# SSL Configuration (use this after obtaining certificates)
|
|
||||||
# Rename to default.conf after SSL setup
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name ${DOMAIN};
|
|
||||||
|
|
||||||
# Certbot challenge
|
|
||||||
location /.well-known/acme-challenge/ {
|
|
||||||
root /var/www/certbot;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Redirect all HTTP to HTTPS
|
|
||||||
location / {
|
|
||||||
return 301 https://$server_name$request_uri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
server_name ${DOMAIN};
|
|
||||||
|
|
||||||
# SSL certificates (mounted from certbot)
|
|
||||||
ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
|
|
||||||
|
|
||||||
# SSL configuration
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
|
||||||
ssl_prefer_server_ciphers off;
|
|
||||||
ssl_session_cache shared:SSL:10m;
|
|
||||||
ssl_session_timeout 1d;
|
|
||||||
ssl_session_tickets off;
|
|
||||||
|
|
||||||
# HSTS (enable after confirming HTTPS works)
|
|
||||||
# add_header Strict-Transport-Security "max-age=63072000" always;
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
location /nginx-health {
|
|
||||||
access_log off;
|
|
||||||
return 200 "healthy\n";
|
|
||||||
add_header Content-Type text/plain;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Proxy all requests to the Next.js app
|
|
||||||
location / {
|
|
||||||
proxy_pass http://app:3000;
|
|
||||||
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;
|
|
||||||
proxy_read_timeout 86400;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Static files caching
|
|
||||||
location /_next/static {
|
|
||||||
proxy_pass http://app:3000;
|
|
||||||
proxy_cache_valid 200 365d;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
}
|
|
||||||
|
|
||||||
# API rate limiting
|
|
||||||
location /api/ {
|
|
||||||
limit_req zone=api burst=20 nodelay;
|
|
||||||
limit_conn addr 10;
|
|
||||||
|
|
||||||
proxy_pass http://app:3000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Auth endpoints - stricter rate limiting
|
|
||||||
location /api/auth/ {
|
|
||||||
limit_req zone=auth burst=5 nodelay;
|
|
||||||
limit_conn addr 5;
|
|
||||||
|
|
||||||
proxy_pass http://app:3000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# Nginx configuration for Synapsis
|
|
||||||
# This configuration includes:
|
|
||||||
# - Gzip compression
|
|
||||||
# - Security headers
|
|
||||||
# - Rate limiting
|
|
||||||
# - Proper handling of Next.js static files
|
|
||||||
|
|
||||||
user nginx;
|
|
||||||
worker_processes auto;
|
|
||||||
error_log /var/log/nginx/error.log warn;
|
|
||||||
pid /var/run/nginx.pid;
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
use epoll;
|
|
||||||
multi_accept on;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
# Logging format
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log main;
|
|
||||||
|
|
||||||
# Performance
|
|
||||||
sendfile on;
|
|
||||||
tcp_nopush on;
|
|
||||||
tcp_nodelay on;
|
|
||||||
keepalive_timeout 65;
|
|
||||||
types_hash_max_size 2048;
|
|
||||||
client_max_body_size 50M;
|
|
||||||
|
|
||||||
# Gzip compression
|
|
||||||
gzip on;
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_proxied any;
|
|
||||||
gzip_comp_level 6;
|
|
||||||
gzip_types
|
|
||||||
text/plain
|
|
||||||
text/css
|
|
||||||
text/xml
|
|
||||||
text/javascript
|
|
||||||
application/json
|
|
||||||
application/javascript
|
|
||||||
application/xml+rss
|
|
||||||
application/rss+xml
|
|
||||||
font/truetype
|
|
||||||
font/opentype
|
|
||||||
application/vnd.ms-fontobject
|
|
||||||
image/svg+xml;
|
|
||||||
|
|
||||||
# Rate limiting zones
|
|
||||||
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
|
||||||
limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;
|
|
||||||
limit_conn_zone $binary_remote_addr zone=addr:10m;
|
|
||||||
|
|
||||||
# Security headers
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
||||||
|
|
||||||
# Include server configurations
|
|
||||||
include /etc/nginx/conf.d/*.conf;
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
INSTALL_DIR="${1:-${INSTALL_DIR:-/opt/synapsis}}"
|
|
||||||
PROJECT_NAME="${PROJECT_NAME:-synapsis}"
|
|
||||||
IMAGE_REPO="${IMAGE_REPO:-ghcr.io/gnosyslabs/synapsis}"
|
|
||||||
|
|
||||||
require_command() {
|
|
||||||
if ! command -v "$1" >/dev/null 2>&1; then
|
|
||||||
echo "❌ Required command not found: $1" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
confirm_uninstall() {
|
|
||||||
if [ "${FORCE:-0}" = "1" ] || [ "${YES:-0}" = "1" ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "========================================"
|
|
||||||
echo " Synapsis Docker Uninstaller"
|
|
||||||
echo "========================================"
|
|
||||||
echo " Install dir: ${INSTALL_DIR}"
|
|
||||||
echo " Project name: ${PROJECT_NAME}"
|
|
||||||
echo ""
|
|
||||||
echo "This will permanently remove:"
|
|
||||||
echo " - Synapsis containers"
|
|
||||||
echo " - Synapsis Docker volumes and network"
|
|
||||||
echo " - Synapsis images pulled from GHCR/local cache"
|
|
||||||
echo " - ${INSTALL_DIR}"
|
|
||||||
echo ""
|
|
||||||
if [ -r /dev/tty ]; then
|
|
||||||
printf "Type DELETE to continue: " > /dev/tty
|
|
||||||
read -r confirmation < /dev/tty
|
|
||||||
else
|
|
||||||
echo "❌ Interactive confirmation requires a TTY." >&2
|
|
||||||
echo " Re-run with FORCE=1 if you want to skip the prompt." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$confirmation" != "DELETE" ]; then
|
|
||||||
echo "Aborted."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup_with_compose() {
|
|
||||||
if [ -f "${INSTALL_DIR}/docker-compose.yml" ]; then
|
|
||||||
echo "🧹 Stopping Synapsis compose stack"
|
|
||||||
if [ -f "${INSTALL_DIR}/.env" ]; then
|
|
||||||
docker compose --env-file "${INSTALL_DIR}/.env" -f "${INSTALL_DIR}/docker-compose.yml" down --volumes --remove-orphans || true
|
|
||||||
else
|
|
||||||
docker compose -f "${INSTALL_DIR}/docker-compose.yml" down --volumes --remove-orphans || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup_named_resources() {
|
|
||||||
for container in synapsis-caddy synapsis-app synapsis-db; do
|
|
||||||
if docker ps -aq --filter "name=^${container}$" | grep -q .; then
|
|
||||||
echo "🗑️ Removing container ${container}"
|
|
||||||
docker rm -f "${container}" >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
for volume in \
|
|
||||||
synapsis_postgres_data \
|
|
||||||
synapsis_uploads_data \
|
|
||||||
synapsis_port_data \
|
|
||||||
synapsis_caddy_data \
|
|
||||||
synapsis_caddy_config
|
|
||||||
do
|
|
||||||
if docker volume inspect "${volume}" >/dev/null 2>&1; then
|
|
||||||
echo "🗑️ Removing volume ${volume}"
|
|
||||||
docker volume rm -f "${volume}" >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if docker network inspect synapsis_synapsis-network >/dev/null 2>&1; then
|
|
||||||
echo "🗑️ Removing network synapsis_synapsis-network"
|
|
||||||
docker network rm synapsis_synapsis-network >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
|
||||||
systemctl disable --now synapsis-updater.service >/dev/null 2>&1 || true
|
|
||||||
rm -f /etc/systemd/system/synapsis-updater.service
|
|
||||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -rf /var/run/synapsis-updater
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup_images() {
|
|
||||||
docker images --format '{{.Repository}}:{{.Tag}}' | while IFS= read -r image; do
|
|
||||||
case "$image" in
|
|
||||||
"${IMAGE_REPO}:"*|synapsis:*)
|
|
||||||
echo "🗑️ Removing image ${image}"
|
|
||||||
docker rmi -f "$image" >/dev/null 2>&1 || true
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
remove_install_dir() {
|
|
||||||
if [ -d "${INSTALL_DIR}" ]; then
|
|
||||||
echo "🗑️ Removing ${INSTALL_DIR}"
|
|
||||||
rm -rf "${INSTALL_DIR}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
require_command docker
|
|
||||||
require_command rm
|
|
||||||
|
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
|
||||||
echo "❌ Run this uninstaller as root or with sudo." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
confirm_uninstall
|
|
||||||
cleanup_with_compose
|
|
||||||
cleanup_named_resources
|
|
||||||
cleanup_images
|
|
||||||
remove_install_dir
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "✅ Synapsis has been removed from this host."
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
REPO="${REPO:-GnosysLabs/Synapsis}"
|
|
||||||
REF="${REF:-main}"
|
|
||||||
INSTALL_DIR="${1:-${INSTALL_DIR:-/opt/synapsis}}"
|
|
||||||
RAW_BASE="https://raw.githubusercontent.com/${REPO}/${REF}"
|
|
||||||
|
|
||||||
require_command() {
|
|
||||||
if ! command -v "$1" >/dev/null 2>&1; then
|
|
||||||
echo "❌ Required command not found: $1" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
download_file() {
|
|
||||||
source_path="$1"
|
|
||||||
target_path="$2"
|
|
||||||
echo "⬇️ Downloading ${source_path}"
|
|
||||||
curl -fsSL "${RAW_BASE}/${source_path}" -o "${target_path}"
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_proxy_mode() {
|
|
||||||
if [ -f "${INSTALL_DIR}/Caddyfile" ] || [ -f "${INSTALL_DIR}/caddy-entrypoint.sh" ]; then
|
|
||||||
printf 'caddy\n'
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf 'none\n'
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_cmd() {
|
|
||||||
if [ -f "${INSTALL_DIR}/.env" ]; then
|
|
||||||
docker compose --env-file "${INSTALL_DIR}/.env" -f "${INSTALL_DIR}/docker-compose.yml" "$@"
|
|
||||||
else
|
|
||||||
docker compose -f "${INSTALL_DIR}/docker-compose.yml" "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
require_command curl
|
|
||||||
require_command docker
|
|
||||||
require_command chmod
|
|
||||||
require_command mkdir
|
|
||||||
|
|
||||||
if [ ! -d "${INSTALL_DIR}" ]; then
|
|
||||||
echo "❌ ${INSTALL_DIR} does not exist." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
ACTIVE_PROXY="$(detect_proxy_mode)"
|
|
||||||
|
|
||||||
case "${ACTIVE_PROXY}" in
|
|
||||||
caddy)
|
|
||||||
download_file "docker-compose.yml" "${INSTALL_DIR}/docker-compose.yml"
|
|
||||||
download_file "docker/Caddyfile" "${INSTALL_DIR}/Caddyfile"
|
|
||||||
download_file "docker/caddy-entrypoint.sh" "${INSTALL_DIR}/caddy-entrypoint.sh"
|
|
||||||
chmod 755 "${INSTALL_DIR}/caddy-entrypoint.sh"
|
|
||||||
;;
|
|
||||||
none)
|
|
||||||
download_file "docker-compose.proxyless.yml" "${INSTALL_DIR}/docker-compose.yml"
|
|
||||||
rm -f "${INSTALL_DIR}/Caddyfile" "${INSTALL_DIR}/caddy-entrypoint.sh"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
download_file "docker/host-updater.py" "${INSTALL_DIR}/host-updater.py"
|
|
||||||
download_file "docker/update-local.sh" "${INSTALL_DIR}/update-local.sh"
|
|
||||||
chmod 755 "${INSTALL_DIR}/host-updater.py" "${INSTALL_DIR}/update-local.sh"
|
|
||||||
|
|
||||||
echo "🐳 Pulling latest Synapsis image"
|
|
||||||
compose_cmd pull
|
|
||||||
|
|
||||||
echo "🚀 Restarting Synapsis"
|
|
||||||
compose_cmd up -d --remove-orphans
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "✅ Synapsis has been updated to the latest published image."
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
REPO="${REPO:-GnosysLabs/Synapsis}"
|
|
||||||
REF="${REF:-main}"
|
|
||||||
INSTALL_DIR="${1:-${INSTALL_DIR:-/opt/synapsis}}"
|
|
||||||
RAW_BASE="https://raw.githubusercontent.com/${REPO}/${REF}"
|
|
||||||
PROXY="${PROXY:-}"
|
|
||||||
HOST_UPDATER_SOCKET_PATH="${HOST_UPDATER_SOCKET_PATH:-/var/run/synapsis-updater/updater.sock}"
|
|
||||||
|
|
||||||
require_command() {
|
|
||||||
if ! command -v "$1" >/dev/null 2>&1; then
|
|
||||||
echo "❌ Required command not found: $1" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
download_file() {
|
|
||||||
source_path="$1"
|
|
||||||
target_path="$2"
|
|
||||||
echo "⬇️ Downloading ${source_path}"
|
|
||||||
curl -fsSL "${RAW_BASE}/${source_path}" -o "${target_path}"
|
|
||||||
}
|
|
||||||
|
|
||||||
set_env_value() {
|
|
||||||
file="$1"
|
|
||||||
key="$2"
|
|
||||||
value="$3"
|
|
||||||
|
|
||||||
escaped_value=$(printf '%s\n' "$value" | sed -e 's/[\/&]/\\&/g')
|
|
||||||
sed -i.bak -E "s|^${key}=.*$|${key}=${escaped_value}|" "$file"
|
|
||||||
rm -f "${file}.bak"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_env_value() {
|
|
||||||
file="$1"
|
|
||||||
key="$2"
|
|
||||||
value="$3"
|
|
||||||
|
|
||||||
if grep -q "^${key}=" "$file" 2>/dev/null; then
|
|
||||||
set_env_value "$file" "$key" "$value"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s=%s\n' "$key" "$value" >> "$file"
|
|
||||||
}
|
|
||||||
|
|
||||||
get_env_value() {
|
|
||||||
file="$1"
|
|
||||||
key="$2"
|
|
||||||
sed -n -E "s/^${key}=(.*)$/\\1/p" "$file" | head -n 1
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_token() {
|
|
||||||
if command -v openssl >/dev/null 2>&1; then
|
|
||||||
openssl rand -hex 32
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
date +%s | sha256sum | awk '{print $1}'
|
|
||||||
}
|
|
||||||
|
|
||||||
normalize_proxy_mode() {
|
|
||||||
case "$1" in
|
|
||||||
caddy|none)
|
|
||||||
printf '%s\n' "$1"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "❌ Unsupported PROXY mode: $1" >&2
|
|
||||||
echo " Supported values: caddy, none" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_proxy_mode() {
|
|
||||||
if [ -n "${PROXY}" ]; then
|
|
||||||
normalize_proxy_mode "${PROXY}"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "${INSTALL_DIR}/Caddyfile" ] || [ -f "${INSTALL_DIR}/caddy-entrypoint.sh" ]; then
|
|
||||||
printf 'caddy\n'
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf 'none\n'
|
|
||||||
}
|
|
||||||
|
|
||||||
install_python3_if_needed() {
|
|
||||||
if command -v python3 >/dev/null 2>&1; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v apt-get >/dev/null 2>&1; then
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y python3
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v apk >/dev/null 2>&1; then
|
|
||||||
apk add --no-cache python3
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
install_host_updater() {
|
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "${INSTALL_DIR}/.env" ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
download_file "docker/host-updater.py" "${INSTALL_DIR}/host-updater.py"
|
|
||||||
download_file "docker/update-local.sh" "${INSTALL_DIR}/update-local.sh"
|
|
||||||
chmod 755 "${INSTALL_DIR}/host-updater.py" "${INSTALL_DIR}/update-local.sh"
|
|
||||||
|
|
||||||
mkdir -p "$(dirname "${HOST_UPDATER_SOCKET_PATH}")"
|
|
||||||
|
|
||||||
updater_token="$(get_env_value "${INSTALL_DIR}/.env" "HOST_UPDATER_TOKEN" || true)"
|
|
||||||
if [ -z "${updater_token}" ]; then
|
|
||||||
updater_token="$(generate_token)"
|
|
||||||
ensure_env_value "${INSTALL_DIR}/.env" "HOST_UPDATER_TOKEN" "${updater_token}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ensure_env_value "${INSTALL_DIR}/.env" "HOST_UPDATER_SOCKET" "${HOST_UPDATER_SOCKET_PATH}"
|
|
||||||
|
|
||||||
if ! command -v systemctl >/dev/null 2>&1; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
install_python3_if_needed
|
|
||||||
if ! command -v python3 >/dev/null 2>&1; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat > "${INSTALL_DIR}/updater.env" <<EOF
|
|
||||||
HOST_UPDATER_TOKEN=${updater_token}
|
|
||||||
REPO=${REPO}
|
|
||||||
REF=${REF}
|
|
||||||
EOF
|
|
||||||
chmod 600 "${INSTALL_DIR}/updater.env"
|
|
||||||
|
|
||||||
cat > /etc/systemd/system/synapsis-updater.service <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=Synapsis Host Updater
|
|
||||||
After=network-online.target docker.service
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=root
|
|
||||||
Group=root
|
|
||||||
WorkingDirectory=${INSTALL_DIR}
|
|
||||||
Environment=INSTALL_DIR=${INSTALL_DIR}
|
|
||||||
Environment=HOST_UPDATER_SOCKET=${HOST_UPDATER_SOCKET_PATH}
|
|
||||||
Environment=HOST_UPDATER_STATUS_FILE=${INSTALL_DIR}/updater-status.json
|
|
||||||
Environment=HOST_UPDATER_LOG_FILE=${INSTALL_DIR}/updater.log
|
|
||||||
Environment=HOST_UPDATER_SCRIPT=${INSTALL_DIR}/update-local.sh
|
|
||||||
EnvironmentFile=${INSTALL_DIR}/updater.env
|
|
||||||
ExecStart=/usr/bin/env python3 ${INSTALL_DIR}/host-updater.py
|
|
||||||
Restart=always
|
|
||||||
RestartSec=3
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable --now synapsis-updater.service >/dev/null 2>&1 || true
|
|
||||||
systemctl restart synapsis-updater.service >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
|
|
||||||
require_command curl
|
|
||||||
require_command docker
|
|
||||||
require_command chmod
|
|
||||||
require_command mkdir
|
|
||||||
|
|
||||||
if [ ! -d "${INSTALL_DIR}" ]; then
|
|
||||||
echo "❌ ${INSTALL_DIR} does not exist." >&2
|
|
||||||
echo " Run the installer first:" >&2
|
|
||||||
echo " curl -fsSL https://synapsis.social/install.sh | bash" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "${INSTALL_DIR}/docker-compose.yml" ]; then
|
|
||||||
echo "❌ ${INSTALL_DIR}/docker-compose.yml was not found." >&2
|
|
||||||
echo " This does not look like a Synapsis install directory." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
ACTIVE_PROXY="$(detect_proxy_mode)"
|
|
||||||
|
|
||||||
echo "========================================"
|
|
||||||
echo " Synapsis Docker Updater"
|
|
||||||
echo "========================================"
|
|
||||||
echo " Repo: ${REPO}"
|
|
||||||
echo " Ref: ${REF}"
|
|
||||||
echo " Install dir: ${INSTALL_DIR}"
|
|
||||||
echo " Proxy mode: ${ACTIVE_PROXY}"
|
|
||||||
echo "========================================"
|
|
||||||
|
|
||||||
mkdir -p "${INSTALL_DIR}"
|
|
||||||
|
|
||||||
download_file "docker/.env.example" "${INSTALL_DIR}/.env.example"
|
|
||||||
|
|
||||||
case "${ACTIVE_PROXY}" in
|
|
||||||
caddy)
|
|
||||||
download_file "docker-compose.yml" "${INSTALL_DIR}/docker-compose.yml"
|
|
||||||
download_file "docker/Caddyfile" "${INSTALL_DIR}/Caddyfile"
|
|
||||||
download_file "docker/caddy-entrypoint.sh" "${INSTALL_DIR}/caddy-entrypoint.sh"
|
|
||||||
chmod 755 "${INSTALL_DIR}/caddy-entrypoint.sh"
|
|
||||||
;;
|
|
||||||
none)
|
|
||||||
download_file "docker-compose.proxyless.yml" "${INSTALL_DIR}/docker-compose.yml"
|
|
||||||
rm -f "${INSTALL_DIR}/Caddyfile" "${INSTALL_DIR}/caddy-entrypoint.sh"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
install_host_updater
|
|
||||||
|
|
||||||
echo "🐳 Pulling latest Synapsis image"
|
|
||||||
if [ -f "${INSTALL_DIR}/.env" ]; then
|
|
||||||
docker compose --env-file "${INSTALL_DIR}/.env" -f "${INSTALL_DIR}/docker-compose.yml" pull
|
|
||||||
echo "🚀 Restarting Synapsis"
|
|
||||||
docker compose --env-file "${INSTALL_DIR}/.env" -f "${INSTALL_DIR}/docker-compose.yml" up -d --remove-orphans
|
|
||||||
else
|
|
||||||
docker compose -f "${INSTALL_DIR}/docker-compose.yml" pull
|
|
||||||
echo "🚀 Restarting Synapsis"
|
|
||||||
docker compose -f "${INSTALL_DIR}/docker-compose.yml" up -d --remove-orphans
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "✅ Synapsis has been updated to the latest published image."
|
|
||||||
+2
-2
@@ -7,8 +7,8 @@ dotenv.config();
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
schema: './src/db/schema.ts',
|
schema: './src/db/schema.ts',
|
||||||
out: './drizzle',
|
out: './drizzle',
|
||||||
dialect: 'postgresql',
|
dialect: 'sqlite',
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: process.env.DATABASE_URL!,
|
url: process.env.DATABASE_PATH || './data/synapsis.db',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,369 +0,0 @@
|
|||||||
CREATE TABLE "blocks" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"user_id" uuid NOT NULL,
|
|
||||||
"blocked_user_id" uuid NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "bot_activity_logs" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"bot_id" uuid NOT NULL,
|
|
||||||
"action" text NOT NULL,
|
|
||||||
"details" text NOT NULL,
|
|
||||||
"success" boolean NOT NULL,
|
|
||||||
"error_message" text,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "bot_content_items" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"source_id" uuid NOT NULL,
|
|
||||||
"external_id" text NOT NULL,
|
|
||||||
"title" text NOT NULL,
|
|
||||||
"content" text,
|
|
||||||
"url" text NOT NULL,
|
|
||||||
"published_at" timestamp NOT NULL,
|
|
||||||
"fetched_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"is_processed" boolean DEFAULT false NOT NULL,
|
|
||||||
"processed_at" timestamp,
|
|
||||||
"post_id" uuid,
|
|
||||||
"interest_score" integer,
|
|
||||||
"interest_reason" text
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "bot_content_sources" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"bot_id" uuid NOT NULL,
|
|
||||||
"type" text NOT NULL,
|
|
||||||
"url" text NOT NULL,
|
|
||||||
"subreddit" text,
|
|
||||||
"api_key_encrypted" text,
|
|
||||||
"fetch_interval_minutes" integer DEFAULT 30 NOT NULL,
|
|
||||||
"keywords" text,
|
|
||||||
"is_active" boolean DEFAULT true NOT NULL,
|
|
||||||
"last_fetch_at" timestamp,
|
|
||||||
"last_error" text,
|
|
||||||
"consecutive_errors" integer DEFAULT 0 NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "bot_mentions" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"bot_id" uuid NOT NULL,
|
|
||||||
"post_id" uuid NOT NULL,
|
|
||||||
"author_id" uuid NOT NULL,
|
|
||||||
"content" text NOT NULL,
|
|
||||||
"is_processed" boolean DEFAULT false NOT NULL,
|
|
||||||
"processed_at" timestamp,
|
|
||||||
"response_post_id" uuid,
|
|
||||||
"is_remote" boolean DEFAULT false NOT NULL,
|
|
||||||
"remote_actor_url" text,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "bot_rate_limits" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"bot_id" uuid NOT NULL,
|
|
||||||
"window_start" timestamp NOT NULL,
|
|
||||||
"window_type" text NOT NULL,
|
|
||||||
"post_count" integer DEFAULT 0 NOT NULL,
|
|
||||||
"reply_count" integer DEFAULT 0 NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "bots" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"user_id" uuid NOT NULL,
|
|
||||||
"name" text NOT NULL,
|
|
||||||
"handle" text NOT NULL,
|
|
||||||
"bio" text,
|
|
||||||
"avatar_url" text,
|
|
||||||
"personality_config" text NOT NULL,
|
|
||||||
"llm_provider" text NOT NULL,
|
|
||||||
"llm_model" text NOT NULL,
|
|
||||||
"llm_api_key_encrypted" text NOT NULL,
|
|
||||||
"schedule_config" text,
|
|
||||||
"autonomous_mode" boolean DEFAULT false NOT NULL,
|
|
||||||
"is_active" boolean DEFAULT true NOT NULL,
|
|
||||||
"is_suspended" boolean DEFAULT false NOT NULL,
|
|
||||||
"suspension_reason" text,
|
|
||||||
"suspended_at" timestamp,
|
|
||||||
"public_key" text NOT NULL,
|
|
||||||
"private_key_encrypted" text NOT NULL,
|
|
||||||
"last_post_at" timestamp,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
CONSTRAINT "bots_handle_unique" UNIQUE("handle")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "follows" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"follower_id" uuid NOT NULL,
|
|
||||||
"following_id" uuid NOT NULL,
|
|
||||||
"ap_id" text,
|
|
||||||
"pending" boolean DEFAULT false,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
CONSTRAINT "follows_ap_id_unique" UNIQUE("ap_id")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "handle_registry" (
|
|
||||||
"handle" text PRIMARY KEY NOT NULL,
|
|
||||||
"did" text NOT NULL,
|
|
||||||
"node_domain" text NOT NULL,
|
|
||||||
"registered_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "likes" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"user_id" uuid NOT NULL,
|
|
||||||
"post_id" uuid NOT NULL,
|
|
||||||
"ap_id" text,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
CONSTRAINT "likes_ap_id_unique" UNIQUE("ap_id")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "media" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"user_id" uuid NOT NULL,
|
|
||||||
"post_id" uuid,
|
|
||||||
"url" text NOT NULL,
|
|
||||||
"alt_text" text,
|
|
||||||
"mime_type" text,
|
|
||||||
"width" integer,
|
|
||||||
"height" integer,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "mutes" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"user_id" uuid NOT NULL,
|
|
||||||
"muted_user_id" uuid NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "nodes" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"domain" text NOT NULL,
|
|
||||||
"name" text NOT NULL,
|
|
||||||
"description" text,
|
|
||||||
"long_description" text,
|
|
||||||
"rules" text,
|
|
||||||
"banner_url" text,
|
|
||||||
"accent_color" text DEFAULT '#FFFFFF',
|
|
||||||
"public_key" text,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
CONSTRAINT "nodes_domain_unique" UNIQUE("domain")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "notifications" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"user_id" uuid NOT NULL,
|
|
||||||
"actor_id" uuid NOT NULL,
|
|
||||||
"post_id" uuid,
|
|
||||||
"type" text NOT NULL,
|
|
||||||
"read_at" timestamp,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "posts" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"user_id" uuid NOT NULL,
|
|
||||||
"content" text NOT NULL,
|
|
||||||
"reply_to_id" uuid,
|
|
||||||
"repost_of_id" uuid,
|
|
||||||
"likes_count" integer DEFAULT 0 NOT NULL,
|
|
||||||
"reposts_count" integer DEFAULT 0 NOT NULL,
|
|
||||||
"replies_count" integer DEFAULT 0 NOT NULL,
|
|
||||||
"is_removed" boolean DEFAULT false NOT NULL,
|
|
||||||
"removed_at" timestamp,
|
|
||||||
"removed_by" uuid,
|
|
||||||
"removed_reason" text,
|
|
||||||
"ap_id" text,
|
|
||||||
"ap_url" text,
|
|
||||||
"link_preview_url" text,
|
|
||||||
"link_preview_title" text,
|
|
||||||
"link_preview_description" text,
|
|
||||||
"link_preview_image" text,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
CONSTRAINT "posts_ap_id_unique" UNIQUE("ap_id")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "remote_followers" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"user_id" uuid NOT NULL,
|
|
||||||
"actor_url" text NOT NULL,
|
|
||||||
"inbox_url" text NOT NULL,
|
|
||||||
"shared_inbox_url" text,
|
|
||||||
"handle" text,
|
|
||||||
"activity_id" text,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
CONSTRAINT "remote_followers_actor_url_unique" UNIQUE("actor_url")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "remote_follows" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"follower_id" uuid NOT NULL,
|
|
||||||
"target_handle" text NOT NULL,
|
|
||||||
"target_actor_url" text NOT NULL,
|
|
||||||
"inbox_url" text NOT NULL,
|
|
||||||
"activity_id" text NOT NULL,
|
|
||||||
"display_name" text,
|
|
||||||
"bio" text,
|
|
||||||
"avatar_url" text,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "remote_posts" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"ap_id" text NOT NULL,
|
|
||||||
"author_handle" text NOT NULL,
|
|
||||||
"author_actor_url" text NOT NULL,
|
|
||||||
"author_display_name" text,
|
|
||||||
"author_avatar_url" text,
|
|
||||||
"content" text NOT NULL,
|
|
||||||
"published_at" timestamp NOT NULL,
|
|
||||||
"link_preview_url" text,
|
|
||||||
"link_preview_title" text,
|
|
||||||
"link_preview_description" text,
|
|
||||||
"link_preview_image" text,
|
|
||||||
"media_json" text,
|
|
||||||
"fetched_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
CONSTRAINT "remote_posts_ap_id_unique" UNIQUE("ap_id")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "reports" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"reporter_id" uuid,
|
|
||||||
"target_type" text NOT NULL,
|
|
||||||
"target_id" uuid NOT NULL,
|
|
||||||
"reason" text NOT NULL,
|
|
||||||
"status" text DEFAULT 'open' NOT NULL,
|
|
||||||
"resolved_at" timestamp,
|
|
||||||
"resolved_by" uuid,
|
|
||||||
"resolution_note" text,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "sessions" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"user_id" uuid NOT NULL,
|
|
||||||
"token" text NOT NULL,
|
|
||||||
"expires_at" timestamp NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
CONSTRAINT "sessions_token_unique" UNIQUE("token")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "users" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"did" text NOT NULL,
|
|
||||||
"handle" text NOT NULL,
|
|
||||||
"email" text,
|
|
||||||
"password_hash" text,
|
|
||||||
"display_name" text,
|
|
||||||
"bio" text,
|
|
||||||
"avatar_url" text,
|
|
||||||
"header_url" text,
|
|
||||||
"private_key_encrypted" text,
|
|
||||||
"public_key" text NOT NULL,
|
|
||||||
"node_id" uuid,
|
|
||||||
"is_suspended" boolean DEFAULT false NOT NULL,
|
|
||||||
"suspension_reason" text,
|
|
||||||
"suspended_at" timestamp,
|
|
||||||
"is_silenced" boolean DEFAULT false NOT NULL,
|
|
||||||
"silence_reason" text,
|
|
||||||
"silenced_at" timestamp,
|
|
||||||
"moved_to" text,
|
|
||||||
"moved_from" text,
|
|
||||||
"migrated_at" timestamp,
|
|
||||||
"followers_count" integer DEFAULT 0 NOT NULL,
|
|
||||||
"following_count" integer DEFAULT 0 NOT NULL,
|
|
||||||
"posts_count" integer DEFAULT 0 NOT NULL,
|
|
||||||
"website" text,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
CONSTRAINT "users_did_unique" UNIQUE("did"),
|
|
||||||
CONSTRAINT "users_handle_unique" UNIQUE("handle"),
|
|
||||||
CONSTRAINT "users_email_unique" UNIQUE("email")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "blocks" ADD CONSTRAINT "blocks_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "blocks" ADD CONSTRAINT "blocks_blocked_user_id_users_id_fk" FOREIGN KEY ("blocked_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "bot_activity_logs" ADD CONSTRAINT "bot_activity_logs_bot_id_bots_id_fk" FOREIGN KEY ("bot_id") REFERENCES "public"."bots"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "bot_content_items" ADD CONSTRAINT "bot_content_items_source_id_bot_content_sources_id_fk" FOREIGN KEY ("source_id") REFERENCES "public"."bot_content_sources"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "bot_content_items" ADD CONSTRAINT "bot_content_items_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "bot_content_sources" ADD CONSTRAINT "bot_content_sources_bot_id_bots_id_fk" FOREIGN KEY ("bot_id") REFERENCES "public"."bots"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "bot_mentions" ADD CONSTRAINT "bot_mentions_bot_id_bots_id_fk" FOREIGN KEY ("bot_id") REFERENCES "public"."bots"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "bot_mentions" ADD CONSTRAINT "bot_mentions_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "bot_mentions" ADD CONSTRAINT "bot_mentions_author_id_users_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "bot_mentions" ADD CONSTRAINT "bot_mentions_response_post_id_posts_id_fk" FOREIGN KEY ("response_post_id") REFERENCES "public"."posts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "bot_rate_limits" ADD CONSTRAINT "bot_rate_limits_bot_id_bots_id_fk" FOREIGN KEY ("bot_id") REFERENCES "public"."bots"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "bots" ADD CONSTRAINT "bots_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "follows" ADD CONSTRAINT "follows_follower_id_users_id_fk" FOREIGN KEY ("follower_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "follows" ADD CONSTRAINT "follows_following_id_users_id_fk" FOREIGN KEY ("following_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "likes" ADD CONSTRAINT "likes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "likes" ADD CONSTRAINT "likes_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "media" ADD CONSTRAINT "media_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "media" ADD CONSTRAINT "media_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "mutes" ADD CONSTRAINT "mutes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "mutes" ADD CONSTRAINT "mutes_muted_user_id_users_id_fk" FOREIGN KEY ("muted_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_actor_id_users_id_fk" FOREIGN KEY ("actor_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "posts" ADD CONSTRAINT "posts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "posts" ADD CONSTRAINT "posts_removed_by_users_id_fk" FOREIGN KEY ("removed_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "remote_followers" ADD CONSTRAINT "remote_followers_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "remote_follows" ADD CONSTRAINT "remote_follows_follower_id_users_id_fk" FOREIGN KEY ("follower_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "reports" ADD CONSTRAINT "reports_reporter_id_users_id_fk" FOREIGN KEY ("reporter_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "reports" ADD CONSTRAINT "reports_resolved_by_users_id_fk" FOREIGN KEY ("resolved_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "users" ADD CONSTRAINT "users_node_id_nodes_id_fk" FOREIGN KEY ("node_id") REFERENCES "public"."nodes"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
||||||
CREATE INDEX "blocks_user_idx" ON "blocks" USING btree ("user_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "bot_activity_logs_bot_idx" ON "bot_activity_logs" USING btree ("bot_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "bot_activity_logs_action_idx" ON "bot_activity_logs" USING btree ("action");--> statement-breakpoint
|
|
||||||
CREATE INDEX "bot_activity_logs_created_idx" ON "bot_activity_logs" USING btree ("created_at");--> statement-breakpoint
|
|
||||||
CREATE INDEX "bot_content_items_source_idx" ON "bot_content_items" USING btree ("source_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "bot_content_items_processed_idx" ON "bot_content_items" USING btree ("is_processed");--> statement-breakpoint
|
|
||||||
CREATE INDEX "bot_content_items_external_idx" ON "bot_content_items" USING btree ("external_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "bot_content_sources_bot_idx" ON "bot_content_sources" USING btree ("bot_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "bot_content_sources_type_idx" ON "bot_content_sources" USING btree ("type");--> statement-breakpoint
|
|
||||||
CREATE INDEX "bot_mentions_bot_idx" ON "bot_mentions" USING btree ("bot_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "bot_mentions_processed_idx" ON "bot_mentions" USING btree ("is_processed");--> statement-breakpoint
|
|
||||||
CREATE INDEX "bot_mentions_created_idx" ON "bot_mentions" USING btree ("created_at");--> statement-breakpoint
|
|
||||||
CREATE INDEX "bot_rate_limits_bot_window_idx" ON "bot_rate_limits" USING btree ("bot_id","window_start");--> statement-breakpoint
|
|
||||||
CREATE INDEX "bots_user_id_idx" ON "bots" USING btree ("user_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "bots_handle_idx" ON "bots" USING btree ("handle");--> statement-breakpoint
|
|
||||||
CREATE INDEX "bots_active_idx" ON "bots" USING btree ("is_active");--> statement-breakpoint
|
|
||||||
CREATE INDEX "follows_follower_idx" ON "follows" USING btree ("follower_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "follows_following_idx" ON "follows" USING btree ("following_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "handle_registry_updated_idx" ON "handle_registry" USING btree ("updated_at");--> statement-breakpoint
|
|
||||||
CREATE INDEX "likes_user_post_idx" ON "likes" USING btree ("user_id","post_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "media_user_idx" ON "media" USING btree ("user_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "media_post_idx" ON "media" USING btree ("post_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "mutes_user_idx" ON "mutes" USING btree ("user_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "notifications_user_idx" ON "notifications" USING btree ("user_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "notifications_created_idx" ON "notifications" USING btree ("created_at");--> statement-breakpoint
|
|
||||||
CREATE INDEX "posts_user_id_idx" ON "posts" USING btree ("user_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "posts_created_at_idx" ON "posts" USING btree ("created_at");--> statement-breakpoint
|
|
||||||
CREATE INDEX "posts_reply_to_idx" ON "posts" USING btree ("reply_to_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "posts_removed_idx" ON "posts" USING btree ("is_removed");--> statement-breakpoint
|
|
||||||
CREATE INDEX "remote_followers_user_idx" ON "remote_followers" USING btree ("user_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "remote_followers_actor_idx" ON "remote_followers" USING btree ("actor_url");--> statement-breakpoint
|
|
||||||
CREATE INDEX "remote_follows_follower_idx" ON "remote_follows" USING btree ("follower_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "remote_follows_target_idx" ON "remote_follows" USING btree ("target_handle");--> statement-breakpoint
|
|
||||||
CREATE INDEX "remote_posts_author_idx" ON "remote_posts" USING btree ("author_handle");--> statement-breakpoint
|
|
||||||
CREATE INDEX "remote_posts_published_idx" ON "remote_posts" USING btree ("published_at");--> statement-breakpoint
|
|
||||||
CREATE INDEX "remote_posts_ap_id_idx" ON "remote_posts" USING btree ("ap_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "reports_status_idx" ON "reports" USING btree ("status");--> statement-breakpoint
|
|
||||||
CREATE INDEX "reports_target_idx" ON "reports" USING btree ("target_type","target_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "reports_reporter_idx" ON "reports" USING btree ("reporter_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "sessions_token_idx" ON "sessions" USING btree ("token");--> statement-breakpoint
|
|
||||||
CREATE INDEX "sessions_user_idx" ON "sessions" USING btree ("user_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "users_handle_idx" ON "users" USING btree ("handle");--> statement-breakpoint
|
|
||||||
CREATE INDEX "users_did_idx" ON "users" USING btree ("did");--> statement-breakpoint
|
|
||||||
CREATE INDEX "users_suspended_idx" ON "users" USING btree ("is_suspended");--> statement-breakpoint
|
|
||||||
CREATE INDEX "users_silenced_idx" ON "users" USING btree ("is_silenced");
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- Remove fetch_interval_minutes column from bot_content_sources
|
|
||||||
ALTER TABLE "bot_content_sources" DROP COLUMN IF EXISTS "fetch_interval_minutes";
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
-- Add bot_id column to posts table
|
|
||||||
ALTER TABLE "posts" ADD COLUMN "bot_id" uuid REFERENCES "bots"("id") ON DELETE SET NULL;
|
|
||||||
|
|
||||||
-- Create index for bot_id
|
|
||||||
CREATE INDEX IF NOT EXISTS "posts_bot_id_idx" ON "posts" ("bot_id");
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
ALTER TABLE "bots" DROP CONSTRAINT "bots_handle_unique";--> statement-breakpoint
|
|
||||||
ALTER TABLE "bot_content_items" DROP CONSTRAINT "bot_content_items_post_id_posts_id_fk";
|
|
||||||
--> statement-breakpoint
|
|
||||||
DROP INDEX "bots_handle_idx";--> statement-breakpoint
|
|
||||||
ALTER TABLE "bot_content_sources" ADD COLUMN "source_config" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "bots" ADD COLUMN "owner_id" uuid NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE "nodes" ADD COLUMN "logo_url" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "posts" ADD COLUMN "bot_id" uuid;--> statement-breakpoint
|
|
||||||
ALTER TABLE "users" ADD COLUMN "is_bot" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE "users" ADD COLUMN "bot_owner_id" uuid;--> statement-breakpoint
|
|
||||||
ALTER TABLE "bot_content_items" ADD CONSTRAINT "bot_content_items_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "bots" ADD CONSTRAINT "bots_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "posts" ADD CONSTRAINT "posts_bot_id_bots_id_fk" FOREIGN KEY ("bot_id") REFERENCES "public"."bots"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "users" ADD CONSTRAINT "users_bot_owner_id_users_id_fk" FOREIGN KEY ("bot_owner_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
CREATE INDEX "bots_owner_id_idx" ON "bots" USING btree ("owner_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "posts_bot_id_idx" ON "posts" USING btree ("bot_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "users_is_bot_idx" ON "users" USING btree ("is_bot");--> statement-breakpoint
|
|
||||||
CREATE INDEX "users_bot_owner_idx" ON "users" USING btree ("bot_owner_id");--> statement-breakpoint
|
|
||||||
ALTER TABLE "bot_content_sources" DROP COLUMN "fetch_interval_minutes";--> statement-breakpoint
|
|
||||||
ALTER TABLE "bots" DROP COLUMN "handle";--> statement-breakpoint
|
|
||||||
ALTER TABLE "bots" DROP COLUMN "bio";--> statement-breakpoint
|
|
||||||
ALTER TABLE "bots" DROP COLUMN "avatar_url";--> statement-breakpoint
|
|
||||||
ALTER TABLE "bots" DROP COLUMN "public_key";--> statement-breakpoint
|
|
||||||
ALTER TABLE "bots" DROP COLUMN "private_key_encrypted";
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- Fix bot_content_items post_id foreign key to allow cascade on delete
|
|
||||||
ALTER TABLE "bot_content_items" DROP CONSTRAINT IF EXISTS "bot_content_items_post_id_posts_id_fk";
|
|
||||||
ALTER TABLE "bot_content_items" ADD CONSTRAINT "bot_content_items_post_id_posts_id_fk"
|
|
||||||
FOREIGN KEY ("post_id") REFERENCES "posts"("id") ON DELETE SET NULL;
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
CREATE TABLE "swarm_nodes" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"domain" text NOT NULL,
|
|
||||||
"name" text,
|
|
||||||
"description" text,
|
|
||||||
"logo_url" text,
|
|
||||||
"public_key" text,
|
|
||||||
"software_version" text,
|
|
||||||
"user_count" integer,
|
|
||||||
"post_count" integer,
|
|
||||||
"discovered_via" text,
|
|
||||||
"discovered_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"last_seen_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"last_sync_at" timestamp,
|
|
||||||
"consecutive_failures" integer DEFAULT 0 NOT NULL,
|
|
||||||
"is_active" boolean DEFAULT true NOT NULL,
|
|
||||||
"trust_score" integer DEFAULT 50 NOT NULL,
|
|
||||||
"capabilities" text,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
CONSTRAINT "swarm_nodes_domain_unique" UNIQUE("domain")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "swarm_seeds" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"domain" text NOT NULL,
|
|
||||||
"priority" integer DEFAULT 100 NOT NULL,
|
|
||||||
"is_enabled" boolean DEFAULT true NOT NULL,
|
|
||||||
"last_contact_at" timestamp,
|
|
||||||
"consecutive_failures" integer DEFAULT 0 NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
CONSTRAINT "swarm_seeds_domain_unique" UNIQUE("domain")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "swarm_sync_log" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"remote_domain" text NOT NULL,
|
|
||||||
"direction" text NOT NULL,
|
|
||||||
"nodes_received" integer DEFAULT 0 NOT NULL,
|
|
||||||
"nodes_sent" integer DEFAULT 0 NOT NULL,
|
|
||||||
"handles_received" integer DEFAULT 0 NOT NULL,
|
|
||||||
"handles_sent" integer DEFAULT 0 NOT NULL,
|
|
||||||
"success" boolean NOT NULL,
|
|
||||||
"error_message" text,
|
|
||||||
"duration_ms" integer,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE INDEX "swarm_nodes_domain_idx" ON "swarm_nodes" USING btree ("domain");--> statement-breakpoint
|
|
||||||
CREATE INDEX "swarm_nodes_active_idx" ON "swarm_nodes" USING btree ("is_active");--> statement-breakpoint
|
|
||||||
CREATE INDEX "swarm_nodes_last_seen_idx" ON "swarm_nodes" USING btree ("last_seen_at");--> statement-breakpoint
|
|
||||||
CREATE INDEX "swarm_nodes_trust_idx" ON "swarm_nodes" USING btree ("trust_score");--> statement-breakpoint
|
|
||||||
CREATE INDEX "swarm_seeds_enabled_idx" ON "swarm_seeds" USING btree ("is_enabled");--> statement-breakpoint
|
|
||||||
CREATE INDEX "swarm_seeds_priority_idx" ON "swarm_seeds" USING btree ("priority");--> statement-breakpoint
|
|
||||||
CREATE INDEX "swarm_sync_log_remote_idx" ON "swarm_sync_log" USING btree ("remote_domain");--> statement-breakpoint
|
|
||||||
CREATE INDEX "swarm_sync_log_created_idx" ON "swarm_sync_log" USING btree ("created_at");
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- Add source_config column for Brave News and News API query builder configurations
|
|
||||||
ALTER TABLE "bot_content_sources" ADD COLUMN "source_config" text;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
CREATE TABLE "muted_nodes" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"user_id" uuid NOT NULL,
|
|
||||||
"node_domain" text NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "nodes" ADD COLUMN "is_nsfw" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE "posts" ADD COLUMN "is_nsfw" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE "swarm_nodes" ADD COLUMN "is_nsfw" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE "users" ADD COLUMN "is_nsfw" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE "users" ADD COLUMN "nsfw_enabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE "users" ADD COLUMN "age_verified_at" timestamp;--> statement-breakpoint
|
|
||||||
ALTER TABLE "muted_nodes" ADD CONSTRAINT "muted_nodes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
CREATE INDEX "muted_nodes_user_idx" ON "muted_nodes" USING btree ("user_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "muted_nodes_domain_idx" ON "muted_nodes" USING btree ("node_domain");--> statement-breakpoint
|
|
||||||
CREATE INDEX "blocks_blocked_user_idx" ON "blocks" USING btree ("blocked_user_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "mutes_muted_user_idx" ON "mutes" USING btree ("muted_user_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "posts_nsfw_idx" ON "posts" USING btree ("is_nsfw");--> statement-breakpoint
|
|
||||||
CREATE INDEX "swarm_nodes_nsfw_idx" ON "swarm_nodes" USING btree ("is_nsfw");--> statement-breakpoint
|
|
||||||
CREATE INDEX "users_nsfw_idx" ON "users" USING btree ("is_nsfw");
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
-- Bots as First-Class Users Migration
|
|
||||||
-- This migration transforms bots to have their own user accounts
|
|
||||||
|
|
||||||
-- Add bot-related fields to users table
|
|
||||||
ALTER TABLE "users" ADD COLUMN "is_bot" boolean DEFAULT false NOT NULL;
|
|
||||||
ALTER TABLE "users" ADD COLUMN "bot_owner_id" uuid REFERENCES "users"("id") ON DELETE CASCADE;
|
|
||||||
|
|
||||||
-- Create indexes for bot fields
|
|
||||||
CREATE INDEX IF NOT EXISTS "users_is_bot_idx" ON "users" ("is_bot");
|
|
||||||
CREATE INDEX IF NOT EXISTS "users_bot_owner_idx" ON "users" ("bot_owner_id");
|
|
||||||
|
|
||||||
-- Add owner_id to bots table (will be populated during migration)
|
|
||||||
ALTER TABLE "bots" ADD COLUMN "owner_id" uuid REFERENCES "users"("id") ON DELETE CASCADE;
|
|
||||||
|
|
||||||
-- Copy existing userId to ownerId (existing bots were owned by the user)
|
|
||||||
UPDATE "bots" SET "owner_id" = "user_id";
|
|
||||||
|
|
||||||
-- Make owner_id NOT NULL after populating
|
|
||||||
ALTER TABLE "bots" ALTER COLUMN "owner_id" SET NOT NULL;
|
|
||||||
|
|
||||||
-- Create index for owner_id
|
|
||||||
CREATE INDEX IF NOT EXISTS "bots_owner_id_idx" ON "bots" ("owner_id");
|
|
||||||
|
|
||||||
-- Remove columns that are now on the user account
|
|
||||||
-- Note: handle, bio, avatarUrl, publicKey, privateKeyEncrypted move to users table
|
|
||||||
-- We'll keep them for now and handle migration in application code
|
|
||||||
-- ALTER TABLE "bots" DROP COLUMN "handle";
|
|
||||||
-- ALTER TABLE "bots" DROP COLUMN "bio";
|
|
||||||
-- ALTER TABLE "bots" DROP COLUMN "avatar_url";
|
|
||||||
-- ALTER TABLE "bots" DROP COLUMN "public_key";
|
|
||||||
-- ALTER TABLE "bots" DROP COLUMN "private_key_encrypted";
|
|
||||||
|
|
||||||
-- Drop the handle index since handle is now on users
|
|
||||||
DROP INDEX IF EXISTS "bots_handle_idx";
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
ALTER TABLE "remote_followers" DROP CONSTRAINT "remote_followers_actor_url_unique";--> statement-breakpoint
|
|
||||||
ALTER TABLE "nodes" ADD COLUMN "favicon_url" text;--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX "remote_followers_user_actor_unique" ON "remote_followers" USING btree ("user_id","actor_url");
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- Add favicon_url column to nodes table
|
|
||||||
ALTER TABLE "nodes" ADD COLUMN IF NOT EXISTS "favicon_url" text;
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
CREATE TABLE "remote_likes" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"post_id" uuid NOT NULL,
|
|
||||||
"actor_handle" text NOT NULL,
|
|
||||||
"actor_node_domain" text NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "remote_reposts" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"post_id" uuid NOT NULL,
|
|
||||||
"actor_handle" text NOT NULL,
|
|
||||||
"actor_node_domain" text NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "nodes" ADD COLUMN "turnstile_site_key" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "nodes" ADD COLUMN "turnstile_secret_key" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "posts" ADD COLUMN "swarm_reply_to_id" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "posts" ADD COLUMN "swarm_reply_to_content" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "posts" ADD COLUMN "swarm_reply_to_author" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "remote_likes" ADD CONSTRAINT "remote_likes_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "remote_reposts" ADD CONSTRAINT "remote_reposts_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
CREATE INDEX "remote_likes_post_idx" ON "remote_likes" USING btree ("post_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "remote_likes_actor_idx" ON "remote_likes" USING btree ("actor_handle","actor_node_domain");--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX "remote_likes_unique" ON "remote_likes" USING btree ("post_id","actor_handle","actor_node_domain");--> statement-breakpoint
|
|
||||||
CREATE INDEX "remote_reposts_post_idx" ON "remote_reposts" USING btree ("post_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "remote_reposts_actor_idx" ON "remote_reposts" USING btree ("actor_handle","actor_node_domain");--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX "remote_reposts_unique" ON "remote_reposts" USING btree ("post_id","actor_handle","actor_node_domain");
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
CREATE TABLE "chat_conversations" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"type" text DEFAULT 'direct' NOT NULL,
|
|
||||||
"participant1_id" uuid NOT NULL,
|
|
||||||
"participant2_handle" text NOT NULL,
|
|
||||||
"last_message_at" timestamp,
|
|
||||||
"last_message_preview" text,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "chat_messages" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"conversation_id" uuid NOT NULL,
|
|
||||||
"sender_handle" text NOT NULL,
|
|
||||||
"sender_display_name" text,
|
|
||||||
"sender_avatar_url" text,
|
|
||||||
"sender_node_domain" text,
|
|
||||||
"encrypted_content" text NOT NULL,
|
|
||||||
"swarm_message_id" text,
|
|
||||||
"delivered_at" timestamp,
|
|
||||||
"read_at" timestamp,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
CONSTRAINT "chat_messages_swarm_message_id_unique" UNIQUE("swarm_message_id")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "chat_typing_indicators" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"conversation_id" uuid NOT NULL,
|
|
||||||
"user_handle" text NOT NULL,
|
|
||||||
"expires_at" timestamp NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "chat_conversations" ADD CONSTRAINT "chat_conversations_participant1_id_users_id_fk" FOREIGN KEY ("participant1_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "chat_messages" ADD CONSTRAINT "chat_messages_conversation_id_chat_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."chat_conversations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "chat_typing_indicators" ADD CONSTRAINT "chat_typing_indicators_conversation_id_chat_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."chat_conversations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
CREATE INDEX "chat_conversations_participant1_idx" ON "chat_conversations" USING btree ("participant1_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "chat_conversations_last_message_idx" ON "chat_conversations" USING btree ("last_message_at");--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX "chat_conversations_unique" ON "chat_conversations" USING btree ("participant1_id","participant2_handle");--> statement-breakpoint
|
|
||||||
CREATE INDEX "chat_messages_conversation_idx" ON "chat_messages" USING btree ("conversation_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "chat_messages_created_idx" ON "chat_messages" USING btree ("created_at");--> statement-breakpoint
|
|
||||||
CREATE INDEX "chat_messages_swarm_id_idx" ON "chat_messages" USING btree ("swarm_message_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "chat_typing_conversation_idx" ON "chat_typing_indicators" USING btree ("conversation_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "chat_typing_expires_idx" ON "chat_typing_indicators" USING btree ("expires_at");--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX "chat_typing_unique" ON "chat_typing_indicators" USING btree ("conversation_id","user_handle");
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- Add private_key_encrypted column to nodes table for cryptographic signing
|
|
||||||
ALTER TABLE "nodes" ADD COLUMN "private_key_encrypted" text;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
CREATE TABLE "remote_identity_cache" (
|
|
||||||
"did" text PRIMARY KEY NOT NULL,
|
|
||||||
"public_key" text NOT NULL,
|
|
||||||
"fetched_at" timestamp NOT NULL,
|
|
||||||
"expires_at" timestamp NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "signed_action_dedupe" (
|
|
||||||
"action_id" text PRIMARY KEY NOT NULL,
|
|
||||||
"did" text NOT NULL,
|
|
||||||
"nonce" text NOT NULL,
|
|
||||||
"ts" bigint NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "chat_messages" ADD COLUMN "sender_encrypted_content" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "chat_messages" ADD COLUMN "sender_chat_public_key" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "nodes" ADD COLUMN "private_key_encrypted" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "users" ADD COLUMN "chat_public_key" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "users" ADD COLUMN "chat_private_key_encrypted" text;--> statement-breakpoint
|
|
||||||
CREATE INDEX "signed_action_dedupe_created_idx" ON "signed_action_dedupe" USING btree ("created_at");
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
CREATE TABLE "chat_device_bundles" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"user_id" uuid NOT NULL,
|
|
||||||
"did" text NOT NULL,
|
|
||||||
"device_id" text NOT NULL,
|
|
||||||
"identity_key" text NOT NULL,
|
|
||||||
"signed_pre_key" text NOT NULL,
|
|
||||||
"signature" text NOT NULL,
|
|
||||||
"last_seen_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "chat_inbox" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"recipient_did" text NOT NULL,
|
|
||||||
"recipient_device_id" text,
|
|
||||||
"sender_did" text NOT NULL,
|
|
||||||
"envelope_json" text NOT NULL,
|
|
||||||
"is_read" boolean DEFAULT false NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"expires_at" timestamp NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "chat_one_time_keys" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"user_id" uuid NOT NULL,
|
|
||||||
"bundle_id" uuid NOT NULL,
|
|
||||||
"key_id" integer NOT NULL,
|
|
||||||
"public_key" text NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "chat_messages" ADD COLUMN "sender_did" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "chat_device_bundles" ADD CONSTRAINT "chat_device_bundles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "chat_one_time_keys" ADD CONSTRAINT "chat_one_time_keys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "chat_one_time_keys" ADD CONSTRAINT "chat_one_time_keys_bundle_id_chat_device_bundles_id_fk" FOREIGN KEY ("bundle_id") REFERENCES "public"."chat_device_bundles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
CREATE INDEX "chat_bundles_user_idx" ON "chat_device_bundles" USING btree ("user_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "chat_bundles_did_idx" ON "chat_device_bundles" USING btree ("did");--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX "chat_bundles_device_unique" ON "chat_device_bundles" USING btree ("user_id","device_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "chat_inbox_recipient_idx" ON "chat_inbox" USING btree ("recipient_did","recipient_device_id");--> statement-breakpoint
|
|
||||||
CREATE INDEX "chat_inbox_created_idx" ON "chat_inbox" USING btree ("created_at");--> statement-breakpoint
|
|
||||||
CREATE INDEX "chat_otk_bundle_idx" ON "chat_one_time_keys" USING btree ("bundle_id");--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX "chat_otk_unique" ON "chat_one_time_keys" USING btree ("bundle_id","key_id");
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-- Migration: Add user storage columns
|
|
||||||
ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "storage_provider" text;
|
|
||||||
ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "storage_api_key_encrypted" text;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-- Migration: Add logo_data and favicon_data columns for base64 storage
|
|
||||||
ALTER TABLE "nodes" ADD COLUMN IF NOT EXISTS "logo_data" text;
|
|
||||||
ALTER TABLE "nodes" ADD COLUMN IF NOT EXISTS "favicon_data" text;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- Migration: Add storage_public_base_url column to users table
|
|
||||||
ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "storage_public_base_url" text;
|
|
||||||
@@ -0,0 +1,620 @@
|
|||||||
|
CREATE TABLE `blocks` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`blocked_user_id` text NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_blocks_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_blocks_blocked_user_id_users_id_fk` FOREIGN KEY (`blocked_user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `bot_activity_logs` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`bot_id` text NOT NULL,
|
||||||
|
`action` text NOT NULL,
|
||||||
|
`details` text NOT NULL,
|
||||||
|
`success` integer NOT NULL,
|
||||||
|
`error_message` text,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_bot_activity_logs_bot_id_bots_id_fk` FOREIGN KEY (`bot_id`) REFERENCES `bots`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `bot_content_items` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`source_id` text NOT NULL,
|
||||||
|
`external_id` text NOT NULL,
|
||||||
|
`title` text NOT NULL,
|
||||||
|
`content` text,
|
||||||
|
`url` text NOT NULL,
|
||||||
|
`published_at` integer NOT NULL,
|
||||||
|
`fetched_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`is_processed` integer DEFAULT false NOT NULL,
|
||||||
|
`processed_at` integer,
|
||||||
|
`post_id` text,
|
||||||
|
`interest_score` integer,
|
||||||
|
`interest_reason` text,
|
||||||
|
CONSTRAINT `fk_bot_content_items_source_id_bot_content_sources_id_fk` FOREIGN KEY (`source_id`) REFERENCES `bot_content_sources`(`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_bot_content_items_post_id_posts_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `bot_content_sources` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`bot_id` text NOT NULL,
|
||||||
|
`type` text NOT NULL,
|
||||||
|
`url` text NOT NULL,
|
||||||
|
`subreddit` text,
|
||||||
|
`api_key_encrypted` text,
|
||||||
|
`source_config` text,
|
||||||
|
`keywords` text,
|
||||||
|
`is_active` integer DEFAULT true NOT NULL,
|
||||||
|
`last_fetch_at` integer,
|
||||||
|
`last_error` text,
|
||||||
|
`consecutive_errors` integer DEFAULT 0 NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_bot_content_sources_bot_id_bots_id_fk` FOREIGN KEY (`bot_id`) REFERENCES `bots`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `bot_mentions` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`bot_id` text NOT NULL,
|
||||||
|
`post_id` text NOT NULL,
|
||||||
|
`author_id` text NOT NULL,
|
||||||
|
`content` text NOT NULL,
|
||||||
|
`is_processed` integer DEFAULT false NOT NULL,
|
||||||
|
`processed_at` integer,
|
||||||
|
`response_post_id` text,
|
||||||
|
`is_remote` integer DEFAULT false NOT NULL,
|
||||||
|
`remote_actor_url` text,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_bot_mentions_bot_id_bots_id_fk` FOREIGN KEY (`bot_id`) REFERENCES `bots`(`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_bot_mentions_post_id_posts_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_bot_mentions_author_id_users_id_fk` FOREIGN KEY (`author_id`) REFERENCES `users`(`id`),
|
||||||
|
CONSTRAINT `fk_bot_mentions_response_post_id_posts_id_fk` FOREIGN KEY (`response_post_id`) REFERENCES `posts`(`id`)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `bot_rate_limits` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`bot_id` text NOT NULL,
|
||||||
|
`window_start` integer NOT NULL,
|
||||||
|
`window_type` text NOT NULL,
|
||||||
|
`post_count` integer DEFAULT 0 NOT NULL,
|
||||||
|
`reply_count` integer DEFAULT 0 NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_bot_rate_limits_bot_id_bots_id_fk` FOREIGN KEY (`bot_id`) REFERENCES `bots`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `bots` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`owner_id` text NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`personality_config` text NOT NULL,
|
||||||
|
`llm_provider` text NOT NULL,
|
||||||
|
`llm_model` text NOT NULL,
|
||||||
|
`llm_endpoint` text,
|
||||||
|
`llm_api_key_encrypted` text NOT NULL,
|
||||||
|
`schedule_config` text,
|
||||||
|
`autonomous_mode` integer DEFAULT false NOT NULL,
|
||||||
|
`is_active` integer DEFAULT true NOT NULL,
|
||||||
|
`is_suspended` integer DEFAULT false NOT NULL,
|
||||||
|
`suspension_reason` text,
|
||||||
|
`suspended_at` integer,
|
||||||
|
`last_post_at` integer,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_bots_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_bots_owner_id_users_id_fk` FOREIGN KEY (`owner_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `chat_conversations` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`type` text DEFAULT 'direct' NOT NULL,
|
||||||
|
`participant1_id` text NOT NULL,
|
||||||
|
`participant2_handle` text NOT NULL,
|
||||||
|
`last_message_at` integer,
|
||||||
|
`last_message_preview` text,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_chat_conversations_participant1_id_users_id_fk` FOREIGN KEY (`participant1_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `chat_messages` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`conversation_id` text NOT NULL,
|
||||||
|
`sender_handle` text NOT NULL,
|
||||||
|
`sender_display_name` text,
|
||||||
|
`sender_avatar_url` text,
|
||||||
|
`sender_node_domain` text,
|
||||||
|
`sender_did` text,
|
||||||
|
`content` text,
|
||||||
|
`swarm_message_id` text,
|
||||||
|
`delivered_at` integer,
|
||||||
|
`read_at` integer,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_chat_messages_conversation_id_chat_conversations_id_fk` FOREIGN KEY (`conversation_id`) REFERENCES `chat_conversations`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `chat_typing_indicators` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`conversation_id` text NOT NULL,
|
||||||
|
`user_handle` text NOT NULL,
|
||||||
|
`expires_at` integer NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_chat_typing_indicators_conversation_id_chat_conversations_id_fk` FOREIGN KEY (`conversation_id`) REFERENCES `chat_conversations`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `follows` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`follower_id` text NOT NULL,
|
||||||
|
`following_id` text NOT NULL,
|
||||||
|
`ap_id` text UNIQUE,
|
||||||
|
`pending` integer DEFAULT false,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_follows_follower_id_users_id_fk` FOREIGN KEY (`follower_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_follows_following_id_users_id_fk` FOREIGN KEY (`following_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `handle_registry` (
|
||||||
|
`handle` text PRIMARY KEY,
|
||||||
|
`did` text NOT NULL,
|
||||||
|
`node_domain` text NOT NULL,
|
||||||
|
`registered_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch()) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `likes` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`post_id` text NOT NULL,
|
||||||
|
`ap_id` text UNIQUE,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_likes_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_likes_post_id_posts_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `media` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`post_id` text,
|
||||||
|
`url` text NOT NULL,
|
||||||
|
`alt_text` text,
|
||||||
|
`mime_type` text,
|
||||||
|
`width` integer,
|
||||||
|
`height` integer,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_media_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_media_post_id_posts_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `muted_nodes` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`node_domain` text NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_muted_nodes_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `mutes` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`muted_user_id` text NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_mutes_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_mutes_muted_user_id_users_id_fk` FOREIGN KEY (`muted_user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `nodes` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`domain` text NOT NULL UNIQUE,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`description` text,
|
||||||
|
`long_description` text,
|
||||||
|
`rules` text,
|
||||||
|
`banner_url` text,
|
||||||
|
`logo_url` text,
|
||||||
|
`favicon_url` text,
|
||||||
|
`logo_data` text,
|
||||||
|
`favicon_data` text,
|
||||||
|
`accent_color` text DEFAULT '#FFFFFF',
|
||||||
|
`public_key` text,
|
||||||
|
`private_key_encrypted` text,
|
||||||
|
`is_nsfw` integer DEFAULT false NOT NULL,
|
||||||
|
`turnstile_site_key` text,
|
||||||
|
`turnstile_secret_key` text,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch()) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `notifications` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`actor_id` text,
|
||||||
|
`actor_handle` text NOT NULL,
|
||||||
|
`actor_display_name` text,
|
||||||
|
`actor_avatar_url` text,
|
||||||
|
`actor_node_domain` text,
|
||||||
|
`target_handle` text,
|
||||||
|
`target_display_name` text,
|
||||||
|
`target_avatar_url` text,
|
||||||
|
`target_node_domain` text,
|
||||||
|
`target_is_bot` integer,
|
||||||
|
`post_id` text,
|
||||||
|
`post_content` text,
|
||||||
|
`type` text NOT NULL,
|
||||||
|
`read_at` integer,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_notifications_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_notifications_actor_id_users_id_fk` FOREIGN KEY (`actor_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_notifications_post_id_posts_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `posts` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`bot_id` text,
|
||||||
|
`content` text NOT NULL,
|
||||||
|
`reply_to_id` text,
|
||||||
|
`repost_of_id` text,
|
||||||
|
`swarm_reply_to_id` text,
|
||||||
|
`swarm_reply_to_content` text,
|
||||||
|
`swarm_reply_to_author` text,
|
||||||
|
`likes_count` integer DEFAULT 0 NOT NULL,
|
||||||
|
`reposts_count` integer DEFAULT 0 NOT NULL,
|
||||||
|
`replies_count` integer DEFAULT 0 NOT NULL,
|
||||||
|
`is_nsfw` integer DEFAULT false NOT NULL,
|
||||||
|
`is_removed` integer DEFAULT false NOT NULL,
|
||||||
|
`removed_at` integer,
|
||||||
|
`removed_by` text,
|
||||||
|
`removed_reason` text,
|
||||||
|
`ap_id` text UNIQUE,
|
||||||
|
`ap_url` text,
|
||||||
|
`link_preview_url` text,
|
||||||
|
`link_preview_title` text,
|
||||||
|
`link_preview_description` text,
|
||||||
|
`link_preview_image` text,
|
||||||
|
`link_preview_type` text,
|
||||||
|
`link_preview_video_url` text,
|
||||||
|
`link_preview_media_json` text,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_posts_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_posts_bot_id_bots_id_fk` FOREIGN KEY (`bot_id`) REFERENCES `bots`(`id`) ON DELETE SET NULL,
|
||||||
|
CONSTRAINT `fk_posts_removed_by_users_id_fk` FOREIGN KEY (`removed_by`) REFERENCES `users`(`id`)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `remote_followers` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`actor_url` text NOT NULL,
|
||||||
|
`inbox_url` text NOT NULL,
|
||||||
|
`shared_inbox_url` text,
|
||||||
|
`handle` text,
|
||||||
|
`activity_id` text,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_remote_followers_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `remote_follows` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`follower_id` text NOT NULL,
|
||||||
|
`target_handle` text NOT NULL,
|
||||||
|
`target_actor_url` text NOT NULL,
|
||||||
|
`inbox_url` text NOT NULL,
|
||||||
|
`activity_id` text NOT NULL,
|
||||||
|
`display_name` text,
|
||||||
|
`bio` text,
|
||||||
|
`avatar_url` text,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_remote_follows_follower_id_users_id_fk` FOREIGN KEY (`follower_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `remote_identity_cache` (
|
||||||
|
`did` text PRIMARY KEY,
|
||||||
|
`public_key` text NOT NULL,
|
||||||
|
`fetched_at` integer NOT NULL,
|
||||||
|
`expires_at` integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `remote_likes` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`post_id` text NOT NULL,
|
||||||
|
`actor_handle` text NOT NULL,
|
||||||
|
`actor_node_domain` text NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_remote_likes_post_id_posts_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `remote_posts` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`ap_id` text NOT NULL,
|
||||||
|
`author_handle` text NOT NULL,
|
||||||
|
`author_actor_url` text NOT NULL,
|
||||||
|
`author_display_name` text,
|
||||||
|
`author_avatar_url` text,
|
||||||
|
`content` text NOT NULL,
|
||||||
|
`published_at` integer NOT NULL,
|
||||||
|
`link_preview_url` text,
|
||||||
|
`link_preview_title` text,
|
||||||
|
`link_preview_description` text,
|
||||||
|
`link_preview_image` text,
|
||||||
|
`link_preview_type` text,
|
||||||
|
`link_preview_video_url` text,
|
||||||
|
`link_preview_media_json` text,
|
||||||
|
`media_json` text,
|
||||||
|
`fetched_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `remote_reposts` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`post_id` text NOT NULL,
|
||||||
|
`actor_handle` text NOT NULL,
|
||||||
|
`actor_node_domain` text NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_remote_reposts_post_id_posts_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `reports` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`reporter_id` text,
|
||||||
|
`target_type` text NOT NULL,
|
||||||
|
`target_id` text NOT NULL,
|
||||||
|
`reason` text NOT NULL,
|
||||||
|
`status` text DEFAULT 'open' NOT NULL,
|
||||||
|
`resolved_at` integer,
|
||||||
|
`resolved_by` text,
|
||||||
|
`resolution_note` text,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_reports_reporter_id_users_id_fk` FOREIGN KEY (`reporter_id`) REFERENCES `users`(`id`) ON DELETE SET NULL,
|
||||||
|
CONSTRAINT `fk_reports_resolved_by_users_id_fk` FOREIGN KEY (`resolved_by`) REFERENCES `users`(`id`)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `sessions` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`token` text NOT NULL,
|
||||||
|
`expires_at` integer NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_sessions_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `signed_action_dedupe` (
|
||||||
|
`action_id` text PRIMARY KEY,
|
||||||
|
`did` text NOT NULL,
|
||||||
|
`nonce` text NOT NULL,
|
||||||
|
`ts` integer NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `swarm_nodes` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`domain` text NOT NULL,
|
||||||
|
`name` text,
|
||||||
|
`description` text,
|
||||||
|
`logo_url` text,
|
||||||
|
`public_key` text,
|
||||||
|
`software_version` text,
|
||||||
|
`user_count` integer,
|
||||||
|
`post_count` integer,
|
||||||
|
`is_nsfw` integer DEFAULT false NOT NULL,
|
||||||
|
`discovered_via` text,
|
||||||
|
`discovered_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`last_seen_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`last_sync_at` integer,
|
||||||
|
`consecutive_failures` integer DEFAULT 0 NOT NULL,
|
||||||
|
`is_active` integer DEFAULT true NOT NULL,
|
||||||
|
`trust_score` integer DEFAULT 50 NOT NULL,
|
||||||
|
`is_blocked` integer DEFAULT false NOT NULL,
|
||||||
|
`block_reason` text,
|
||||||
|
`blocked_at` integer,
|
||||||
|
`capabilities` text,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch()) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `swarm_seeds` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`domain` text NOT NULL UNIQUE,
|
||||||
|
`priority` integer DEFAULT 100 NOT NULL,
|
||||||
|
`is_enabled` integer DEFAULT true NOT NULL,
|
||||||
|
`last_contact_at` integer,
|
||||||
|
`consecutive_failures` integer DEFAULT 0 NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `swarm_sync_log` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`remote_domain` text NOT NULL,
|
||||||
|
`direction` text NOT NULL,
|
||||||
|
`nodes_received` integer DEFAULT 0 NOT NULL,
|
||||||
|
`nodes_sent` integer DEFAULT 0 NOT NULL,
|
||||||
|
`handles_received` integer DEFAULT 0 NOT NULL,
|
||||||
|
`handles_sent` integer DEFAULT 0 NOT NULL,
|
||||||
|
`success` integer NOT NULL,
|
||||||
|
`error_message` text,
|
||||||
|
`duration_ms` integer,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `user_swarm_likes` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`node_domain` text NOT NULL,
|
||||||
|
`original_post_id` text NOT NULL,
|
||||||
|
`author_handle` text NOT NULL,
|
||||||
|
`author_display_name` text,
|
||||||
|
`author_avatar_url` text,
|
||||||
|
`content` text NOT NULL,
|
||||||
|
`post_created_at` integer NOT NULL,
|
||||||
|
`likes_count` integer DEFAULT 0 NOT NULL,
|
||||||
|
`reposts_count` integer DEFAULT 0 NOT NULL,
|
||||||
|
`replies_count` integer DEFAULT 0 NOT NULL,
|
||||||
|
`link_preview_url` text,
|
||||||
|
`link_preview_title` text,
|
||||||
|
`link_preview_description` text,
|
||||||
|
`link_preview_image` text,
|
||||||
|
`link_preview_type` text,
|
||||||
|
`link_preview_video_url` text,
|
||||||
|
`link_preview_media_json` text,
|
||||||
|
`media_json` text,
|
||||||
|
`liked_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_user_swarm_likes_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `user_swarm_reposts` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`node_domain` text NOT NULL,
|
||||||
|
`original_post_id` text NOT NULL,
|
||||||
|
`author_handle` text NOT NULL,
|
||||||
|
`author_display_name` text,
|
||||||
|
`author_avatar_url` text,
|
||||||
|
`content` text NOT NULL,
|
||||||
|
`post_created_at` integer NOT NULL,
|
||||||
|
`likes_count` integer DEFAULT 0 NOT NULL,
|
||||||
|
`reposts_count` integer DEFAULT 0 NOT NULL,
|
||||||
|
`replies_count` integer DEFAULT 0 NOT NULL,
|
||||||
|
`link_preview_url` text,
|
||||||
|
`link_preview_title` text,
|
||||||
|
`link_preview_description` text,
|
||||||
|
`link_preview_image` text,
|
||||||
|
`link_preview_type` text,
|
||||||
|
`link_preview_video_url` text,
|
||||||
|
`link_preview_media_json` text,
|
||||||
|
`media_json` text,
|
||||||
|
`reposted_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_user_swarm_reposts_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`did` text NOT NULL,
|
||||||
|
`handle` text NOT NULL,
|
||||||
|
`email` text UNIQUE,
|
||||||
|
`password_hash` text,
|
||||||
|
`display_name` text,
|
||||||
|
`bio` text,
|
||||||
|
`avatar_url` text,
|
||||||
|
`header_url` text,
|
||||||
|
`private_key_encrypted` text,
|
||||||
|
`public_key` text NOT NULL,
|
||||||
|
`node_id` text,
|
||||||
|
`is_bot` integer DEFAULT false NOT NULL,
|
||||||
|
`bot_owner_id` text,
|
||||||
|
`is_nsfw` integer DEFAULT false NOT NULL,
|
||||||
|
`nsfw_enabled` integer DEFAULT false NOT NULL,
|
||||||
|
`age_verified_at` integer,
|
||||||
|
`is_suspended` integer DEFAULT false NOT NULL,
|
||||||
|
`suspension_reason` text,
|
||||||
|
`suspended_at` integer,
|
||||||
|
`is_silenced` integer DEFAULT false NOT NULL,
|
||||||
|
`silence_reason` text,
|
||||||
|
`silenced_at` integer,
|
||||||
|
`moved_to` text,
|
||||||
|
`moved_from` text,
|
||||||
|
`migrated_at` integer,
|
||||||
|
`storage_provider` text,
|
||||||
|
`storage_endpoint` text,
|
||||||
|
`storage_public_base_url` text,
|
||||||
|
`storage_region` text,
|
||||||
|
`storage_bucket` text,
|
||||||
|
`storage_access_key_encrypted` text,
|
||||||
|
`storage_secret_key_encrypted` text,
|
||||||
|
`followers_count` integer DEFAULT 0 NOT NULL,
|
||||||
|
`following_count` integer DEFAULT 0 NOT NULL,
|
||||||
|
`posts_count` integer DEFAULT 0 NOT NULL,
|
||||||
|
`website` text,
|
||||||
|
`dm_privacy` text DEFAULT 'everyone' NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
CONSTRAINT `fk_users_node_id_nodes_id_fk` FOREIGN KEY (`node_id`) REFERENCES `nodes`(`id`),
|
||||||
|
CONSTRAINT `users_bot_owner_id_users_id_fk` FOREIGN KEY (`bot_owner_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX `blocks_user_idx` ON `blocks` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `blocks_blocked_user_idx` ON `blocks` (`blocked_user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `bot_activity_logs_bot_idx` ON `bot_activity_logs` (`bot_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `bot_activity_logs_action_idx` ON `bot_activity_logs` (`action`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `bot_activity_logs_created_idx` ON `bot_activity_logs` (`created_at`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `bot_content_items_source_idx` ON `bot_content_items` (`source_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `bot_content_items_processed_idx` ON `bot_content_items` (`is_processed`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `bot_content_items_external_idx` ON `bot_content_items` (`external_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `bot_content_sources_bot_idx` ON `bot_content_sources` (`bot_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `bot_content_sources_type_idx` ON `bot_content_sources` (`type`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `bot_mentions_bot_idx` ON `bot_mentions` (`bot_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `bot_mentions_processed_idx` ON `bot_mentions` (`is_processed`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `bot_mentions_created_idx` ON `bot_mentions` (`created_at`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `bot_rate_limits_bot_window_idx` ON `bot_rate_limits` (`bot_id`,`window_start`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `bots_user_id_idx` ON `bots` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `bots_owner_id_idx` ON `bots` (`owner_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `bots_active_idx` ON `bots` (`is_active`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `chat_conversations_participant1_idx` ON `chat_conversations` (`participant1_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `chat_conversations_last_message_idx` ON `chat_conversations` (`last_message_at`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `chat_conversations_unique` ON `chat_conversations` (`participant1_id`,`participant2_handle`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `chat_messages_conversation_idx` ON `chat_messages` (`conversation_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `chat_messages_created_idx` ON `chat_messages` (`created_at`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `chat_messages_swarm_id_idx` ON `chat_messages` (`swarm_message_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `chat_typing_conversation_idx` ON `chat_typing_indicators` (`conversation_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `chat_typing_expires_idx` ON `chat_typing_indicators` (`expires_at`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `chat_typing_unique` ON `chat_typing_indicators` (`conversation_id`,`user_handle`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `follows_follower_idx` ON `follows` (`follower_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `follows_following_idx` ON `follows` (`following_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `handle_registry_updated_idx` ON `handle_registry` (`updated_at`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `likes_user_post_idx` ON `likes` (`user_id`,`post_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `media_user_idx` ON `media` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `media_post_idx` ON `media` (`post_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `muted_nodes_user_idx` ON `muted_nodes` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `muted_nodes_domain_idx` ON `muted_nodes` (`node_domain`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `mutes_user_idx` ON `mutes` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `mutes_muted_user_idx` ON `mutes` (`muted_user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `notifications_user_idx` ON `notifications` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `notifications_created_idx` ON `notifications` (`created_at`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `posts_user_id_idx` ON `posts` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `posts_bot_id_idx` ON `posts` (`bot_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `posts_created_at_idx` ON `posts` (`created_at`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `posts_reply_to_idx` ON `posts` (`reply_to_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `posts_removed_idx` ON `posts` (`is_removed`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `posts_nsfw_idx` ON `posts` (`is_nsfw`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `remote_followers_user_idx` ON `remote_followers` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `remote_followers_actor_idx` ON `remote_followers` (`actor_url`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `remote_followers_user_actor_unique` ON `remote_followers` (`user_id`,`actor_url`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `remote_follows_follower_idx` ON `remote_follows` (`follower_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `remote_follows_target_idx` ON `remote_follows` (`target_handle`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `remote_likes_post_idx` ON `remote_likes` (`post_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `remote_likes_actor_idx` ON `remote_likes` (`actor_handle`,`actor_node_domain`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `remote_likes_unique` ON `remote_likes` (`post_id`,`actor_handle`,`actor_node_domain`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `remote_posts_author_idx` ON `remote_posts` (`author_handle`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `remote_posts_published_idx` ON `remote_posts` (`published_at`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `remote_posts_ap_id_idx` ON `remote_posts` (`ap_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `remote_reposts_post_idx` ON `remote_reposts` (`post_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `remote_reposts_actor_idx` ON `remote_reposts` (`actor_handle`,`actor_node_domain`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `remote_reposts_unique` ON `remote_reposts` (`post_id`,`actor_handle`,`actor_node_domain`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `reports_status_idx` ON `reports` (`status`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `reports_target_idx` ON `reports` (`target_type`,`target_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `reports_reporter_idx` ON `reports` (`reporter_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `sessions_token_idx` ON `sessions` (`token`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `sessions_user_idx` ON `sessions` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `signed_action_dedupe_created_idx` ON `signed_action_dedupe` (`created_at`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `swarm_nodes_domain_idx` ON `swarm_nodes` (`domain`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `swarm_nodes_active_idx` ON `swarm_nodes` (`is_active`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `swarm_nodes_last_seen_idx` ON `swarm_nodes` (`last_seen_at`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `swarm_nodes_trust_idx` ON `swarm_nodes` (`trust_score`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `swarm_nodes_nsfw_idx` ON `swarm_nodes` (`is_nsfw`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `swarm_nodes_blocked_idx` ON `swarm_nodes` (`is_blocked`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `swarm_seeds_enabled_idx` ON `swarm_seeds` (`is_enabled`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `swarm_seeds_priority_idx` ON `swarm_seeds` (`priority`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `swarm_sync_log_remote_idx` ON `swarm_sync_log` (`remote_domain`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `swarm_sync_log_created_idx` ON `swarm_sync_log` (`created_at`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `user_swarm_likes_user_idx` ON `user_swarm_likes` (`user_id`,`liked_at`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `user_swarm_likes_post_idx` ON `user_swarm_likes` (`node_domain`,`original_post_id`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `user_swarm_likes_unique` ON `user_swarm_likes` (`user_id`,`node_domain`,`original_post_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `user_swarm_reposts_user_idx` ON `user_swarm_reposts` (`user_id`,`reposted_at`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `user_swarm_reposts_post_idx` ON `user_swarm_reposts` (`node_domain`,`original_post_id`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `user_swarm_reposts_unique` ON `user_swarm_reposts` (`user_id`,`node_domain`,`original_post_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `users_handle_idx` ON `users` (`handle`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `users_did_idx` ON `users` (`did`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `users_suspended_idx` ON `users` (`is_suspended`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `users_silenced_idx` ON `users` (`is_silenced`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `users_is_bot_idx` ON `users` (`is_bot`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `users_bot_owner_idx` ON `users` (`bot_owner_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `users_nsfw_idx` ON `users` (`is_nsfw`);
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,104 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"entries": [
|
|
||||||
{
|
|
||||||
"idx": 0,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1769153858323,
|
|
||||||
"tag": "0000_clumsy_donald_blake",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 1,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1769153858324,
|
|
||||||
"tag": "0004_add_source_config",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 2,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1769367465905,
|
|
||||||
"tag": "0002_add_logo_url",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 3,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1769370833410,
|
|
||||||
"tag": "0003_small_reavers",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 4,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1769377790354,
|
|
||||||
"tag": "0004_luxuriant_lockheed",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 5,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1769422771465,
|
|
||||||
"tag": "0005_yummy_millenium_guard",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 6,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1769427398729,
|
|
||||||
"tag": "0006_loud_moonstone",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 7,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1769446756226,
|
|
||||||
"tag": "0007_kind_agent_zero",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 8,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1769453302714,
|
|
||||||
"tag": "0008_illegal_carlie_cooper",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 9,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1769562092857,
|
|
||||||
"tag": "0009_sweet_chat",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 10,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1769579537338,
|
|
||||||
"tag": "0010_add_sender_did_to_chat_messages",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 11,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1769586658154,
|
|
||||||
"tag": "0011_elite_kat_farrell",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 12,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1769586699065,
|
|
||||||
"tag": "0012_quick_mockingbird",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 13,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1769638500000,
|
|
||||||
"tag": "0013_add_storage_public_base_url",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
+5
-4
@@ -1,12 +1,13 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
// Enable standalone output for Docker deployment
|
// Turso ships a platform-native Node module and must remain a runtime dependency.
|
||||||
// This creates a minimal server.js that doesn't require full node_modules
|
serverExternalPackages: ['@tursodatabase/database'],
|
||||||
output: 'standalone',
|
|
||||||
|
|
||||||
// Turbopack configuration
|
// Turbopack configuration
|
||||||
turbopack: {},
|
turbopack: {
|
||||||
|
root: process.cwd(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Generated
+278
-494
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -7,9 +7,10 @@
|
|||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"dev:dev": "dotenv -e .env.dev -- next dev --turbopack -p 3050",
|
"dev:dev": "dotenv -e .env.dev -- next dev --turbopack -p 3050",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "npm run db:migrate && next start --hostname 127.0.0.1",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
|
"db:migrate": "tsx scripts/migrate.ts",
|
||||||
"db:generate:dev": "dotenv -e .env.dev -- drizzle-kit generate",
|
"db:generate:dev": "dotenv -e .env.dev -- drizzle-kit generate",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"db:push:dev": "dotenv -e .env.dev -- drizzle-kit push",
|
"db:push:dev": "dotenv -e .env.dev -- drizzle-kit push",
|
||||||
@@ -21,19 +22,18 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.972.0",
|
"@aws-sdk/client-s3": "^3.972.0",
|
||||||
|
"@tursodatabase/database": "0.7.0",
|
||||||
"@upstash/redis": "^1.34.3",
|
"@upstash/redis": "^1.34.3",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"drizzle-kit": "^0.31.8",
|
"drizzle-orm": "1.0.0-rc.4",
|
||||||
"drizzle-orm": "^0.44.1",
|
|
||||||
"jose": "^6.0.11",
|
"jose": "^6.0.11",
|
||||||
"libsodium-wrappers-sumo": "^0.8.2",
|
"libsodium-wrappers-sumo": "^0.8.2",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"multiformats": "^13.4.2",
|
"multiformats": "^13.4.2",
|
||||||
"next": "16.1.4",
|
"next": "16.1.4",
|
||||||
"next-auth": "^5.0.0-beta.25",
|
"next-auth": "^5.0.0-beta.25",
|
||||||
"pg": "^8.17.2",
|
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
@@ -43,11 +43,11 @@
|
|||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20.19.30",
|
"@types/node": "^20.19.30",
|
||||||
"@types/pg": "^8.16.0",
|
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
|
"drizzle-kit": "1.0.0-rc.4",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.1.4",
|
"eslint-config-next": "16.1.4",
|
||||||
"fast-check": "^4.5.3",
|
"fast-check": "^4.5.3",
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { db } from '../src/db';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
await db.execute(`ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS sender_did text;`);
|
|
||||||
console.log('Added sender_did column to chat_messages');
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Caddy Entrypoint Script for Synapsis
|
|
||||||
# Reads the dynamic port from the shared file and starts Caddy
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SHARED_PORT_FILE="/var/run/synapsis/port"
|
|
||||||
DEFAULT_PORT=3000
|
|
||||||
|
|
||||||
# Wait for the port file to exist (with timeout)
|
|
||||||
echo "⏳ Waiting for Synapsis app to announce its port..."
|
|
||||||
TIMEOUT=60
|
|
||||||
ELAPSED=0
|
|
||||||
|
|
||||||
while [ ! -f "$SHARED_PORT_FILE" ] && [ $ELAPSED -lt $TIMEOUT ]; do
|
|
||||||
sleep 1
|
|
||||||
ELAPSED=$((ELAPSED + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -f "$SHARED_PORT_FILE" ]; then
|
|
||||||
APP_PORT=$(cat "$SHARED_PORT_FILE")
|
|
||||||
echo "✅ Found Synapsis app on port: $APP_PORT"
|
|
||||||
else
|
|
||||||
echo "⚠️ Port file not found after ${TIMEOUT}s, using default port: $DEFAULT_PORT"
|
|
||||||
APP_PORT=$DEFAULT_PORT
|
|
||||||
fi
|
|
||||||
|
|
||||||
export APP_PORT
|
|
||||||
|
|
||||||
# Validate that APP_PORT is a number
|
|
||||||
if ! echo "$APP_PORT" | grep -qE '^[0-9]+$'; then
|
|
||||||
echo "❌ Invalid port number: $APP_PORT"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "🌐 Starting Caddy reverse proxy to app:$APP_PORT..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Start Caddy with the environment variable set
|
|
||||||
exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Synapsis Docker Entrypoint Script
|
|
||||||
# Handles database migrations, port detection, and application startup
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Default port range for auto-detection
|
|
||||||
PORT_START=${PORT_START:-3000}
|
|
||||||
PORT_END=${PORT_END:-3020}
|
|
||||||
|
|
||||||
# Function to check if a port is available
|
|
||||||
check_port_available() {
|
|
||||||
local port=$1
|
|
||||||
if ! nc -z localhost "$port" 2>/dev/null; then
|
|
||||||
return 0 # Port is available
|
|
||||||
else
|
|
||||||
return 1 # Port is in use
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to find first available port in range
|
|
||||||
find_available_port() {
|
|
||||||
local start=$1
|
|
||||||
local end=$2
|
|
||||||
|
|
||||||
for port in $(seq "$start" "$end"); do
|
|
||||||
if check_port_available "$port"; then
|
|
||||||
echo "$port"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "ERROR: No available ports found in range $start-$end" >&2
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Handle PORT=auto
|
|
||||||
if [ "${PORT}" = "auto" ]; then
|
|
||||||
echo "🔍 PORT=auto detected, scanning for available port in range ${PORT_START}-${PORT_END}..."
|
|
||||||
|
|
||||||
DETECTED_PORT=$(find_available_port "$PORT_START" "$PORT_END")
|
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "❌ Failed to find an available port. Exiting."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export PORT="$DETECTED_PORT"
|
|
||||||
echo "✅ Using automatically detected port: $PORT"
|
|
||||||
else
|
|
||||||
echo "📡 Using configured port: $PORT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure PORT is set
|
|
||||||
if [ -z "$PORT" ]; then
|
|
||||||
echo "⚠️ PORT not set, defaulting to 3000"
|
|
||||||
export PORT=3000
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Write port to shared file for Caddy to read
|
|
||||||
SHARED_PORT_FILE="/var/run/synapsis/port"
|
|
||||||
mkdir -p "$(dirname "$SHARED_PORT_FILE")"
|
|
||||||
echo "$PORT" > "$SHARED_PORT_FILE"
|
|
||||||
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 ""
|
|
||||||
|
|
||||||
# Start the Next.js application
|
|
||||||
exec node server.js
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
IMAGE_REPO="${IMAGE_REPO:-ghcr.io/gnosyslabs/synapsis}"
|
|
||||||
PACKAGE_API="${PACKAGE_API:-/orgs/GnosysLabs/packages/container/synapsis/versions?per_page=100}"
|
|
||||||
BUILDER="${BUILDER:-}"
|
|
||||||
PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}"
|
|
||||||
DATE_PREFIX="${DATE_PREFIX:-$(date -u +%Y.%m.%d)}"
|
|
||||||
SOURCE_REPO="${SOURCE_REPO:-https://github.com/GnosysLabs/Synapsis}"
|
|
||||||
PRUNE_BUILD_CACHE="${PRUNE_BUILD_CACHE:-1}"
|
|
||||||
APP_VERSION="${APP_VERSION:-}"
|
|
||||||
EXTRA_TAGS="${EXTRA_TAGS:-}"
|
|
||||||
|
|
||||||
require_command() {
|
|
||||||
if ! command -v "$1" >/dev/null 2>&1; then
|
|
||||||
echo "❌ Required command not found: $1" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
require_command docker
|
|
||||||
require_command gh
|
|
||||||
require_command git
|
|
||||||
|
|
||||||
CURRENT_SHA="$(git rev-parse --short HEAD)"
|
|
||||||
CURRENT_FULL_SHA="$(git rev-parse HEAD)"
|
|
||||||
BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
||||||
GITHUB_URL="${SOURCE_REPO}/commit/${CURRENT_FULL_SHA}"
|
|
||||||
|
|
||||||
if [ -z "${APP_VERSION}" ]; then
|
|
||||||
existing_tags="$(
|
|
||||||
gh api "${PACKAGE_API}" --paginate --jq '.[].metadata.container.tags[]?' 2>/dev/null || true
|
|
||||||
)"
|
|
||||||
|
|
||||||
max_build=0
|
|
||||||
for tag in ${existing_tags}; do
|
|
||||||
case "${tag}" in
|
|
||||||
"${DATE_PREFIX}".*)
|
|
||||||
build_number="${tag##${DATE_PREFIX}.}"
|
|
||||||
case "${build_number}" in
|
|
||||||
''|*[!0-9]*)
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
if [ "${build_number}" -gt "${max_build}" ]; then
|
|
||||||
max_build="${build_number}"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
next_build=$((max_build + 1))
|
|
||||||
APP_VERSION="${DATE_PREFIX}.${next_build}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
BUILD_ARGS="
|
|
||||||
--platform ${PLATFORMS}
|
|
||||||
--build-arg APP_VERSION=${APP_VERSION}
|
|
||||||
--build-arg APP_COMMIT=${CURRENT_FULL_SHA}
|
|
||||||
--build-arg APP_BUILD_DATE=${BUILD_DATE}
|
|
||||||
--build-arg APP_GITHUB_URL=${GITHUB_URL}
|
|
||||||
--build-arg APP_IMAGE_REPO=${IMAGE_REPO}
|
|
||||||
--build-arg APP_SOURCE_REPO=${SOURCE_REPO}
|
|
||||||
"
|
|
||||||
|
|
||||||
TAG_ARGS="
|
|
||||||
-t ${IMAGE_REPO}:latest
|
|
||||||
-t ${IMAGE_REPO}:${APP_VERSION}
|
|
||||||
-t ${IMAGE_REPO}:${CURRENT_SHA}
|
|
||||||
"
|
|
||||||
|
|
||||||
for extra_tag in ${EXTRA_TAGS}; do
|
|
||||||
TAG_ARGS="${TAG_ARGS}
|
|
||||||
-t ${IMAGE_REPO}:${extra_tag}"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "========================================"
|
|
||||||
echo " Synapsis Docker Publish"
|
|
||||||
echo "========================================"
|
|
||||||
echo " Version: ${APP_VERSION}"
|
|
||||||
echo " Commit: ${CURRENT_SHA}"
|
|
||||||
echo " Build Date: ${BUILD_DATE}"
|
|
||||||
echo " Image: ${IMAGE_REPO}"
|
|
||||||
echo " Platforms: ${PLATFORMS}"
|
|
||||||
echo "========================================"
|
|
||||||
|
|
||||||
set -- docker buildx build
|
|
||||||
|
|
||||||
if [ -n "${BUILDER}" ]; then
|
|
||||||
set -- "$@" --builder "${BUILDER}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# shellcheck disable=SC2086
|
|
||||||
set -- "$@" $BUILD_ARGS -f docker/Dockerfile
|
|
||||||
# shellcheck disable=SC2086
|
|
||||||
set -- "$@" $TAG_ARGS --push .
|
|
||||||
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "✅ Published:"
|
|
||||||
echo " ${IMAGE_REPO}:latest"
|
|
||||||
echo " ${IMAGE_REPO}:${APP_VERSION}"
|
|
||||||
echo " ${IMAGE_REPO}:${CURRENT_SHA}"
|
|
||||||
|
|
||||||
for extra_tag in ${EXTRA_TAGS}; do
|
|
||||||
echo " ${IMAGE_REPO}:${extra_tag}"
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "${PRUNE_BUILD_CACHE}" = "1" ]; then
|
|
||||||
echo ""
|
|
||||||
echo "🧹 Pruning BuildKit cache"
|
|
||||||
if [ -n "${BUILDER}" ]; then
|
|
||||||
docker buildx prune --builder "${BUILDER}" -af >/dev/null
|
|
||||||
else
|
|
||||||
docker buildx prune -af >/dev/null
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
@@ -10,7 +10,7 @@ async function main() {
|
|||||||
console.log('--- Inspecting Remote Key ---');
|
console.log('--- Inspecting Remote Key ---');
|
||||||
|
|
||||||
const entry = await db.query.handleRegistry.findFirst({
|
const entry = await db.query.handleRegistry.findFirst({
|
||||||
where: eq(handleRegistry.did, TARGET_DID),
|
where: { did: TARGET_DID },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ async function migrateDIDs() {
|
|||||||
|
|
||||||
// Find all users with legacy did:synapsis: format
|
// Find all users with legacy did:synapsis: format
|
||||||
const legacyUsers = await db.query.users.findMany({
|
const legacyUsers = await db.query.users.findMany({
|
||||||
where: (users, { like }) => like(users.did, 'did:synapsis:%'),
|
where: { did: { like: 'did:synapsis:%' } },
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Found ${legacyUsers.length} users with legacy DID format\n`);
|
console.log(`Found ${legacyUsers.length} users with legacy DID format\n`);
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { migrate } from 'drizzle-orm/tursodatabase/migrator';
|
||||||
|
import { db } from '../src/db';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const result = await migrate(db, { migrationsFolder: './drizzle' });
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
throw new Error(`Database migration failed: ${JSON.stringify(result)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Database migrations are up to date.');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db, handleRegistry } from '@/db';
|
import { db, handleRegistry } from '@/db';
|
||||||
import { desc, eq, gt } from 'drizzle-orm';
|
import { desc } from 'drizzle-orm';
|
||||||
import { normalizeHandle } from '@/lib/federation/handles';
|
import { normalizeHandle } from '@/lib/federation/handles';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
@@ -17,7 +17,7 @@ export async function GET(request: Request) {
|
|||||||
if (handleParam) {
|
if (handleParam) {
|
||||||
const cleanHandle = normalizeHandle(handleParam);
|
const cleanHandle = normalizeHandle(handleParam);
|
||||||
const entry = await db.query.handleRegistry.findFirst({
|
const entry = await db.query.handleRegistry.findFirst({
|
||||||
where: eq(handleRegistry.handle, cleanHandle),
|
where: { handle: cleanHandle },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
@@ -36,8 +36,8 @@ export async function GET(request: Request) {
|
|||||||
|
|
||||||
const sinceDate = sinceParam ? new Date(sinceParam) : null;
|
const sinceDate = sinceParam ? new Date(sinceParam) : null;
|
||||||
const entries = await db.query.handleRegistry.findMany({
|
const entries = await db.query.handleRegistry.findMany({
|
||||||
where: sinceDate ? gt(handleRegistry.updatedAt, sinceDate) : undefined,
|
where: sinceDate ? { updatedAt: { gt: sinceDate } } : undefined,
|
||||||
orderBy: [desc(handleRegistry.updatedAt)],
|
orderBy: () => [desc(handleRegistry.updatedAt)],
|
||||||
limit,
|
limit,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,29 +38,6 @@ export default function AdminPage() {
|
|||||||
const [logoUploadError, setLogoUploadError] = useState<string | null>(null);
|
const [logoUploadError, setLogoUploadError] = useState<string | null>(null);
|
||||||
const [isUploadingFavicon, setIsUploadingFavicon] = useState(false);
|
const [isUploadingFavicon, setIsUploadingFavicon] = useState(false);
|
||||||
const [faviconUploadError, setFaviconUploadError] = useState<string | null>(null);
|
const [faviconUploadError, setFaviconUploadError] = useState<string | null>(null);
|
||||||
const [updateStatus, setUpdateStatus] = useState<{
|
|
||||||
current: { version: string; commit: string | null; buildDate: string | null };
|
|
||||||
latest: { version: string; commit: string | null; buildDate: string | null } | null;
|
|
||||||
updateAvailable: boolean;
|
|
||||||
updater: {
|
|
||||||
available: boolean;
|
|
||||||
status: string;
|
|
||||||
message?: string;
|
|
||||||
lastStartedAt?: string | null;
|
|
||||||
lastFinishedAt?: string | null;
|
|
||||||
lastExitCode?: number | null;
|
|
||||||
lastError?: string | null;
|
|
||||||
trigger?: 'manual' | 'auto' | null;
|
|
||||||
config?: {
|
|
||||||
autoUpdateEnabled: boolean;
|
|
||||||
intervalMinutes: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
} | null>(null);
|
|
||||||
const [loadingUpdateStatus, setLoadingUpdateStatus] = useState(false);
|
|
||||||
const [triggeringUpdate, setTriggeringUpdate] = useState(false);
|
|
||||||
const [savingAutoUpdate, setSavingAutoUpdate] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/admin/me')
|
fetch('/api/admin/me')
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
@@ -93,42 +70,12 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadUpdateStatus = async () => {
|
|
||||||
setLoadingUpdateStatus(true);
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/admin/update', { cache: 'no-store' });
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error('Failed to load update status');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
setUpdateStatus(data);
|
|
||||||
} catch {
|
|
||||||
setUpdateStatus(null);
|
|
||||||
} finally {
|
|
||||||
setLoadingUpdateStatus(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
loadNodeSettings();
|
loadNodeSettings();
|
||||||
loadUpdateStatus();
|
|
||||||
}
|
}
|
||||||
}, [isAdmin]);
|
}, [isAdmin]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isAdmin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const interval = window.setInterval(() => {
|
|
||||||
loadUpdateStatus();
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
return () => window.clearInterval(interval);
|
|
||||||
}, [isAdmin]);
|
|
||||||
|
|
||||||
const handleSaveSettings = async (override?: typeof nodeSettings) => {
|
const handleSaveSettings = async (override?: typeof nodeSettings) => {
|
||||||
const payload = override ?? nodeSettings;
|
const payload = override ?? nodeSettings;
|
||||||
setSavingSettings(true);
|
setSavingSettings(true);
|
||||||
@@ -234,33 +181,6 @@ export default function AdminPage() {
|
|||||||
setBannerPromptError('');
|
setBannerPromptError('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTriggerUpdate = async () => {
|
|
||||||
setTriggeringUpdate(true);
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/admin/update', { method: 'POST', keepalive: true });
|
|
||||||
const data = await res.json().catch(() => ({}));
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(data.error || 'Failed to start update');
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast(data.message || 'Update started. Synapsis will restart shortly.', 'success');
|
|
||||||
await loadUpdateStatus();
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : 'Failed to start update';
|
|
||||||
if (message.toLowerCase().includes('fetch') || message.toLowerCase().includes('network')) {
|
|
||||||
showToast('Update likely started. The node is restarting, so this page may disconnect briefly.', 'success');
|
|
||||||
window.setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 5000);
|
|
||||||
} else {
|
|
||||||
showToast(message, 'error');
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setTriggeringUpdate(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogoUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleLogoUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
@@ -297,38 +217,6 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleAutoUpdate = async () => {
|
|
||||||
if (!updateStatus?.updater.available || !updateStatus.updater.config) return;
|
|
||||||
|
|
||||||
const nextValue = !updateStatus.updater.config.autoUpdateEnabled;
|
|
||||||
setSavingAutoUpdate(true);
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/admin/update', {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ autoUpdateEnabled: nextValue }),
|
|
||||||
});
|
|
||||||
const data = await res.json().catch(() => ({}));
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(data.error || 'Failed to update auto-update setting');
|
|
||||||
}
|
|
||||||
|
|
||||||
setUpdateStatus((prev) => prev ? {
|
|
||||||
...prev,
|
|
||||||
updater: {
|
|
||||||
...prev.updater,
|
|
||||||
config: data.config,
|
|
||||||
},
|
|
||||||
} : prev);
|
|
||||||
showToast(nextValue ? 'Automatic updates enabled' : 'Automatic updates disabled', 'success');
|
|
||||||
} catch (error) {
|
|
||||||
showToast(error instanceof Error ? error.message : 'Failed to update auto-update setting', 'error');
|
|
||||||
} finally {
|
|
||||||
setSavingAutoUpdate(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFaviconUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFaviconUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
@@ -703,86 +591,6 @@ export default function AdminPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
|
||||||
padding: '16px',
|
|
||||||
background: 'var(--background-secondary)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
}}>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '16px', flexWrap: 'wrap' }}>
|
|
||||||
<div>
|
|
||||||
<h2 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '6px' }}>System Update</h2>
|
|
||||||
<p style={{ fontSize: '13px', color: 'var(--foreground-secondary)', margin: 0 }}>
|
|
||||||
Keep this node on the latest published Synapsis build.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={handleTriggerUpdate}
|
|
||||||
disabled={
|
|
||||||
triggeringUpdate ||
|
|
||||||
loadingUpdateStatus ||
|
|
||||||
updateStatus?.updater.available === false ||
|
|
||||||
updateStatus?.updater.status === 'updating' ||
|
|
||||||
!updateStatus?.updateAvailable
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{triggeringUpdate || updateStatus?.updater.status === 'updating' ? 'Updating...' : 'Update Now'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{updateStatus?.updater.available && updateStatus?.updater.config && (
|
|
||||||
<div style={{
|
|
||||||
marginTop: '16px',
|
|
||||||
padding: '12px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
background: 'var(--background)',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
gap: '16px',
|
|
||||||
}}>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontSize: '13px', fontWeight: 600, marginBottom: '4px' }}>
|
|
||||||
Automatic updates
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '12px', color: 'var(--foreground-secondary)' }}>
|
|
||||||
Enabled by default. This node checks for updates every {updateStatus.updater.config.intervalMinutes} minutes and installs them automatically.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className={`btn btn-sm ${updateStatus.updater.config.autoUpdateEnabled ? 'btn-primary' : 'btn-ghost'}`}
|
|
||||||
onClick={handleToggleAutoUpdate}
|
|
||||||
disabled={savingAutoUpdate}
|
|
||||||
style={{ flexShrink: 0 }}
|
|
||||||
>
|
|
||||||
{savingAutoUpdate
|
|
||||||
? 'Saving...'
|
|
||||||
: updateStatus.updater.config.autoUpdateEnabled
|
|
||||||
? 'Disable'
|
|
||||||
: 'Enable'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!updateStatus?.updater.available && (
|
|
||||||
<div style={{
|
|
||||||
marginTop: '16px',
|
|
||||||
padding: '12px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
background: 'var(--background)',
|
|
||||||
fontSize: '12px',
|
|
||||||
color: 'var(--foreground-secondary)',
|
|
||||||
}}>
|
|
||||||
One-click updates are unavailable on this host. Use:
|
|
||||||
<div style={{ marginTop: '8px', fontFamily: 'monospace', color: 'var(--foreground)' }}>
|
|
||||||
curl -fsSL https://synapsis.social/update.sh | bash
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -51,10 +51,7 @@ export async function POST(request: Request) {
|
|||||||
// First get conversation IDs where user is participant1 (local user)
|
// First get conversation IDs where user is participant1 (local user)
|
||||||
// For participant2, we need to check by handle since it's stored as text (can be remote)
|
// For participant2, we need to check by handle since it's stored as text (can be remote)
|
||||||
const conversations = await db.query.chatConversations.findMany({
|
const conversations = await db.query.chatConversations.findMany({
|
||||||
where: or(
|
where: { OR: [{ participant1Id: userId }, { participant2Handle: user.handle }] },
|
||||||
eq(chatConversations.participant1Id, userId),
|
|
||||||
eq(chatConversations.participant2Handle, user.handle)
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const conversationIds = conversations.map(c => c.id);
|
const conversationIds = conversations.map(c => c.id);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
// Check if email is already taken by another user
|
// Check if email is already taken by another user
|
||||||
const existingUser = await db.query.users.findFirst({
|
const existingUser = await db.query.users.findFirst({
|
||||||
where: eq(users.email, newEmail.toLowerCase()),
|
where: { email: newEmail.toLowerCase() },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingUser && existingUser.id !== user.id) {
|
if (existingUser && existingUser.id !== user.id) {
|
||||||
|
|||||||
@@ -156,11 +156,11 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'This account has already been migrated' }, { status: 400 });
|
return NextResponse.json({ error: 'This account has already been migrated' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||||
|
|
||||||
// Fetch user's posts
|
// Fetch user's posts
|
||||||
const userPosts = await db.query.posts.findMany({
|
const userPosts = await db.query.posts.findMany({
|
||||||
where: eq(posts.userId, user.id),
|
where: { userId: user.id },
|
||||||
with: {
|
with: {
|
||||||
media: true,
|
media: true,
|
||||||
},
|
},
|
||||||
@@ -169,19 +169,19 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
// Fetch user's following list (local and remote)
|
// Fetch user's following list (local and remote)
|
||||||
const userFollowing = await db.query.follows.findMany({
|
const userFollowing = await db.query.follows.findMany({
|
||||||
where: eq(follows.followerId, user.id),
|
where: { followerId: user.id },
|
||||||
with: {
|
with: {
|
||||||
following: true,
|
following: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const userRemoteFollowing = await db.query.remoteFollows.findMany({
|
const userRemoteFollowing = await db.query.remoteFollows.findMany({
|
||||||
where: eq(remoteFollows.followerId, user.id),
|
where: { followerId: user.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch DMs
|
// Fetch DMs
|
||||||
const userConversations = await db.query.chatConversations.findMany({
|
const userConversations = await db.query.chatConversations.findMany({
|
||||||
where: eq(chatConversations.participant1Id, user.id),
|
where: { participant1Id: user.id },
|
||||||
with: {
|
with: {
|
||||||
messages: true
|
messages: true
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
// Fetch Bots
|
// Fetch Bots
|
||||||
const userBots = await db.query.bots.findMany({
|
const userBots = await db.query.bots.findMany({
|
||||||
where: eq(bots.ownerId, user.id),
|
where: { ownerId: user.id },
|
||||||
with: {
|
with: {
|
||||||
user: true,
|
user: true,
|
||||||
contentSources: true,
|
contentSources: true,
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
// Check if DID already exists on this node
|
// Check if DID already exists on this node
|
||||||
const existingDid = await db.query.users.findFirst({
|
const existingDid = await db.query.users.findFirst({
|
||||||
where: eq(users.did, manifest.did),
|
where: { did: manifest.did },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingDid) {
|
if (existingDid) {
|
||||||
@@ -222,7 +222,7 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
// Check if handle is available
|
// Check if handle is available
|
||||||
const existingHandle = await db.query.users.findFirst({
|
const existingHandle = await db.query.users.findFirst({
|
||||||
where: eq(users.handle, handleClean),
|
where: { handle: handleClean },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingHandle) {
|
if (existingHandle) {
|
||||||
@@ -232,7 +232,7 @@ export async function POST(req: NextRequest) {
|
|||||||
}, { status: 409 });
|
}, { status: 409 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||||
const oldActorUrl = `https://${manifest.sourceNode}/users/${manifest.handle}`;
|
const oldActorUrl = `https://${manifest.sourceNode}/users/${manifest.handle}`;
|
||||||
const newActorUrl = `https://${nodeDomain}/users/${handleClean}`;
|
const newActorUrl = `https://${nodeDomain}/users/${handleClean}`;
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
// Check if this is an NSFW node and auto-enable NSFW settings
|
// Check if this is an NSFW node and auto-enable NSFW settings
|
||||||
const node = await db.query.nodes.findFirst({
|
const node = await db.query.nodes.findFirst({
|
||||||
where: eq(nodes.domain, nodeDomain),
|
where: { domain: nodeDomain },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (node?.isNsfw) {
|
if (node?.isNsfw) {
|
||||||
@@ -319,7 +319,7 @@ export async function POST(req: NextRequest) {
|
|||||||
} else {
|
} else {
|
||||||
// Local follow - look up user and add to follows table
|
// Local follow - look up user and add to follows table
|
||||||
const targetUser = await db.query.users.findFirst({
|
const targetUser = await db.query.users.findFirst({
|
||||||
where: eq(users.handle, follow.handle.toLowerCase()),
|
where: { handle: follow.handle.toLowerCase() },
|
||||||
});
|
});
|
||||||
if (targetUser) {
|
if (targetUser) {
|
||||||
await db.insert(follows).values({
|
await db.insert(follows).values({
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
// Find the user on this node
|
// Find the user on this node
|
||||||
const user = await db.query.users.findFirst({
|
const user = await db.query.users.findFirst({
|
||||||
where: eq(users.handle, oldHandle.toLowerCase()),
|
where: { handle: oldHandle.toLowerCase() },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -67,7 +67,7 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
// Get all followers to notify
|
// Get all followers to notify
|
||||||
const userFollowers = await db.query.follows.findMany({
|
const userFollowers = await db.query.follows.findMany({
|
||||||
where: eq(follows.followingId, user.id),
|
where: { followingId: user.id },
|
||||||
with: {
|
with: {
|
||||||
follower: true,
|
follower: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ export async function PATCH(req: NextRequest) {
|
|||||||
await requireAdmin();
|
await requireAdmin();
|
||||||
const data = await req.json();
|
const data = await req.json();
|
||||||
|
|
||||||
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||||
|
|
||||||
let node = await db.query.nodes.findFirst({
|
let node = await db.query.nodes.findFirst({
|
||||||
where: eq(nodes.domain, domain),
|
where: { domain: domain },
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Fallback: If not found, check if there is exactly ONE node in the system.
|
// 2. Fallback: If not found, check if there is exactly ONE node in the system.
|
||||||
|
|||||||
@@ -95,9 +95,9 @@ export async function POST(req: NextRequest) {
|
|||||||
const dataUrl = `data:${mimeType};base64,${base64Data}`;
|
const dataUrl = `data:${mimeType};base64,${base64Data}`;
|
||||||
|
|
||||||
// Get current node
|
// Get current node
|
||||||
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||||
let node = await db.query.nodes.findFirst({
|
let node = await db.query.nodes.findFirst({
|
||||||
where: eq(nodes.domain, domain),
|
where: { domain: domain },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fallback: If not found, check if there is exactly ONE node in the system
|
// Fallback: If not found, check if there is exactly ONE node in the system
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export async function GET() {
|
|||||||
await requireAdmin();
|
await requireAdmin();
|
||||||
|
|
||||||
const nodes = await db.query.swarmNodes.findMany({
|
const nodes = await db.query.swarmNodes.findMany({
|
||||||
orderBy: [desc(swarmNodes.isBlocked), desc(swarmNodes.blockedAt), desc(swarmNodes.lastSeenAt)],
|
orderBy: () => [desc(swarmNodes.isBlocked), desc(swarmNodes.blockedAt), desc(swarmNodes.lastSeenAt)],
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@@ -47,7 +47,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const data = mutateNodeSchema.parse(body);
|
const data = mutateNodeSchema.parse(body);
|
||||||
const domain = normalizeNodeDomain(data.domain);
|
const domain = normalizeNodeDomain(data.domain);
|
||||||
const localDomain = normalizeNodeDomain(process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000');
|
const localDomain = normalizeNodeDomain(process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821');
|
||||||
|
|
||||||
if (domain === localDomain) {
|
if (domain === localDomain) {
|
||||||
return NextResponse.json({ error: 'Cannot block this node itself' }, { status: 400 });
|
return NextResponse.json({ error: 'Cannot block this node itself' }, { status: 400 });
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export async function PATCH(request: Request, context: RouteContext) {
|
|||||||
const data = moderationSchema.parse(body);
|
const data = moderationSchema.parse(body);
|
||||||
|
|
||||||
const post = await db.query.posts.findFirst({
|
const post = await db.query.posts.findFirst({
|
||||||
where: eq(posts.id, id),
|
where: { id: id },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
|
|||||||
@@ -15,19 +15,16 @@ export async function GET(request: Request) {
|
|||||||
const status = searchParams.get('status') || 'active'; // active | removed | all
|
const status = searchParams.get('status') || 'active'; // active | removed | all
|
||||||
const limit = Math.min(parseInt(searchParams.get('limit') || '25'), 50);
|
const limit = Math.min(parseInt(searchParams.get('limit') || '25'), 50);
|
||||||
|
|
||||||
const where =
|
const where = status === 'all'
|
||||||
status === 'active'
|
? undefined
|
||||||
? eq(posts.isRemoved, false)
|
: { isRemoved: status === 'removed' };
|
||||||
: status === 'removed'
|
|
||||||
? eq(posts.isRemoved, true)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const results = await db.query.posts.findMany({
|
const results = await db.query.posts.findMany({
|
||||||
where,
|
where,
|
||||||
with: {
|
with: {
|
||||||
author: true,
|
author: true,
|
||||||
},
|
},
|
||||||
orderBy: [desc(posts.createdAt)],
|
orderBy: () => [desc(posts.createdAt)],
|
||||||
limit,
|
limit,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export async function PATCH(request: Request, context: RouteContext) {
|
|||||||
const data = updateSchema.parse(body);
|
const data = updateSchema.parse(body);
|
||||||
|
|
||||||
const report = await db.query.reports.findFirst({
|
const report = await db.query.reports.findFirst({
|
||||||
where: eq(reports.id, id),
|
where: { id: id },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!report) {
|
if (!report) {
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ export async function GET(request: Request) {
|
|||||||
const limit = Math.min(parseInt(searchParams.get('limit') || '25'), 50);
|
const limit = Math.min(parseInt(searchParams.get('limit') || '25'), 50);
|
||||||
|
|
||||||
const reportRows = await db.query.reports.findMany({
|
const reportRows = await db.query.reports.findMany({
|
||||||
where: status === 'all' ? undefined : eq(reports.status, status),
|
where: status === 'all' ? undefined : { status: status },
|
||||||
orderBy: [desc(reports.createdAt)],
|
orderBy: () => [desc(reports.createdAt)],
|
||||||
limit,
|
limit,
|
||||||
with: {
|
with: {
|
||||||
reporter: true,
|
reporter: true,
|
||||||
@@ -34,13 +34,13 @@ export async function GET(request: Request) {
|
|||||||
|
|
||||||
const postTargetsRaw = postIds.length
|
const postTargetsRaw = postIds.length
|
||||||
? await db.query.posts.findMany({
|
? await db.query.posts.findMany({
|
||||||
where: inArray(posts.id, postIds),
|
where: { id: { in: postIds } },
|
||||||
with: { author: true },
|
with: { author: true },
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
const userTargetsRaw = userIds.length
|
const userTargetsRaw = userIds.length
|
||||||
? await db.query.users.findMany({
|
? await db.query.users.findMany({
|
||||||
where: inArray(users.id, userIds),
|
where: { id: { in: userIds } },
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { requireAdmin } from '@/lib/auth/admin';
|
|
||||||
import { getCurrentBuildInfo, getLatestPublishedBuild, compareBuildVersions } from '@/lib/version';
|
|
||||||
import { getHostUpdaterStatus, triggerHostUpdate, updateHostUpdaterConfig } from '@/lib/host-updater';
|
|
||||||
|
|
||||||
function isUpdateAvailable(currentVersion: string, latestVersion: string | null | undefined) {
|
|
||||||
if (!latestVersion) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return compareBuildVersions(currentVersion, latestVersion) < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
await requireAdmin();
|
|
||||||
|
|
||||||
const [latest, updater] = await Promise.all([
|
|
||||||
getLatestPublishedBuild(),
|
|
||||||
getHostUpdaterStatus(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const current = getCurrentBuildInfo();
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
current,
|
|
||||||
latest,
|
|
||||||
updateAvailable: isUpdateAvailable(current.version, latest?.version),
|
|
||||||
updater,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.message === 'Admin required') {
|
|
||||||
return NextResponse.json({ error: 'Admin required' }, { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('[Admin Update] Status error:', error);
|
|
||||||
return NextResponse.json({ error: 'Failed to get update status' }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST() {
|
|
||||||
try {
|
|
||||||
await requireAdmin();
|
|
||||||
|
|
||||||
const result = await triggerHostUpdate();
|
|
||||||
return NextResponse.json(result, { status: 202 });
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.message === 'Admin required') {
|
|
||||||
return NextResponse.json({ error: 'Admin required' }, { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('[Admin Update] Trigger error:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error instanceof Error ? error.message : 'Failed to trigger update' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PATCH(request: Request) {
|
|
||||||
try {
|
|
||||||
await requireAdmin();
|
|
||||||
|
|
||||||
const body = await request.json();
|
|
||||||
if (typeof body.autoUpdateEnabled !== 'boolean') {
|
|
||||||
return NextResponse.json({ error: 'autoUpdateEnabled must be a boolean' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await updateHostUpdaterConfig(body.autoUpdateEnabled);
|
|
||||||
return NextResponse.json(result);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.message === 'Admin required') {
|
|
||||||
return NextResponse.json({ error: 'Admin required' }, { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('[Admin Update] Config error:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error instanceof Error ? error.message : 'Failed to update auto-update settings' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,7 @@ export async function PATCH(request: Request, context: RouteContext) {
|
|||||||
const data = moderationSchema.parse(body);
|
const data = moderationSchema.parse(body);
|
||||||
|
|
||||||
const user = await db.query.users.findFirst({
|
const user = await db.query.users.findFirst({
|
||||||
where: eq(users.id, id),
|
where: { id: id },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export async function GET(req: NextRequest) {
|
|||||||
let existingUser = null;
|
let existingUser = null;
|
||||||
try {
|
try {
|
||||||
existingUser = await db.query.users.findFirst({
|
existingUser = await db.query.users.findFirst({
|
||||||
where: eq(users.handle, handle),
|
where: { handle: handle },
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// Handle fresh installs where the users table isn't created yet.
|
// Handle fresh installs where the users table isn't created yet.
|
||||||
|
|||||||
@@ -81,9 +81,9 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Check if this is an NSFW node and auto-enable NSFW settings
|
// Check if this is an NSFW node and auto-enable NSFW settings
|
||||||
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||||
const node = await db.query.nodes.findFirst({
|
const node = await db.query.nodes.findFirst({
|
||||||
where: eq(nodes.domain, nodeDomain),
|
where: { domain: nodeDomain },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (node?.isNsfw) {
|
if (node?.isNsfw) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export async function GET(
|
|||||||
|
|
||||||
// Verify bot exists and user owns it
|
// Verify bot exists and user owns it
|
||||||
const bot = await db.query.bots.findFirst({
|
const bot = await db.query.bots.findFirst({
|
||||||
where: eq(bots.id, botId),
|
where: { id: botId },
|
||||||
columns: { id: true, userId: true },
|
columns: { id: true, userId: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export async function GET(
|
|||||||
|
|
||||||
// Verify bot exists and user owns it
|
// Verify bot exists and user owns it
|
||||||
const bot = await db.query.bots.findFirst({
|
const bot = await db.query.bots.findFirst({
|
||||||
where: eq(bots.id, botId),
|
where: { id: botId },
|
||||||
columns: { id: true, userId: true },
|
columns: { id: true, userId: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export async function POST(
|
|||||||
|
|
||||||
// Verify bot exists and user owns it
|
// Verify bot exists and user owns it
|
||||||
const bot = await db.query.bots.findFirst({
|
const bot = await db.query.bots.findFirst({
|
||||||
where: eq(bots.id, botId),
|
where: { id: botId },
|
||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export async function GET(
|
|||||||
|
|
||||||
// Verify bot exists and user owns it
|
// Verify bot exists and user owns it
|
||||||
const bot = await db.query.bots.findFirst({
|
const bot = await db.query.bots.findFirst({
|
||||||
where: eq(bots.id, botId),
|
where: { id: botId },
|
||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export async function POST(
|
|||||||
|
|
||||||
// Verify bot exists
|
// Verify bot exists
|
||||||
const bot = await db.query.bots.findFirst({
|
const bot = await db.query.bots.findFirst({
|
||||||
where: eq(bots.id, botId),
|
where: { id: botId },
|
||||||
columns: { id: true, userId: true },
|
columns: { id: true, userId: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user