feat(env): initialize /opt/ infrastructure framework, config definitions, and git repo

This commit is contained in:
Claus Lohmar 2026-06-14 12:56:33 +00:00
commit 3ec860b67d
8 changed files with 400 additions and 0 deletions

26
.gitignore vendored Normal file
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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)
}
}