initial commit
This commit is contained in:
BIN
backend/app.db
Normal file
BIN
backend/app.db
Normal file
Binary file not shown.
142
backend/db.go
Normal file
142
backend/db.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func database() *sql.DB {
|
||||
db, err := sql.Open("sqlite", "./app.db")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Seed default menu items if empty
|
||||
row := db.QueryRow("SELECT COUNT(*) FROM menu_items")
|
||||
var count int
|
||||
_ = row.Scan(&count)
|
||||
if count == 0 {
|
||||
log.Println("Seeding default menu_items…")
|
||||
defaults := []struct {
|
||||
category string
|
||||
name string
|
||||
}{
|
||||
{"soup", "Erőleves"},
|
||||
{"soup", "Dörgicsei Csibeleves"},
|
||||
{"soup", "Újházi Tyúkhúsleves"},
|
||||
{"soup", "Jókai Bableves"},
|
||||
{"soup", "Tárkonyos Raguleves"},
|
||||
{"soup", "Frankfurti Leves"},
|
||||
{"soup", "Tavaszi Zöldségleves"},
|
||||
{"soup", "Fokhagyma Krémleves"},
|
||||
|
||||
{"main", "Babragus Csülök"},
|
||||
{"main", "Rántott Csirkemell"},
|
||||
{"main", "Rántott Gomba és Rántott Camembert"},
|
||||
{"main", "Cordon Bleu"},
|
||||
{"main", "Cigánypecsenye Kakastaréjjal"},
|
||||
{"main", "Mozzarellás-Paradicsomos Csirkemell Roston"},
|
||||
{"main", "Kemencés Fokhagymás Csülökszeletek"},
|
||||
{"main", "Holstein Szelet"},
|
||||
{"main", "Jalapeños Sajtgolyók"},
|
||||
{"main", "Cornflakes Bundában Sült Csirkemell"},
|
||||
{"main", "Baconköntösben Pirított Csirkemáj"},
|
||||
|
||||
{"side", "Friss Saláta Kapros Joghurtos Öntettel"},
|
||||
{"side", "Párolt Rizs"},
|
||||
{"side", "Fűszeres Steakburgonya"},
|
||||
{"side", "Grill Zöldség"},
|
||||
{"side", "Lilahagymás Kukoricasaláta"},
|
||||
}
|
||||
|
||||
for _, d := range defaults {
|
||||
_, err := db.Exec("INSERT INTO menu_items (category, name) VALUES (?, ?)", d.category, d.name)
|
||||
if err != nil {
|
||||
log.Println("Error seeding menu item:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create users table
|
||||
_, 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 {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Create settings table
|
||||
_, 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
|
||||
_, err = db.Exec(`INSERT OR IGNORE INTO settings (key, value) VALUES ('finalize_time', '10:30')`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS menu_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
category TEXT NOT NULL,
|
||||
name TEXT NOT NULL
|
||||
)
|
||||
`)
|
||||
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
|
||||
}
|
||||
24
backend/go.mod
Normal file
24
backend/go.mod
Normal file
@@ -0,0 +1,24 @@
|
||||
module menu
|
||||
|
||||
go 1.25
|
||||
|
||||
require github.com/go-chi/chi/v5 v5.2.3
|
||||
|
||||
require (
|
||||
github.com/go-chi/cors v1.2.2
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
modernc.org/sqlite v1.39.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
55
backend/go.sum
Normal file
55
backend/go.sum
Normal file
@@ -0,0 +1,55 @@
|
||||
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.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
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/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
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 v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/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/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/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/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
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/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||
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/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
|
||||
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
984
backend/main.go
Normal file
984
backend/main.go
Normal file
@@ -0,0 +1,984 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type (
|
||||
Foetel = string
|
||||
Leves = string
|
||||
Koret = string
|
||||
)
|
||||
|
||||
type Valasztek struct {
|
||||
Foetelek []Foetel `json:"foetelek"`
|
||||
Levesek []Leves `json:"levesek"`
|
||||
Koretek []Koret `json:"koretek"`
|
||||
}
|
||||
|
||||
type Menu struct {
|
||||
Foetelek []Foetel
|
||||
Levesek []Leves
|
||||
Koretek []Koret
|
||||
}
|
||||
|
||||
type Order struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Soup string `json:"soup"`
|
||||
Main string `json:"main"`
|
||||
Side string `json:"side"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type App struct {
|
||||
DB *sql.DB
|
||||
Broker *Broker
|
||||
FinalizeUpdate chan struct{} // notify scheduler to reload time
|
||||
}
|
||||
|
||||
type Broker struct {
|
||||
clients map[chan []byte]bool
|
||||
add chan chan []byte
|
||||
remove chan chan []byte
|
||||
broadcast chan []byte
|
||||
}
|
||||
|
||||
type registerRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type loginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type selectionRequest struct {
|
||||
Main string `json:"main"`
|
||||
Side string `json:"side"`
|
||||
Soup string `json:"soup"`
|
||||
}
|
||||
|
||||
var jwtSecret = []byte("supersecretkey") // TODO: move to env variable
|
||||
|
||||
func generateToken(userID int, username string) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"user_id": userID,
|
||||
"username": username,
|
||||
"exp": time.Now().Add(2 * time.Hour).Unix(),
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(jwtSecret)
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
var req registerRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Username == "" || req.Password == "" {
|
||||
http.Error(w, "username and password required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := app.DB.Exec(
|
||||
"INSERT INTO users (username, password, role) VALUES (?, ?, ?)",
|
||||
req.Username, req.Password, 100,
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, "username already exists", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
|
||||
token, err := generateToken(int(id), req.Username)
|
||||
if err != nil {
|
||||
http.Error(w, "could not generate token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]string{
|
||||
"token": token,
|
||||
"username": req.Username,
|
||||
})
|
||||
}
|
||||
|
||||
func (app *App) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
var req loginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var id int
|
||||
var storedPassword string
|
||||
err := app.DB.QueryRow("SELECT id, password FROM users WHERE username = ?", req.Username).Scan(&id, &storedPassword)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid username or password", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if storedPassword != req.Password {
|
||||
http.Error(w, "invalid username or password", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := generateToken(id, req.Username)
|
||||
if err != nil {
|
||||
http.Error(w, "could not generate token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]string{
|
||||
"token": token,
|
||||
"username": req.Username,
|
||||
})
|
||||
}
|
||||
|
||||
func usernameFromJWT(r *http.Request) (string, error) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return "", nil
|
||||
}
|
||||
tokenStr := parts[1]
|
||||
|
||||
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) { return jwtSecret, nil })
|
||||
if err != nil || !token.Valid {
|
||||
return "", err
|
||||
}
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
||||
if u, ok := claims["username"].(string); ok {
|
||||
return u, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func newBroker() *Broker {
|
||||
b := &Broker{
|
||||
clients: make(map[chan []byte]bool),
|
||||
add: make(chan chan []byte),
|
||||
remove: make(chan chan []byte),
|
||||
broadcast: make(chan []byte),
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case c := <-b.add:
|
||||
b.clients[c] = true
|
||||
log.Printf("SSE client added. Total: %d", len(b.clients))
|
||||
case c := <-b.remove:
|
||||
if _, ok := b.clients[c]; ok {
|
||||
delete(b.clients, c)
|
||||
close(c)
|
||||
log.Printf("SSE client removed. Total: %d", len(b.clients))
|
||||
}
|
||||
case msg := <-b.broadcast:
|
||||
log.Printf("Broker broadcasting to %d clients", len(b.clients))
|
||||
for c := range b.clients {
|
||||
select {
|
||||
case c <- msg:
|
||||
log.Println("Message queued for client")
|
||||
default:
|
||||
log.Println("Client channel full, skipping")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return b
|
||||
}
|
||||
|
||||
func (app *App) handleGetOrders(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := app.DB.Query("SELECT id, username, soup, main, side, created_at, status FROM orders WHERE status = 'active' ORDER BY id DESC")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []Order
|
||||
for rows.Next() {
|
||||
var o Order
|
||||
if err := rows.Scan(&o.ID, &o.Username, &o.Soup, &o.Main, &o.Side, &o.CreatedAt, &o.Status); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
out = append(out, o)
|
||||
}
|
||||
writeJSON(w, out)
|
||||
}
|
||||
|
||||
func (app *App) handleAddOrder(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct{ Soup, Main, Side string }
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil || in.Main == "" || in.Side == "" {
|
||||
http.Error(w, "invalid order", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
username, err := usernameFromJWT(r)
|
||||
if err != nil || username == "" {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Format(time.RFC3339)
|
||||
|
||||
// // Step 1: mark any previous active orders for this user as history
|
||||
// _, _ = app.DB.Exec("UPDATE orders SET status = 'history' WHERE username = ? AND status = 'active'", username)
|
||||
|
||||
// Step 2: insert new active order
|
||||
res, err := app.DB.Exec(
|
||||
"INSERT INTO orders (username, soup, main, side, created_at, status) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
username, in.Soup, in.Main, in.Side, now, "active",
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
id, _ := res.LastInsertId()
|
||||
ord := Order{
|
||||
ID: int(id),
|
||||
Username: username,
|
||||
Soup: in.Soup,
|
||||
Main: in.Main,
|
||||
Side: in.Side,
|
||||
CreatedAt: now,
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
// broadcast to SSE clients
|
||||
if data, err := json.Marshal(ord); err == nil {
|
||||
log.Printf("Broadcasting active order via SSE: %+v", ord)
|
||||
app.Broker.broadcast <- data
|
||||
}
|
||||
|
||||
writeJSON(w, ord)
|
||||
}
|
||||
|
||||
func (app *App) handleDeleteOrder(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
username, err := usernameFromJWT(r)
|
||||
if err != nil || username == "" {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := app.DB.Exec("UPDATE orders SET status = 'history' WHERE id = ? AND username = ?", id, username)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
http.Error(w, "order not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// broadcast deletion (tell clients to refresh)
|
||||
msg := map[string]any{"id": id, "status": "history", "event": "deleted"}
|
||||
if data, err := json.Marshal(msg); err == nil {
|
||||
app.Broker.broadcast <- data
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]string{"status": "archived"})
|
||||
}
|
||||
|
||||
func (app *App) handleOrdersStream(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Del("Content-Encoding")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "stream unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ch := make(chan []byte, 1)
|
||||
app.Broker.add <- ch
|
||||
defer func() { app.Broker.remove <- ch }()
|
||||
|
||||
// open the stream
|
||||
w.Write([]byte(":ok\n\n"))
|
||||
flusher.Flush()
|
||||
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
log.Println("SSE client disconnected")
|
||||
return
|
||||
case msg := <-ch:
|
||||
log.Printf("SSE send: %s", string(msg))
|
||||
w.Write([]byte("data: "))
|
||||
w.Write(msg)
|
||||
w.Write([]byte("\n\n"))
|
||||
flusher.Flush()
|
||||
case <-ticker.C:
|
||||
// log.Println("SSE heartbeat -> :ping")
|
||||
w.Write([]byte(":ping\n\n"))
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) handleSaveSelection(w http.ResponseWriter, r *http.Request) {
|
||||
tokenStr := r.Header.Get("Authorization")
|
||||
if tokenStr == "" {
|
||||
http.Error(w, "missing token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Strip "Bearer " prefix
|
||||
if len(tokenStr) > 7 && tokenStr[:7] == "Bearer " {
|
||||
tokenStr = tokenStr[7:]
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret, nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
http.Error(w, "invalid token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
userID := int(claims["user_id"].(float64))
|
||||
|
||||
var req selectionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Format(time.RFC3339)
|
||||
|
||||
// Insert or update selection (simple approach: delete old one, insert new)
|
||||
_, _ = app.DB.Exec("DELETE FROM selections WHERE user_id = ?", userID)
|
||||
_, err = app.DB.Exec(
|
||||
"INSERT INTO selections (user_id, main, side, soup, created_at) VALUES (?, ?, ?, ?, ?)",
|
||||
userID, req.Main, req.Side, req.Soup, now,
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to save selection", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]string{
|
||||
"status": "ok",
|
||||
"main": req.Main,
|
||||
"side": req.Side,
|
||||
"soup": req.Soup,
|
||||
})
|
||||
}
|
||||
|
||||
func (app *App) handleGetSelection(w http.ResponseWriter, r *http.Request) {
|
||||
tokenStr := r.Header.Get("Authorization")
|
||||
if tokenStr == "" {
|
||||
http.Error(w, "missing token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if len(tokenStr) > 7 && tokenStr[:7] == "Bearer " {
|
||||
tokenStr = tokenStr[7:]
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret, nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
http.Error(w, "invalid token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
userID := int(claims["user_id"].(float64))
|
||||
|
||||
row := app.DB.QueryRow("SELECT main, side, soup, created_at FROM selections WHERE user_id = ?", userID)
|
||||
var main, side, soup, createdAt string
|
||||
err = row.Scan(&main, &side, &soup, &createdAt)
|
||||
if err == sql.ErrNoRows {
|
||||
writeJSON(w, map[string]string{"status": "none"})
|
||||
return
|
||||
} else if err != nil {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]string{
|
||||
"main": main,
|
||||
"side": side,
|
||||
"soup": soup,
|
||||
"created_at": createdAt,
|
||||
})
|
||||
}
|
||||
|
||||
func (app *App) handleOptions(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := app.DB.Query("SELECT category, name FROM menu_items ORDER BY id ASC")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
valasztek := Valasztek{
|
||||
Levesek: []Leves{},
|
||||
Foetelek: []Foetel{},
|
||||
Koretek: []Koret{},
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var category, name string
|
||||
if err := rows.Scan(&category, &name); err == nil {
|
||||
switch category {
|
||||
case "soup":
|
||||
valasztek.Levesek = append(valasztek.Levesek, name)
|
||||
case "main":
|
||||
valasztek.Foetelek = append(valasztek.Foetelek, name)
|
||||
case "side":
|
||||
valasztek.Koretek = append(valasztek.Koretek, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, valasztek)
|
||||
}
|
||||
|
||||
func (app *App) handleGetMenuRaw(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := app.DB.Query("SELECT id, category, name FROM menu_items ORDER BY id ASC")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []map[string]any
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var category, name string
|
||||
if err := rows.Scan(&id, &category, &name); err == nil {
|
||||
items = append(items, map[string]any{
|
||||
"id": id,
|
||||
"category": category,
|
||||
"name": name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, items)
|
||||
}
|
||||
|
||||
func (app *App) getUserRole(username string) (int, error) {
|
||||
var role int
|
||||
err := app.DB.QueryRow("SELECT role FROM users WHERE username = ?", username).Scan(&role)
|
||||
return role, err
|
||||
}
|
||||
|
||||
func (app *App) requireLevel(maxAllowed int) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := usernameFromJWT(r)
|
||||
if err != nil || username == "" {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
role, err := app.getUserRole(username)
|
||||
if err != nil {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if role > maxAllowed {
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) handleWhoAmI(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := usernameFromJWT(r)
|
||||
if err != nil || username == "" {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
role, err := app.getUserRole(username)
|
||||
if err != nil {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]any{
|
||||
"username": username,
|
||||
"role": role,
|
||||
})
|
||||
}
|
||||
|
||||
func (app *App) handleGetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := app.DB.Query("SELECT id, username, role FROM users ORDER BY id")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var users []map[string]any
|
||||
for rows.Next() {
|
||||
var id, role int
|
||||
var username string
|
||||
rows.Scan(&id, &username, &role)
|
||||
users = append(users, map[string]any{"id": id, "username": username, "role": role})
|
||||
}
|
||||
|
||||
writeJSON(w, users)
|
||||
}
|
||||
|
||||
func (app *App) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
res, err := app.DB.Exec("DELETE FROM users WHERE id = ?", id)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
http.Error(w, "user not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
// main.go
|
||||
func (app *App) handleUpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Role *int `json:"role"` // pointer = optional
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Username == "" && req.Password == "" && req.Role == nil {
|
||||
http.Error(w, "nothing to update", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// build dynamic UPDATE
|
||||
query := "UPDATE users SET "
|
||||
args := []any{}
|
||||
first := true
|
||||
if req.Username != "" {
|
||||
if !first {
|
||||
query += ", "
|
||||
}
|
||||
first = false
|
||||
query += "username = ?"
|
||||
args = append(args, req.Username)
|
||||
}
|
||||
if req.Password != "" {
|
||||
if !first {
|
||||
query += ", "
|
||||
}
|
||||
first = false
|
||||
query += "password = ?"
|
||||
args = append(args, req.Password)
|
||||
}
|
||||
if req.Role != nil {
|
||||
if !first {
|
||||
query += ", "
|
||||
}
|
||||
first = false
|
||||
query += "role = ?"
|
||||
args = append(args, *req.Role)
|
||||
}
|
||||
query += " WHERE id = ?"
|
||||
args = append(args, id)
|
||||
|
||||
res, err := app.DB.Exec(query, args...)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n == 0 {
|
||||
http.Error(w, "user not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]string{"status": "updated"})
|
||||
}
|
||||
|
||||
func (app *App) handleAdminMenu(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
Action string `json:"action"`
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch in.Action {
|
||||
case "add":
|
||||
_, err := app.DB.Exec("INSERT INTO menu_items (category, name) VALUES (?, ?)", in.Category, in.Name)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
case "update":
|
||||
_, err := app.DB.Exec("UPDATE menu_items SET name = ? WHERE id = ?", in.Name, in.ID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
case "delete":
|
||||
_, err := app.DB.Exec("DELETE FROM menu_items WHERE id = ?", in.ID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// return updated menu
|
||||
rows, _ := app.DB.Query("SELECT id, category, name FROM menu_items ORDER BY id ASC")
|
||||
defer rows.Close()
|
||||
var items []map[string]any
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var category, name string
|
||||
rows.Scan(&id, &category, &name)
|
||||
items = append(items, map[string]any{"id": id, "category": category, "name": name})
|
||||
}
|
||||
writeJSON(w, items)
|
||||
}
|
||||
|
||||
func (app *App) handleGetAllOrders(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := app.DB.Query("SELECT id, username, soup, main, side, created_at, status FROM orders ORDER BY id DESC")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []Order
|
||||
for rows.Next() {
|
||||
var o Order
|
||||
if err := rows.Scan(&o.ID, &o.Username, &o.Soup, &o.Main, &o.Side, &o.CreatedAt, &o.Status); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
out = append(out, o)
|
||||
}
|
||||
writeJSON(w, out)
|
||||
}
|
||||
|
||||
func (app *App) handleAdminUpdateOrderStatus(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
var in struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := app.DB.Exec("UPDATE orders SET status = ? WHERE id = ?", in.Status, id)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
switch in.Status {
|
||||
case "history":
|
||||
// If status is history -> send deleted event
|
||||
msg := map[string]any{"id": id, "status": "history", "event": "deleted"}
|
||||
if data, err := json.Marshal(msg); err == nil {
|
||||
app.Broker.broadcast <- data
|
||||
}
|
||||
case "active":
|
||||
// If status is active -> send new order
|
||||
var o Order
|
||||
row := app.DB.QueryRow("SELECT id, username, soup, main, side, created_at, status FROM orders WHERE id = ?", id)
|
||||
if err := row.Scan(&o.ID, &o.Username, &o.Soup, &o.Main, &o.Side, &o.CreatedAt, &o.Status); err == nil {
|
||||
if data, err := json.Marshal(o); err == nil {
|
||||
app.Broker.broadcast <- data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (app *App) handleAdminDeleteOrder(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
// Step 1: set to history (so user views update immediately)
|
||||
_, _ = app.DB.Exec("UPDATE orders SET status = 'history' WHERE id = ?", id)
|
||||
|
||||
// Step 2: broadcast history update
|
||||
msg := map[string]any{"id": id, "status": "history", "event": "deleted"}
|
||||
if data, err := json.Marshal(msg); err == nil {
|
||||
app.Broker.broadcast <- data
|
||||
}
|
||||
|
||||
// Step 3: hard delete from DB
|
||||
_, err := app.DB.Exec("DELETE FROM orders WHERE id = ?", id)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
// GET current finalize time
|
||||
func (app *App) handleGetFinalizeTime(w http.ResponseWriter, r *http.Request) {
|
||||
var t string
|
||||
err := app.DB.QueryRow("SELECT value FROM settings WHERE key = 'finalize_time'").Scan(&t)
|
||||
if err == sql.ErrNoRows {
|
||||
t = "10:30" // default if not set
|
||||
}
|
||||
writeJSON(w, map[string]string{"time": t})
|
||||
}
|
||||
|
||||
// POST new finalize time
|
||||
func (app *App) handleSetFinalizeTime(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
Time string `json:"time"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
http.Error(w, "bad request", 400)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = app.DB.Exec(`INSERT OR REPLACE INTO settings (key,value) VALUES ('finalize_time', ?)`, in.Time)
|
||||
|
||||
// notify scheduler
|
||||
select {
|
||||
case app.FinalizeUpdate <- struct{}{}:
|
||||
default: // don’t block if already queued
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
// POST trigger finalize now
|
||||
func (app *App) handleFinalizeNow(w http.ResponseWriter, r *http.Request) {
|
||||
if err := finalizeOrders(app); err != nil {
|
||||
http.Error(w, "failed", 500)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]string{"status": "done"})
|
||||
}
|
||||
|
||||
func (app *App) getSetting(key, def string) string {
|
||||
var v string
|
||||
err := app.DB.QueryRow("SELECT value FROM settings WHERE key = ?", key).Scan(&v)
|
||||
if err != nil || v == "" {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func startDailyCleanup(app *App) {
|
||||
loc, _ := time.LoadLocation("Europe/Budapest")
|
||||
|
||||
go func() {
|
||||
for {
|
||||
// read finalize_time from DB (default "10:30")
|
||||
t := app.getSetting("finalize_time", "10:30")
|
||||
|
||||
// parse "HH:MM"
|
||||
parts := strings.Split(t, ":")
|
||||
hour, _ := strconv.Atoi(parts[0])
|
||||
minute, _ := strconv.Atoi(parts[1])
|
||||
|
||||
now := time.Now().In(loc)
|
||||
next := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, loc)
|
||||
if !next.After(now) {
|
||||
next = next.Add(24 * time.Hour)
|
||||
}
|
||||
|
||||
log.Println("Next finalize scheduled at", next)
|
||||
|
||||
timer := time.NewTimer(time.Until(next))
|
||||
|
||||
select {
|
||||
case <-timer.C:
|
||||
if err := finalizeOrders(app); err != nil {
|
||||
log.Println("daily finalize failed:", err)
|
||||
} else {
|
||||
log.Println("orders finalized at", time.Now().In(loc))
|
||||
}
|
||||
case <-app.FinalizeUpdate:
|
||||
log.Println("Finalize time updated, recalculating…")
|
||||
timer.Stop()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func finalizeOrders(app *App) error {
|
||||
rows, err := app.DB.Query("SELECT id, username, soup, main, side FROM orders WHERE status = 'active'")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var summary []Order
|
||||
for rows.Next() {
|
||||
var o Order
|
||||
if err := rows.Scan(&o.ID, &o.Username, &o.Soup, &o.Main, &o.Side); err == nil {
|
||||
summary = append(summary, o)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: send summary (log, email, or message)
|
||||
sendSummary(summary)
|
||||
|
||||
// Step 3: archive
|
||||
_, err = app.DB.Exec("UPDATE orders SET status = 'history' WHERE status = 'active'")
|
||||
|
||||
// Step 4: broadcast deletions to SSE clients
|
||||
for _, o := range summary {
|
||||
msg := map[string]any{"id": o.ID, "status": "history", "event": "deleted"}
|
||||
if data, err := json.Marshal(msg); err == nil {
|
||||
app.Broker.broadcast <- data
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func sendSummary(orders []Order) {
|
||||
// TODO: replace with email or message
|
||||
log.Println("Daily summary:")
|
||||
for _, o := range orders {
|
||||
log.Printf("%s: %s, %s, %s", o.Username, o.Soup, o.Main, o.Side)
|
||||
}
|
||||
}
|
||||
|
||||
func server(app *App) *http.Server {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// Middleware
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300,
|
||||
}))
|
||||
|
||||
// Routes (bind methods to app)
|
||||
r.Post("/api/register", app.handleRegister)
|
||||
r.Post("/api/login", app.handleLogin)
|
||||
r.Get("/api/options", app.handleOptions)
|
||||
r.Post("/api/selection", app.handleSaveSelection)
|
||||
r.Get("/api/selection", app.handleGetSelection)
|
||||
r.Get("/api/orders", app.handleGetOrders)
|
||||
r.Post("/api/orders", app.handleAddOrder)
|
||||
r.Get("/api/orders/stream", app.handleOrdersStream)
|
||||
r.Delete("/api/orders/{id}", app.handleDeleteOrder)
|
||||
r.Get("/api/me", app.handleWhoAmI)
|
||||
r.Get("/api/finalize/time", app.handleGetFinalizeTime)
|
||||
|
||||
// Only role 0 and 1 allowed
|
||||
r.Route("/api/admin", func(r chi.Router) {
|
||||
r.Use(app.requireLevel(1))
|
||||
r.Get("/users", app.handleGetUsers)
|
||||
r.Post("/menu", app.handleAdminMenu)
|
||||
r.Get("/menu", app.handleGetMenuRaw)
|
||||
r.Get("/users", app.handleGetUsers)
|
||||
r.Delete("/users/{id}", app.handleDeleteUser)
|
||||
r.Put("/users/{id}", app.handleUpdateUser)
|
||||
r.Get("/orders", app.handleGetAllOrders)
|
||||
r.Put("/orders/{id}/status", app.handleAdminUpdateOrderStatus)
|
||||
r.Delete("/orders/{id}", app.handleAdminDeleteOrder)
|
||||
r.Get("/finalize/time", app.handleGetFinalizeTime)
|
||||
r.Post("/finalize/time", app.handleSetFinalizeTime)
|
||||
r.Post("/finalize/now", app.handleFinalizeNow)
|
||||
})
|
||||
|
||||
// Moderators (role <= 50)
|
||||
r.Route("/api/mod", func(r chi.Router) {
|
||||
r.Use(app.requireLevel(50))
|
||||
r.Post("/menu", app.handleAdminMenu) // menu editor
|
||||
r.Get("/menu", app.handleGetMenuRaw)
|
||||
r.Get("/finalize/time", app.handleGetFinalizeTime)
|
||||
r.Post("/finalize/time", app.handleSetFinalizeTime)
|
||||
r.Post("/finalize/now", app.handleFinalizeNow)
|
||||
})
|
||||
|
||||
// Return configured server
|
||||
return &http.Server{
|
||||
Addr: "0.0.0.0:7153",
|
||||
Handler: r,
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
db := database()
|
||||
defer db.Close()
|
||||
|
||||
app := &App{
|
||||
DB: db,
|
||||
Broker: newBroker(),
|
||||
FinalizeUpdate: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
// Create server
|
||||
srv := server(app)
|
||||
|
||||
log.Println("Server listening on", srv.Addr)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
go startDailyCleanup(app)
|
||||
|
||||
}
|
||||
BIN
backend/sqlite3.exe
Normal file
BIN
backend/sqlite3.exe
Normal file
Binary file not shown.
Reference in New Issue
Block a user