This commit is contained in:
tamakyi
2026-09-08 10:10:44 +08:00
commit 2ed27dd55e
288 changed files with 67061 additions and 0 deletions
+77
View File
@@ -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)
}
+14
View File
@@ -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("/")
}
+172
View File
@@ -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")
}
+92
View File
@@ -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")
}