move api/server to api/edge/bff
This commit is contained in:
196
api/edge/bff/internal/server/accountapiimp/password.go
Normal file
196
api/edge/bff/internal/server/accountapiimp/password.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package accountapiimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (a *AccountAPI) checkPassword(_ *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
|
||||
return sresponse.Account(a.logger, account, accessToken)
|
||||
}
|
||||
|
||||
func (a *AccountAPI) changePassword(r *http.Request, user *model.Account, token *sresponse.TokenData) http.HandlerFunc {
|
||||
// TODO: add rate check
|
||||
var pcr srequest.ChangePassword
|
||||
if err := json.NewDecoder(r.Body).Decode(&pcr); err != nil {
|
||||
a.logger.Warn("Failed to decode password change request", zap.Error(err), mzap.StorableRef(user))
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
if err := a.accService.ValidatePassword(pcr.New, &pcr.Old); err != nil {
|
||||
a.logger.Debug("Password validation failed", zap.Error(err), mzap.StorableRef(user))
|
||||
return sresponse.BadRPassword(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
if !user.MatchPassword(pcr.Old) {
|
||||
a.logger.Info("Old password does not match", mzap.StorableRef(user))
|
||||
return a.reportUnauthorized("old password does not match")
|
||||
}
|
||||
user.Password = pcr.New
|
||||
if err := user.HashPassword(); err != nil {
|
||||
a.logger.Warn("Failed to hash new password", zap.Error(err), mzap.StorableRef(user))
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
}
|
||||
if err := a.db.Update(ctx, user); err != nil {
|
||||
a.logger.Warn("Failed to save account", zap.Error(err), mzap.StorableRef(user))
|
||||
return response.Internal(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
if err := a.rtdb.RevokeAll(ctx, *user.GetID(), pcr.DeviceID); err != nil {
|
||||
a.logger.Warn("Failed to revoke refresh tokens",
|
||||
zap.Error(err), mzap.StorableRef(user), zap.String("device_id", pcr.DeviceID))
|
||||
}
|
||||
|
||||
return sresponse.Account(a.logger, user, token)
|
||||
}
|
||||
|
||||
func (a *AccountAPI) forgotPassword(r *http.Request) http.HandlerFunc {
|
||||
var req srequest.ForgotPassword
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
a.logger.Warn("Failed to decode password change request", zap.Error(err))
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
if req.Login == "" {
|
||||
a.logger.Debug("Email is missing in the request")
|
||||
return a.reportEmailMissing()
|
||||
}
|
||||
|
||||
// Always use the lower case email address
|
||||
req.Login = strings.ToLower(req.Login)
|
||||
|
||||
// Get user
|
||||
ctx := r.Context()
|
||||
user, err := a.db.GetByEmail(ctx, req.Login)
|
||||
if (errors.Is(err, merrors.ErrNoData)) || (user == nil) {
|
||||
a.logger.Debug("User not found while recovering password", zap.Error(err), zap.String("email", req.Login))
|
||||
return a.reportNoEmailRegistered()
|
||||
}
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to fetch user", zap.Error(err), zap.String("email", req.Login))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
// Generate reset password token
|
||||
verificationToken, err := a.accService.ResetPassword(ctx, user)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to generate reset password token", zap.Error(err), mzap.StorableRef(user))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
// Send reset password email
|
||||
if err = a.sendPasswordResetEmail(user, verificationToken); err != nil {
|
||||
a.logger.Warn("Failed to send reset password email", zap.Error(err), mzap.StorableRef(user))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
a.logger.Info("Reset password email sent successfully", zap.String("email", user.Login))
|
||||
return response.Success(a.logger)
|
||||
}
|
||||
|
||||
func (a *AccountAPI) resetPassword(r *http.Request) http.HandlerFunc {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get account reference and token from URL parameters using parameter helpers
|
||||
accountRef, err := a.aph.GetRef(r)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to get account reference", zap.Error(err), mutil.PLog(a.aph, r))
|
||||
return response.BadReference(a.logger, a.Name(), a.aph.Name(), a.aph.GetID(r), err)
|
||||
}
|
||||
|
||||
token := a.tph.GetID(r)
|
||||
if token == "" {
|
||||
a.logger.Warn("Missing token in reset password request")
|
||||
return response.BadRequest(a.logger, a.Name(), "missing_parameters", "token is required")
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
var user model.Account
|
||||
err = a.db.Get(ctx, accountRef, &user)
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
a.logger.Info("User not found for password reset", zap.String("account_ref", accountRef.Hex()))
|
||||
return response.NotFound(a.logger, a.Name(), "User not found")
|
||||
}
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to get user for password reset", zap.Error(err), zap.String("account_ref", accountRef.Hex()))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
t, err := a.vdb.Consume(ctx, accountRef, model.PurposePasswordReset, token)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to consume password reset token", zap.Error(err), zap.String("token", token))
|
||||
return a.mapTokenErrorToResponse(err)
|
||||
}
|
||||
|
||||
if t.Purpose != model.PurposePasswordReset {
|
||||
a.logger.Warn("Invalid token purpose for password reset", zap.String("expected", string(model.PurposePasswordReset)), zap.String("actual", string(t.Purpose)))
|
||||
return response.DataConflict(a.logger, a.Name(), "Invalid token purpose")
|
||||
}
|
||||
|
||||
if t.AccountRef != accountRef {
|
||||
a.logger.Warn("Token account reference does not match request account reference", zap.String("token_account_ref", t.AccountRef.Hex()), zap.String("request_account_ref", accountRef.Hex()))
|
||||
return response.DataConflict(a.logger, a.Name(), "Token does not match account")
|
||||
}
|
||||
|
||||
// Parse new password from request body
|
||||
var req srequest.ResetPassword
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
a.logger.Warn("Failed to decode reset password request", zap.Error(err))
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
// Validate new password
|
||||
if err := a.accService.ValidatePassword(req.Password, nil); err != nil {
|
||||
a.logger.Debug("Password validation failed", zap.Error(err), mzap.StorableRef(&user))
|
||||
return sresponse.BadRPassword(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
// Execute password reset in transaction to ensure atomicity
|
||||
if _, err := a.tf.CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) {
|
||||
return a.resetPasswordTransactionBody(ctx, &user, req.Password)
|
||||
}); err != nil {
|
||||
a.logger.Warn("Failed to execute password reset transaction", zap.Error(err), mzap.StorableRef(&user))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
a.logger.Info("Password reset successful", mzap.StorableRef(&user))
|
||||
return response.Success(a.logger)
|
||||
}
|
||||
|
||||
// resetPasswordTransactionBody contains the transaction logic for password reset
|
||||
func (a *AccountAPI) resetPasswordTransactionBody(ctx context.Context, user *model.Account, newPassword string) (any, error) {
|
||||
// Update user with new password and clear reset token
|
||||
user.Password = newPassword
|
||||
|
||||
// Hash the new password
|
||||
if err := user.HashPassword(); err != nil {
|
||||
a.logger.Warn("Failed to hash new password", zap.Error(err), mzap.StorableRef(user))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the updated user
|
||||
if err := a.db.Update(ctx, user); err != nil {
|
||||
a.logger.Warn("Failed to save user with new password", zap.Error(err), mzap.StorableRef(user))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Revoke all refresh tokens for this user (force re-login)
|
||||
if err := a.rtdb.RevokeAll(ctx, user.ID, ""); err != nil {
|
||||
a.logger.Warn("Failed to revoke refresh tokens after password reset", zap.Error(err), mzap.StorableRef(user))
|
||||
// Don't fail the transaction if token revocation fails, but log it
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
Reference in New Issue
Block a user