Template
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