first commit

This commit is contained in:
orohi
2026-06-17 04:28:17 +03:00
commit 8c21b62a4f
15 changed files with 965 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
DATABASE_URL=postgres://panel:panel_secret@localhost:5432/panel?sslmode=disable
+4
View File
@@ -0,0 +1,4 @@
.env
*.exe
/bin/
/dist/
+5
View File
@@ -0,0 +1,5 @@
FROM migrate/migrate:v4.18.2
COPY migrations /migrations
ENTRYPOINT ["migrate", "-path", "/migrations", "-database"]
+77
View File
@@ -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
+60
View File
@@ -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
}
+31
View File
@@ -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()
}
+35
View File
@@ -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:
+22
View File
@@ -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
)
+85
View File
@@ -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=
+18
View File
@@ -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
}
+44
View File
@@ -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
}
+34
View File
@@ -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
}
+146
View File
@@ -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"`
}
+46
View File
@@ -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;
+357
View File
@@ -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();