package publichttp import ( "context" "errors" "fmt" "net/http" "net/mail" "strings" "sync" "time" "galaxy/authsession/internal/service/confirmemailcode" "galaxy/authsession/internal/service/sendemailcode" "galaxy/authsession/internal/service/shared" "galaxy/authsession/internal/telemetry" "github.com/gin-gonic/gin" "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" ) const jsonContentType = "application/json; charset=utf-8" const publicHTTPServiceName = "galaxy-authsession-public" type sendEmailCodeRequest struct { Email string `json:"email"` } type sendEmailCodeResponse struct { ChallengeID string `json:"challenge_id"` } type confirmEmailCodeRequest struct { ChallengeID string `json:"challenge_id"` Code string `json:"code"` ClientPublicKey string `json:"client_public_key"` } type confirmEmailCodeResponse struct { DeviceSessionID string `json:"device_session_id"` } type errorResponse struct { Error errorBody `json:"error"` } type errorBody struct { Code string `json:"code"` Message string `json:"message"` } var configureGinModeOnce sync.Once func newHandlerWithConfig(cfg Config, deps Dependencies) (http.Handler, error) { if err := cfg.Validate(); err != nil { return nil, err } normalizedDeps, err := normalizeDependencies(deps) if err != nil { return nil, err } configureGinModeOnce.Do(func() { gin.SetMode(gin.ReleaseMode) }) engine := gin.New() engine.Use(newOTelMiddleware(normalizedDeps.Telemetry)) engine.Use(withPublicObservability(normalizedDeps.Logger, normalizedDeps.Telemetry)) engine.POST( "/api/v1/public/auth/send-email-code", handleSendEmailCode(normalizedDeps.SendEmailCode, cfg.RequestTimeout), ) engine.POST( "/api/v1/public/auth/confirm-email-code", handleConfirmEmailCode(normalizedDeps.ConfirmEmailCode, cfg.RequestTimeout), ) return engine, nil } func newOTelMiddleware(runtime *telemetry.Runtime) gin.HandlerFunc { options := []otelgin.Option{} if runtime != nil { options = append( options, otelgin.WithTracerProvider(runtime.TracerProvider()), otelgin.WithMeterProvider(runtime.MeterProvider()), ) } return otelgin.Middleware(publicHTTPServiceName, options...) } func handleSendEmailCode(useCase SendEmailCodeUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request sendEmailCodeRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, projectSendEmailCodeError(shared.InvalidRequest(err.Error()))) return } if err := validateSendEmailCodeRequest(&request); err != nil { abortWithProjection(c, projectSendEmailCodeError(shared.InvalidRequest(err.Error()))) return } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, sendemailcode.Input{Email: request.Email}) if err != nil { abortWithProjection(c, projectSendEmailCodeError(err)) return } if err := validateSendEmailCodeResult(&result); err != nil { abortWithProjection(c, unavailableProjection(fmt.Errorf("send email code response: %w", err))) return } c.JSON(http.StatusOK, sendEmailCodeResponse{ChallengeID: result.ChallengeID}) } } func handleConfirmEmailCode(useCase ConfirmEmailCodeUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request confirmEmailCodeRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, projectConfirmEmailCodeError(shared.InvalidRequest(err.Error()))) return } if err := validateConfirmEmailCodeRequest(&request); err != nil { abortWithProjection(c, projectConfirmEmailCodeError(shared.InvalidRequest(err.Error()))) return } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, confirmemailcode.Input{ ChallengeID: request.ChallengeID, Code: request.Code, ClientPublicKey: request.ClientPublicKey, }) if err != nil { abortWithProjection(c, projectConfirmEmailCodeError(err)) return } if err := validateConfirmEmailCodeResult(&result); err != nil { abortWithProjection(c, unavailableProjection(fmt.Errorf("confirm email code response: %w", err))) return } c.JSON(http.StatusOK, confirmEmailCodeResponse{DeviceSessionID: result.DeviceSessionID}) } } func validateSendEmailCodeRequest(request *sendEmailCodeRequest) error { request.Email = strings.TrimSpace(request.Email) if request.Email == "" { return errors.New("email must not be empty") } parsedAddress, err := mail.ParseAddress(request.Email) if err != nil || parsedAddress.Name != "" || parsedAddress.Address != request.Email { return errors.New("email must be a single valid email address") } return nil } func validateSendEmailCodeResult(result *sendemailcode.Result) error { result.ChallengeID = strings.TrimSpace(result.ChallengeID) if result.ChallengeID == "" { return errors.New("challenge_id must not be empty") } return nil } func validateConfirmEmailCodeRequest(request *confirmEmailCodeRequest) error { request.ChallengeID = strings.TrimSpace(request.ChallengeID) if request.ChallengeID == "" { return errors.New("challenge_id must not be empty") } request.Code = strings.TrimSpace(request.Code) if request.Code == "" { return errors.New("code must not be empty") } request.ClientPublicKey = strings.TrimSpace(request.ClientPublicKey) if request.ClientPublicKey == "" { return errors.New("client_public_key must not be empty") } return nil } func validateConfirmEmailCodeResult(result *confirmemailcode.Result) error { result.DeviceSessionID = strings.TrimSpace(result.DeviceSessionID) if result.DeviceSessionID == "" { return errors.New("device_session_id must not be empty") } return nil } func projectSendEmailCodeError(err error) shared.PublicErrorProjection { if isTimeoutOrCanceled(err) { return unavailableProjection(err) } projection := shared.ProjectPublicError(err) if !shared.IsSendEmailCodePublicErrorCode(projection.Code) { return unavailableProjection(err) } return projection } func projectConfirmEmailCodeError(err error) shared.PublicErrorProjection { if isTimeoutOrCanceled(err) { return unavailableProjection(err) } projection := shared.ProjectPublicError(err) if !shared.IsConfirmEmailCodePublicErrorCode(projection.Code) { return unavailableProjection(err) } return projection } func unavailableProjection(err error) shared.PublicErrorProjection { return shared.ProjectPublicError(shared.ServiceUnavailable(err)) } func isTimeoutOrCanceled(err error) bool { return errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) }