fix(deploy): remove private source checkout from customer installer
This commit is contained in:
1 parent
d33ec5c47d
commit
e06a43c76f
68 files changed
+2374
-71
No files matched your search
@@ -0,0 +1,19 @@
|
|||||||
|
# Customer deployment boundary
|
||||||
|
|
||||||
|
The customer deployment is composed from public images and public, schema-only
|
||||||
|
migration assets. It has no source checkout, no build context, and no Auth
|
||||||
|
container. The default image registry is `git.leee.xin/overcooked` and the
|
||||||
|
default version is `latest`.
|
||||||
|
|
||||||
|
The S control service is exposed on port 18080 and the S WebUI on 18083. E is
|
||||||
|
exposed on 8096. Ports 80 and 443 are not claimed, so an existing edge proxy
|
||||||
|
does not conflict with this deployment.
|
||||||
|
|
||||||
|
Generated keys are written only to `/opt/overcooked/production.env` with mode
|
||||||
|
600. The installer does not log generated values, activation codes, or Auth
|
||||||
|
configuration. Failure logs are redacted before display.
|
||||||
|
|
||||||
|
The installer must stop on missing prerequisites, invalid public address,
|
||||||
|
occupied ports, failed image verification, failed schema migration, or failed
|
||||||
|
health checks. It must never continue with a partially initialized database.
|
||||||
|
|
||||||
@@ -1,10 +1,96 @@
|
|||||||
# OverCooked Customer Installer
|
# OverCooked Customer Installer
|
||||||
|
|
||||||
Public customer deployment script only. No Auth source, credentials, keys, or production configuration are included.
|
This repository is the public, image-only customer installer. A customer does
|
||||||
|
not need access to any application source repository. The installer downloads
|
||||||
|
only these public images:
|
||||||
|
|
||||||
|
- `git.leee.xin/overcooked/overcooked-e`
|
||||||
|
- `git.leee.xin/overcooked/overcooked-s-control`
|
||||||
|
- `git.leee.xin/overcooked/overcooked-s-webui`
|
||||||
|
- `git.leee.xin/overcooked/overcooked-media-node`
|
||||||
|
|
||||||
|
It does not download or start an Auth image, and it does not use Docker build
|
||||||
|
contexts.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Download this script and set the Auth URL, Auth public key map, and customer server IP:
|
```sh
|
||||||
|
curl -fsSL \\
|
||||||
|
https://git.leee.xin/OverCooked/OverCooked-Installer/raw/branch/main/install.sh \\
|
||||||
|
-o install.sh
|
||||||
|
|
||||||
curl -fsSL https://git.leee.xin/OverCooked/OverCooked-Installer/raw/branch/main/install.sh -o install.sh
|
sudo env \\
|
||||||
sudo env OVERCOOKED_AUTH_URL=https://auth.example.invalid OVERCOOKED_AUTH_PUBLIC_KEYS='{"auth-1":"PUBLIC_KEY"}' OVERCOOKED_SERVER_IP=203.0.113.10 ./install.sh
|
OVERCOOKED_SERVER_IP=203.0.113.10 \\
|
||||||
|
OVERCOOKED_AUTH_PUBLIC_KEYS='{"auth-1":"PUBLIC_KEY"}' \\
|
||||||
|
OVERCOOKED_ACTIVATION_CODE='ACTIVATION_CODE' \\
|
||||||
|
OVERCOOKED_TURNSTILE_SITE_KEY='TURNSTILE_SITE_KEY' \\
|
||||||
|
OVERCOOKED_TURNSTILE_SECRET_KEY='TURNSTILE_SECRET_KEY' \\
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The default central Auth endpoint is `http://148.135.97.10:18091`. Override it
|
||||||
|
with `OVERCOOKED_AUTH_URL`. Auth private keys, administrator credentials,
|
||||||
|
license databases, provider credentials, and production secrets are never
|
||||||
|
included in this repository.
|
||||||
|
|
||||||
|
The current S production image requires the Turnstile key pair shown above.
|
||||||
|
The installer fails early if the pair is missing or incomplete; it never uses
|
||||||
|
placeholder credentials.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
- WebUI: `http://SERVER_IP:18083/`
|
||||||
|
- S API: `http://SERVER_IP:18080/`
|
||||||
|
- E: `http://SERVER_IP:8096/`
|
||||||
|
- First setup: `http://SERVER_IP:18083/setup`
|
||||||
|
|
||||||
|
The installer checks these host ports before changing service state and does
|
||||||
|
not bind ports 80 or 443. Configure the firewall to allow the chosen public
|
||||||
|
ports and restrict database/Valkey traffic to the Docker networks.
|
||||||
|
|
||||||
|
The server address is selected from `OVERCOOKED_SERVER_IP`, or from the
|
||||||
|
interface selected by `ip -4 route get 1.1.1.1`. A detected RFC1918,
|
||||||
|
loopback, link-local, or carrier-grade NAT address is rejected unless explicitly
|
||||||
|
provided through `OVERCOOKED_SERVER_IP`.
|
||||||
|
|
||||||
|
## Configuration and logs
|
||||||
|
|
||||||
|
`OVERCOOKED_REGISTRY` and `OVERCOOKED_VERSION` override the default image
|
||||||
|
registry and tag. `OVERCOOKED_ROOT` changes the default `/opt/overcooked`
|
||||||
|
installation directory. Generated `production.env` is mode 600 and contains
|
||||||
|
the setup token and randomly generated service keys. The installer never
|
||||||
|
prints those values. Keep the file private.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose --env-file /opt/overcooked/production.env \\
|
||||||
|
-f /opt/overcooked/compose/s.yml -p overcooked-s ps
|
||||||
|
docker compose --env-file /opt/overcooked/production.env \\
|
||||||
|
-f /opt/overcooked/compose/e.yml -p overcooked-e logs --tail=100
|
||||||
|
```
|
||||||
|
|
||||||
|
Migration is fail-closed. E uses the migration command built into its official
|
||||||
|
image. S uses only the schema migration assets published in this repository.
|
||||||
|
If either migration fails, the installer exits non-zero and does not start the
|
||||||
|
corresponding application service.
|
||||||
|
|
||||||
|
## Common 404/401 download failures
|
||||||
|
|
||||||
|
The customer command must fetch the raw script from the public Installer
|
||||||
|
repository. A 404 usually means a wrong repository, branch, or raw path. A
|
||||||
|
401/403 means that a private source URL or credentialed mirror was used. Do
|
||||||
|
not grant customer access to application source to work around these errors.
|
||||||
|
Use the public raw URL and verify anonymous image pulls instead.
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
Review the project names and volumes first, then stop the two projects:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose --env-file /opt/overcooked/production.env \\
|
||||||
|
-f /opt/overcooked/compose/s.yml -p overcooked-s down
|
||||||
|
docker compose --env-file /opt/overcooked/production.env \\
|
||||||
|
-f /opt/overcooked/compose/e.yml -p overcooked-e down
|
||||||
|
```
|
||||||
|
|
||||||
|
The named volumes contain customer data and are intentionally not removed by
|
||||||
|
the installer or this documentation.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
3395c27ad4158e70d47e7d002c141b1261f502875f2e7ac5b26ade51b7689138 install.sh
|
||||||
|
028d415f0c0aef0ed9a807573239079f9e7e8cb3ed796d9dec90f4be8cafdd05 compose/s.yml
|
||||||
|
fab213abb69e0141139c0d4234a7b1636588206f6588942a55c3380114069d92 compose/e.yml
|
||||||
|
c226c5e1feb5cd6948ee9b32897e77240e21793090d8f42234ca999450ec90b0 config/e-server.production.yaml
|
||||||
|
3e3dc775017577fad3b36b6c21036dfcb6e530793de4c18c825dda888f62e4e3 s-migrate.sh
|
||||||
|
07f432c281ba90565cd376b42967909b6ff2d19d04454a3ba4e3b184117d45af s-migrations.tar.gz
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
name: overcooked-e
|
||||||
|
|
||||||
|
x-logging: &logging
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: 10m
|
||||||
|
max-file: "5"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: product_e
|
||||||
|
POSTGRES_USER: product_e
|
||||||
|
POSTGRES_PASSWORD: ${PRODUCT_E_POSTGRES_PASSWORD:?required}
|
||||||
|
POSTGRES_INITDB_ARGS: --data-checksums
|
||||||
|
volumes:
|
||||||
|
- e-postgres:/var/lib/postgresql/data
|
||||||
|
- e-backup-staging:/backup
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U product_e -d product_e"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
networks: [backend]
|
||||||
|
security_opt: [no-new-privileges:true]
|
||||||
|
logging: *logging
|
||||||
|
mem_limit: 2g
|
||||||
|
|
||||||
|
valkey:
|
||||||
|
image: valkey/valkey:8-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
VALKEY_PASSWORD: ${PRODUCT_E_VALKEY_PASSWORD:?required}
|
||||||
|
command: ["sh", "-ec", "exec valkey-server --appendonly yes --save 60 1 --requirepass \"$${VALKEY_PASSWORD}\""]
|
||||||
|
volumes:
|
||||||
|
- e-valkey:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "valkey-cli -a \"$${VALKEY_PASSWORD}\" --no-auth-warning ping | grep -q PONG"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
networks: [backend]
|
||||||
|
security_opt: [no-new-privileges:true]
|
||||||
|
logging: *logging
|
||||||
|
mem_limit: 1g
|
||||||
|
|
||||||
|
e-server:
|
||||||
|
image: ${PRODUCT_E_IMAGE:?required}
|
||||||
|
restart: unless-stopped
|
||||||
|
init: true
|
||||||
|
depends_on:
|
||||||
|
postgres: {condition: service_healthy}
|
||||||
|
valkey: {condition: service_healthy}
|
||||||
|
ports:
|
||||||
|
- ${PRODUCT_E_HTTP_PORT_MAPPING:-0.0.0.0:8096:8096}
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp:size=64m,mode=1777
|
||||||
|
volumes:
|
||||||
|
- e-data:/data
|
||||||
|
- e-plugin-data:/data/plugins
|
||||||
|
- ${OVERCOOKED_ROOT:-/opt/overcooked}/config/e-server.production.yaml:/etc/overcooked-e/config.yaml:ro
|
||||||
|
environment:
|
||||||
|
PRODUCT_E_CONFIG: /etc/overcooked-e/config.yaml
|
||||||
|
PRODUCT_E_ENVIRONMENT: production
|
||||||
|
PRODUCT_E_INSTANCE_ID: ${PRODUCT_E_INSTANCE_ID:-production-e-1}
|
||||||
|
PRODUCT_E_DATABASE_URL: ${PRODUCT_E_DATABASE_URL:?required}
|
||||||
|
PRODUCT_E_VALKEY_ADDRESS: valkey:6379
|
||||||
|
PRODUCT_E_VALKEY_PASSWORD: ${PRODUCT_E_VALKEY_PASSWORD:?required}
|
||||||
|
PRODUCT_E_CONTROL_PLANE_URL: ${PRODUCT_E_CONTROL_PLANE_URL:?required}
|
||||||
|
PRODUCT_E_CONTROL_PLANE_KEY_ID: ${PRODUCT_E_CONTROL_PLANE_KEY_ID:?required}
|
||||||
|
PRODUCT_E_CONTROL_PLANE_SHARED_SECRET: ${PRODUCT_E_CONTROL_PLANE_SHARED_SECRET:?required}
|
||||||
|
PRODUCT_E_LICENSE_ENABLED: ${PRODUCT_E_LICENSE_ENABLED:-true}
|
||||||
|
PRODUCT_E_AUTH_URL: ${PRODUCT_E_AUTH_URL:?required}
|
||||||
|
PRODUCT_E_AUTH_ALLOW_INSECURE_HTTP: ${PRODUCT_E_AUTH_ALLOW_INSECURE_HTTP:-false}
|
||||||
|
PRODUCT_E_AUTH_ACTIVATION_CODE: ${PRODUCT_E_AUTH_ACTIVATION_CODE:-}
|
||||||
|
PRODUCT_E_AUTH_PUBLIC_KEYS: ${PRODUCT_E_AUTH_PUBLIC_KEYS:?required}
|
||||||
|
PRODUCT_E_PLAYBACK_TICKET_SECRET: ${PRODUCT_E_PLAYBACK_TICKET_SECRET:?required}
|
||||||
|
PRODUCT_E_PLAYBACK_TICKET_KEY_ID: ${PRODUCT_E_PLAYBACK_TICKET_KEY_ID:?required}
|
||||||
|
PRODUCT_E_PLAYBACK_TICKET_PREVIOUS_PUBLIC_KEYS: ${PRODUCT_E_PLAYBACK_TICKET_PREVIOUS_PUBLIC_KEYS:-{}}
|
||||||
|
PRODUCT_E_PLAYBACK_LEASE_KEY: ${PRODUCT_E_PLAYBACK_LEASE_KEY:?required}
|
||||||
|
PRODUCT_E_STORAGE_CREDENTIAL_KEY: ${PRODUCT_E_STORAGE_CREDENTIAL_KEY:?required}
|
||||||
|
PRODUCT_E_STORAGE_CREDENTIAL_KEY_ID: ${PRODUCT_E_STORAGE_CREDENTIAL_KEY_ID:?required}
|
||||||
|
PRODUCT_E_STORAGE_CREDENTIAL_PREVIOUS_KEYS: ${PRODUCT_E_STORAGE_CREDENTIAL_PREVIOUS_KEYS:-{}}
|
||||||
|
PRODUCT_E_PLUGIN_ENABLED: "true"
|
||||||
|
PRODUCT_E_PLUGIN_UNSIGNED_POLICY: require-signature
|
||||||
|
PRODUCT_E_PLUGIN_TRUSTED_PUBLISHERS: ${PRODUCT_E_PLUGIN_TRUSTED_PUBLISHERS:-{}}
|
||||||
|
PRODUCT_E_PLUGIN_SECRET_KEY_ID: ${PRODUCT_E_PLUGIN_SECRET_KEY_ID:?required}
|
||||||
|
PRODUCT_E_PLUGIN_SECRET_KEY: ${PRODUCT_E_PLUGIN_SECRET_KEY:?required}
|
||||||
|
PRODUCT_E_PLUGIN_PREVIOUS_SECRET_KEYS: ${PRODUCT_E_PLUGIN_PREVIOUS_SECRET_KEYS:-{}}
|
||||||
|
PRODUCT_E_TMDB_TOKEN: ${PRODUCT_E_TMDB_TOKEN:-}
|
||||||
|
PRODUCT_E_BANGUMI_TOKEN: ${PRODUCT_E_BANGUMI_TOKEN:-}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:8096/health/live || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
start_period: 20s
|
||||||
|
retries: 6
|
||||||
|
stop_grace_period: 45s
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
|
control-plane:
|
||||||
|
aliases: [product-e]
|
||||||
|
egress:
|
||||||
|
security_opt: [no-new-privileges:true]
|
||||||
|
cap_drop: [ALL]
|
||||||
|
logging: *logging
|
||||||
|
mem_limit: 4g
|
||||||
|
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
|
internal: true
|
||||||
|
control-plane:
|
||||||
|
external: true
|
||||||
|
name: overcooked-control
|
||||||
|
egress:
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
e-postgres:
|
||||||
|
e-valkey:
|
||||||
|
e-data:
|
||||||
|
e-plugin-data:
|
||||||
|
e-backup-staging:
|
||||||
|
|
||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
name: overcooked-s
|
||||||
|
|
||||||
|
x-logging: &logging
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: 10m
|
||||||
|
max-file: "5"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: product_s
|
||||||
|
POSTGRES_USER: product_s
|
||||||
|
POSTGRES_PASSWORD: ${S_POSTGRES_PASSWORD:?required}
|
||||||
|
POSTGRES_INITDB_ARGS: --data-checksums
|
||||||
|
volumes:
|
||||||
|
- s-postgres:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U product_s -d product_s"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
networks: [backend]
|
||||||
|
security_opt: [no-new-privileges:true]
|
||||||
|
logging: *logging
|
||||||
|
mem_limit: 512m
|
||||||
|
|
||||||
|
valkey:
|
||||||
|
image: valkey/valkey:8-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
S_VALKEY_PASSWORD: ${S_VALKEY_PASSWORD:?required}
|
||||||
|
command: ["sh", "-ec", "exec valkey-server --appendonly yes --maxmemory-policy noeviction --requirepass \"$${S_VALKEY_PASSWORD}\""]
|
||||||
|
volumes:
|
||||||
|
- s-valkey:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "valkey-cli -a \"$${S_VALKEY_PASSWORD}\" --no-auth-warning ping | grep -q PONG"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
networks: [backend]
|
||||||
|
security_opt: [no-new-privileges:true]
|
||||||
|
logging: *logging
|
||||||
|
mem_limit: 128m
|
||||||
|
|
||||||
|
s-migrate:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
depends_on:
|
||||||
|
postgres: {condition: service_healthy}
|
||||||
|
environment:
|
||||||
|
PGHOST: postgres
|
||||||
|
PGUSER: product_s
|
||||||
|
PGDATABASE: product_s
|
||||||
|
PGPASSWORD: ${S_POSTGRES_PASSWORD:?required}
|
||||||
|
volumes:
|
||||||
|
- ${OVERCOOKED_ROOT:-/opt/overcooked}/migrations:/migrations:ro
|
||||||
|
- ${OVERCOOKED_ROOT:-/opt/overcooked}/s-migrate.sh:/migrate.sh:ro
|
||||||
|
command: ["sh", "/migrate.sh"]
|
||||||
|
networks: [backend]
|
||||||
|
security_opt: [no-new-privileges:true]
|
||||||
|
restart: "no"
|
||||||
|
logging: *logging
|
||||||
|
mem_limit: 128m
|
||||||
|
|
||||||
|
s-control:
|
||||||
|
image: ${S_CONTROL_IMAGE:?required}
|
||||||
|
restart: unless-stopped
|
||||||
|
init: true
|
||||||
|
depends_on:
|
||||||
|
s-migrate: {condition: service_completed_successfully}
|
||||||
|
valkey: {condition: service_healthy}
|
||||||
|
ports:
|
||||||
|
- ${S_API_PORT_MAPPING:-0.0.0.0:18080:8080}
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp:size=32m,mode=1777
|
||||||
|
environment:
|
||||||
|
S_HTTP_ADDR: :8080
|
||||||
|
S_DATABASE_URL: postgres://product_s:${S_POSTGRES_PASSWORD}@postgres:5432/product_s?sslmode=disable
|
||||||
|
S_VALKEY_URL: redis://:${S_VALKEY_PASSWORD}@valkey:6379/0
|
||||||
|
S_INTERNAL_KEY_ID: ${S_INTERNAL_KEY_ID:?required}
|
||||||
|
S_INTERNAL_SHARED_SECRET: ${S_INTERNAL_SHARED_SECRET:?required}
|
||||||
|
S_ACCESS_TOKEN_SECRET: ${S_ACCESS_TOKEN_SECRET:?required}
|
||||||
|
S_SETUP_TOKEN: ${S_SETUP_TOKEN:?required}
|
||||||
|
S_ACCESS_TOKEN_TTL: ${S_ACCESS_TOKEN_TTL:-15m}
|
||||||
|
S_EMBY_ACCESS_TOKEN_TTL: ${S_EMBY_ACCESS_TOKEN_TTL:-720h}
|
||||||
|
S_REFRESH_TOKEN_TTL: ${S_REFRESH_TOKEN_TTL:-720h}
|
||||||
|
S_PLAYBACK_SESSION_TTL: ${S_PLAYBACK_SESSION_TTL:-90s}
|
||||||
|
S_E_BASE_URL: ${S_E_BASE_URL:?required}
|
||||||
|
S_E_PUBLIC_URL: ${S_E_PUBLIC_URL:-}
|
||||||
|
S_E_KEY_ID: ${S_E_KEY_ID:?required}
|
||||||
|
S_E_INTERNAL_SECRET: ${S_E_INTERNAL_SECRET:?required}
|
||||||
|
S_E_ALLOW_INSECURE_HTTP: ${S_E_ALLOW_INSECURE_HTTP:-false}
|
||||||
|
S_AUTH_BASE_URL: ${S_AUTH_BASE_URL:?required}
|
||||||
|
S_AUTH_ALLOW_INSECURE_HTTP: ${S_AUTH_ALLOW_INSECURE_HTTP:-false}
|
||||||
|
S_AUTH_PUBLIC_KEYS: ${S_AUTH_PUBLIC_KEYS:?required}
|
||||||
|
S_AUTH_HEARTBEAT_INTERVAL: ${S_AUTH_HEARTBEAT_INTERVAL:-5m}
|
||||||
|
S_INSTANCE_ID: ${S_INSTANCE_ID:-s-production-1}
|
||||||
|
S_PLATFORM: ${S_PLATFORM:-linux-amd64}
|
||||||
|
S_VERSION: ${S_VERSION:-customer}
|
||||||
|
S_BACKUP_DIRECTORY: /var/lib/overcooked-s/backups
|
||||||
|
S_UPDATE_DIRECTORY: /var/lib/overcooked-s/updates
|
||||||
|
S_TURNSTILE_SITE_KEY: ${S_TURNSTILE_SITE_KEY:?required}
|
||||||
|
S_TURNSTILE_SECRET_KEY: ${S_TURNSTILE_SECRET_KEY:?required}
|
||||||
|
S_TURNSTILE_HOSTNAME: ${S_TURNSTILE_HOSTNAME:-}
|
||||||
|
S_TELEGRAM_MTPROTO_URL: ${S_TELEGRAM_MTPROTO_URL:-http://telegram-disabled.invalid:8098}
|
||||||
|
S_PORTAL_LOCATION: ${S_PORTAL_LOCATION:-Private Node}
|
||||||
|
S_PORTAL_SERVER_URL: ${S_PORTAL_SERVER_URL:-}
|
||||||
|
S_PORTAL_WEB_CLIENT_URL: ${S_PORTAL_WEB_CLIENT_URL:-}
|
||||||
|
S_PORTAL_ANNOUNCEMENT: ${S_PORTAL_ANNOUNCEMENT:-}
|
||||||
|
S_STORAGE_WEBHOOK_SECRET: ${S_STORAGE_WEBHOOK_SECRET:-}
|
||||||
|
S_NODE_IMAGE_ARCHIVE: /var/lib/overcooked-s/node-images/media-node.tar.gz
|
||||||
|
S_NODE_IMAGE_NAME: ${S_NODE_IMAGE_NAME:-overcooked-media-node:latest}
|
||||||
|
S_NODE_CONTROLLER_HOST: ${S_NODE_CONTROLLER_HOST:-}
|
||||||
|
volumes:
|
||||||
|
- s-backups:/var/lib/overcooked-s/backups
|
||||||
|
- s-updates:/var/lib/overcooked-s/updates
|
||||||
|
- ${OVERCOOKED_ROOT:-/opt/overcooked}/node-images:/var/lib/overcooked-s/node-images:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:8080/health/live || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
start_period: 15s
|
||||||
|
retries: 6
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
|
control-plane:
|
||||||
|
aliases: [product-s]
|
||||||
|
security_opt: [no-new-privileges:true]
|
||||||
|
cap_drop: [ALL]
|
||||||
|
logging: *logging
|
||||||
|
mem_limit: 384m
|
||||||
|
|
||||||
|
webui:
|
||||||
|
image: ${S_WEBUI_IMAGE:?required}
|
||||||
|
restart: unless-stopped
|
||||||
|
init: true
|
||||||
|
depends_on:
|
||||||
|
s-control: {condition: service_healthy}
|
||||||
|
ports:
|
||||||
|
- ${S_WEB_PORT_MAPPING:-0.0.0.0:18083:3000}
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp:size=64m,mode=1777
|
||||||
|
- /app/.next/cache:size=64m,mode=0755,uid=1000,gid=1000
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
|
frontend:
|
||||||
|
control-plane:
|
||||||
|
aliases: [product-s-web]
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:3000/ || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
start_period: 20s
|
||||||
|
retries: 6
|
||||||
|
security_opt: [no-new-privileges:true]
|
||||||
|
cap_drop: [ALL]
|
||||||
|
logging: *logging
|
||||||
|
mem_limit: 384m
|
||||||
|
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
|
internal: true
|
||||||
|
frontend:
|
||||||
|
control-plane:
|
||||||
|
external: true
|
||||||
|
name: overcooked-control
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
s-postgres:
|
||||||
|
s-valkey:
|
||||||
|
s-backups:
|
||||||
|
s-updates:
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
service:
|
||||||
|
name: overcooked-e
|
||||||
|
instance_id: customer-e
|
||||||
|
environment: production
|
||||||
|
emby:
|
||||||
|
server_name: OverCooked
|
||||||
|
server_id: 10000000-0000-4000-8000-000000000001
|
||||||
|
compatibility_version: 4.8.0.80
|
||||||
|
control_plane:
|
||||||
|
base_url: https://s-control.invalid
|
||||||
|
key_id: customer-s
|
||||||
|
shared_secret: replace-by-environment
|
||||||
|
timeout: 10s
|
||||||
|
server:
|
||||||
|
address: 0.0.0.0:8096
|
||||||
|
database:
|
||||||
|
url: postgres://product_e:replace-by-environment@postgres:5432/product_e?sslmode=disable
|
||||||
|
valkey:
|
||||||
|
address: valkey:6379
|
||||||
|
observability:
|
||||||
|
log_level: info
|
||||||
|
trace_sample_ratio: 0.05
|
||||||
|
playback:
|
||||||
|
node_id: 10000000-0000-4000-8000-000000000001
|
||||||
|
plugins:
|
||||||
|
enabled: true
|
||||||
|
root: /data/plugins
|
||||||
|
sdk_socket: unix:///data/plugins/runtime/core.sock
|
||||||
|
core_version: 0.1.0
|
||||||
|
unsigned_policy: require-signature
|
||||||
|
trusted_publishers: {}
|
||||||
|
secret_key_id: customer-plugin
|
||||||
|
previous_secret_keys: {}
|
||||||
|
network_allowed_domains: []
|
||||||
|
network_allowed_cidrs: []
|
||||||
+226
-67
@@ -1,118 +1,277 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
umask 077
|
||||||
|
|
||||||
# Public customer bootstrap. Auth is an external service; no Auth image or
|
# Standalone public customer bootstrap. It never checks out application source
|
||||||
# private source is downloaded by this script.
|
# and it never pulls an Auth image.
|
||||||
REGISTRY="${OVERCOOKED_REGISTRY:-git.leee.xin/overcooked}"
|
REGISTRY=${OVERCOOKED_REGISTRY:-git.leee.xin/overcooked}
|
||||||
VERSION="${OVERCOOKED_VERSION:-latest}"
|
VERSION=${OVERCOOKED_VERSION:-latest}
|
||||||
ROOT="${OVERCOOKED_ROOT:-/opt/overcooked}"
|
ROOT=${OVERCOOKED_ROOT:-/opt/overcooked}
|
||||||
SERVER_IP="${OVERCOOKED_SERVER_IP:-}"
|
SERVER_IP=${OVERCOOKED_SERVER_IP:-}
|
||||||
ACTIVATION_CODE="${OVERCOOKED_ACTIVATION_CODE:-}"
|
ACTIVATION_CODE=${OVERCOOKED_ACTIVATION_CODE:-}
|
||||||
# Central OverCooked Auth gateway. Override for private deployments.
|
AUTH_URL=${OVERCOOKED_AUTH_URL:-http://148.135.97.10:18091}
|
||||||
AUTH_URL="${OVERCOOKED_AUTH_URL:-http://148.135.97.10:18091}"
|
AUTH_PUBLIC_KEYS=${OVERCOOKED_AUTH_PUBLIC_KEYS:-}
|
||||||
AUTH_PUBLIC_KEYS="${OVERCOOKED_AUTH_PUBLIC_KEYS:-}"
|
INSTALLER_BASE_URL=${OVERCOOKED_INSTALLER_BASE_URL:-https://git.leee.xin/OverCooked/OverCooked-Installer/raw/branch/main}
|
||||||
|
TURNSTILE_SITE_KEY=${OVERCOOKED_TURNSTILE_SITE_KEY:-}
|
||||||
|
TURNSTILE_SECRET_KEY=${OVERCOOKED_TURNSTILE_SECRET_KEY:-}
|
||||||
|
S_COMPOSE=$ROOT/compose/s.yml
|
||||||
|
E_COMPOSE=$ROOT/compose/e.yml
|
||||||
|
ENV_FILE=$ROOT/production.env
|
||||||
|
S_PROJECT=overcooked-s
|
||||||
|
E_PROJECT=overcooked-e
|
||||||
|
FAILURE_REPORTED=0
|
||||||
|
S_POSTGRES_PASSWORD=not-generated
|
||||||
|
S_VALKEY_PASSWORD=not-generated
|
||||||
|
S_INTERNAL_SHARED_SECRET=not-generated
|
||||||
|
S_ACCESS_TOKEN_SECRET=not-generated
|
||||||
|
S_SETUP_TOKEN=not-generated
|
||||||
|
E_POSTGRES_PASSWORD=not-generated
|
||||||
|
E_PLAYBACK_TICKET_SECRET=not-generated
|
||||||
|
E_PLAYBACK_LEASE_KEY=not-generated
|
||||||
|
E_STORAGE_CREDENTIAL_KEY=not-generated
|
||||||
|
E_PLUGIN_SECRET_KEY=not-generated
|
||||||
|
|
||||||
die() { echo "overcooked-install: $*" >&2; exit 1; }
|
die() { echo "overcooked-install: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
is_ipv4() {
|
||||||
|
old_ifs=$IFS; IFS=.; set -- $1; IFS=$old_ifs
|
||||||
|
[ "$#" = 4 ] || return 1
|
||||||
|
for octet in "$@"; do
|
||||||
|
case "$octet" in ''|*[!0-9]*) return 1 ;; esac
|
||||||
|
[ "$octet" -le 255 ] 2>/dev/null || return 1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
is_private_ipv4() {
|
||||||
|
second=$(printf '%s\n' "$1" | cut -d. -f2)
|
||||||
|
case "$1" in
|
||||||
|
0.*|10.*|127.*|169.254.*|192.168.*|100.64.*|100.65.*|100.66.*|100.67.*|100.68.*|100.69.*|100.70.*|100.71.*|100.72.*|100.73.*|100.74.*|100.75.*|100.76.*|100.77.*|100.78.*|100.79.*|100.8*.*|100.9*.*) return 0 ;;
|
||||||
|
172.*) [ "$second" -ge 16 ] 2>/dev/null && [ "$second" -le 31 ] 2>/dev/null; return ;;
|
||||||
|
esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
check_port() {
|
||||||
|
port=$1
|
||||||
|
if command -v ss >/dev/null 2>&1 && ss -H -ltn 2>/dev/null | awk -v p=":$port" '$4 ~ p "$" || $4 ~ p "\\]$" { found=1 } END { exit !found }'; then
|
||||||
|
die "端口 $port 已被占用,请停止占用进程或调整部署端口"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
redact_logs() {
|
||||||
|
sed -e "s|$S_POSTGRES_PASSWORD|[REDACTED]|g" \
|
||||||
|
-e "s|$S_VALKEY_PASSWORD|[REDACTED]|g" \
|
||||||
|
-e "s|$S_INTERNAL_SHARED_SECRET|[REDACTED]|g" \
|
||||||
|
-e "s|$S_ACCESS_TOKEN_SECRET|[REDACTED]|g" \
|
||||||
|
-e "s|$S_SETUP_TOKEN|[REDACTED]|g" \
|
||||||
|
-e "s|$E_POSTGRES_PASSWORD|[REDACTED]|g" \
|
||||||
|
-e "s|$E_PLAYBACK_TICKET_SECRET|[REDACTED]|g" \
|
||||||
|
-e "s|$E_PLAYBACK_LEASE_KEY|[REDACTED]|g" \
|
||||||
|
-e "s|$E_STORAGE_CREDENTIAL_KEY|[REDACTED]|g" \
|
||||||
|
-e "s|$E_PLUGIN_SECRET_KEY|[REDACTED]|g"
|
||||||
|
}
|
||||||
|
show_failure() {
|
||||||
|
[ "$FAILURE_REPORTED" = 1 ] && return 0
|
||||||
|
FAILURE_REPORTED=1
|
||||||
|
[ -f "$ENV_FILE" ] || return 0
|
||||||
|
echo "安装失败,以下是脱敏后的容器状态和最近日志:" >&2
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" ps >&2 || true
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" ps >&2 || true
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" logs --tail=100 2>&1 | redact_logs >&2 || true
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" logs --tail=100 2>&1 | redact_logs >&2 || true
|
||||||
|
}
|
||||||
|
on_exit() { rc=$?; [ "$rc" -eq 0 ] || show_failure; exit "$rc"; }
|
||||||
|
trap on_exit EXIT
|
||||||
|
|
||||||
[ "$(id -u)" = 0 ] || die "请使用 sudo/root 执行"
|
[ "$(id -u)" = 0 ] || die "请使用 sudo/root 执行"
|
||||||
command -v docker >/dev/null 2>&1 || die "未找到 Docker Engine"
|
command -v docker >/dev/null 2>&1 || die "未找到 Docker Engine"
|
||||||
docker compose version >/dev/null 2>&1 || die "未找到 Docker Compose v2"
|
docker compose version >/dev/null 2>&1 || die "未找到 Docker Compose v2"
|
||||||
command -v git >/dev/null 2>&1 || die "未找到 git"
|
command -v curl >/dev/null 2>&1 || die "未找到 curl"
|
||||||
[ -n "$SERVER_IP" ] || SERVER_IP=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')
|
|
||||||
[ -n "$SERVER_IP" ] || SERVER_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
|
||||||
[ -n "$SERVER_IP" ] || die "无法识别服务器 IP,请设置 OVERCOOKED_SERVER_IP"
|
|
||||||
case "$SERVER_IP" in *[!0-9.]*|*[!0-9]*) die "仅支持 IPv4 地址:$SERVER_IP";; esac
|
|
||||||
[ -n "$AUTH_PUBLIC_KEYS" ] || die "请设置 OVERCOOKED_AUTH_PUBLIC_KEYS(Auth 公钥 JSON)"
|
|
||||||
[ -n "$ACTIVATION_CODE" ] || printf '请输入授权码: '; [ -n "$ACTIVATION_CODE" ] || read -r ACTIVATION_CODE
|
|
||||||
[ -n "$ACTIVATION_CODE" ] || die "授权码不能为空"
|
|
||||||
|
|
||||||
install -d -m 700 "$ROOT"
|
|
||||||
for repo in OverCooked-S Overcooked-E; do
|
|
||||||
if [ ! -d "$ROOT/$repo/.git" ]; then
|
|
||||||
git clone --depth 1 "https://git.leee.xin/OverCooked/$repo.git" "$ROOT/$repo"
|
|
||||||
else
|
|
||||||
git -C "$ROOT/$repo" fetch --depth 1 origin && git -C "$ROOT/$repo" reset --hard FETCH_HEAD
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
install -d -m 700 "$ROOT/config"
|
|
||||||
command -v openssl >/dev/null 2>&1 || die "未找到 openssl"
|
command -v openssl >/dev/null 2>&1 || die "未找到 openssl"
|
||||||
|
command -v tar >/dev/null 2>&1 || die "未找到 tar"
|
||||||
|
command -v mktemp >/dev/null 2>&1 || die "未找到 mktemp"
|
||||||
|
for pair in "OVERCOOKED_REGISTRY:$REGISTRY" "OVERCOOKED_VERSION:$VERSION" "OVERCOOKED_ROOT:$ROOT" "OVERCOOKED_AUTH_URL:$AUTH_URL" "OVERCOOKED_INSTALLER_BASE_URL:$INSTALLER_BASE_URL"; do
|
||||||
|
name=${pair%%:*}; value=${pair#*:}; [ -n "$value" ] || die "$name 不能为空"
|
||||||
|
case "$value" in *' '*|*' '*|*'\r'*|*'\n'*) die "$name 包含非法空白字符" ;; esac
|
||||||
|
done
|
||||||
|
case "$REGISTRY" in *://*|*/|*//*|*=*) die "OVERCOOKED_REGISTRY 必须是镜像 Registry 路径" ;; esac
|
||||||
|
case "$VERSION" in */*|*:*|*[!A-Za-z0-9._-]*) die "OVERCOOKED_VERSION 格式无效" ;; esac
|
||||||
|
case "$ROOT" in /*) ;; *) die "OVERCOOKED_ROOT 必须是绝对路径" ;; esac
|
||||||
|
case "$INSTALLER_BASE_URL" in *://*) ;; *) die "Installer 地址必须是 HTTP(S) URL" ;; esac
|
||||||
|
|
||||||
|
if [ -z "$SERVER_IP" ]; then
|
||||||
|
command -v ip >/dev/null 2>&1 || die "未找到 ip,请设置 OVERCOOKED_SERVER_IP"
|
||||||
|
SERVER_IP=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')
|
||||||
|
[ -n "$SERVER_IP" ] || die "无法识别服务器公网 IPv4,请设置 OVERCOOKED_SERVER_IP"
|
||||||
|
EXPLICIT_SERVER_IP=0
|
||||||
|
else
|
||||||
|
EXPLICIT_SERVER_IP=1
|
||||||
|
fi
|
||||||
|
is_ipv4 "$SERVER_IP" || die "仅支持有效 IPv4 地址:$SERVER_IP"
|
||||||
|
if [ "$EXPLICIT_SERVER_IP" = 0 ] && is_private_ipv4 "$SERVER_IP"; then
|
||||||
|
die "检测到私有地址,请设置 OVERCOOKED_SERVER_IP=公网IPv4"
|
||||||
|
fi
|
||||||
|
[ -n "$AUTH_PUBLIC_KEYS" ] || die "请设置 OVERCOOKED_AUTH_PUBLIC_KEYS(Auth 公钥 JSON)"
|
||||||
|
case "$AUTH_PUBLIC_KEYS" in \{*\}) ;; *) die "OVERCOOKED_AUTH_PUBLIC_KEYS 必须是 JSON 对象" ;; esac
|
||||||
|
case "$AUTH_PUBLIC_KEYS" in *'\n'*|*'\r'*) die "OVERCOOKED_AUTH_PUBLIC_KEYS 不得包含换行" ;; esac
|
||||||
|
if [ -z "$ACTIVATION_CODE" ]; then
|
||||||
|
[ -t 0 ] || die "授权码不能为空,请设置 OVERCOOKED_ACTIVATION_CODE"
|
||||||
|
printf '请输入授权码: ' >&2; IFS= read -r ACTIVATION_CODE
|
||||||
|
fi
|
||||||
|
[ -n "$ACTIVATION_CODE" ] || die "授权码不能为空"
|
||||||
|
if [ -z "$TURNSTILE_SITE_KEY" ] || [ -z "$TURNSTILE_SECRET_KEY" ]; then
|
||||||
|
die "当前 S 生产镜像需要 OVERCOOKED_TURNSTILE_SITE_KEY 和 OVERCOOKED_TURNSTILE_SECRET_KEY;请提供成对配置"
|
||||||
|
fi
|
||||||
|
check_port 18080; check_port 18083; check_port 8096
|
||||||
|
|
||||||
|
install -d -m 700 "$ROOT" "$ROOT/config" "$ROOT/compose" "$ROOT/migrations" "$ROOT/node-images"
|
||||||
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
fetch_asset() {
|
||||||
|
relative=$1; destination=$2; source="$SCRIPT_DIR/$relative"
|
||||||
|
if [ -f "$source" ]; then install -m 600 "$source" "$destination"; return; fi
|
||||||
|
temporary=$(mktemp "$destination.download.XXXXXX")
|
||||||
|
curl -fsSL --retry 3 --connect-timeout 10 --max-time 120 "$INSTALLER_BASE_URL/$relative" -o "$temporary" || die "下载公开 Installer 资产失败:$relative"
|
||||||
|
[ -s "$temporary" ] || die "公开 Installer 资产为空:$relative"
|
||||||
|
install -m 600 "$temporary" "$destination"; rm -f "$temporary"
|
||||||
|
}
|
||||||
|
fetch_asset compose/s.yml "$S_COMPOSE"
|
||||||
|
fetch_asset compose/e.yml "$E_COMPOSE"
|
||||||
|
fetch_asset s-migrate.sh "$ROOT/s-migrate.sh"
|
||||||
|
fetch_asset config/e-server.production.yaml "$ROOT/config/e-server.production.yaml"
|
||||||
|
fetch_asset s-migrations.tar.gz "$ROOT/s-migrations.tar.gz"
|
||||||
|
tar -xzf "$ROOT/s-migrations.tar.gz" -C "$ROOT/migrations"; rm -f "$ROOT/s-migrations.tar.gz"
|
||||||
|
find "$ROOT/migrations" -type f ! -name '*.up.sql' -delete
|
||||||
|
find "$ROOT/migrations" -type f -name '*.up.sql' -exec chmod 600 {} \;
|
||||||
|
|
||||||
secret() { openssl rand -hex 32; }
|
secret() { openssl rand -hex 32; }
|
||||||
s_db=$(secret); s_valkey=$(secret); internal=$(secret); access=$(secret); setup=$(secret)
|
S_POSTGRES_PASSWORD=$(secret); S_VALKEY_PASSWORD=$(secret)
|
||||||
e_db=$(secret); ticket=$(secret); lease=$(secret); storage=$(secret); plugin=$(secret)
|
S_INTERNAL_SHARED_SECRET=$(secret); S_ACCESS_TOKEN_SECRET=$(secret); S_SETUP_TOKEN=$(secret)
|
||||||
cat >"$ROOT/production.env" <<EOF
|
E_POSTGRES_PASSWORD=$(secret); E_PLAYBACK_TICKET_SECRET=$(secret); E_PLAYBACK_LEASE_KEY=$(secret)
|
||||||
|
E_STORAGE_CREDENTIAL_KEY=$(secret); E_PLUGIN_SECRET_KEY=$(secret)
|
||||||
|
INSTANCE_SUFFIX=$(hostname 2>/dev/null | tr -cd '[:alnum:]' | cut -c1-24); [ -n "$INSTANCE_SUFFIX" ] || INSTANCE_SUFFIX=customer
|
||||||
|
|
||||||
|
cat > "$ENV_FILE" <<EOF
|
||||||
|
OVERCOOKED_ROOT=$ROOT
|
||||||
S_CONTROL_IMAGE=$REGISTRY/overcooked-s-control:$VERSION
|
S_CONTROL_IMAGE=$REGISTRY/overcooked-s-control:$VERSION
|
||||||
S_WEBUI_IMAGE=$REGISTRY/overcooked-s-webui:$VERSION
|
S_WEBUI_IMAGE=$REGISTRY/overcooked-s-webui:$VERSION
|
||||||
S_POSTGRES_PASSWORD=$s_db
|
S_POSTGRES_PASSWORD=$S_POSTGRES_PASSWORD
|
||||||
S_VALKEY_PASSWORD=$s_valkey
|
S_VALKEY_PASSWORD=$S_VALKEY_PASSWORD
|
||||||
S_INTERNAL_KEY_ID=customer-s
|
S_INTERNAL_KEY_ID=customer-s
|
||||||
S_INTERNAL_SHARED_SECRET=$internal
|
S_INTERNAL_SHARED_SECRET=$S_INTERNAL_SHARED_SECRET
|
||||||
S_ACCESS_TOKEN_SECRET=$access
|
S_ACCESS_TOKEN_SECRET=$S_ACCESS_TOKEN_SECRET
|
||||||
S_SETUP_TOKEN=$setup
|
S_SETUP_TOKEN=$S_SETUP_TOKEN
|
||||||
S_E_BASE_URL=http://e-server:8096
|
S_E_BASE_URL=http://e-server:8096
|
||||||
S_E_PUBLIC_URL=http://$SERVER_IP:8096
|
S_E_PUBLIC_URL=http://$SERVER_IP:8096
|
||||||
S_E_KEY_ID=customer-e
|
S_E_KEY_ID=customer-e
|
||||||
S_E_INTERNAL_SECRET=$internal
|
S_E_INTERNAL_SECRET=$S_INTERNAL_SHARED_SECRET
|
||||||
S_E_ALLOW_INSECURE_HTTP=true
|
S_E_ALLOW_INSECURE_HTTP=true
|
||||||
S_AUTH_BASE_URL=$AUTH_URL
|
S_AUTH_BASE_URL=$AUTH_URL
|
||||||
S_AUTH_ALLOW_INSECURE_HTTP=$(case "$AUTH_URL" in http://*) echo true;; *) echo false;; esac)
|
S_AUTH_ALLOW_INSECURE_HTTP=$(case "$AUTH_URL" in http://*) echo true ;; *) echo false ;; esac)
|
||||||
S_AUTH_PUBLIC_KEYS=$AUTH_PUBLIC_KEYS
|
S_AUTH_PUBLIC_KEYS=$AUTH_PUBLIC_KEYS
|
||||||
S_INSTANCE_ID=s-$(hostname | tr -cd '[:alnum:]' | cut -c1-24)
|
S_INSTANCE_ID=s-$INSTANCE_SUFFIX
|
||||||
|
S_PLATFORM=linux-amd64
|
||||||
|
S_VERSION=$VERSION
|
||||||
S_PUBLIC_HOST=$SERVER_IP
|
S_PUBLIC_HOST=$SERVER_IP
|
||||||
S_PORTAL_SERVER_URL=http://$SERVER_IP:18080
|
S_PORTAL_SERVER_URL=http://$SERVER_IP:18080
|
||||||
S_PORTAL_WEB_CLIENT_URL=http://$SERVER_IP:18083/login
|
S_PORTAL_WEB_CLIENT_URL=http://$SERVER_IP:18083/login
|
||||||
S_API_PORT_MAPPING=0.0.0.0:18080:8080
|
S_API_PORT_MAPPING=0.0.0.0:18080:8080
|
||||||
S_WEB_PORT_MAPPING=0.0.0.0:18083:3000
|
S_WEB_PORT_MAPPING=0.0.0.0:18083:3000
|
||||||
|
S_TURNSTILE_SITE_KEY=$TURNSTILE_SITE_KEY
|
||||||
|
S_TURNSTILE_SECRET_KEY=$TURNSTILE_SECRET_KEY
|
||||||
PRODUCT_E_IMAGE=$REGISTRY/overcooked-e:$VERSION
|
PRODUCT_E_IMAGE=$REGISTRY/overcooked-e:$VERSION
|
||||||
PRODUCT_E_INSTANCE_ID=e-$(hostname | tr -cd '[:alnum:]' | cut -c1-24)
|
PRODUCT_E_INSTANCE_ID=e-$INSTANCE_SUFFIX
|
||||||
PRODUCT_E_DATABASE_URL=postgres://product_e:$e_db@postgres:5432/product_e?sslmode=disable
|
PRODUCT_E_DATABASE_URL=postgres://product_e:$E_POSTGRES_PASSWORD@postgres:5432/product_e?sslmode=disable
|
||||||
PRODUCT_E_POSTGRES_PASSWORD=$e_db
|
PRODUCT_E_POSTGRES_PASSWORD=$E_POSTGRES_PASSWORD
|
||||||
PRODUCT_E_VALKEY_PASSWORD=$s_valkey
|
PRODUCT_E_VALKEY_PASSWORD=$S_VALKEY_PASSWORD
|
||||||
PRODUCT_E_CONTROL_PLANE_URL=http://s-control:8080
|
PRODUCT_E_CONTROL_PLANE_URL=http://s-control:8080
|
||||||
PRODUCT_E_CONTROL_PLANE_KEY_ID=customer-s
|
PRODUCT_E_CONTROL_PLANE_KEY_ID=customer-s
|
||||||
PRODUCT_E_CONTROL_PLANE_SHARED_SECRET=$internal
|
PRODUCT_E_CONTROL_PLANE_SHARED_SECRET=$S_INTERNAL_SHARED_SECRET
|
||||||
PRODUCT_E_LICENSE_ENABLED=true
|
PRODUCT_E_LICENSE_ENABLED=true
|
||||||
PRODUCT_E_AUTH_URL=$AUTH_URL
|
PRODUCT_E_AUTH_URL=$AUTH_URL
|
||||||
|
PRODUCT_E_AUTH_ALLOW_INSECURE_HTTP=$(case "$AUTH_URL" in http://*) echo true ;; *) echo false ;; esac)
|
||||||
PRODUCT_E_AUTH_ACTIVATION_CODE=$ACTIVATION_CODE
|
PRODUCT_E_AUTH_ACTIVATION_CODE=$ACTIVATION_CODE
|
||||||
PRODUCT_E_AUTH_PUBLIC_KEYS=$AUTH_PUBLIC_KEYS
|
PRODUCT_E_AUTH_PUBLIC_KEYS=$AUTH_PUBLIC_KEYS
|
||||||
PRODUCT_E_PLAYBACK_TICKET_KEY_ID=customer-playback
|
PRODUCT_E_PLAYBACK_TICKET_KEY_ID=customer-playback
|
||||||
PRODUCT_E_PLAYBACK_TICKET_SECRET=$ticket
|
PRODUCT_E_PLAYBACK_TICKET_SECRET=$E_PLAYBACK_TICKET_SECRET
|
||||||
PRODUCT_E_PLAYBACK_LEASE_KEY=$lease
|
PRODUCT_E_PLAYBACK_LEASE_KEY=$E_PLAYBACK_LEASE_KEY
|
||||||
PRODUCT_E_STORAGE_CREDENTIAL_KEY_ID=customer-storage
|
PRODUCT_E_STORAGE_CREDENTIAL_KEY_ID=customer-storage
|
||||||
PRODUCT_E_STORAGE_CREDENTIAL_KEY=$storage
|
PRODUCT_E_STORAGE_CREDENTIAL_KEY=$E_STORAGE_CREDENTIAL_KEY
|
||||||
PRODUCT_E_PLUGIN_SECRET_KEY_ID=customer-plugin
|
PRODUCT_E_PLUGIN_SECRET_KEY_ID=customer-plugin
|
||||||
PRODUCT_E_PLUGIN_SECRET_KEY=$plugin
|
PRODUCT_E_PLUGIN_SECRET_KEY=$E_PLUGIN_SECRET_KEY
|
||||||
PRODUCT_E_PLUGIN_TRUSTED_PUBLISHERS={}
|
PRODUCT_E_PLUGIN_TRUSTED_PUBLISHERS={}
|
||||||
EOF
|
EOF
|
||||||
chmod 600 "$ROOT/production.env"
|
chmod 600 "$ENV_FILE"
|
||||||
|
|
||||||
cat >"$ROOT/config/e-server.production.yaml" <<EOF
|
cat > "$ROOT/config/e-server.production.yaml" <<EOF
|
||||||
service:
|
service:
|
||||||
|
name: overcooked-e
|
||||||
|
instance_id: e-$INSTANCE_SUFFIX
|
||||||
environment: production
|
environment: production
|
||||||
|
emby:
|
||||||
|
server_name: OverCooked
|
||||||
|
server_id: 10000000-0000-4000-8000-000000000001
|
||||||
|
compatibility_version: 4.8.0.80
|
||||||
|
control_plane:
|
||||||
|
base_url: https://s-control.invalid
|
||||||
|
key_id: customer-s
|
||||||
|
shared_secret: replace-by-environment
|
||||||
|
timeout: 10s
|
||||||
server:
|
server:
|
||||||
address: 0.0.0.0:8096
|
address: 0.0.0.0:8096
|
||||||
database:
|
database:
|
||||||
url: postgres://product_e:$(grep '^PRODUCT_E_POSTGRES_PASSWORD=' "$ROOT/production.env" | cut -d= -f2)@postgres:5432/product_e?sslmode=disable
|
url: postgres://product_e:replace-by-environment@postgres:5432/product_e?sslmode=disable
|
||||||
valkey:
|
valkey:
|
||||||
address: valkey:6379
|
address: valkey:6379
|
||||||
control_plane:
|
observability:
|
||||||
base_url: http://s-control:8080
|
log_level: info
|
||||||
|
trace_sample_ratio: 0.05
|
||||||
playback:
|
playback:
|
||||||
node_id: e-$(hostname | tr -cd '[:alnum:]' | cut -c1-24)
|
node_id: 10000000-0000-4000-8000-000000000001
|
||||||
|
plugins:
|
||||||
|
enabled: true
|
||||||
|
root: /data/plugins
|
||||||
|
sdk_socket: unix:///data/plugins/runtime/core.sock
|
||||||
|
core_version: 0.1.0
|
||||||
|
unsigned_policy: require-signature
|
||||||
|
trusted_publishers: {}
|
||||||
|
secret_key_id: customer-plugin
|
||||||
|
previous_secret_keys: {}
|
||||||
|
network_allowed_domains: []
|
||||||
|
network_allowed_cidrs: []
|
||||||
EOF
|
EOF
|
||||||
chmod 600 "$ROOT/config/e-server.production.yaml"
|
chmod 600 "$ROOT/config/e-server.production.yaml"
|
||||||
cp "$ROOT/config/e-server.production.yaml" "$ROOT/Overcooked-E/deploy/compose/config/e-server.production.yaml"
|
|
||||||
cat >"$ROOT/config/e-public.override.yml" <<'EOF'
|
|
||||||
services:
|
|
||||||
e-server:
|
|
||||||
ports:
|
|
||||||
- "0.0.0.0:8096:8096"
|
|
||||||
EOF
|
|
||||||
docker network inspect overcooked-control >/dev/null 2>&1 || docker network create overcooked-control >/dev/null
|
docker network inspect overcooked-control >/dev/null 2>&1 || docker network create overcooked-control >/dev/null
|
||||||
docker compose -p overcooked-e --env-file "$ROOT/production.env" -f "$ROOT/Overcooked-E/deploy/compose/production.yml" run --rm e-server migrate
|
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" config >/dev/null
|
||||||
docker compose -p overcooked-e --env-file "$ROOT/production.env" -f "$ROOT/Overcooked-E/deploy/compose/production.yml" -f "$ROOT/Overcooked-E/deploy/compose/control-plane-network.yml" -f "$ROOT/config/e-public.override.yml" up -d --no-build
|
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" config >/dev/null
|
||||||
docker compose -p overcooked-s --env-file "$ROOT/production.env" -f "$ROOT/OverCooked-S/deploy/s/docker-compose.production.yml" up -d --no-build
|
|
||||||
echo "配置已写入 $ROOT/production.env"
|
for image in "$REGISTRY/overcooked-e:$VERSION" "$REGISTRY/overcooked-s-control:$VERSION" "$REGISTRY/overcooked-s-webui:$VERSION" "$REGISTRY/overcooked-media-node:$VERSION"; do
|
||||||
|
docker pull "$image"
|
||||||
|
docker image inspect "$image" >/dev/null 2>&1 || die "镜像校验失败:$image"
|
||||||
|
done
|
||||||
|
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" up -d postgres valkey
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" run --rm --no-deps e-server migrate -config /etc/overcooked-e/config.yaml
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" up -d postgres valkey
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" run --rm --no-deps s-migrate
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" up -d e-server
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" up -d s-control webui
|
||||||
|
|
||||||
|
wait_for() {
|
||||||
|
label=$1; url=$2; attempt=1
|
||||||
|
while [ "$attempt" -le 60 ]; do
|
||||||
|
curl -fsS --max-time 5 "$url" >/dev/null 2>&1 && return 0
|
||||||
|
sleep 2; attempt=$((attempt + 1))
|
||||||
|
done
|
||||||
|
die "$label 健康检查失败:$url"
|
||||||
|
}
|
||||||
|
wait_for "S live" http://127.0.0.1:18080/health/live
|
||||||
|
wait_for "S ready" http://127.0.0.1:18080/health/ready
|
||||||
|
wait_for "E live" http://127.0.0.1:8096/health/live
|
||||||
|
wait_for "WebUI" http://127.0.0.1:18083/
|
||||||
|
wait_for "Setup" http://127.0.0.1:18083/setup
|
||||||
|
|
||||||
|
echo "OverCooked 客户部署已通过容器健康检查。"
|
||||||
echo "WebUI: http://$SERVER_IP:18083/"
|
echo "WebUI: http://$SERVER_IP:18083/"
|
||||||
echo "API: http://$SERVER_IP:18080/"
|
echo "API: http://$SERVER_IP:18080/"
|
||||||
echo "E: http://$SERVER_IP:8096/"
|
echo "E: http://$SERVER_IP:8096/"
|
||||||
echo "首次启动后打开 http://$SERVER_IP:18083/setup 输入授权码和页面显示的 Setup Token。"
|
echo "首次初始化: http://$SERVER_IP:18083/setup"
|
||||||
echo "镜像来源:$REGISTRY(不包含 Auth 镜像)"
|
echo "Setup Token 已保存于 $ENV_FILE(不会在终端显示,请限制文件访问权限)。"
|
||||||
|
echo "镜像来源:$REGISTRY(仅公开 E、S Control、S WebUI、Media Node;不包含 Auth 镜像)"
|
||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
: "${PGHOST:?required}"
|
||||||
|
: "${PGUSER:?required}"
|
||||||
|
: "${PGDATABASE:?required}"
|
||||||
|
: "${PGPASSWORD:?required}"
|
||||||
|
|
||||||
|
psql -v ON_ERROR_STOP=1 -c '
|
||||||
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||||
|
version text PRIMARY KEY,
|
||||||
|
applied_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
)
|
||||||
|
'
|
||||||
|
|
||||||
|
for migration in /migrations/*.up.sql; do
|
||||||
|
version=$(basename "$migration" .up.sql)
|
||||||
|
case "$version" in
|
||||||
|
*[!0-9A-Za-z_-]*) echo "invalid migration name: $version" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
applied=$(psql -v ON_ERROR_STOP=1 -Atc "SELECT count(*) FROM schema_migrations WHERE version='$version'")
|
||||||
|
[ "$applied" = 1 ] && continue
|
||||||
|
{
|
||||||
|
printf 'BEGIN;\nSELECT pg_advisory_xact_lock(731006);\n'
|
||||||
|
cat "$migration"
|
||||||
|
printf "\nINSERT INTO schema_migrations(version) VALUES ('%s');\nCOMMIT;\n" "$version"
|
||||||
|
} | psql -v ON_ERROR_STOP=1
|
||||||
|
done
|
||||||
Binary file not shown.
@@ -0,0 +1,81 @@
|
|||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
|
||||||
|
CREATE TABLE users (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
username text NOT NULL,
|
||||||
|
username_normalized text NOT NULL UNIQUE,
|
||||||
|
email text NOT NULL,
|
||||||
|
email_normalized text NOT NULL UNIQUE,
|
||||||
|
role text NOT NULL DEFAULT 'user' CHECK (role IN ('user','admin')),
|
||||||
|
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','disabled','deleted')),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
deleted_at timestamptz
|
||||||
|
);
|
||||||
|
CREATE TABLE credentials (
|
||||||
|
user_id uuid PRIMARY KEY REFERENCES users(id),
|
||||||
|
password_hash text NOT NULL,
|
||||||
|
password_changed_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE TABLE memberships (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id),
|
||||||
|
status text NOT NULL CHECK (status IN ('active','expired','revoked')),
|
||||||
|
starts_at timestamptz NOT NULL, ends_at timestamptz NOT NULL,
|
||||||
|
source text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CHECK (ends_at > starts_at)
|
||||||
|
);
|
||||||
|
CREATE INDEX memberships_active_user_idx ON memberships(user_id, ends_at DESC) WHERE status = 'active';
|
||||||
|
CREATE TABLE refresh_token_families (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id),
|
||||||
|
revoked_at timestamptz, created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE TABLE refresh_tokens (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), family_id uuid NOT NULL REFERENCES refresh_token_families(id),
|
||||||
|
token_hash bytea NOT NULL UNIQUE, expires_at timestamptz NOT NULL, consumed_at timestamptz,
|
||||||
|
replaced_by uuid, created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX refresh_tokens_family_idx ON refresh_tokens(family_id);
|
||||||
|
CREATE TABLE card_batches (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, card_type text NOT NULL CHECK (card_type IN ('registration','renewal')),
|
||||||
|
duration_days integer NOT NULL CHECK (duration_days > 0), created_by uuid REFERENCES users(id),
|
||||||
|
revoked_at timestamptz, created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE TABLE cards (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), batch_id uuid NOT NULL REFERENCES card_batches(id),
|
||||||
|
code_hash bytea NOT NULL UNIQUE, remaining_uses integer NOT NULL DEFAULT 1 CHECK (remaining_uses >= 0),
|
||||||
|
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','revoked','exhausted')),
|
||||||
|
expires_at timestamptz, created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE TABLE card_usages (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), card_id uuid NOT NULL REFERENCES cards(id), user_id uuid NOT NULL REFERENCES users(id),
|
||||||
|
used_at timestamptz NOT NULL DEFAULT now(), UNIQUE(card_id, user_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX card_usages_user_idx ON card_usages(user_id, used_at DESC);
|
||||||
|
CREATE TABLE audit_events (
|
||||||
|
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, actor_user_id uuid REFERENCES users(id), action text NOT NULL,
|
||||||
|
target_type text NOT NULL, target_id text NOT NULL, detail jsonb NOT NULL DEFAULT '{}'::jsonb, created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX audit_events_target_idx ON audit_events(target_type, target_id, created_at DESC);
|
||||||
|
CREATE TABLE policy_rules (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), subject_type text NOT NULL CHECK (subject_type IN ('global','membership','user')),
|
||||||
|
subject_id uuid, priority integer NOT NULL DEFAULT 0, enabled boolean NOT NULL DEFAULT true,
|
||||||
|
max_devices integer, max_online integer, max_playbacks integer, max_device_playbacks integer, max_bitrate_kbps integer,
|
||||||
|
downloads_allowed boolean, proxy_allowed boolean, transcode_allowed boolean, library_id text,
|
||||||
|
allowed_cidrs text[] NOT NULL DEFAULT '{}', countries text[] NOT NULL DEFAULT '{}', regions text[] NOT NULL DEFAULT '{}',
|
||||||
|
asns bigint[] NOT NULL DEFAULT '{}', carriers text[] NOT NULL DEFAULT '{}', route_id text, created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CHECK ((subject_type = 'global' AND subject_id IS NULL) OR (subject_type <> 'global' AND subject_id IS NOT NULL))
|
||||||
|
);
|
||||||
|
CREATE INDEX policy_rules_subject_idx ON policy_rules(subject_type, subject_id, priority DESC) WHERE enabled;
|
||||||
|
CREATE TABLE devices (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id), fingerprint_hash bytea NOT NULL,
|
||||||
|
first_seen_at timestamptz NOT NULL DEFAULT now(), last_seen_at timestamptz NOT NULL DEFAULT now(), revoked_at timestamptz,
|
||||||
|
UNIQUE(user_id, fingerprint_hash)
|
||||||
|
);
|
||||||
|
CREATE TABLE playback_sessions (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id), device_id uuid REFERENCES devices(id),
|
||||||
|
e_session_id text NOT NULL UNIQUE, library_id text NOT NULL, route_id text, state text NOT NULL CHECK (state IN ('reserved','active','released','expired')),
|
||||||
|
lease_token_hash bytea NOT NULL UNIQUE, expires_at timestamptz NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), released_at timestamptz
|
||||||
|
);
|
||||||
|
CREATE INDEX playback_sessions_active_user_idx ON playback_sessions(user_id, expires_at) WHERE state IN ('reserved','active');
|
||||||
|
CREATE INDEX playback_sessions_active_device_idx ON playback_sessions(device_id, expires_at) WHERE state IN ('reserved','active');
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
CREATE TABLE e_command_requests (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), actor_user_id uuid NOT NULL REFERENCES users(id),
|
||||||
|
command text NOT NULL, target_id text NOT NULL, idempotency_key text NOT NULL UNIQUE,
|
||||||
|
state text NOT NULL CHECK (state IN ('accepted','completed','failed')), created_at timestamptz NOT NULL DEFAULT now(), completed_at timestamptz
|
||||||
|
);
|
||||||
|
CREATE INDEX e_command_requests_actor_idx ON e_command_requests(actor_user_id, created_at DESC);
|
||||||
|
CREATE TABLE e_event_inbox (
|
||||||
|
event_id uuid PRIMARY KEY, event_type text NOT NULL, occurred_at timestamptz NOT NULL,
|
||||||
|
payload jsonb NOT NULL, received_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE invite_codes (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), creator_user_id uuid NOT NULL REFERENCES users(id),
|
||||||
|
code_hash bytea NOT NULL UNIQUE, max_uses integer NOT NULL DEFAULT 1 CHECK (max_uses > 0), used_count integer NOT NULL DEFAULT 0 CHECK (used_count >= 0),
|
||||||
|
expires_at timestamptz, revoked_at timestamptz, created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE TABLE invite_relations (
|
||||||
|
parent_user_id uuid NOT NULL REFERENCES users(id), child_user_id uuid NOT NULL REFERENCES users(id), invite_code_id uuid NOT NULL REFERENCES invite_codes(id),
|
||||||
|
risk_state text NOT NULL DEFAULT 'normal' CHECK (risk_state IN ('normal','review','blocked')), detached_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY(parent_user_id, child_user_id), CHECK (parent_user_id <> child_user_id)
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX invite_relations_child_active_idx ON invite_relations(child_user_id) WHERE detached_at IS NULL;
|
||||||
|
CREATE TABLE media_requests (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id), query_text text NOT NULL,
|
||||||
|
e_media_id uuid, availability text NOT NULL CHECK (availability IN ('unknown','available','unavailable')), status text NOT NULL DEFAULT 'open' CHECK (status IN ('open','planned','fulfilled','rejected','cancelled')),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX media_requests_status_idx ON media_requests(status, created_at DESC);
|
||||||
|
CREATE TABLE ticket_types (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL UNIQUE, enabled boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now());
|
||||||
|
CREATE TABLE tickets (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), owner_user_id uuid NOT NULL REFERENCES users(id), type_id uuid REFERENCES ticket_types(id), subject text NOT NULL,
|
||||||
|
status text NOT NULL DEFAULT 'open' CHECK (status IN ('open','pending','resolved','closed')), priority text NOT NULL DEFAULT 'normal' CHECK (priority IN ('low','normal','high')),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), closed_at timestamptz
|
||||||
|
);
|
||||||
|
CREATE INDEX tickets_owner_idx ON tickets(owner_user_id, updated_at DESC);
|
||||||
|
CREATE INDEX tickets_status_idx ON tickets(status, updated_at DESC);
|
||||||
|
CREATE TABLE ticket_replies (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), ticket_id uuid NOT NULL REFERENCES tickets(id), author_user_id uuid NOT NULL REFERENCES users(id), body text NOT NULL, created_at timestamptz NOT NULL DEFAULT now());
|
||||||
|
CREATE INDEX ticket_replies_ticket_idx ON ticket_replies(ticket_id, created_at);
|
||||||
|
CREATE TABLE ticket_assignments (ticket_id uuid PRIMARY KEY REFERENCES tickets(id), admin_user_id uuid NOT NULL REFERENCES users(id), assigned_at timestamptz NOT NULL DEFAULT now());
|
||||||
|
CREATE TABLE telegram_accounts (user_id uuid PRIMARY KEY REFERENCES users(id), telegram_user_id bigint NOT NULL UNIQUE, username text, bound_at timestamptz NOT NULL DEFAULT now(), disabled_at timestamptz);
|
||||||
|
CREATE TABLE telegram_bind_codes (code_hash bytea PRIMARY KEY, user_id uuid NOT NULL REFERENCES users(id), expires_at timestamptz NOT NULL, consumed_at timestamptz, created_at timestamptz NOT NULL DEFAULT now());
|
||||||
|
|
||||||
|
CREATE TABLE license_products (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, edition text NOT NULL, max_s_instances integer NOT NULL CHECK (max_s_instances > 0),
|
||||||
|
max_e_nodes integer NOT NULL CHECK (max_e_nodes >= 0), feature_flags text[] NOT NULL DEFAULT '{}', valid_days integer NOT NULL CHECK (valid_days > 0), offline_days integer NOT NULL DEFAULT 60 CHECK (offline_days BETWEEN 0 AND 60), created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE TABLE license_activations (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), product_id uuid NOT NULL REFERENCES license_products(id), activation_hash bytea NOT NULL UNIQUE,
|
||||||
|
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','revoked','exhausted')), max_instances integer NOT NULL DEFAULT 1 CHECK (max_instances > 0), created_at timestamptz NOT NULL DEFAULT now(), revoked_at timestamptz
|
||||||
|
);
|
||||||
|
CREATE TABLE license_instances (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), activation_id uuid NOT NULL REFERENCES license_activations(id), instance_id uuid NOT NULL,
|
||||||
|
fingerprint_hash bytea NOT NULL, signed_license text NOT NULL, expires_at timestamptz NOT NULL, offline_until timestamptz NOT NULL, last_seen_at timestamptz NOT NULL DEFAULT now(), unbound_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(activation_id, instance_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX license_instances_active_idx ON license_instances(activation_id) WHERE unbound_at IS NULL;
|
||||||
|
|
||||||
|
CREATE TABLE outbox_events (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), topic text NOT NULL, aggregate_type text NOT NULL, aggregate_id text NOT NULL,
|
||||||
|
payload jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), published_at timestamptz, attempts integer NOT NULL DEFAULT 0, last_error text
|
||||||
|
, locked_by text, locked_until timestamptz
|
||||||
|
);
|
||||||
|
CREATE INDEX outbox_events_pending_idx ON outbox_events(created_at) WHERE published_at IS NULL;
|
||||||
|
CREATE TABLE runtime_config (key text PRIMARY KEY, value jsonb NOT NULL, updated_at timestamptz NOT NULL DEFAULT now(), updated_by uuid REFERENCES users(id));
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN ban_reason text,
|
||||||
|
ADD COLUMN banned_until timestamptz;
|
||||||
|
|
||||||
|
CREATE TABLE user_permissions (
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
permission text NOT NULL CHECK (permission IN (
|
||||||
|
'users.read',
|
||||||
|
'users.write',
|
||||||
|
'cards.manage',
|
||||||
|
'operations.manage',
|
||||||
|
'e.manage',
|
||||||
|
'audit.read',
|
||||||
|
'licenses.manage'
|
||||||
|
)),
|
||||||
|
granted_by uuid REFERENCES users(id),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (user_id, permission)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX user_permissions_permission_idx ON user_permissions(permission, user_id);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN auth_version bigint NOT NULL DEFAULT 1 CHECK (auth_version > 0);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE internal_auth_nonces (
|
||||||
|
nonce_hash bytea PRIMARY KEY,
|
||||||
|
expires_at timestamptz NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX internal_auth_nonces_expiry_idx ON internal_auth_nonces(expires_at);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
CREATE TABLE playback_decisions (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id),
|
||||||
|
media_id uuid NOT NULL,
|
||||||
|
library_id uuid NOT NULL,
|
||||||
|
source_id uuid NOT NULL,
|
||||||
|
device_id text NOT NULL,
|
||||||
|
route_id text,
|
||||||
|
allowed boolean NOT NULL,
|
||||||
|
deny_code text,
|
||||||
|
max_bitrate bigint NOT NULL DEFAULT 0 CHECK (max_bitrate >= 0),
|
||||||
|
allow_download boolean NOT NULL DEFAULT false,
|
||||||
|
max_playbacks integer NOT NULL DEFAULT 0 CHECK (max_playbacks >= 0),
|
||||||
|
max_device_playbacks integer NOT NULL DEFAULT 0 CHECK (max_device_playbacks >= 0),
|
||||||
|
policy_revision bigint NOT NULL CHECK (policy_revision >= 0),
|
||||||
|
expires_at timestamptz NOT NULL,
|
||||||
|
consumed_at timestamptz,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX playback_decisions_expiry_idx ON playback_decisions(expires_at);
|
||||||
|
CREATE INDEX playback_decisions_user_idx ON playback_decisions(user_id, created_at DESC);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
CREATE TABLE commercial_license_cache (
|
||||||
|
instance_id text PRIMARY KEY,
|
||||||
|
license_id text NOT NULL,
|
||||||
|
signed_license text NOT NULL,
|
||||||
|
edition text NOT NULL,
|
||||||
|
features text[] NOT NULL DEFAULT '{}',
|
||||||
|
max_s_instances integer NOT NULL CHECK (max_s_instances > 0),
|
||||||
|
max_e_nodes integer NOT NULL CHECK (max_e_nodes > 0),
|
||||||
|
expires_at timestamptz NOT NULL,
|
||||||
|
offline_until timestamptz NOT NULL,
|
||||||
|
status text NOT NULL CHECK (status IN ('active','revoked','expired')),
|
||||||
|
last_verified_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX commercial_license_cache_status_expiry_idx ON commercial_license_cache(status,expires_at);
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
CREATE TABLE gateway_settings (
|
||||||
|
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
||||||
|
public_origin text NOT NULL DEFAULT '',
|
||||||
|
trusted_proxy_cidrs text[] NOT NULL DEFAULT '{}',
|
||||||
|
active_revision bigint,
|
||||||
|
updated_by uuid REFERENCES users(id),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
INSERT INTO gateway_settings(singleton) VALUES (true);
|
||||||
|
|
||||||
|
CREATE TABLE gateway_service_pools (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
code text NOT NULL UNIQUE CHECK (code ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$'),
|
||||||
|
name text NOT NULL CHECK (length(btrim(name)) BETWEEN 1 AND 128),
|
||||||
|
enabled boolean NOT NULL DEFAULT true,
|
||||||
|
is_default boolean NOT NULL DEFAULT false,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX gateway_service_pools_one_default_idx ON gateway_service_pools(is_default) WHERE is_default;
|
||||||
|
INSERT INTO gateway_service_pools(code,name,is_default) VALUES ('default','Default',true);
|
||||||
|
|
||||||
|
CREATE TABLE gateway_nodes (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
node_id text NOT NULL UNIQUE CHECK (node_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$'),
|
||||||
|
name text NOT NULL CHECK (length(btrim(name)) BETWEEN 1 AND 128),
|
||||||
|
gateway_url text NOT NULL CHECK (length(gateway_url) BETWEEN 8 AND 2048),
|
||||||
|
pool_id uuid NOT NULL REFERENCES gateway_service_pools(id),
|
||||||
|
country text NOT NULL DEFAULT '',
|
||||||
|
region text NOT NULL DEFAULT '',
|
||||||
|
city text NOT NULL DEFAULT '',
|
||||||
|
carrier text NOT NULL DEFAULT '',
|
||||||
|
asn bigint NOT NULL DEFAULT 0 CHECK (asn BETWEEN 0 AND 4294967295),
|
||||||
|
weight integer NOT NULL DEFAULT 100 CHECK (weight BETWEEN 1 AND 10000),
|
||||||
|
max_connections bigint NOT NULL DEFAULT 0 CHECK (max_connections >= 0),
|
||||||
|
max_playback_sessions bigint NOT NULL DEFAULT 0 CHECK (max_playback_sessions >= 0),
|
||||||
|
max_transcode_sessions bigint NOT NULL DEFAULT 0 CHECK (max_transcode_sessions >= 0),
|
||||||
|
bandwidth_capacity_mbps bigint NOT NULL DEFAULT 0 CHECK (bandwidth_capacity_mbps >= 0),
|
||||||
|
library_ids text[] NOT NULL DEFAULT '{}',
|
||||||
|
storage_ids text[] NOT NULL DEFAULT '{}',
|
||||||
|
enabled boolean NOT NULL DEFAULT true,
|
||||||
|
allow_login boolean NOT NULL DEFAULT true,
|
||||||
|
allow_new_playback boolean NOT NULL DEFAULT true,
|
||||||
|
maintenance boolean NOT NULL DEFAULT false,
|
||||||
|
draining boolean NOT NULL DEFAULT false,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX gateway_nodes_pool_idx ON gateway_nodes(pool_id,enabled,node_id);
|
||||||
|
CREATE INDEX gateway_nodes_route_idx ON gateway_nodes(enabled,maintenance,draining,country,region,carrier,asn);
|
||||||
|
|
||||||
|
CREATE TABLE gateway_user_routes (
|
||||||
|
user_id uuid PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
forced_node_id text REFERENCES gateway_nodes(node_id) ON UPDATE CASCADE,
|
||||||
|
forced_pool_id uuid REFERENCES gateway_service_pools(id),
|
||||||
|
preferred_country text NOT NULL DEFAULT '',
|
||||||
|
preferred_region text NOT NULL DEFAULT '',
|
||||||
|
updated_by uuid REFERENCES users(id),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE gateway_config_versions (
|
||||||
|
revision bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
snapshot jsonb NOT NULL CHECK (jsonb_typeof(snapshot)='object'),
|
||||||
|
source_revision bigint REFERENCES gateway_config_versions(revision),
|
||||||
|
created_by uuid NOT NULL REFERENCES users(id),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX gateway_config_versions_created_idx ON gateway_config_versions(created_at DESC);
|
||||||
|
ALTER TABLE gateway_settings ADD CONSTRAINT gateway_settings_active_revision_fk FOREIGN KEY(active_revision) REFERENCES gateway_config_versions(revision);
|
||||||
|
|
||||||
|
CREATE TABLE gateway_route_events (
|
||||||
|
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
request_id text NOT NULL,
|
||||||
|
node_id text,
|
||||||
|
outcome text NOT NULL CHECK (outcome IN ('routed','rejected','upstream_failed')),
|
||||||
|
reason text NOT NULL DEFAULT '',
|
||||||
|
country text NOT NULL DEFAULT '',
|
||||||
|
region text NOT NULL DEFAULT '',
|
||||||
|
carrier text NOT NULL DEFAULT '',
|
||||||
|
asn bigint NOT NULL DEFAULT 0,
|
||||||
|
latency_ms integer NOT NULL DEFAULT 0 CHECK (latency_ms >= 0),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX gateway_route_events_recent_idx ON gateway_route_events(created_at DESC);
|
||||||
|
CREATE INDEX gateway_route_events_node_idx ON gateway_route_events(node_id,created_at DESC);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
ALTER TABLE gateway_settings
|
||||||
|
ADD COLUMN routing_weights jsonb NOT NULL DEFAULT '{"country":1000,"region":100,"city":20,"carrier":10,"asn":10}'::jsonb,
|
||||||
|
ADD COLUMN recovery_ramp_seconds integer NOT NULL DEFAULT 300 CHECK (recovery_ramp_seconds BETWEEN 0 AND 3600),
|
||||||
|
ADD CONSTRAINT gateway_settings_routing_weights_object CHECK (jsonb_typeof(routing_weights) = 'object');
|
||||||
|
|
||||||
|
ALTER TABLE gateway_route_events
|
||||||
|
ADD COLUMN username text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN device_hash text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN resource_id text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN path_class text NOT NULL DEFAULT 'other'
|
||||||
|
CHECK (path_class IN ('login','browse','image','playback','transcode','websocket','other')),
|
||||||
|
ADD COLUMN selection_source text NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
CREATE INDEX gateway_route_events_outcome_created_idx
|
||||||
|
ON gateway_route_events(outcome,created_at DESC);
|
||||||
|
CREATE INDEX gateway_route_events_location_created_idx
|
||||||
|
ON gateway_route_events(country,region,carrier,created_at DESC);
|
||||||
|
CREATE INDEX gateway_route_events_username_created_idx
|
||||||
|
ON gateway_route_events(username,created_at DESC) WHERE username <> '';
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
ALTER TABLE commercial_license_cache
|
||||||
|
ADD COLUMN customer_id text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN product text NOT NULL DEFAULT 'overcooked',
|
||||||
|
ADD COLUMN max_users integer NOT NULL DEFAULT 0 CHECK (max_users >= 0),
|
||||||
|
ADD COLUMN max_service_pools integer NOT NULL DEFAULT 0 CHECK (max_service_pools >= 0),
|
||||||
|
ADD COLUMN not_before timestamptz NOT NULL DEFAULT now(),
|
||||||
|
ADD COLUMN maintenance_until timestamptz;
|
||||||
|
|
||||||
|
CREATE INDEX commercial_license_cache_product_status_idx
|
||||||
|
ON commercial_license_cache(product,status,offline_until);
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
CREATE TABLE e_address_settings (
|
||||||
|
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
||||||
|
trusted_proxy_cidrs text[] NOT NULL DEFAULT '{}',
|
||||||
|
default_address_id uuid,
|
||||||
|
rule_revision bigint NOT NULL DEFAULT 1,
|
||||||
|
updated_by uuid REFERENCES users(id),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
INSERT INTO e_address_settings(singleton) VALUES (true);
|
||||||
|
|
||||||
|
CREATE TABLE e_addresses (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name text NOT NULL CHECK (length(btrim(name)) BETWEEN 1 AND 128),
|
||||||
|
public_url text NOT NULL CHECK (public_url ~ '^https?://[^/?#[:space:]]+'),
|
||||||
|
country text NOT NULL DEFAULT '',
|
||||||
|
region text NOT NULL DEFAULT '',
|
||||||
|
city text NOT NULL DEFAULT '',
|
||||||
|
carrier text NOT NULL DEFAULT '',
|
||||||
|
asn bigint NOT NULL DEFAULT 0 CHECK (asn BETWEEN 0 AND 4294967295),
|
||||||
|
node_group text NOT NULL DEFAULT '',
|
||||||
|
priority integer NOT NULL DEFAULT 100 CHECK (priority BETWEEN -100000 AND 100000),
|
||||||
|
enabled boolean NOT NULL DEFAULT true,
|
||||||
|
healthy boolean NOT NULL DEFAULT false,
|
||||||
|
license_valid boolean NOT NULL DEFAULT false,
|
||||||
|
allow_user_publish boolean NOT NULL DEFAULT true,
|
||||||
|
maintenance boolean NOT NULL DEFAULT false,
|
||||||
|
description text NOT NULL DEFAULT '',
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX e_addresses_public_url_idx ON e_addresses(public_url);
|
||||||
|
CREATE INDEX e_addresses_selection_idx ON e_addresses(enabled,healthy,license_valid,allow_user_publish,maintenance,priority);
|
||||||
|
|
||||||
|
ALTER TABLE e_address_settings
|
||||||
|
ADD CONSTRAINT e_address_settings_default_fk FOREIGN KEY(default_address_id) REFERENCES e_addresses(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE TABLE e_address_rules (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
priority integer NOT NULL DEFAULT 100 CHECK (priority BETWEEN -100000 AND 100000),
|
||||||
|
country text NOT NULL DEFAULT '',
|
||||||
|
region text NOT NULL DEFAULT '',
|
||||||
|
carrier text NOT NULL DEFAULT '',
|
||||||
|
asn bigint NOT NULL DEFAULT 0 CHECK (asn BETWEEN 0 AND 4294967295),
|
||||||
|
node_group text NOT NULL DEFAULT '',
|
||||||
|
address_id uuid NOT NULL REFERENCES e_addresses(id) ON DELETE CASCADE,
|
||||||
|
enabled boolean NOT NULL DEFAULT true,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX e_address_rules_match_idx ON e_address_rules(enabled,priority,country,region,carrier,asn,node_group);
|
||||||
|
|
||||||
|
CREATE TABLE e_user_assignments (
|
||||||
|
user_id uuid PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
address_id uuid NOT NULL REFERENCES e_addresses(id),
|
||||||
|
fixed boolean NOT NULL DEFAULT false,
|
||||||
|
rule_revision bigint NOT NULL DEFAULT 1,
|
||||||
|
reason text NOT NULL DEFAULT '',
|
||||||
|
source_ip_hash text NOT NULL DEFAULT '',
|
||||||
|
assigned_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
expires_at timestamptz,
|
||||||
|
updated_by uuid REFERENCES users(id),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX e_user_assignments_expiry_idx ON e_user_assignments(expires_at);
|
||||||
|
|
||||||
|
CREATE TABLE e_assignment_history (
|
||||||
|
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
address_id uuid REFERENCES e_addresses(id) ON DELETE SET NULL,
|
||||||
|
source_ip_hash text NOT NULL DEFAULT '',
|
||||||
|
reason text NOT NULL DEFAULT '',
|
||||||
|
rule_revision bigint NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX e_assignment_history_user_idx ON e_assignment_history(user_id,created_at DESC);
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
INSERT INTO e_addresses (
|
||||||
|
id, name, public_url, country, region, city, carrier, asn, node_group,
|
||||||
|
priority, enabled, healthy, license_valid, allow_user_publish,
|
||||||
|
maintenance, description, created_at, updated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
node.id,
|
||||||
|
node.name,
|
||||||
|
rtrim(node.gateway_url, '/'),
|
||||||
|
node.country,
|
||||||
|
node.region,
|
||||||
|
node.city,
|
||||||
|
node.carrier,
|
||||||
|
node.asn,
|
||||||
|
pool.code,
|
||||||
|
node.weight,
|
||||||
|
node.enabled,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
node.maintenance OR node.draining,
|
||||||
|
'Migrated from retired proxy node ' || node.node_id,
|
||||||
|
node.created_at,
|
||||||
|
node.updated_at
|
||||||
|
FROM gateway_nodes AS node
|
||||||
|
JOIN gateway_service_pools AS pool ON pool.id = node.pool_id
|
||||||
|
WHERE node.gateway_url ~ '^https?://[^/?#[:space:]]+'
|
||||||
|
ON CONFLICT (public_url) DO UPDATE SET
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
country = EXCLUDED.country,
|
||||||
|
region = EXCLUDED.region,
|
||||||
|
city = EXCLUDED.city,
|
||||||
|
carrier = EXCLUDED.carrier,
|
||||||
|
asn = EXCLUDED.asn,
|
||||||
|
node_group = EXCLUDED.node_group,
|
||||||
|
priority = EXCLUDED.priority,
|
||||||
|
description = CASE
|
||||||
|
WHEN e_addresses.description = '' THEN EXCLUDED.description
|
||||||
|
ELSE e_addresses.description
|
||||||
|
END,
|
||||||
|
updated_at = GREATEST(e_addresses.updated_at, EXCLUDED.updated_at);
|
||||||
|
|
||||||
|
DROP TABLE gateway_route_events;
|
||||||
|
DROP TABLE gateway_settings;
|
||||||
|
DROP TABLE gateway_config_versions;
|
||||||
|
DROP TABLE gateway_user_routes;
|
||||||
|
DROP TABLE gateway_nodes;
|
||||||
|
DROP TABLE gateway_service_pools;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
ALTER TABLE e_address_rules
|
||||||
|
ADD COLUMN match_count bigint NOT NULL DEFAULT 0 CHECK (match_count >= 0),
|
||||||
|
ADD COLUMN last_matched_at timestamptz;
|
||||||
|
|
||||||
|
CREATE TABLE e_address_selection_events (
|
||||||
|
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
user_id uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
address_id uuid REFERENCES e_addresses(id) ON DELETE SET NULL,
|
||||||
|
rule_id uuid REFERENCES e_address_rules(id) ON DELETE SET NULL,
|
||||||
|
outcome text NOT NULL CHECK (outcome IN ('selected','unavailable','access_denied')),
|
||||||
|
reason text NOT NULL DEFAULT '',
|
||||||
|
source_ip_hash text NOT NULL DEFAULT '',
|
||||||
|
country text NOT NULL DEFAULT '',
|
||||||
|
region text NOT NULL DEFAULT '',
|
||||||
|
carrier text NOT NULL DEFAULT '',
|
||||||
|
asn bigint NOT NULL DEFAULT 0 CHECK (asn BETWEEN 0 AND 4294967295),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX e_address_selection_events_created_idx ON e_address_selection_events(created_at DESC);
|
||||||
|
CREATE INDEX e_address_selection_events_outcome_idx ON e_address_selection_events(outcome,created_at DESC);
|
||||||
|
CREATE INDEX e_address_selection_events_user_idx ON e_address_selection_events(user_id,created_at DESC);
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
ALTER TABLE users DROP CONSTRAINT users_role_check;
|
||||||
|
ALTER TABLE users ADD CONSTRAINT users_role_check CHECK (role IN ('user','owner','admin','operator','support','auditor'));
|
||||||
|
|
||||||
|
ALTER TABLE user_permissions DROP CONSTRAINT user_permissions_permission_check;
|
||||||
|
ALTER TABLE user_permissions ADD CONSTRAINT user_permissions_permission_check CHECK (permission IN (
|
||||||
|
'users.read','users.write','memberships.manage','cards.manage','invites.manage',
|
||||||
|
'requests.manage','tickets.manage','notifications.manage','security.manage',
|
||||||
|
'operations.manage','e.manage','audit.read','logs.read','backups.manage',
|
||||||
|
'updates.manage','settings.manage','licenses.manage'
|
||||||
|
));
|
||||||
|
|
||||||
|
CREATE TABLE membership_plans (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
code text NOT NULL UNIQUE CHECK (code ~ '^[a-z0-9][a-z0-9._-]{0,63}$'),
|
||||||
|
name text NOT NULL CHECK (length(btrim(name)) BETWEEN 1 AND 128),
|
||||||
|
duration_days integer NOT NULL CHECK (duration_days > 0),
|
||||||
|
max_playbacks integer NOT NULL DEFAULT 0 CHECK (max_playbacks >= 0),
|
||||||
|
max_devices integer NOT NULL DEFAULT 0 CHECK (max_devices >= 0),
|
||||||
|
max_bitrate_kbps integer NOT NULL DEFAULT 0 CHECK (max_bitrate_kbps >= 0),
|
||||||
|
enabled boolean NOT NULL DEFAULT true,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE memberships DROP CONSTRAINT memberships_status_check;
|
||||||
|
ALTER TABLE memberships ADD CONSTRAINT memberships_status_check CHECK (status IN ('active','paused','expired','revoked'));
|
||||||
|
ALTER TABLE memberships
|
||||||
|
ADD COLUMN plan_id uuid REFERENCES membership_plans(id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN paused_at timestamptz,
|
||||||
|
ADD COLUMN pause_reason text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN updated_at timestamptz NOT NULL DEFAULT now();
|
||||||
|
|
||||||
|
ALTER TABLE media_requests DROP CONSTRAINT media_requests_status_check;
|
||||||
|
UPDATE media_requests SET status='submitted' WHERE status='open';
|
||||||
|
UPDATE media_requests SET status='accepted' WHERE status='planned';
|
||||||
|
ALTER TABLE media_requests ADD CONSTRAINT media_requests_status_check CHECK (status IN (
|
||||||
|
'submitted','reviewing','accepted','existing','downloading','fulfilled','failed','rejected','cancelled'
|
||||||
|
));
|
||||||
|
ALTER TABLE media_requests
|
||||||
|
ADD COLUMN media_type text NOT NULL DEFAULT 'other' CHECK (media_type IN ('movie','series','season','episode','other')),
|
||||||
|
ADD COLUMN release_year integer CHECK (release_year BETWEEN 1800 AND 3000),
|
||||||
|
ADD COLUMN priority text NOT NULL DEFAULT 'normal' CHECK (priority IN ('low','normal','high','urgent')),
|
||||||
|
ADD COLUMN assignee_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN public_reply text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN internal_note text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN merged_into uuid REFERENCES media_requests(id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN closed_at timestamptz;
|
||||||
|
|
||||||
|
CREATE TABLE media_request_messages (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
request_id uuid NOT NULL REFERENCES media_requests(id) ON DELETE CASCADE,
|
||||||
|
author_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
visibility text NOT NULL CHECK (visibility IN ('user','internal')),
|
||||||
|
body text NOT NULL CHECK (length(btrim(body)) BETWEEN 1 AND 5000),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX media_request_messages_request_idx ON media_request_messages(request_id,created_at);
|
||||||
|
|
||||||
|
ALTER TABLE tickets DROP CONSTRAINT tickets_status_check;
|
||||||
|
UPDATE tickets SET status='in_progress' WHERE status='pending';
|
||||||
|
ALTER TABLE tickets ADD CONSTRAINT tickets_status_check CHECK (status IN ('open','in_progress','waiting_user','resolved','closed'));
|
||||||
|
ALTER TABLE tickets DROP CONSTRAINT tickets_priority_check;
|
||||||
|
ALTER TABLE tickets ADD CONSTRAINT tickets_priority_check CHECK (priority IN ('low','normal','high','urgent'));
|
||||||
|
ALTER TABLE tickets
|
||||||
|
ADD COLUMN sla_due_at timestamptz,
|
||||||
|
ADD COLUMN last_user_reply_at timestamptz,
|
||||||
|
ADD COLUMN last_staff_reply_at timestamptz,
|
||||||
|
ADD COLUMN reopened_at timestamptz;
|
||||||
|
ALTER TABLE ticket_replies
|
||||||
|
ADD COLUMN visibility text NOT NULL DEFAULT 'user' CHECK (visibility IN ('user','internal'));
|
||||||
|
|
||||||
|
CREATE TABLE announcements (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title text NOT NULL CHECK (length(btrim(title)) BETWEEN 1 AND 200),
|
||||||
|
body text NOT NULL CHECK (length(btrim(body)) BETWEEN 1 AND 20000),
|
||||||
|
status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','scheduled','published','withdrawn')),
|
||||||
|
audience text NOT NULL DEFAULT 'all' CHECK (audience IN ('all','active_members','staff')),
|
||||||
|
pinned boolean NOT NULL DEFAULT false,
|
||||||
|
publish_at timestamptz,
|
||||||
|
expires_at timestamptz,
|
||||||
|
created_by uuid NOT NULL REFERENCES users(id),
|
||||||
|
updated_by uuid NOT NULL REFERENCES users(id),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CHECK (expires_at IS NULL OR publish_at IS NULL OR expires_at > publish_at)
|
||||||
|
);
|
||||||
|
CREATE INDEX announcements_status_publish_idx ON announcements(status,publish_at DESC);
|
||||||
|
|
||||||
|
CREATE TABLE announcement_reads (
|
||||||
|
announcement_id uuid NOT NULL REFERENCES announcements(id) ON DELETE CASCADE,
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
read_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (announcement_id,user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE notification_events (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
event_type text NOT NULL,
|
||||||
|
target_user_id uuid REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
title text NOT NULL CHECK (length(btrim(title)) BETWEEN 1 AND 200),
|
||||||
|
body text NOT NULL CHECK (length(body) <= 10000),
|
||||||
|
link_path text NOT NULL DEFAULT '' CHECK (link_path='' OR link_path ~ '^/'),
|
||||||
|
related_type text NOT NULL DEFAULT '',
|
||||||
|
related_id text NOT NULL DEFAULT '',
|
||||||
|
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX notification_events_target_idx ON notification_events(target_user_id,created_at DESC);
|
||||||
|
|
||||||
|
CREATE TABLE notification_deliveries (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
event_id uuid NOT NULL REFERENCES notification_events(id) ON DELETE CASCADE,
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
channel text NOT NULL CHECK (channel IN ('in_app','email','telegram')),
|
||||||
|
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','sending','sent','failed','cancelled')),
|
||||||
|
attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
||||||
|
next_attempt_at timestamptz,
|
||||||
|
last_error text NOT NULL DEFAULT '',
|
||||||
|
read_at timestamptz,
|
||||||
|
deleted_at timestamptz,
|
||||||
|
sent_at timestamptz,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(event_id,user_id,channel)
|
||||||
|
);
|
||||||
|
CREATE INDEX notification_deliveries_user_idx ON notification_deliveries(user_id,channel,created_at DESC) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX notification_deliveries_pending_idx ON notification_deliveries(next_attempt_at,created_at) WHERE status IN ('pending','failed');
|
||||||
|
|
||||||
|
CREATE TABLE risk_rules (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name text NOT NULL CHECK (length(btrim(name)) BETWEEN 1 AND 128),
|
||||||
|
event_type text NOT NULL CHECK (event_type IN ('concurrency','multi_region','device_churn','auth_failures','manual')),
|
||||||
|
window_seconds integer NOT NULL CHECK (window_seconds BETWEEN 60 AND 2592000),
|
||||||
|
threshold integer NOT NULL CHECK (threshold > 0),
|
||||||
|
action text NOT NULL DEFAULT 'review' CHECK (action IN ('observe','review','revoke_sessions','disable_user')),
|
||||||
|
enabled boolean NOT NULL DEFAULT true,
|
||||||
|
created_by uuid NOT NULL REFERENCES users(id),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE risk_events (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
rule_id uuid REFERENCES risk_rules(id) ON DELETE SET NULL,
|
||||||
|
user_id uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
event_type text NOT NULL,
|
||||||
|
severity text NOT NULL CHECK (severity IN ('info','low','medium','high','critical')),
|
||||||
|
status text NOT NULL DEFAULT 'open' CHECK (status IN ('open','reviewing','resolved','dismissed','appealed')),
|
||||||
|
evidence jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(evidence)='object'),
|
||||||
|
resolution text NOT NULL DEFAULT '',
|
||||||
|
resolved_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
resolved_at timestamptz,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX risk_events_status_idx ON risk_events(status,severity,created_at DESC);
|
||||||
|
|
||||||
|
ALTER TABLE refresh_token_families
|
||||||
|
ADD COLUMN device_name text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN client_name text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN platform text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN ip_hash text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN last_seen_at timestamptz NOT NULL DEFAULT now();
|
||||||
|
|
||||||
|
ALTER TABLE devices
|
||||||
|
ADD COLUMN device_name text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN client_name text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN platform text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN client_version text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN last_ip_hash text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN trusted boolean NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN blocked_at timestamptz,
|
||||||
|
ADD COLUMN blocked_by uuid REFERENCES users(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE TABLE configuration_versions (
|
||||||
|
revision bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
domain text NOT NULL CHECK (domain IN ('s','e')),
|
||||||
|
values jsonb NOT NULL CHECK (jsonb_typeof(values)='object'),
|
||||||
|
state text NOT NULL DEFAULT 'draft' CHECK (state IN ('draft','published','superseded','failed')),
|
||||||
|
source_revision bigint REFERENCES configuration_versions(revision),
|
||||||
|
created_by uuid NOT NULL REFERENCES users(id),
|
||||||
|
published_at timestamptz,
|
||||||
|
failure_reason text NOT NULL DEFAULT '',
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX configuration_versions_published_domain_idx ON configuration_versions(domain) WHERE state='published';
|
||||||
|
|
||||||
|
CREATE TABLE operation_jobs (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
job_type text NOT NULL,
|
||||||
|
resource_type text NOT NULL DEFAULT '',
|
||||||
|
resource_id text NOT NULL DEFAULT '',
|
||||||
|
idempotency_key text NOT NULL UNIQUE,
|
||||||
|
state text NOT NULL DEFAULT 'queued' CHECK (state IN ('queued','running','pausing','paused','cancelling','cancelled','succeeded','partial_failed','failed')),
|
||||||
|
progress_total bigint NOT NULL DEFAULT 0 CHECK (progress_total >= 0),
|
||||||
|
progress_completed bigint NOT NULL DEFAULT 0 CHECK (progress_completed >= 0),
|
||||||
|
progress_failed bigint NOT NULL DEFAULT 0 CHECK (progress_failed >= 0),
|
||||||
|
error_code text NOT NULL DEFAULT '',
|
||||||
|
error_summary text NOT NULL DEFAULT '',
|
||||||
|
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
started_at timestamptz,
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
finished_at timestamptz,
|
||||||
|
CHECK (progress_completed + progress_failed <= progress_total OR progress_total=0)
|
||||||
|
);
|
||||||
|
CREATE INDEX operation_jobs_state_idx ON operation_jobs(state,created_at DESC);
|
||||||
|
|
||||||
|
CREATE TABLE api_idempotency_keys (
|
||||||
|
actor_user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
operation text NOT NULL,
|
||||||
|
idempotency_key text NOT NULL,
|
||||||
|
resource_id text NOT NULL DEFAULT '',
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
expires_at timestamptz NOT NULL DEFAULT now() + interval '24 hours',
|
||||||
|
PRIMARY KEY (actor_user_id,operation,idempotency_key)
|
||||||
|
);
|
||||||
|
CREATE INDEX api_idempotency_keys_expiry_idx ON api_idempotency_keys(expires_at);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE e_address_selection_events DROP CONSTRAINT e_address_selection_events_outcome_check;
|
||||||
|
ALTER TABLE e_address_selection_events ADD CONSTRAINT e_address_selection_events_outcome_check CHECK (outcome IN ('selected','unavailable','access_denied','rate_limited'));
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
ALTER TABLE audit_events
|
||||||
|
ADD COLUMN request_id text NOT NULL DEFAULT '';
|
||||||
|
CREATE INDEX audit_events_request_idx ON audit_events(request_id, created_at DESC) WHERE request_id <> '';
|
||||||
|
|
||||||
|
CREATE TABLE runtime_log_events (
|
||||||
|
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
level text NOT NULL CHECK (level IN ('debug','info','warn','error')),
|
||||||
|
message text NOT NULL CHECK (length(message) BETWEEN 1 AND 256),
|
||||||
|
request_id text NOT NULL DEFAULT '',
|
||||||
|
method text NOT NULL DEFAULT '',
|
||||||
|
path text NOT NULL DEFAULT '',
|
||||||
|
status_code integer NOT NULL DEFAULT 0 CHECK (status_code BETWEEN 0 AND 599),
|
||||||
|
duration_ms bigint NOT NULL DEFAULT 0 CHECK (duration_ms >= 0),
|
||||||
|
detail jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(detail)='object'),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX runtime_log_events_time_idx ON runtime_log_events(created_at DESC);
|
||||||
|
CREATE INDEX runtime_log_events_request_idx ON runtime_log_events(request_id,created_at DESC) WHERE request_id <> '';
|
||||||
|
|
||||||
|
ALTER TABLE operation_jobs
|
||||||
|
ADD COLUMN schema_version integer NOT NULL DEFAULT 1,
|
||||||
|
ADD COLUMN stage text NOT NULL DEFAULT 'queued',
|
||||||
|
ADD COLUMN priority integer NOT NULL DEFAULT 0 CHECK (priority BETWEEN -100 AND 100),
|
||||||
|
ADD COLUMN progress_skipped bigint NOT NULL DEFAULT 0 CHECK (progress_skipped >= 0),
|
||||||
|
ADD COLUMN retryable boolean NOT NULL DEFAULT true,
|
||||||
|
ADD COLUMN next_retry_at timestamptz,
|
||||||
|
ADD COLUMN claimed_by text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN payload jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(payload)='object');
|
||||||
|
|
||||||
|
CREATE TABLE backup_policies (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
scope text NOT NULL CHECK (scope IN ('s','e','auth','all')),
|
||||||
|
enabled boolean NOT NULL DEFAULT true,
|
||||||
|
interval_hours integer NOT NULL DEFAULT 24 CHECK (interval_hours BETWEEN 1 AND 720),
|
||||||
|
retention_count integer NOT NULL DEFAULT 14 CHECK (retention_count BETWEEN 1 AND 365),
|
||||||
|
updated_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(scope)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE backup_artifacts (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
scope text NOT NULL CHECK (scope IN ('s','e','auth','all')),
|
||||||
|
state text NOT NULL DEFAULT 'queued' CHECK (state IN ('queued','running','succeeded','failed','expired','deleted')),
|
||||||
|
storage_key text NOT NULL,
|
||||||
|
size_bytes bigint NOT NULL DEFAULT 0 CHECK (size_bytes >= 0),
|
||||||
|
sha256 text NOT NULL DEFAULT '' CHECK (sha256 = '' OR sha256 ~ '^[0-9a-f]{64}$'),
|
||||||
|
job_id uuid REFERENCES operation_jobs(id) ON DELETE SET NULL,
|
||||||
|
error_code text NOT NULL DEFAULT '',
|
||||||
|
error_summary text NOT NULL DEFAULT '',
|
||||||
|
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
completed_at timestamptz,
|
||||||
|
deleted_at timestamptz
|
||||||
|
);
|
||||||
|
CREATE INDEX backup_artifacts_scope_idx ON backup_artifacts(scope,created_at DESC);
|
||||||
|
|
||||||
|
CREATE TABLE system_runtime_state (
|
||||||
|
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
||||||
|
maintenance boolean NOT NULL DEFAULT false,
|
||||||
|
maintenance_reason text NOT NULL DEFAULT '',
|
||||||
|
maintenance_job_id uuid REFERENCES operation_jobs(id) ON DELETE SET NULL,
|
||||||
|
updated_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
INSERT INTO system_runtime_state(singleton) VALUES (true) ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
CREATE TABLE update_artifacts (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
product text NOT NULL CHECK (product IN ('s','e','auth')),
|
||||||
|
platform text NOT NULL,
|
||||||
|
version text NOT NULL,
|
||||||
|
channel text NOT NULL CHECK (channel IN ('stable','preview')),
|
||||||
|
download_url text NOT NULL DEFAULT '',
|
||||||
|
sha256 text NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$'),
|
||||||
|
signature text NOT NULL DEFAULT '',
|
||||||
|
min_s_version text NOT NULL DEFAULT '',
|
||||||
|
min_e_version text NOT NULL DEFAULT '',
|
||||||
|
state text NOT NULL DEFAULT 'authorized' CHECK (state IN ('authorized','downloaded','verified','staged','applied','rolled_back','failed')),
|
||||||
|
storage_key text NOT NULL DEFAULT '',
|
||||||
|
job_id uuid REFERENCES operation_jobs(id) ON DELETE SET NULL,
|
||||||
|
error_code text NOT NULL DEFAULT '',
|
||||||
|
error_summary text NOT NULL DEFAULT '',
|
||||||
|
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(product,platform,version,channel)
|
||||||
|
);
|
||||||
|
CREATE INDEX update_artifacts_state_idx ON update_artifacts(product,state,updated_at DESC);
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN deleted_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN deletion_reason text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN restored_at timestamptz;
|
||||||
|
|
||||||
|
ALTER TABLE card_batches
|
||||||
|
ADD COLUMN membership_plan_id uuid REFERENCES membership_plans(id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN edition text NOT NULL DEFAULT '' CHECK (char_length(edition) <= 64),
|
||||||
|
ADD COLUMN max_uses integer NOT NULL DEFAULT 1 CHECK (max_uses > 0);
|
||||||
|
|
||||||
|
ALTER TABLE card_batches DROP CONSTRAINT card_batches_card_type_check;
|
||||||
|
ALTER TABLE card_batches ADD CONSTRAINT card_batches_card_type_check CHECK (card_type IN ('registration','renewal','membership'));
|
||||||
|
|
||||||
|
ALTER TABLE cards
|
||||||
|
ADD COLUMN label text NOT NULL DEFAULT '' CHECK (char_length(label) <= 128);
|
||||||
|
|
||||||
|
CREATE TABLE notification_channel_configs (
|
||||||
|
channel text PRIMARY KEY CHECK (channel IN ('smtp','telegram')),
|
||||||
|
enabled boolean NOT NULL DEFAULT false,
|
||||||
|
settings jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(settings)='object'),
|
||||||
|
secret_cipher bytea,
|
||||||
|
updated_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO notification_channel_configs(channel) VALUES ('smtp'),('telegram');
|
||||||
|
|
||||||
|
CREATE TABLE notification_templates (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
template_key text NOT NULL CHECK (template_key ~ '^[a-z0-9][a-z0-9._-]{0,127}$'),
|
||||||
|
channel text NOT NULL CHECK (channel IN ('email','telegram')),
|
||||||
|
subject text NOT NULL DEFAULT '' CHECK (char_length(subject) <= 300),
|
||||||
|
body text NOT NULL CHECK (char_length(body) BETWEEN 1 AND 20000),
|
||||||
|
enabled boolean NOT NULL DEFAULT true,
|
||||||
|
updated_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(template_key,channel)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE notification_delivery_attempts (
|
||||||
|
id bigserial PRIMARY KEY,
|
||||||
|
delivery_id uuid NOT NULL REFERENCES notification_deliveries(id) ON DELETE CASCADE,
|
||||||
|
attempt integer NOT NULL CHECK (attempt > 0),
|
||||||
|
status text NOT NULL CHECK (status IN ('sent','failed','skipped')),
|
||||||
|
error_code text NOT NULL DEFAULT '',
|
||||||
|
error_summary text NOT NULL DEFAULT '',
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(delivery_id,attempt)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE telegram_runtime_state (
|
||||||
|
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
||||||
|
update_offset bigint NOT NULL DEFAULT 0,
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
INSERT INTO telegram_runtime_state(singleton) VALUES(true);
|
||||||
|
|
||||||
|
CREATE TABLE user_ip_history (
|
||||||
|
id bigserial PRIMARY KEY,
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
ip_hash text NOT NULL CHECK (char_length(ip_hash)=64),
|
||||||
|
country text NOT NULL DEFAULT '',
|
||||||
|
region text NOT NULL DEFAULT '',
|
||||||
|
carrier text NOT NULL DEFAULT '',
|
||||||
|
asn bigint,
|
||||||
|
first_seen_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
last_seen_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
seen_count bigint NOT NULL DEFAULT 1 CHECK (seen_count > 0),
|
||||||
|
UNIQUE(user_id,ip_hash)
|
||||||
|
);
|
||||||
|
CREATE INDEX user_ip_history_user_time_idx ON user_ip_history(user_id,last_seen_at DESC);
|
||||||
|
|
||||||
|
CREATE TABLE security_list_entries (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
subject_type text NOT NULL CHECK (subject_type IN ('ip','cidr','device','user')),
|
||||||
|
mode text NOT NULL CHECK (mode IN ('allow','deny')),
|
||||||
|
subject_value text NOT NULL CHECK (char_length(subject_value) BETWEEN 1 AND 512),
|
||||||
|
reason text NOT NULL CHECK (char_length(btrim(reason)) BETWEEN 1 AND 1000),
|
||||||
|
expires_at timestamptz,
|
||||||
|
created_by uuid NOT NULL REFERENCES users(id),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
revoked_at timestamptz,
|
||||||
|
revoked_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
UNIQUE(subject_type,mode,subject_value)
|
||||||
|
);
|
||||||
|
CREATE INDEX security_list_active_idx ON security_list_entries(subject_type,mode,expires_at) WHERE revoked_at IS NULL;
|
||||||
|
|
||||||
|
CREATE TABLE user_appeals (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
risk_event_id uuid REFERENCES risk_events(id) ON DELETE SET NULL,
|
||||||
|
body text NOT NULL CHECK (char_length(btrim(body)) BETWEEN 1 AND 5000),
|
||||||
|
status text NOT NULL DEFAULT 'submitted' CHECK (status IN ('submitted','reviewing','accepted','rejected')),
|
||||||
|
resolution text NOT NULL DEFAULT '',
|
||||||
|
resolved_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
resolved_at timestamptz,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE media_request_callbacks (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name text NOT NULL CHECK (char_length(btrim(name)) BETWEEN 1 AND 128),
|
||||||
|
callback_url text NOT NULL CHECK (callback_url ~ '^https://'),
|
||||||
|
secret_cipher bytea NOT NULL,
|
||||||
|
enabled boolean NOT NULL DEFAULT true,
|
||||||
|
created_by uuid NOT NULL REFERENCES users(id),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE media_request_callback_events (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
callback_id uuid NOT NULL REFERENCES media_request_callbacks(id) ON DELETE CASCADE,
|
||||||
|
external_event_id text NOT NULL CHECK (char_length(external_event_id) BETWEEN 1 AND 256),
|
||||||
|
request_id uuid REFERENCES media_requests(id) ON DELETE SET NULL,
|
||||||
|
payload jsonb NOT NULL CHECK (jsonb_typeof(payload)='object'),
|
||||||
|
signature_valid boolean NOT NULL,
|
||||||
|
applied boolean NOT NULL DEFAULT false,
|
||||||
|
error_summary text NOT NULL DEFAULT '',
|
||||||
|
received_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(callback_id,external_event_id)
|
||||||
|
);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE system_installation (
|
||||||
|
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
||||||
|
installation_id uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
setup_completed_at timestamptz,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO system_installation(singleton) VALUES(true);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE e_address_rules
|
||||||
|
ALTER COLUMN address_id DROP NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE e_address_rules
|
||||||
|
ADD CONSTRAINT e_address_rules_target_check
|
||||||
|
CHECK (address_id IS NOT NULL OR length(btrim(node_group)) > 0);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
ALTER TABLE playback_decisions
|
||||||
|
ADD COLUMN device_name text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN client_name text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN client_version text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN remote_ip_hash text NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
ALTER TABLE playback_sessions
|
||||||
|
ADD COLUMN media_id uuid,
|
||||||
|
ADD COLUMN source_id uuid,
|
||||||
|
ADD COLUMN node_id uuid,
|
||||||
|
ADD COLUMN position_ticks bigint NOT NULL DEFAULT 0 CHECK (position_ticks >= 0),
|
||||||
|
ADD COLUMN device_name text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN client_name text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN client_version text NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
CREATE INDEX playback_sessions_recent_idx
|
||||||
|
ON playback_sessions(updated_at DESC, id DESC);
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
CREATE TABLE subtitle_sources (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name text NOT NULL CHECK (char_length(btrim(name)) BETWEEN 1 AND 128),
|
||||||
|
source_type text NOT NULL DEFAULT 'json_rest_v1' CHECK (source_type IN ('json_rest_v1')),
|
||||||
|
base_url text NOT NULL CHECK (base_url ~ '^https://'),
|
||||||
|
api_key_cipher bytea,
|
||||||
|
enabled boolean NOT NULL DEFAULT true,
|
||||||
|
languages text[] NOT NULL DEFAULT '{}',
|
||||||
|
priority integer NOT NULL DEFAULT 100 CHECK (priority BETWEEN 1 AND 10000),
|
||||||
|
timeout_seconds integer NOT NULL DEFAULT 10 CHECK (timeout_seconds BETWEEN 2 AND 30),
|
||||||
|
created_by uuid NOT NULL REFERENCES users(id),
|
||||||
|
updated_by uuid NOT NULL REFERENCES users(id),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (name)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX subtitle_sources_enabled_idx
|
||||||
|
ON subtitle_sources(priority,id) WHERE enabled;
|
||||||
|
|
||||||
|
CREATE TABLE subtitle_search_candidates (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
source_id uuid NOT NULL REFERENCES subtitle_sources(id) ON DELETE CASCADE,
|
||||||
|
media_item_id uuid NOT NULL,
|
||||||
|
media_source_id uuid NOT NULL,
|
||||||
|
external_id text NOT NULL CHECK (char_length(external_id) BETWEEN 1 AND 512),
|
||||||
|
title text NOT NULL CHECK (char_length(title) BETWEEN 1 AND 512),
|
||||||
|
language text NOT NULL DEFAULT '' CHECK (char_length(language) <= 64),
|
||||||
|
codec text NOT NULL CHECK (codec IN ('srt','ass','ssa','vtt','sup','sub','idx')),
|
||||||
|
release_group text NOT NULL DEFAULT '' CHECK (char_length(release_group) <= 256),
|
||||||
|
score double precision NOT NULL DEFAULT 0 CHECK (score >= 0 AND score <= 1),
|
||||||
|
download_path text NOT NULL CHECK (char_length(download_path) BETWEEN 1 AND 2048),
|
||||||
|
hearing_impaired boolean NOT NULL DEFAULT false,
|
||||||
|
created_by uuid NOT NULL REFERENCES users(id),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
expires_at timestamptz NOT NULL DEFAULT (now() + interval '1 hour')
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX subtitle_search_candidates_actor_idx
|
||||||
|
ON subtitle_search_candidates(created_by,created_at DESC);
|
||||||
|
CREATE INDEX subtitle_search_candidates_expiry_idx
|
||||||
|
ON subtitle_search_candidates(expires_at);
|
||||||
|
|
||||||
|
CREATE TABLE subtitle_imports (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
candidate_id uuid NOT NULL REFERENCES subtitle_search_candidates(id) ON DELETE CASCADE,
|
||||||
|
actor_user_id uuid NOT NULL REFERENCES users(id),
|
||||||
|
idempotency_key text NOT NULL CHECK (char_length(idempotency_key) BETWEEN 16 AND 128),
|
||||||
|
state text NOT NULL DEFAULT 'processing' CHECK (state IN ('processing','succeeded','failed')),
|
||||||
|
result jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(result)='object'),
|
||||||
|
error_code text NOT NULL DEFAULT '',
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(actor_user_id,idempotency_key)
|
||||||
|
);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE metadata_provider_credentials (
|
||||||
|
provider text PRIMARY KEY CHECK (provider IN ('tmdb','bangumi')),
|
||||||
|
token_cipher bytea,
|
||||||
|
enabled boolean NOT NULL DEFAULT true,
|
||||||
|
updated_by uuid NOT NULL REFERENCES users(id),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX metadata_provider_credentials_enabled_idx
|
||||||
|
ON metadata_provider_credentials(provider) WHERE enabled;
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
CREATE TABLE point_rules (
|
||||||
|
rule_key text PRIMARY KEY CHECK (rule_key ~ '^[a-z0-9][a-z0-9._-]{0,63}$'),
|
||||||
|
name text NOT NULL CHECK (char_length(btrim(name)) BETWEEN 1 AND 128),
|
||||||
|
points integer NOT NULL CHECK (points BETWEEN -100000 AND 100000),
|
||||||
|
enabled boolean NOT NULL DEFAULT true,
|
||||||
|
description text NOT NULL DEFAULT '' CHECK (char_length(description) <= 1000),
|
||||||
|
updated_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO point_rules(rule_key,name,points,description) VALUES
|
||||||
|
('playback.completed','有效观看',5,'单次观看达到 10 分钟后奖励,每个播放会话仅奖励一次'),
|
||||||
|
('feedback.accepted','有效反馈',10,'管理员确认媒体问题后奖励');
|
||||||
|
|
||||||
|
CREATE TABLE point_accounts (
|
||||||
|
user_id uuid PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
balance bigint NOT NULL DEFAULT 0 CHECK (balance >= 0),
|
||||||
|
lifetime_earned bigint NOT NULL DEFAULT 0 CHECK (lifetime_earned >= 0),
|
||||||
|
lifetime_spent bigint NOT NULL DEFAULT 0 CHECK (lifetime_spent >= 0),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE point_ledger (
|
||||||
|
id bigserial PRIMARY KEY,
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
delta integer NOT NULL CHECK (delta <> 0 AND delta BETWEEN -100000 AND 100000),
|
||||||
|
balance_after bigint NOT NULL CHECK (balance_after >= 0),
|
||||||
|
reason text NOT NULL CHECK (char_length(btrim(reason)) BETWEEN 1 AND 200),
|
||||||
|
source_type text NOT NULL DEFAULT 'manual' CHECK (source_type IN ('manual','playback_session','media_feedback','redemption')),
|
||||||
|
source_id text NOT NULL DEFAULT '' CHECK (char_length(source_id) <= 128),
|
||||||
|
actor_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(user_id,source_type,source_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX point_ledger_user_time_idx ON point_ledger(user_id,created_at DESC,id DESC);
|
||||||
|
|
||||||
|
CREATE TABLE media_feedback (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
media_id uuid,
|
||||||
|
source_id uuid,
|
||||||
|
playback_session_id uuid REFERENCES playback_sessions(id) ON DELETE SET NULL,
|
||||||
|
category text NOT NULL CHECK (category IN ('playback','subtitle','audio','metadata','quality','other')),
|
||||||
|
title text NOT NULL CHECK (char_length(btrim(title)) BETWEEN 1 AND 200),
|
||||||
|
detail text NOT NULL CHECK (char_length(btrim(detail)) BETWEEN 1 AND 5000),
|
||||||
|
status text NOT NULL DEFAULT 'submitted' CHECK (status IN ('submitted','reviewing','resolved','rejected')),
|
||||||
|
priority text NOT NULL DEFAULT 'normal' CHECK (priority IN ('low','normal','high','urgent')),
|
||||||
|
resolution text NOT NULL DEFAULT '' CHECK (char_length(resolution) <= 5000),
|
||||||
|
assigned_to uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
resolved_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
resolved_at timestamptz,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX media_feedback_user_time_idx ON media_feedback(user_id,created_at DESC);
|
||||||
|
CREATE INDEX media_feedback_queue_idx ON media_feedback(status,priority,updated_at DESC);
|
||||||
|
|
||||||
|
CREATE TABLE telegram_bot_settings (
|
||||||
|
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
||||||
|
enabled boolean NOT NULL DEFAULT true,
|
||||||
|
commands_enabled boolean NOT NULL DEFAULT true,
|
||||||
|
rate_limit_per_minute integer NOT NULL DEFAULT 12 CHECK (rate_limit_per_minute BETWEEN 1 AND 120),
|
||||||
|
welcome_message text NOT NULL DEFAULT '欢迎使用站点助手,请先绑定账户。' CHECK (char_length(welcome_message) BETWEEN 1 AND 1000),
|
||||||
|
maintenance_message text NOT NULL DEFAULT '' CHECK (char_length(maintenance_message) <= 1000),
|
||||||
|
updated_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
INSERT INTO telegram_bot_settings(singleton) VALUES(true);
|
||||||
|
|
||||||
|
CREATE TABLE telegram_command_logs (
|
||||||
|
id bigserial PRIMARY KEY,
|
||||||
|
telegram_user_id bigint NOT NULL,
|
||||||
|
user_id uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
command text NOT NULL CHECK (char_length(command) BETWEEN 1 AND 64),
|
||||||
|
result text NOT NULL CHECK (char_length(result) BETWEEN 1 AND 64),
|
||||||
|
duration_ms integer NOT NULL DEFAULT 0 CHECK (duration_ms >= 0),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX telegram_command_logs_rate_idx ON telegram_command_logs(telegram_user_id,created_at DESC);
|
||||||
|
CREATE INDEX telegram_command_logs_admin_idx ON telegram_command_logs(created_at DESC,id DESC);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
ALTER TABLE point_ledger DROP CONSTRAINT point_ledger_source_type_check;
|
||||||
|
ALTER TABLE point_ledger ADD CONSTRAINT point_ledger_source_type_check
|
||||||
|
CHECK (source_type IN ('manual','playback_session','media_feedback','redemption','daily_checkin'));
|
||||||
|
|
||||||
|
INSERT INTO point_rules(rule_key,name,points,description) VALUES
|
||||||
|
('daily_checkin.base','每日签到',5,'每天首次签到获得的积分'),
|
||||||
|
('daily_checkin.streak_7','连续签到 7 天',10,'连续签到第 7 天的额外积分'),
|
||||||
|
('daily_checkin.streak_30','连续签到 30 天',30,'连续签到第 30 天的额外积分')
|
||||||
|
ON CONFLICT(rule_key) DO NOTHING;
|
||||||
|
|
||||||
|
CREATE TABLE daily_checkins (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
checkin_date date NOT NULL,
|
||||||
|
streak integer NOT NULL CHECK(streak > 0),
|
||||||
|
reward_points integer NOT NULL CHECK(reward_points >= 0),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(user_id,checkin_date)
|
||||||
|
);
|
||||||
|
CREATE INDEX daily_checkins_user_time_idx ON daily_checkins(user_id,checkin_date DESC);
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
ALTER TABLE membership_plans
|
||||||
|
ADD COLUMN price_points integer NOT NULL DEFAULT 0 CHECK(price_points >= 0),
|
||||||
|
ADD COLUMN self_service boolean NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
INSERT INTO membership_plans(code,name,duration_days,max_playbacks,max_devices,max_bitrate_kbps,price_points,self_service,enabled)
|
||||||
|
VALUES('points-30d','30 天会员',30,1,3,0,100,true,true)
|
||||||
|
ON CONFLICT(code) DO NOTHING;
|
||||||
|
|
||||||
|
ALTER TABLE point_ledger DROP CONSTRAINT point_ledger_source_type_check;
|
||||||
|
ALTER TABLE point_ledger ADD CONSTRAINT point_ledger_source_type_check
|
||||||
|
CHECK (source_type IN ('manual','playback_session','media_feedback','redemption','daily_checkin','membership_purchase'));
|
||||||
|
|
||||||
|
CREATE TABLE membership_purchases (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
plan_id uuid NOT NULL REFERENCES membership_plans(id),
|
||||||
|
price_points integer NOT NULL CHECK(price_points >= 0),
|
||||||
|
membership_id uuid REFERENCES memberships(id),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX membership_purchases_user_time_idx ON membership_purchases(user_id,created_at DESC);
|
||||||
|
|
||||||
|
CREATE TABLE watch_calendar_entries (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
title text NOT NULL CHECK(length(btrim(title)) BETWEEN 1 AND 200),
|
||||||
|
episode_label text NOT NULL DEFAULT '' CHECK(length(episode_label) <= 100),
|
||||||
|
next_episode_at timestamptz NOT NULL,
|
||||||
|
notes text NOT NULL DEFAULT '' CHECK(length(notes) <= 1000),
|
||||||
|
completed boolean NOT NULL DEFAULT false,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX watch_calendar_user_time_idx ON watch_calendar_entries(user_id,completed,next_episode_at);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE telegram_bot_settings
|
||||||
|
ADD COLUMN server_name text NOT NULL DEFAULT 'OverCooked' CHECK(char_length(btrim(server_name)) BETWEEN 1 AND 80),
|
||||||
|
ADD COLUMN icon text NOT NULL DEFAULT '🎬' CHECK(char_length(btrim(icon)) BETWEEN 1 AND 16);
|
||||||
|
|
||||||
|
CREATE TABLE telegram_chat_states (
|
||||||
|
chat_id bigint PRIMARY KEY,
|
||||||
|
message_id bigint NOT NULL CHECK(message_id > 0),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
CREATE TABLE site_brand_settings (
|
||||||
|
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
||||||
|
server_name text NOT NULL DEFAULT 'OverCooked' CHECK (char_length(btrim(server_name)) BETWEEN 1 AND 80),
|
||||||
|
emby_display_name text NOT NULL DEFAULT 'OverCooked' CHECK (char_length(btrim(emby_display_name)) BETWEEN 1 AND 80),
|
||||||
|
icon_data_url text NOT NULL DEFAULT '' CHECK (char_length(icon_data_url) <= 700000),
|
||||||
|
updated_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO site_brand_settings(singleton) VALUES(true);
|
||||||
|
|
||||||
|
ALTER TABLE telegram_bot_settings
|
||||||
|
ADD COLUMN official_chat_id bigint NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN official_chat_title text NOT NULL DEFAULT '' CHECK (char_length(official_chat_title) <= 120);
|
||||||
|
|
||||||
|
ALTER TABLE telegram_chat_states
|
||||||
|
ADD COLUMN message_kind text NOT NULL DEFAULT 'text' CHECK (message_kind IN ('text','photo'));
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
ALTER TABLE media_requests
|
||||||
|
ADD COLUMN provider text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN external_id text NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN original_title text NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
ALTER TABLE media_requests
|
||||||
|
ADD CONSTRAINT media_requests_provider_check CHECK (provider IN ('', 'tmdb', 'bangumi')),
|
||||||
|
ADD CONSTRAINT media_requests_external_id_length CHECK (length(external_id) <= 512),
|
||||||
|
ADD CONSTRAINT media_requests_original_title_length CHECK (length(original_title) <= 300);
|
||||||
|
|
||||||
|
CREATE INDEX media_requests_external_metadata_idx
|
||||||
|
ON media_requests(provider, external_id)
|
||||||
|
WHERE provider <> '' AND external_id <> '';
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
CREATE TABLE support_attachments (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
ticket_id uuid REFERENCES tickets(id) ON DELETE CASCADE,
|
||||||
|
media_feedback_id uuid REFERENCES media_feedback(id) ON DELETE CASCADE,
|
||||||
|
uploader_user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
filename text NOT NULL CHECK (length(filename) BETWEEN 1 AND 255),
|
||||||
|
mime_type text NOT NULL CHECK (length(mime_type) BETWEEN 1 AND 128),
|
||||||
|
size_bytes bigint NOT NULL CHECK (size_bytes BETWEEN 1 AND 8388608),
|
||||||
|
content bytea NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CHECK ((ticket_id IS NOT NULL)::int + (media_feedback_id IS NOT NULL)::int = 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX support_attachments_ticket_idx ON support_attachments(ticket_id,created_at,id) WHERE ticket_id IS NOT NULL;
|
||||||
|
CREATE INDEX support_attachments_feedback_idx ON support_attachments(media_feedback_id,created_at,id) WHERE media_feedback_id IS NOT NULL;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
ALTER TABLE site_brand_settings
|
||||||
|
ADD COLUMN banner_enabled boolean NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN banner_image_data_url text NOT NULL DEFAULT '' CHECK (char_length(banner_image_data_url) <= 2800000),
|
||||||
|
ADD COLUMN banner_link_url text NOT NULL DEFAULT '' CHECK (char_length(banner_link_url) <= 2000);
|
||||||
|
|
||||||
|
CREATE TABLE telegram_password_flows (
|
||||||
|
telegram_user_id bigint PRIMARY KEY,
|
||||||
|
stage text NOT NULL CHECK (stage IN ('new_password','confirm_password')),
|
||||||
|
password_hash text NOT NULL DEFAULT '',
|
||||||
|
expires_at timestamptz NOT NULL,
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX telegram_password_flows_expiry_idx ON telegram_password_flows(expires_at);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE site_brand_settings
|
||||||
|
ADD COLUMN login_background_data_url text NOT NULL DEFAULT ''
|
||||||
|
CHECK (char_length(login_background_data_url) <= 5600000);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE site_brand_settings
|
||||||
|
DROP CONSTRAINT IF EXISTS site_brand_settings_banner_image_data_url_check,
|
||||||
|
DROP CONSTRAINT IF EXISTS site_brand_settings_login_background_data_url_check;
|
||||||
|
|
||||||
|
ALTER TABLE site_brand_settings
|
||||||
|
ADD CONSTRAINT site_brand_settings_banner_image_data_url_check
|
||||||
|
CHECK (char_length(banner_image_data_url) <= 28000000),
|
||||||
|
ADD CONSTRAINT site_brand_settings_login_background_data_url_check
|
||||||
|
CHECK (char_length(login_background_data_url) <= 28000000);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE point_accounts
|
||||||
|
DROP CONSTRAINT IF EXISTS point_accounts_balance_check;
|
||||||
|
|
||||||
|
ALTER TABLE point_ledger
|
||||||
|
DROP CONSTRAINT IF EXISTS point_ledger_balance_after_check;
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
ALTER TABLE site_brand_settings
|
||||||
|
ADD COLUMN IF NOT EXISTS points_name text NOT NULL DEFAULT '积分'
|
||||||
|
CHECK (char_length(btrim(points_name)) BETWEEN 1 AND 20);
|
||||||
|
|
||||||
|
UPDATE memberships
|
||||||
|
SET ends_at = timestamptz '9999-12-31 23:59:59+00'
|
||||||
|
WHERE ends_at > timestamptz '9999-12-31 23:59:59+00';
|
||||||
|
|
||||||
|
UPDATE card_batches SET duration_days = 365000 WHERE duration_days > 365000;
|
||||||
|
UPDATE membership_plans SET duration_days = 365000 WHERE duration_days > 365000;
|
||||||
|
|
||||||
|
ALTER TABLE card_batches
|
||||||
|
ADD CONSTRAINT card_batches_duration_days_limit CHECK (duration_days BETWEEN 1 AND 365000);
|
||||||
|
ALTER TABLE membership_plans
|
||||||
|
ADD CONSTRAINT membership_plans_duration_days_limit CHECK (duration_days BETWEEN 1 AND 365000);
|
||||||
|
|
||||||
|
ALTER TABLE point_ledger DROP CONSTRAINT IF EXISTS point_ledger_source_type_check;
|
||||||
|
ALTER TABLE point_ledger
|
||||||
|
ADD CONSTRAINT point_ledger_source_type_check CHECK (source_type IN ('manual','playback_session','media_feedback','redemption','daily_checkin','membership_purchase','telegram_red_packet'));
|
||||||
|
|
||||||
|
CREATE TABLE telegram_red_packets (
|
||||||
|
id uuid PRIMARY KEY,
|
||||||
|
chat_id bigint NOT NULL,
|
||||||
|
creator_user_id uuid NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||||
|
creator_telegram_id bigint NOT NULL,
|
||||||
|
allocation_mode text NOT NULL CHECK (allocation_mode IN ('normal','random')),
|
||||||
|
claim_mode text NOT NULL CHECK (claim_mode IN ('button','password')),
|
||||||
|
total_points integer NOT NULL CHECK (total_points BETWEEN 1 AND 100000),
|
||||||
|
packet_count integer NOT NULL CHECK (packet_count BETWEEN 1 AND 1000),
|
||||||
|
remaining_points integer NOT NULL CHECK (remaining_points >= 0),
|
||||||
|
remaining_count integer NOT NULL CHECK (remaining_count >= 0),
|
||||||
|
password_hash text NOT NULL DEFAULT '',
|
||||||
|
message text NOT NULL DEFAULT '' CHECK (char_length(message) <= 200),
|
||||||
|
status text NOT NULL DEFAULT 'open' CHECK (status IN ('open','expired','closed')),
|
||||||
|
message_id bigint NOT NULL DEFAULT 0,
|
||||||
|
expires_at timestamptz NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CHECK (remaining_count <= packet_count),
|
||||||
|
CHECK (remaining_points <= total_points)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX telegram_red_packets_chat_open_idx ON telegram_red_packets(chat_id, expires_at DESC) WHERE status = 'open';
|
||||||
|
|
||||||
|
CREATE TABLE telegram_red_packet_claims (
|
||||||
|
id bigserial PRIMARY KEY,
|
||||||
|
packet_id uuid NOT NULL REFERENCES telegram_red_packets(id) ON DELETE CASCADE,
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||||
|
telegram_user_id bigint NOT NULL,
|
||||||
|
amount integer NOT NULL CHECK (amount > 0),
|
||||||
|
claimed_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(packet_id, user_id),
|
||||||
|
UNIQUE(packet_id, telegram_user_id)
|
||||||
|
);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
ALTER TABLE user_permissions DROP CONSTRAINT user_permissions_permission_check;
|
||||||
|
ALTER TABLE user_permissions ADD CONSTRAINT user_permissions_permission_check CHECK (permission IN (
|
||||||
|
'users.read','users.write','memberships.manage','cards.manage','invites.manage',
|
||||||
|
'requests.manage','tickets.manage','notifications.manage','security.manage',
|
||||||
|
'operations.manage','e.manage','plugins.manage','audit.read','logs.read','backups.manage',
|
||||||
|
'updates.manage','settings.manage','licenses.manage'
|
||||||
|
));
|
||||||
|
|
||||||
|
INSERT INTO user_permissions (user_id, permission, granted_by)
|
||||||
|
SELECT id, 'plugins.manage', id
|
||||||
|
FROM users
|
||||||
|
WHERE role = 'owner'
|
||||||
|
ON CONFLICT (user_id, permission) DO NOTHING;
|
||||||
|
|
||||||
|
ALTER TABLE e_command_requests
|
||||||
|
ADD COLUMN response_status integer CHECK (response_status BETWEEN 100 AND 599),
|
||||||
|
ADD COLUMN response_body jsonb;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- The plugin control plane is authenticated by E, but existing S transactions
|
||||||
|
-- require a UUID actor for foreign keys and role checks. This principal has no
|
||||||
|
-- credential row and is excluded from all user-management views.
|
||||||
|
INSERT INTO users (id, username, username_normalized, email, email_normalized, role, status)
|
||||||
|
VALUES (
|
||||||
|
'00000000-0000-0000-0000-000000000036',
|
||||||
|
'__plugin_service__',
|
||||||
|
'__plugin_service__',
|
||||||
|
'plugin-service@internal.invalid',
|
||||||
|
'plugin-service@internal.invalid',
|
||||||
|
'admin',
|
||||||
|
'active'
|
||||||
|
)
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
CREATE TABLE user_groups (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name text NOT NULL,
|
||||||
|
name_normalized text NOT NULL UNIQUE,
|
||||||
|
description text NOT NULL DEFAULT '',
|
||||||
|
created_by uuid REFERENCES users(id),
|
||||||
|
updated_by uuid REFERENCES users(id),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CHECK (char_length(btrim(name)) BETWEEN 1 AND 80),
|
||||||
|
CHECK (char_length(description) <= 1000)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_group_members (
|
||||||
|
group_id uuid NOT NULL REFERENCES user_groups(id) ON DELETE CASCADE,
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (group_id, user_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX user_group_members_user_idx ON user_group_members(user_id, group_id);
|
||||||
|
|
||||||
|
CREATE TABLE user_group_permissions (
|
||||||
|
group_id uuid NOT NULL REFERENCES user_groups(id) ON DELETE CASCADE,
|
||||||
|
permission text NOT NULL,
|
||||||
|
granted_by uuid REFERENCES users(id),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (group_id, permission),
|
||||||
|
CHECK (permission IN (
|
||||||
|
'users.read','users.write','memberships.manage','cards.manage','invites.manage',
|
||||||
|
'requests.manage','tickets.manage','notifications.manage','security.manage',
|
||||||
|
'operations.manage','e.manage','plugins.manage','audit.read','logs.read',
|
||||||
|
'backups.manage','updates.manage','settings.manage','licenses.manage'
|
||||||
|
))
|
||||||
|
);
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE store_settings (
|
||||||
|
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
||||||
|
title text NOT NULL DEFAULT '会员商店',
|
||||||
|
introduction text NOT NULL DEFAULT '',
|
||||||
|
notice text NOT NULL DEFAULT '',
|
||||||
|
enabled boolean NOT NULL DEFAULT true,
|
||||||
|
allow_point_purchase boolean NOT NULL DEFAULT true,
|
||||||
|
updated_by text NOT NULL DEFAULT '',
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO store_settings(singleton) VALUES(true) ON CONFLICT (singleton) DO NOTHING;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE telegram_bot_settings
|
||||||
|
ADD COLUMN command_auto_delete_seconds integer NOT NULL DEFAULT 0
|
||||||
|
CHECK (command_auto_delete_seconds BETWEEN 0 AND 3600);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE telegram_bot_settings
|
||||||
|
ALTER COLUMN command_auto_delete_seconds SET DEFAULT 60;
|
||||||
|
|
||||||
|
-- The preceding migration introduced this setting with a disabled default.
|
||||||
|
-- Enable the requested cleanup for existing installations while retaining 0
|
||||||
|
-- as the explicit opt-out value in the administration UI.
|
||||||
|
UPDATE telegram_bot_settings
|
||||||
|
SET command_auto_delete_seconds = 60
|
||||||
|
WHERE singleton AND command_auto_delete_seconds = 0;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- WebUI refresh-token sessions are account sessions, not playback devices.
|
||||||
|
-- Retire legacy rows created by the old login path only when they have never
|
||||||
|
-- been used to reserve or play media, preserving real browser playback data.
|
||||||
|
UPDATE devices device
|
||||||
|
SET revoked_at = COALESCE(device.revoked_at, now())
|
||||||
|
WHERE device.revoked_at IS NULL
|
||||||
|
AND lower(device.client_name) LIKE '%webui%'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM playback_sessions session WHERE session.device_id = device.id
|
||||||
|
);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE auth_login_failures (
|
||||||
|
subject_type text NOT NULL CHECK (subject_type IN ('account','ip')),
|
||||||
|
subject_hash text NOT NULL,
|
||||||
|
failed_count integer NOT NULL DEFAULT 0 CHECK (failed_count >= 0),
|
||||||
|
window_started_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
locked_until timestamptz,
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (subject_type, subject_hash)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX auth_login_failures_cleanup_idx ON auth_login_failures(updated_at);
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS plugin_user_policies (
|
||||||
|
user_id uuid PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
is_disabled boolean NOT NULL DEFAULT false,
|
||||||
|
is_administrator boolean NOT NULL DEFAULT false,
|
||||||
|
allow_video_playback boolean NOT NULL DEFAULT true,
|
||||||
|
allow_audio_playback boolean NOT NULL DEFAULT true,
|
||||||
|
allow_content_downloading boolean NOT NULL DEFAULT false,
|
||||||
|
enable_remote_access boolean NOT NULL DEFAULT true,
|
||||||
|
enabled_library_ids text[] NOT NULL DEFAULT '{}',
|
||||||
|
blocked_library_ids text[] NOT NULL DEFAULT '{}',
|
||||||
|
max_concurrent_streams integer NOT NULL DEFAULT 0 CHECK (max_concurrent_streams >= 0),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE playback_decisions
|
||||||
|
ADD COLUMN IF NOT EXISTS media_type text NOT NULL DEFAULT '' CHECK (media_type IN ('','video','audio'));
|
||||||
|
ALTER TABLE playback_sessions
|
||||||
|
ADD COLUMN IF NOT EXISTS media_type text NOT NULL DEFAULT '' CHECK (media_type IN ('','video','audio'));
|
||||||
|
CREATE INDEX IF NOT EXISTS playback_sessions_plugin_report_idx ON playback_sessions(created_at DESC,user_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_group_media_policies (
|
||||||
|
group_id uuid PRIMARY KEY REFERENCES user_groups(id) ON DELETE CASCADE,
|
||||||
|
enabled_library_ids text[] NOT NULL DEFAULT '{}',
|
||||||
|
blocked_library_ids text[] NOT NULL DEFAULT '{}',
|
||||||
|
updated_by uuid REFERENCES users(id),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CHECK (cardinality(enabled_library_ids) <= 500),
|
||||||
|
CHECK (cardinality(blocked_library_ids) <= 500)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE invite_codes
|
||||||
|
ADD COLUMN IF NOT EXISTS owner_reward_points integer NOT NULL DEFAULT 0 CHECK (owner_reward_points >= 0),
|
||||||
|
ADD COLUMN IF NOT EXISTS owner_reward_membership_days integer NOT NULL DEFAULT 0 CHECK (owner_reward_membership_days >= 0),
|
||||||
|
ADD COLUMN IF NOT EXISTS redeemer_reward_points integer NOT NULL DEFAULT 0 CHECK (redeemer_reward_points >= 0),
|
||||||
|
ADD COLUMN IF NOT EXISTS redeemer_reward_membership_days integer NOT NULL DEFAULT 0 CHECK (redeemer_reward_membership_days >= 0);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS invite_redemptions (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
invite_id uuid NOT NULL REFERENCES invite_codes(id) ON DELETE CASCADE,
|
||||||
|
redeemed_by_user_id uuid NOT NULL REFERENCES users(id),
|
||||||
|
idempotency_key text NOT NULL CHECK (char_length(idempotency_key) BETWEEN 1 AND 256),
|
||||||
|
owner_reward_points integer NOT NULL DEFAULT 0,
|
||||||
|
owner_reward_membership_days integer NOT NULL DEFAULT 0,
|
||||||
|
redeemer_reward_points integer NOT NULL DEFAULT 0,
|
||||||
|
redeemer_reward_membership_days integer NOT NULL DEFAULT 0,
|
||||||
|
redeemed_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (invite_id, redeemed_by_user_id),
|
||||||
|
UNIQUE (invite_id, idempotency_key)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS invite_redemptions_user_time_idx ON invite_redemptions(redeemed_by_user_id,redeemed_at DESC);
|
||||||
|
|
||||||
|
INSERT INTO membership_plans(code,name,duration_days,max_playbacks,max_devices,max_bitrate_kbps,price_points,self_service,enabled)
|
||||||
|
VALUES('points-7d','7 天会员',7,1,3,0,30,true,true)
|
||||||
|
ON CONFLICT(code) DO NOTHING;
|
||||||
|
|
||||||
|
ALTER TABLE point_ledger DROP CONSTRAINT IF EXISTS point_ledger_source_type_check;
|
||||||
|
ALTER TABLE point_ledger ADD CONSTRAINT point_ledger_source_type_check CHECK (source_type IN (
|
||||||
|
'manual','playback_session','media_feedback','redemption','daily_checkin',
|
||||||
|
'membership_purchase','telegram_red_packet','invite_reward'
|
||||||
|
));
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS plugin_telegram_contexts (
|
||||||
|
token_hash bytea PRIMARY KEY,
|
||||||
|
plugin_id text NOT NULL,
|
||||||
|
tenant_id text NOT NULL,
|
||||||
|
telegram_user_id bigint NOT NULL,
|
||||||
|
chat_id bigint NOT NULL,
|
||||||
|
telegram_username text NOT NULL DEFAULT '',
|
||||||
|
expires_at timestamptz NOT NULL,
|
||||||
|
consumed_at timestamptz,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CHECK (expires_at > created_at)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS plugin_telegram_contexts_expiry_idx ON plugin_telegram_contexts(expires_at) WHERE consumed_at IS NULL;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS plugin_telegram_sends (
|
||||||
|
plugin_id text NOT NULL,
|
||||||
|
tenant_id text NOT NULL,
|
||||||
|
action text NOT NULL CHECK (action IN ('send_photo','send_document')),
|
||||||
|
idempotency_key text NOT NULL CHECK (char_length(idempotency_key) BETWEEN 1 AND 256),
|
||||||
|
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','sent')),
|
||||||
|
message_id bigint NOT NULL DEFAULT 0,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (plugin_id,tenant_id,action,idempotency_key)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS plugin_telegram_sends_pending_idx ON plugin_telegram_sends(updated_at) WHERE status='pending';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS plugin_audit_records (
|
||||||
|
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
plugin_id text NOT NULL,
|
||||||
|
plugin_version text NOT NULL,
|
||||||
|
tenant_id text NOT NULL,
|
||||||
|
actor_type text NOT NULL,
|
||||||
|
actor_id text NOT NULL,
|
||||||
|
action text NOT NULL,
|
||||||
|
target_type text NOT NULL,
|
||||||
|
target_id text NOT NULL DEFAULT '',
|
||||||
|
request_id text NOT NULL DEFAULT '',
|
||||||
|
trace_id text NOT NULL DEFAULT '',
|
||||||
|
result text NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS plugin_audit_records_plugin_time_idx ON plugin_audit_records(plugin_id,tenant_id,created_at DESC,id DESC);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE telegram_membership_signup_flows (
|
||||||
|
token_hash text PRIMARY KEY,
|
||||||
|
actor_user_id uuid NOT NULL REFERENCES users(id),
|
||||||
|
telegram_user_id bigint NOT NULL,
|
||||||
|
telegram_username text NOT NULL DEFAULT '',
|
||||||
|
started_at timestamptz,
|
||||||
|
consumed_at timestamptz,
|
||||||
|
expires_at timestamptz NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX telegram_membership_signup_flows_active_target_idx
|
||||||
|
ON telegram_membership_signup_flows(telegram_user_id)
|
||||||
|
WHERE consumed_at IS NULL;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
ALTER TABLE plugin_telegram_contexts
|
||||||
|
ADD COLUMN IF NOT EXISTS message_id bigint NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS chat_type text NOT NULL DEFAULT 'private',
|
||||||
|
ADD COLUMN IF NOT EXISTS inbound_media jsonb NOT NULL DEFAULT '[]'::jsonb;
|
||||||
|
|
||||||
|
ALTER TABLE plugin_telegram_sends DROP CONSTRAINT IF EXISTS plugin_telegram_sends_action_check;
|
||||||
|
ALTER TABLE plugin_telegram_sends ADD CONSTRAINT plugin_telegram_sends_action_check
|
||||||
|
CHECK (action IN ('send_message','send_photo','send_document','resend_inbound_media','bot_api','chat_api'));
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE plugin_telegram_contexts
|
||||||
|
ADD COLUMN IF NOT EXISTS chat_type text NOT NULL DEFAULT 'private';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE plugin_telegram_sends DROP CONSTRAINT IF EXISTS plugin_telegram_sends_action_check;
|
||||||
|
ALTER TABLE plugin_telegram_sends ADD CONSTRAINT plugin_telegram_sends_action_check
|
||||||
|
CHECK (action IN ('send_message','send_photo','send_document','resend_inbound_media','bot_api','chat_api'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE telegram_membership_signup_flows
|
||||||
|
ADD COLUMN membership_duration_days integer NOT NULL DEFAULT 30
|
||||||
|
CHECK (membership_duration_days BETWEEN 1 AND 365000);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
CREATE TABLE telegram_group_members (
|
||||||
|
chat_id bigint NOT NULL,
|
||||||
|
telegram_user_id bigint NOT NULL,
|
||||||
|
username text NOT NULL DEFAULT '',
|
||||||
|
first_seen_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
last_seen_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (chat_id, telegram_user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE telegram_group_cleanup_flows (
|
||||||
|
token_hash text PRIMARY KEY,
|
||||||
|
actor_user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
chat_id bigint NOT NULL,
|
||||||
|
expires_at timestamptz NOT NULL,
|
||||||
|
consumed_at timestamptz,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX telegram_group_cleanup_flows_active_idx
|
||||||
|
ON telegram_group_cleanup_flows(actor_user_id, chat_id)
|
||||||
|
WHERE consumed_at IS NULL;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- A disabled row records binding history only. It must not reserve the numeric
|
||||||
|
-- Telegram identity forever, otherwise a user who unbinds cannot bind a newly
|
||||||
|
-- registered panel account.
|
||||||
|
ALTER TABLE telegram_accounts DROP CONSTRAINT IF EXISTS telegram_accounts_telegram_user_id_key;
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS telegram_accounts_telegram_user_id_active_idx
|
||||||
|
ON telegram_accounts(telegram_user_id)
|
||||||
|
WHERE disabled_at IS NULL;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
CREATE TABLE storage_webhook_events (
|
||||||
|
event_id text PRIMARY KEY,
|
||||||
|
library_id uuid NOT NULL,
|
||||||
|
event_type text NOT NULL,
|
||||||
|
paths jsonb NOT NULL,
|
||||||
|
task_ids jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||||
|
state text NOT NULL CHECK (state IN ('processing','accepted','failed')),
|
||||||
|
error_code text NOT NULL DEFAULT '',
|
||||||
|
received_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
completed_at timestamptz
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX storage_webhook_events_received_idx ON storage_webhook_events(received_at DESC);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE security_list_entries DROP CONSTRAINT security_list_entries_subject_type_check;
|
||||||
|
ALTER TABLE security_list_entries ADD CONSTRAINT security_list_entries_subject_type_check
|
||||||
|
CHECK (subject_type IN ('ip','cidr','device','user','user_agent'));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE security_list_entries DROP CONSTRAINT security_list_entries_subject_type_check;
|
||||||
|
ALTER TABLE security_list_entries ADD CONSTRAINT security_list_entries_subject_type_check
|
||||||
|
CHECK (subject_type IN ('ip','cidr','device','user','user_agent','client_name','client_version'));
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
CREATE TABLE api_keys (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
name text NOT NULL CHECK (length(btrim(name)) BETWEEN 1 AND 128),
|
||||||
|
prefix text NOT NULL UNIQUE CHECK (prefix ~ '^oc_[a-z0-9]{8}$'),
|
||||||
|
key_hash bytea NOT NULL UNIQUE,
|
||||||
|
expires_at timestamptz,
|
||||||
|
enabled boolean NOT NULL DEFAULT true,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
last_used_at timestamptz
|
||||||
|
);
|
||||||
|
CREATE INDEX api_keys_user_idx ON api_keys(user_id,created_at DESC);
|
||||||
|
CREATE INDEX api_keys_active_idx ON api_keys(prefix) WHERE enabled;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE playback_sessions ADD COLUMN IF NOT EXISTS bytes_served bigint NOT NULL DEFAULT 0 CHECK (bytes_served >= 0);
|
||||||
|
ALTER TABLE playback_sessions ADD COLUMN IF NOT EXISTS estimated_bytes bigint NOT NULL DEFAULT 0 CHECK (estimated_bytes >= 0);
|
||||||
|
CREATE INDEX IF NOT EXISTS playback_sessions_traffic_user_idx ON playback_sessions(user_id, updated_at DESC);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE playback_sessions ADD COLUMN IF NOT EXISTS estimated_bytes bigint NOT NULL DEFAULT 0 CHECK (estimated_bytes >= 0);
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
CREATE TABLE agent_settings (
|
||||||
|
id smallint PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
||||||
|
enabled boolean NOT NULL DEFAULT false,
|
||||||
|
provider_name text NOT NULL DEFAULT '',
|
||||||
|
provider_type text NOT NULL DEFAULT 'openai_compatible' CHECK (provider_type IN ('openai_compatible','openai','custom_http')),
|
||||||
|
base_url text NOT NULL DEFAULT '',
|
||||||
|
model text NOT NULL DEFAULT '',
|
||||||
|
encrypted_api_key bytea,
|
||||||
|
timeout_ms integer NOT NULL DEFAULT 30000 CHECK (timeout_ms BETWEEN 1000 AND 120000),
|
||||||
|
max_tokens integer NOT NULL DEFAULT 2048 CHECK (max_tokens BETWEEN 1 AND 32768),
|
||||||
|
temperature double precision NOT NULL DEFAULT 0.2 CHECK (temperature BETWEEN 0 AND 2),
|
||||||
|
system_prompt text NOT NULL DEFAULT '',
|
||||||
|
max_history integer NOT NULL DEFAULT 20 CHECK (max_history BETWEEN 1 AND 100),
|
||||||
|
rate_limit integer NOT NULL DEFAULT 30 CHECK (rate_limit BETWEEN 1 AND 600),
|
||||||
|
request_logging boolean NOT NULL DEFAULT false,
|
||||||
|
tool_enabled boolean NOT NULL DEFAULT true,
|
||||||
|
confirmation_required boolean NOT NULL DEFAULT true,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_by uuid REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE agent_sessions (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
admin_user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
title text NOT NULL DEFAULT 'OverCooked Admin Agent',
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
expires_at timestamptz NOT NULL DEFAULT (now() + interval '24 hours')
|
||||||
|
);
|
||||||
|
CREATE INDEX agent_sessions_admin_idx ON agent_sessions(admin_user_id, updated_at DESC);
|
||||||
|
|
||||||
|
CREATE TABLE agent_messages (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
session_id uuid NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
|
||||||
|
role text NOT NULL CHECK (role IN ('system','user','assistant','tool')),
|
||||||
|
content text NOT NULL,
|
||||||
|
tool_name text,
|
||||||
|
tool_call_id text,
|
||||||
|
confirmation_state text NOT NULL DEFAULT 'none' CHECK (confirmation_state IN ('none','pending','confirmed','cancelled','expired')),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX agent_messages_session_idx ON agent_messages(session_id, created_at);
|
||||||
|
|
||||||
|
CREATE TABLE agent_audit (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
session_id uuid REFERENCES agent_sessions(id) ON DELETE SET NULL,
|
||||||
|
admin_user_id uuid NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||||
|
capability text NOT NULL,
|
||||||
|
action text NOT NULL,
|
||||||
|
target text NOT NULL DEFAULT '',
|
||||||
|
request_summary text NOT NULL DEFAULT '',
|
||||||
|
result jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
status text NOT NULL CHECK (status IN ('planned','pending_confirmation','success','cancelled','failed')),
|
||||||
|
error_code text NOT NULL DEFAULT '',
|
||||||
|
trace_id text NOT NULL DEFAULT '',
|
||||||
|
latency_ms bigint NOT NULL DEFAULT 0,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX agent_audit_admin_idx ON agent_audit(admin_user_id, created_at DESC);
|
||||||
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
cd "$root"
|
||||||
|
|
||||||
|
sh -n install.sh
|
||||||
|
sh -n s-migrate.sh
|
||||||
|
|
||||||
|
if rg -n 'git[[:space:]]+clone|OverCooked-S\.git|Overcooked-E\.git|Authorization:[[:space:]]*Bearer' . --glob '!.git/**'; then
|
||||||
|
echo 'forbidden source checkout or authorization header found' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
test -f compose/s.yml
|
||||||
|
test -f compose/e.yml
|
||||||
|
test -f config/e-server.production.yaml
|
||||||
|
test -f s-migrations.tar.gz
|
||||||
|
test "$(find s-migrations -type f -name '*.up.sql' | wc -l)" -gt 0
|
||||||
|
! rg -n '^([[:space:]]*)build:' compose
|
||||||
|
! rg -n 'overcooked-auth|overcooked-auth-webui' compose install.sh
|
||||||
|
echo 'static customer installer checks passed'
|
||||||
|
|
||||||
Reference in new issue
Block a user