feat: auth, site creation with PHP version selection
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"}
|
||||
}
|
||||
Reference in New Issue
Block a user