Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f07cc86db | |||
| 90eaa68604 | |||
| d4981130ec | |||
| f0f769c5e8 | |||
| a30fe60414 | |||
| 2dadc5362d |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,6 +6,7 @@ frontend/out/
|
|||||||
# Backend
|
# Backend
|
||||||
backend/src/*/bin/
|
backend/src/*/bin/
|
||||||
backend/src/*/obj/
|
backend/src/*/obj/
|
||||||
|
backend/.idea/
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
.env
|
.env
|
||||||
@@ -20,6 +21,9 @@ backend/src/*/obj/
|
|||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
|
||||||
|
# Local development
|
||||||
|
backend/src/*/appsettings.Local.json
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -7,13 +7,30 @@ steps:
|
|||||||
image: appleboy/drone-ssh
|
image: appleboy/drone-ssh
|
||||||
when:
|
when:
|
||||||
branch: main
|
branch: main
|
||||||
|
environment:
|
||||||
|
R2_ACCOUNT_ID:
|
||||||
|
from_secret: r2_account_id
|
||||||
|
R2_ACCESS_KEY_ID:
|
||||||
|
from_secret: r2_access_key_id
|
||||||
|
R2_SECRET_ACCESS_KEY:
|
||||||
|
from_secret: r2_secret_access_key
|
||||||
settings:
|
settings:
|
||||||
host: 31.131.18.254
|
host: 31.131.18.254
|
||||||
username: deploy
|
username: deploy
|
||||||
key:
|
key:
|
||||||
from_secret: ssh_key
|
from_secret: ssh_key
|
||||||
|
envs:
|
||||||
|
- R2_ACCOUNT_ID
|
||||||
|
- R2_ACCESS_KEY_ID
|
||||||
|
- R2_SECRET_ACCESS_KEY
|
||||||
script:
|
script:
|
||||||
- cd /srv/apps/gb-site && git pull origin main
|
- cd /srv/apps/gb-site && git pull origin main
|
||||||
|
- |
|
||||||
|
cat > /srv/apps/gb-site/deploy/.env << EOF
|
||||||
|
R2_ACCOUNT_ID=$R2_ACCOUNT_ID
|
||||||
|
R2_ACCESS_KEY_ID=$R2_ACCESS_KEY_ID
|
||||||
|
R2_SECRET_ACCESS_KEY=$R2_SECRET_ACCESS_KEY
|
||||||
|
EOF
|
||||||
- cd /srv/apps/gb-site/deploy && docker compose -f docker-compose.prod.yml build --no-cache
|
- cd /srv/apps/gb-site/deploy && docker compose -f docker-compose.prod.yml build --no-cache
|
||||||
- cd /srv/apps/gb-site/deploy && docker compose -f docker-compose.prod.yml up -d
|
- cd /srv/apps/gb-site/deploy && docker compose -f docker-compose.prod.yml up -d
|
||||||
|
|
||||||
@@ -21,12 +38,29 @@ steps:
|
|||||||
image: appleboy/drone-ssh
|
image: appleboy/drone-ssh
|
||||||
when:
|
when:
|
||||||
branch: dev
|
branch: dev
|
||||||
|
environment:
|
||||||
|
R2_ACCOUNT_ID:
|
||||||
|
from_secret: r2_account_id
|
||||||
|
R2_ACCESS_KEY_ID:
|
||||||
|
from_secret: r2_access_key_id
|
||||||
|
R2_SECRET_ACCESS_KEY:
|
||||||
|
from_secret: r2_secret_access_key
|
||||||
settings:
|
settings:
|
||||||
host: 31.131.18.254
|
host: 31.131.18.254
|
||||||
username: deploy
|
username: deploy
|
||||||
key:
|
key:
|
||||||
from_secret: ssh_key
|
from_secret: ssh_key
|
||||||
|
envs:
|
||||||
|
- R2_ACCOUNT_ID
|
||||||
|
- R2_ACCESS_KEY_ID
|
||||||
|
- R2_SECRET_ACCESS_KEY
|
||||||
script:
|
script:
|
||||||
- cd /srv/apps/gb-site && git pull origin dev
|
- cd /srv/apps/gb-site-dev && git pull origin dev
|
||||||
- cd /srv/apps/gb-site/deploy && docker compose -f docker-compose.dev.yml build --no-cache
|
- |
|
||||||
- cd /srv/apps/gb-site/deploy && docker compose -f docker-compose.dev.yml up -d
|
cat > /srv/apps/gb-site-dev/deploy/.env << EOF
|
||||||
|
R2_ACCOUNT_ID=$R2_ACCOUNT_ID
|
||||||
|
R2_ACCESS_KEY_ID=$R2_ACCESS_KEY_ID
|
||||||
|
R2_SECRET_ACCESS_KEY=$R2_SECRET_ACCESS_KEY
|
||||||
|
EOF
|
||||||
|
- cd /srv/apps/gb-site-dev/deploy && docker compose -f docker-compose.dev.yml build --no-cache
|
||||||
|
- cd /srv/apps/gb-site-dev/deploy && docker compose -f docker-compose.dev.yml up -d
|
||||||
|
|||||||
73
CLAUDE.md
73
CLAUDE.md
@@ -28,7 +28,13 @@ GBSite/
|
|||||||
│ └── Dockerfile # Multi-stage: dotnet sdk → aspnet runtime
|
│ └── Dockerfile # Multi-stage: dotnet sdk → aspnet runtime
|
||||||
├── deploy/
|
├── deploy/
|
||||||
│ ├── docker-compose.prod.yml
|
│ ├── docker-compose.prod.yml
|
||||||
│ └── docker-compose.dev.yml
|
│ ├── docker-compose.dev.yml
|
||||||
|
│ └── docker-compose.local.yml # Local backend (Docker mode)
|
||||||
|
├── scripts/
|
||||||
|
│ ├── local-tunnel.ps1 # SSH tunnel to dev DB
|
||||||
|
│ ├── local-backend-docker.ps1 # Start Docker backend
|
||||||
|
│ ├── local-backend-dotnet.ps1 # Start native backend
|
||||||
|
│ └── local-frontend.ps1 # Start frontend dev server
|
||||||
├── .woodpecker/
|
├── .woodpecker/
|
||||||
│ └── deploy.yml # CI/CD pipeline
|
│ └── deploy.yml # CI/CD pipeline
|
||||||
└── CLAUDE.md # This file
|
└── CLAUDE.md # This file
|
||||||
@@ -40,8 +46,9 @@ GBSite/
|
|||||||
|-----|-------------|---------|-----|
|
|-----|-------------|---------|-----|
|
||||||
| **Prod** | https://new.goodbrick.com.ua | https://new.goodbrick.com.ua/api/* | prod_db |
|
| **Prod** | https://new.goodbrick.com.ua | https://new.goodbrick.com.ua/api/* | prod_db |
|
||||||
| **Dev** | https://dev.goodbrick.com.ua | https://dev.goodbrick.com.ua/api/* | dev_db |
|
| **Dev** | https://dev.goodbrick.com.ua | https://dev.goodbrick.com.ua/api/* | dev_db |
|
||||||
|
| **Local** | http://localhost:3000 | http://localhost:5000/api/* | dev_db (via SSH tunnel) |
|
||||||
|
|
||||||
## Docker Containers
|
## Docker Containers (Server)
|
||||||
|
|
||||||
| Container | Internal Port | Host Port | Network |
|
| Container | Internal Port | Host Port | Network |
|
||||||
|-----------|--------------|-----------|---------|
|
|-----------|--------------|-----------|---------|
|
||||||
@@ -58,7 +65,48 @@ Path-based routing — один домен для фронта и API:
|
|||||||
|
|
||||||
Backend получает запросы с prefix `/api/` (Caddy использует `handle`, не `handle_path`).
|
Backend получает запросы с prefix `/api/` (Caddy использует `handle`, не `handle_path`).
|
||||||
|
|
||||||
## Development
|
## Local Development
|
||||||
|
|
||||||
|
### Quick Start (3 терминала)
|
||||||
|
|
||||||
|
**Terminal 1 — SSH Tunnel (всегда нужен):**
|
||||||
|
```powershell
|
||||||
|
.\scripts\local-tunnel.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Terminal 2 — Backend (выбрать один):**
|
||||||
|
```powershell
|
||||||
|
# Работа с фронтом — бэкенд в Docker:
|
||||||
|
.\scripts\local-backend-docker.ps1
|
||||||
|
|
||||||
|
# Работа с бэком — dotnet run с hot-reload:
|
||||||
|
.\scripts\local-backend-dotnet.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Terminal 3 — Frontend:**
|
||||||
|
```powershell
|
||||||
|
.\scripts\local-frontend.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Как это работает
|
||||||
|
|
||||||
|
- **SSH Tunnel:** `localhost:5433` → сервер `31.131.18.254:5432` (dev_db)
|
||||||
|
- **Backend:** слушает на `localhost:5000`, подключается к БД через туннель
|
||||||
|
- **Frontend:** `npm run dev` на `:3000`, Next.js rewrites проксируют `/api/*` → `localhost:5000`
|
||||||
|
- **Переключение режимов:** Ctrl+C в терминале с бэком, запустить другой скрипт. Фронт перезапускать не нужно.
|
||||||
|
|
||||||
|
### Конфигурация
|
||||||
|
|
||||||
|
- `backend/src/GBSite.Api/appsettings.Local.json` — connection string к dev_db (gitignored)
|
||||||
|
- `frontend/.env.local` — `LOCAL_API_URL` и `INTERNAL_API_URL` для локального проксирования
|
||||||
|
- `deploy/docker-compose.local.yml` — Docker backend с `host.docker.internal:5433`
|
||||||
|
|
||||||
|
### URLs
|
||||||
|
- Frontend: http://localhost:3000
|
||||||
|
- Backend API: http://localhost:5000/api/health
|
||||||
|
- DB: localhost:5433 (через SSH tunnel к dev_db)
|
||||||
|
|
||||||
|
## Development (manual)
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
```bash
|
```bash
|
||||||
@@ -82,6 +130,7 @@ cd backend && dotnet build
|
|||||||
- Controllers use attribute routing: `[Route("api/[controller]")]`
|
- Controllers use attribute routing: `[Route("api/[controller]")]`
|
||||||
- Health check: `GET /api/health` — returns `{ status, database, environment }`
|
- Health check: `GET /api/health` — returns `{ status, database, environment }`
|
||||||
- Health ping: `GET /api/health/ping` — returns `{ status: "pong" }`
|
- Health ping: `GET /api/health/ping` — returns `{ status: "pong" }`
|
||||||
|
- Upload: `POST /api/upload` — multipart form, fields: `file` (required), `path` (optional folder prefix)
|
||||||
|
|
||||||
## Frontend Conventions
|
## Frontend Conventions
|
||||||
|
|
||||||
@@ -91,7 +140,7 @@ cd backend && dotnet build
|
|||||||
- `src/components/` — React components (create when needed)
|
- `src/components/` — React components (create when needed)
|
||||||
- shadcn/ui components via `npx shadcn@latest add <component>`
|
- shadcn/ui components via `npx shadcn@latest add <component>`
|
||||||
- Server-side calls to backend use `INTERNAL_API_URL` env var
|
- Server-side calls to backend use `INTERNAL_API_URL` env var
|
||||||
- Client-side calls use relative paths (`/api/...`) — Caddy proxies them
|
- Client-side calls use relative paths (`/api/...`) — Caddy proxies them on server, Next.js rewrites locally
|
||||||
|
|
||||||
## Git & CI/CD
|
## Git & CI/CD
|
||||||
|
|
||||||
@@ -118,6 +167,21 @@ Host=postgres;Port=5432;Database={db};Username={user};Password={pass}
|
|||||||
```
|
```
|
||||||
|
|
||||||
Configured via env var `ConnectionStrings__Default` in docker-compose files.
|
Configured via env var `ConnectionStrings__Default` in docker-compose files.
|
||||||
|
Locally via `appsettings.Local.json` with SSH tunnel (`localhost:5433`).
|
||||||
|
|
||||||
|
## CDN & Image Storage
|
||||||
|
|
||||||
|
- **Storage:** Cloudflare R2 (S3-compatible)
|
||||||
|
- **CDN domain:** https://cdn.goodbrick.com.ua
|
||||||
|
- **Bucket:** goodbrick
|
||||||
|
- **Frontend:** `NEXT_PUBLIC_CDN_URL` env var, `cdnUrl()` helper in `src/lib/cdn.ts`
|
||||||
|
- **Backend:** `R2__*` env vars, `R2StorageService` for uploads
|
||||||
|
- **Upload endpoint:** `POST /api/upload` — multipart/form-data, max 10MB, images only (jpeg/png/webp/avif)
|
||||||
|
|
||||||
|
### Folder Convention
|
||||||
|
- `products/<slug>/main.jpg` — product photos
|
||||||
|
- `catalog/<slug>/cover.jpg` — collection covers
|
||||||
|
- `site/<name>.jpg` — site-wide images (hero, about, etc.)
|
||||||
|
|
||||||
## Key Rules
|
## Key Rules
|
||||||
|
|
||||||
@@ -125,3 +189,4 @@ Configured via env var `ConnectionStrings__Default` in docker-compose files.
|
|||||||
- Frontend standalone output (`output: "standalone"` in next.config.ts) — required for Docker
|
- Frontend standalone output (`output: "standalone"` in next.config.ts) — required for Docker
|
||||||
- Backend listens on port 5000 (`ASPNETCORE_URLS=http://+:5000`)
|
- Backend listens on port 5000 (`ASPNETCORE_URLS=http://+:5000`)
|
||||||
- All containers must be in `app-network` (external Docker network)
|
- All containers must be in `app-network` (external Docker network)
|
||||||
|
- `appsettings.Local.json` is gitignored — local dev overrides only
|
||||||
|
|||||||
129
README.md
129
README.md
@@ -1,94 +1,59 @@
|
|||||||
# GoodBrick Project
|
# GoodBrick Project
|
||||||
|
|
||||||
Корпоративная инфраструктура для компании GoodBrick на базе self-hosted сервисов.
|
Сайт производителя фасадной плитки GoodBrick — каталог продукции, корпоративная информация.
|
||||||
|
|
||||||
## 🚀 Deployed Services
|
## Services
|
||||||
|
|
||||||
| Service | URL | Status | Purpose |
|
| Service | URL | Purpose |
|
||||||
|---------|-----|--------|---------|
|
|---------|-----|---------|
|
||||||
| **Gitea** | https://git.goodbrick.com.ua | ✅ Running | Git сервер |
|
| **Production** | https://new.goodbrick.com.ua | Продакшн приложение |
|
||||||
| **Uptime Kuma** | https://status.goodbrick.com.ua | ✅ Running | Мониторинг |
|
| **Development** | https://dev.goodbrick.com.ua | Dev окружение |
|
||||||
| **Woodpecker CI** | https://ci.goodbrick.com.ua | ✅ Running | CI/CD |
|
| **Gitea** | https://git.goodbrick.com.ua | Git сервер |
|
||||||
| **Production App** | https://new.goodbrick.com.ua | 🔨 TODO | Продакшн приложение |
|
| **Woodpecker CI** | https://ci.goodbrick.com.ua | CI/CD |
|
||||||
| **Development App** | https://dev.goodbrick.com.ua | 🔨 TODO | Dev окружение |
|
| **Uptime Kuma** | https://status.goodbrick.com.ua | Мониторинг |
|
||||||
|
|
||||||
## 📚 Documentation
|
## Stack
|
||||||
|
|
||||||
- **[SERVER.md](./SERVER.md)** - Полная документация по серверу, сервисам, Docker, базам данных и troubleshooting
|
- **Frontend:** Next.js 16 + shadcn/ui + Tailwind CSS 4
|
||||||
|
- **Backend:** .NET 9 Web API + Npgsql
|
||||||
|
- **Database:** PostgreSQL 16
|
||||||
|
- **Proxy:** Caddy 2 (auto-SSL)
|
||||||
|
- **CI/CD:** Woodpecker CI
|
||||||
|
|
||||||
## 🖥️ Server Info
|
## Local Development
|
||||||
|
|
||||||
|
See [CLAUDE.md](./CLAUDE.md#local-development) for full local dev setup.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Terminal 1: SSH tunnel to dev database
|
||||||
|
.\scripts\local-tunnel.ps1
|
||||||
|
|
||||||
|
# Terminal 2: Backend (choose one)
|
||||||
|
.\scripts\local-backend-dotnet.ps1 # native, with hot-reload
|
||||||
|
.\scripts\local-backend-docker.ps1 # Docker container
|
||||||
|
|
||||||
|
# Terminal 3: Frontend
|
||||||
|
.\scripts\local-frontend.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- **[CLAUDE.md](./CLAUDE.md)** — Project conventions, structure, local dev setup
|
||||||
|
- **[SERVER.md](./SERVER.md)** — Server infrastructure, services, troubleshooting
|
||||||
|
|
||||||
|
## Git Workflow
|
||||||
|
|
||||||
|
- `main` branch → auto-deploy to production
|
||||||
|
- `dev` branch → auto-deploy to dev environment
|
||||||
|
- Commit style: conventional, English
|
||||||
|
|
||||||
|
## Server
|
||||||
|
|
||||||
- **Host:** 31.131.18.254
|
- **Host:** 31.131.18.254
|
||||||
- **SSH:** `ssh deploy@31.131.18.254` (key-based auth)
|
- **SSH:** `ssh deploy@31.131.18.254` (key-based)
|
||||||
- **OS:** Linux
|
|
||||||
- **Docker Network:** `app-network`
|
- **Docker Network:** `app-network`
|
||||||
|
|
||||||
## 🗄️ Databases
|
## Next Steps
|
||||||
|
|
||||||
PostgreSQL 16 с отдельными базами для каждого сервиса:
|
- [ ] Настроить email для Gitea
|
||||||
- `gitea_db` - Gitea
|
- [ ] Настроить автобэкапы баз данных
|
||||||
- `prod_db` - Production app
|
|
||||||
- `dev_db` - Development app
|
|
||||||
|
|
||||||
Все credentials в `/srv/postgres/CREDENTIALS.txt` на сервере.
|
|
||||||
|
|
||||||
## 🔧 Stack
|
|
||||||
|
|
||||||
- **Reverse Proxy:** Caddy 2 (автоматический SSL)
|
|
||||||
- **Database:** PostgreSQL 16
|
|
||||||
- **Git:** Gitea
|
|
||||||
- **CI/CD:** Woodpecker CI
|
|
||||||
- **Monitoring:** Uptime Kuma
|
|
||||||
- **Container:** Docker + Docker Compose
|
|
||||||
|
|
||||||
## 📝 Quick Commands
|
|
||||||
|
|
||||||
### SSH в сервер
|
|
||||||
```bash
|
|
||||||
ssh deploy@31.131.18.254
|
|
||||||
```
|
|
||||||
|
|
||||||
### Просмотр всех контейнеров
|
|
||||||
```bash
|
|
||||||
ssh deploy@31.131.18.254 'docker ps'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Просмотр логов
|
|
||||||
```bash
|
|
||||||
ssh deploy@31.131.18.254 'docker logs <container_name> --tail 50'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Рестарт сервиса
|
|
||||||
```bash
|
|
||||||
ssh deploy@31.131.18.254 'cd /srv/<service> && docker compose restart'
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🏗️ Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
C:\Work\goodbrick\GBSite\
|
|
||||||
├── README.md ← Этот файл
|
|
||||||
├── SERVER.md ← Полная документация сервера
|
|
||||||
└── [project files] ← Файлы проектов
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔐 Security
|
|
||||||
|
|
||||||
- SSH: Только key-based аутентификация
|
|
||||||
- PostgreSQL: Изолированные пользователи для каждого приложения
|
|
||||||
- Docker: Все сервисы в изолированной сети `app-network`
|
|
||||||
- SSL: Автоматические сертификаты через Let's Encrypt
|
|
||||||
|
|
||||||
## 🎯 Next Steps
|
|
||||||
|
|
||||||
1. Deploy production app на `new.goodbrick.com.ua`
|
|
||||||
2. Deploy development app на `dev.goodbrick.com.ua`
|
|
||||||
3. Настроить CI/CD пайплайны в Woodpecker
|
|
||||||
4. Настроить email для Gitea
|
|
||||||
5. Настроить автобэкапы баз данных
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Документация актуальна на:** 2026-02-09
|
|
||||||
**Разработчик:** Ivan
|
|
||||||
**AI Assistant:** Claude Sonnet 4.5
|
|
||||||
|
|||||||
333
SERVER.md
333
SERVER.md
@@ -3,7 +3,7 @@
|
|||||||
## SSH Access
|
## SSH Access
|
||||||
- **Host:** 31.131.18.254
|
- **Host:** 31.131.18.254
|
||||||
- **User:** deploy
|
- **User:** deploy
|
||||||
- **Auth:** SSH key (already configured, no password needed)
|
- **Auth:** SSH key (no password)
|
||||||
- **Command:** `ssh deploy@31.131.18.254`
|
- **Command:** `ssh deploy@31.131.18.254`
|
||||||
|
|
||||||
## Server Structure
|
## Server Structure
|
||||||
@@ -12,12 +12,14 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
/srv/
|
/srv/
|
||||||
├── apps/ # Empty, ready for applications
|
├── apps/
|
||||||
├── gitea/ # Git server
|
│ ├── gb-site/ # Production app (main branch)
|
||||||
├── postgres/ # PostgreSQL database
|
│ └── gb-site-dev/ # Development app (dev branch, git worktree)
|
||||||
├── proxy/ # Caddy reverse proxy
|
├── gitea/ # Git server
|
||||||
├── uptime-kuma/ # Status monitoring
|
├── postgres/ # PostgreSQL database
|
||||||
└── woodpecker/ # CI/CD server
|
├── proxy/ # Caddy reverse proxy
|
||||||
|
├── uptime-kuma/ # Status monitoring
|
||||||
|
└── woodpecker/ # CI/CD server
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docker Network
|
## Docker Network
|
||||||
@@ -27,12 +29,13 @@
|
|||||||
|
|
||||||
## Live Services
|
## Live Services
|
||||||
|
|
||||||
### Production URLs
|
| Service | URL |
|
||||||
- **Gitea:** https://git.goodbrick.com.ua
|
|---------|-----|
|
||||||
- **Uptime Kuma:** https://status.goodbrick.com.ua
|
| **Production App** | https://new.goodbrick.com.ua |
|
||||||
- **Woodpecker CI:** https://ci.goodbrick.com.ua ✅ Working!
|
| **Development App** | https://dev.goodbrick.com.ua |
|
||||||
- **Production App:** https://new.goodbrick.com.ua (TODO)
|
| **Gitea** | https://git.goodbrick.com.ua |
|
||||||
- **Development App:** https://dev.goodbrick.com.ua (TODO)
|
| **Woodpecker CI** | https://ci.goodbrick.com.ua |
|
||||||
|
| **Uptime Kuma** | https://status.goodbrick.com.ua |
|
||||||
|
|
||||||
All domains point to `31.131.18.254` via A records.
|
All domains point to `31.131.18.254` via A records.
|
||||||
|
|
||||||
@@ -49,6 +52,7 @@ All domains point to `31.131.18.254` via A records.
|
|||||||
- **SSL:** Automatic via Let's Encrypt
|
- **SSL:** Automatic via Let's Encrypt
|
||||||
|
|
||||||
#### Caddyfile Routes
|
#### Caddyfile Routes
|
||||||
|
|
||||||
```caddyfile
|
```caddyfile
|
||||||
git.goodbrick.com.ua {
|
git.goodbrick.com.ua {
|
||||||
reverse_proxy gitea:3000
|
reverse_proxy gitea:3000
|
||||||
@@ -65,17 +69,25 @@ ci.goodbrick.com.ua {
|
|||||||
}
|
}
|
||||||
|
|
||||||
new.goodbrick.com.ua {
|
new.goodbrick.com.ua {
|
||||||
reverse_proxy new:3000
|
handle /api/* {
|
||||||
|
reverse_proxy gb-prod-backend:5000
|
||||||
|
}
|
||||||
|
handle {
|
||||||
|
reverse_proxy gb-prod-frontend:3000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dev.goodbrick.com.ua {
|
dev.goodbrick.com.ua {
|
||||||
reverse_proxy dev:3000
|
handle /api/* {
|
||||||
|
reverse_proxy gb-dev-backend:5000
|
||||||
|
}
|
||||||
|
handle {
|
||||||
|
reverse_proxy gb-dev-frontend:3000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Important Notes:**
|
**Important:** `handle` (not `handle_path`) preserves the `/api/` prefix in the request.
|
||||||
- Use container names (not `127.0.0.1` or `localhost`)
|
|
||||||
- `flush_interval -1` required for Woodpecker's Server-Sent Events
|
|
||||||
|
|
||||||
**Restart:**
|
**Restart:**
|
||||||
```bash
|
```bash
|
||||||
@@ -88,7 +100,7 @@ ssh deploy@31.131.18.254 'cd /srv/proxy && docker compose restart'
|
|||||||
- **Location:** `/srv/postgres`
|
- **Location:** `/srv/postgres`
|
||||||
- **Container:** `postgres`
|
- **Container:** `postgres`
|
||||||
- **Image:** `postgres:16`
|
- **Image:** `postgres:16`
|
||||||
- **Port:** 127.0.0.1:5432 (localhost only)
|
- **Port:** 127.0.0.1:5432 (localhost only, not exposed to internet)
|
||||||
- **Data:** `/srv/postgres/data`
|
- **Data:** `/srv/postgres/data`
|
||||||
- **Credentials:** `/srv/postgres/CREDENTIALS.txt` (chmod 600)
|
- **Credentials:** `/srv/postgres/CREDENTIALS.txt` (chmod 600)
|
||||||
|
|
||||||
@@ -102,34 +114,13 @@ Database: app
|
|||||||
|
|
||||||
#### Application Databases
|
#### Application Databases
|
||||||
|
|
||||||
**Gitea:**
|
| App | User | Password | Database |
|
||||||
```
|
|-----|------|----------|----------|
|
||||||
User: gitea_user
|
| Gitea | gitea_user | gitea_pass_zYWT5JWu3iAbbW7m | gitea_db |
|
||||||
Password: gitea_pass_zYWT5JWu3iAbbW7m
|
| Production | prod_user | prod_pass_kL9mN2pQ7xR8sT4v | prod_db |
|
||||||
Database: gitea_db
|
| Development | dev_user | dev_pass_vB6nM3qP8yW2rT9k | dev_db |
|
||||||
Connection String: postgres://gitea_user:gitea_pass_zYWT5JWu3iAbbW7m@postgres:5432/gitea_db
|
|
||||||
```
|
|
||||||
|
|
||||||
**Production App:**
|
Each app has isolated DB and user. Users cannot access other databases.
|
||||||
```
|
|
||||||
User: prod_user
|
|
||||||
Password: prod_pass_kL9mN2pQ7xR8sT4v
|
|
||||||
Database: prod_db
|
|
||||||
Connection String: postgres://prod_user:prod_pass_kL9mN2pQ7xR8sT4v@postgres:5432/prod_db
|
|
||||||
```
|
|
||||||
|
|
||||||
**Development App:**
|
|
||||||
```
|
|
||||||
User: dev_user
|
|
||||||
Password: dev_pass_vB6nM3qP8yW2rT9k
|
|
||||||
Database: dev_db
|
|
||||||
Connection String: postgres://dev_user:dev_pass_vB6nM3qP8yW2rT9k@postgres:5432/dev_db
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important:**
|
|
||||||
- Each app has its own isolated database and user
|
|
||||||
- Users cannot access other databases
|
|
||||||
- PostgreSQL only accessible via Docker network
|
|
||||||
|
|
||||||
**Restart:**
|
**Restart:**
|
||||||
```bash
|
```bash
|
||||||
@@ -143,34 +134,13 @@ ssh deploy@31.131.18.254 'cd /srv/postgres && docker compose restart'
|
|||||||
- **Container:** `gitea`
|
- **Container:** `gitea`
|
||||||
- **Image:** `gitea/gitea:latest`
|
- **Image:** `gitea/gitea:latest`
|
||||||
- **URL:** https://git.goodbrick.com.ua
|
- **URL:** https://git.goodbrick.com.ua
|
||||||
- **Admin:** admin (set during installation)
|
- **Ports:** 127.0.0.1:3002:3000 (HTTP), 127.0.0.1:2222:22 (SSH)
|
||||||
- **Ports:**
|
- **Database:** gitea_db
|
||||||
- 127.0.0.1:3002:3000 (HTTP)
|
|
||||||
- 127.0.0.1:2222:22 (SSH)
|
|
||||||
- **Database:** gitea_db (PostgreSQL)
|
|
||||||
- **Data:** `/srv/gitea/data`
|
|
||||||
|
|
||||||
#### OAuth Application for Woodpecker
|
#### OAuth Application for Woodpecker
|
||||||
- **Client ID:** 157422cf-7391-4fb0-8f2d-27083676dda6
|
- **Client ID:** 157422cf-7391-4fb0-8f2d-27083676dda6
|
||||||
- **Redirect URI:** https://ci.goodbrick.com.ua/authorize
|
- **Redirect URI:** https://ci.goodbrick.com.ua/authorize
|
||||||
|
|
||||||
**Config:**
|
|
||||||
```yaml
|
|
||||||
environment:
|
|
||||||
- GITEA__server__DOMAIN=git.goodbrick.com.ua
|
|
||||||
- GITEA__server__ROOT_URL=https://git.goodbrick.com.ua/
|
|
||||||
- GITEA__webhook__ALLOWED_HOST_LIST=* # Allows webhooks to Woodpecker
|
|
||||||
- GITEA__database__DB_TYPE=postgres
|
|
||||||
- GITEA__database__HOST=postgres:5432
|
|
||||||
- GITEA__database__NAME=gitea_db
|
|
||||||
- GITEA__database__USER=gitea_user
|
|
||||||
```
|
|
||||||
|
|
||||||
**Restart:**
|
|
||||||
```bash
|
|
||||||
ssh deploy@31.131.18.254 'cd /srv/gitea && docker compose restart'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4. Uptime Kuma (Monitoring)
|
### 4. Uptime Kuma (Monitoring)
|
||||||
@@ -179,244 +149,77 @@ ssh deploy@31.131.18.254 'cd /srv/gitea && docker compose restart'
|
|||||||
- **Image:** `louislam/uptime-kuma:2`
|
- **Image:** `louislam/uptime-kuma:2`
|
||||||
- **URL:** https://status.goodbrick.com.ua
|
- **URL:** https://status.goodbrick.com.ua
|
||||||
- **Port:** 127.0.0.1:3001:3001
|
- **Port:** 127.0.0.1:3001:3001
|
||||||
- **Data:** `/srv/uptime-kuma/data`
|
|
||||||
|
|
||||||
**Restart:**
|
|
||||||
```bash
|
|
||||||
ssh deploy@31.131.18.254 'cd /srv/uptime-kuma && docker compose restart'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5. Woodpecker CI ✅
|
### 5. Woodpecker CI
|
||||||
- **Location:** `/srv/woodpecker`
|
- **Location:** `/srv/woodpecker`
|
||||||
- **Containers:** `woodpecker-server`, `woodpecker-agent`
|
- **Containers:** `woodpecker-server`, `woodpecker-agent`
|
||||||
- **Image:** `woodpeckerci/woodpecker-server:latest` (v2.8.3), `woodpeckerci/woodpecker-agent:latest`
|
- **Image:** `woodpeckerci/woodpecker-server:latest` (v2.8.3+)
|
||||||
- **URL:** https://ci.goodbrick.com.ua
|
- **URL:** https://ci.goodbrick.com.ua
|
||||||
- **Port:** 127.0.0.1:8000:8000
|
- **Port:** 127.0.0.1:8000:8000
|
||||||
- **Data:** `/srv/woodpecker/data`
|
|
||||||
|
|
||||||
**Config:**
|
**Notes:**
|
||||||
```yaml
|
- OAuth authentication via Gitea
|
||||||
environment:
|
|
||||||
# Server
|
|
||||||
- WOODPECKER_OPEN=true
|
|
||||||
- WOODPECKER_HOST=https://ci.goodbrick.com.ua
|
|
||||||
- WOODPECKER_GITEA=true
|
|
||||||
- WOODPECKER_GITEA_URL=https://git.goodbrick.com.ua
|
|
||||||
- WOODPECKER_GITEA_CLIENT=157422cf-7391-4fb0-8f2d-27083676dda6
|
|
||||||
- WOODPECKER_GITEA_SECRET=gto_2r3udrnmvtebt37v35bzl375eq5bumxqu2dpwwdfflbumnp5fy7q
|
|
||||||
- WOODPECKER_AGENT_SECRET=supersecrettoken123
|
|
||||||
- WOODPECKER_ADMIN=admin
|
|
||||||
|
|
||||||
# Agent
|
|
||||||
- WOODPECKER_SERVER=woodpecker-server:9000
|
|
||||||
- WOODPECKER_AGENT_SECRET=supersecrettoken123
|
|
||||||
```
|
|
||||||
|
|
||||||
**Status:** ✅ Working! Login with Gitea account.
|
|
||||||
|
|
||||||
**Important Notes:**
|
|
||||||
- Uses OAuth authentication via Gitea
|
|
||||||
- Agent connects to server on port 9000 (gRPC)
|
- Agent connects to server on port 9000 (gRPC)
|
||||||
- Agent has access to Docker socket for running builds
|
- Agent has Docker socket access for builds
|
||||||
- Version 2.8.3 fixes JavaScript security issues (v2.7.2 had SES errors)
|
- v2.8.3+ required (older versions have SES JS errors)
|
||||||
|
|
||||||
**Restart:**
|
|
||||||
```bash
|
|
||||||
ssh deploy@31.131.18.254 'cd /srv/woodpecker && docker compose restart'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Common Commands
|
## Common Commands
|
||||||
|
|
||||||
### View all containers
|
|
||||||
```bash
|
```bash
|
||||||
|
# View all containers
|
||||||
ssh deploy@31.131.18.254 'docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'
|
ssh deploy@31.131.18.254 'docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'
|
||||||
```
|
|
||||||
|
|
||||||
### View container logs
|
# View container logs
|
||||||
```bash
|
|
||||||
ssh deploy@31.131.18.254 'docker logs <container_name> --tail 50'
|
ssh deploy@31.131.18.254 'docker logs <container_name> --tail 50'
|
||||||
ssh deploy@31.131.18.254 'docker logs <container_name> -f' # Follow logs
|
ssh deploy@31.131.18.254 'docker logs <container_name> -f'
|
||||||
```
|
|
||||||
|
|
||||||
### Restart a service
|
# Restart a service
|
||||||
```bash
|
|
||||||
ssh deploy@31.131.18.254 'cd /srv/<service> && docker compose restart'
|
ssh deploy@31.131.18.254 'cd /srv/<service> && docker compose restart'
|
||||||
```
|
|
||||||
|
|
||||||
### Restart all services
|
# Access PostgreSQL CLI
|
||||||
```bash
|
|
||||||
ssh deploy@31.131.18.254 'cd /srv/postgres && docker compose restart && \
|
|
||||||
cd /srv/gitea && docker compose restart && \
|
|
||||||
cd /srv/uptime-kuma && docker compose restart && \
|
|
||||||
cd /srv/woodpecker && docker compose restart && \
|
|
||||||
cd /srv/proxy && docker compose restart'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Access PostgreSQL CLI
|
|
||||||
```bash
|
|
||||||
ssh deploy@31.131.18.254 'docker exec -it postgres psql -U app -d app'
|
ssh deploy@31.131.18.254 'docker exec -it postgres psql -U app -d app'
|
||||||
```
|
|
||||||
|
|
||||||
### Check Docker network
|
# Check Docker network
|
||||||
```bash
|
|
||||||
ssh deploy@31.131.18.254 'docker network inspect app-network'
|
ssh deploy@31.131.18.254 'docker network inspect app-network'
|
||||||
ssh deploy@31.131.18.254 'docker ps --format "{{.Names}}\t{{.Networks}}"'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test container connectivity
|
|
||||||
```bash
|
|
||||||
ssh deploy@31.131.18.254 'docker exec <container1> ping -c 2 <container2>'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Issue: Container can't reach another container
|
### Container can't reach another container
|
||||||
**Symptoms:** "connection refused", "no such host"
|
**Symptoms:** "connection refused", "no such host"
|
||||||
|
|
||||||
**Check:**
|
|
||||||
```bash
|
```bash
|
||||||
# Verify all containers are in app-network
|
# Check all containers are in app-network
|
||||||
ssh deploy@31.131.18.254 'docker ps --format "{{.Names}}\t{{.Networks}}"'
|
ssh deploy@31.131.18.254 'docker ps --format "{{.Names}}\t{{.Networks}}"'
|
||||||
|
# Fix: ensure docker-compose has networks: app-network: external: true
|
||||||
```
|
```
|
||||||
|
|
||||||
**Solution:**
|
### Caddy "502 Bad Gateway"
|
||||||
1. Ensure all docker-compose.yml files have:
|
1. Target service is down → `docker ps | grep <service>`
|
||||||
```yaml
|
2. Not in app-network → check networks
|
||||||
networks:
|
3. Wrong port/name in Caddyfile → check config
|
||||||
app-network:
|
|
||||||
external: true
|
|
||||||
```
|
|
||||||
2. Restart the affected service
|
|
||||||
|
|
||||||
---
|
### Woodpecker blank page / JS errors
|
||||||
|
1. Update to v2.8.3+
|
||||||
### Issue: "dial tcp: lookup <service> on 127.0.0.11:53: no such host"
|
2. Ensure `flush_interval -1` in Caddy
|
||||||
**Cause:** Container not in app-network
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
ssh deploy@31.131.18.254 'cd /srv/<service> && docker compose down && docker compose up -d'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Issue: Caddy shows "502 Bad Gateway"
|
|
||||||
**Causes:**
|
|
||||||
1. Target service is down
|
|
||||||
2. Target service not in app-network
|
|
||||||
3. Wrong port/container name in Caddyfile
|
|
||||||
|
|
||||||
**Check:**
|
|
||||||
```bash
|
|
||||||
# Check if target is running
|
|
||||||
ssh deploy@31.131.18.254 'docker ps | grep <service>'
|
|
||||||
|
|
||||||
# Check Caddy logs
|
|
||||||
ssh deploy@31.131.18.254 'docker logs caddy --tail 50'
|
|
||||||
|
|
||||||
# Test direct connection
|
|
||||||
ssh deploy@31.131.18.254 'docker exec caddy wget -O- http://<container>:<port>'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Issue: Woodpecker shows blank page or JavaScript errors
|
|
||||||
**Symptoms:** "SES Removing unpermitted intrinsics", SyntaxError
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
1. Update to latest version (2.8.3+)
|
|
||||||
2. Ensure Caddy has `flush_interval -1` for SSE support
|
|
||||||
3. Clear browser cache (Ctrl+Shift+R)
|
3. Clear browser cache (Ctrl+Shift+R)
|
||||||
|
|
||||||
---
|
### SSL certificate issues
|
||||||
|
Usually DNS propagation delay. Check `docker logs caddy | grep certificate` and `nslookup <domain>`.
|
||||||
### Issue: SSL certificate not working
|
|
||||||
**Cause:** Usually DNS propagation or Let's Encrypt rate limits
|
|
||||||
|
|
||||||
**Check:**
|
|
||||||
```bash
|
|
||||||
# Check Caddy logs
|
|
||||||
ssh deploy@31.131.18.254 'docker logs caddy | grep -i certificate'
|
|
||||||
|
|
||||||
# Check if domain resolves
|
|
||||||
nslookup <domain>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:** Wait for DNS propagation (up to 24h) or check DNS A records.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Security Notes
|
## TODO
|
||||||
|
|
||||||
1. **PostgreSQL:**
|
- [ ] Настроить email для Gitea
|
||||||
- Not exposed to internet (127.0.0.1 only)
|
- [ ] Настроить автобэкапы PostgreSQL
|
||||||
- Separate users for each application
|
- [ ] Настроить алерты в Uptime Kuma
|
||||||
- Credentials stored in `/srv/postgres/CREDENTIALS.txt` (chmod 600)
|
|
||||||
|
|
||||||
2. **SSH:**
|
|
||||||
- Key-based authentication only
|
|
||||||
- User `deploy` has Docker permissions
|
|
||||||
|
|
||||||
3. **Docker Network:**
|
|
||||||
- All containers isolated in `app-network`
|
|
||||||
- No direct internet access except through Caddy
|
|
||||||
|
|
||||||
4. **Secrets:**
|
|
||||||
- Never commit credentials to git
|
|
||||||
- Store sensitive data in environment variables
|
|
||||||
- Use `.env` files (gitignored)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Next Steps / TODO
|
**Last Updated:** 2026-02-10
|
||||||
|
|
||||||
- [ ] Deploy production app to `new.goodbrick.com.ua` (port 3100)
|
|
||||||
- [ ] Deploy development app to `dev.goodbrick.com.ua` (port 3200)
|
|
||||||
- [ ] Set up email for Gitea (optional)
|
|
||||||
- [ ] Configure Woodpecker pipelines for repositories
|
|
||||||
- [ ] Set up automatic backups for PostgreSQL databases
|
|
||||||
- [ ] Configure monitoring alerts in Uptime Kuma
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Locations Summary
|
|
||||||
|
|
||||||
```
|
|
||||||
Server (31.131.18.254):
|
|
||||||
├── /srv/
|
|
||||||
│ ├── apps/
|
|
||||||
│ ├── gitea/
|
|
||||||
│ │ ├── docker-compose.yml
|
|
||||||
│ │ └── data/
|
|
||||||
│ ├── postgres/
|
|
||||||
│ │ ├── docker-compose.yml
|
|
||||||
│ │ ├── data/
|
|
||||||
│ │ └── CREDENTIALS.txt ← All DB credentials
|
|
||||||
│ ├── proxy/
|
|
||||||
│ │ ├── docker-compose.yml
|
|
||||||
│ │ ├── Caddyfile ← All routes
|
|
||||||
│ │ ├── data/
|
|
||||||
│ │ └── config/
|
|
||||||
│ ├── uptime-kuma/
|
|
||||||
│ │ ├── docker-compose.yml
|
|
||||||
│ │ └── data/
|
|
||||||
│ └── woodpecker/
|
|
||||||
│ ├── docker-compose.yml
|
|
||||||
│ └── data/
|
|
||||||
|
|
||||||
Local:
|
|
||||||
└── C:\Work\goodbrick\GBSite\
|
|
||||||
├── SERVER.md ← This file
|
|
||||||
└── README.md ← Project overview
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated:** 2026-02-09
|
|
||||||
**Maintained by:** Claude AI Assistant
|
|
||||||
|
|||||||
14
backend/src/GBSite.Api/Configuration/R2Settings.cs
Normal file
14
backend/src/GBSite.Api/Configuration/R2Settings.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace GBSite.Api.Configuration;
|
||||||
|
|
||||||
|
public class R2Settings
|
||||||
|
{
|
||||||
|
public const string SectionName = "R2";
|
||||||
|
|
||||||
|
public required string AccountId { get; set; }
|
||||||
|
public required string AccessKeyId { get; set; }
|
||||||
|
public required string SecretAccessKey { get; set; }
|
||||||
|
public required string BucketName { get; set; }
|
||||||
|
public string PublicUrl { get; set; } = "https://cdn.goodbrick.com.ua";
|
||||||
|
|
||||||
|
public string ServiceUrl => $"https://{AccountId}.r2.cloudflarestorage.com";
|
||||||
|
}
|
||||||
44
backend/src/GBSite.Api/Controllers/UploadController.cs
Normal file
44
backend/src/GBSite.Api/Controllers/UploadController.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using GBSite.Api.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace GBSite.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class UploadController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly R2StorageService? _storage;
|
||||||
|
|
||||||
|
public UploadController(R2StorageService? storage = null)
|
||||||
|
{
|
||||||
|
_storage = storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[RequestSizeLimit(10 * 1024 * 1024)]
|
||||||
|
public async Task<IActionResult> Upload(
|
||||||
|
IFormFile file,
|
||||||
|
[FromForm] string? path,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (_storage is null)
|
||||||
|
return StatusCode(503, new { error = "Storage service not configured" });
|
||||||
|
|
||||||
|
if (file.Length == 0)
|
||||||
|
return BadRequest(new { error = "File is empty" });
|
||||||
|
|
||||||
|
var allowedTypes = new[] { "image/jpeg", "image/png", "image/webp", "image/avif" };
|
||||||
|
if (!allowedTypes.Contains(file.ContentType))
|
||||||
|
return BadRequest(new { error = $"File type '{file.ContentType}' not allowed" });
|
||||||
|
|
||||||
|
var fileName = Path.GetFileName(file.FileName);
|
||||||
|
var key = string.IsNullOrEmpty(path)
|
||||||
|
? fileName
|
||||||
|
: $"{path.Trim('/')}/{fileName}";
|
||||||
|
|
||||||
|
await using var stream = file.OpenReadStream();
|
||||||
|
var cdnUrl = await _storage.UploadAsync(stream, key, file.ContentType, ct);
|
||||||
|
|
||||||
|
return Ok(new { url = cdnUrl, key });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,11 @@
|
|||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UserSecretsId>805cad54-8a19-4713-b893-a1ec63696146</UserSecretsId>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AWSSDK.S3" Version="3.7.*" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" />
|
||||||
<PackageReference Include="Npgsql" Version="10.0.1" />
|
<PackageReference Include="Npgsql" Version="10.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,8 +1,32 @@
|
|||||||
|
using Amazon.S3;
|
||||||
|
using GBSite.Api.Configuration;
|
||||||
|
using GBSite.Api.Services;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
|
// R2 Storage
|
||||||
|
var r2Section = builder.Configuration.GetSection(R2Settings.SectionName);
|
||||||
|
builder.Services.Configure<R2Settings>(r2Section);
|
||||||
|
|
||||||
|
var r2Settings = r2Section.Get<R2Settings>();
|
||||||
|
if (r2Settings is not null && !string.IsNullOrEmpty(r2Settings.AccountId))
|
||||||
|
{
|
||||||
|
builder.Services.AddSingleton<IAmazonS3>(_ =>
|
||||||
|
{
|
||||||
|
var config = new AmazonS3Config
|
||||||
|
{
|
||||||
|
ServiceURL = r2Settings.ServiceUrl,
|
||||||
|
ForcePathStyle = true
|
||||||
|
};
|
||||||
|
return new AmazonS3Client(r2Settings.AccessKeyId, r2Settings.SecretAccessKey, config);
|
||||||
|
});
|
||||||
|
builder.Services.AddSingleton<R2StorageService>();
|
||||||
|
}
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": false,
|
"launchBrowser": false,
|
||||||
"applicationUrl": "http://localhost:5224",
|
"applicationUrl": "http://localhost:5000",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
|
|||||||
43
backend/src/GBSite.Api/Services/R2StorageService.cs
Normal file
43
backend/src/GBSite.Api/Services/R2StorageService.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using Amazon.S3;
|
||||||
|
using Amazon.S3.Model;
|
||||||
|
using GBSite.Api.Configuration;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace GBSite.Api.Services;
|
||||||
|
|
||||||
|
public class R2StorageService
|
||||||
|
{
|
||||||
|
private readonly IAmazonS3 _s3;
|
||||||
|
private readonly R2Settings _settings;
|
||||||
|
|
||||||
|
public R2StorageService(IAmazonS3 s3, IOptions<R2Settings> settings)
|
||||||
|
{
|
||||||
|
_s3 = s3;
|
||||||
|
_settings = settings.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> UploadAsync(
|
||||||
|
Stream stream,
|
||||||
|
string key,
|
||||||
|
string contentType,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var request = new PutObjectRequest
|
||||||
|
{
|
||||||
|
BucketName = _settings.BucketName,
|
||||||
|
Key = key,
|
||||||
|
InputStream = stream,
|
||||||
|
ContentType = contentType,
|
||||||
|
DisablePayloadSigning = true
|
||||||
|
};
|
||||||
|
|
||||||
|
await _s3.PutObjectAsync(request, ct);
|
||||||
|
|
||||||
|
return $"{_settings.PublicUrl.TrimEnd('/')}/{key}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(string key, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _s3.DeleteObjectAsync(_settings.BucketName, key, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,5 +5,12 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*",
|
||||||
|
"R2": {
|
||||||
|
"AccountId": "",
|
||||||
|
"AccessKeyId": "",
|
||||||
|
"SecretAccessKey": "",
|
||||||
|
"BucketName": "",
|
||||||
|
"PublicUrl": "https://cdn.goodbrick.com.ua"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
services:
|
services:
|
||||||
gb-dev-frontend:
|
gb-dev-frontend:
|
||||||
build: ../frontend
|
build:
|
||||||
|
context: ../frontend
|
||||||
|
args:
|
||||||
|
- NEXT_PUBLIC_POSTHOG_KEY=phc_pe0mP58n724h9eFxanbGIUsfMyS14gnAmr5tYez9V3Q
|
||||||
|
- NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
|
||||||
|
- NEXT_PUBLIC_CDN_URL=https://cdn.goodbrick.com.ua
|
||||||
container_name: gb-dev-frontend
|
container_name: gb-dev-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@@ -20,6 +25,11 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- ASPNETCORE_ENVIRONMENT=Development
|
- ASPNETCORE_ENVIRONMENT=Development
|
||||||
- ConnectionStrings__Default=Host=postgres;Port=5432;Database=dev_db;Username=dev_user;Password=dev_pass_vB6nM3qP8yW2rT9k
|
- ConnectionStrings__Default=Host=postgres;Port=5432;Database=dev_db;Username=dev_user;Password=dev_pass_vB6nM3qP8yW2rT9k
|
||||||
|
- R2__AccountId=${R2_ACCOUNT_ID}
|
||||||
|
- R2__AccessKeyId=${R2_ACCESS_KEY_ID}
|
||||||
|
- R2__SecretAccessKey=${R2_SECRET_ACCESS_KEY}
|
||||||
|
- R2__BucketName=${R2_BUCKET_NAME:-gb-bucket}
|
||||||
|
- R2__PublicUrl=https://cdn.goodbrick.com.ua
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
|
|
||||||
|
|||||||
14
deploy/docker-compose.local.yml
Normal file
14
deploy/docker-compose.local.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
gb-local-backend:
|
||||||
|
build: ../backend
|
||||||
|
container_name: gb-local-backend
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:5000:5000"
|
||||||
|
environment:
|
||||||
|
- ASPNETCORE_ENVIRONMENT=Development
|
||||||
|
- ConnectionStrings__Default=Host=host.docker.internal;Port=5433;Database=dev_db;Username=dev_user;Password=dev_pass_vB6nM3qP8yW2rT9k
|
||||||
|
- R2__AccountId=${R2_ACCOUNT_ID}
|
||||||
|
- R2__AccessKeyId=${R2_ACCESS_KEY_ID}
|
||||||
|
- R2__SecretAccessKey=${R2_SECRET_ACCESS_KEY}
|
||||||
|
- R2__BucketName=${R2_BUCKET_NAME:-gb-bucket}
|
||||||
|
- R2__PublicUrl=https://cdn.goodbrick.com.ua
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
services:
|
services:
|
||||||
gb-prod-frontend:
|
gb-prod-frontend:
|
||||||
build: ../frontend
|
build:
|
||||||
|
context: ../frontend
|
||||||
|
args:
|
||||||
|
- NEXT_PUBLIC_POSTHOG_KEY=phc_pe0mP58n724h9eFxanbGIUsfMyS14gnAmr5tYez9V3Q
|
||||||
|
- NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
|
||||||
|
- NEXT_PUBLIC_CDN_URL=https://cdn.goodbrick.com.ua
|
||||||
container_name: gb-prod-frontend
|
container_name: gb-prod-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@@ -20,6 +25,11 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- ASPNETCORE_ENVIRONMENT=Production
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
- ConnectionStrings__Default=Host=postgres;Port=5432;Database=prod_db;Username=prod_user;Password=prod_pass_kL9mN2pQ7xR8sT4v
|
- ConnectionStrings__Default=Host=postgres;Port=5432;Database=prod_db;Username=prod_user;Password=prod_pass_kL9mN2pQ7xR8sT4v
|
||||||
|
- R2__AccountId=${R2_ACCOUNT_ID}
|
||||||
|
- R2__AccessKeyId=${R2_ACCESS_KEY_ID}
|
||||||
|
- R2__SecretAccessKey=${R2_SECRET_ACCESS_KEY}
|
||||||
|
- R2__BucketName=${R2_BUCKET_NAME:-gb-bucket}
|
||||||
|
- R2__PublicUrl=https://cdn.goodbrick.com.ua
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
# URL бэкенда для server-side запросов (внутри Docker network)
|
# URL бэкенда для server-side запросов (внутри Docker network)
|
||||||
INTERNAL_API_URL=http://gb-prod-backend:5000
|
INTERNAL_API_URL=http://gb-prod-backend:5000
|
||||||
|
|
||||||
|
# URL бэкенда для проксирования /api/* в локальной разработке (не задавать на сервере!)
|
||||||
|
# LOCAL_API_URL=http://localhost:5000
|
||||||
|
|
||||||
|
# CDN URL для изображений (Cloudflare R2)
|
||||||
|
NEXT_PUBLIC_CDN_URL=https://cdn.goodbrick.com.ua
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ RUN npm ci
|
|||||||
|
|
||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
ARG NEXT_PUBLIC_POSTHOG_KEY
|
||||||
|
ARG NEXT_PUBLIC_POSTHOG_HOST
|
||||||
|
ARG NEXT_PUBLIC_CDN_URL
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|||||||
@@ -2,6 +2,21 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{ protocol: "https", hostname: "cdn.goodbrick.com.ua" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
async rewrites() {
|
||||||
|
const apiUrl = process.env.LOCAL_API_URL;
|
||||||
|
if (!apiUrl) return [];
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/api/:path*",
|
||||||
|
destination: `${apiUrl}/api/:path*`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
442
frontend/package-lock.json
generated
442
frontend/package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
|
"posthog-js": "^1.343.2",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
@@ -1975,6 +1976,332 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@opentelemetry/api": {
|
||||||
|
"version": "1.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||||
|
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/api-logs": {
|
||||||
|
"version": "0.208.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz",
|
||||||
|
"integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/api": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/core": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/exporter-logs-otlp-http": {
|
||||||
|
"version": "0.208.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.208.0.tgz",
|
||||||
|
"integrity": "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/api-logs": "0.208.0",
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/otlp-exporter-base": "0.208.0",
|
||||||
|
"@opentelemetry/otlp-transformer": "0.208.0",
|
||||||
|
"@opentelemetry/sdk-logs": "0.208.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/otlp-exporter-base": {
|
||||||
|
"version": "0.208.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz",
|
||||||
|
"integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/otlp-transformer": "0.208.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/otlp-transformer": {
|
||||||
|
"version": "0.208.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz",
|
||||||
|
"integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/api-logs": "0.208.0",
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/resources": "2.2.0",
|
||||||
|
"@opentelemetry/sdk-logs": "0.208.0",
|
||||||
|
"@opentelemetry/sdk-metrics": "2.2.0",
|
||||||
|
"@opentelemetry/sdk-trace-base": "2.2.0",
|
||||||
|
"protobufjs": "^7.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/resources": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/core": "2.5.0",
|
||||||
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/sdk-logs": {
|
||||||
|
"version": "0.208.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz",
|
||||||
|
"integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/api-logs": "0.208.0",
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/resources": "2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.4.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/sdk-metrics": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/resources": "2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.9.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/sdk-trace-base": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/resources": "2.2.0",
|
||||||
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/core": "2.2.0",
|
||||||
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@opentelemetry/semantic-conventions": {
|
||||||
|
"version": "1.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz",
|
||||||
|
"integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@posthog/core": {
|
||||||
|
"version": "1.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.20.2.tgz",
|
||||||
|
"integrity": "sha512-aQhrUzOHYr0z/bkwDsJ5hXahdh6oyeWdx2/CHwR6vFG3eK07J69lbuGOj+HGOOxJP1eAdNnsk8J0fj1vqRA9+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cross-spawn": "^7.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@posthog/types": {
|
||||||
|
"version": "1.343.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.343.2.tgz",
|
||||||
|
"integrity": "sha512-8+RAnTOf/85b2NU+b4cpUZsuftRZQOFgTNDNCb1LyntXNgf7yD7KYxprC1rCayCZ/YpkmYy3xXPytBVYTXwdqg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/aspromise": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/base64": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/codegen": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/eventemitter": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/fetch": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@protobufjs/aspromise": "^1.1.1",
|
||||||
|
"@protobufjs/inquire": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/float": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/inquire": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/path": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/pool": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/utf8": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/number": {
|
"node_modules/@radix-ui/number": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||||
@@ -3874,9 +4201,7 @@
|
|||||||
"version": "20.19.33",
|
"version": "20.19.33",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
|
||||||
"integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
|
"integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@@ -3910,6 +4235,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@types/validate-npm-package-name": {
|
"node_modules/@types/validate-npm-package-name": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz",
|
||||||
@@ -5319,6 +5651,17 @@
|
|||||||
"node": ">=6.6.0"
|
"node": ">=6.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/core-js": {
|
||||||
|
"version": "3.48.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
|
||||||
|
"integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/core-js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cors": {
|
"node_modules/cors": {
|
||||||
"version": "2.8.6",
|
"version": "2.8.6",
|
||||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
||||||
@@ -5368,7 +5711,6 @@
|
|||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"path-key": "^3.1.0",
|
"path-key": "^3.1.0",
|
||||||
@@ -5648,6 +5990,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "17.2.4",
|
"version": "17.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz",
|
||||||
@@ -6632,6 +6983,12 @@
|
|||||||
"node": "^12.20 || >= 14.13"
|
"node": "^12.20 || >= 14.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.4.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
|
||||||
|
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/figures": {
|
"node_modules/figures": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
|
||||||
@@ -7947,7 +8304,6 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/iterator.prototype": {
|
"node_modules/iterator.prototype": {
|
||||||
@@ -8486,6 +8842,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/long": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||||
|
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/loose-envify": {
|
"node_modules/loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
@@ -9338,7 +9700,6 @@
|
|||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -9440,6 +9801,27 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/posthog-js": {
|
||||||
|
"version": "1.343.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.343.2.tgz",
|
||||||
|
"integrity": "sha512-Aq5EG/knjf21Kx+rbxMHyyFknOfU49Z+b5WhbrQDiUQC5R2Dp/w1IPpQPNh0i0pScuPFyzG3YVisxRscFhuGqQ==",
|
||||||
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@opentelemetry/api-logs": "^0.208.0",
|
||||||
|
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
|
||||||
|
"@opentelemetry/resources": "^2.2.0",
|
||||||
|
"@opentelemetry/sdk-logs": "^0.208.0",
|
||||||
|
"@posthog/core": "1.20.2",
|
||||||
|
"@posthog/types": "1.343.2",
|
||||||
|
"core-js": "^3.38.1",
|
||||||
|
"dompurify": "^3.3.1",
|
||||||
|
"fflate": "^0.4.8",
|
||||||
|
"preact": "^10.28.2",
|
||||||
|
"query-selector-shadow-dom": "^1.0.1",
|
||||||
|
"web-vitals": "^5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/powershell-utils": {
|
"node_modules/powershell-utils": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
|
||||||
@@ -9453,6 +9835,16 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.28.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.3.tgz",
|
||||||
|
"integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@@ -9515,6 +9907,30 @@
|
|||||||
"react-is": "^16.13.1"
|
"react-is": "^16.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/protobufjs": {
|
||||||
|
"version": "7.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
||||||
|
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@protobufjs/aspromise": "^1.1.2",
|
||||||
|
"@protobufjs/base64": "^1.1.2",
|
||||||
|
"@protobufjs/codegen": "^2.0.4",
|
||||||
|
"@protobufjs/eventemitter": "^1.1.0",
|
||||||
|
"@protobufjs/fetch": "^1.1.0",
|
||||||
|
"@protobufjs/float": "^1.0.2",
|
||||||
|
"@protobufjs/inquire": "^1.1.0",
|
||||||
|
"@protobufjs/path": "^1.1.2",
|
||||||
|
"@protobufjs/pool": "^1.1.0",
|
||||||
|
"@protobufjs/utf8": "^1.1.0",
|
||||||
|
"@types/node": ">=13.7.0",
|
||||||
|
"long": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@@ -9555,6 +9971,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/query-selector-shadow-dom": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
@@ -10345,7 +10767,6 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"shebang-regex": "^3.0.0"
|
"shebang-regex": "^3.0.0"
|
||||||
@@ -10358,7 +10779,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -11203,7 +11623,6 @@
|
|||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unicorn-magic": {
|
"node_modules/unicorn-magic": {
|
||||||
@@ -11414,11 +11833,16 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/web-vitals": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isexe": "^2.0.0"
|
"isexe": "^2.0.0"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
|
"posthog-js": "^1.343.2",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
|
|||||||
5
frontend/public/robots.txt
Normal file
5
frontend/public/robots.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
Allow: /_next/image
|
||||||
|
|
||||||
|
Sitemap: https://new.goodbrick.com.ua/sitemap.xml
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import { PostHogProvider } from "@/lib/posthog";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
@@ -27,7 +28,7 @@ export default function RootLayout({
|
|||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
{children}
|
<PostHogProvider>{children}</PostHogProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { PostHogTestButton } from "./posthog-test-button";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center">
|
<div className="flex min-h-screen items-center justify-center">
|
||||||
@@ -13,6 +15,7 @@ export default function Home() {
|
|||||||
<span>+</span>
|
<span>+</span>
|
||||||
<span>.NET 9</span>
|
<span>.NET 9</span>
|
||||||
</div>
|
</div>
|
||||||
|
<PostHogTestButton />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
18
frontend/src/app/posthog-test-button.tsx
Normal file
18
frontend/src/app/posthog-test-button.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { usePostHog } from "posthog-js/react";
|
||||||
|
|
||||||
|
export function PostHogTestButton() {
|
||||||
|
const posthog = usePostHog();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
posthog.capture("my_custom_event", { property: "value" })
|
||||||
|
}
|
||||||
|
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
|
||||||
|
>
|
||||||
|
Send PostHog Test Event
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
frontend/src/lib/cdn.ts
Normal file
6
frontend/src/lib/cdn.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
const CDN_BASE_URL =
|
||||||
|
process.env.NEXT_PUBLIC_CDN_URL || "https://cdn.goodbrick.com.ua";
|
||||||
|
|
||||||
|
export function cdnUrl(path: string): string {
|
||||||
|
return `${CDN_BASE_URL.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
||||||
|
}
|
||||||
52
frontend/src/lib/posthog.tsx
Normal file
52
frontend/src/lib/posthog.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import posthog from "posthog-js";
|
||||||
|
import { PostHogProvider as PHProvider, usePostHog } from "posthog-js/react";
|
||||||
|
import { useEffect, Suspense } from "react";
|
||||||
|
import { usePathname, useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
|
const POSTHOG_KEY = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
||||||
|
const POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://eu.i.posthog.com";
|
||||||
|
|
||||||
|
function PostHogPageView() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const ph = usePostHog();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pathname && ph) {
|
||||||
|
let url = window.origin + pathname;
|
||||||
|
if (searchParams.toString()) {
|
||||||
|
url += "?" + searchParams.toString();
|
||||||
|
}
|
||||||
|
ph.capture("$pageview", { $current_url: url });
|
||||||
|
}
|
||||||
|
}, [pathname, searchParams, ph]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (POSTHOG_KEY) {
|
||||||
|
posthog.init(POSTHOG_KEY, {
|
||||||
|
api_host: POSTHOG_HOST,
|
||||||
|
capture_pageview: false, // manual pageview tracking for SPA
|
||||||
|
capture_pageleave: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!POSTHOG_KEY) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PHProvider client={posthog}>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<PostHogPageView />
|
||||||
|
</Suspense>
|
||||||
|
{children}
|
||||||
|
</PHProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
scripts/local-backend-docker.ps1
Normal file
4
scripts/local-backend-docker.ps1
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Write-Host "Starting Docker backend on http://localhost:5000" -ForegroundColor Cyan
|
||||||
|
Write-Host "Requires: SSH tunnel running (scripts/local-tunnel.ps1)"
|
||||||
|
Write-Host ""
|
||||||
|
docker compose -f "$PSScriptRoot\..\deploy\docker-compose.local.yml" up --build
|
||||||
6
scripts/local-backend-dotnet.ps1
Normal file
6
scripts/local-backend-dotnet.ps1
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Write-Host "Starting backend (dotnet run) on http://localhost:5000" -ForegroundColor Cyan
|
||||||
|
Write-Host "Requires: SSH tunnel running (scripts/local-tunnel.ps1)"
|
||||||
|
Write-Host ""
|
||||||
|
Push-Location "$PSScriptRoot\..\backend"
|
||||||
|
dotnet run --project src/GBSite.Api
|
||||||
|
Pop-Location
|
||||||
6
scripts/local-frontend.ps1
Normal file
6
scripts/local-frontend.ps1
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Write-Host "Starting frontend on http://localhost:3000" -ForegroundColor Cyan
|
||||||
|
Write-Host "API proxy -> http://localhost:5000"
|
||||||
|
Write-Host ""
|
||||||
|
Push-Location "$PSScriptRoot\..\frontend"
|
||||||
|
npm run dev
|
||||||
|
Pop-Location
|
||||||
4
scripts/local-tunnel.ps1
Normal file
4
scripts/local-tunnel.ps1
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Write-Host "SSH Tunnel: localhost:5433 -> server:5432 (dev_db)" -ForegroundColor Cyan
|
||||||
|
Write-Host "Press Ctrl+C to stop"
|
||||||
|
Write-Host ""
|
||||||
|
ssh -N -L 5433:127.0.0.1:5432 -o ServerAliveInterval=60 deploy@31.131.18.254
|
||||||
184
scripts/replicate-db.sh
Normal file
184
scripts/replicate-db.sh
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# replicate-db.sh — Nightly prod_db -> dev_db replication with backups
|
||||||
|
# Runs via cron as user "deploy" at 04:00 Kyiv time
|
||||||
|
#
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- Lock (prevent parallel runs) ---
|
||||||
|
LOCK_FILE="/tmp/replicate-db.lock"
|
||||||
|
exec 200>"$LOCK_FILE"
|
||||||
|
flock -n 200 || { echo "[$(date)] Another instance is already running. Exiting."; exit 1; }
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
POSTGRES_CONTAINER="postgres"
|
||||||
|
SUPERUSER="app"
|
||||||
|
SUPERUSER_PASS="zYWT5JWu3iAbbW7mOyd1"
|
||||||
|
|
||||||
|
SOURCE_DB="prod_db"
|
||||||
|
TARGET_DB="dev_db"
|
||||||
|
TARGET_USER="dev_user"
|
||||||
|
|
||||||
|
DEV_BACKEND_CONTAINER="gb-dev-backend"
|
||||||
|
DEV_COMPOSE_DIR="/srv/apps/gb-site-dev/deploy"
|
||||||
|
DEV_COMPOSE_FILE="docker-compose.dev.yml"
|
||||||
|
|
||||||
|
BACKUP_DIR="/srv/backups"
|
||||||
|
BACKUP_RETENTION_DAYS=14
|
||||||
|
|
||||||
|
LOG_DIR="/srv/logs"
|
||||||
|
TIMESTAMP=$(date '+%Y%m%d-%H%M%S')
|
||||||
|
LOG_FILE="${LOG_DIR}/replicate-db-${TIMESTAMP}.log"
|
||||||
|
LOG_RETENTION_DAYS=30
|
||||||
|
|
||||||
|
CONTAINER_DUMP="/tmp/prod_dump.dump"
|
||||||
|
|
||||||
|
# --- Logging ---
|
||||||
|
mkdir -p "$LOG_DIR" "$BACKUP_DIR"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
local ts
|
||||||
|
ts=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
echo "[$ts] $1" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
local ts
|
||||||
|
ts=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
echo "[$ts] ERROR: $1" | tee -a "$LOG_FILE" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Cleanup on exit ---
|
||||||
|
cleanup() {
|
||||||
|
local exit_code=$?
|
||||||
|
|
||||||
|
# Remove dump from container
|
||||||
|
log "Cleaning up dump file inside container..."
|
||||||
|
docker exec "$POSTGRES_CONTAINER" rm -f "$CONTAINER_DUMP" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Always restart dev backend
|
||||||
|
if ! docker ps --format '{{.Names}}' | grep -q "^${DEV_BACKEND_CONTAINER}$"; then
|
||||||
|
log "Restarting dev backend..."
|
||||||
|
cd "$DEV_COMPOSE_DIR" && docker compose -f "$DEV_COMPOSE_FILE" up -d gb-dev-backend 2>>"$LOG_FILE" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean old logs
|
||||||
|
find "$LOG_DIR" -name "replicate-db-*.log" -mtime +${LOG_RETENTION_DAYS} -delete 2>/dev/null || true
|
||||||
|
|
||||||
|
if [ $exit_code -eq 0 ]; then
|
||||||
|
log "=== Replication completed successfully ==="
|
||||||
|
else
|
||||||
|
log_error "=== Replication FAILED with exit code $exit_code ==="
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# ===== MAIN =====
|
||||||
|
log "=== Starting prod_db -> dev_db replication ==="
|
||||||
|
|
||||||
|
# 1. Check postgres container
|
||||||
|
log "Step 1: Checking postgres container..."
|
||||||
|
if ! docker ps --format '{{.Names}}' | grep -q "^${POSTGRES_CONTAINER}$"; then
|
||||||
|
log_error "PostgreSQL container is not running!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Backup prod_db to host filesystem
|
||||||
|
log "Step 2: Backing up prod_db..."
|
||||||
|
BACKUP_FILE="${BACKUP_DIR}/prod_db_${TIMESTAMP}.dump"
|
||||||
|
|
||||||
|
docker exec -e PGPASSWORD="$SUPERUSER_PASS" "$POSTGRES_CONTAINER" \
|
||||||
|
pg_dump -U "$SUPERUSER" -d "$SOURCE_DB" \
|
||||||
|
--format=custom \
|
||||||
|
--file=/tmp/prod_backup.dump \
|
||||||
|
2>>"$LOG_FILE"
|
||||||
|
|
||||||
|
docker cp "${POSTGRES_CONTAINER}:/tmp/prod_backup.dump" "$BACKUP_FILE" 2>>"$LOG_FILE"
|
||||||
|
docker exec "$POSTGRES_CONTAINER" rm -f /tmp/prod_backup.dump 2>/dev/null || true
|
||||||
|
|
||||||
|
BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
||||||
|
log "Backup saved: $BACKUP_FILE ($BACKUP_SIZE)"
|
||||||
|
|
||||||
|
# 3. Rotate old backups
|
||||||
|
log "Step 3: Rotating backups older than ${BACKUP_RETENTION_DAYS} days..."
|
||||||
|
DELETED_COUNT=$(find "$BACKUP_DIR" -name "prod_db_*.dump" -mtime +${BACKUP_RETENTION_DAYS} -delete -print | wc -l)
|
||||||
|
log "Deleted $DELETED_COUNT old backup(s)."
|
||||||
|
|
||||||
|
# 4. Stop dev backend
|
||||||
|
log "Step 4: Stopping dev backend..."
|
||||||
|
docker stop "$DEV_BACKEND_CONTAINER" 2>>"$LOG_FILE" || log "Warning: dev backend was already stopped."
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# 5. Dump prod_db for replication (inside container)
|
||||||
|
log "Step 5: Dumping prod_db for replication..."
|
||||||
|
docker exec -e PGPASSWORD="$SUPERUSER_PASS" "$POSTGRES_CONTAINER" \
|
||||||
|
pg_dump -U "$SUPERUSER" -d "$SOURCE_DB" \
|
||||||
|
--format=custom \
|
||||||
|
--no-owner \
|
||||||
|
--no-acl \
|
||||||
|
--file="$CONTAINER_DUMP" \
|
||||||
|
2>>"$LOG_FILE"
|
||||||
|
log "Dump completed."
|
||||||
|
|
||||||
|
# 6. Terminate connections to dev_db
|
||||||
|
log "Step 6: Terminating connections to dev_db..."
|
||||||
|
docker exec -e PGPASSWORD="$SUPERUSER_PASS" "$POSTGRES_CONTAINER" \
|
||||||
|
psql -U "$SUPERUSER" -d postgres -c \
|
||||||
|
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${TARGET_DB}' AND pid <> pg_backend_pid();" \
|
||||||
|
>>"$LOG_FILE" 2>&1 || true
|
||||||
|
|
||||||
|
# 7. Drop and recreate dev_db
|
||||||
|
log "Step 7: Recreating dev_db..."
|
||||||
|
docker exec -e PGPASSWORD="$SUPERUSER_PASS" "$POSTGRES_CONTAINER" \
|
||||||
|
psql -U "$SUPERUSER" -d postgres -c "DROP DATABASE IF EXISTS ${TARGET_DB};" \
|
||||||
|
>>"$LOG_FILE" 2>&1
|
||||||
|
|
||||||
|
docker exec -e PGPASSWORD="$SUPERUSER_PASS" "$POSTGRES_CONTAINER" \
|
||||||
|
psql -U "$SUPERUSER" -d postgres -c "CREATE DATABASE ${TARGET_DB} OWNER ${TARGET_USER};" \
|
||||||
|
>>"$LOG_FILE" 2>&1
|
||||||
|
log "Database recreated."
|
||||||
|
|
||||||
|
# 8. Restore dump into dev_db
|
||||||
|
log "Step 8: Restoring into dev_db..."
|
||||||
|
docker exec -e PGPASSWORD="$SUPERUSER_PASS" "$POSTGRES_CONTAINER" \
|
||||||
|
pg_restore -U "$SUPERUSER" -d "$TARGET_DB" \
|
||||||
|
--no-owner \
|
||||||
|
--no-acl \
|
||||||
|
--role="$TARGET_USER" \
|
||||||
|
"$CONTAINER_DUMP" \
|
||||||
|
2>>"$LOG_FILE"
|
||||||
|
log "Restore completed."
|
||||||
|
|
||||||
|
# 9. Grant privileges to dev_user
|
||||||
|
# Note: --role=dev_user in pg_restore already sets object ownership
|
||||||
|
log "Step 9: Granting privileges to dev_user..."
|
||||||
|
docker exec -e PGPASSWORD="$SUPERUSER_PASS" "$POSTGRES_CONTAINER" \
|
||||||
|
psql -U "$SUPERUSER" -d "$TARGET_DB" -c "
|
||||||
|
ALTER SCHEMA public OWNER TO ${TARGET_USER};
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE ${TARGET_DB} TO ${TARGET_USER};
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${TARGET_USER};
|
||||||
|
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${TARGET_USER};
|
||||||
|
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO ${TARGET_USER};
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${TARGET_USER};
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${TARGET_USER};
|
||||||
|
" >>"$LOG_FILE" 2>&1
|
||||||
|
log "Privileges granted."
|
||||||
|
|
||||||
|
# 10. Start dev backend
|
||||||
|
log "Step 10: Starting dev backend..."
|
||||||
|
cd "$DEV_COMPOSE_DIR" && docker compose -f "$DEV_COMPOSE_FILE" up -d gb-dev-backend \
|
||||||
|
2>>"$LOG_FILE"
|
||||||
|
log "Dev backend started."
|
||||||
|
|
||||||
|
# 11. Health check
|
||||||
|
log "Step 11: Health check..."
|
||||||
|
sleep 5
|
||||||
|
HEALTH=$(curl -sf http://127.0.0.1:5200/api/health/ping 2>/dev/null || echo "FAILED")
|
||||||
|
if [ "$HEALTH" = "FAILED" ]; then
|
||||||
|
log "Warning: Health check did not respond (backend may still be starting)."
|
||||||
|
else
|
||||||
|
log "Health check OK: $HEALTH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "=== Replication finished ==="
|
||||||
Reference in New Issue
Block a user