Refactored project structure

This commit is contained in:
2025-10-16 22:45:31 +02:00
parent cf74cce51c
commit d13ea9fc27
13 changed files with 1156 additions and 1023 deletions

377
backend/handlers/api.go Normal file
View File

@@ -0,0 +1,377 @@
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,
})
}