feat(core): reshape app bootstrap and access control
This commit is contained in:
@@ -3,7 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/spenc/maintainarr/internal/app"
|
"maintainarr/internal/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,4 +1,4 @@
|
|||||||
module github.com/spenc/maintainarr
|
module maintainarr
|
||||||
|
|
||||||
go 1.25.7
|
go 1.25.7
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||||
"github.com/spenc/maintainarr/internal/config"
|
"maintainarr/internal/config"
|
||||||
"github.com/spenc/maintainarr/internal/db"
|
"maintainarr/internal/db"
|
||||||
"github.com/spenc/maintainarr/internal/handlers"
|
"maintainarr/internal/handlers"
|
||||||
localmiddleware "github.com/spenc/maintainarr/internal/middleware"
|
localmiddleware "maintainarr/internal/middleware"
|
||||||
"github.com/spenc/maintainarr/internal/models"
|
"maintainarr/internal/models"
|
||||||
"github.com/spenc/maintainarr/internal/services"
|
"maintainarr/internal/services"
|
||||||
"github.com/spenc/maintainarr/internal/views"
|
"maintainarr/internal/views"
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
@@ -53,15 +53,19 @@ func New() (*App, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if org.ThemeMode == "" {
|
||||||
|
if err := repo.UpdateOrganizationTheme(ctx, org.Theme, cfg.DefaultMode); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
org.ThemeMode = cfg.DefaultMode
|
||||||
|
}
|
||||||
if err := repo.SeedDefaults(ctx, org.ID); err != nil {
|
if err := repo.SeedDefaults(ctx, org.ID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.BootstrapAdmin {
|
if err := repo.EnsurePrimaryAdmin(ctx); err != nil {
|
||||||
if err := bootstrapAdmin(ctx, repo, auth, org); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
handler := handlers.New(repo, auth, sessions, nodes, renderer, org, cfg.BaseURL)
|
handler := handlers.New(repo, auth, sessions, nodes, renderer, org, cfg.BaseURL)
|
||||||
router := chi.NewRouter()
|
router := chi.NewRouter()
|
||||||
@@ -85,16 +89,23 @@ func New() (*App, error) {
|
|||||||
protected.Get("/setup-2fa", handler.SetupOTPPage)
|
protected.Get("/setup-2fa", handler.SetupOTPPage)
|
||||||
protected.Post("/setup-2fa", handler.SetupOTP)
|
protected.Post("/setup-2fa", handler.SetupOTP)
|
||||||
protected.Get("/dashboard", handler.Dashboard)
|
protected.Get("/dashboard", handler.Dashboard)
|
||||||
|
protected.Get("/dashboard/nodes", handler.DashboardNodes)
|
||||||
protected.Get("/nodes/{nodeID}", handler.NodeOverview)
|
protected.Get("/nodes/{nodeID}", handler.NodeOverview)
|
||||||
|
protected.Get("/nodes/{nodeID}/stats", handler.NodeStatsAPI)
|
||||||
protected.Get("/nodes/{nodeID}/console", handler.NodeConsole)
|
protected.Get("/nodes/{nodeID}/console", handler.NodeConsole)
|
||||||
protected.Get("/nodes/{nodeID}/console/ws", handler.NodeConsoleWebSocket)
|
protected.Get("/nodes/{nodeID}/console/ws", handler.NodeConsoleWebSocket)
|
||||||
protected.Get("/groups", handler.GroupsPage)
|
protected.Get("/groups", handler.GroupsPage)
|
||||||
protected.Get("/automations", handler.AutomationsPage)
|
protected.Get("/automations", handler.AutomationsPage)
|
||||||
protected.Get("/settings", handler.SettingsPage)
|
protected.Get("/settings", handler.SettingsPage)
|
||||||
|
protected.Post("/settings/theme", handler.UpdateTheme)
|
||||||
|
|
||||||
protected.Group(func(editor chi.Router) {
|
protected.Group(func(editor chi.Router) {
|
||||||
editor.Use(localmiddleware.RequireRole(models.RoleEditor))
|
editor.Use(localmiddleware.RequireRole(models.RoleEditor))
|
||||||
|
editor.Post("/groups", handler.CreateGroup)
|
||||||
|
editor.Post("/nodes", handler.CreateNode)
|
||||||
editor.Post("/nodes/{nodeID}/actions/{action}", handler.NodeAction)
|
editor.Post("/nodes/{nodeID}/actions/{action}", handler.NodeAction)
|
||||||
|
editor.Post("/nodes/{nodeID}/commands", handler.NodeQuickCommand)
|
||||||
|
editor.Post("/nodes/{nodeID}/delete", handler.DeleteNode)
|
||||||
editor.Post("/automations", handler.CreateAutomation)
|
editor.Post("/automations", handler.CreateAutomation)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -120,30 +131,3 @@ func (a *App) Run() error {
|
|||||||
fmt.Printf("Maintainarr listening on %s\n", a.config.Address)
|
fmt.Printf("Maintainarr listening on %s\n", a.config.Address)
|
||||||
return a.server.ListenAndServe()
|
return a.server.ListenAndServe()
|
||||||
}
|
}
|
||||||
|
|
||||||
func bootstrapAdmin(ctx context.Context, repo *services.Repository, auth *services.AuthService, org models.Organization) error {
|
|
||||||
count, err := repo.CountUsers(ctx)
|
|
||||||
if err != nil || count > 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
password, err := auth.HashPassword("admin123!")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
key, err := auth.NewOTP("admin@maintainarr.local")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return repo.CreateUser(ctx, &models.User{
|
|
||||||
Organization: org.ID,
|
|
||||||
Name: "Bootstrap Admin",
|
|
||||||
Email: "admin@maintainarr.local",
|
|
||||||
PasswordHash: password,
|
|
||||||
Role: models.RoleAdmin,
|
|
||||||
OTPSecret: key.Secret(),
|
|
||||||
OTPEnabled: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ type Config struct {
|
|||||||
OrgName string
|
OrgName string
|
||||||
BaseURL string
|
BaseURL string
|
||||||
DefaultTheme string
|
DefaultTheme string
|
||||||
|
DefaultMode string
|
||||||
RefreshCron string
|
RefreshCron string
|
||||||
BootstrapAdmin bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() Config {
|
func Load() Config {
|
||||||
@@ -25,9 +25,9 @@ func Load() Config {
|
|||||||
EncryptionKey: env("MAINTAINARR_ENCRYPTION_KEY", "change-me-encryption-key-32bytes"),
|
EncryptionKey: env("MAINTAINARR_ENCRYPTION_KEY", "change-me-encryption-key-32bytes"),
|
||||||
OrgName: env("MAINTAINARR_ORG_NAME", "Maintainarr"),
|
OrgName: env("MAINTAINARR_ORG_NAME", "Maintainarr"),
|
||||||
BaseURL: env("MAINTAINARR_BASE_URL", "http://localhost:8080"),
|
BaseURL: env("MAINTAINARR_BASE_URL", "http://localhost:8080"),
|
||||||
DefaultTheme: env("MAINTAINARR_THEME", "emerald"),
|
DefaultTheme: env("MAINTAINARR_THEME", "blue"),
|
||||||
RefreshCron: env("MAINTAINARR_REFRESH_CRON", "@every 5m"),
|
DefaultMode: env("MAINTAINARR_THEME_MODE", "dark"),
|
||||||
BootstrapAdmin: env("MAINTAINARR_BOOTSTRAP_ADMIN", "true") == "true",
|
RefreshCron: env("MAINTAINARR_REFRESH_CRON", "@every 5s"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ func migrate(ctx context.Context, database *sql.DB) error {
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
theme TEXT NOT NULL DEFAULT 'emerald',
|
theme TEXT NOT NULL DEFAULT 'emerald',
|
||||||
|
theme_mode TEXT NOT NULL DEFAULT 'dark',
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);`,
|
);`,
|
||||||
`CREATE TABLE IF NOT EXISTS users (
|
`CREATE TABLE IF NOT EXISTS users (
|
||||||
@@ -72,6 +73,7 @@ func migrate(ctx context.Context, database *sql.DB) error {
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
organization_id INTEGER NOT NULL,
|
organization_id INTEGER NOT NULL,
|
||||||
group_id INTEGER,
|
group_id INTEGER,
|
||||||
|
tag TEXT NOT NULL DEFAULT '',
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
distro TEXT NOT NULL,
|
distro TEXT NOT NULL,
|
||||||
hostname TEXT NOT NULL,
|
hostname TEXT NOT NULL,
|
||||||
@@ -80,6 +82,12 @@ func migrate(ctx context.Context, database *sql.DB) error {
|
|||||||
ssh_port INTEGER NOT NULL DEFAULT 22,
|
ssh_port INTEGER NOT NULL DEFAULT 22,
|
||||||
ssh_username TEXT NOT NULL DEFAULT '',
|
ssh_username TEXT NOT NULL DEFAULT '',
|
||||||
ssh_password TEXT NOT NULL DEFAULT '',
|
ssh_password TEXT NOT NULL DEFAULT '',
|
||||||
|
package_manager TEXT NOT NULL DEFAULT '',
|
||||||
|
architecture TEXT NOT NULL DEFAULT '',
|
||||||
|
kernel_version TEXT NOT NULL DEFAULT '',
|
||||||
|
cpu_model TEXT NOT NULL DEFAULT '',
|
||||||
|
memory_total_mb INTEGER NOT NULL DEFAULT 0,
|
||||||
|
disk_total_gb INTEGER NOT NULL DEFAULT 0,
|
||||||
cpu_usage REAL NOT NULL DEFAULT 0,
|
cpu_usage REAL NOT NULL DEFAULT 0,
|
||||||
ram_usage REAL NOT NULL DEFAULT 0,
|
ram_usage REAL NOT NULL DEFAULT 0,
|
||||||
disk_usage REAL NOT NULL DEFAULT 0,
|
disk_usage REAL NOT NULL DEFAULT 0,
|
||||||
@@ -97,6 +105,7 @@ func migrate(ctx context.Context, database *sql.DB) error {
|
|||||||
organization_id INTEGER NOT NULL,
|
organization_id INTEGER NOT NULL,
|
||||||
node_id INTEGER,
|
node_id INTEGER,
|
||||||
group_id INTEGER,
|
group_id INTEGER,
|
||||||
|
tag TEXT NOT NULL DEFAULT '',
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
trigger_type TEXT NOT NULL,
|
trigger_type TEXT NOT NULL,
|
||||||
schedule TEXT NOT NULL DEFAULT '',
|
schedule TEXT NOT NULL DEFAULT '',
|
||||||
@@ -131,5 +140,21 @@ func migrate(ctx context.Context, database *sql.DB) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
alterStatements := []string{
|
||||||
|
`ALTER TABLE organizations ADD COLUMN theme_mode TEXT NOT NULL DEFAULT 'dark';`,
|
||||||
|
`ALTER TABLE nodes ADD COLUMN tag TEXT NOT NULL DEFAULT '';`,
|
||||||
|
`ALTER TABLE nodes ADD COLUMN package_manager TEXT NOT NULL DEFAULT '';`,
|
||||||
|
`ALTER TABLE nodes ADD COLUMN architecture TEXT NOT NULL DEFAULT '';`,
|
||||||
|
`ALTER TABLE nodes ADD COLUMN kernel_version TEXT NOT NULL DEFAULT '';`,
|
||||||
|
`ALTER TABLE nodes ADD COLUMN cpu_model TEXT NOT NULL DEFAULT '';`,
|
||||||
|
`ALTER TABLE nodes ADD COLUMN memory_total_mb INTEGER NOT NULL DEFAULT 0;`,
|
||||||
|
`ALTER TABLE nodes ADD COLUMN disk_total_gb INTEGER NOT NULL DEFAULT 0;`,
|
||||||
|
`ALTER TABLE automation_jobs ADD COLUMN tag TEXT NOT NULL DEFAULT '';`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, statement := range alterStatements {
|
||||||
|
_, _ = database.ExecContext(ctx, statement)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/spenc/maintainarr/internal/models"
|
"maintainarr/internal/models"
|
||||||
"github.com/spenc/maintainarr/internal/services"
|
"maintainarr/internal/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
type contextKey string
|
type contextKey string
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type Organization struct {
|
|||||||
ID int64
|
ID int64
|
||||||
Name string
|
Name string
|
||||||
Theme string
|
Theme string
|
||||||
|
ThemeMode string
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +43,8 @@ type Node struct {
|
|||||||
ID int64
|
ID int64
|
||||||
OrganizationID int64
|
OrganizationID int64
|
||||||
GroupID *int64
|
GroupID *int64
|
||||||
|
GroupName string
|
||||||
|
Tag string
|
||||||
Name string
|
Name string
|
||||||
Distro string
|
Distro string
|
||||||
Hostname string
|
Hostname string
|
||||||
@@ -50,6 +53,12 @@ type Node struct {
|
|||||||
SSHPort int
|
SSHPort int
|
||||||
SSHUsername string
|
SSHUsername string
|
||||||
SSHPassword string
|
SSHPassword string
|
||||||
|
PackageManager string
|
||||||
|
Architecture string
|
||||||
|
KernelVersion string
|
||||||
|
CPUModel string
|
||||||
|
MemoryTotalMB int64
|
||||||
|
DiskTotalGB int64
|
||||||
CPUUsage float64
|
CPUUsage float64
|
||||||
RAMUsage float64
|
RAMUsage float64
|
||||||
DiskUsage float64
|
DiskUsage float64
|
||||||
@@ -66,6 +75,9 @@ type AutomationJob struct {
|
|||||||
OrganizationID int64
|
OrganizationID int64
|
||||||
NodeID *int64
|
NodeID *int64
|
||||||
GroupID *int64
|
GroupID *int64
|
||||||
|
NodeName string
|
||||||
|
GroupName string
|
||||||
|
Tag string
|
||||||
Name string
|
Name string
|
||||||
TriggerType string
|
TriggerType string
|
||||||
Schedule string
|
Schedule string
|
||||||
@@ -80,10 +92,13 @@ type CommandRun struct {
|
|||||||
ID int64
|
ID int64
|
||||||
JobID *int64
|
JobID *int64
|
||||||
NodeID int64
|
NodeID int64
|
||||||
|
JobName string
|
||||||
|
NodeName string
|
||||||
Action string
|
Action string
|
||||||
Status string
|
Status string
|
||||||
Output string
|
Output string
|
||||||
TriggeredBy *int64
|
TriggeredBy *int64
|
||||||
StartedAt time.Time
|
StartedAt time.Time
|
||||||
FinishedAt *time.Time
|
FinishedAt *time.Time
|
||||||
|
DurationText string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
"github.com/spenc/maintainarr/internal/models"
|
"maintainarr/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository struct {
|
type Repository struct {
|
||||||
@@ -22,10 +22,10 @@ func (r *Repository) DB() *sql.DB {
|
|||||||
func (r *Repository) EnsureOrganization(ctx context.Context, name, theme string) (models.Organization, error) {
|
func (r *Repository) EnsureOrganization(ctx context.Context, name, theme string) (models.Organization, error) {
|
||||||
var organization models.Organization
|
var organization models.Organization
|
||||||
err := r.db.QueryRowContext(ctx, `
|
err := r.db.QueryRowContext(ctx, `
|
||||||
SELECT id, name, theme, created_at
|
SELECT id, name, theme, theme_mode, created_at
|
||||||
FROM organizations
|
FROM organizations
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`).Scan(&organization.ID, &organization.Name, &organization.Theme, &organization.CreatedAt)
|
`).Scan(&organization.ID, &organization.Name, &organization.Theme, &organization.ThemeMode, &organization.CreatedAt)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return organization, nil
|
return organization, nil
|
||||||
}
|
}
|
||||||
@@ -33,16 +33,36 @@ func (r *Repository) EnsureOrganization(ctx context.Context, name, theme string)
|
|||||||
return organization, err
|
return organization, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := r.db.ExecContext(ctx, `INSERT INTO organizations (name, theme) VALUES (?, ?)`, name, theme)
|
result, err := r.db.ExecContext(ctx, `INSERT INTO organizations (name, theme, theme_mode) VALUES (?, ?, 'dark')`, name, theme)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return organization, err
|
return organization, err
|
||||||
}
|
}
|
||||||
organization.ID, _ = result.LastInsertId()
|
organization.ID, _ = result.LastInsertId()
|
||||||
organization.Name = name
|
organization.Name = name
|
||||||
organization.Theme = theme
|
organization.Theme = theme
|
||||||
|
organization.ThemeMode = "dark"
|
||||||
return organization, nil
|
return organization, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) GetOrganization(ctx context.Context) (models.Organization, error) {
|
||||||
|
var organization models.Organization
|
||||||
|
err := r.db.QueryRowContext(ctx, `
|
||||||
|
SELECT id, name, theme, theme_mode, created_at
|
||||||
|
FROM organizations
|
||||||
|
LIMIT 1
|
||||||
|
`).Scan(&organization.ID, &organization.Name, &organization.Theme, &organization.ThemeMode, &organization.CreatedAt)
|
||||||
|
return organization, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) UpdateOrganizationTheme(ctx context.Context, theme, themeMode string) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, `
|
||||||
|
UPDATE organizations
|
||||||
|
SET theme = ?, theme_mode = ?
|
||||||
|
WHERE id = (SELECT id FROM organizations LIMIT 1)
|
||||||
|
`, theme, themeMode)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) CountUsers(ctx context.Context) (int, error) {
|
func (r *Repository) CountUsers(ctx context.Context) (int, error) {
|
||||||
var count int
|
var count int
|
||||||
err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM users`).Scan(&count)
|
err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM users`).Scan(&count)
|
||||||
@@ -101,54 +121,104 @@ func (r *Repository) EnableUserOTP(ctx context.Context, id int64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) SeedDefaults(ctx context.Context, orgID int64) error {
|
func (r *Repository) EnsurePrimaryAdmin(ctx context.Context) error {
|
||||||
var groups int
|
var adminCount int
|
||||||
if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM vm_groups`).Scan(&groups); err != nil {
|
if err := r.db.QueryRowContext(ctx, `
|
||||||
return err
|
SELECT COUNT(*)
|
||||||
}
|
FROM users
|
||||||
if groups == 0 {
|
WHERE role = ?
|
||||||
if _, err := r.db.ExecContext(ctx, `
|
`, models.RoleAdmin).Scan(&adminCount); err != nil {
|
||||||
INSERT INTO vm_groups (organization_id, name, description, color_token)
|
|
||||||
VALUES
|
|
||||||
(?, 'Core Infra', 'Hypervisors, DNS, auth, storage', 'primary'),
|
|
||||||
(?, 'Edge Fleet', 'Proxies, tunnel boxes, ingress points', 'accent'),
|
|
||||||
(?, 'Lab', 'Test nodes and staging sandboxes', 'warning')
|
|
||||||
`, orgID, orgID, orgID); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if adminCount > 0 {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var nodes int
|
var userID int64
|
||||||
if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM nodes`).Scan(&nodes); err != nil {
|
err := r.db.QueryRowContext(ctx, `
|
||||||
return err
|
SELECT id
|
||||||
|
FROM users
|
||||||
|
ORDER BY created_at ASC, id ASC
|
||||||
|
LIMIT 1
|
||||||
|
`).Scan(&userID)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
if nodes == 0 {
|
|
||||||
_, err := r.db.ExecContext(ctx, `
|
|
||||||
INSERT INTO nodes (organization_id, name, distro, hostname, ip_address, mac_address, ssh_port, cpu_usage, ram_usage, disk_usage, uptime_seconds, auto_updates_enabled, notes)
|
|
||||||
VALUES
|
|
||||||
(?, 'Aurora', 'Ubuntu', 'aurora.local', '192.168.1.20', 'AA:BB:CC:DD:EE:01', 22, 34.5, 62.4, 40.1, 864000, 1, 'Primary app host'),
|
|
||||||
(?, 'Forge', 'Debian', 'forge.local', '192.168.1.25', 'AA:BB:CC:DD:EE:02', 22, 12.8, 44.9, 71.2, 421920, 1, 'Build runner'),
|
|
||||||
(?, 'Nimbus', 'Arch', 'nimbus.local', '192.168.1.33', 'AA:BB:CC:DD:EE:03', 22, 73.1, 80.6, 55.8, 128800, 0, 'Bleeding-edge sandbox')
|
|
||||||
`, orgID, orgID, orgID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = r.db.ExecContext(ctx, `
|
||||||
|
UPDATE users
|
||||||
|
SET role = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`, models.RoleAdmin, userID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) ListGroups(ctx context.Context, orgID int64) ([]models.VMGroup, error) {
|
||||||
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
|
SELECT id, organization_id, name, description, color_token, created_at
|
||||||
|
FROM vm_groups
|
||||||
|
WHERE organization_id = ?
|
||||||
|
ORDER BY name ASC
|
||||||
|
`, orgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var groups []models.VMGroup
|
||||||
|
for rows.Next() {
|
||||||
|
var group models.VMGroup
|
||||||
|
if err := rows.Scan(&group.ID, &group.OrganizationID, &group.Name, &group.Description, &group.ColorToken, &group.CreatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
groups = append(groups, group)
|
||||||
}
|
}
|
||||||
|
|
||||||
var jobs int
|
return groups, rows.Err()
|
||||||
if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM automation_jobs`).Scan(&jobs); err != nil {
|
}
|
||||||
return err
|
|
||||||
}
|
func (r *Repository) CreateGroup(ctx context.Context, group *models.VMGroup) error {
|
||||||
if jobs == 0 {
|
result, err := r.db.ExecContext(ctx, `
|
||||||
_, err := r.db.ExecContext(ctx, `
|
INSERT INTO vm_groups (organization_id, name, description, color_token)
|
||||||
INSERT INTO automation_jobs (organization_id, node_id, name, trigger_type, schedule, command, enabled)
|
VALUES (?, ?, ?, ?)
|
||||||
VALUES
|
`, group.OrganizationID, group.Name, group.Description, group.ColorToken)
|
||||||
(?, 1, 'Nightly apt upgrade', 'recurring', '0 3 * * *', 'sudo apt update && sudo apt upgrade -y && sudo apt autoremove -y', 1),
|
if err != nil {
|
||||||
(?, 2, 'Weekly docker prune', 'recurring', '0 4 * * 0', 'docker system prune -af', 1),
|
|
||||||
(?, 3, 'High-load audit hook', 'triggered', '', 'journalctl -p 3 -n 200', 1)
|
|
||||||
`, orgID, orgID, orgID)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
group.ID, _ = result.LastInsertId()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) ListTags(ctx context.Context, orgID int64) ([]string, error) {
|
||||||
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
|
SELECT DISTINCT tag
|
||||||
|
FROM nodes
|
||||||
|
WHERE organization_id = ? AND tag <> ''
|
||||||
|
ORDER BY tag ASC
|
||||||
|
`, orgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var tags []string
|
||||||
|
for rows.Next() {
|
||||||
|
var tag string
|
||||||
|
if err := rows.Scan(&tag); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tags = append(tags, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) SeedDefaults(ctx context.Context, orgID int64) error {
|
||||||
|
_ = ctx
|
||||||
|
_ = orgID
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user