feat(env): initialize /opt/ infrastructure framework, config definitions, and git repo
This commit is contained in:
commit
3ec860b67d
8 changed files with 400 additions and 0 deletions
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Binaries
|
||||
/bin/
|
||||
/data/*.db
|
||||
/data/*.db-wal
|
||||
/data/*.db-shm
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor/IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Setup check binary
|
||||
src/setupcheck
|
||||
src/setupcheck.exe
|
||||
|
||||
# Templ generated
|
||||
**/*_templ.go
|
||||
29
config.yaml
Normal file
29
config.yaml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Next Workspace (NextWks) Configuration
|
||||
# Path: /opt/nextwks/config.yaml
|
||||
|
||||
server:
|
||||
host: "0.0.0.0"
|
||||
port: 8080
|
||||
|
||||
admin:
|
||||
secret_token: "CHANGE_ME_ADMIN_SECRET_TOKEN"
|
||||
|
||||
database:
|
||||
type: "sqlite"
|
||||
path: "/opt/nextwks/data/nextwks.db"
|
||||
|
||||
authelia:
|
||||
host: "http://127.0.0.1:9091"
|
||||
config_path: "/opt/authelia/config/configuration.yml"
|
||||
users_db_path: "/opt/authelia/data/users_database.yml"
|
||||
|
||||
smtp:
|
||||
host: ""
|
||||
port: 587
|
||||
username: ""
|
||||
password: ""
|
||||
from: "noreply@nextwks.local"
|
||||
|
||||
session:
|
||||
secret: "CHANGE_ME_SESSION_SECRET"
|
||||
expiry_minutes: 60
|
||||
40
src/cmd/setupcheck/main.go
Normal file
40
src/cmd/setupcheck/main.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
checks := []struct {
|
||||
path string
|
||||
purpose string
|
||||
mustExist bool
|
||||
}{
|
||||
{"/opt/nextwks/config.yaml", "NextWks configuration", true},
|
||||
{"/opt/nextwks/bin", "Binary output directory", true},
|
||||
{"/opt/nextwks/data", "Data directory", true},
|
||||
{"/opt/authelia/config/configuration.yml", "Authelia mock configuration", true},
|
||||
}
|
||||
|
||||
allPassed := true
|
||||
for _, c := range checks {
|
||||
_, err := os.Stat(c.path)
|
||||
if c.mustExist && os.IsNotExist(err) {
|
||||
fmt.Printf("❌ MISSING: %s (%s)\n", c.path, c.purpose)
|
||||
allPassed = false
|
||||
} else if c.mustExist && err != nil {
|
||||
fmt.Printf("❌ ERROR: %s - %v\n", c.path, err)
|
||||
allPassed = false
|
||||
} else {
|
||||
fmt.Printf("✅ OK: %s (%s)\n", c.path, c.purpose)
|
||||
}
|
||||
}
|
||||
|
||||
if allPassed {
|
||||
fmt.Println("\n✅ All system paths verified!")
|
||||
} else {
|
||||
fmt.Println("\n❌ Some paths are missing or have errors")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
90
src/core/config/config.go
Normal file
90
src/core/config/config.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config represents the full NextWks configuration.
|
||||
type Config struct {
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Admin AdminConfig `yaml:"admin"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
Authelia AutheliaConfig `yaml:"authelia"`
|
||||
SMTP SMTPConfig `yaml:"smtp"`
|
||||
Session SessionConfig `yaml:"session"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
}
|
||||
|
||||
type AdminConfig struct {
|
||||
SecretToken string `yaml:"secret_token"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Type string `yaml:"type"`
|
||||
Path string `yaml:"path"`
|
||||
}
|
||||
|
||||
type AutheliaConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
ConfigPath string `yaml:"config_path"`
|
||||
UsersDBPath string `yaml:"users_db_path"`
|
||||
}
|
||||
|
||||
type SMTPConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
From string `yaml:"from"`
|
||||
}
|
||||
|
||||
type SessionConfig struct {
|
||||
Secret string `yaml:"secret"`
|
||||
ExpiryMinutes int `yaml:"expiry_minutes"`
|
||||
}
|
||||
|
||||
// Load reads and parses the YAML configuration file.
|
||||
func Load(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read config file: %w", err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse config file: %w", err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// AutheliaSessionSecret extracts the session.secret from Authelia's configuration.
|
||||
func AutheliaSessionSecret(cfgPath string) (string, error) {
|
||||
data, err := os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read authelia config: %w", err)
|
||||
}
|
||||
|
||||
var autheliaCfg struct {
|
||||
Session struct {
|
||||
Secret string `yaml:"secret"`
|
||||
} `yaml:"session"`
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &autheliaCfg); err != nil {
|
||||
return "", fmt.Errorf("parse authelia config: %w", err)
|
||||
}
|
||||
|
||||
if autheliaCfg.Session.Secret == "" {
|
||||
return "", fmt.Errorf("authelia session.secret not found in %s", cfgPath)
|
||||
}
|
||||
|
||||
return autheliaCfg.Session.Secret, nil
|
||||
}
|
||||
94
src/core/db/db.go
Normal file
94
src/core/db/db.go
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// Database wraps the SQLite connection and provides migration helpers.
|
||||
type Database struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
// Initialize opens (or creates) the SQLite database at the given path.
|
||||
func Initialize(dbPath string) (*Database, error) {
|
||||
// Ensure the data directory exists
|
||||
dir := filepath.Dir(dbPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("create data directory: %w", err)
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open database: %w", err)
|
||||
}
|
||||
|
||||
// Enable WAL mode for better concurrency
|
||||
if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
||||
return nil, fmt.Errorf("enable WAL mode: %w", err)
|
||||
}
|
||||
|
||||
// Enable foreign keys
|
||||
if _, err := db.Exec("PRAGMA foreign_keys=ON"); err != nil {
|
||||
return nil, fmt.Errorf("enable foreign keys: %w", err)
|
||||
}
|
||||
|
||||
return &Database{DB: db}, nil
|
||||
}
|
||||
|
||||
// Migrate runs automatic schema migrations on startup.
|
||||
func (d *Database) Migrate() error {
|
||||
migrations := []string{
|
||||
`sessions`,
|
||||
`audit_logs`,
|
||||
}
|
||||
|
||||
// Verify all required tables exist
|
||||
for _, table := range migrations {
|
||||
if err := d.ensureTable(table); err != nil {
|
||||
return fmt.Errorf("ensure table %s: %w", table, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Database) ensureTable(name string) error {
|
||||
switch name {
|
||||
case "sessions":
|
||||
_, err := d.DB.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at DATETIME NOT NULL
|
||||
)
|
||||
`)
|
||||
return err
|
||||
|
||||
case "audit_logs":
|
||||
_, err := d.DB.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
action TEXT NOT NULL,
|
||||
actor TEXT NOT NULL,
|
||||
target TEXT,
|
||||
details TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("unknown table: %s", name)
|
||||
}
|
||||
|
||||
// Close cleanly shuts down the database connection.
|
||||
func (d *Database) Close() error {
|
||||
return d.DB.Close()
|
||||
}
|
||||
20
src/go.mod
Normal file
20
src/go.mod
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
module git.lohmar.co.uk/lexton-it/NextWks
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/a-h/templ v0.3.1020 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.3.0 // indirect
|
||||
github.com/go-chi/cors v1.2.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.72.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.52.0 // indirect
|
||||
)
|
||||
30
src/go.sum
Normal file
30
src/go.sum
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
|
||||
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
|
||||
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
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/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
||||
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo=
|
||||
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||
71
src/main.go
Normal file
71
src/main.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"git.lohmar.co.uk/lexton-it/NextWks/core/config"
|
||||
"git.lohmar.co.uk/lexton-it/NextWks/core/db"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
logger.Info("starting Next Workspace (NextWks)")
|
||||
|
||||
// Load configuration
|
||||
cfg, err := config.Load("/opt/nextwks/config.yaml")
|
||||
if err != nil {
|
||||
logger.Error("failed to load config", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Initialize database
|
||||
database, err := db.Initialize(cfg.Database.Path)
|
||||
if err != nil {
|
||||
logger.Error("failed to initialize database", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
// Run schema migrations
|
||||
if err := database.Migrate(); err != nil {
|
||||
logger.Error("failed to run migrations", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.Info("database initialized and migrated")
|
||||
|
||||
// Setup HTTP router
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Health check
|
||||
mux.HandleFunc("GET /api/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"status":"ok"}`))
|
||||
})
|
||||
|
||||
// Start server
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
// Graceful shutdown
|
||||
go func() {
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigChan
|
||||
logger.Info("shutting down server...")
|
||||
server.Close()
|
||||
}()
|
||||
|
||||
logger.Info("server listening", "address", addr)
|
||||
if err := server.ListenAndServe(); err != http.ErrServerClosed {
|
||||
logger.Error("server error", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue