initial commit
This commit is contained in:
169
dev/.env.example
Normal file
169
dev/.env.example
Normal file
@@ -0,0 +1,169 @@
|
||||
NODE_ENV=development
|
||||
|
||||
FLUXER_API_PUBLIC_ENDPOINT=http://127.0.0.1:8088/api
|
||||
FLUXER_API_CLIENT_ENDPOINT=
|
||||
FLUXER_APP_ENDPOINT=http://localhost:8088
|
||||
FLUXER_GATEWAY_ENDPOINT=ws://127.0.0.1:8088/gateway
|
||||
FLUXER_MEDIA_ENDPOINT=http://127.0.0.1:8088/media
|
||||
FLUXER_CDN_ENDPOINT=https://fluxerstatic.com
|
||||
FLUXER_MARKETING_ENDPOINT=http://127.0.0.1:8088
|
||||
FLUXER_ADMIN_ENDPOINT=http://127.0.0.1:8088
|
||||
FLUXER_INVITE_ENDPOINT=http://fluxer.gg
|
||||
FLUXER_GIFT_ENDPOINT=http://fluxer.gift
|
||||
FLUXER_API_HOST=api:8080
|
||||
|
||||
FLUXER_API_PORT=8080
|
||||
FLUXER_GATEWAY_WS_PORT=8080
|
||||
FLUXER_GATEWAY_RPC_PORT=8081
|
||||
FLUXER_MEDIA_PROXY_PORT=8080
|
||||
FLUXER_ADMIN_PORT=8080
|
||||
FLUXER_MARKETING_PORT=8080
|
||||
FLUXER_GEOIP_PORT=8080
|
||||
GEOIP_HOST=geoip:8080
|
||||
|
||||
FLUXER_PATH_GATEWAY=/gateway
|
||||
FLUXER_PATH_ADMIN=/admin
|
||||
FLUXER_PATH_MARKETING=/marketing
|
||||
|
||||
API_HOST=api:8080
|
||||
FLUXER_GATEWAY_RPC_HOST=
|
||||
FLUXER_GATEWAY_PUSH_ENABLED=false
|
||||
FLUXER_GATEWAY_PUSH_USER_GUILD_SETTINGS_CACHE_MB=1024
|
||||
FLUXER_GATEWAY_PUSH_SUBSCRIPTIONS_CACHE_MB=1024
|
||||
FLUXER_GATEWAY_PUSH_BLOCKED_IDS_CACHE_MB=1024
|
||||
FLUXER_GATEWAY_IDENTIFY_RATE_LIMIT_ENABLED=false
|
||||
|
||||
FLUXER_MEDIA_PROXY_HOST=
|
||||
MEDIA_PROXY_ENDPOINT=
|
||||
|
||||
VAPID_PUBLIC_KEY=
|
||||
VAPID_PRIVATE_KEY=
|
||||
VAPID_EMAIL=support@fluxer.app
|
||||
|
||||
SUDO_MODE_SECRET=
|
||||
PASSKEYS_ENABLED=true
|
||||
PASSKEY_RP_NAME=Fluxer
|
||||
PASSKEY_RP_ID=127.0.0.1
|
||||
PASSKEY_ALLOWED_ORIGINS=http://127.0.0.1:8088,http://localhost:8088
|
||||
|
||||
ADMIN_OAUTH2_CLIENT_ID=
|
||||
ADMIN_OAUTH2_CLIENT_SECRET=
|
||||
ADMIN_OAUTH2_AUTO_CREATE=false
|
||||
ADMIN_OAUTH2_REDIRECT_URI=http://127.0.0.1:8088/admin/oauth2_callback
|
||||
|
||||
RELEASE_CHANNEL=stable
|
||||
|
||||
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/fluxer
|
||||
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
CASSANDRA_HOSTS=cassandra
|
||||
CASSANDRA_KEYSPACE=fluxer
|
||||
CASSANDRA_LOCAL_DC=datacenter1
|
||||
CASSANDRA_USERNAME=cassandra
|
||||
CASSANDRA_PASSWORD=cassandra
|
||||
|
||||
AWS_S3_ENDPOINT=http://minio:9000
|
||||
AWS_ACCESS_KEY_ID=minioadmin
|
||||
AWS_SECRET_ACCESS_KEY=minioadmin
|
||||
|
||||
AWS_S3_BUCKET_CDN=fluxer
|
||||
AWS_S3_BUCKET_UPLOADS=fluxer-uploads
|
||||
AWS_S3_BUCKET_DOWNLOADS=fluxer-downloads
|
||||
AWS_S3_BUCKET_REPORTS=fluxer-reports
|
||||
AWS_S3_BUCKET_HARVESTS=fluxer-harvests
|
||||
|
||||
R2_S3_ENDPOINT=http://minio:9000
|
||||
R2_ACCESS_KEY_ID=minioadmin
|
||||
R2_SECRET_ACCESS_KEY=minioadmin
|
||||
|
||||
METRICS_MODE=noop
|
||||
|
||||
CLICKHOUSE_URL=http://clickhouse:8123
|
||||
CLICKHOUSE_DATABASE=fluxer_metrics
|
||||
CLICKHOUSE_USER=fluxer
|
||||
CLICKHOUSE_PASSWORD=fluxer_dev
|
||||
|
||||
ANOMALY_DETECTION_ENABLED=true
|
||||
ANOMALY_WINDOW_SIZE=100
|
||||
ANOMALY_ZSCORE_THRESHOLD=3.0
|
||||
ANOMALY_CHECK_INTERVAL_SECS=60
|
||||
ANOMALY_COOLDOWN_SECS=300
|
||||
ANOMALY_ERROR_RATE_THRESHOLD=0.05
|
||||
ALERT_WEBHOOK_URL=
|
||||
|
||||
EMAIL_ENABLED=false
|
||||
SENDGRID_FROM_EMAIL=noreply@fluxer.app
|
||||
SENDGRID_FROM_NAME=Fluxer
|
||||
SENDGRID_API_KEY=
|
||||
SENDGRID_WEBHOOK_PUBLIC_KEY=
|
||||
|
||||
SMS_ENABLED=false
|
||||
TWILIO_ACCOUNT_SID=
|
||||
TWILIO_AUTH_TOKEN=
|
||||
TWILIO_VERIFY_SERVICE_SID=
|
||||
|
||||
CAPTCHA_ENABLED=true
|
||||
CAPTCHA_PRIMARY_PROVIDER=turnstile
|
||||
|
||||
HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001
|
||||
HCAPTCHA_PUBLIC_SITE_KEY=10000000-ffff-ffff-ffff-000000000001
|
||||
HCAPTCHA_SECRET_KEY=0x0000000000000000000000000000000000000000
|
||||
|
||||
TURNSTILE_SITE_KEY=1x00000000000000000000AA
|
||||
TURNSTILE_PUBLIC_SITE_KEY=1x00000000000000000000AA
|
||||
TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
|
||||
|
||||
SEARCH_ENABLED=true
|
||||
MEILISEARCH_URL=http://meilisearch:7700
|
||||
MEILISEARCH_API_KEY=masterKey
|
||||
|
||||
STRIPE_ENABLED=false
|
||||
STRIPE_SECRET_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
STRIPE_PRICE_ID_MONTHLY_USD=
|
||||
STRIPE_PRICE_ID_MONTHLY_EUR=
|
||||
STRIPE_PRICE_ID_YEARLY_USD=
|
||||
STRIPE_PRICE_ID_YEARLY_EUR=
|
||||
STRIPE_PRICE_ID_VISIONARY_USD=
|
||||
STRIPE_PRICE_ID_VISIONARY_EUR=
|
||||
STRIPE_PRICE_ID_GIFT_VISIONARY_USD=
|
||||
STRIPE_PRICE_ID_GIFT_VISIONARY_EUR=
|
||||
STRIPE_PRICE_ID_GIFT_1_MONTH_USD=
|
||||
STRIPE_PRICE_ID_GIFT_1_MONTH_EUR=
|
||||
STRIPE_PRICE_ID_GIFT_1_YEAR_USD=
|
||||
STRIPE_PRICE_ID_GIFT_1_YEAR_EUR=
|
||||
|
||||
CLOUDFLARE_PURGE_ENABLED=false
|
||||
CLOUDFLARE_ZONE_ID=
|
||||
CLOUDFLARE_API_TOKEN=
|
||||
CLOUDFLARE_TUNNEL_TOKEN=
|
||||
|
||||
VOICE_ENABLED=true
|
||||
LIVEKIT_API_KEY=
|
||||
LIVEKIT_API_SECRET=
|
||||
LIVEKIT_WEBHOOK_URL=http://api:8080/webhooks/livekit
|
||||
LIVEKIT_AUTO_CREATE_DUMMY_DATA=true
|
||||
|
||||
CLAMAV_ENABLED=false
|
||||
CLAMAV_HOST=clamav
|
||||
CLAMAV_PORT=3310
|
||||
|
||||
TENOR_API_KEY=
|
||||
YOUTUBE_API_KEY=
|
||||
IPINFO_TOKEN=
|
||||
|
||||
SECRET_KEY_BASE=
|
||||
GATEWAY_RPC_SECRET=
|
||||
GATEWAY_ADMIN_SECRET=
|
||||
ERLANG_COOKIE=fluxer_dev_cookie
|
||||
MEDIA_PROXY_SECRET_KEY=
|
||||
|
||||
SELF_HOSTED=false
|
||||
AUTO_JOIN_INVITE_CODE=
|
||||
FLUXER_VISIONARIES_GUILD_ID=
|
||||
FLUXER_OPERATORS_GUILD_ID=
|
||||
|
||||
GIT_SHA=dev
|
||||
BUILD_TIMESTAMP=
|
||||
66
dev/Caddyfile.dev
Normal file
66
dev/Caddyfile.dev
Normal file
@@ -0,0 +1,66 @@
|
||||
:8088 {
|
||||
encode zstd gzip
|
||||
|
||||
@api path /api/*
|
||||
handle @api {
|
||||
handle_path /api/* {
|
||||
reverse_proxy api:8080
|
||||
}
|
||||
}
|
||||
|
||||
@media path /media/*
|
||||
handle @media {
|
||||
handle_path /media/* {
|
||||
reverse_proxy media:8080
|
||||
}
|
||||
}
|
||||
|
||||
@s3 path /s3/*
|
||||
handle @s3 {
|
||||
handle_path /s3/* {
|
||||
reverse_proxy minio:9000
|
||||
}
|
||||
}
|
||||
|
||||
@admin path /admin /admin/*
|
||||
handle @admin {
|
||||
uri strip_prefix /admin
|
||||
reverse_proxy admin:8080
|
||||
}
|
||||
|
||||
@geoip path /geoip/*
|
||||
handle @geoip {
|
||||
handle_path /geoip/* {
|
||||
reverse_proxy geoip:8080
|
||||
}
|
||||
}
|
||||
|
||||
@marketing path /marketing /marketing/*
|
||||
handle @marketing {
|
||||
uri strip_prefix /marketing
|
||||
reverse_proxy marketing:8080
|
||||
}
|
||||
|
||||
@gateway path /gateway /gateway/*
|
||||
handle @gateway {
|
||||
uri strip_prefix /gateway
|
||||
reverse_proxy gateway:8080
|
||||
}
|
||||
|
||||
@livekit path /livekit /livekit/*
|
||||
handle @livekit {
|
||||
handle_path /livekit/* {
|
||||
reverse_proxy livekit:7880
|
||||
}
|
||||
}
|
||||
|
||||
@metrics path /metrics /metrics/*
|
||||
handle @metrics {
|
||||
uri strip_prefix /metrics
|
||||
reverse_proxy metrics:8080
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy host.docker.internal:3000
|
||||
}
|
||||
}
|
||||
160
dev/compose.data.yaml
Normal file
160
dev/compose.data.yaml
Normal file
@@ -0,0 +1,160 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: fluxer
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
cassandra:
|
||||
image: scylladb/scylla:latest
|
||||
command: --smp 1 --memory 512M --overprovisioned 1 --developer-mode 1 --api-address 0.0.0.0
|
||||
ports:
|
||||
- '9042:9042'
|
||||
volumes:
|
||||
- scylla_data:/var/lib/scylla
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'cqlsh -e "describe cluster"']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 90s
|
||||
|
||||
redis:
|
||||
image: valkey/valkey:latest
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: valkey-server --save 60 1 --loglevel warning
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
minio:
|
||||
image: minio/minio
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: minioadmin
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test: ['CMD', 'mc', 'ready', 'local']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
minio-setup:
|
||||
image: minio/mc
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
mc alias set minio http://minio:9000 minioadmin minioadmin;
|
||||
mc mb --ignore-existing minio/fluxer-metrics;
|
||||
mc mb --ignore-existing minio/fluxer-uploads;
|
||||
exit 0;
|
||||
"
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: 'no'
|
||||
|
||||
clamav:
|
||||
image: clamav/clamav:latest
|
||||
volumes:
|
||||
- clamav_data:/var/lib/clamav
|
||||
environment:
|
||||
CLAMAV_NO_FRESHCLAMD: 'false'
|
||||
CLAMAV_NO_CLAMD: 'false'
|
||||
CLAMAV_NO_MILTERD: 'true'
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test: ['CMD', '/usr/local/bin/clamdcheck.sh']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 300s
|
||||
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1.25.0
|
||||
volumes:
|
||||
- meilisearch_data:/meili_data
|
||||
environment:
|
||||
MEILI_ENV: development
|
||||
MEILI_MASTER_KEY: masterKey
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
livekit:
|
||||
image: livekit/livekit-server:latest
|
||||
command: --config /etc/livekit.yaml --dev
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
- ./livekit.yaml:/etc/livekit.yaml:ro
|
||||
ports:
|
||||
- '7880:7880'
|
||||
- '7882:7882/udp'
|
||||
- '7999:7999/udp'
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:24.8
|
||||
hostname: clickhouse
|
||||
profiles:
|
||||
- clickhouse
|
||||
environment:
|
||||
- CLICKHOUSE_DB=fluxer_metrics
|
||||
- CLICKHOUSE_USER=fluxer
|
||||
- CLICKHOUSE_PASSWORD=fluxer_dev
|
||||
- CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1
|
||||
volumes:
|
||||
- clickhouse_data:/var/lib/clickhouse
|
||||
- clickhouse_logs:/var/log/clickhouse-server
|
||||
networks:
|
||||
- fluxer-shared
|
||||
ports:
|
||||
- '8123:8123'
|
||||
- '9000:9000'
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test: ['CMD', 'clickhouse-client', '--query', 'SELECT 1']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 262144
|
||||
hard: 262144
|
||||
|
||||
networks:
|
||||
fluxer-shared:
|
||||
name: fluxer-shared
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
scylla_data:
|
||||
redis_data:
|
||||
minio_data:
|
||||
clamav_data:
|
||||
meilisearch_data:
|
||||
clickhouse_data:
|
||||
clickhouse_logs:
|
||||
398
dev/compose.yaml
Normal file
398
dev/compose.yaml
Normal file
@@ -0,0 +1,398 @@
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:2
|
||||
ports:
|
||||
- '8088:8088'
|
||||
volumes:
|
||||
- ./Caddyfile.dev:/etc/caddy/Caddyfile:ro
|
||||
- ../fluxer_app/dist:/app/dist:ro
|
||||
networks:
|
||||
- fluxer-shared
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
restart: on-failure
|
||||
|
||||
cloudflared:
|
||||
image: cloudflare/cloudflared:latest
|
||||
command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TUNNEL_TOKEN}
|
||||
env_file:
|
||||
- ./.env
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
api:
|
||||
image: node:24-bookworm-slim
|
||||
working_dir: /workspace
|
||||
command: bash -lc "corepack enable pnpm && CI=true pnpm install && npx tsx watch --clear-screen=false src/App.ts"
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- CI=true
|
||||
- VAPID_PUBLIC_KEY=BJHAPp7Xg4oeN_D6-EVu0D-bDyPDwFFJiLn7CzkUjUvaG_F-keQGpA_-RiNugCosTPhhdvdrn4mEOh-_1Bt35V8
|
||||
- FLUXER_METRICS_HOST=metrics:8080
|
||||
volumes:
|
||||
- ../fluxer_api:/workspace
|
||||
- api_node_modules:/workspace/node_modules
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
worker:
|
||||
image: node:24-bookworm-slim
|
||||
working_dir: /workspace
|
||||
command: bash -lc "corepack enable pnpm && CI=true pnpm install && npm run dev:worker"
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- CI=true
|
||||
- FLUXER_METRICS_HOST=metrics:8080
|
||||
volumes:
|
||||
- ../fluxer_api:/workspace
|
||||
- api_node_modules:/workspace/node_modules
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- cassandra
|
||||
|
||||
media:
|
||||
build:
|
||||
context: ../fluxer_media_proxy
|
||||
dockerfile: Dockerfile
|
||||
target: build
|
||||
working_dir: /workspace
|
||||
command: >
|
||||
bash -lc "
|
||||
corepack enable pnpm &&
|
||||
CI=true pnpm install &&
|
||||
pnpm dev
|
||||
"
|
||||
user: root
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- CI=true
|
||||
- NODE_ENV=development
|
||||
- FLUXER_METRICS_HOST=metrics:8080
|
||||
volumes:
|
||||
- ../fluxer_media_proxy:/workspace
|
||||
- media_node_modules:/workspace/node_modules
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
admin:
|
||||
build:
|
||||
context: ../fluxer_admin
|
||||
dockerfile: Dockerfile.dev
|
||||
working_dir: /workspace
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- PORT=8080
|
||||
- APP_MODE=admin
|
||||
- FLUXER_METRICS_HOST=metrics:8080
|
||||
volumes:
|
||||
- admin_build:/workspace/build
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
develop:
|
||||
watch:
|
||||
- action: rebuild
|
||||
path: ../fluxer_admin/src
|
||||
- action: rebuild
|
||||
path: ../fluxer_admin/tailwind.css
|
||||
|
||||
marketing:
|
||||
build:
|
||||
context: ../fluxer_marketing
|
||||
dockerfile: Dockerfile.dev
|
||||
working_dir: /workspace
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- PORT=8080
|
||||
- FLUXER_METRICS_HOST=metrics:8080
|
||||
volumes:
|
||||
- marketing_build:/workspace/build
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
develop:
|
||||
watch:
|
||||
- action: rebuild
|
||||
path: ../fluxer_marketing/src
|
||||
- action: rebuild
|
||||
path: ../fluxer_marketing/tailwind.css
|
||||
|
||||
docs:
|
||||
image: node:24-bookworm-slim
|
||||
working_dir: /workspace
|
||||
command: bash -lc "corepack enable pnpm && CI=true pnpm install && pnpm dev"
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- CI=true
|
||||
- NODE_ENV=development
|
||||
volumes:
|
||||
- ../fluxer_docs:/workspace
|
||||
- docs_node_modules:/workspace/node_modules
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
geoip:
|
||||
image: golang:1.25.5
|
||||
working_dir: /workspace
|
||||
command: bash -c "mkdir -p /data && if [ ! -f /data/ipinfo_lite.mmdb ] && [ -n \"$$IPINFO_TOKEN\" ]; then echo 'Downloading GeoIP database...'; curl -fsSL -o /data/ipinfo_lite.mmdb \"https://ipinfo.io/data/ipinfo_lite.mmdb?token=$$IPINFO_TOKEN\" && echo 'GeoIP database downloaded'; fi && go run ."
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
- ../fluxer_geoip:/workspace
|
||||
- ./geoip_data:/data
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
gateway:
|
||||
image: erlang:28-slim
|
||||
working_dir: /workspace
|
||||
command: bash -c "apt-get update && apt-get install -y --no-install-recommends build-essential linux-libc-dev curl ca-certificates gettext-base git && curl -fsSL https://github.com/erlang/rebar3/releases/download/3.24.0/rebar3 -o /usr/local/bin/rebar3 && chmod +x /usr/local/bin/rebar3 && rebar3 compile && exec ./docker-entrypoint.sh"
|
||||
hostname: gateway
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- RELEASE_NODE=fluxer_gateway@gateway
|
||||
- LOGGER_LEVEL=debug
|
||||
- CLUSTER_NAME=fluxer_gateway
|
||||
- CLUSTER_DISCOVERY_DNS=gateway
|
||||
- NODE_COOKIE=fluxer_dev_cookie
|
||||
- VAPID_PUBLIC_KEY=BJHAPp7Xg4oeN_D6-EVu0D-bDyPDwFFJiLn7CzkUjUvaG_F-keQGpA_-RiNugCosTPhhdvdrn4mEOh-_1Bt35V8
|
||||
- VAPID_PRIVATE_KEY=Ze8J4aSmwV5B77zz9NzTU_IdyFyR1hMiKaYF2G61Y-E
|
||||
- VAPID_EMAIL=support@fluxer.app
|
||||
- FLUXER_METRICS_HOST=metrics:8080
|
||||
volumes:
|
||||
- ../fluxer_gateway:/workspace
|
||||
- gateway_build:/workspace/_build
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
postgres:
|
||||
image: postgres:17
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: fluxer
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
cassandra:
|
||||
image: scylladb/scylla:latest
|
||||
command: --smp 1 --memory 512M --overprovisioned 1 --developer-mode 1 --api-address 0.0.0.0
|
||||
ports:
|
||||
- '9042:9042'
|
||||
volumes:
|
||||
- scylla_data:/var/lib/scylla
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'cqlsh -e "describe cluster"']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 90s
|
||||
|
||||
redis:
|
||||
image: valkey/valkey:latest
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: valkey-server --save 60 1 --loglevel warning
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
minio:
|
||||
image: minio/minio
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: minioadmin
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test: ['CMD', 'mc', 'ready', 'local']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
minio-setup:
|
||||
image: minio/mc
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
mc alias set minio http://minio:9000 minioadmin minioadmin;
|
||||
mc mb --ignore-existing minio/fluxer-metrics;
|
||||
mc mb --ignore-existing minio/fluxer-uploads;
|
||||
exit 0;
|
||||
"
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: 'no'
|
||||
|
||||
clamav:
|
||||
image: clamav/clamav:latest
|
||||
volumes:
|
||||
- clamav_data:/var/lib/clamav
|
||||
environment:
|
||||
CLAMAV_NO_FRESHCLAMD: 'false'
|
||||
CLAMAV_NO_CLAMD: 'false'
|
||||
CLAMAV_NO_MILTERD: 'true'
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test: ['CMD', '/usr/local/bin/clamdcheck.sh']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 300s
|
||||
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1.25.0
|
||||
volumes:
|
||||
- meilisearch_data:/meili_data
|
||||
environment:
|
||||
MEILI_ENV: development
|
||||
MEILI_MASTER_KEY: masterKey
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:24.8
|
||||
hostname: clickhouse
|
||||
profiles:
|
||||
- clickhouse
|
||||
environment:
|
||||
- CLICKHOUSE_DB=fluxer_metrics
|
||||
- CLICKHOUSE_USER=fluxer
|
||||
- CLICKHOUSE_PASSWORD=fluxer_dev
|
||||
- CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1
|
||||
volumes:
|
||||
- clickhouse_data:/var/lib/clickhouse
|
||||
- clickhouse_logs:/var/log/clickhouse-server
|
||||
networks:
|
||||
- fluxer-shared
|
||||
ports:
|
||||
- '8123:8123'
|
||||
- '9000:9000'
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test: ['CMD', 'clickhouse-client', '--query', 'SELECT 1']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 262144
|
||||
hard: 262144
|
||||
|
||||
metrics:
|
||||
build:
|
||||
context: ../fluxer_metrics
|
||||
dockerfile: Dockerfile
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- METRICS_PORT=8080
|
||||
- METRICS_MODE=${METRICS_MODE:-noop}
|
||||
- CLICKHOUSE_URL=http://clickhouse:8123
|
||||
- CLICKHOUSE_DATABASE=fluxer_metrics
|
||||
- CLICKHOUSE_USER=fluxer
|
||||
- CLICKHOUSE_PASSWORD=fluxer_dev
|
||||
- ANOMALY_DETECTION_ENABLED=true
|
||||
- FLUXER_ADMIN_ENDPOINT=${FLUXER_ADMIN_ENDPOINT:-}
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
metrics-clickhouse:
|
||||
extends:
|
||||
service: metrics
|
||||
profiles:
|
||||
- clickhouse
|
||||
environment:
|
||||
- METRICS_MODE=clickhouse
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
|
||||
cassandra-migrate:
|
||||
image: debian:bookworm-slim
|
||||
command:
|
||||
[
|
||||
'bash',
|
||||
'-lc',
|
||||
'apt-get update && apt-get install -y dnsutils && sleep 30 && /cassandra-migrate --host cassandra --username cassandra --password cassandra up',
|
||||
]
|
||||
working_dir: /workspace
|
||||
volumes:
|
||||
- ../scripts/cassandra-migrate/target/release/cassandra-migrate:/cassandra-migrate
|
||||
- ../fluxer_devops/cassandra/migrations:/workspace/fluxer_devops/cassandra/migrations
|
||||
networks:
|
||||
- fluxer-shared
|
||||
depends_on:
|
||||
cassandra:
|
||||
condition: service_healthy
|
||||
restart: 'no'
|
||||
|
||||
livekit:
|
||||
image: livekit/livekit-server:latest
|
||||
command: --config /etc/livekit.yaml --dev
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
- ./livekit.yaml:/etc/livekit.yaml:ro
|
||||
ports:
|
||||
- '7880:7880'
|
||||
- '7882:7882/udp'
|
||||
- '7999:7999/udp'
|
||||
networks:
|
||||
- fluxer-shared
|
||||
restart: on-failure
|
||||
|
||||
networks:
|
||||
fluxer-shared:
|
||||
name: fluxer-shared
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
scylla_data:
|
||||
redis_data:
|
||||
minio_data:
|
||||
clamav_data:
|
||||
meilisearch_data:
|
||||
clickhouse_data:
|
||||
clickhouse_logs:
|
||||
api_node_modules:
|
||||
media_node_modules:
|
||||
admin_build:
|
||||
marketing_build:
|
||||
gateway_build:
|
||||
docs_node_modules:
|
||||
34
dev/main.go
Normal file
34
dev/main.go
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"fluxer.dev/dev/pkg/commands"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := commands.NewRootCmd().Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
305
dev/pkg/commands/commands.go
Normal file
305
dev/pkg/commands/commands.go
Normal file
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"fluxer.dev/dev/pkg/integrations"
|
||||
"fluxer.dev/dev/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultComposeFile = "dev/compose.yaml"
|
||||
defaultEnvFile = "dev/.env"
|
||||
)
|
||||
|
||||
// NewRootCmd creates the root command
|
||||
func NewRootCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "devctl",
|
||||
Short: "Fluxer development control tool",
|
||||
Long: "Docker Compose wrapper and development utilities for Fluxer.",
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
NewUpCmd(),
|
||||
NewDownCmd(),
|
||||
NewRestartCmd(),
|
||||
NewLogsCmd(),
|
||||
NewPsCmd(),
|
||||
NewExecCmd(),
|
||||
NewShellCmd(),
|
||||
|
||||
NewLivekitSyncCmd(),
|
||||
NewGeoIPDownloadCmd(),
|
||||
NewEnsureNetworkCmd(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewUpCmd starts services
|
||||
func NewUpCmd() *cobra.Command {
|
||||
var detach bool
|
||||
var build bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "up [services...]",
|
||||
Short: "Start services",
|
||||
Long: "Start all or specific services using docker compose",
|
||||
RunE: func(cmd *cobra.Command, services []string) error {
|
||||
args := []string{"--env-file", defaultEnvFile, "-f", defaultComposeFile, "up"}
|
||||
if detach {
|
||||
args = append(args, "-d")
|
||||
}
|
||||
if build {
|
||||
args = append(args, "--build")
|
||||
}
|
||||
args = append(args, services...)
|
||||
return runDockerCompose(args...)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&detach, "detach", "d", true, "Run in background")
|
||||
cmd.Flags().BoolVar(&build, "build", false, "Build images before starting")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewDownCmd stops and removes containers
|
||||
func NewDownCmd() *cobra.Command {
|
||||
var volumes bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "down",
|
||||
Short: "Stop and remove containers",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
dcArgs := []string{"--env-file", defaultEnvFile, "-f", defaultComposeFile, "down"}
|
||||
if volumes {
|
||||
dcArgs = append(dcArgs, "-v")
|
||||
}
|
||||
return runDockerCompose(dcArgs...)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&volumes, "volumes", "v", false, "Remove volumes")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewRestartCmd restarts services
|
||||
func NewRestartCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "restart [services...]",
|
||||
Short: "Restart services",
|
||||
RunE: func(cmd *cobra.Command, services []string) error {
|
||||
args := []string{"--env-file", defaultEnvFile, "-f", defaultComposeFile, "restart"}
|
||||
args = append(args, services...)
|
||||
return runDockerCompose(args...)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewLogsCmd shows service logs
|
||||
func NewLogsCmd() *cobra.Command {
|
||||
var follow bool
|
||||
var tail string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "logs [services...]",
|
||||
Short: "Show service logs",
|
||||
RunE: func(cmd *cobra.Command, services []string) error {
|
||||
args := []string{"--env-file", defaultEnvFile, "-f", defaultComposeFile, "logs"}
|
||||
if follow {
|
||||
args = append(args, "-f")
|
||||
}
|
||||
if tail != "" {
|
||||
args = append(args, "--tail", tail)
|
||||
}
|
||||
args = append(args, services...)
|
||||
return runDockerCompose(args...)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&follow, "follow", "f", true, "Follow log output")
|
||||
cmd.Flags().StringVarP(&tail, "tail", "n", "100", "Number of lines to show from the end")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewPsCmd lists containers
|
||||
func NewPsCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "ps",
|
||||
Short: "List containers",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDockerCompose("--env-file", defaultEnvFile, "-f", defaultComposeFile, "ps")
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewExecCmd executes a command in a running container
|
||||
func NewExecCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "exec SERVICE COMMAND...",
|
||||
Short: "Execute a command in a running container",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
dcArgs := []string{"--env-file", defaultEnvFile, "-f", defaultComposeFile, "exec"}
|
||||
dcArgs = append(dcArgs, args...)
|
||||
return runDockerCompose(dcArgs...)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewShellCmd opens a shell in a container
|
||||
func NewShellCmd() *cobra.Command {
|
||||
var shell string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "sh SERVICE",
|
||||
Short: "Open a shell in a container",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
service := args[0]
|
||||
return runDockerCompose("--env-file", defaultEnvFile, "-f", defaultComposeFile, "exec", service, shell)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&shell, "shell", "sh", "Shell to use (sh, bash, etc.)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewLivekitSyncCmd syncs LiveKit configuration
|
||||
func NewLivekitSyncCmd() *cobra.Command {
|
||||
var envPath string
|
||||
var outputPath string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "livekit-sync",
|
||||
Short: "Generate LiveKit configuration from environment variables",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
env, err := utils.ParseEnvFile(envPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read env file: %w", err)
|
||||
}
|
||||
|
||||
written, err := integrations.WriteLivekitFileFromEnv(outputPath, env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !written {
|
||||
fmt.Println("⚠️ Voice/LiveKit is disabled - no config generated")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("✅ LiveKit config written to %s\n", outputPath)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&envPath, "env", "e", defaultEnvFile, "Environment file path")
|
||||
cmd.Flags().StringVarP(&outputPath, "output", "o", "dev/livekit.yaml", "Output path")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewGeoIPDownloadCmd downloads GeoIP database
|
||||
func NewGeoIPDownloadCmd() *cobra.Command {
|
||||
var token string
|
||||
var envPath string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "geoip-download",
|
||||
Short: "Download GeoIP database from IPInfo",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return integrations.DownloadGeoIP(token, envPath)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&token, "token", "", "IPInfo API token")
|
||||
cmd.Flags().StringVarP(&envPath, "env", "e", defaultEnvFile, "Env file to read token from")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewEnsureNetworkCmd ensures the Docker network exists
|
||||
func NewEnsureNetworkCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "ensure-network",
|
||||
Short: "Ensure the fluxer-shared Docker network exists",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return ensureNetwork()
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// runDockerCompose runs a docker compose command
|
||||
func runDockerCompose(args ...string) error {
|
||||
cmd := exec.Command("docker", append([]string{"compose"}, args...)...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// ensureNetwork ensures the fluxer-shared network exists
|
||||
func ensureNetwork() error {
|
||||
checkCmd := exec.Command("docker", "network", "ls", "--format", "{{.Name}}")
|
||||
output, err := checkCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list networks: %w", err)
|
||||
}
|
||||
|
||||
networks := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
for _, net := range networks {
|
||||
if net == "fluxer-shared" {
|
||||
fmt.Println("✅ fluxer-shared network already exists")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Creating fluxer-shared network...")
|
||||
createCmd := exec.Command("docker", "network", "create", "fluxer-shared")
|
||||
createCmd.Stdout = os.Stdout
|
||||
createCmd.Stderr = os.Stderr
|
||||
if err := createCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to create network: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("✅ fluxer-shared network created")
|
||||
return nil
|
||||
}
|
||||
95
dev/pkg/integrations/geoip.go
Normal file
95
dev/pkg/integrations/geoip.go
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package integrations
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"fluxer.dev/dev/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultGeoIPDir = "dev/geoip"
|
||||
DefaultGeoIPFile = "country_asn.mmdb"
|
||||
)
|
||||
|
||||
// DownloadGeoIP downloads the GeoIP database from IPInfo
|
||||
func DownloadGeoIP(tokenFlag, envPath string) error {
|
||||
token := strings.TrimSpace(tokenFlag)
|
||||
if token == "" {
|
||||
token = strings.TrimSpace(os.Getenv("IPINFO_TOKEN"))
|
||||
}
|
||||
|
||||
if token == "" && envPath != "" {
|
||||
env, err := utils.ParseEnvFile(envPath)
|
||||
if err == nil {
|
||||
token = strings.TrimSpace(env["IPINFO_TOKEN"])
|
||||
}
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
return errors.New("IPInfo token required; provide via --token, IPINFO_TOKEN env var, or the config/env")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(DefaultGeoIPDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
outPath := filepath.Join(DefaultGeoIPDir, DefaultGeoIPFile)
|
||||
u := fmt.Sprintf("https://ipinfo.io/data/free/country_asn.mmdb?token=%s", url.QueryEscape(token))
|
||||
|
||||
fmt.Printf("Downloading GeoIP database to %s...\n", outPath)
|
||||
|
||||
resp, err := http.Get(u)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download GeoIP db: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||
return fmt.Errorf("unexpected response (%d): %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
f, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
n, err := io.Copy(f, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return errors.New("downloaded GeoIP file is empty; check your IPInfo token")
|
||||
}
|
||||
|
||||
fmt.Printf("✅ GeoIP database downloaded (%d bytes).\n", n)
|
||||
fmt.Println()
|
||||
fmt.Println("If you're running a GeoIP service container, restart it so it picks up the new database.")
|
||||
return nil
|
||||
}
|
||||
82
dev/pkg/integrations/livekit.go
Normal file
82
dev/pkg/integrations/livekit.go
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package integrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WriteLivekitFileFromEnv writes LiveKit configuration from environment variables
|
||||
func WriteLivekitFileFromEnv(path string, env map[string]string) (bool, error) {
|
||||
voiceEnabled := strings.ToLower(strings.TrimSpace(env["VOICE_ENABLED"])) == "true"
|
||||
if !voiceEnabled {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
apiKey := strings.TrimSpace(env["LIVEKIT_API_KEY"])
|
||||
apiSecret := strings.TrimSpace(env["LIVEKIT_API_SECRET"])
|
||||
webhookURL := strings.TrimSpace(env["LIVEKIT_WEBHOOK_URL"])
|
||||
|
||||
if apiKey == "" || apiSecret == "" || webhookURL == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
redisURL := strings.TrimSpace(env["REDIS_URL"])
|
||||
redisAddr := strings.TrimPrefix(redisURL, "redis://")
|
||||
if redisAddr == "" {
|
||||
redisAddr = "redis:6379"
|
||||
}
|
||||
|
||||
yaml := fmt.Sprintf(`port: 7880
|
||||
|
||||
redis:
|
||||
address: "%s"
|
||||
db: 0
|
||||
|
||||
keys:
|
||||
"%s": "%s"
|
||||
|
||||
rtc:
|
||||
tcp_port: 7881
|
||||
|
||||
webhook:
|
||||
api_key: "%s"
|
||||
urls:
|
||||
- "%s"
|
||||
|
||||
room:
|
||||
auto_create: true
|
||||
max_participants: 100
|
||||
empty_timeout: 300
|
||||
|
||||
development: true
|
||||
`, redisAddr, apiKey, apiSecret, apiKey, webhookURL)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(yaml), 0o600); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
154
dev/pkg/utils/helpers.go
Normal file
154
dev/pkg/utils/helpers.go
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FileExists checks if a file exists at the given path
|
||||
func FileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// BoolString converts a boolean to a string ("true" or "false")
|
||||
func BoolString(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
// FirstNonZeroInt returns the first non-zero integer from the provided values,
|
||||
// or the default value if all are zero
|
||||
func FirstNonZeroInt(values ...int) int {
|
||||
for _, v := range values {
|
||||
if v != 0 {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// DefaultString returns the value if non-empty, otherwise returns the default
|
||||
func DefaultString(value, defaultValue string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// RandomString generates a random alphanumeric string of the given length
|
||||
func RandomString(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for i := range b {
|
||||
b[i] = charset[int(b[i])%len(charset)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// RandomBase32 generates a random base32-encoded string (without padding)
|
||||
func RandomBase32(byteLength int) string {
|
||||
b := make([]byte, byteLength)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return strings.TrimRight(base32.StdEncoding.EncodeToString(b), "=")
|
||||
}
|
||||
|
||||
// GenerateSnowflake generates a snowflake ID
|
||||
// Format: timestamp (42 bits) + worker ID (10 bits) + sequence (12 bits)
|
||||
func GenerateSnowflake() string {
|
||||
const fluxerEpoch = 1420070400000
|
||||
timestamp := time.Now().UnixMilli() - fluxerEpoch
|
||||
workerID := int64(0)
|
||||
sequence := int64(0)
|
||||
snowflake := (timestamp << 22) | (workerID << 12) | sequence
|
||||
return fmt.Sprintf("%d", snowflake)
|
||||
}
|
||||
|
||||
// ValidateURL validates that a string is a valid URL
|
||||
func ValidateURL(urlStr string) error {
|
||||
if urlStr == "" {
|
||||
return fmt.Errorf("URL cannot be empty")
|
||||
}
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid URL: %w", err)
|
||||
}
|
||||
if parsedURL.Scheme == "" {
|
||||
return fmt.Errorf("URL must have a scheme (http:// or https://)")
|
||||
}
|
||||
if parsedURL.Host == "" {
|
||||
return fmt.Errorf("URL must have a host")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseEnvFile parses a .env file and returns a map of key-value pairs
|
||||
func ParseEnvFile(path string) (map[string]string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
env := make(map[string]string)
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
if len(value) >= 2 {
|
||||
if (value[0] == '"' && value[len(value)-1] == '"') ||
|
||||
(value[0] == '\'' && value[len(value)-1] == '\'') {
|
||||
value = value[1 : len(value)-1]
|
||||
}
|
||||
}
|
||||
|
||||
env[key] = value
|
||||
}
|
||||
|
||||
return env, scanner.Err()
|
||||
}
|
||||
154
dev/setup.sh
Executable file
154
dev/setup.sh
Executable file
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (C) 2026 Fluxer Contributors
|
||||
#
|
||||
# This file is part of Fluxer.
|
||||
#
|
||||
# Fluxer is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fluxer is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
check_command() {
|
||||
if command -v "$1" &> /dev/null; then
|
||||
echo -e "${GREEN}[OK]${NC} $1 is installed"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}[MISSING]${NC} $1 is not installed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_node_version() {
|
||||
if command -v node &> /dev/null; then
|
||||
NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
|
||||
if [ "$NODE_VERSION" -ge 20 ]; then
|
||||
echo -e "${GREEN}[OK]${NC} Node.js $(node -v) is installed"
|
||||
return 0
|
||||
else
|
||||
echo -e "${YELLOW}[WARN]${NC} Node.js $(node -v) is installed, but v20+ is recommended"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}[MISSING]${NC} Node.js is not installed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== Fluxer Development Setup ==="
|
||||
echo ""
|
||||
|
||||
echo "Checking prerequisites..."
|
||||
echo ""
|
||||
|
||||
MISSING=0
|
||||
|
||||
check_node_version || MISSING=1
|
||||
check_command pnpm || MISSING=1
|
||||
check_command docker || MISSING=1
|
||||
check_command rustc || echo -e "${YELLOW}[OPTIONAL]${NC} Rust is not installed (needed for fluxer_app WASM modules)"
|
||||
check_command wasm-pack || echo -e "${YELLOW}[OPTIONAL]${NC} wasm-pack is not installed (needed for fluxer_app WASM modules)"
|
||||
check_command go || echo -e "${YELLOW}[OPTIONAL]${NC} Go is not installed (needed for fluxer_geoip)"
|
||||
|
||||
echo ""
|
||||
|
||||
if [ "$MISSING" -eq 1 ]; then
|
||||
echo -e "${RED}Some required dependencies are missing. Please install them before continuing.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Creating Docker network if needed..."
|
||||
if docker network inspect fluxer-shared &> /dev/null; then
|
||||
echo -e "${GREEN}[OK]${NC} Docker network 'fluxer-shared' already exists"
|
||||
else
|
||||
docker network create fluxer-shared
|
||||
echo -e "${GREEN}[OK]${NC} Created Docker network 'fluxer-shared'"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
if [ ! -f "$SCRIPT_DIR/.env" ]; then
|
||||
echo "Creating .env from .env.example..."
|
||||
cp "$SCRIPT_DIR/.env.example" "$SCRIPT_DIR/.env"
|
||||
echo -e "${GREEN}[OK]${NC} Created .env file"
|
||||
else
|
||||
echo -e "${GREEN}[OK]${NC} .env file already exists"
|
||||
fi
|
||||
|
||||
mkdir -p "$SCRIPT_DIR/geoip_data"
|
||||
if [ ! -f "$SCRIPT_DIR/geoip_data/ipinfo_lite.mmdb" ]; then
|
||||
echo -e "${YELLOW}[INFO]${NC} GeoIP database not found."
|
||||
echo " Set IPINFO_TOKEN in .env and run the geoip service to download it,"
|
||||
echo " or manually download ipinfo_lite.mmdb to dev/geoip_data/"
|
||||
else
|
||||
echo -e "${GREEN}[OK]${NC} GeoIP database exists"
|
||||
fi
|
||||
|
||||
if [ ! -f "$SCRIPT_DIR/livekit.yaml" ]; then
|
||||
echo "Creating default livekit.yaml..."
|
||||
cat > "$SCRIPT_DIR/livekit.yaml" << 'EOF'
|
||||
port: 7880
|
||||
|
||||
redis:
|
||||
address: 'redis:6379'
|
||||
db: 0
|
||||
|
||||
keys:
|
||||
'e1dG953yAoJPIsK1dzfTWAKMNE9gmnPL': 'rCtIICXHtAwSAJ4glb11jARcXCCgMTGvvTKLIlpD0pEoANLgjCNPD1Ysm8uWhQTB'
|
||||
|
||||
rtc:
|
||||
tcp_port: 7881
|
||||
|
||||
webhook:
|
||||
api_key: 'e1dG953yAoJPIsK1dzfTWAKMNE9gmnPL'
|
||||
urls:
|
||||
- 'http://api:8080/webhooks/livekit'
|
||||
|
||||
room:
|
||||
auto_create: true
|
||||
max_participants: 100
|
||||
empty_timeout: 300
|
||||
|
||||
development: true
|
||||
EOF
|
||||
echo -e "${GREEN}[OK]${NC} Created livekit.yaml"
|
||||
else
|
||||
echo -e "${GREEN}[OK]${NC} livekit.yaml already exists"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Setup Complete ==="
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo ""
|
||||
echo "1. Start data stores:"
|
||||
echo " docker compose -f compose.data.yaml up -d"
|
||||
echo ""
|
||||
echo "2. Start app services:"
|
||||
echo " docker compose up -d api worker media gateway admin marketing docs geoip metrics caddy"
|
||||
echo ""
|
||||
echo "3. Run the frontend on your host machine:"
|
||||
echo " cd ../fluxer_app && pnpm install && pnpm dev"
|
||||
echo ""
|
||||
echo "4. Access the app at: http://localhost:8088"
|
||||
echo ""
|
||||
echo "Optional: Start Cloudflare tunnel:"
|
||||
echo " docker compose up -d cloudflared"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user