slack-auth-1770277926/services/auth-api/internal/api/handlers/user.go
rdev-worker fd9bf961bb
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /implement-feature auth-system --requirements 'User model with email/...
2026-02-05 07:59:55 +00:00

145 lines
4.1 KiB
Go

package handlers
import (
"errors"
"net/http"
"git.threesix.ai/jordan/slack-auth-1770277926/pkg/app"
"git.threesix.ai/jordan/slack-auth-1770277926/pkg/auth"
"git.threesix.ai/jordan/slack-auth-1770277926/pkg/httperror"
"git.threesix.ai/jordan/slack-auth-1770277926/pkg/httpresponse"
"git.threesix.ai/jordan/slack-auth-1770277926/pkg/logging"
"git.threesix.ai/jordan/slack-auth-1770277926/services/auth-api/internal/domain"
"git.threesix.ai/jordan/slack-auth-1770277926/services/auth-api/internal/service"
)
// User handles HTTP requests for user/auth resources.
type User struct {
svc *service.UserService
logger *logging.Logger
}
// NewUser creates a new User handler with injected dependencies.
func NewUser(svc *service.UserService, logger *logging.Logger) *User {
return &User{
svc: svc,
logger: logger.WithComponent("UserHandler"),
}
}
// RegisterRequest is the request body for user registration.
type RegisterRequest struct {
Email string `json:"email" validate:"required,email,max=254"`
Password string `json:"password" validate:"required,min=8,max=72"`
}
// LoginRequest is the request body for user login.
type LoginRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
// UserResponse is the response for a user resource (excludes password).
type UserResponse struct {
ID string `json:"id"`
Email string `json:"email"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// AuthResponse is the response for authentication endpoints.
type AuthResponse struct {
User UserResponse `json:"user"`
Token string `json:"token"`
}
// toUserResponse converts a domain user to an API response.
func toUserResponse(u *domain.User) UserResponse {
return UserResponse{
ID: u.ID.String(),
Email: u.Email,
CreatedAt: u.CreatedAt.Format("2006-01-02T15:04:05Z"),
UpdatedAt: u.UpdatedAt.Format("2006-01-02T15:04:05Z"),
}
}
// Register creates a new user account.
func (h *User) Register(w http.ResponseWriter, r *http.Request) error {
var req RegisterRequest
if err := app.BindAndValidate(r, &req); err != nil {
return err
}
output, err := h.svc.Register(r.Context(), service.RegisterInput{
Email: req.Email,
Password: req.Password,
})
if err != nil {
return mapUserDomainError(err)
}
httpresponse.Created(w, r, AuthResponse{
User: toUserResponse(output.User),
Token: output.Token,
})
return nil
}
// Login authenticates a user and returns a JWT token.
func (h *User) Login(w http.ResponseWriter, r *http.Request) error {
var req LoginRequest
if err := app.BindAndValidate(r, &req); err != nil {
return err
}
output, err := h.svc.Login(r.Context(), service.LoginInput{
Email: req.Email,
Password: req.Password,
})
if err != nil {
return mapUserDomainError(err)
}
httpresponse.OK(w, r, AuthResponse{
User: toUserResponse(output.User),
Token: output.Token,
})
return nil
}
// Me returns the current authenticated user's profile.
func (h *User) Me(w http.ResponseWriter, r *http.Request) error {
// Get authenticated user from context
authUser := auth.GetUser(r.Context())
if authUser == nil {
return httperror.Unauthorized("authentication required")
}
// Fetch full user data from service
user, err := h.svc.GetByID(r.Context(), domain.UserID(authUser.ID))
if err != nil {
return mapUserDomainError(err)
}
httpresponse.OK(w, r, toUserResponse(user))
return nil
}
// mapUserDomainError converts domain errors to HTTP errors.
func mapUserDomainError(err error) error {
switch {
case errors.Is(err, domain.ErrUserNotFound):
return httperror.NotFound("user not found")
case errors.Is(err, domain.ErrDuplicateUser):
return httperror.Conflict("user with this email already exists")
case errors.Is(err, domain.ErrInvalidEmail):
return httperror.BadRequest("invalid email address")
case errors.Is(err, domain.ErrInvalidPassword):
return httperror.BadRequest("password must be between 8 and 72 characters")
case errors.Is(err, domain.ErrInvalidCredentials):
return httperror.Unauthorized("invalid email or password")
default:
return err
}
}