a1d93f7a8e
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
97 lines
2.4 KiB
Go
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
|
|
}
|