287 lines
9.7 KiB
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
|
|
}
|
|
}
|