0904
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
// 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 (
|
||||
"path"
|
||||
|
||||
"github.com/flamego/recaptcha"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"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/shared"
|
||||
)
|
||||
|
||||
func Login(ctx context.Context) {
|
||||
ctx.Success("auth/login")
|
||||
}
|
||||
|
||||
func LoginAction(ctx context.Context, f form.Login, recaptcha recaptcha.RecaptchaV3) {
|
||||
if ctx.HasError() {
|
||||
ctx.Success("auth/login")
|
||||
return
|
||||
}
|
||||
|
||||
// Check captcha code (unified: recaptcha, turnstile, or math).
|
||||
uri := ctx.Request().Request.RequestURI // Keep the query when redirecting.
|
||||
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(uri)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "captcha.error"))
|
||||
ctx.Redirect(uri)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := db.Users.Authenticate(ctx.Request().Context(), f.Email, f.Password)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrBadCredential) {
|
||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "auth.errBadCredential"))
|
||||
} else {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to authenticate user")
|
||||
ctx.SetInternalErrorFlash()
|
||||
}
|
||||
ctx.Redirect(uri)
|
||||
return
|
||||
}
|
||||
|
||||
to := ctx.Query("to")
|
||||
to = path.Clean("/" + to)
|
||||
if to == "" {
|
||||
to = "/_/" + user.Domain
|
||||
}
|
||||
|
||||
// 会话固定防护:登录认证成功后、写入用户标识之前,重新生成会话 ID,
|
||||
// 使登录前种下的匿名会话失效,防止攻击者借固定会话 ID 劫持已认证会话。
|
||||
if err := ctx.Session.RegenerateID(ctx.ResponseWriter(), ctx.Request().Request); err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to regenerate session ID on login")
|
||||
ctx.SetInternalErrorFlash()
|
||||
ctx.Redirect(uri)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Session.Set("uid", user.ID)
|
||||
ctx.Session.Set("pwd_ver", user.PasswordVersion)
|
||||
ctx.Redirect(to)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// 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 (
|
||||
"github.com/tamakyi/TamaBox/internal/context"
|
||||
)
|
||||
|
||||
func Logout(ctx context.Context) {
|
||||
ctx.Session.Flush()
|
||||
ctx.Redirect("/")
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// 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")
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// 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 (
|
||||
"github.com/flamego/recaptcha"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/tamakyi/TamaBox/internal/captcha"
|
||||
"github.com/tamakyi/TamaBox/internal/conf"
|
||||
"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/shared"
|
||||
)
|
||||
|
||||
func Register(ctx context.Context) {
|
||||
ctx.Success("auth/register")
|
||||
}
|
||||
|
||||
func RegisterAction(ctx context.Context, f form.Register, recaptcha recaptcha.RecaptchaV3) {
|
||||
if ctx.HasError() {
|
||||
ctx.Success("auth/register")
|
||||
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("/register")
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "captcha.error"))
|
||||
ctx.Redirect("/register")
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Users.Create(ctx.Request().Context(), db.CreateUserOptions{
|
||||
Name: f.Name,
|
||||
Password: f.Password,
|
||||
Email: f.Email,
|
||||
Avatar: conf.Upload.DefaultAvatarURL,
|
||||
Domain: f.Domain,
|
||||
Background: conf.Upload.DefaultBackground,
|
||||
Intro: "问你想问的",
|
||||
Qrcodebackcolor: "#FDF8F0",
|
||||
Qrcodecolor: "#C45500",
|
||||
Usernamecolor: "#000000",
|
||||
Introcolor: "#000000",
|
||||
Dotscale: "0.35",
|
||||
BackgroundImage: conf.Upload.DefaultBackgroundImageURL,
|
||||
Backgroundimagealpha: "0.8",
|
||||
Qrcodepdpcolor: "#C45500",
|
||||
Acfunlink: "none",
|
||||
Bililink: "none",
|
||||
Weibolink: "none",
|
||||
Bloglink: "box.shiroko.one",
|
||||
Xiaohongshulink: "none",
|
||||
Language: i18n.Resolve(f.Language), // 注册时选择的界面语言;留空回退站点默认
|
||||
}); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, db.ErrDuplicateEmail):
|
||||
ctx.SetError(errors.New(i18n.T(ctx.Lang(), "auth.errDuplicateEmail")))
|
||||
case errors.Is(err, db.ErrDuplicateDomain):
|
||||
ctx.SetError(errors.New(i18n.T(ctx.Lang(), "auth.errDuplicateDomain")))
|
||||
case errors.Is(err, db.ErrBadCredential):
|
||||
ctx.SetError(errors.New(i18n.T(ctx.Lang(), "auth.errBadCredential")))
|
||||
|
||||
default:
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to create new user")
|
||||
ctx.SetInternalError()
|
||||
}
|
||||
|
||||
ctx.Success("auth/register")
|
||||
return
|
||||
}
|
||||
|
||||
// 成功提示跟随用户注册时选择的语言
|
||||
lang := "zh-CN"
|
||||
if l, ok := ctx.Data["Lang"].(string); ok && l != "" {
|
||||
lang = l
|
||||
}
|
||||
ctx.SetSuccessFlash(i18n.T(lang, "auth.register.success"))
|
||||
ctx.Redirect("/login")
|
||||
}
|
||||
Reference in New Issue
Block a user