New code verification service
Some checks failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
Some checks failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
This commit is contained in:
48
api/server/internal/server/confirmationimp/request.go
Normal file
48
api/server/internal/server/confirmationimp/request.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package confirmationimp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
emodel "github.com/tech/sendico/server/interface/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (a *ConfirmationAPI) requestCode(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc {
|
||||
var req confirmationRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
a.logger.Warn("Failed to decode confirmation request", zap.Error(err))
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
target, err := a.parseTarget(req.Target)
|
||||
if err != nil {
|
||||
return response.BadRequest(a.logger, a.Name(), "invalid_target", err.Error())
|
||||
}
|
||||
|
||||
if target == model.ConfirmationTargetLogin && (token == nil || !token.Pending) {
|
||||
return response.Forbidden(a.logger, a.Name(), "pending_token_required", "login confirmation requires pending token")
|
||||
}
|
||||
|
||||
destination := a.resolveDestination(req.Destination, account)
|
||||
if destination == "" {
|
||||
return response.BadRequest(a.logger, a.Name(), "missing_destination", "email destination is required")
|
||||
}
|
||||
code, rec, err := a.store.Create(r.Context(), account.ID, destination, target, a.config, a.generateCode)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to create confirmation code", zap.Error(err), mzap.ObjRef("account_ref", account.ID))
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
a.sendCode(account, target, destination, code)
|
||||
|
||||
return response.Accepted(a.logger, confirmationResponse{
|
||||
TTLSeconds: int(time.Until(rec.ExpiresAt).Seconds()),
|
||||
CooldownSeconds: int(a.config.Cooldown.Seconds()),
|
||||
Destination: maskEmail(destination),
|
||||
})
|
||||
}
|
||||
56
api/server/internal/server/confirmationimp/resend.go
Normal file
56
api/server/internal/server/confirmationimp/resend.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package confirmationimp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
emodel "github.com/tech/sendico/server/interface/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (a *ConfirmationAPI) resendCode(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc {
|
||||
var req confirmationRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
a.logger.Warn("Failed to decode confirmation resend request", zap.Error(err))
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
target, err := a.parseTarget(req.Target)
|
||||
if err != nil {
|
||||
return response.BadRequest(a.logger, a.Name(), "invalid_target", err.Error())
|
||||
}
|
||||
|
||||
if target == model.ConfirmationTargetLogin && (token == nil || !token.Pending) {
|
||||
return response.Forbidden(a.logger, a.Name(), "pending_token_required", "login confirmation requires pending token")
|
||||
}
|
||||
|
||||
destination := a.resolveDestination(req.Destination, account)
|
||||
if destination == "" {
|
||||
return response.BadRequest(a.logger, a.Name(), "missing_destination", "email destination is required")
|
||||
}
|
||||
code, rec, err := a.store.Resend(r.Context(), account.ID, destination, target, a.config, a.generateCode)
|
||||
switch {
|
||||
case errors.Is(err, errConfirmationNotFound):
|
||||
return response.NotFound(a.logger, a.Name(), "no_active_code_for_resend")
|
||||
case errors.Is(err, errConfirmationCooldown):
|
||||
return response.Forbidden(a.logger, a.Name(), "cooldown_active", "please wait before requesting another code")
|
||||
case errors.Is(err, errConfirmationResendLimit):
|
||||
return response.Forbidden(a.logger, a.Name(), "resend_limit_reached", "too many resend attempts")
|
||||
case err != nil:
|
||||
a.logger.Warn("Failed to resend confirmation code", zap.Error(err), mzap.ObjRef("account_ref", account.ID))
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
a.sendCode(account, target, destination, code)
|
||||
|
||||
return response.Accepted(a.logger, confirmationResponse{
|
||||
TTLSeconds: int(time.Until(rec.ExpiresAt).Seconds()),
|
||||
CooldownSeconds: int(a.config.Cooldown.Seconds()),
|
||||
Destination: maskEmail(destination),
|
||||
})
|
||||
}
|
||||
158
api/server/internal/server/confirmationimp/service.go
Normal file
158
api/server/internal/server/confirmationimp/service.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package confirmationimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
"github.com/tech/sendico/pkg/db/refreshtokens"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/messaging"
|
||||
cnotifications "github.com/tech/sendico/pkg/messaging/notifications/confirmation"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
eapi "github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
emodel "github.com/tech/sendico/server/interface/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
CodeLength int
|
||||
TTL time.Duration
|
||||
MaxAttempts int
|
||||
Cooldown time.Duration
|
||||
ResendLimit int
|
||||
}
|
||||
|
||||
func defaultConfig() Config {
|
||||
return Config{
|
||||
CodeLength: 6,
|
||||
TTL: 10 * time.Minute,
|
||||
MaxAttempts: 5,
|
||||
Cooldown: time.Minute,
|
||||
ResendLimit: 5,
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
return defaultConfig()
|
||||
}
|
||||
|
||||
type ConfirmationAPI struct {
|
||||
logger mlogger.Logger
|
||||
config Config
|
||||
store *ConfirmationStore
|
||||
rtdb refreshtokens.DB
|
||||
producer messaging.Producer
|
||||
tokenConfig middleware.TokenConfig
|
||||
signature middleware.Signature
|
||||
}
|
||||
|
||||
func (a *ConfirmationAPI) Name() mservice.Type {
|
||||
return mservice.Confirmations
|
||||
}
|
||||
|
||||
func (a *ConfirmationAPI) Finish(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateAPI(a eapi.API) (*ConfirmationAPI, error) {
|
||||
cdb, err := a.DBFactory().NewConfirmationsDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rtdb, err := a.DBFactory().NewRefreshTokensDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &ConfirmationAPI{
|
||||
logger: a.Logger().Named(mservice.Confirmations),
|
||||
config: defaultConfig(),
|
||||
store: NewStore(cdb),
|
||||
rtdb: rtdb,
|
||||
producer: a.Register().Messaging().Producer(),
|
||||
tokenConfig: a.Config().Mw.Token,
|
||||
signature: middleware.SignatureConf(a.Config().Mw),
|
||||
}
|
||||
|
||||
a.Register().PendingAccountHandler(p.Name(), "/confirmations", api.Post, p.requestCode)
|
||||
a.Register().PendingAccountHandler(p.Name(), "/confirmations/resend", api.Post, p.resendCode)
|
||||
a.Register().PendingAccountHandler(p.Name(), "/confirmations/verify", api.Post, p.verifyCode)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (a *ConfirmationAPI) generateCode() (string, error) {
|
||||
const digits = "0123456789"
|
||||
b := make([]byte, a.config.CodeLength)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i := range b {
|
||||
b[i] = digits[int(b[i])%len(digits)]
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (a *ConfirmationAPI) parseTarget(raw string) (model.ConfirmationTarget, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case string(model.ConfirmationTargetLogin):
|
||||
return model.ConfirmationTargetLogin, nil
|
||||
case string(model.ConfirmationTargetPayout):
|
||||
return model.ConfirmationTargetPayout, nil
|
||||
default:
|
||||
return "", merrors.InvalidArgument(fmt.Sprintf("unsupported target '%s'", raw), "target")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ConfirmationAPI) resolveDestination(reqDest string, account *model.Account) string {
|
||||
destination := strings.ToLower(strings.TrimSpace(reqDest))
|
||||
if destination == "" && account != nil {
|
||||
destination = strings.ToLower(strings.TrimSpace(account.Login))
|
||||
}
|
||||
return destination
|
||||
}
|
||||
|
||||
func (a *ConfirmationAPI) sendCode(account *model.Account, target model.ConfirmationTarget, destination, code string) {
|
||||
a.logger.Info("Confirmation code generated",
|
||||
zap.String("target", string(target)),
|
||||
zap.String("destination", maskEmail(destination)),
|
||||
mzap.ObjRef("account_ref", account.ID))
|
||||
if err := a.producer.SendMessage(cnotifications.Code(a.Name(), account.ID, destination, target, code)); err != nil {
|
||||
a.logger.Warn("Failed to send confirmation code notification", zap.Error(err), mzap.ObjRef("account_ref", account.ID))
|
||||
}
|
||||
a.logger.Debug("Confirmation code debug dump (do not log in production)", zap.String("code", code))
|
||||
}
|
||||
|
||||
func maskEmail(email string) string {
|
||||
parts := strings.Split(email, "@")
|
||||
if len(parts) != 2 {
|
||||
return email
|
||||
}
|
||||
local := parts[0]
|
||||
if len(local) > 2 {
|
||||
local = local[:1] + "***" + local[len(local)-1:]
|
||||
} else {
|
||||
local = local[:1] + "***"
|
||||
}
|
||||
return local + "@" + parts[1]
|
||||
}
|
||||
|
||||
func (a *ConfirmationAPI) createAccessToken(account *model.Account) (sresponse.TokenData, error) {
|
||||
ja := jwtauth.New(a.signature.Algorithm, a.signature.PrivateKey, a.signature.PublicKey)
|
||||
_, res, err := ja.Encode(emodel.Account2Claims(account, a.tokenConfig.Expiration.Account))
|
||||
token := sresponse.TokenData{
|
||||
Token: res,
|
||||
Expiration: time.Now().Add(time.Duration(a.tokenConfig.Expiration.Account) * time.Hour),
|
||||
}
|
||||
return token, err
|
||||
}
|
||||
181
api/server/internal/server/confirmationimp/store.go
Normal file
181
api/server/internal/server/confirmationimp/store.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package confirmationimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/confirmation"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
var (
|
||||
errConfirmationNotFound = errors.New("confirmation not found or expired")
|
||||
errConfirmationUsed = errors.New("confirmation already used")
|
||||
errConfirmationMismatch = errors.New("confirmation code mismatch")
|
||||
errConfirmationAttemptsExceeded = errors.New("confirmation attempts exceeded")
|
||||
errConfirmationCooldown = errors.New("confirmation cooldown active")
|
||||
errConfirmationResendLimit = errors.New("confirmation resend limit reached")
|
||||
)
|
||||
|
||||
type ConfirmationStore struct {
|
||||
db confirmation.DB
|
||||
}
|
||||
|
||||
func NewStore(db confirmation.DB) *ConfirmationStore {
|
||||
return &ConfirmationStore{db: db}
|
||||
}
|
||||
|
||||
func (s *ConfirmationStore) Create(
|
||||
ctx context.Context,
|
||||
accountRef primitive.ObjectID,
|
||||
destination string,
|
||||
target model.ConfirmationTarget,
|
||||
cfg Config,
|
||||
generator func() (string, error),
|
||||
) (string, *model.ConfirmationCode, error) {
|
||||
if err := s.db.DeleteTuple(ctx, accountRef, destination, target); err != nil && !errors.Is(err, merrors.ErrNoData) {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
code, _, rec, err := s.buildRecord(accountRef, destination, target, cfg, generator)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if err := s.db.Create(ctx, rec); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return code, rec, nil
|
||||
}
|
||||
|
||||
func (s *ConfirmationStore) Resend(
|
||||
ctx context.Context,
|
||||
accountRef primitive.ObjectID,
|
||||
destination string,
|
||||
target model.ConfirmationTarget,
|
||||
cfg Config,
|
||||
generator func() (string, error),
|
||||
) (string, *model.ConfirmationCode, error) {
|
||||
now := time.Now().UTC()
|
||||
active, err := s.db.FindActive(ctx, accountRef, destination, target, now.Unix())
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
return s.Create(ctx, accountRef, destination, target, cfg, generator)
|
||||
}
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if active.ResendCount >= active.ResendLimit {
|
||||
return "", nil, errConfirmationResendLimit
|
||||
}
|
||||
if now.Before(active.CooldownUntil) {
|
||||
return "", nil, errConfirmationCooldown
|
||||
}
|
||||
|
||||
code, salt, updated, err := s.buildRecord(accountRef, destination, target, cfg, generator)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
// Preserve attempt counters but bump resend count.
|
||||
updated.ID = active.ID
|
||||
updated.CreatedAt = active.CreatedAt
|
||||
updated.Attempts = active.Attempts
|
||||
updated.ResendCount = active.ResendCount + 1
|
||||
updated.Salt = salt
|
||||
|
||||
if err := s.db.Update(ctx, updated); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return code, updated, nil
|
||||
}
|
||||
|
||||
func (s *ConfirmationStore) Verify(
|
||||
ctx context.Context,
|
||||
accountRef primitive.ObjectID,
|
||||
destination string,
|
||||
target model.ConfirmationTarget,
|
||||
code string,
|
||||
) error {
|
||||
now := time.Now().UTC()
|
||||
rec, err := s.db.FindActive(ctx, accountRef, destination, target, now.Unix())
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
return errConfirmationNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rec.Used {
|
||||
return errConfirmationUsed
|
||||
}
|
||||
|
||||
rec.Attempts++
|
||||
if rec.Attempts > rec.MaxAttempts {
|
||||
rec.Used = true
|
||||
_ = s.db.Update(ctx, rec)
|
||||
return errConfirmationAttemptsExceeded
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare(rec.CodeHash, hashCode(rec.Salt, code)) != 1 {
|
||||
_ = s.db.Update(ctx, rec)
|
||||
return errConfirmationMismatch
|
||||
}
|
||||
|
||||
rec.Used = true
|
||||
return s.db.Update(ctx, rec)
|
||||
}
|
||||
|
||||
func (s *ConfirmationStore) buildRecord(
|
||||
accountRef primitive.ObjectID,
|
||||
destination string,
|
||||
target model.ConfirmationTarget,
|
||||
cfg Config,
|
||||
generator func() (string, error),
|
||||
) (string, []byte, *model.ConfirmationCode, error) {
|
||||
code, err := generator()
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
salt, err := newSalt()
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
rec := model.NewConfirmationCode(accountRef)
|
||||
rec.Destination = destination
|
||||
rec.Target = target
|
||||
rec.CodeHash = hashCode(salt, code)
|
||||
rec.Salt = salt
|
||||
rec.ExpiresAt = now.Add(cfg.TTL)
|
||||
rec.MaxAttempts = cfg.MaxAttempts
|
||||
rec.ResendLimit = cfg.ResendLimit
|
||||
rec.CooldownUntil = now.Add(cfg.Cooldown)
|
||||
rec.Used = false
|
||||
rec.Attempts = 0
|
||||
rec.ResendCount = 0
|
||||
rec.CreatedAt = now
|
||||
rec.UpdatedAt = now
|
||||
|
||||
return code, salt, rec, nil
|
||||
}
|
||||
|
||||
func hashCode(salt []byte, code string) []byte {
|
||||
h := sha256.New()
|
||||
h.Write(salt)
|
||||
h.Write([]byte(code))
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func newSalt() ([]byte, error) {
|
||||
buf := make([]byte, 16)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
23
api/server/internal/server/confirmationimp/types.go
Normal file
23
api/server/internal/server/confirmationimp/types.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package confirmationimp
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type confirmationRequest struct {
|
||||
Target string `json:"target"`
|
||||
Destination string `json:"destination,omitempty"`
|
||||
}
|
||||
|
||||
type confirmationVerifyRequest struct {
|
||||
Target string `json:"target"`
|
||||
Code string `json:"code"`
|
||||
Destination string `json:"destination,omitempty"`
|
||||
SessionIdentifier model.SessionIdentifier `json:"sessionIdentifier"`
|
||||
}
|
||||
|
||||
type confirmationResponse struct {
|
||||
TTLSeconds int `json:"ttl_seconds"`
|
||||
CooldownSeconds int `json:"cooldown_seconds"`
|
||||
Destination string `json:"destination"`
|
||||
}
|
||||
88
api/server/internal/server/confirmationimp/verify.go
Normal file
88
api/server/internal/server/confirmationimp/verify.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package confirmationimp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
emodel "github.com/tech/sendico/server/interface/model"
|
||||
rtokens "github.com/tech/sendico/server/internal/api/routers/tokens"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (a *ConfirmationAPI) verifyCode(r *http.Request, account *model.Account, token *emodel.AccountToken) http.HandlerFunc {
|
||||
var req confirmationVerifyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
a.logger.Warn("Failed to decode confirmation verification request", zap.Error(err))
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
target, err := a.parseTarget(req.Target)
|
||||
if err != nil {
|
||||
return response.BadRequest(a.logger, a.Name(), "invalid_target", err.Error())
|
||||
}
|
||||
|
||||
if target == model.ConfirmationTargetLogin && (token == nil || !token.Pending) {
|
||||
return response.Forbidden(a.logger, a.Name(), "pending_token_required", "login confirmation requires pending token")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.Code) == "" {
|
||||
return response.BadRequest(a.logger, a.Name(), "missing_code", "confirmation code is required")
|
||||
}
|
||||
|
||||
destination := a.resolveDestination(req.Destination, account)
|
||||
if destination == "" {
|
||||
return response.BadRequest(a.logger, a.Name(), "missing_destination", "email destination is required")
|
||||
}
|
||||
err = a.store.Verify(r.Context(), account.ID, destination, target, strings.TrimSpace(req.Code))
|
||||
switch {
|
||||
case errors.Is(err, errConfirmationNotFound):
|
||||
return response.NotFound(a.logger, a.Name(), "code_not_found_or_expired")
|
||||
case errors.Is(err, errConfirmationUsed):
|
||||
return response.Forbidden(a.logger, a.Name(), "code_used", "code has already been used")
|
||||
case errors.Is(err, errConfirmationAttemptsExceeded):
|
||||
return response.Forbidden(a.logger, a.Name(), "attempt_limit_reached", "too many failed attempts")
|
||||
case errors.Is(err, errConfirmationMismatch):
|
||||
return response.Forbidden(a.logger, a.Name(), "invalid_code", "code does not match")
|
||||
case err != nil:
|
||||
a.logger.Warn("Failed to verify confirmation code", zap.Error(err), mzap.ObjRef("account_ref", account.ID))
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
a.logger.Info("Confirmation code verified", zap.String("target", string(target)), mzap.ObjRef("account_ref", account.ID))
|
||||
if target == model.ConfirmationTargetLogin {
|
||||
if req.SessionIdentifier.ClientID == "" || req.SessionIdentifier.DeviceID == "" {
|
||||
return response.BadRequest(a.logger, a.Name(), "missing_session", "session identifier is required")
|
||||
}
|
||||
accessToken, err := a.createAccessToken(account)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to generate access token", zap.Error(err))
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
}
|
||||
refreshToken, err := rtokens.PrepareRefreshToken(
|
||||
r.Context(),
|
||||
r,
|
||||
a.rtdb,
|
||||
a.tokenConfig.Length,
|
||||
a.tokenConfig.Expiration.Refresh,
|
||||
&req.SessionIdentifier,
|
||||
account,
|
||||
a.logger,
|
||||
)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to generate refresh token", zap.Error(err))
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
}
|
||||
rt := sresponse.TokenData{
|
||||
Token: refreshToken.RefreshToken,
|
||||
Expiration: refreshToken.ExpiresAt,
|
||||
}
|
||||
return sresponse.Login(a.logger, account, &accessToken, &rt)
|
||||
}
|
||||
return response.Success(a.logger)
|
||||
}
|
||||
Reference in New Issue
Block a user