feat: authsession service
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user