Initial commit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christoph K.
2026-03-21 15:03:55 +01:00
commit dfd66e43c6
78 changed files with 6219 additions and 0 deletions

View 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)
}

View 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
}

View 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)
})
}

View 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)
}

View 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)
}

View 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)
}