Files
marktvogt.de/backend/internal/domain/user/handler.go
T
vikingowl a1d93f7a8e feat: implement MVP backend API
Go backend with Gin, pgx, Valkey (go-valkey), and PostGIS.

Domains:
- Market search with PostGIS geo-queries (ST_DWithin, ST_Distance),
  German full-text search (tsvector + ILIKE fallback for compound words),
  date range filtering, pagination, and slug-based detail endpoint
- Auth with email+password (bcrypt), JWT access tokens (15min),
  session tokens (30d, dual Valkey+Postgres storage), OAuth
  (Google/GitHub/Facebook), magic links, and TOTP 2FA
- User profile with CRUD, soft-delete (30d grace), and restore

Infrastructure:
- 6 database migrations (users, sessions, oauth_accounts, magic_links,
  markets with PostGIS+FTS, totp_secrets)
- Middleware: recovery, request ID, structured logging (slog), CORS,
  per-IP rate limiting, JWT auth
- Seed data: 10 medieval markets across DACH region
- Docker Compose (PostGIS 17 + Valkey 8), multi-stage Dockerfile,
  Woodpecker CI pipeline, Kubernetes manifests
- Justfile, golangci-lint config, env example
2026-02-18 05:52:20 +01:00

97 lines
2.4 KiB
Go

package user
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"marktvogt.de/backend/internal/pkg/apierror"
"marktvogt.de/backend/internal/pkg/validate"
)
type Handler struct {
service *Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
func (h *Handler) GetProfile(c *gin.Context) {
userID := getUserID(c)
u, err := h.service.GetProfile(c.Request.Context(), userID)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
apiErr := apierror.NotFound("user")
c.JSON(apiErr.Status, apierror.NewResponse(apiErr))
return
}
apiErr := apierror.Internal("failed to get profile")
c.JSON(apiErr.Status, apierror.NewResponse(apiErr))
return
}
c.JSON(http.StatusOK, ProfileResponse{Data: ToProfileData(u)})
}
func (h *Handler) UpdateProfile(c *gin.Context) {
var req UpdateProfileRequest
if apiErr := validate.BindJSON(c, &req); apiErr != nil {
c.JSON(apiErr.Status, apierror.NewResponse(apiErr))
return
}
userID := getUserID(c)
u, err := h.service.UpdateProfile(c.Request.Context(), userID, req)
if err != nil {
apiErr := apierror.Internal("failed to update profile")
c.JSON(apiErr.Status, apierror.NewResponse(apiErr))
return
}
c.JSON(http.StatusOK, ProfileResponse{Data: ToProfileData(u)})
}
func (h *Handler) DeleteProfile(c *gin.Context) {
userID := getUserID(c)
if err := h.service.SoftDelete(c.Request.Context(), userID); err != nil {
apiErr := apierror.Internal("failed to delete account")
c.JSON(apiErr.Status, apierror.NewResponse(apiErr))
return
}
c.JSON(http.StatusOK, gin.H{"data": gin.H{"message": "account scheduled for deletion in 30 days"}})
}
func (h *Handler) RestoreProfile(c *gin.Context) {
userID := getUserID(c)
if err := h.service.Restore(c.Request.Context(), userID); err != nil {
msg := err.Error()
if msg == "account not found or not deleted" || msg == "restoration period expired" {
apiErr := apierror.Gone(msg)
c.JSON(apiErr.Status, apierror.NewResponse(apiErr))
return
}
apiErr := apierror.Internal("failed to restore account")
c.JSON(apiErr.Status, apierror.NewResponse(apiErr))
return
}
c.JSON(http.StatusOK, gin.H{"data": gin.H{"message": "account restored"}})
}
func getUserID(c *gin.Context) uuid.UUID {
v, exists := c.Get("user_id")
if !exists {
return uuid.Nil
}
id, ok := v.(uuid.UUID)
if !ok {
return uuid.Nil
}
return id
}