Files
feedmee/backend/main.go
2025-10-16 09:22:18 +02:00

1038 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"database/sql"
"encoding/json"
"fmt"
"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 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: // dont 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() {
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)
}
}