package main import ( "database/sql" "encoding/json" "fmt" "log" "net/http" "os" "strconv" "strings" "time" "github.com/joho/godotenv" "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 FinalizedSummary struct { Pickup []string `json:"pickup"` Kitchen []string `json:"kitchen"` } type App struct { DB *sql.DB Broker *Broker FinalizeUpdate chan struct{} // notify scheduler to reload time LastSummary *FinalizedSummary } 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(app, 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 } time.Sleep(20 * time.Millisecond) } return err } func sendSummary(app *App, orders []Order) *FinalizedSummary { // Pickup view userMap := make(map[string][]string) for _, o := range orders { items := []string{} if o.Soup != "" { items = append(items, o.Soup) } items = append(items, o.Main, o.Side) userMap[o.Username] = append(userMap[o.Username], items...) } pickup := []string{} for user, items := range userMap { pickup = append(pickup, fmt.Sprintf("%s: [%s]", user, strings.Join(items, ", "))) } // Kitchen view kitchenMap := make(map[string]int) for _, o := range orders { if o.Soup != "" { kitchenMap[o.Soup]++ } kitchenMap[o.Main]++ kitchenMap[o.Side]++ } kitchen := []string{} for item, count := range kitchenMap { kitchen = append(kitchen, fmt.Sprintf("%s: %d", item, count)) } summary := &FinalizedSummary{Pickup: pickup, Kitchen: kitchen} app.LastSummary = summary // Log log.Println("=== Pickup view ===") for _, line := range pickup { log.Println(line) } log.Println("=== Kitchen view ===") for _, line := range kitchen { log.Println(line) } return summary } func (app *App) handleGetLastSummary(w http.ResponseWriter, r *http.Request) { if app.LastSummary == nil { writeJSON(w, map[string]string{"status": "none"}) return } writeJSON(w, app.LastSummary) } 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) r.Get("/finalize/last", app.handleGetLastSummary) }) // 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) r.Get("/finalize/last", app.handleGetLastSummary) }) // Return configured server return &http.Server{ Addr: "0.0.0.0:7153", Handler: r, } } func main() { err := godotenv.Load() if err != nil { log.Println("Cannot find .env file, using system env variables") } adminUser := os.Getenv("ADMIN_USER") adminPass := os.Getenv("ADMIN_PASS") if adminUser == "" || adminPass == "" { log.Fatal("No ADMIN_USER or ADMIN_PASS env variable set") } db := database() defer db.Close() app := &App{ DB: db, Broker: newBroker(), FinalizeUpdate: make(chan struct{}, 1), } // Create server srv := server(app) go startDailyCleanup(app) log.Println("Server listening on", srv.Addr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err) } }