// Copyright 2022 E99p1ant. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package auth import ( "os" "time" "github.com/flamego/cache" "github.com/flamego/recaptcha" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/thanhpk/randstr" "github.com/tamakyi/TamaBox/internal/captcha" "github.com/tamakyi/TamaBox/internal/context" "github.com/tamakyi/TamaBox/internal/db" "github.com/tamakyi/TamaBox/internal/form" "github.com/tamakyi/TamaBox/internal/i18n" "github.com/tamakyi/TamaBox/internal/mail" "github.com/tamakyi/TamaBox/internal/shared" ) func ForgotPassword(ctx context.Context) { ctx.Success("auth/forgot-password") } func ForgotPasswordAction(ctx context.Context, f form.ForgotPassword, cache cache.Cache, recaptcha recaptcha.RecaptchaV3) { if ctx.HasError() { ctx.Success("auth/forgot-password") return } // Check captcha code (unified: recaptcha, turnstile, or math). ok, err := captcha.Verify(f.Recaptcha, shared.ResolveClientIP(ctx.Request().Request), ctx.Session, recaptcha, ctx.Lang()) if err != nil { logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to check captcha") ctx.SetErrorFlash(err.Error()) ctx.Redirect("/forgot-password") return } if !ok { ctx.SetErrorFlash(i18n.T(ctx.Lang(), "captcha.error")) ctx.Redirect("/forgot-password") return } user, err := db.Users.GetByEmail(ctx.Request().Context(), f.Email) if err != nil { if errors.Is(err, db.ErrUserNotExists) { // 防邮箱枚举:不区分邮箱是否存在,统一渲染"已发送"提示页(不实际发信)。 ctx.Data["email"] = f.Email ctx.Success("auth/forgot-password-sent") return } logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to get user by email") ctx.SetInternalErrorFlash() ctx.Redirect("/forgot-password") return } emailSentCacheKey := "forgot-password-email-sent:" + user.Email _, err = cache.Get(ctx.Request().Context(), emailSentCacheKey) if err != nil { if !errors.Is(err, os.ErrNotExist) { logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to read password recovery email sent cache") } } else { ctx.SetErrorFlash(i18n.T(ctx.Lang(), "auth.errEmailFrequent")) ctx.Redirect("/forgot-password") return } code := randstr.String(64) recoveryCodeCacheKey := "forgot-password-recovery-code:" + code if err := cache.Set(ctx.Request().Context(), recoveryCodeCacheKey, user.ID, 24*time.Hour); err != nil { logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to set password recovery code cache") ctx.SetInternalErrorFlash() ctx.Redirect("/forgot-password") return } if err := mail.SendPasswordRecoveryMail(user.Email, code); err != nil { logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to send password recovery mail") ctx.SetErrorFlash(i18n.T(ctx.Lang(), "auth.errEmailFailed")) ctx.Redirect("/forgot-password") return } if err := cache.Set(ctx.Request().Context(), emailSentCacheKey, time.Now(), 2*time.Minute); err != nil { logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to set password recovery email cache") } ctx.Data["email"] = user.Email ctx.Success("auth/forgot-password-sent") } func checkRecoverPasswordCode(ctx context.Context, cache cache.Cache) (*db.User, bool) { code := ctx.Query("code") recoveryCodeCacheKey := "forgot-password-recovery-code:" + code userIDItf, err := cache.Get(ctx.Request().Context(), recoveryCodeCacheKey) if err != nil { if errors.Is(err, os.ErrNotExist) { ctx.SetErrorFlash(i18n.T(ctx.Lang(), "auth.errCodeExpired")) } else { logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to read password recovery code cache") ctx.SetInternalErrorFlash() } ctx.Redirect("/login") return nil, false } userID, ok := userIDItf.(uint) if !ok { logrus.WithContext(ctx.Request().Context()).WithField("user_id_itf", userIDItf).Error("Failed to convert user id interface to uint") ctx.SetInternalErrorFlash() ctx.Redirect("/login") return nil, false } user, err := db.Users.GetByID(ctx.Request().Context(), userID) if err != nil { ctx.SetErrorFlash(i18n.T(ctx.Lang(), "auth.errUserNotFound")) ctx.Redirect("/login") return nil, false } return user, true } func RecoverPassword(ctx context.Context, cache cache.Cache) { user, ok := checkRecoverPasswordCode(ctx, cache) if !ok { return } ctx.Data["User"] = user ctx.Success("auth/password-recovery") } func RecoverPasswordAction(ctx context.Context, cache cache.Cache, f form.RecoverPassword) { user, ok := checkRecoverPasswordCode(ctx, cache) if !ok { return } if ctx.HasError() { ctx.Data["User"] = user ctx.Success("auth/password-recovery") return } code := ctx.Query("code") recoveryCodeCacheKey := "forgot-password-recovery-code:" + code if err := cache.Delete(ctx.Request().Context(), recoveryCodeCacheKey); err != nil { logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to delete password recovery code cache") ctx.SetInternalErrorFlash() ctx.Redirect("/login") return } if err := db.Users.UpdatePassword(ctx.Request().Context(), user.ID, f.NewPassword); err != nil { logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to update user password") ctx.SetInternalErrorFlash() ctx.Refresh() return } ctx.SetSuccessFlash(i18n.T(ctx.Lang(), "auth.errPasswordChanged")) ctx.Redirect("/login") }