auth_service.go 71 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962
  1. package service
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "crypto/sha256"
  6. "encoding/binary"
  7. "encoding/hex"
  8. "errors"
  9. "fmt"
  10. "net/mail"
  11. "strconv"
  12. "strings"
  13. "time"
  14. dbent "github.com/Wei-Shaw/sub2api/ent"
  15. "github.com/Wei-Shaw/sub2api/ent/authidentity"
  16. "github.com/Wei-Shaw/sub2api/internal/config"
  17. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  18. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  19. "github.com/golang-jwt/jwt/v5"
  20. "golang.org/x/crypto/bcrypt"
  21. )
  22. var (
  23. ErrInvalidCredentials = infraerrors.Unauthorized("INVALID_CREDENTIALS", "invalid email or password")
  24. ErrUserNotActive = infraerrors.Forbidden("USER_NOT_ACTIVE", "user is not active")
  25. ErrEmailExists = infraerrors.Conflict("EMAIL_EXISTS", "email already exists")
  26. ErrEmailReserved = infraerrors.BadRequest("EMAIL_RESERVED", "email is reserved")
  27. ErrInvalidToken = infraerrors.Unauthorized("INVALID_TOKEN", "invalid token")
  28. ErrTokenExpired = infraerrors.Unauthorized("TOKEN_EXPIRED", "token has expired")
  29. ErrAccessTokenExpired = infraerrors.Unauthorized("ACCESS_TOKEN_EXPIRED", "access token has expired")
  30. ErrTokenTooLarge = infraerrors.BadRequest("TOKEN_TOO_LARGE", "token too large")
  31. ErrTokenRevoked = infraerrors.Unauthorized("TOKEN_REVOKED", "token has been revoked")
  32. ErrRefreshTokenInvalid = infraerrors.Unauthorized("REFRESH_TOKEN_INVALID", "invalid refresh token")
  33. ErrRefreshTokenExpired = infraerrors.Unauthorized("REFRESH_TOKEN_EXPIRED", "refresh token has expired")
  34. ErrRefreshTokenReused = infraerrors.Unauthorized("REFRESH_TOKEN_REUSED", "refresh token has been reused")
  35. ErrEmailVerifyRequired = infraerrors.BadRequest("EMAIL_VERIFY_REQUIRED", "email verification is required")
  36. ErrEmailSuffixNotAllowed = infraerrors.BadRequest("EMAIL_SUFFIX_NOT_ALLOWED", "email suffix is not allowed")
  37. ErrEmailDomainRegistrationLimit = infraerrors.BadRequest(
  38. "EMAIL_DOMAIN_REGISTRATION_LIMIT",
  39. "this email domain cannot register another account; use a mainstream email or contact support to add the enterprise domain",
  40. )
  41. ErrRegDisabled = infraerrors.Forbidden("REGISTRATION_DISABLED", "registration is currently disabled")
  42. ErrServiceUnavailable = infraerrors.ServiceUnavailable("SERVICE_UNAVAILABLE", "service temporarily unavailable")
  43. ErrInvitationCodeRequired = infraerrors.BadRequest("INVITATION_CODE_REQUIRED", "invitation code is required")
  44. ErrInvitationCodeInvalid = infraerrors.BadRequest("INVITATION_CODE_INVALID", "invalid or used invitation code")
  45. ErrOAuthInvitationRequired = infraerrors.Forbidden("OAUTH_INVITATION_REQUIRED", "invitation code required to complete oauth registration")
  46. ErrCaptchaProviderConflict = infraerrors.ServiceUnavailable("CAPTCHA_PROVIDER_CONFLICT", "multiple captcha providers are enabled")
  47. )
  48. // maxTokenLength 限制 token 大小,避免超长 header 触发解析时的异常内存分配。
  49. const maxTokenLength = 8192
  50. // refreshTokenPrefix is the prefix for refresh tokens to distinguish them from access tokens.
  51. const refreshTokenPrefix = "rt_"
  52. // JWTClaims JWT载荷数据
  53. type JWTClaims struct {
  54. UserID int64 `json:"user_id"`
  55. Email string `json:"email"`
  56. Role string `json:"role"`
  57. TokenVersion int64 `json:"token_version"` // Used to invalidate tokens on password change
  58. // SessionID 会话 ID(与 refresh token family 对应),用于单会话撤销与 step-up 授权绑定。
  59. SessionID string `json:"sid,omitempty"`
  60. // BindingHash 会话指纹哈希(IP+UA),会话绑定开启时校验;空值表示旧 token(平滑升级)。
  61. BindingHash string `json:"bnd,omitempty"`
  62. jwt.RegisteredClaims
  63. }
  64. // AuthService 认证服务
  65. type AuthService struct {
  66. entClient *dbent.Client
  67. userRepo UserRepository
  68. redeemRepo RedeemCodeRepository
  69. refreshTokenCache RefreshTokenCache
  70. cfg *config.Config
  71. settingService *SettingService
  72. emailService *EmailService
  73. turnstileService *TurnstileService
  74. tencentCaptchaService *TencentCaptchaService
  75. aliyunCaptchaService *AliyunCaptchaService
  76. emailQueueService *EmailQueueService
  77. promoService *PromoService
  78. affiliateService *AffiliateService
  79. defaultSubAssigner DefaultSubscriptionAssigner
  80. userPlatformQuotaRepo UserPlatformQuotaRepository
  81. }
  82. type CaptchaProof struct {
  83. // TurnstileToken 承载 Cloudflare Turnstile token;阿里云验证码复用该字段承载 captchaVerifyParam
  84. TurnstileToken string
  85. TencentTicket string
  86. TencentRandstr string
  87. }
  88. type DefaultSubscriptionAssigner interface {
  89. AssignOrExtendSubscription(ctx context.Context, input *AssignSubscriptionInput) (*UserSubscription, bool, error)
  90. }
  91. type signupGrantPlan struct {
  92. Balance float64
  93. Concurrency int
  94. Subscriptions []DefaultSubscriptionSetting
  95. PlatformQuotas map[string]*DefaultPlatformQuotaSetting
  96. }
  97. // NewAuthService 创建认证服务实例
  98. func NewAuthService(
  99. entClient *dbent.Client,
  100. userRepo UserRepository,
  101. redeemRepo RedeemCodeRepository,
  102. refreshTokenCache RefreshTokenCache,
  103. cfg *config.Config,
  104. settingService *SettingService,
  105. emailService *EmailService,
  106. turnstileService *TurnstileService,
  107. emailQueueService *EmailQueueService,
  108. promoService *PromoService,
  109. defaultSubAssigner DefaultSubscriptionAssigner,
  110. affiliateService *AffiliateService,
  111. userPlatformQuotaRepo UserPlatformQuotaRepository,
  112. ) *AuthService {
  113. return &AuthService{
  114. entClient: entClient,
  115. userRepo: userRepo,
  116. redeemRepo: redeemRepo,
  117. refreshTokenCache: refreshTokenCache,
  118. cfg: cfg,
  119. settingService: settingService,
  120. emailService: emailService,
  121. turnstileService: turnstileService,
  122. emailQueueService: emailQueueService,
  123. promoService: promoService,
  124. affiliateService: affiliateService,
  125. defaultSubAssigner: defaultSubAssigner,
  126. userPlatformQuotaRepo: userPlatformQuotaRepo,
  127. }
  128. }
  129. func (s *AuthService) EntClient() *dbent.Client {
  130. if s == nil {
  131. return nil
  132. }
  133. return s.entClient
  134. }
  135. func (s *AuthService) SetTencentCaptchaService(tencentCaptchaService *TencentCaptchaService) {
  136. s.tencentCaptchaService = tencentCaptchaService
  137. }
  138. func (s *AuthService) SetAliyunCaptchaService(aliyunCaptchaService *AliyunCaptchaService) {
  139. s.aliyunCaptchaService = aliyunCaptchaService
  140. }
  141. // Register 用户注册,返回token和用户
  142. func (s *AuthService) Register(ctx context.Context, email, password string) (string, *User, error) {
  143. return s.RegisterWithVerification(ctx, email, password, "", "", "", "")
  144. }
  145. // RegisterWithVerification 用户注册(支持邮件验证、优惠码、邀请码和邀请返利码),返回token和用户。
  146. func (s *AuthService) RegisterWithVerification(ctx context.Context, email, password, verifyCode, promoCode, invitationCode, affiliateCode string) (string, *User, error) {
  147. // 检查是否开放注册(默认关闭:settingService 未配置时不允许注册)
  148. if s.settingService == nil || !s.settingService.IsRegistrationEnabled(ctx) {
  149. return "", nil, ErrRegDisabled
  150. }
  151. // 防止用户注册 LinuxDo OAuth 合成邮箱,避免第三方登录与本地账号发生碰撞。
  152. if isReservedEmail(email) {
  153. return "", nil, ErrEmailReserved
  154. }
  155. // 检查是否需要邀请码
  156. var invitationRedeemCode *RedeemCode
  157. if s.settingService != nil && s.settingService.IsInvitationCodeEnabled(ctx) {
  158. if invitationCode == "" {
  159. return "", nil, ErrInvitationCodeRequired
  160. }
  161. // 验证邀请码
  162. redeemCode, err := s.redeemRepo.GetByCode(ctx, invitationCode)
  163. if err != nil {
  164. logger.LegacyPrintf("service.auth", "[Auth] Invalid invitation code: %s, error: %v", invitationCode, err)
  165. return "", nil, ErrInvitationCodeInvalid
  166. }
  167. // 检查类型和状态
  168. if redeemCode.Type != RedeemTypeInvitation || !redeemCode.CanUse() {
  169. logger.LegacyPrintf("service.auth", "[Auth] Invitation code invalid: type=%s, status=%s", redeemCode.Type, redeemCode.Status)
  170. return "", nil, ErrInvitationCodeInvalid
  171. }
  172. invitationRedeemCode = redeemCode
  173. }
  174. // 检查是否需要邮件验证
  175. if s.settingService != nil && s.settingService.IsEmailVerifyEnabled(ctx) {
  176. // 如果邮件验证已开启但邮件服务未配置,拒绝注册
  177. // 这是一个配置错误,不应该允许绕过验证
  178. if s.emailService == nil {
  179. logger.LegacyPrintf("service.auth", "%s", "[Auth] Email verification enabled but email service not configured, rejecting registration")
  180. return "", nil, ErrServiceUnavailable
  181. }
  182. if verifyCode == "" {
  183. return "", nil, ErrEmailVerifyRequired
  184. }
  185. // 验证邮箱验证码
  186. if err := s.emailService.VerifyCode(ctx, email, verifyCode); err != nil {
  187. return "", nil, fmt.Errorf("verify code: %w", err)
  188. }
  189. }
  190. // 检查邮箱是否已存在(含 +别名 / Gmail 点号变体归一化,防止单个收件箱批量派生注册)
  191. existsEmail, err := s.existsByEmailOrAlias(ctx, email)
  192. if err != nil {
  193. logger.LegacyPrintf("service.auth", "[Auth] Database error checking email exists: %v", err)
  194. return "", nil, ErrServiceUnavailable
  195. }
  196. if existsEmail {
  197. return "", nil, ErrEmailExists
  198. }
  199. if err := s.validateRegistrationEmailQuota(ctx, email); err != nil {
  200. return "", nil, err
  201. }
  202. // 密码哈希
  203. hashedPassword, err := s.HashPassword(password)
  204. if err != nil {
  205. return "", nil, fmt.Errorf("hash password: %w", err)
  206. }
  207. grantPlan := s.resolveSignupGrantPlan(ctx, "email")
  208. // 新用户默认 RPM(0 = 不限制)。注册时写入,后续作为用户级兜底。
  209. var defaultRPMLimit int
  210. if s.settingService != nil {
  211. defaultRPMLimit = s.settingService.GetDefaultUserRPMLimit(ctx)
  212. }
  213. // 创建用户
  214. user := &User{
  215. Email: email,
  216. PasswordHash: hashedPassword,
  217. Role: RoleUser,
  218. Balance: grantPlan.Balance,
  219. Concurrency: grantPlan.Concurrency,
  220. RPMLimit: defaultRPMLimit,
  221. Status: StatusActive,
  222. }
  223. if err := s.createUserAndClaimInvitation(ctx, user, invitationRedeemCode); err != nil {
  224. // 优先检查邮箱冲突错误(竞态条件下可能发生)
  225. switch {
  226. case errors.Is(err, ErrEmailExists):
  227. return "", nil, ErrEmailExists
  228. case errors.Is(err, ErrEmailDomainRegistrationLimit):
  229. return "", nil, ErrEmailDomainRegistrationLimit
  230. case errors.Is(err, ErrInvitationCodeInvalid):
  231. return "", nil, ErrInvitationCodeInvalid
  232. default:
  233. logger.LegacyPrintf("service.auth", "[Auth] Database error creating user: %v", err)
  234. return "", nil, ErrServiceUnavailable
  235. }
  236. }
  237. s.postAuthUserBootstrap(ctx, user, "email", true)
  238. s.assignSubscriptions(ctx, user.ID, grantPlan.Subscriptions, "auto assigned by signup defaults")
  239. // snapshot user × platform quota(fail-open)
  240. _ = s.snapshotPlatformQuotaDefaults(ctx, user.ID, &grantPlan)
  241. if s.affiliateService != nil {
  242. if _, err := s.affiliateService.EnsureUserAffiliate(ctx, user.ID); err != nil {
  243. logger.LegacyPrintf("service.auth", "[Auth] Failed to initialize affiliate profile for user %d: %v", user.ID, err)
  244. }
  245. if code := strings.TrimSpace(affiliateCode); code != "" {
  246. if err := s.affiliateService.BindInviterByCode(ctx, user.ID, code); err != nil {
  247. // 邀请返利码绑定失败不影响注册,只记录日志
  248. logger.LegacyPrintf("service.auth", "[Auth] Failed to bind affiliate inviter for user %d: %v", user.ID, err)
  249. }
  250. }
  251. }
  252. // 邀请码占用已由 createUserAndClaimInvitation 在“用户创建 + 邀请码占用”的
  253. // 同一个数据库事务内原子完成(一次性约束,见函数注释),此处不再单独标记。
  254. // 应用优惠码(如果提供且功能已启用)
  255. if promoCode != "" && s.promoService != nil && s.settingService != nil && s.settingService.IsPromoCodeEnabled(ctx) {
  256. if err := s.promoService.ApplyPromoCode(ctx, user.ID, promoCode); err != nil {
  257. // 优惠码应用失败不影响注册,只记录日志
  258. logger.LegacyPrintf("service.auth", "[Auth] Failed to apply promo code for user %d: %v", user.ID, err)
  259. } else {
  260. // 重新获取用户信息以获取更新后的余额
  261. if updatedUser, err := s.userRepo.GetByID(ctx, user.ID); err == nil {
  262. user = updatedUser
  263. }
  264. }
  265. }
  266. // 生成token
  267. token, err := s.GenerateToken(ctx, user)
  268. if err != nil {
  269. return "", nil, fmt.Errorf("generate token: %w", err)
  270. }
  271. return token, user, nil
  272. }
  273. // SendVerifyCodeResult 发送验证码返回结果
  274. type SendVerifyCodeResult struct {
  275. Countdown int `json:"countdown"` // 倒计时秒数
  276. }
  277. // SendVerifyCode 发送邮箱验证码(同步方式)
  278. func (s *AuthService) SendVerifyCode(ctx context.Context, email string, locale ...string) error {
  279. // 检查是否开放注册(默认关闭)
  280. if s.settingService == nil || !s.settingService.IsRegistrationEnabled(ctx) {
  281. return ErrRegDisabled
  282. }
  283. if isReservedEmail(email) {
  284. return ErrEmailReserved
  285. }
  286. // 检查邮箱是否已存在(含 +别名 / Gmail 点号变体归一化,防止单个收件箱批量派生注册)
  287. existsEmail, err := s.existsByEmailOrAlias(ctx, email)
  288. if err != nil {
  289. logger.LegacyPrintf("service.auth", "[Auth] Database error checking email exists: %v", err)
  290. return ErrServiceUnavailable
  291. }
  292. if existsEmail {
  293. return ErrEmailExists
  294. }
  295. if err := s.validateRegistrationEmailQuota(ctx, email); err != nil {
  296. return err
  297. }
  298. // 发送验证码
  299. if s.emailService == nil {
  300. return errors.New("email service not configured")
  301. }
  302. // 获取网站名称
  303. siteName := "Sub2API"
  304. if s.settingService != nil {
  305. siteName = s.settingService.GetSiteName(ctx)
  306. }
  307. return s.emailService.SendVerifyCode(ctx, email, siteName, firstEmailLocale(locale))
  308. }
  309. // SendVerifyCodeAsync 异步发送邮箱验证码并返回倒计时
  310. func (s *AuthService) SendVerifyCodeAsync(ctx context.Context, email string, locale ...string) (*SendVerifyCodeResult, error) {
  311. logger.LegacyPrintf("service.auth", "[Auth] SendVerifyCodeAsync called for email: %s", email)
  312. // 检查是否开放注册(默认关闭)
  313. if s.settingService == nil || !s.settingService.IsRegistrationEnabled(ctx) {
  314. logger.LegacyPrintf("service.auth", "%s", "[Auth] Registration is disabled")
  315. return nil, ErrRegDisabled
  316. }
  317. if isReservedEmail(email) {
  318. return nil, ErrEmailReserved
  319. }
  320. // 检查邮箱是否已存在(含 +别名 / Gmail 点号变体归一化;在发信前拦截,避免批量脚本消耗发信配额)
  321. existsEmail, err := s.existsByEmailOrAlias(ctx, email)
  322. if err != nil {
  323. logger.LegacyPrintf("service.auth", "[Auth] Database error checking email exists: %v", err)
  324. return nil, ErrServiceUnavailable
  325. }
  326. if existsEmail {
  327. logger.LegacyPrintf("service.auth", "[Auth] Email already exists: %s", email)
  328. return nil, ErrEmailExists
  329. }
  330. if err := s.validateRegistrationEmailQuota(ctx, email); err != nil {
  331. return nil, err
  332. }
  333. // 检查邮件队列服务是否配置
  334. if s.emailQueueService == nil {
  335. logger.LegacyPrintf("service.auth", "%s", "[Auth] Email queue service not configured")
  336. return nil, errors.New("email queue service not configured")
  337. }
  338. // 获取网站名称
  339. siteName := "Sub2API"
  340. if s.settingService != nil {
  341. siteName = s.settingService.GetSiteName(ctx)
  342. }
  343. // 异步发送
  344. logger.LegacyPrintf("service.auth", "[Auth] Enqueueing verify code for: %s", email)
  345. if err := s.emailQueueService.EnqueueVerifyCode(email, siteName, firstEmailLocale(locale)); err != nil {
  346. logger.LegacyPrintf("service.auth", "[Auth] Failed to enqueue: %v", err)
  347. return nil, fmt.Errorf("enqueue verify code: %w", err)
  348. }
  349. logger.LegacyPrintf("service.auth", "[Auth] Verify code enqueued successfully for: %s", email)
  350. return &SendVerifyCodeResult{
  351. Countdown: 60, // 60秒倒计时
  352. }, nil
  353. }
  354. // VerifyCaptchaForRegister 在注册场景下验证当前启用的验证码。
  355. // 当邮箱验证开启且已提交验证码时,说明验证码发送阶段已完成验证码校验,
  356. // 此处跳过二次校验,避免一次性 token 在注册提交时重复使用导致误报失败。
  357. func (s *AuthService) VerifyCaptchaForRegister(ctx context.Context, proof CaptchaProof, remoteIP, verifyCode string) error {
  358. if s.IsEmailVerifyEnabled(ctx) && strings.TrimSpace(verifyCode) != "" {
  359. logger.LegacyPrintf("service.auth", "%s", "[Auth] Email verify flow detected, skip duplicate captcha check on register")
  360. return nil
  361. }
  362. return s.VerifyCaptcha(ctx, proof, remoteIP)
  363. }
  364. func (s *AuthService) VerifyCaptcha(ctx context.Context, proof CaptchaProof, remoteIP string) error {
  365. required := s.cfg != nil && s.cfg.Server.Mode == "release" && s.cfg.Turnstile.Required
  366. if s.settingService == nil {
  367. if required {
  368. return ErrTurnstileNotConfigured
  369. }
  370. return nil
  371. }
  372. providerConfig, err := s.settingService.GetCaptchaProviderConfig(ctx)
  373. if err != nil {
  374. logger.LegacyPrintf("service.auth", "%s", "[Auth] Failed to read captcha provider settings")
  375. return ErrServiceUnavailable
  376. }
  377. turnstileEnabled := providerConfig.TurnstileEnabled
  378. tencentEnabled := providerConfig.Tencent.Enabled
  379. aliyunEnabled := providerConfig.Aliyun.Enabled
  380. if captchaProvidersConflict(turnstileEnabled, tencentEnabled, aliyunEnabled) {
  381. return ErrCaptchaProviderConflict
  382. }
  383. if tencentEnabled {
  384. if s.tencentCaptchaService == nil {
  385. return ErrTencentCaptchaNotConfigured
  386. }
  387. return s.tencentCaptchaService.VerifyTicketWithConfig(ctx, providerConfig.Tencent, proof.TencentTicket, proof.TencentRandstr, remoteIP)
  388. }
  389. if aliyunEnabled {
  390. if s.aliyunCaptchaService == nil {
  391. return ErrAliyunCaptchaNotConfigured
  392. }
  393. return s.aliyunCaptchaService.VerifyParamWithConfig(ctx, providerConfig.Aliyun, proof.TurnstileToken)
  394. }
  395. if turnstileEnabled {
  396. if s.turnstileService == nil || strings.TrimSpace(providerConfig.TurnstileSecretKey) == "" {
  397. return ErrTurnstileNotConfigured
  398. }
  399. return s.turnstileService.VerifyTokenWithSecret(ctx, providerConfig.TurnstileSecretKey, proof.TurnstileToken, remoteIP)
  400. }
  401. if required {
  402. return ErrTurnstileNotConfigured
  403. }
  404. return nil
  405. }
  406. // captchaProvidersConflict 同一时间仅允许启用一家人机验证服务商
  407. func captchaProvidersConflict(enabled ...bool) bool {
  408. count := 0
  409. for _, e := range enabled {
  410. if e {
  411. count++
  412. }
  413. }
  414. return count > 1
  415. }
  416. // VerifyActionCaptchaIfEnabled 仅保护动作触发的扩展入口(OAuth 登录启动、passkey 登录),
  417. // 腾讯天御与阿里云验证码启用时拦截;不扩大 Cloudflare Turnstile 的既有覆盖范围。
  418. func (s *AuthService) VerifyActionCaptchaIfEnabled(ctx context.Context, proof CaptchaProof, remoteIP string) error {
  419. if s == nil || s.settingService == nil {
  420. return ErrServiceUnavailable
  421. }
  422. providerConfig, err := s.settingService.GetCaptchaProviderConfig(ctx)
  423. if err != nil {
  424. logger.LegacyPrintf("service.auth", "%s", "[Auth] Failed to read captcha provider settings")
  425. return ErrServiceUnavailable
  426. }
  427. tencentEnabled := providerConfig.Tencent.Enabled
  428. aliyunEnabled := providerConfig.Aliyun.Enabled
  429. if !tencentEnabled && !aliyunEnabled {
  430. return nil
  431. }
  432. if captchaProvidersConflict(providerConfig.TurnstileEnabled, tencentEnabled, aliyunEnabled) {
  433. return ErrCaptchaProviderConflict
  434. }
  435. if aliyunEnabled {
  436. if s.aliyunCaptchaService == nil {
  437. return ErrAliyunCaptchaNotConfigured
  438. }
  439. return s.aliyunCaptchaService.VerifyParamWithConfig(ctx, providerConfig.Aliyun, proof.TurnstileToken)
  440. }
  441. if s.tencentCaptchaService == nil {
  442. return ErrTencentCaptchaNotConfigured
  443. }
  444. return s.tencentCaptchaService.VerifyTicketWithConfig(
  445. ctx,
  446. providerConfig.Tencent,
  447. proof.TencentTicket,
  448. proof.TencentRandstr,
  449. remoteIP,
  450. )
  451. }
  452. // VerifyTurnstileForRegister 保留旧内部接口,生产 handler 使用 VerifyCaptchaForRegister。
  453. func (s *AuthService) VerifyTurnstileForRegister(ctx context.Context, token, remoteIP, verifyCode string) error {
  454. return s.VerifyCaptchaForRegister(ctx, CaptchaProof{TurnstileToken: token}, remoteIP, verifyCode)
  455. }
  456. // VerifyTurnstile 保留旧内部接口,生产 handler 使用 VerifyCaptcha。
  457. func (s *AuthService) VerifyTurnstile(ctx context.Context, token string, remoteIP string) error {
  458. return s.VerifyCaptcha(ctx, CaptchaProof{TurnstileToken: token}, remoteIP)
  459. }
  460. // IsTurnstileEnabled 检查是否启用Turnstile验证
  461. func (s *AuthService) IsTurnstileEnabled(ctx context.Context) bool {
  462. if s.turnstileService == nil {
  463. return false
  464. }
  465. return s.turnstileService.IsEnabled(ctx)
  466. }
  467. // IsRegistrationEnabled 检查是否开放注册
  468. func (s *AuthService) IsRegistrationEnabled(ctx context.Context) bool {
  469. if s.settingService == nil {
  470. return false // 安全默认:settingService 未配置时关闭注册
  471. }
  472. return s.settingService.IsRegistrationEnabled(ctx)
  473. }
  474. // IsEmailVerifyEnabled 检查是否开启邮件验证
  475. func (s *AuthService) IsEmailVerifyEnabled(ctx context.Context) bool {
  476. if s.settingService == nil {
  477. return false
  478. }
  479. return s.settingService.IsEmailVerifyEnabled(ctx)
  480. }
  481. // Login 用户登录,返回JWT token
  482. func (s *AuthService) Login(ctx context.Context, email, password string) (string, *User, error) {
  483. // 查找用户
  484. user, err := s.userRepo.GetByEmail(ctx, email)
  485. if err != nil {
  486. if errors.Is(err, ErrUserNotFound) {
  487. return "", nil, ErrInvalidCredentials
  488. }
  489. // 记录数据库错误但不暴露给用户
  490. logger.LegacyPrintf("service.auth", "[Auth] Database error during login: %v", err)
  491. return "", nil, ErrServiceUnavailable
  492. }
  493. // 验证密码
  494. if !s.CheckPassword(password, user.PasswordHash) {
  495. return "", nil, ErrInvalidCredentials
  496. }
  497. // 检查用户状态
  498. if !user.IsActive() {
  499. return "", nil, ErrUserNotActive
  500. }
  501. // 生成JWT token
  502. token, err := s.GenerateToken(ctx, user)
  503. if err != nil {
  504. return "", nil, fmt.Errorf("generate token: %w", err)
  505. }
  506. return token, user, nil
  507. }
  508. // LoginOrRegisterOAuth 用于第三方 OAuth/SSO 登录:
  509. // - 如果邮箱已存在:直接登录(不需要本地密码)
  510. // - 如果邮箱不存在:创建新用户并登录
  511. //
  512. // 注意:该函数用于 LinuxDo OAuth 登录场景(不同于上游账号的 OAuth,例如 Claude/OpenAI/Gemini)。
  513. // 为了满足现有数据库约束(需要密码哈希),新用户会生成随机密码并进行哈希保存。
  514. func (s *AuthService) LoginOrRegisterOAuth(ctx context.Context, email, username string) (string, *User, error) {
  515. email = strings.TrimSpace(email)
  516. if email == "" || len(email) > 255 {
  517. return "", nil, infraerrors.BadRequest("INVALID_EMAIL", "invalid email")
  518. }
  519. if _, err := mail.ParseAddress(email); err != nil {
  520. return "", nil, infraerrors.BadRequest("INVALID_EMAIL", "invalid email")
  521. }
  522. username = strings.TrimSpace(username)
  523. if len([]rune(username)) > 100 {
  524. username = string([]rune(username)[:100])
  525. }
  526. user, err := s.userRepo.GetByEmail(ctx, email)
  527. if err != nil {
  528. if errors.Is(err, ErrUserNotFound) {
  529. // OAuth 首次登录视为注册(fail-close:settingService 未配置时不允许注册)
  530. if s.settingService == nil || !s.settingService.IsRegistrationEnabled(ctx) {
  531. return "", nil, ErrRegDisabled
  532. }
  533. randomPassword, err := randomHexString(32)
  534. if err != nil {
  535. logger.LegacyPrintf("service.auth", "[Auth] Failed to generate random password for oauth signup: %v", err)
  536. return "", nil, ErrServiceUnavailable
  537. }
  538. hashedPassword, err := s.HashPassword(randomPassword)
  539. if err != nil {
  540. return "", nil, fmt.Errorf("hash password: %w", err)
  541. }
  542. signupSource := inferLegacySignupSource(email)
  543. grantPlan := s.resolveSignupGrantPlan(ctx, signupSource)
  544. var defaultRPMLimit int
  545. if s.settingService != nil {
  546. defaultRPMLimit = s.settingService.GetDefaultUserRPMLimit(ctx)
  547. }
  548. newUser := &User{
  549. Email: email,
  550. Username: username,
  551. PasswordHash: hashedPassword,
  552. Role: RoleUser,
  553. Balance: grantPlan.Balance,
  554. Concurrency: grantPlan.Concurrency,
  555. RPMLimit: defaultRPMLimit,
  556. Status: StatusActive,
  557. SignupSource: signupSource,
  558. }
  559. if err := s.userRepo.Create(ctx, newUser); err != nil {
  560. if errors.Is(err, ErrEmailExists) {
  561. // 并发场景:GetByEmail 与 Create 之间用户被创建。
  562. user, err = s.userRepo.GetByEmail(ctx, email)
  563. if err != nil {
  564. logger.LegacyPrintf("service.auth", "[Auth] Database error getting user after conflict: %v", err)
  565. return "", nil, ErrServiceUnavailable
  566. }
  567. } else {
  568. logger.LegacyPrintf("service.auth", "[Auth] Database error creating oauth user: %v", err)
  569. return "", nil, ErrServiceUnavailable
  570. }
  571. } else {
  572. user = newUser
  573. s.postAuthUserBootstrap(ctx, user, signupSource, false)
  574. s.assignSubscriptions(ctx, user.ID, grantPlan.Subscriptions, "auto assigned by signup defaults")
  575. // snapshot user × platform quota(fail-open)
  576. _ = s.snapshotPlatformQuotaDefaults(ctx, user.ID, &grantPlan)
  577. }
  578. } else {
  579. logger.LegacyPrintf("service.auth", "[Auth] Database error during oauth login: %v", err)
  580. return "", nil, ErrServiceUnavailable
  581. }
  582. }
  583. if !user.IsActive() {
  584. return "", nil, ErrUserNotActive
  585. }
  586. // 尽力补全:当用户名为空时,使用第三方返回的用户名回填。
  587. if user.Username == "" && username != "" {
  588. user.Username = username
  589. if err := s.userRepo.Update(ctx, user, UserUpdateFields{Username: true}); err != nil {
  590. logger.LegacyPrintf("service.auth", "[Auth] Failed to update username after oauth login: %v", err)
  591. }
  592. }
  593. token, err := s.GenerateToken(ctx, user)
  594. if err != nil {
  595. return "", nil, fmt.Errorf("generate token: %w", err)
  596. }
  597. return token, user, nil
  598. }
  599. // canBypassRegistrationDisabledForOAuth 在钉钉企业模式(internal_only)且
  600. // dingtalk_connect_bypass_registration=true 时,允许跳过全局 registration_enabled 检查。
  601. func (s *AuthService) canBypassRegistrationDisabledForOAuth(ctx context.Context, signupSource string) bool {
  602. if signupSource != "dingtalk" {
  603. return false
  604. }
  605. cfg, err := s.settingService.GetDingTalkConnectOAuthConfig(ctx)
  606. if err != nil || !cfg.Enabled || !cfg.BypassRegistration {
  607. return false
  608. }
  609. return cfg.CorpRestrictionPolicy == "internal_only"
  610. }
  611. // LoginOrRegisterOAuthWithTokenPair 用于第三方 OAuth/SSO 登录,返回完整的 TokenPair。
  612. // 与 LoginOrRegisterOAuth 功能相同,但返回 TokenPair 而非单个 token。
  613. // invitationCode 仅在邀请码注册模式下新用户注册时使用;已有账号登录时忽略。
  614. // affiliateCode 用于邀请返利绑定,仅在新用户注册时使用。
  615. // signupSource 标识来源渠道("dingtalk"/"linuxdo"/"wechat"/"oidc" 等),仅用于豁免检查。
  616. func (s *AuthService) LoginOrRegisterOAuthWithTokenPair(ctx context.Context, email, username, invitationCode, affiliateCode, signupSource string) (*TokenPair, *User, error) {
  617. return s.loginOrRegisterOAuthWithTokenPair(ctx, email, username, invitationCode, affiliateCode, "", signupSource)
  618. }
  619. // LoginOrRegisterOAuthWithTokenPairAndPromoCode behaves like
  620. // LoginOrRegisterOAuthWithTokenPair and applies promoCode only when a new user
  621. // is created.
  622. func (s *AuthService) LoginOrRegisterOAuthWithTokenPairAndPromoCode(ctx context.Context, email, username, invitationCode, affiliateCode, promoCode, signupSource string) (*TokenPair, *User, error) {
  623. return s.loginOrRegisterOAuthWithTokenPair(ctx, email, username, invitationCode, affiliateCode, promoCode, signupSource)
  624. }
  625. func (s *AuthService) loginOrRegisterOAuthWithTokenPair(ctx context.Context, email, username, invitationCode, affiliateCode, promoCode, signupSource string) (*TokenPair, *User, error) {
  626. // 检查 refreshTokenCache 是否可用
  627. if s.refreshTokenCache == nil {
  628. return nil, nil, errors.New("refresh token cache not configured")
  629. }
  630. email = strings.TrimSpace(email)
  631. if email == "" || len(email) > 255 {
  632. return nil, nil, infraerrors.BadRequest("INVALID_EMAIL", "invalid email")
  633. }
  634. if _, err := mail.ParseAddress(email); err != nil {
  635. return nil, nil, infraerrors.BadRequest("INVALID_EMAIL", "invalid email")
  636. }
  637. username = strings.TrimSpace(username)
  638. if len([]rune(username)) > 100 {
  639. username = string([]rune(username)[:100])
  640. }
  641. user, err := s.userRepo.GetByEmail(ctx, email)
  642. created := false
  643. if err != nil {
  644. if errors.Is(err, ErrUserNotFound) {
  645. // OAuth 首次登录视为注册
  646. if s.settingService == nil || (!s.settingService.IsRegistrationEnabled(ctx) && !s.canBypassRegistrationDisabledForOAuth(ctx, signupSource)) {
  647. return nil, nil, ErrRegDisabled
  648. }
  649. // 检查是否需要邀请码
  650. var invitationRedeemCode *RedeemCode
  651. if s.settingService != nil && s.settingService.IsInvitationCodeEnabled(ctx) {
  652. if invitationCode == "" {
  653. return nil, nil, ErrOAuthInvitationRequired
  654. }
  655. redeemCode, err := s.redeemRepo.GetByCode(ctx, invitationCode)
  656. if err != nil {
  657. return nil, nil, ErrInvitationCodeInvalid
  658. }
  659. if redeemCode.Type != RedeemTypeInvitation || !redeemCode.CanUse() {
  660. return nil, nil, ErrInvitationCodeInvalid
  661. }
  662. invitationRedeemCode = redeemCode
  663. }
  664. randomPassword, err := randomHexString(32)
  665. if err != nil {
  666. logger.LegacyPrintf("service.auth", "[Auth] Failed to generate random password for oauth signup: %v", err)
  667. return nil, nil, ErrServiceUnavailable
  668. }
  669. hashedPassword, err := s.HashPassword(randomPassword)
  670. if err != nil {
  671. return nil, nil, fmt.Errorf("hash password: %w", err)
  672. }
  673. // 优先用 caller 显式传入的 signupSource(如 "dingtalk" / "linuxdo" / "oidc" / "wechat"),
  674. // 否则才按邮箱后缀推断——避免有真实邮箱的 OAuth 用户被推断为 "email" 渠道,导致渠道授权错读。
  675. if strings.TrimSpace(signupSource) == "" {
  676. signupSource = inferLegacySignupSource(email)
  677. }
  678. grantPlan := s.resolveSignupGrantPlan(ctx, signupSource)
  679. var defaultRPMLimit int
  680. if s.settingService != nil {
  681. defaultRPMLimit = s.settingService.GetDefaultUserRPMLimit(ctx)
  682. }
  683. newUser := &User{
  684. Email: email,
  685. Username: username,
  686. PasswordHash: hashedPassword,
  687. Role: RoleUser,
  688. Balance: grantPlan.Balance,
  689. Concurrency: grantPlan.Concurrency,
  690. RPMLimit: defaultRPMLimit,
  691. Status: StatusActive,
  692. SignupSource: signupSource,
  693. }
  694. if s.entClient != nil && invitationRedeemCode != nil {
  695. tx, err := s.entClient.Tx(ctx)
  696. if err != nil {
  697. logger.LegacyPrintf("service.auth", "[Auth] Failed to begin transaction for oauth registration: %v", err)
  698. return nil, nil, ErrServiceUnavailable
  699. }
  700. defer func() { _ = tx.Rollback() }()
  701. txCtx := dbent.NewTxContext(ctx, tx)
  702. if err := s.userRepo.Create(txCtx, newUser); err != nil {
  703. if errors.Is(err, ErrEmailExists) {
  704. user, err = s.userRepo.GetByEmail(ctx, email)
  705. if err != nil {
  706. logger.LegacyPrintf("service.auth", "[Auth] Database error getting user after conflict: %v", err)
  707. return nil, nil, ErrServiceUnavailable
  708. }
  709. } else {
  710. logger.LegacyPrintf("service.auth", "[Auth] Database error creating oauth user: %v", err)
  711. return nil, nil, ErrServiceUnavailable
  712. }
  713. } else {
  714. if err := s.redeemRepo.Use(txCtx, invitationRedeemCode.ID, newUser.ID); err != nil {
  715. return nil, nil, ErrInvitationCodeInvalid
  716. }
  717. if err := tx.Commit(); err != nil {
  718. logger.LegacyPrintf("service.auth", "[Auth] Failed to commit oauth registration transaction: %v", err)
  719. return nil, nil, ErrServiceUnavailable
  720. }
  721. user = newUser
  722. created = true
  723. s.postAuthUserBootstrap(ctx, user, signupSource, false)
  724. s.assignSubscriptions(ctx, user.ID, grantPlan.Subscriptions, "auto assigned by signup defaults")
  725. // snapshot user × platform quota(fail-open)
  726. _ = s.snapshotPlatformQuotaDefaults(ctx, user.ID, &grantPlan)
  727. s.bindOAuthAffiliate(ctx, user.ID, affiliateCode)
  728. }
  729. } else {
  730. if err := s.userRepo.Create(ctx, newUser); err != nil {
  731. if errors.Is(err, ErrEmailExists) {
  732. user, err = s.userRepo.GetByEmail(ctx, email)
  733. if err != nil {
  734. logger.LegacyPrintf("service.auth", "[Auth] Database error getting user after conflict: %v", err)
  735. return nil, nil, ErrServiceUnavailable
  736. }
  737. } else {
  738. logger.LegacyPrintf("service.auth", "[Auth] Database error creating oauth user: %v", err)
  739. return nil, nil, ErrServiceUnavailable
  740. }
  741. } else {
  742. user = newUser
  743. created = true
  744. s.postAuthUserBootstrap(ctx, user, signupSource, false)
  745. s.assignSubscriptions(ctx, user.ID, grantPlan.Subscriptions, "auto assigned by signup defaults")
  746. // snapshot user × platform quota(fail-open)
  747. _ = s.snapshotPlatformQuotaDefaults(ctx, user.ID, &grantPlan)
  748. s.bindOAuthAffiliate(ctx, user.ID, affiliateCode)
  749. if invitationRedeemCode != nil {
  750. if err := s.redeemRepo.Use(ctx, invitationRedeemCode.ID, user.ID); err != nil {
  751. return nil, nil, ErrInvitationCodeInvalid
  752. }
  753. }
  754. }
  755. }
  756. } else {
  757. logger.LegacyPrintf("service.auth", "[Auth] Database error during oauth login: %v", err)
  758. return nil, nil, ErrServiceUnavailable
  759. }
  760. }
  761. if !user.IsActive() {
  762. return nil, nil, ErrUserNotActive
  763. }
  764. if user.Username == "" && username != "" {
  765. user.Username = username
  766. if err := s.userRepo.Update(ctx, user, UserUpdateFields{Username: true}); err != nil {
  767. logger.LegacyPrintf("service.auth", "[Auth] Failed to update username after oauth login: %v", err)
  768. }
  769. }
  770. if created {
  771. user = s.applyOAuthSignupPromoCode(ctx, user, promoCode)
  772. }
  773. tokenPair, err := s.GenerateTokenPair(ctx, user, "")
  774. if err != nil {
  775. return nil, nil, fmt.Errorf("generate token pair: %w", err)
  776. }
  777. return tokenPair, user, nil
  778. }
  779. func (s *AuthService) ApplyOAuthSignupPromoCode(ctx context.Context, userID int64, promoCode string) {
  780. if userID <= 0 {
  781. return
  782. }
  783. s.applyOAuthSignupPromoCode(ctx, &User{ID: userID}, promoCode)
  784. }
  785. func (s *AuthService) applyOAuthSignupPromoCode(ctx context.Context, user *User, promoCode string) *User {
  786. promoCode = strings.TrimSpace(promoCode)
  787. if user == nil || user.ID <= 0 || promoCode == "" || s.promoService == nil || s.settingService == nil || !s.settingService.IsPromoCodeEnabled(ctx) {
  788. return user
  789. }
  790. if err := s.promoService.ApplyPromoCode(ctx, user.ID, promoCode); err != nil {
  791. logger.LegacyPrintf("service.auth", "[Auth] Failed to apply promo code for oauth user %d: %v", user.ID, err)
  792. return user
  793. }
  794. if updatedUser, err := s.userRepo.GetByID(ctx, user.ID); err == nil {
  795. return updatedUser
  796. }
  797. return user
  798. }
  799. func (s *AuthService) assignSubscriptions(ctx context.Context, userID int64, items []DefaultSubscriptionSetting, notes string) {
  800. if s.settingService == nil || s.defaultSubAssigner == nil || userID <= 0 {
  801. return
  802. }
  803. for _, item := range items {
  804. if _, _, err := s.defaultSubAssigner.AssignOrExtendSubscription(ctx, &AssignSubscriptionInput{
  805. UserID: userID,
  806. GroupID: item.GroupID,
  807. ValidityDays: item.ValidityDays,
  808. Notes: notes,
  809. }); err != nil {
  810. logger.LegacyPrintf("service.auth", "[Auth] Failed to assign default subscription: user_id=%d group_id=%d err=%v", userID, item.GroupID, err)
  811. }
  812. }
  813. }
  814. func (s *AuthService) resolveSignupGrantPlan(ctx context.Context, signupSource string) signupGrantPlan {
  815. plan := signupGrantPlan{}
  816. if s != nil && s.cfg != nil {
  817. plan.Balance = s.cfg.Default.UserBalance
  818. plan.Concurrency = s.cfg.Default.UserConcurrency
  819. }
  820. if s == nil || s.settingService == nil {
  821. return plan
  822. }
  823. plan.Balance = s.settingService.GetDefaultBalance(ctx)
  824. plan.Concurrency = s.settingService.GetDefaultConcurrency(ctx)
  825. plan.Subscriptions = s.settingService.GetDefaultSubscriptions(ctx)
  826. // ============ 全局 quota 装载(必须在 ResolveAuthSourceGrantSettings 之前) ============
  827. // 无论 auth source 是否 enabled,全局层都要先装载,确保 !enabled 早退路径也携带全局 quota。
  828. if quotas, err := s.settingService.GetDefaultPlatformQuotas(ctx); err == nil {
  829. plan.PlatformQuotas = quotas
  830. } else {
  831. logger.LegacyPrintf("service.auth", "[Auth] Warning: load default platform quotas failed: %v (fail-open)", err)
  832. }
  833. // ============================================================================================
  834. resolved, enabled, err := s.settingService.ResolveAuthSourceGrantSettings(ctx, signupSource, false)
  835. if err != nil {
  836. logger.LegacyPrintf("service.auth", "[Auth] Failed to load auth source signup defaults for %s: %v", signupSource, err)
  837. return plan
  838. }
  839. if !enabled {
  840. return plan // plan.PlatformQuotas 已含全局层
  841. }
  842. plan.Balance = resolved.Balance
  843. plan.Concurrency = resolved.Concurrency
  844. plan.Subscriptions = resolved.Subscriptions
  845. // ============ auth source quota merge(仅在 enabled 分支内) ============
  846. asQuotas := s.settingService.GetAuthSourcePlatformQuotas(ctx, signupSource)
  847. if plan.PlatformQuotas != nil {
  848. for platform, patch := range asQuotas {
  849. if dst := plan.PlatformQuotas[platform]; dst != nil {
  850. mergePlatformQuotaDefaults(dst, patch)
  851. }
  852. }
  853. }
  854. // ==============================================================================
  855. return plan
  856. }
  857. func authSourceSignupSettings(defaults *AuthSourceDefaultSettings, signupSource string) (ProviderDefaultGrantSettings, bool) {
  858. if defaults == nil {
  859. return ProviderDefaultGrantSettings{}, false
  860. }
  861. switch strings.ToLower(strings.TrimSpace(signupSource)) {
  862. case "email":
  863. return defaults.Email, true
  864. case "linuxdo":
  865. return defaults.LinuxDo, true
  866. case "oidc":
  867. return defaults.OIDC, true
  868. case "wechat":
  869. return defaults.WeChat, true
  870. case "github":
  871. return defaults.GitHub, true
  872. case "google":
  873. return defaults.Google, true
  874. case "dingtalk":
  875. return defaults.DingTalk, true
  876. default:
  877. return ProviderDefaultGrantSettings{}, false
  878. }
  879. }
  880. // bindOAuthAffiliate initializes the affiliate profile and binds the inviter
  881. // for an OAuth-registered user. Failures are logged but never block registration.
  882. func (s *AuthService) bindOAuthAffiliate(ctx context.Context, userID int64, affiliateCode string) {
  883. if s.affiliateService == nil || userID <= 0 {
  884. return
  885. }
  886. if _, err := s.affiliateService.EnsureUserAffiliate(ctx, userID); err != nil {
  887. logger.LegacyPrintf("service.auth", "[Auth] Failed to initialize affiliate profile for user %d: %v", userID, err)
  888. }
  889. if code := strings.TrimSpace(affiliateCode); code != "" {
  890. if err := s.affiliateService.BindInviterByCode(ctx, userID, code); err != nil {
  891. logger.LegacyPrintf("service.auth", "[Auth] Failed to bind affiliate inviter for user %d: %v", userID, err)
  892. }
  893. }
  894. }
  895. func (s *AuthService) postAuthUserBootstrap(ctx context.Context, user *User, signupSource string, touchLogin bool) {
  896. if user == nil || user.ID <= 0 {
  897. return
  898. }
  899. if strings.TrimSpace(signupSource) == "" {
  900. signupSource = "email"
  901. }
  902. s.updateUserSignupSource(ctx, user.ID, signupSource)
  903. if touchLogin {
  904. s.touchUserLogin(ctx, user.ID)
  905. }
  906. }
  907. func (s *AuthService) updateUserSignupSource(ctx context.Context, userID int64, signupSource string) {
  908. if s == nil || s.entClient == nil || userID <= 0 {
  909. return
  910. }
  911. if strings.TrimSpace(signupSource) == "" {
  912. return
  913. }
  914. if err := s.entClient.User.UpdateOneID(userID).
  915. SetSignupSource(signupSource).
  916. Exec(ctx); err != nil {
  917. logger.LegacyPrintf("service.auth", "[Auth] Failed to update signup source: user_id=%d source=%s err=%v", userID, signupSource, err)
  918. }
  919. }
  920. func (s *AuthService) touchUserLogin(ctx context.Context, userID int64) {
  921. if s == nil || s.entClient == nil || userID <= 0 {
  922. return
  923. }
  924. now := time.Now().UTC()
  925. if err := s.entClient.User.UpdateOneID(userID).
  926. SetLastLoginAt(now).
  927. SetLastActiveAt(now).
  928. Exec(ctx); err != nil {
  929. logger.LegacyPrintf("service.auth", "[Auth] Failed to touch login timestamps: user_id=%d err=%v", userID, err)
  930. }
  931. }
  932. func (s *AuthService) backfillEmailIdentityOnSuccessfulLogin(ctx context.Context, user *User) {
  933. if s == nil || user == nil || user.ID <= 0 {
  934. return
  935. }
  936. identity, created := s.ensureEmailAuthIdentity(ctx, user, "auth_service_login_backfill")
  937. if s.shouldApplyEmailFirstBindDefaults(ctx, user.ID, identity, created) {
  938. if err := s.ApplyProviderDefaultSettingsOnFirstBind(ctx, user.ID, "email"); err != nil {
  939. logger.LegacyPrintf("service.auth", "[Auth] Failed to apply email first bind defaults: user_id=%d err=%v", user.ID, err)
  940. }
  941. }
  942. }
  943. func (s *AuthService) shouldApplyEmailFirstBindDefaults(
  944. ctx context.Context,
  945. userID int64,
  946. identity *dbent.AuthIdentity,
  947. created bool,
  948. ) bool {
  949. source := emailAuthIdentitySource(identity.Metadata)
  950. if source == "auth_service_login_backfill" {
  951. return false
  952. }
  953. if created {
  954. return true
  955. }
  956. if s == nil || s.entClient == nil || userID <= 0 || identity == nil || identity.UserID != userID {
  957. return false
  958. }
  959. if source != "auth_service_dual_write" {
  960. return false
  961. }
  962. hasGrant, err := s.hasProviderGrantRecord(ctx, userID, "email", "first_bind")
  963. if err != nil {
  964. logger.LegacyPrintf("service.auth", "[Auth] Failed to inspect email first bind grant state: user_id=%d err=%v", userID, err)
  965. return false
  966. }
  967. return !hasGrant
  968. }
  969. func emailAuthIdentitySource(metadata map[string]any) string {
  970. if len(metadata) == 0 {
  971. return ""
  972. }
  973. raw, ok := metadata["source"]
  974. if !ok {
  975. return ""
  976. }
  977. return strings.TrimSpace(fmt.Sprint(raw))
  978. }
  979. func (s *AuthService) hasProviderGrantRecord(
  980. ctx context.Context,
  981. userID int64,
  982. providerType string,
  983. grantReason string,
  984. ) (bool, error) {
  985. if s == nil || s.entClient == nil || userID <= 0 {
  986. return false, nil
  987. }
  988. rows, err := s.entClient.QueryContext(
  989. ctx,
  990. `SELECT 1 FROM user_provider_default_grants WHERE user_id = $1 AND provider_type = $2 AND grant_reason = $3 LIMIT 1`,
  991. userID,
  992. strings.TrimSpace(providerType),
  993. strings.TrimSpace(grantReason),
  994. )
  995. if err != nil {
  996. return false, err
  997. }
  998. defer func() { _ = rows.Close() }()
  999. return rows.Next(), rows.Err()
  1000. }
  1001. func (s *AuthService) ensureEmailAuthIdentity(ctx context.Context, user *User, source string) (*dbent.AuthIdentity, bool) {
  1002. if s == nil || s.entClient == nil || user == nil || user.ID <= 0 {
  1003. return nil, false
  1004. }
  1005. email := strings.ToLower(strings.TrimSpace(user.Email))
  1006. if email == "" || isReservedEmail(email) {
  1007. return nil, false
  1008. }
  1009. if strings.TrimSpace(source) == "" {
  1010. source = "auth_service_dual_write"
  1011. }
  1012. client := s.entClient
  1013. if tx := dbent.TxFromContext(ctx); tx != nil {
  1014. client = tx.Client()
  1015. }
  1016. buildQuery := func() *dbent.AuthIdentityQuery {
  1017. return client.AuthIdentity.Query().Where(
  1018. authidentity.ProviderTypeEQ("email"),
  1019. authidentity.ProviderKeyEQ("email"),
  1020. authidentity.ProviderSubjectEQ(email),
  1021. )
  1022. }
  1023. existed, err := buildQuery().Exist(ctx)
  1024. if err != nil {
  1025. logger.LegacyPrintf("service.auth", "[Auth] Failed to inspect email auth identity: user_id=%d email=%s err=%v", user.ID, email, err)
  1026. return nil, false
  1027. }
  1028. if !existed {
  1029. if err = client.AuthIdentity.Create().
  1030. SetUserID(user.ID).
  1031. SetProviderType("email").
  1032. SetProviderKey("email").
  1033. SetProviderSubject(email).
  1034. SetVerifiedAt(time.Now().UTC()).
  1035. SetMetadata(map[string]any{
  1036. "source": strings.TrimSpace(source),
  1037. }).
  1038. OnConflictColumns(
  1039. authidentity.FieldProviderType,
  1040. authidentity.FieldProviderKey,
  1041. authidentity.FieldProviderSubject,
  1042. ).
  1043. DoNothing().
  1044. Exec(ctx); err != nil {
  1045. if isSQLNoRowsError(err) {
  1046. return nil, false
  1047. }
  1048. }
  1049. if err != nil {
  1050. logger.LegacyPrintf("service.auth", "[Auth] Failed to ensure email auth identity: user_id=%d email=%s err=%v", user.ID, email, err)
  1051. return nil, false
  1052. }
  1053. }
  1054. identity, err := buildQuery().Only(ctx)
  1055. if err != nil {
  1056. logger.LegacyPrintf("service.auth", "[Auth] Failed to reload email auth identity: user_id=%d email=%s err=%v", user.ID, email, err)
  1057. return nil, false
  1058. }
  1059. if identity.UserID != user.ID {
  1060. logger.LegacyPrintf("service.auth", "[Auth] Email auth identity ownership mismatch: user_id=%d email=%s owner_id=%d", user.ID, email, identity.UserID)
  1061. return nil, false
  1062. }
  1063. return identity, !existed
  1064. }
  1065. func inferLegacySignupSource(email string) string {
  1066. normalized := strings.ToLower(strings.TrimSpace(email))
  1067. switch {
  1068. case strings.HasSuffix(normalized, DingTalkConnectSyntheticEmailDomain):
  1069. return "dingtalk"
  1070. case strings.HasSuffix(normalized, LinuxDoConnectSyntheticEmailDomain):
  1071. return "linuxdo"
  1072. case strings.HasSuffix(normalized, OIDCConnectSyntheticEmailDomain):
  1073. return "oidc"
  1074. case strings.HasSuffix(normalized, WeChatConnectSyntheticEmailDomain):
  1075. return "wechat"
  1076. default:
  1077. return "email"
  1078. }
  1079. }
  1080. func (s *AuthService) validateRegistrationEmailPolicy(ctx context.Context, email string) error {
  1081. if s.settingService == nil {
  1082. return nil
  1083. }
  1084. whitelist := s.settingService.GetRegistrationEmailSuffixWhitelist(ctx)
  1085. if !IsRegistrationEmailSuffixAllowed(email, whitelist) {
  1086. return buildEmailSuffixNotAllowedError(whitelist)
  1087. }
  1088. return nil
  1089. }
  1090. // validateRegistrationEmailQuota 保留白名单为空时的全放行行为;配置白名单后,
  1091. // 非白名单域名默认直接拒绝(严格白名单模式);仅当域名限量注册开关开启时,
  1092. // 非白名单域名每个最多允许一个账户。
  1093. func (s *AuthService) validateRegistrationEmailQuota(ctx context.Context, email string) error {
  1094. if s.settingService == nil {
  1095. return nil
  1096. }
  1097. whitelist := s.settingService.GetRegistrationEmailSuffixWhitelist(ctx)
  1098. if !IsRegistrationEmailSuffixLimited(email, whitelist) {
  1099. return nil
  1100. }
  1101. if !s.settingService.IsRegistrationEmailDomainQuotaEnabled(ctx) {
  1102. return buildEmailSuffixNotAllowedError(whitelist)
  1103. }
  1104. domain := RegistrationEmailDomain(email)
  1105. if domain == "" {
  1106. return buildEmailSuffixNotAllowedError(whitelist)
  1107. }
  1108. quotaRepo, ok := s.userRepo.(RegistrationEmailDomainRepository)
  1109. if !ok {
  1110. // 生产装配必须提供原子仓储能力;没有数据库的 unit 测试桩保留旧路径,
  1111. // 避免无关测试被注册专用依赖干扰。
  1112. if s.entClient != nil {
  1113. return ErrServiceUnavailable
  1114. }
  1115. return nil
  1116. }
  1117. count, err := quotaRepo.CountUsersByEmailDomain(ctx, domain)
  1118. if err != nil {
  1119. logger.LegacyPrintf("service.auth", "[Auth] Failed to count registration email domain %s: %v", domain, err)
  1120. return ErrServiceUnavailable
  1121. }
  1122. if count > 0 {
  1123. return ErrEmailDomainRegistrationLimit
  1124. }
  1125. return nil
  1126. }
  1127. func (s *AuthService) createUserWithRegistrationEmailGuard(ctx context.Context, user *User) error {
  1128. if s == nil || s.userRepo == nil {
  1129. return ErrServiceUnavailable
  1130. }
  1131. whitelist := []string{}
  1132. if s.settingService != nil {
  1133. whitelist = s.settingService.GetRegistrationEmailSuffixWhitelist(ctx)
  1134. }
  1135. domain := RegistrationEmailDomain(user.Email)
  1136. if !IsRegistrationEmailSuffixLimited(user.Email, whitelist) {
  1137. return s.userRepo.CreateWithEmailAliasGuard(ctx, user)
  1138. }
  1139. // 开关关闭时非白名单域名在校验阶段已被拒绝;此处兜底防御设置竞态变更。
  1140. if s.settingService == nil || !s.settingService.IsRegistrationEmailDomainQuotaEnabled(ctx) {
  1141. return buildEmailSuffixNotAllowedError(whitelist)
  1142. }
  1143. if domain == "" {
  1144. return buildEmailSuffixNotAllowedError(whitelist)
  1145. }
  1146. quotaRepo, ok := s.userRepo.(RegistrationEmailDomainRepository)
  1147. if !ok {
  1148. if s.entClient != nil {
  1149. return ErrServiceUnavailable
  1150. }
  1151. return s.userRepo.CreateWithEmailAliasGuard(ctx, user)
  1152. }
  1153. return quotaRepo.CreateWithEmailAliasGuardAndDomainLimit(ctx, user, domain)
  1154. }
  1155. // createUserAndClaimInvitation 原子化完成“用户创建 + 邀请码占用”。
  1156. //
  1157. // 背景:邀请码属于一次性凭证,必须保证“一个邀请码最多注册一个账号”。旧实现先检查
  1158. // CanUse()、再创建用户、最后才 redeemRepo.Use()(且失败仅记日志),检查与消耗分离且
  1159. // 不在同一事务,并发注册可在同一邀请码上同时通过检查并各自创建账号(TOCTOU 竞态)。
  1160. //
  1161. // 本实现把两者放入同一个数据库事务:
  1162. // - 占用走 redeemRepo.Use 的条件更新(WHERE status='unused',乐观锁);
  1163. // - 并发下只有一个事务能占用成功,其余事务回滚——既不产生多余账号,也不让码被烧掉;
  1164. // - 事务回滚同时撤销用户创建,避免“账号已建、码被占用”的中间态。
  1165. //
  1166. // 无邀请码时保持原单次创建路径(不开事务);entClient 缺失的异常配置下退化为顺序执行,
  1167. // 并发正确性仍由 Use 的条件更新兜底(可能产生孤儿用户,但不会放行第二个注册)。
  1168. func (s *AuthService) createUserAndClaimInvitation(ctx context.Context, user *User, invitation *RedeemCode) error {
  1169. commitUser := func(execCtx context.Context) error {
  1170. if err := s.createUserWithRegistrationEmailGuard(execCtx, user); err != nil {
  1171. return err
  1172. }
  1173. if invitation == nil {
  1174. return nil
  1175. }
  1176. // createUserWithRegistrationEmailGuard 会回填 user.ID(applyUserEntityToService),
  1177. // 直接以其原子占用邀请码;占用失败即整体回滚(含用户创建,见 user_repo.create
  1178. // 对外部事务的复用)。
  1179. if err := s.redeemRepo.Use(execCtx, invitation.ID, user.ID); err != nil {
  1180. // 并发下唯一的合法失败路径:另一个注册已占用该码
  1181. logger.LegacyPrintf("service.auth",
  1182. "[Auth] Rejected registration: invitation code %s already claimed (user_id=%d err=%v)",
  1183. invitation.Code, user.ID, err)
  1184. return ErrInvitationCodeInvalid
  1185. }
  1186. return nil
  1187. }
  1188. if invitation == nil {
  1189. return commitUser(ctx)
  1190. }
  1191. if s.entClient == nil {
  1192. return commitUser(ctx)
  1193. }
  1194. tx, err := s.entClient.Tx(ctx)
  1195. if err != nil {
  1196. logger.LegacyPrintf("service.auth", "[Auth] Failed to start registration transaction: %v", err)
  1197. return ErrServiceUnavailable
  1198. }
  1199. defer func() { _ = tx.Rollback() }()
  1200. execCtx := dbent.NewTxContext(ctx, tx)
  1201. if err := commitUser(execCtx); err != nil {
  1202. return err
  1203. }
  1204. if err := tx.Commit(); err != nil {
  1205. logger.LegacyPrintf("service.auth", "[Auth] Failed to commit registration transaction: %v", err)
  1206. return ErrServiceUnavailable
  1207. }
  1208. return nil
  1209. }
  1210. func buildEmailSuffixNotAllowedError(whitelist []string) error {
  1211. if len(whitelist) == 0 {
  1212. return ErrEmailSuffixNotAllowed
  1213. }
  1214. allowed := strings.Join(whitelist, ", ")
  1215. return infraerrors.BadRequest(
  1216. "EMAIL_SUFFIX_NOT_ALLOWED",
  1217. fmt.Sprintf("email suffix is not allowed, allowed suffixes: %s", allowed),
  1218. ).WithMetadata(map[string]string{
  1219. "allowed_suffixes": strings.Join(whitelist, ","),
  1220. "allowed_suffix_count": strconv.Itoa(len(whitelist)),
  1221. })
  1222. }
  1223. // ValidateToken 验证JWT token并返回用户声明
  1224. func (s *AuthService) ValidateToken(tokenString string) (*JWTClaims, error) {
  1225. // 先做长度校验,尽早拒绝异常超长 token,降低 DoS 风险。
  1226. if len(tokenString) > maxTokenLength {
  1227. return nil, ErrTokenTooLarge
  1228. }
  1229. // 使用解析器并限制可接受的签名算法,防止算法混淆。
  1230. parser := jwt.NewParser(jwt.WithValidMethods([]string{
  1231. jwt.SigningMethodHS256.Name,
  1232. jwt.SigningMethodHS384.Name,
  1233. jwt.SigningMethodHS512.Name,
  1234. }))
  1235. // 保留默认 claims 校验(exp/nbf),避免放行过期或未生效的 token。
  1236. token, err := parser.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (any, error) {
  1237. // 验证签名方法
  1238. if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
  1239. return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
  1240. }
  1241. return []byte(s.cfg.JWT.Secret), nil
  1242. })
  1243. if err != nil {
  1244. if errors.Is(err, jwt.ErrTokenExpired) {
  1245. // token 过期但仍返回 claims(用于 RefreshToken 等场景)
  1246. // jwt-go 在解析时即使遇到过期错误,token.Claims 仍会被填充
  1247. if claims, ok := token.Claims.(*JWTClaims); ok {
  1248. return claims, ErrTokenExpired
  1249. }
  1250. return nil, ErrTokenExpired
  1251. }
  1252. return nil, ErrInvalidToken
  1253. }
  1254. if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
  1255. return claims, nil
  1256. }
  1257. return nil, ErrInvalidToken
  1258. }
  1259. func randomHexString(byteLength int) (string, error) {
  1260. if byteLength <= 0 {
  1261. byteLength = 16
  1262. }
  1263. buf := make([]byte, byteLength)
  1264. if _, err := rand.Read(buf); err != nil {
  1265. return "", err
  1266. }
  1267. return hex.EncodeToString(buf), nil
  1268. }
  1269. func isReservedEmail(email string) bool {
  1270. normalized := strings.ToLower(strings.TrimSpace(email))
  1271. return strings.HasSuffix(normalized, LinuxDoConnectSyntheticEmailDomain) ||
  1272. strings.HasSuffix(normalized, OIDCConnectSyntheticEmailDomain) ||
  1273. strings.HasSuffix(normalized, WeChatConnectSyntheticEmailDomain) ||
  1274. strings.HasSuffix(normalized, DingTalkConnectSyntheticEmailDomain)
  1275. }
  1276. // GenerateToken 生成JWT access token
  1277. // 使用新的access_token_expire_minutes配置项(如果配置了),否则回退到expire_hour。
  1278. // 会话指纹(IP/UA)从 ctx 中提取(由 HTTP 入口中间件注入),缺失时生成不带绑定的 token。
  1279. func (s *AuthService) GenerateToken(ctx context.Context, user *User) (string, error) {
  1280. sessionID, err := randomHexString(8)
  1281. if err != nil {
  1282. return "", fmt.Errorf("generate session id: %w", err)
  1283. }
  1284. return s.generateAccessToken(user, sessionID, sessionBindingHashFromContext(ctx))
  1285. }
  1286. // generateAccessToken 生成带会话 ID 与绑定指纹的 access token。
  1287. func (s *AuthService) generateAccessToken(user *User, sessionID, bindingHash string) (string, error) {
  1288. now := time.Now()
  1289. var expiresAt time.Time
  1290. if s.cfg.JWT.AccessTokenExpireMinutes > 0 {
  1291. expiresAt = now.Add(time.Duration(s.cfg.JWT.AccessTokenExpireMinutes) * time.Minute)
  1292. } else {
  1293. // 向后兼容:使用旧的expire_hour配置
  1294. expiresAt = now.Add(time.Duration(s.cfg.JWT.ExpireHour) * time.Hour)
  1295. }
  1296. claims := &JWTClaims{
  1297. UserID: user.ID,
  1298. Email: user.Email,
  1299. Role: user.Role,
  1300. TokenVersion: resolvedTokenVersion(user),
  1301. SessionID: sessionID,
  1302. BindingHash: bindingHash,
  1303. RegisteredClaims: jwt.RegisteredClaims{
  1304. ExpiresAt: jwt.NewNumericDate(expiresAt),
  1305. IssuedAt: jwt.NewNumericDate(now),
  1306. NotBefore: jwt.NewNumericDate(now),
  1307. },
  1308. }
  1309. token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
  1310. tokenString, err := token.SignedString([]byte(s.cfg.JWT.Secret))
  1311. if err != nil {
  1312. return "", fmt.Errorf("sign token: %w", err)
  1313. }
  1314. return tokenString, nil
  1315. }
  1316. // GetAccessTokenExpiresIn 返回Access Token的有效期(秒)
  1317. // 用于前端设置刷新定时器
  1318. func (s *AuthService) GetAccessTokenExpiresIn() int {
  1319. if s.cfg.JWT.AccessTokenExpireMinutes > 0 {
  1320. return s.cfg.JWT.AccessTokenExpireMinutes * 60
  1321. }
  1322. return s.cfg.JWT.ExpireHour * 3600
  1323. }
  1324. // HashPassword 使用bcrypt加密密码
  1325. func (s *AuthService) HashPassword(password string) (string, error) {
  1326. hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
  1327. if err != nil {
  1328. return "", err
  1329. }
  1330. return string(hashedBytes), nil
  1331. }
  1332. // CheckPassword 验证密码是否匹配
  1333. func (s *AuthService) CheckPassword(password, hashedPassword string) bool {
  1334. err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
  1335. return err == nil
  1336. }
  1337. // RefreshToken 刷新token
  1338. func (s *AuthService) RefreshToken(ctx context.Context, oldTokenString string) (string, error) {
  1339. // 验证旧token(即使过期也允许,用于刷新)
  1340. claims, err := s.ValidateToken(oldTokenString)
  1341. if err != nil && !errors.Is(err, ErrTokenExpired) {
  1342. return "", err
  1343. }
  1344. // 获取最新的用户信息
  1345. user, err := s.userRepo.GetByID(ctx, claims.UserID)
  1346. if err != nil {
  1347. if errors.Is(err, ErrUserNotFound) {
  1348. return "", ErrInvalidToken
  1349. }
  1350. logger.LegacyPrintf("service.auth", "[Auth] Database error refreshing token: %v", err)
  1351. return "", ErrServiceUnavailable
  1352. }
  1353. // 检查用户状态
  1354. if !user.IsActive() {
  1355. return "", ErrUserNotActive
  1356. }
  1357. // Security: Check TokenVersion to prevent refreshing revoked tokens
  1358. // This ensures tokens issued before a password change cannot be refreshed
  1359. if claims.TokenVersion != resolvedTokenVersion(user) {
  1360. return "", ErrTokenRevoked
  1361. }
  1362. // 会话绑定检查:指纹变化的旧 token 不允许换发新 token。
  1363. if s.settingService != nil && s.settingService.IsSessionBindingEnabled(ctx) && claims.BindingHash != "" {
  1364. if current := sessionBindingHashFromContext(ctx); current != "" && current != claims.BindingHash {
  1365. _ = s.RevokeSessionFamily(ctx, claims.SessionID)
  1366. return "", ErrSessionBindingMismatch
  1367. }
  1368. }
  1369. // 生成新token
  1370. return s.GenerateToken(ctx, user)
  1371. }
  1372. // IsPasswordResetEnabled 检查是否启用密码重置功能
  1373. // 要求:必须同时开启邮件验证且 SMTP 配置正确
  1374. func (s *AuthService) IsPasswordResetEnabled(ctx context.Context) bool {
  1375. if s.settingService == nil {
  1376. return false
  1377. }
  1378. // Must have email verification enabled and SMTP configured
  1379. if !s.settingService.IsEmailVerifyEnabled(ctx) {
  1380. return false
  1381. }
  1382. return s.settingService.IsPasswordResetEnabled(ctx)
  1383. }
  1384. // preparePasswordReset validates the password reset request and returns necessary data
  1385. // Returns (siteName, resetURL, shouldProceed)
  1386. // shouldProceed is false when we should silently return success (to prevent enumeration)
  1387. func (s *AuthService) preparePasswordReset(ctx context.Context, email, frontendBaseURL string) (string, string, bool) {
  1388. // Check if user exists (but don't reveal this to the caller)
  1389. user, err := s.userRepo.GetByEmail(ctx, email)
  1390. if err != nil {
  1391. if errors.Is(err, ErrUserNotFound) {
  1392. // Security: Log but don't reveal that user doesn't exist
  1393. logger.LegacyPrintf("service.auth", "[Auth] Password reset requested for non-existent email: %s", email)
  1394. return "", "", false
  1395. }
  1396. logger.LegacyPrintf("service.auth", "[Auth] Database error checking email for password reset: %v", err)
  1397. return "", "", false
  1398. }
  1399. // Check if user is active
  1400. if !user.IsActive() {
  1401. logger.LegacyPrintf("service.auth", "[Auth] Password reset requested for inactive user: %s", email)
  1402. return "", "", false
  1403. }
  1404. // Get site name
  1405. siteName := "Sub2API"
  1406. if s.settingService != nil {
  1407. siteName = s.settingService.GetSiteName(ctx)
  1408. }
  1409. // Build reset URL base
  1410. resetURL := fmt.Sprintf("%s/reset-password", strings.TrimSuffix(frontendBaseURL, "/"))
  1411. return siteName, resetURL, true
  1412. }
  1413. // RequestPasswordReset 请求密码重置(同步发送)
  1414. // Security: Returns the same response regardless of whether the email exists (prevent user enumeration)
  1415. func (s *AuthService) RequestPasswordReset(ctx context.Context, email, frontendBaseURL string, locale ...string) error {
  1416. if !s.IsPasswordResetEnabled(ctx) {
  1417. return infraerrors.Forbidden("PASSWORD_RESET_DISABLED", "password reset is not enabled")
  1418. }
  1419. if s.emailService == nil {
  1420. return ErrServiceUnavailable
  1421. }
  1422. siteName, resetURL, shouldProceed := s.preparePasswordReset(ctx, email, frontendBaseURL)
  1423. if !shouldProceed {
  1424. return nil // Silent success to prevent enumeration
  1425. }
  1426. if err := s.emailService.SendPasswordResetEmail(ctx, email, siteName, resetURL, firstEmailLocale(locale)); err != nil {
  1427. logger.LegacyPrintf("service.auth", "[Auth] Failed to send password reset email to %s: %v", email, err)
  1428. return nil // Silent success to prevent enumeration
  1429. }
  1430. logger.LegacyPrintf("service.auth", "[Auth] Password reset email sent to: %s", email)
  1431. return nil
  1432. }
  1433. // RequestPasswordResetAsync 异步请求密码重置(队列发送)
  1434. // Security: Returns the same response regardless of whether the email exists (prevent user enumeration)
  1435. func (s *AuthService) RequestPasswordResetAsync(ctx context.Context, email, frontendBaseURL string, locale ...string) error {
  1436. if !s.IsPasswordResetEnabled(ctx) {
  1437. return infraerrors.Forbidden("PASSWORD_RESET_DISABLED", "password reset is not enabled")
  1438. }
  1439. if s.emailQueueService == nil {
  1440. return ErrServiceUnavailable
  1441. }
  1442. siteName, resetURL, shouldProceed := s.preparePasswordReset(ctx, email, frontendBaseURL)
  1443. if !shouldProceed {
  1444. return nil // Silent success to prevent enumeration
  1445. }
  1446. if err := s.emailQueueService.EnqueuePasswordReset(email, siteName, resetURL, firstEmailLocale(locale)); err != nil {
  1447. logger.LegacyPrintf("service.auth", "[Auth] Failed to enqueue password reset email for %s: %v", email, err)
  1448. return nil // Silent success to prevent enumeration
  1449. }
  1450. logger.LegacyPrintf("service.auth", "[Auth] Password reset email enqueued for: %s", email)
  1451. return nil
  1452. }
  1453. // ResetPassword 重置密码
  1454. // Security: Increments TokenVersion to invalidate all existing JWT tokens
  1455. func (s *AuthService) ResetPassword(ctx context.Context, email, token, newPassword string) error {
  1456. // Check if password reset is enabled
  1457. if !s.IsPasswordResetEnabled(ctx) {
  1458. return infraerrors.Forbidden("PASSWORD_RESET_DISABLED", "password reset is not enabled")
  1459. }
  1460. if s.emailService == nil {
  1461. return ErrServiceUnavailable
  1462. }
  1463. // Verify and consume the reset token (one-time use)
  1464. if err := s.emailService.ConsumePasswordResetToken(ctx, email, token); err != nil {
  1465. return err
  1466. }
  1467. // Get user
  1468. user, err := s.userRepo.GetByEmail(ctx, email)
  1469. if err != nil {
  1470. if errors.Is(err, ErrUserNotFound) {
  1471. return ErrInvalidResetToken // Token was valid but user was deleted
  1472. }
  1473. logger.LegacyPrintf("service.auth", "[Auth] Database error getting user for password reset: %v", err)
  1474. return ErrServiceUnavailable
  1475. }
  1476. // Check if user is active
  1477. if !user.IsActive() {
  1478. return ErrUserNotActive
  1479. }
  1480. // Hash new password
  1481. hashedPassword, err := s.HashPassword(newPassword)
  1482. if err != nil {
  1483. return fmt.Errorf("hash password: %w", err)
  1484. }
  1485. // Update password and increment TokenVersion
  1486. user.PasswordHash = hashedPassword
  1487. user.TokenVersion++ // Invalidate all existing tokens
  1488. // TokenVersion 无对应数据库列(见 resolvedTokenVersion:由 email+password_hash 指纹推导),
  1489. // 写回 password_hash 本身即可让旧 token 失效。
  1490. if err := s.userRepo.Update(ctx, user, UserUpdateFields{PasswordHash: true}); err != nil {
  1491. logger.LegacyPrintf("service.auth", "[Auth] Database error updating password for user %d: %v", user.ID, err)
  1492. return ErrServiceUnavailable
  1493. }
  1494. // Also revoke all refresh tokens for this user
  1495. if err := s.RevokeAllUserSessions(ctx, user.ID); err != nil {
  1496. logger.LegacyPrintf("service.auth", "[Auth] Failed to revoke refresh tokens for user %d: %v", user.ID, err)
  1497. // Don't return error - password was already changed successfully
  1498. }
  1499. logger.LegacyPrintf("service.auth", "[Auth] Password reset successful for user: %s", email)
  1500. return nil
  1501. }
  1502. // ==================== Refresh Token Methods ====================
  1503. // TokenPair 包含Access Token和Refresh Token
  1504. type TokenPair struct {
  1505. AccessToken string `json:"access_token"`
  1506. RefreshToken string `json:"refresh_token"`
  1507. ExpiresIn int `json:"expires_in"` // Access Token有效期(秒)
  1508. }
  1509. // TokenPairWithUser extends TokenPair with user role for backend mode checks
  1510. type TokenPairWithUser struct {
  1511. TokenPair
  1512. UserRole string
  1513. }
  1514. // GenerateTokenPair 生成Access Token和Refresh Token对
  1515. // familyID: 可选的Token家族ID,用于Token轮转时保持家族关系
  1516. func (s *AuthService) GenerateTokenPair(ctx context.Context, user *User, familyID string) (*TokenPair, error) {
  1517. // 检查 refreshTokenCache 是否可用
  1518. if s.refreshTokenCache == nil {
  1519. return nil, errors.New("refresh token cache not configured")
  1520. }
  1521. // 提前确定家族ID:作为 access token 的会话ID(sid),保证同一会话的
  1522. // access/refresh token 可以互相关联(单会话撤销、step-up 授权绑定)。
  1523. if familyID == "" {
  1524. familyBytes := make([]byte, 16)
  1525. if _, err := rand.Read(familyBytes); err != nil {
  1526. return nil, fmt.Errorf("generate family id: %w", err)
  1527. }
  1528. familyID = hex.EncodeToString(familyBytes)
  1529. }
  1530. // 生成Access Token(携带会话ID与绑定指纹)
  1531. accessToken, err := s.generateAccessToken(user, familyID, sessionBindingHashFromContext(ctx))
  1532. if err != nil {
  1533. return nil, fmt.Errorf("generate access token: %w", err)
  1534. }
  1535. // 生成Refresh Token
  1536. refreshToken, err := s.generateRefreshToken(ctx, user, familyID)
  1537. if err != nil {
  1538. return nil, fmt.Errorf("generate refresh token: %w", err)
  1539. }
  1540. return &TokenPair{
  1541. AccessToken: accessToken,
  1542. RefreshToken: refreshToken,
  1543. ExpiresIn: s.GetAccessTokenExpiresIn(),
  1544. }, nil
  1545. }
  1546. // generateRefreshToken 生成并存储Refresh Token
  1547. func (s *AuthService) generateRefreshToken(ctx context.Context, user *User, familyID string) (string, error) {
  1548. // 生成随机Token
  1549. tokenBytes := make([]byte, 32)
  1550. if _, err := rand.Read(tokenBytes); err != nil {
  1551. return "", fmt.Errorf("generate random bytes: %w", err)
  1552. }
  1553. rawToken := refreshTokenPrefix + hex.EncodeToString(tokenBytes)
  1554. // 计算Token哈希(存储哈希而非原始Token)
  1555. tokenHash := hashToken(rawToken)
  1556. // 如果没有提供familyID,生成新的
  1557. if familyID == "" {
  1558. familyBytes := make([]byte, 16)
  1559. if _, err := rand.Read(familyBytes); err != nil {
  1560. return "", fmt.Errorf("generate family id: %w", err)
  1561. }
  1562. familyID = hex.EncodeToString(familyBytes)
  1563. }
  1564. now := time.Now()
  1565. ttl := time.Duration(s.cfg.JWT.RefreshTokenExpireDays) * 24 * time.Hour
  1566. data := &RefreshTokenData{
  1567. UserID: user.ID,
  1568. TokenVersion: resolvedTokenVersion(user),
  1569. FamilyID: familyID,
  1570. BindingHash: sessionBindingHashFromContext(ctx),
  1571. CreatedAt: now,
  1572. ExpiresAt: now.Add(ttl),
  1573. }
  1574. // 存储Token数据
  1575. if err := s.refreshTokenCache.StoreRefreshToken(ctx, tokenHash, data, ttl); err != nil {
  1576. return "", fmt.Errorf("store refresh token: %w", err)
  1577. }
  1578. // 添加到用户Token集合
  1579. if err := s.refreshTokenCache.AddToUserTokenSet(ctx, user.ID, tokenHash, ttl); err != nil {
  1580. logger.LegacyPrintf("service.auth", "[Auth] Failed to add token to user set: %v", err)
  1581. // 不影响主流程
  1582. }
  1583. // 添加到家族Token集合
  1584. if err := s.refreshTokenCache.AddToFamilyTokenSet(ctx, familyID, tokenHash, ttl); err != nil {
  1585. logger.LegacyPrintf("service.auth", "[Auth] Failed to add token to family set: %v", err)
  1586. // 不影响主流程
  1587. }
  1588. return rawToken, nil
  1589. }
  1590. // RefreshTokenPair 使用Refresh Token刷新Token对
  1591. // 实现Token轮转:每次刷新都会生成新的Refresh Token,旧Token立即失效
  1592. func (s *AuthService) RefreshTokenPair(ctx context.Context, refreshToken string) (*TokenPairWithUser, error) {
  1593. // 检查 refreshTokenCache 是否可用
  1594. if s.refreshTokenCache == nil {
  1595. return nil, ErrRefreshTokenInvalid
  1596. }
  1597. // 验证Token格式
  1598. if !strings.HasPrefix(refreshToken, refreshTokenPrefix) {
  1599. return nil, ErrRefreshTokenInvalid
  1600. }
  1601. tokenHash := hashToken(refreshToken)
  1602. // 获取Token数据
  1603. data, err := s.refreshTokenCache.GetRefreshToken(ctx, tokenHash)
  1604. if err != nil {
  1605. if errors.Is(err, ErrRefreshTokenNotFound) {
  1606. // Token不存在,可能是已被使用(Token轮转)或已过期
  1607. logger.LegacyPrintf("service.auth", "[Auth] Refresh token not found, possible reuse attack")
  1608. return nil, ErrRefreshTokenInvalid
  1609. }
  1610. logger.LegacyPrintf("service.auth", "[Auth] Error getting refresh token: %v", err)
  1611. return nil, ErrServiceUnavailable
  1612. }
  1613. // 检查Token是否过期
  1614. if time.Now().After(data.ExpiresAt) {
  1615. // 删除过期Token
  1616. _ = s.refreshTokenCache.DeleteRefreshToken(ctx, tokenHash)
  1617. return nil, ErrRefreshTokenExpired
  1618. }
  1619. // 获取用户信息
  1620. user, err := s.userRepo.GetByID(ctx, data.UserID)
  1621. if err != nil {
  1622. if errors.Is(err, ErrUserNotFound) {
  1623. // 用户已删除,撤销整个Token家族
  1624. _ = s.refreshTokenCache.DeleteTokenFamily(ctx, data.FamilyID)
  1625. return nil, ErrRefreshTokenInvalid
  1626. }
  1627. logger.LegacyPrintf("service.auth", "[Auth] Database error getting user for token refresh: %v", err)
  1628. return nil, ErrServiceUnavailable
  1629. }
  1630. // 检查用户状态
  1631. if !user.IsActive() {
  1632. // 用户被禁用,撤销整个Token家族
  1633. _ = s.refreshTokenCache.DeleteTokenFamily(ctx, data.FamilyID)
  1634. return nil, ErrUserNotActive
  1635. }
  1636. // 检查TokenVersion(密码更改后所有Token失效)
  1637. if data.TokenVersion != resolvedTokenVersion(user) {
  1638. // TokenVersion不匹配,撤销整个Token家族
  1639. _ = s.refreshTokenCache.DeleteTokenFamily(ctx, data.FamilyID)
  1640. return nil, ErrTokenRevoked
  1641. }
  1642. // 会话绑定检查:IP/UA 任一变化即撤销整个会话家族。
  1643. // data.BindingHash 为空表示功能开启前签发的旧会话,放行并在轮转时补齐绑定。
  1644. if s.settingService != nil && s.settingService.IsSessionBindingEnabled(ctx) && data.BindingHash != "" {
  1645. if current := sessionBindingHashFromContext(ctx); current != "" && current != data.BindingHash {
  1646. _ = s.refreshTokenCache.DeleteTokenFamily(ctx, data.FamilyID)
  1647. logger.LegacyPrintf("service.auth", "[Auth] Session binding mismatch on refresh for user %d, family revoked", data.UserID)
  1648. return nil, ErrSessionBindingMismatch
  1649. }
  1650. }
  1651. // Token轮转:立即使旧Token失效
  1652. if err := s.refreshTokenCache.DeleteRefreshToken(ctx, tokenHash); err != nil {
  1653. logger.LegacyPrintf("service.auth", "[Auth] Failed to delete old refresh token: %v", err)
  1654. // 继续处理,不影响主流程
  1655. }
  1656. // 生成新的Token对,保持同一个家族ID
  1657. pair, err := s.GenerateTokenPair(ctx, user, data.FamilyID)
  1658. if err != nil {
  1659. return nil, err
  1660. }
  1661. return &TokenPairWithUser{
  1662. TokenPair: *pair,
  1663. UserRole: user.Role,
  1664. }, nil
  1665. }
  1666. // RevokeRefreshToken 撤销单个Refresh Token
  1667. func (s *AuthService) RevokeRefreshToken(ctx context.Context, refreshToken string) error {
  1668. if s.refreshTokenCache == nil {
  1669. return nil // No-op if cache not configured
  1670. }
  1671. if !strings.HasPrefix(refreshToken, refreshTokenPrefix) {
  1672. return ErrRefreshTokenInvalid
  1673. }
  1674. tokenHash := hashToken(refreshToken)
  1675. return s.refreshTokenCache.DeleteRefreshToken(ctx, tokenHash)
  1676. }
  1677. // RevokeSessionFamily 撤销单个会话家族(该会话的所有 refresh token)。
  1678. // 用于会话绑定失效等单会话级撤销场景,不影响用户的其他设备会话。
  1679. func (s *AuthService) RevokeSessionFamily(ctx context.Context, familyID string) error {
  1680. if s.refreshTokenCache == nil || familyID == "" {
  1681. return nil
  1682. }
  1683. return s.refreshTokenCache.DeleteTokenFamily(ctx, familyID)
  1684. }
  1685. // RevokeAllUserSessions 撤销用户的所有会话(所有Refresh Token)
  1686. // 用于密码更改或用户主动登出所有设备
  1687. func (s *AuthService) RevokeAllUserSessions(ctx context.Context, userID int64) error {
  1688. if s.refreshTokenCache == nil {
  1689. return nil // No-op if cache not configured
  1690. }
  1691. return s.refreshTokenCache.DeleteUserRefreshTokens(ctx, userID)
  1692. }
  1693. // RevokeAllUserTokens invalidates both stateless access tokens and refresh sessions.
  1694. //
  1695. // 注意:users 表没有 token_version 列(resolvedTokenVersion 由 email+password_hash
  1696. // 指纹推导),因此对 user.TokenVersion 自增只影响内存副本。之前紧跟其后的整行
  1697. // Update 不写任何有效数据,却会用旧快照覆盖并发写入的列,故已移除。
  1698. // 会话撤销由下面的 refresh session 清理承担;改密路径通过 password_hash 变化
  1699. // 改变指纹,从而使旧 token 失效。
  1700. func (s *AuthService) RevokeAllUserTokens(ctx context.Context, userID int64) error {
  1701. if _, err := s.userRepo.GetByID(ctx, userID); err != nil {
  1702. return fmt.Errorf("get user: %w", err)
  1703. }
  1704. if err := s.RevokeAllUserSessions(ctx, userID); err != nil {
  1705. logger.LegacyPrintf("service.auth", "[Auth] Failed to revoke refresh sessions after token invalidation for user %d: %v", userID, err)
  1706. }
  1707. return nil
  1708. }
  1709. // hashToken 计算Token的SHA256哈希
  1710. func hashToken(token string) string {
  1711. hash := sha256.Sum256([]byte(token))
  1712. return hex.EncodeToString(hash[:])
  1713. }
  1714. func resolvedTokenVersion(user *User) int64 {
  1715. if user == nil {
  1716. return 0
  1717. }
  1718. if user.TokenVersionResolved {
  1719. return user.TokenVersion
  1720. }
  1721. material := strings.ToLower(strings.TrimSpace(user.Email)) + "\n" + user.PasswordHash
  1722. sum := sha256.Sum256([]byte(material))
  1723. fingerprint := int64(binary.BigEndian.Uint64(sum[:8]) & 0x7fffffffffffffff)
  1724. return user.TokenVersion ^ fingerprint
  1725. }
  1726. // snapshotPlatformQuotaDefaults 把 plan.PlatformQuotas(platform × 3 window)以
  1727. // BulkInsertInitial 形式写入 user_platform_quotas 表。失败 fail-open(仅 warn log)。
  1728. func (s *AuthService) snapshotPlatformQuotaDefaults(ctx context.Context, userID int64, plan *signupGrantPlan) error {
  1729. if s.userPlatformQuotaRepo == nil || plan == nil || len(plan.PlatformQuotas) == 0 {
  1730. return nil
  1731. }
  1732. // 平台配额快照是 best-effort(fail-open):必须脱离调用方事务执行。
  1733. // 否则某平台违反 user_platform_quotas 的 CHECK 约束(如尚未进约束的新平台)会让
  1734. // 整个调用方事务被 Postgres 标记 aborted,把"无关紧要的默认配额快照"放大成
  1735. // "整笔注册失败"(OAuth pending 路径曾因此 500 → 清 cookie → 404)。
  1736. ctx = dbent.WithoutTx(ctx)
  1737. records := make([]UserPlatformQuotaRecord, 0, len(plan.PlatformQuotas))
  1738. for platform, q := range plan.PlatformQuotas {
  1739. rec := UserPlatformQuotaRecord{
  1740. UserID: userID,
  1741. Platform: platform,
  1742. }
  1743. if q != nil {
  1744. rec.DailyLimitUSD = q.DailyLimitUSD
  1745. rec.WeeklyLimitUSD = q.WeeklyLimitUSD
  1746. rec.MonthlyLimitUSD = q.MonthlyLimitUSD
  1747. }
  1748. records = append(records, rec)
  1749. }
  1750. if err := s.userPlatformQuotaRepo.BulkInsertInitial(ctx, records); err != nil {
  1751. logger.LegacyPrintf("service.auth", "[Auth] Warning: snapshot platform quota failed user=%d: %v (fail-open)", userID, err)
  1752. return nil // fail-open:返回 nil,让调用方继续
  1753. }
  1754. return nil
  1755. }