aurganize-backend/backend/internal/handlers/user_handler.go

287 lines
9.7 KiB
Go

package handlers
import (
"net/http"
"github.com/creativenoz/aurganize-v62/backend/internal/config"
"github.com/creativenoz/aurganize-v62/backend/internal/models"
"github.com/creativenoz/aurganize-v62/backend/internal/services"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
)
type UserRegisterHander struct {
config *config.Config
authService *services.AuthService
userService *services.UserService
tenantService *services.TenantService
}
func NewUserRegisterHandler(config *config.Config, authService *services.AuthService, userService *services.UserService, tenantService *services.TenantService) *UserRegisterHander {
log.Info().
Str("handler", "user_register").
Str("component", "handler_init").
Bool("has_auth_service", authService != nil).
Bool("has_user_service", userService != nil).
Bool("has_tenant_service", tenantService != nil).
Msg("user registration handler initialized")
return &UserRegisterHander{
config: config,
authService: authService,
userService: userService,
tenantService: tenantService,
}
}
type RegisterUserRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
FirstName *string `json:"first_name"`
LastName *string `json:"last_name"`
TenantName string `json:"tenant_name" validate:"required"`
}
type RegisterUserResponse struct {
User *models.UserResponse `json:"user"`
Tenant interface{} `json:"tenant"`
AccessToken string `json:"access_token"`
RefershToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
}
func (h *UserRegisterHander) Register(c echo.Context) error {
log.Info().
Str("handler", "user_register").
Str("action", "registration_attempt").
Str("ip", c.RealIP()).
Str("user_agent", c.Request().UserAgent()).
Msg("new user registration attempt started")
var req RegisterUserRequest
if err := c.Bind(&req); err != nil {
log.Warn().
Str("handler", "user_register").
Str("action", "registration_bind_failed").
Str("ip", c.RealIP()).
Err(err).
Msg("failed to bind registration request - malformed json or content-type")
return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
}
if err := c.Validate(&req); err != nil {
log.Warn().
Str("handler", "user_register").
Str("action", "registration_validation_failed").
Str("email", req.Email).
Str("tenant_name", req.TenantName).
Str("validation_error", err.Error()).
Bool("has_first_name", req.FirstName != nil).
Bool("has_last_name", req.LastName != nil).
Str("ip", c.RealIP()).
Msg("registration validation failed")
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
log.Info().
Str("handler", "user_register").
Str("action", "checking_tenant_availability").
Str("tenant_name", req.TenantName).
Str("email", req.Email).
Msg("validated registration request, checking tenant name availability")
ctx := c.Request().Context()
tenantExists, err := h.tenantService.SlugExists(ctx, req.TenantName)
if err != nil {
log.Error().
Str("handler", "user_register").
Str("action", "tenant_check_failed").
Str("tenant_name", req.TenantName).
Str("email", req.Email).
Err(err).
Msg("failed to check tenant name availability - database or service error")
return echo.NewHTTPError(http.StatusInternalServerError, "failed to check tenant")
}
if tenantExists {
log.Warn().
Str("handler", "user_register").
Str("action", "tenant_name_conflict").
Str("requested_tenant_name", req.TenantName).
Str("email", req.Email).
Str("ip", c.RealIP()).
Msg("registration failed - organization name already taken")
return echo.NewHTTPError(http.StatusConflict, "organization name already taken")
}
log.Info().
Str("handler", "user_register").
Str("action", "creating_tenant_and_user").
Str("tenant_name", req.TenantName).
Str("email", req.Email).
Bool("has_first_name", req.FirstName != nil).
Bool("has_last_name", req.LastName != nil).
Msg("tenant available, creating organization and user account")
tenant, user, err := h.tenantService.CreateWithUser(ctx, &models.CreateTenantWithUserInput{
TenantName: req.TenantName,
Email: &req.Email,
Password: &req.Password,
FirstName: req.FirstName,
LastName: req.LastName,
})
if err != nil {
if err == services.ErrEmailAlreadyExists {
log.Warn().
Str("handler", "user_register").
Str("action", "email_already_exists").
Str("email", req.Email).
Str("tenant_name", req.TenantName).
Str("ip", c.RealIP()).
Msg("registration failed - email already registered")
return echo.NewHTTPError(http.StatusConflict, "email already registered")
}
if err == services.ErrWeakPassword {
log.Warn().
Str("handler", "user_register").
Str("action", "weak_password_rejected").
Str("email", req.Email).
Str("tenant_name", req.TenantName).
Int("password_length", len(req.Password)).
Msg("registration failed - password too weak")
return echo.NewHTTPError(http.StatusBadRequest, "password is too weak")
}
log.Error().
Str("handler", "user_register").
Str("action", "registration_failed_unexpected").
Str("email", req.Email).
Str("tenant_name", req.TenantName).
Err(err).
Msg("registration failed - unexpected error during account creation")
return echo.NewHTTPError(http.StatusInternalServerError, "registration failed")
}
log.Info().
Str("handler", "user_register").
Str("action", "account_created_successfully").
Str("user_id", user.ID.String()).
Str("tenant_id", user.TenantID.String()).
Str("tenant_name", tenant.Name).
Str("email", user.Email).
Str("user_role", user.Role).
Msg("account created successfully, generating authentication tokens")
// Generate Tokens
accessToken, err := h.authService.GenerateAccessToken(user)
if err != nil {
log.Error().
Str("handler", "user_register").
Str("action", "access_token_generation_failed").
Str("user_id", user.ID.String()).
Str("tenant_id", user.TenantID.String()).
Str("email", user.Email).
Err(err).
Msg("CRITICAL: account created but failed to generate access token")
return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate access token")
}
userAgent := c.Request().UserAgent()
ipAddress := c.RealIP()
log.Debug().
Str("handler", "user_register").
Str("action", "generating_refresh_token").
Str("user_id", user.ID.String()).
Str("ip", ipAddress).
Str("user_agent", userAgent).
Msg("generating refresh token and creating first session")
refreshToken, _, err := h.authService.GenerateRefreshToken(ctx, user, &userAgent, &ipAddress)
if err != nil {
log.Error().
Str("handler", "user_register").
Str("action", "refresh_token_generation_failed").
Str("user_id", user.ID.String()).
Str("tenant_id", user.TenantID.String()).
Str("email", user.Email).
Err(err).
Msg("CRITICAL: account created but failed to generate refresh token")
return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate refresh token")
}
log.Debug().
Str("handler", "user_register").
Str("action", "setting_auth_cookies").
Str("user_id", user.ID.String()).
Str("cookie_domain", h.config.Cookie.CookieDomain).
Bool("cookie_secure", h.config.Cookie.CookieSecure).
Msg("setting access and refresh token cookies")
h.setAccessTokenCookie(c, accessToken)
h.setRefreshTokenCookie(c, refreshToken)
log.Info().
Str("handler", "user_register").
Str("action", "registration_success").
Str("user_id", user.ID.String()).
Str("tenant_id", user.TenantID.String()).
Str("email", user.Email).
Str("tenant_name", tenant.Name).
Str("user_role", user.Role).
Str("ip", ipAddress).
Str("user_agent", userAgent).
Bool("has_full_name", req.FirstName != nil && req.LastName != nil).
Msg("user registration completed successfully")
return c.JSON(
http.StatusCreated, RegisterUserResponse{
User: user.ToResponse(),
Tenant: tenant.ToResponse(),
AccessToken: accessToken,
RefershToken: refreshToken,
ExpiresIn: int(h.config.JWT.AccessExpiry.Seconds()),
},
)
}
func (h *UserRegisterHander) setAccessTokenCookie(c echo.Context, token string) {
cookie := &http.Cookie{
Name: "access_token",
Value: token,
Path: "/", // Available to all paths
Domain: h.config.Cookie.CookieDomain,
MaxAge: int(h.config.JWT.AccessExpiry.Seconds()), // Browser deletes after this time
Secure: h.config.Cookie.CookieSecure, // HTTPS only in production
HttpOnly: true, // JavaScript cannot access (XSS protection)
SameSite: h.parseSameSite(h.config.Cookie.CookieSameSite), // CSRF protection
}
c.SetCookie(cookie)
}
func (h *UserRegisterHander) setRefreshTokenCookie(c echo.Context, token string) {
cookie := &http.Cookie{
Name: "refresh_token",
Value: token,
Path: "/", // Available to all paths
Domain: h.config.Cookie.CookieDomain,
MaxAge: int(h.config.JWT.RefreshExpiry.Seconds()), // Much longer than access token
Secure: h.config.Cookie.CookieSecure, // HTTPS only in production
HttpOnly: true, // JavaScript cannot access (XSS protection)
SameSite: h.parseSameSite(h.config.Cookie.CookieSameSite), // CSRF protection
}
c.SetCookie(cookie)
}
func (h *UserRegisterHander) parseSameSite(s string) http.SameSite {
switch s {
case "strict":
return http.SameSiteStrictMode // Never send cookie cross-site
case "lax":
return http.SameSiteLaxMode // Send on top-level navigation only
case "none":
return http.SameSiteNoneMode // Always send (requires Secure=true)
default:
return http.SameSiteDefaultMode // Let browser decide
}
}