// 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 { // 防枚举:邮箱是否已注册,对外必须与"注册成功"不可区分。 // // 注册接口原本会直接回"该邮箱已被注册",而找回密码接口(ForgotPasswordAction) // 早就统一成"已发送"。两处口径不一致的结果是:任何人都能批量确认 // "某个邮箱是不是这个匿名提问箱的用户"—— 这是成员关系泄露,比一般站更敏感。 // 真实原因只写服务端日志(运营排查要看得到到底撞的是哪一类)。 case errors.Is(err, db.ErrDuplicateEmail): logrus.WithContext(ctx.Request().Context()). WithField("email", f.Email). Warn("注册命中已存在的邮箱,按成功流程静默处理(防枚举)") registerSucceeded(ctx) return case errors.Is(err, db.ErrDeactivatedEmail): logrus.WithContext(ctx.Request().Context()). WithField("email", f.Email). Warn("注册命中已停用账号的邮箱,按成功流程静默处理(防枚举)") registerSucceeded(ctx) return // 域名占用不遮蔽:域名是公开命名空间,/_/ 本来就可探测, // 遮蔽只会让用户以为自己注册成功、后续登不上又找不到原因。 case errors.Is(err, db.ErrDuplicateDomain): ctx.SetError(errors.New(i18n.T(ctx.Lang(), "auth.errDuplicateDomain"))) case errors.Is(err, db.ErrDeactivatedDomain): ctx.SetError(errors.New(i18n.T(ctx.Lang(), "auth.errDeactivatedDomain"))) 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 } registerSucceeded(ctx) } // registerSucceeded 是注册接口唯一的"成功"出口。 // // 抽成函数是为了让"真的建成功了"和"因为邮箱已存在而没建"走同一条路径 —— // 一旦两处分别写,响应体的措辞、跳转目标、flash 文案迟早会漂移出差异, // 差异就是枚举信号。新增失败分支时若想遮蔽,直接调用本函数即可。 func registerSucceeded(ctx context.Context) { // 成功提示跟随用户注册时选择的语言 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") }