Initial commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
86
backend/internal/handler/exercise.go
Executable file
86
backend/internal/handler/exercise.go
Executable file
@@ -0,0 +1,86 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"krafttrainer/internal/model"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (h *Handler) handleListExercises(w http.ResponseWriter, r *http.Request) {
|
||||
muscleGroup := r.URL.Query().Get("muscle_group")
|
||||
query := r.URL.Query().Get("q")
|
||||
|
||||
exercises, err := h.store.ListExercises(muscleGroup, query)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Laden der Übungen")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, exercises)
|
||||
}
|
||||
|
||||
func (h *Handler) handleCreateExercise(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.CreateExerciseRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiger Request-Body")
|
||||
return
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
exercise, err := h.store.CreateExercise(&req)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Erstellen der Übung")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, exercise)
|
||||
}
|
||||
|
||||
func (h *Handler) handleUpdateExercise(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültige ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req model.CreateExerciseRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiger Request-Body")
|
||||
return
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
exercise, err := h.store.UpdateExercise(id, &req)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Aktualisieren der Übung")
|
||||
return
|
||||
}
|
||||
if exercise == nil {
|
||||
writeError(w, http.StatusNotFound, "Übung nicht gefunden")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, exercise)
|
||||
}
|
||||
|
||||
func (h *Handler) handleDeleteExercise(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültige ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.store.SoftDeleteExercise(id)
|
||||
if err == sql.ErrNoRows {
|
||||
writeError(w, http.StatusNotFound, "Übung nicht gefunden")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Löschen der Übung")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
83
backend/internal/handler/handler.go
Executable file
83
backend/internal/handler/handler.go
Executable file
@@ -0,0 +1,83 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"krafttrainer/internal/store"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Handler bündelt alle HTTP-Handler und hält eine Referenz auf den Store.
|
||||
type Handler struct {
|
||||
store *store.Store
|
||||
}
|
||||
|
||||
// New erstellt einen neuen Handler.
|
||||
func New(store *store.Store) *Handler {
|
||||
return &Handler{store: store}
|
||||
}
|
||||
|
||||
// RegisterRoutes registriert alle API-Routen am ServeMux.
|
||||
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
|
||||
// Exercises
|
||||
mux.HandleFunc("GET /api/v1/exercises", h.handleListExercises)
|
||||
mux.HandleFunc("POST /api/v1/exercises", h.handleCreateExercise)
|
||||
mux.HandleFunc("PUT /api/v1/exercises/{id}", h.handleUpdateExercise)
|
||||
mux.HandleFunc("DELETE /api/v1/exercises/{id}", h.handleDeleteExercise)
|
||||
|
||||
// Training Sets
|
||||
mux.HandleFunc("GET /api/v1/sets", h.handleListSets)
|
||||
mux.HandleFunc("POST /api/v1/sets", h.handleCreateSet)
|
||||
mux.HandleFunc("PUT /api/v1/sets/{id}", h.handleUpdateSet)
|
||||
mux.HandleFunc("DELETE /api/v1/sets/{id}", h.handleDeleteSet)
|
||||
|
||||
// Sessions
|
||||
mux.HandleFunc("POST /api/v1/sessions", h.handleCreateSession)
|
||||
mux.HandleFunc("GET /api/v1/sessions", h.handleListSessions)
|
||||
mux.HandleFunc("GET /api/v1/sessions/{id}", h.handleGetSession)
|
||||
mux.HandleFunc("PUT /api/v1/sessions/{id}/end", h.handleEndSession)
|
||||
|
||||
// Session Logs
|
||||
mux.HandleFunc("POST /api/v1/sessions/{id}/logs", h.handleCreateLog)
|
||||
mux.HandleFunc("PUT /api/v1/sessions/{id}/logs/{logId}", h.handleUpdateLog)
|
||||
mux.HandleFunc("DELETE /api/v1/sessions/{id}/logs/{logId}", h.handleDeleteLog)
|
||||
|
||||
// Stats
|
||||
mux.HandleFunc("GET /api/v1/exercises/{id}/last-log", h.handleGetLastLog)
|
||||
mux.HandleFunc("GET /api/v1/exercises/{id}/history", h.handleGetExerciseHistory)
|
||||
mux.HandleFunc("GET /api/v1/stats/overview", h.handleGetStatsOverview)
|
||||
}
|
||||
|
||||
// --- Hilfsfunktionen ---
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, data any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, message string) {
|
||||
writeJSON(w, status, map[string]string{"error": message})
|
||||
}
|
||||
|
||||
func decodeJSON(r *http.Request, dst any) error {
|
||||
dec := json.NewDecoder(r.Body)
|
||||
dec.DisallowUnknownFields()
|
||||
return dec.Decode(dst)
|
||||
}
|
||||
|
||||
func pathID(r *http.Request, name string) (int64, error) {
|
||||
return strconv.ParseInt(r.PathValue(name), 10, 64)
|
||||
}
|
||||
|
||||
func queryInt(r *http.Request, name string, defaultVal int) int {
|
||||
v := r.URL.Query().Get(name)
|
||||
if v == "" {
|
||||
return defaultVal
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return defaultVal
|
||||
}
|
||||
return n
|
||||
}
|
||||
52
backend/internal/handler/middleware.go
Executable file
52
backend/internal/handler/middleware.go
Executable file
@@ -0,0 +1,52 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Chain wendet Middlewares in der angegebenen Reihenfolge an.
|
||||
func Chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
|
||||
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||
h = middlewares[i](h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// CORS erlaubt Cross-Origin-Requests vom Vite Dev-Server.
|
||||
func CORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "http://*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RequestLogger loggt eingehende Requests mit Dauer.
|
||||
func RequestLogger(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
next.ServeHTTP(w, r)
|
||||
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
|
||||
})
|
||||
}
|
||||
|
||||
// Recoverer fängt Panics in Handlern ab und gibt 500 zurück.
|
||||
func Recoverer(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Printf("PANIC: %v", err)
|
||||
writeError(w, http.StatusInternalServerError, "Interner Serverfehler")
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
190
backend/internal/handler/session.go
Executable file
190
backend/internal/handler/session.go
Executable file
@@ -0,0 +1,190 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"krafttrainer/internal/model"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (h *Handler) handleCreateSession(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.CreateSessionRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiger Request-Body")
|
||||
return
|
||||
}
|
||||
if req.SetID == 0 {
|
||||
writeError(w, http.StatusBadRequest, "set_id ist erforderlich")
|
||||
return
|
||||
}
|
||||
|
||||
session, err := h.store.CreateSession(req.SetID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "existiert nicht") {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Starten der Session")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, session)
|
||||
}
|
||||
|
||||
func (h *Handler) handleListSessions(w http.ResponseWriter, r *http.Request) {
|
||||
limit := queryInt(r, "limit", 20)
|
||||
offset := queryInt(r, "offset", 0)
|
||||
|
||||
sessions, err := h.store.ListSessions(limit, offset)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Laden der Sessions")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, sessions)
|
||||
}
|
||||
|
||||
func (h *Handler) handleGetSession(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültige ID")
|
||||
return
|
||||
}
|
||||
|
||||
session, err := h.store.GetSession(id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Laden der Session")
|
||||
return
|
||||
}
|
||||
if session == nil {
|
||||
writeError(w, http.StatusNotFound, "Session nicht gefunden")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, session)
|
||||
}
|
||||
|
||||
func (h *Handler) handleEndSession(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültige ID")
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Note string `json:"note"`
|
||||
}
|
||||
// Body ist optional
|
||||
decodeJSON(r, &body)
|
||||
|
||||
session, err := h.store.EndSession(id, body.Note)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Beenden der Session")
|
||||
return
|
||||
}
|
||||
if session == nil {
|
||||
writeError(w, http.StatusNotFound, "Session nicht gefunden oder bereits beendet")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, session)
|
||||
}
|
||||
|
||||
func (h *Handler) handleCreateLog(w http.ResponseWriter, r *http.Request) {
|
||||
sessionID, err := pathID(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültige Session-ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req model.CreateLogRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiger Request-Body")
|
||||
return
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
log, err := h.store.CreateLog(sessionID, &req)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "SESSION_CLOSED") {
|
||||
writeError(w, http.StatusBadRequest, "Session ist bereits beendet")
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "UNIQUE_VIOLATION") {
|
||||
writeError(w, http.StatusConflict, err.Error())
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "existiert nicht") {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Loggen des Satzes")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, log)
|
||||
}
|
||||
|
||||
func (h *Handler) handleUpdateLog(w http.ResponseWriter, r *http.Request) {
|
||||
sessionID, err := pathID(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültige Session-ID")
|
||||
return
|
||||
}
|
||||
logID, err := pathID(r, "logId")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültige Log-ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req model.UpdateLogRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiger Request-Body")
|
||||
return
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
log, err := h.store.UpdateLog(sessionID, logID, &req)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "SESSION_CLOSED") {
|
||||
writeError(w, http.StatusBadRequest, "Session ist bereits beendet")
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Aktualisieren des Satzes")
|
||||
return
|
||||
}
|
||||
if log == nil {
|
||||
writeError(w, http.StatusNotFound, "Log nicht gefunden")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, log)
|
||||
}
|
||||
|
||||
func (h *Handler) handleDeleteLog(w http.ResponseWriter, r *http.Request) {
|
||||
sessionID, err := pathID(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültige Session-ID")
|
||||
return
|
||||
}
|
||||
logID, err := pathID(r, "logId")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültige Log-ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.store.DeleteLog(sessionID, logID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "SESSION_CLOSED") {
|
||||
writeError(w, http.StatusBadRequest, "Session ist bereits beendet")
|
||||
return
|
||||
}
|
||||
if err == sql.ErrNoRows {
|
||||
writeError(w, http.StatusNotFound, "Log nicht gefunden")
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Löschen des Satzes")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
47
backend/internal/handler/stats.go
Executable file
47
backend/internal/handler/stats.go
Executable file
@@ -0,0 +1,47 @@
|
||||
package handler
|
||||
|
||||
import "net/http"
|
||||
|
||||
func (h *Handler) handleGetLastLog(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültige ID")
|
||||
return
|
||||
}
|
||||
|
||||
lastLog, err := h.store.GetLastLog(id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Laden des letzten Logs")
|
||||
return
|
||||
}
|
||||
if lastLog == nil {
|
||||
writeError(w, http.StatusNotFound, "Noch kein Log für diese Übung")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, lastLog)
|
||||
}
|
||||
|
||||
func (h *Handler) handleGetExerciseHistory(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültige ID")
|
||||
return
|
||||
}
|
||||
limit := queryInt(r, "limit", 30)
|
||||
|
||||
logs, err := h.store.GetExerciseHistory(id, limit)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Laden der Übungshistorie")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, logs)
|
||||
}
|
||||
|
||||
func (h *Handler) handleGetStatsOverview(w http.ResponseWriter, r *http.Request) {
|
||||
overview, err := h.store.GetStatsOverview()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Laden der Statistiken")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, overview)
|
||||
}
|
||||
92
backend/internal/handler/training_set.go
Executable file
92
backend/internal/handler/training_set.go
Executable file
@@ -0,0 +1,92 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"krafttrainer/internal/model"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (h *Handler) handleListSets(w http.ResponseWriter, r *http.Request) {
|
||||
sets, err := h.store.ListSets()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Laden der Sets")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, sets)
|
||||
}
|
||||
|
||||
func (h *Handler) handleCreateSet(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.CreateSetRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiger Request-Body")
|
||||
return
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
set, err := h.store.CreateSet(&req)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "existiert nicht") {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Erstellen des Sets")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, set)
|
||||
}
|
||||
|
||||
func (h *Handler) handleUpdateSet(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültige ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req model.UpdateSetRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiger Request-Body")
|
||||
return
|
||||
}
|
||||
if err := req.Validate(); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
set, err := h.store.UpdateSet(id, &req)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "existiert nicht") {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Aktualisieren des Sets")
|
||||
return
|
||||
}
|
||||
if set == nil {
|
||||
writeError(w, http.StatusNotFound, "Set nicht gefunden")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, set)
|
||||
}
|
||||
|
||||
func (h *Handler) handleDeleteSet(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r, "id")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültige ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.store.SoftDeleteSet(id)
|
||||
if err == sql.ErrNoRows {
|
||||
writeError(w, http.StatusNotFound, "Set nicht gefunden")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Löschen des Sets")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
Reference in New Issue
Block a user