fix(deploy): remove private source checkout from customer installer

This commit is contained in:
root committed 2026-09-04 13:53:46 +00:00
1 parent d33ec5c47d
commit e06a43c76f
68 files changed
+2374 -71

No files matched your search

+19
View File
@@ -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.
+90 -4
View File
@@ -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.
+6
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
2026.09.04
+126
View File
@@ -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
View File
@@ -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:
+35
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+81
View File
@@ -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');
+64
View File
@@ -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);
+48
View File
@@ -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)
);
+9
View File
@@ -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);
+20
View File
@@ -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;
+34
View File
@@ -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'
))
);
+12
View File
@@ -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);
+104
View File
@@ -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'));
+13
View File
@@ -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);
+60
View File
@@ -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);
+23
View File
@@ -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'