first commit
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
DATABASE_URL=postgres://panel:panel_secret@localhost:5432/panel?sslmode=disable
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.env
|
||||||
|
*.exe
|
||||||
|
/bin/
|
||||||
|
/dist/
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
FROM migrate/migrate:v4.18.2
|
||||||
|
|
||||||
|
COPY migrations /migrations
|
||||||
|
|
||||||
|
ENTRYPOINT ["migrate", "-path", "/migrations", "-database"]
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# PanelHosting
|
||||||
|
|
||||||
|
Панель управления хостингом (аналог FastPanel) на Go + Docker + PostgreSQL.
|
||||||
|
|
||||||
|
Биллинг и оплата — в планах. Сейчас реализована база данных и инфраструктура для панели.
|
||||||
|
|
||||||
|
## Стек
|
||||||
|
|
||||||
|
- **Go 1.23** — backend
|
||||||
|
- **PostgreSQL 16** — хранение данных
|
||||||
|
- **Docker Compose** — локальная разработка
|
||||||
|
- **golang-migrate** — миграции схемы
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
### 1. Переменные окружения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Запуск PostgreSQL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Миграции
|
||||||
|
|
||||||
|
Через Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --profile migrate run --rm migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
Или локально:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export DATABASE_URL=postgres://panel:panel_secret@localhost:5432/panel?sslmode=disable
|
||||||
|
go run ./cmd/migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Проверка подключения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/panel
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
cmd/
|
||||||
|
migrate/ — CLI для применения миграций
|
||||||
|
panel/ — точка входа панели (API в разработке)
|
||||||
|
internal/
|
||||||
|
config/ — конфигурация из env
|
||||||
|
database/ — пул соединений и миграции
|
||||||
|
models/ — Go-модели
|
||||||
|
migrations/ — SQL-схема
|
||||||
|
```
|
||||||
|
|
||||||
|
## Схема БД
|
||||||
|
|
||||||
|
| Раздел | Таблицы |
|
||||||
|
|--------|---------|
|
||||||
|
| Auth | `users`, `sessions`, `api_keys` |
|
||||||
|
| Серверы | `servers`, `server_access` |
|
||||||
|
| Сайты | `sites`, `domains` |
|
||||||
|
| Сервисы | `databases`, `database_users`, `ftp_accounts`, `ssl_certificates` |
|
||||||
|
| Почта | `mail_domains`, `mail_accounts` |
|
||||||
|
| DNS | `dns_zones`, `dns_records` |
|
||||||
|
| Операции | `cron_jobs`, `backups`, `jobs` |
|
||||||
|
| Аудит | `audit_logs` |
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Proprietary
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/panelhosting/panel/internal/config"
|
||||||
|
"github.com/panelhosting/panel/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
down := flag.Bool("down", false, "rollback all migrations")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationsPath, err := resolveMigrationsPath()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *down {
|
||||||
|
if err := database.RollbackMigrations(cfg.DatabaseURL, migrationsPath); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("migrations rolled back")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.RunMigrations(cfg.DatabaseURL, migrationsPath); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("migrations applied")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
pool, err := database.NewPool(ctx, cfg.DatabaseURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer pool.Close()
|
||||||
|
log.Println("database connection ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveMigrationsPath() (string, error) {
|
||||||
|
if p := os.Getenv("MIGRATIONS_PATH"); p != "" {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(wd, "migrations"), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/panelhosting/panel/internal/config"
|
||||||
|
"github.com/panelhosting/panel/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
pool, err := database.NewPool(ctx, cfg.DatabaseURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer pool.Close()
|
||||||
|
|
||||||
|
log.Println("panel database ready (API coming soon)")
|
||||||
|
<-ctx.Done()
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: panel-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: panel
|
||||||
|
POSTGRES_PASSWORD: panel_secret
|
||||||
|
POSTGRES_DB: panel
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U panel -d panel"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
migrate:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.migrate
|
||||||
|
container_name: panel-migrate
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
command:
|
||||||
|
- "postgres://panel:panel_secret@postgres:5432/panel?sslmode=disable"
|
||||||
|
- "up"
|
||||||
|
profiles:
|
||||||
|
- migrate
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
module github.com/panelhosting/panel
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/jackc/pgx/v5 v5.7.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
|
golang.org/x/crypto v0.31.0 // indirect
|
||||||
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
|
golang.org/x/text v0.21.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8=
|
||||||
|
github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM=
|
||||||
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
|
||||||
|
github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||||
|
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||||
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
|
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
||||||
|
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
||||||
|
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
||||||
|
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
|
||||||
|
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
|
||||||
|
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
||||||
|
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
||||||
|
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||||
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
DatabaseURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() (*Config, error) {
|
||||||
|
url := os.Getenv("DATABASE_URL")
|
||||||
|
if url == "" {
|
||||||
|
return nil, fmt.Errorf("DATABASE_URL is required")
|
||||||
|
}
|
||||||
|
return &Config{DatabaseURL: url}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RunMigrations(databaseURL, migrationsPath string) error {
|
||||||
|
m, err := migrate.New(
|
||||||
|
fmt.Sprintf("file://%s", migrationsPath),
|
||||||
|
databaseURL,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create migrator: %w", err)
|
||||||
|
}
|
||||||
|
defer m.Close()
|
||||||
|
|
||||||
|
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||||
|
return fmt.Errorf("migrate up: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RollbackMigrations(databaseURL, migrationsPath string) error {
|
||||||
|
m, err := migrate.New(
|
||||||
|
fmt.Sprintf("file://%s", migrationsPath),
|
||||||
|
databaseURL,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create migrator: %w", err)
|
||||||
|
}
|
||||||
|
defer m.Close()
|
||||||
|
|
||||||
|
if err := m.Down(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||||
|
return fmt.Errorf("migrate down: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewPool(ctx context.Context, databaseURL string) (*pgxpool.Pool, error) {
|
||||||
|
cfg, err := pgxpool.ParseConfig(databaseURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse database url: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.MaxConns = 20
|
||||||
|
cfg.MinConns = 2
|
||||||
|
cfg.MaxConnLifetime = time.Hour
|
||||||
|
cfg.MaxConnIdleTime = 30 * time.Minute
|
||||||
|
cfg.HealthCheckPeriod = time.Minute
|
||||||
|
|
||||||
|
pool, err := pgxpool.NewWithConfig(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create pool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pool.Ping(ctx); err != nil {
|
||||||
|
pool.Close()
|
||||||
|
return nil, fmt.Errorf("ping database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserStatusActive UserStatus = "active"
|
||||||
|
UserStatusSuspended UserStatus = "suspended"
|
||||||
|
UserStatusPending UserStatus = "pending"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserRole string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RoleSuperAdmin UserRole = "superadmin"
|
||||||
|
RoleAdmin UserRole = "admin"
|
||||||
|
RoleReseller UserRole = "reseller"
|
||||||
|
RoleUser UserRole = "user"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
UUID uuid.UUID `json:"uuid"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
PasswordHash string `json:"-"`
|
||||||
|
Role UserRole `json:"role"`
|
||||||
|
Status UserStatus `json:"status"`
|
||||||
|
Locale string `json:"locale"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ServerOnline ServerStatus = "online"
|
||||||
|
ServerOffline ServerStatus = "offline"
|
||||||
|
ServerMaintenance ServerStatus = "maintenance"
|
||||||
|
ServerProvisioning ServerStatus = "provisioning"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
UUID uuid.UUID `json:"uuid"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
IPAddress string `json:"ip_address"`
|
||||||
|
SSHPort int `json:"ssh_port"`
|
||||||
|
Status ServerStatus `json:"status"`
|
||||||
|
AgentVersion *string `json:"agent_version,omitempty"`
|
||||||
|
OSInfo map[string]any `json:"os_info"`
|
||||||
|
Resources map[string]any `json:"resources"`
|
||||||
|
Settings map[string]any `json:"settings"`
|
||||||
|
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SiteStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SiteActive SiteStatus = "active"
|
||||||
|
SiteSuspended SiteStatus = "suspended"
|
||||||
|
SiteCreating SiteStatus = "creating"
|
||||||
|
SiteDeleting SiteStatus = "deleting"
|
||||||
|
SiteError SiteStatus = "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Site struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
UUID uuid.UUID `json:"uuid"`
|
||||||
|
ServerID int64 `json:"server_id"`
|
||||||
|
OwnerID int64 `json:"owner_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DocumentRoot string `json:"document_root"`
|
||||||
|
PHPVersion *string `json:"php_version,omitempty"`
|
||||||
|
Status SiteStatus `json:"status"`
|
||||||
|
Settings map[string]any `json:"settings"`
|
||||||
|
DiskQuotaMB *int `json:"disk_quota_mb,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Domain struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
SiteID int64 `json:"site_id"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
IsPrimary bool `json:"is_primary"`
|
||||||
|
SSLEnabled bool `json:"ssl_enabled"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DBEngine string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EngineMySQL DBEngine = "mysql"
|
||||||
|
EnginePostgreSQL DBEngine = "postgresql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Database struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
UUID uuid.UUID `json:"uuid"`
|
||||||
|
SiteID int64 `json:"site_id"`
|
||||||
|
ServerID int64 `json:"server_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Engine DBEngine `json:"engine"`
|
||||||
|
Charset string `json:"charset"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
JobPending JobStatus = "pending"
|
||||||
|
JobRunning JobStatus = "running"
|
||||||
|
JobCompleted JobStatus = "completed"
|
||||||
|
JobFailed JobStatus = "failed"
|
||||||
|
JobCancelled JobStatus = "cancelled"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Job struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
UUID uuid.UUID `json:"uuid"`
|
||||||
|
ServerID *int64 `json:"server_id,omitempty"`
|
||||||
|
SiteID *int64 `json:"site_id,omitempty"`
|
||||||
|
UserID *int64 `json:"user_id,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Payload map[string]any `json:"payload"`
|
||||||
|
Status JobStatus `json:"status"`
|
||||||
|
Result map[string]any `json:"result,omitempty"`
|
||||||
|
ErrorMessage *string `json:"error_message,omitempty"`
|
||||||
|
Attempts int `json:"attempts"`
|
||||||
|
MaxAttempts int `json:"max_attempts"`
|
||||||
|
ScheduledAt time.Time `json:"scheduled_at"`
|
||||||
|
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||||
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
DROP TRIGGER IF EXISTS trg_cron_jobs_updated_at ON cron_jobs;
|
||||||
|
DROP TRIGGER IF EXISTS trg_dns_records_updated_at ON dns_records;
|
||||||
|
DROP TRIGGER IF EXISTS trg_dns_zones_updated_at ON dns_zones;
|
||||||
|
DROP TRIGGER IF EXISTS trg_mail_accounts_updated_at ON mail_accounts;
|
||||||
|
DROP TRIGGER IF EXISTS trg_ssl_certificates_updated_at ON ssl_certificates;
|
||||||
|
DROP TRIGGER IF EXISTS trg_ftp_accounts_updated_at ON ftp_accounts;
|
||||||
|
DROP TRIGGER IF EXISTS trg_sites_updated_at ON sites;
|
||||||
|
DROP TRIGGER IF EXISTS trg_servers_updated_at ON servers;
|
||||||
|
DROP TRIGGER IF EXISTS trg_users_updated_at ON users;
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS set_updated_at();
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS audit_logs;
|
||||||
|
DROP TABLE IF EXISTS jobs;
|
||||||
|
DROP TABLE IF EXISTS backups;
|
||||||
|
DROP TABLE IF EXISTS cron_jobs;
|
||||||
|
DROP TABLE IF EXISTS dns_records;
|
||||||
|
DROP TABLE IF EXISTS dns_zones;
|
||||||
|
DROP TABLE IF EXISTS mail_accounts;
|
||||||
|
DROP TABLE IF EXISTS mail_domains;
|
||||||
|
DROP TABLE IF EXISTS ssl_certificates;
|
||||||
|
DROP TABLE IF EXISTS ftp_accounts;
|
||||||
|
DROP TABLE IF EXISTS database_users;
|
||||||
|
DROP TABLE IF EXISTS databases;
|
||||||
|
DROP TABLE IF EXISTS domains;
|
||||||
|
DROP TABLE IF EXISTS sites;
|
||||||
|
DROP TABLE IF EXISTS server_access;
|
||||||
|
DROP TABLE IF EXISTS servers;
|
||||||
|
DROP TABLE IF EXISTS api_keys;
|
||||||
|
DROP TABLE IF EXISTS sessions;
|
||||||
|
DROP TABLE IF EXISTS users;
|
||||||
|
|
||||||
|
DROP TYPE IF EXISTS dns_record_type;
|
||||||
|
DROP TYPE IF EXISTS job_status;
|
||||||
|
DROP TYPE IF EXISTS backup_status;
|
||||||
|
DROP TYPE IF EXISTS backup_type;
|
||||||
|
DROP TYPE IF EXISTS db_engine;
|
||||||
|
DROP TYPE IF EXISTS ssl_status;
|
||||||
|
DROP TYPE IF EXISTS ssl_type;
|
||||||
|
DROP TYPE IF EXISTS site_status;
|
||||||
|
DROP TYPE IF EXISTS server_status;
|
||||||
|
DROP TYPE IF EXISTS user_role;
|
||||||
|
DROP TYPE IF EXISTS user_status;
|
||||||
|
|
||||||
|
DROP EXTENSION IF EXISTS citext;
|
||||||
|
DROP EXTENSION IF EXISTS pgcrypto;
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
-- Panel hosting control panel — initial schema
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "citext";
|
||||||
|
|
||||||
|
-- ─── Enums ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE TYPE user_status AS ENUM ('active', 'suspended', 'pending');
|
||||||
|
CREATE TYPE user_role AS ENUM ('superadmin', 'admin', 'reseller', 'user');
|
||||||
|
|
||||||
|
CREATE TYPE server_status AS ENUM ('online', 'offline', 'maintenance', 'provisioning');
|
||||||
|
CREATE TYPE site_status AS ENUM ('active', 'suspended', 'creating', 'deleting', 'error');
|
||||||
|
CREATE TYPE ssl_type AS ENUM ('letsencrypt', 'custom', 'self_signed');
|
||||||
|
CREATE TYPE ssl_status AS ENUM ('pending', 'active', 'expired', 'error');
|
||||||
|
CREATE TYPE db_engine AS ENUM ('mysql', 'postgresql');
|
||||||
|
CREATE TYPE backup_type AS ENUM ('full', 'files', 'database');
|
||||||
|
CREATE TYPE backup_status AS ENUM ('pending', 'running', 'completed', 'failed');
|
||||||
|
CREATE TYPE job_status AS ENUM ('pending', 'running', 'completed', 'failed', 'cancelled');
|
||||||
|
CREATE TYPE dns_record_type AS ENUM ('A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SRV', 'PTR');
|
||||||
|
|
||||||
|
-- ─── Users & auth ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE TABLE users (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
uuid UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
|
||||||
|
email CITEXT NOT NULL UNIQUE,
|
||||||
|
username VARCHAR(64) NOT NULL UNIQUE,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
role user_role NOT NULL DEFAULT 'user',
|
||||||
|
status user_status NOT NULL DEFAULT 'pending',
|
||||||
|
locale VARCHAR(10) NOT NULL DEFAULT 'ru',
|
||||||
|
timezone VARCHAR(64) NOT NULL DEFAULT 'Europe/Moscow',
|
||||||
|
last_login_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE sessions (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
token_hash TEXT NOT NULL UNIQUE,
|
||||||
|
ip_address INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
|
||||||
|
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
|
||||||
|
|
||||||
|
CREATE TABLE api_keys (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(128) NOT NULL,
|
||||||
|
key_hash TEXT NOT NULL UNIQUE,
|
||||||
|
key_prefix VARCHAR(16) NOT NULL,
|
||||||
|
last_used_at TIMESTAMPTZ,
|
||||||
|
expires_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_api_keys_user_id ON api_keys(user_id);
|
||||||
|
|
||||||
|
-- ─── Servers (managed nodes) ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE TABLE servers (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
uuid UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
|
||||||
|
name VARCHAR(128) NOT NULL,
|
||||||
|
hostname VARCHAR(255) NOT NULL,
|
||||||
|
ip_address INET NOT NULL,
|
||||||
|
ssh_port INT NOT NULL DEFAULT 22,
|
||||||
|
status server_status NOT NULL DEFAULT 'provisioning',
|
||||||
|
agent_token_hash TEXT,
|
||||||
|
agent_version VARCHAR(32),
|
||||||
|
os_info JSONB NOT NULL DEFAULT '{}',
|
||||||
|
resources JSONB NOT NULL DEFAULT '{}',
|
||||||
|
settings JSONB NOT NULL DEFAULT '{}',
|
||||||
|
last_seen_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_servers_hostname ON servers(hostname);
|
||||||
|
|
||||||
|
-- User access to servers (multi-tenant / reseller model)
|
||||||
|
CREATE TABLE server_access (
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
||||||
|
can_manage BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (user_id, server_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ─── Sites (websites) ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE TABLE sites (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
uuid UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
|
||||||
|
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE RESTRICT,
|
||||||
|
owner_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||||
|
name VARCHAR(128) NOT NULL,
|
||||||
|
document_root VARCHAR(512) NOT NULL,
|
||||||
|
php_version VARCHAR(16),
|
||||||
|
status site_status NOT NULL DEFAULT 'creating',
|
||||||
|
settings JSONB NOT NULL DEFAULT '{}',
|
||||||
|
disk_quota_mb INT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (server_id, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_sites_owner_id ON sites(owner_id);
|
||||||
|
CREATE INDEX idx_sites_server_id ON sites(server_id);
|
||||||
|
|
||||||
|
CREATE TABLE domains (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
site_id BIGINT NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
||||||
|
domain CITEXT NOT NULL,
|
||||||
|
is_primary BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ssl_enabled BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (domain)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_domains_site_id ON domains(site_id);
|
||||||
|
|
||||||
|
-- Only one primary domain per site
|
||||||
|
CREATE UNIQUE INDEX idx_domains_site_primary
|
||||||
|
ON domains(site_id) WHERE is_primary = true;
|
||||||
|
|
||||||
|
-- ─── Databases ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE TABLE databases (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
uuid UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
|
||||||
|
site_id BIGINT NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
||||||
|
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE RESTRICT,
|
||||||
|
name VARCHAR(64) NOT NULL,
|
||||||
|
engine db_engine NOT NULL DEFAULT 'mysql',
|
||||||
|
charset VARCHAR(32) NOT NULL DEFAULT 'utf8mb4',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (server_id, engine, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_databases_site_id ON databases(site_id);
|
||||||
|
|
||||||
|
CREATE TABLE database_users (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
database_id BIGINT NOT NULL REFERENCES databases(id) ON DELETE CASCADE,
|
||||||
|
username VARCHAR(64) NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
privileges TEXT[] NOT NULL DEFAULT '{ALL}',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (database_id, username)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ─── FTP ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE TABLE ftp_accounts (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
site_id BIGINT NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
||||||
|
username VARCHAR(64) NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
home_path VARCHAR(512) NOT NULL,
|
||||||
|
quota_mb INT,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (site_id, username)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ─── SSL certificates ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE TABLE ssl_certificates (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
domain_id BIGINT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
||||||
|
type ssl_type NOT NULL,
|
||||||
|
status ssl_status NOT NULL DEFAULT 'pending',
|
||||||
|
issuer VARCHAR(255),
|
||||||
|
cert_pem TEXT,
|
||||||
|
key_pem TEXT,
|
||||||
|
chain_pem TEXT,
|
||||||
|
issued_at TIMESTAMPTZ,
|
||||||
|
expires_at TIMESTAMPTZ,
|
||||||
|
auto_renew BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_ssl_certificates_domain_id ON ssl_certificates(domain_id);
|
||||||
|
CREATE INDEX idx_ssl_certificates_expires_at ON ssl_certificates(expires_at);
|
||||||
|
|
||||||
|
-- ─── Mail ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE TABLE mail_domains (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
site_id BIGINT NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
||||||
|
domain CITEXT NOT NULL UNIQUE,
|
||||||
|
dkim_enabled BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE mail_accounts (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
mail_domain_id BIGINT NOT NULL REFERENCES mail_domains(id) ON DELETE CASCADE,
|
||||||
|
email CITEXT NOT NULL UNIQUE,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
quota_mb INT NOT NULL DEFAULT 1024,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_mail_accounts_mail_domain_id ON mail_accounts(mail_domain_id);
|
||||||
|
|
||||||
|
-- ─── DNS ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE TABLE dns_zones (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
server_id BIGINT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
||||||
|
domain CITEXT NOT NULL,
|
||||||
|
serial BIGINT NOT NULL DEFAULT 1,
|
||||||
|
ttl INT NOT NULL DEFAULT 3600,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (server_id, domain)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE dns_records (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
zone_id BIGINT NOT NULL REFERENCES dns_zones(id) ON DELETE CASCADE,
|
||||||
|
type dns_record_type NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL DEFAULT '@',
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
ttl INT NOT NULL DEFAULT 3600,
|
||||||
|
priority INT,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_dns_records_zone_id ON dns_records(zone_id);
|
||||||
|
|
||||||
|
-- ─── Cron jobs ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE TABLE cron_jobs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
site_id BIGINT NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
||||||
|
schedule VARCHAR(128) NOT NULL,
|
||||||
|
command TEXT NOT NULL,
|
||||||
|
run_as VARCHAR(64) NOT NULL DEFAULT 'www-data',
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_cron_jobs_site_id ON cron_jobs(site_id);
|
||||||
|
|
||||||
|
-- ─── Backups ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE TABLE backups (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
uuid UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
|
||||||
|
site_id BIGINT NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
||||||
|
type backup_type NOT NULL,
|
||||||
|
status backup_status NOT NULL DEFAULT 'pending',
|
||||||
|
storage_path VARCHAR(1024),
|
||||||
|
size_bytes BIGINT,
|
||||||
|
error_message TEXT,
|
||||||
|
started_at TIMESTAMPTZ,
|
||||||
|
completed_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_backups_site_id ON backups(site_id);
|
||||||
|
CREATE INDEX idx_backups_status ON backups(status);
|
||||||
|
|
||||||
|
-- ─── Async job queue (panel → agent tasks) ───────────────────────────────────
|
||||||
|
|
||||||
|
CREATE TABLE jobs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
uuid UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
|
||||||
|
server_id BIGINT REFERENCES servers(id) ON DELETE SET NULL,
|
||||||
|
site_id BIGINT REFERENCES sites(id) ON DELETE SET NULL,
|
||||||
|
user_id BIGINT REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
type VARCHAR(64) NOT NULL,
|
||||||
|
payload JSONB NOT NULL DEFAULT '{}',
|
||||||
|
status job_status NOT NULL DEFAULT 'pending',
|
||||||
|
result JSONB,
|
||||||
|
error_message TEXT,
|
||||||
|
attempts INT NOT NULL DEFAULT 0,
|
||||||
|
max_attempts INT NOT NULL DEFAULT 3,
|
||||||
|
scheduled_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
started_at TIMESTAMPTZ,
|
||||||
|
completed_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_jobs_status_scheduled ON jobs(status, scheduled_at)
|
||||||
|
WHERE status IN ('pending', 'running');
|
||||||
|
CREATE INDEX idx_jobs_server_id ON jobs(server_id);
|
||||||
|
|
||||||
|
-- ─── Audit log ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE TABLE audit_logs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
action VARCHAR(64) NOT NULL,
|
||||||
|
resource VARCHAR(64) NOT NULL,
|
||||||
|
resource_id BIGINT,
|
||||||
|
details JSONB NOT NULL DEFAULT '{}',
|
||||||
|
ip_address INET,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
|
||||||
|
CREATE INDEX idx_audit_logs_resource ON audit_logs(resource, resource_id);
|
||||||
|
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
|
||||||
|
|
||||||
|
-- ─── updated_at trigger ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION set_updated_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = now();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_users_updated_at
|
||||||
|
BEFORE UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_servers_updated_at
|
||||||
|
BEFORE UPDATE ON servers FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_sites_updated_at
|
||||||
|
BEFORE UPDATE ON sites FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_ftp_accounts_updated_at
|
||||||
|
BEFORE UPDATE ON ftp_accounts FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_ssl_certificates_updated_at
|
||||||
|
BEFORE UPDATE ON ssl_certificates FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_mail_accounts_updated_at
|
||||||
|
BEFORE UPDATE ON mail_accounts FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_dns_zones_updated_at
|
||||||
|
BEFORE UPDATE ON dns_zones FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_dns_records_updated_at
|
||||||
|
BEFORE UPDATE ON dns_records FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_cron_jobs_updated_at
|
||||||
|
BEFORE UPDATE ON cron_jobs FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
|
||||||
Reference in New Issue
Block a user