galt #1
149
backend/db.go
149
backend/db.go
@@ -2,16 +2,19 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"github.com/pressly/goose/v3"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed migrations/*.sql
|
||||||
|
var embedMigrations embed.FS
|
||||||
|
|
||||||
// Create new sqlite database
|
// Create new sqlite database
|
||||||
// If filepath parameter is empty
|
// If filepath parameter is empty
|
||||||
func NewDatabaseConnection(dataDir string, dbName string) (*sql.DB, error) {
|
func NewDatabaseConnection(dataDir string, dbName string) (*sql.DB, error) {
|
||||||
@@ -35,21 +38,47 @@ func NewDatabaseConnection(dataDir string, dbName string) (*sql.DB, error) {
|
|||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// database
|
func MigrateDatabase(db *sql.DB) {
|
||||||
//
|
dialect := "sqlite3"
|
||||||
// Manage database connection to ./app.db
|
goose.SetBaseFS(embedMigrations)
|
||||||
// Handles CRUD
|
|
||||||
func database(db *sql.DB) *sql.DB {
|
if err := goose.SetDialect(dialect); err != nil {
|
||||||
if db == nil {
|
slog.Error("Database dialect error", "dialect", dialect, "error", err)
|
||||||
slog.Error("No connection to the database!")
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
// Seed default menu items if empty
|
|
||||||
row := db.QueryRow("SELECT COUNT(*) FROM menu_items")
|
if err := goose.Up(db, "migrations"); err != nil {
|
||||||
|
slog.Error("Database migration error", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
migratedVersion, err := goose.GetDBVersion(db)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Datatabase migration version check error", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Database up to date", "version", migratedVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SeedDatabase(db *sql.DB, superadminPassword string) error {
|
||||||
|
seededValue := "false"
|
||||||
|
row := db.QueryRow("SELECT value FROM settings WHERE key='seeded';")
|
||||||
|
_ = row.Scan(&seededValue)
|
||||||
|
|
||||||
|
if seededValue == "true" {
|
||||||
|
slog.Info("Database already seeded with default data, skipping")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Database empty, seeding with default values")
|
||||||
|
|
||||||
|
row = db.QueryRow("SELECT COUNT(*) FROM menu_items")
|
||||||
var count int
|
var count int
|
||||||
_ = row.Scan(&count)
|
_ = row.Scan(&count)
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
log.Println("Seeding default menu_items…")
|
slog.Info("Seeding default menu_items")
|
||||||
|
|
||||||
defaults := []struct {
|
defaults := []struct {
|
||||||
category string
|
category string
|
||||||
name string
|
name string
|
||||||
@@ -85,88 +114,32 @@ func database(db *sql.DB) *sql.DB {
|
|||||||
for _, d := range defaults {
|
for _, d := range defaults {
|
||||||
_, err := db.Exec("INSERT INTO menu_items (category, name) VALUES (?, ?)", d.category, d.name)
|
_, err := db.Exec("INSERT INTO menu_items (category, name) VALUES (?, ?)", d.category, d.name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error seeding menu item:", err)
|
return fmt.Errorf("Error seeding menu_items table: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
slog.Info("Table menu_items already seeded, skipping")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create users table
|
_, err := db.Exec(`INSERT OR REPLACE INTO settings (key, value) VALUES ('seeded', 'true')`)
|
||||||
_, err := db.Exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
username TEXT NOT NULL UNIQUE,
|
|
||||||
password TEXT NOT NULL,
|
|
||||||
role INTEGER NOT NULL DEFAULT 2
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return fmt.Errorf("Error saving setting table: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create settings table
|
updateSuperadminPassword(db, superadminPassword)
|
||||||
_, err = db.Exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
|
||||||
key TEXT PRIMARY KEY,
|
|
||||||
value TEXT NOT NULL
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed default finalize_time if missing
|
return nil
|
||||||
_, err = db.Exec(`INSERT OR IGNORE INTO settings (key, value) VALUES ('finalize_time', '10:30')`)
|
}
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
func updateSuperadminPassword(db *sql.DB, superadminPassword string) {
|
||||||
}
|
_, err := db.Exec(`INSERT OR REPLACE INTO users (id, username, password, role)
|
||||||
|
VALUES (
|
||||||
_, err = db.Exec(`
|
1,
|
||||||
CREATE TABLE IF NOT EXISTS menu_items (
|
'superadmin',
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
?,
|
||||||
category TEXT NOT NULL,
|
COALESCE((SELECT role FROM users WHERE id = 1), '0')
|
||||||
name TEXT NOT NULL
|
)`, superadminPassword)
|
||||||
)
|
if err != nil {
|
||||||
`)
|
slog.Error("Error setting superadmin password", "error", err)
|
||||||
if err != nil {
|
}
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create selections table (latest choice per user)
|
|
||||||
_, err = db.Exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS selections (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
main TEXT NOT NULL,
|
|
||||||
side TEXT NOT NULL,
|
|
||||||
soup TEXT,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create orders table (all placed orders for history + realtime updates)
|
|
||||||
_, err = db.Exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS orders (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
username TEXT NOT NULL,
|
|
||||||
soup TEXT,
|
|
||||||
main TEXT NOT NULL,
|
|
||||||
side TEXT NOT NULL,
|
|
||||||
created_at TEXT NOT NULL
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add status column if it doesn't exist
|
|
||||||
_, err = db.Exec(`ALTER TABLE orders ADD COLUMN status TEXT NOT NULL DEFAULT 'active'`)
|
|
||||||
if err != nil && !strings.Contains(err.Error(), "duplicate column name") {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return db
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,19 @@ require (
|
|||||||
modernc.org/sqlite v1.39.0
|
modernc.org/sqlite v1.39.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||||
|
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/pressly/goose/v3 v3.26.0
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||||
golang.org/x/sys v0.34.0 // indirect
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
|
|||||||
@@ -14,16 +14,24 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
|||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
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/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||||
|
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
|
||||||
|
github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||||
|
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
|||||||
@@ -23,14 +23,8 @@ func main() {
|
|||||||
slog.Warn("Cannot find .env file, using system env variables")
|
slog.Warn("Cannot find .env file, using system env variables")
|
||||||
}
|
}
|
||||||
|
|
||||||
adminUser := os.Getenv("ADMIN_USER")
|
|
||||||
adminPass := os.Getenv("ADMIN_PASS")
|
adminPass := os.Getenv("ADMIN_PASS")
|
||||||
|
|
||||||
if adminUser == "" {
|
|
||||||
slog.Error("Missing ADMIN_USER env variable")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if adminPass == "" {
|
if adminPass == "" {
|
||||||
slog.Error("Missing ADMIN_PASS env variable")
|
slog.Error("Missing ADMIN_PASS env variable")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -49,11 +43,17 @@ func main() {
|
|||||||
slog.Error("Databse error", "error", err)
|
slog.Error("Databse error", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
db := database(dbConnection)
|
|
||||||
defer db.Close()
|
// Migrate database
|
||||||
|
MigrateDatabase(dbConnection)
|
||||||
|
SeedDatabase(dbConnection, adminPass)
|
||||||
|
updateSuperadminPassword(dbConnection, adminPass)
|
||||||
|
|
||||||
|
// db := database(dbConnection)
|
||||||
|
defer dbConnection.Close()
|
||||||
|
|
||||||
// Create App
|
// Create App
|
||||||
app := handlers.NewApp(db, handlers.NewBroker())
|
app := handlers.NewApp(dbConnection, handlers.NewBroker())
|
||||||
|
|
||||||
// Create server
|
// Create server
|
||||||
srv := NewServer(app, "0.0.0.0:7153", "*")
|
srv := NewServer(app, "0.0.0.0:7153", "*")
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
role INTEGER NOT NULL DEFAULT 2
|
||||||
|
)
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS users;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO settings (key, value) VALUES ('finalize_time', '10:30');
|
||||||
|
INSERT INTO settings (key, value) VALUES ('seeded', 'false');
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS settings;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE IF NOT EXISTS menu_items (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
category TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
)
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS menu_items;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE IF NOT EXISTS selections (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
main TEXT NOT NULL,
|
||||||
|
side TEXT NOT NULL,
|
||||||
|
soup TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
)
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS selections;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE IF NOT EXISTS orders (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
soup TEXT,
|
||||||
|
main TEXT NOT NULL,
|
||||||
|
side TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active',
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
)
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS orders;
|
||||||
|
-- +goose StatementEnd
|
||||||
Reference in New Issue
Block a user