378 lines
9.7 KiB
Go
378 lines
9.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
// 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})
|
|
}
|
|
|
|
// Handle User Registration
|
|
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,
|
|
})
|
|
}
|
|
|
|
// Handle User Login
|
|
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,
|
|
})
|
|
}
|
|
|
|
// Get Menu Options from DB
|
|
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)
|
|
}
|
|
|
|
// Handle User Saved Selection
|
|
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,
|
|
})
|
|
}
|
|
|
|
// Handle User's selected Menu
|
|
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,
|
|
})
|
|
}
|
|
|
|
// Get all active Orders
|
|
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)
|
|
}
|
|
|
|
// Handle new Order
|
|
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)
|
|
}
|
|
|
|
// Handle Order deletion
|
|
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"})
|
|
}
|
|
|
|
// Send Orders over SSE to client
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get User's information and Role
|
|
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,
|
|
})
|
|
}
|