feat: auth, site creation with PHP version selection

This commit is contained in:
orohi
2026-06-17 04:46:44 +03:00
parent 0f31c24bf9
commit b7753505b8
20 changed files with 1097 additions and 114 deletions
+95
View File
@@ -0,0 +1,95 @@
package repository
import (
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/panelhosting/panel/internal/models"
)
var ErrServerNotFound = errors.New("server not found")
type ServerRepository struct {
pool *pgxpool.Pool
}
func NewServerRepository(pool *pgxpool.Pool) *ServerRepository {
return &ServerRepository{pool: pool}
}
func (r *ServerRepository) Count(ctx context.Context) (int64, error) {
var count int64
err := r.pool.QueryRow(ctx, `SELECT COUNT(*) FROM servers`).Scan(&count)
return count, err
}
func (r *ServerRepository) CreateDefault(ctx context.Context, name, hostname, ip string) (*models.Server, error) {
const q = `
INSERT INTO servers (name, hostname, ip_address, status)
VALUES ($1, $2, $3::inet, 'online')
RETURNING id, uuid, name, hostname, ip_address::text, ssh_port, status,
agent_version, os_info, resources, settings, last_seen_at, created_at, updated_at
`
var s models.Server
err := r.pool.QueryRow(ctx, q, name, hostname, ip).Scan(
&s.ID, &s.UUID, &s.Name, &s.Hostname, &s.IPAddress, &s.SSHPort, &s.Status,
&s.AgentVersion, &s.OSInfo, &s.Resources, &s.Settings, &s.LastSeenAt, &s.CreatedAt, &s.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("create server: %w", err)
}
return &s, nil
}
func (r *ServerRepository) GetFirst(ctx context.Context) (*models.Server, error) {
const q = `
SELECT id, uuid, name, hostname, ip_address::text, ssh_port, status,
agent_version, os_info, resources, settings, last_seen_at, created_at, updated_at
FROM servers ORDER BY id LIMIT 1
`
var s models.Server
err := r.pool.QueryRow(ctx, q).Scan(
&s.ID, &s.UUID, &s.Name, &s.Hostname, &s.IPAddress, &s.SSHPort, &s.Status,
&s.AgentVersion, &s.OSInfo, &s.Resources, &s.Settings, &s.LastSeenAt, &s.CreatedAt, &s.UpdatedAt,
)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrServerNotFound
}
if err != nil {
return nil, fmt.Errorf("get first server: %w", err)
}
return &s, nil
}
func (r *ServerRepository) GetByID(ctx context.Context, id int64) (*models.Server, error) {
const q = `
SELECT id, uuid, name, hostname, ip_address::text, ssh_port, status,
agent_version, os_info, resources, settings, last_seen_at, created_at, updated_at
FROM servers WHERE id = $1
`
var s models.Server
err := r.pool.QueryRow(ctx, q, id).Scan(
&s.ID, &s.UUID, &s.Name, &s.Hostname, &s.IPAddress, &s.SSHPort, &s.Status,
&s.AgentVersion, &s.OSInfo, &s.Resources, &s.Settings, &s.LastSeenAt, &s.CreatedAt, &s.UpdatedAt,
)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrServerNotFound
}
if err != nil {
return nil, fmt.Errorf("get server: %w", err)
}
return &s, nil
}
func (r *ServerRepository) GrantAccess(ctx context.Context, userID, serverID int64, canManage bool) error {
const q = `
INSERT INTO server_access (user_id, server_id, can_manage)
VALUES ($1, $2, $3)
ON CONFLICT (user_id, server_id) DO NOTHING
`
_, err := r.pool.Exec(ctx, q, userID, serverID, canManage)
return err
}
+98
View File
@@ -0,0 +1,98 @@
package repository
import (
"context"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/panelhosting/panel/internal/models"
)
var ErrUserNotFound = errors.New("user not found")
func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*models.User, error) {
const q = `
SELECT id, uuid, email, username, password_hash, role, status, locale, timezone, last_login_at, created_at, updated_at
FROM users WHERE username = $1
`
var u models.User
err := r.pool.QueryRow(ctx, q, username).Scan(
&u.ID, &u.UUID, &u.Email, &u.Username, &u.PasswordHash, &u.Role, &u.Status,
&u.Locale, &u.Timezone, &u.LastLoginAt, &u.CreatedAt, &u.UpdatedAt,
)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrUserNotFound
}
if err != nil {
return nil, fmt.Errorf("get user by username: %w", err)
}
return &u, nil
}
func (r *UserRepository) GetByID(ctx context.Context, id int64) (*models.User, error) {
const q = `
SELECT id, uuid, email, username, password_hash, role, status, locale, timezone, last_login_at, created_at, updated_at
FROM users WHERE id = $1
`
var u models.User
err := r.pool.QueryRow(ctx, q, id).Scan(
&u.ID, &u.UUID, &u.Email, &u.Username, &u.PasswordHash, &u.Role, &u.Status,
&u.Locale, &u.Timezone, &u.LastLoginAt, &u.CreatedAt, &u.UpdatedAt,
)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrUserNotFound
}
if err != nil {
return nil, fmt.Errorf("get user by id: %w", err)
}
return &u, nil
}
func (r *UserRepository) UpdateLastLogin(ctx context.Context, id int64) error {
_, err := r.pool.Exec(ctx, `UPDATE users SET last_login_at = now() WHERE id = $1`, id)
return err
}
type SessionRepository struct {
pool *pgxpool.Pool
}
func NewSessionRepository(pool *pgxpool.Pool) *SessionRepository {
return &SessionRepository{pool: pool}
}
func (r *SessionRepository) Create(ctx context.Context, userID int64, tokenHash string, ip, userAgent string, expiresAt time.Time) error {
const q = `
INSERT INTO sessions (user_id, token_hash, ip_address, user_agent, expires_at)
VALUES ($1, $2, NULLIF($3, '')::inet, NULLIF($4, ''), $5)
`
_, err := r.pool.Exec(ctx, q, userID, tokenHash, ip, userAgent, expiresAt)
if err != nil {
return fmt.Errorf("create session: %w", err)
}
return nil
}
func (r *SessionRepository) GetUserIDByToken(ctx context.Context, tokenHash string) (int64, error) {
const q = `
SELECT user_id FROM sessions
WHERE token_hash = $1 AND expires_at > now()
`
var userID int64
err := r.pool.QueryRow(ctx, q, tokenHash).Scan(&userID)
if errors.Is(err, pgx.ErrNoRows) {
return 0, ErrUserNotFound
}
if err != nil {
return 0, fmt.Errorf("get session: %w", err)
}
return userID, nil
}
func (r *SessionRepository) DeleteByToken(ctx context.Context, tokenHash string) error {
_, err := r.pool.Exec(ctx, `DELETE FROM sessions WHERE token_hash = $1`, tokenHash)
return err
}
+156
View File
@@ -0,0 +1,156 @@
package repository
import (
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/panelhosting/panel/internal/models"
)
type SiteRepository struct {
pool *pgxpool.Pool
}
func NewSiteRepository(pool *pgxpool.Pool) *SiteRepository {
return &SiteRepository{pool: pool}
}
type SiteWithDomain struct {
models.Site
PrimaryDomain string `json:"primary_domain"`
}
func (r *SiteRepository) Create(ctx context.Context, serverID, ownerID int64, name, documentRoot, phpVersion, domain string) (*SiteWithDomain, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
const siteQ = `
INSERT INTO sites (server_id, owner_id, name, document_root, php_version, status)
VALUES ($1, $2, $3, $4, $5, 'active')
RETURNING id, uuid, server_id, owner_id, name, document_root, php_version, status,
settings, disk_quota_mb, created_at, updated_at
`
var s models.Site
var phpVer *string
if phpVersion != "" {
phpVer = &phpVersion
}
err = tx.QueryRow(ctx, siteQ, serverID, ownerID, name, documentRoot, phpVer).Scan(
&s.ID, &s.UUID, &s.ServerID, &s.OwnerID, &s.Name, &s.DocumentRoot, &s.PHPVersion, &s.Status,
&s.Settings, &s.DiskQuotaMB, &s.CreatedAt, &s.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("create site: %w", err)
}
const domainQ = `
INSERT INTO domains (site_id, domain, is_primary, ssl_enabled)
VALUES ($1, $2, true, true)
`
if _, err = tx.Exec(ctx, domainQ, s.ID, domain); err != nil {
return nil, fmt.Errorf("create domain: %w", err)
}
if err = tx.Commit(ctx); err != nil {
return nil, err
}
return &SiteWithDomain{Site: s, PrimaryDomain: domain}, nil
}
func (r *SiteRepository) ListForUser(ctx context.Context, userID int64, isAdmin bool) ([]SiteWithDomain, error) {
var q string
var args []any
if isAdmin {
q = siteListQuery + ` ORDER BY s.created_at DESC`
} else {
q = siteListQuery + ` WHERE s.owner_id = $1 ORDER BY s.created_at DESC`
args = append(args, userID)
}
rows, err := r.pool.Query(ctx, q, args...)
if err != nil {
return nil, fmt.Errorf("list sites: %w", err)
}
defer rows.Close()
return scanSiteList(rows)
}
const siteListQuery = `
SELECT s.id, s.uuid, s.server_id, s.owner_id, s.name, s.document_root, s.php_version, s.status,
s.settings, s.disk_quota_mb, s.created_at, s.updated_at,
COALESCE(d.domain::text, '')
FROM sites s
LEFT JOIN domains d ON d.site_id = s.id AND d.is_primary = true
`
func scanSiteList(rows pgx.Rows) ([]SiteWithDomain, error) {
var list []SiteWithDomain
for rows.Next() {
var item SiteWithDomain
err := rows.Scan(
&item.ID, &item.UUID, &item.ServerID, &item.OwnerID, &item.Name, &item.DocumentRoot,
&item.PHPVersion, &item.Status, &item.Settings, &item.DiskQuotaMB,
&item.CreatedAt, &item.UpdatedAt, &item.PrimaryDomain,
)
if err != nil {
return nil, err
}
list = append(list, item)
}
return list, rows.Err()
}
type PHPVersionRepository struct {
pool *pgxpool.Pool
}
func NewPHPVersionRepository(pool *pgxpool.Pool) *PHPVersionRepository {
return &PHPVersionRepository{pool: pool}
}
func (r *PHPVersionRepository) ListActive(ctx context.Context) ([]string, error) {
rows, err := r.pool.Query(ctx, `
SELECT version FROM php_versions WHERE is_active = true ORDER BY sort_order
`)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return defaultPHPVersions(), nil
}
return nil, err
}
defer rows.Close()
var versions []string
for rows.Next() {
var v string
if err := rows.Scan(&v); err != nil {
return nil, err
}
versions = append(versions, v)
}
if len(versions) == 0 {
return defaultPHPVersions(), nil
}
return versions, rows.Err()
}
func (r *PHPVersionRepository) IsActive(ctx context.Context, version string) (bool, error) {
var active bool
err := r.pool.QueryRow(ctx, `SELECT is_active FROM php_versions WHERE version = $1`, version).Scan(&active)
if errors.Is(err, pgx.ErrNoRows) {
return false, nil
}
return active, err
}
func defaultPHPVersions() []string {
return []string{"8.1", "8.2", "8.3", "8.4"}
}