package client_recovery import ( "encoding/base64" "errors" "gitea.pena/PenaSide/codeword/internal/models" "gitea.pena/PenaSide/codeword/internal/repository" "gitea.pena/PenaSide/codeword/internal/services" "github.com/gofiber/fiber/v2" "go.uber.org/zap" "gitea.pena/PenaSide/common/log_mw" "time" "strings" ) type Deps struct { Logger *zap.Logger Service *services.RecoveryService DefaultURL string RecoveryURL string } type RecoveryController struct { logger *zap.Logger service *services.RecoveryService defaultURL string recoveryURL string } func NewRecoveryController(deps Deps) *RecoveryController { return &RecoveryController{ logger: deps.Logger, service: deps.Service, defaultURL: deps.DefaultURL, recoveryURL: deps.RecoveryURL, } } func (r *RecoveryController) HandleRecoveryRequest(c *fiber.Ctx) error { hlogger := log_mw.ExtractLogger(c) var req models.RecoveryRequest if err := c.BodyParser(&req); err != nil { r.logger.Error("Failed to parse recovery request", zap.Error(err)) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Bad Request"}) } if req.Email == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "email is required"}) } referralURL := c.Get("Referrer") if req.RedirectionURL == "" && referralURL != "" { req.RedirectionURL = referralURL } else if req.RedirectionURL == "" { req.RedirectionURL = r.defaultURL } user, err := r.service.FindUserByEmail(c.Context(), req.Email) if err != nil || user == nil { r.logger.Error("Failed to find user by email", zap.Error(err)) return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"}) } key, err := r.service.GenerateKey() if err != nil { r.logger.Error("Failed to generate key", zap.Error(err)) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"}) } signUrl := referralURL + req.RedirectionURL sign := base64.URLEncoding.EncodeToString(key) id, err := r.service.StoreRecoveryRecord(c.Context(), models.StoreRecDeps{ UserID: user.ID.Hex(), Email: user.Email, Key: sign, Url: signUrl, }) if err != nil { r.logger.Error("Failed to store recovery record", zap.Error(err)) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"}) } signWithID := sign + id // подпись с id записи err = r.service.RecoveryEmailTask(c.Context(), models.RecEmailDeps{UserID: user.ID.Hex(), Email: req.Email, SignWithID: strings.Replace(signUrl, "/changepwd","",1) + "/"+signWithID, ID: id}) if err != nil { r.logger.Error("Failed to send recovery email", zap.Error(err)) if errors.Is(err, repository.ErrAlreadyReported) { return c.Status(fiber.StatusAlreadyReported).JSON(fiber.Map{"error": "already reported"}) } return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"}) } hlogger.Emit(models.InfoPasswordRestorationRequested{ CtxID: id, CtxUserID: user.ID.Hex(), CtxReturnURL: r.recoveryURL + signWithID, CtxEmail: req.Email, }) return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Recovery email sent successfully"}) } func (r *RecoveryController) HandleRecoveryLink(c *fiber.Ctx) error { hlogger := log_mw.ExtractLogger(c) sign := c.Params("sign") record, err := r.service.GetRecoveryRecord(c.Context(), sign) if err != nil { r.logger.Error("Recovery link expired", zap.String("signature", sign)) return c.Redirect("https://hub.pena.digital/recover/expired") } if time.Since(record.CreatedAt) > 15*time.Minute { r.logger.Error("Recovery link expired", zap.String("signature", sign)) return c.Redirect(record.SignUrl + "/expired") } tokens, err := r.service.ExchangeForTokens(record.UserID, record.Sign) if err != nil { r.logger.Error("Failed to exchange recovery link for tokens", zap.Error(err)) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"}) } c.Cookie(&fiber.Cookie{ Name: "refreshToken", Value: tokens["refreshToken"], Domain: ".pena.digital", Expires: time.Now().Add(30 * 24 * time.Hour), Secure: true, HTTPOnly: true, }) c.Cookie(&fiber.Cookie{ Name: "refreshToken", Value: tokens["refreshToken"], Domain: ".pena.digital", Expires: time.Now().Add(30 * 24 * time.Hour), Secure: true, HTTPOnly: true, }) hlogger.Emit(models.InfoPasswordRestored{ CtxID: record.ID.String(), CtxUserID: record.UserID, }) return c.Redirect(record.SignUrl + "?auth=" + tokens["accessToken"]) }