197 lines
7.3 KiB
Go
197 lines
7.3 KiB
Go
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
|
|
if err := a.accService.ResetPassword(ctx, user); 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, user.ResetPasswordToken); 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)
|
|
}
|
|
|
|
// Validate reset token
|
|
if user.ResetPasswordToken == "" {
|
|
a.logger.Debug("No reset token found for user", mzap.StorableRef(&user))
|
|
return response.BadRequest(a.logger, a.Name(), "no_reset_token", "No password reset token found for this user")
|
|
}
|
|
|
|
if user.ResetPasswordToken != token {
|
|
a.logger.Debug("Reset token mismatch", mzap.StorableRef(&user))
|
|
return response.BadRequest(a.logger, a.Name(), "invalid_token", "Invalid or expired reset token")
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
if req.Password == "" {
|
|
a.logger.Debug("New password is empty")
|
|
return response.BadRequest(a.logger, a.Name(), "empty_password", "New password cannot be empty")
|
|
}
|
|
|
|
// 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
|
|
user.ResetPasswordToken = "" // Clear the token after use
|
|
|
|
// 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
|
|
}
|