feat(auth): 用户账户 - 完善个人资料与登录日志 (#752)

Co-authored-by: wuligao <1247662379@qq.com>
This commit is contained in:
ddcat666
2026-10-13 05:31:30 +08:00
committed by GitHub
parent e70fe1b9c9
commit 4c1d2b4012
83 changed files with 5836 additions and 328 deletions

View File

@@ -80,8 +80,9 @@ type linuxDOSettingValue struct {
}
type LinuxDOCallbackResult struct {
Session *AuthSessionResult
Next string
Session *AuthSessionResult
Next string
Reauthenticated bool
}
func (s *Service) AdminLinuxDOSetting(actor *model.User) (*PublicLinuxDOSetting, error) {
@@ -155,14 +156,44 @@ func (s *Service) BeginLinuxDOLogin(nextPath string, acceptedTerms bool) (string
if !setting.Enabled {
return "", kernel.Forbidden("Linux.do 登录尚未启用")
}
return s.beginLinuxDOAuthorization(setting, &model.OAuthState{NextPath: safeOAuthNext(nextPath), AcceptedTerms: acceptedTerms})
}
func (s *Service) BeginLinuxDOReauthentication(actor *model.User, cookie string) (string, error) {
user, session, err := s.profileSession(actor, cookie)
if err != nil {
return "", err
}
if _, err := s.repo.UserIdentityForUser(user.ID, "linuxdo"); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", kernel.Forbidden("该账号没有关联 Linux.do 身份")
}
return "", err
}
_, setting, err := s.readLinuxDOSetting()
if err != nil {
return "", err
}
if !setting.Enabled {
return "", kernel.Forbidden("Linux.do 登录尚未启用")
}
return s.beginLinuxDOAuthorization(setting, &model.OAuthState{NextPath: "/settings?section=profile&reauth=success", ReauthUserID: user.ID, ReauthSessionID: session.ID})
}
func (s *Service) LinuxDOReauthenticationState(stateValue string) (bool, error) {
if strings.TrimSpace(stateValue) == "" {
return false, nil
}
return s.repo.LinuxDOReauthenticationState(HashToken(stateValue))
}
func (s *Service) beginLinuxDOAuthorization(setting linuxDOSettingValue, record *model.OAuthState) (string, error) {
state := RandomToken()
verifier := RandomToken()
challengeBytes := sha256.Sum256([]byte(verifier))
challenge := base64.RawURLEncoding.EncodeToString(challengeBytes[:])
if err := s.repo.CreateOAuthState(&model.OAuthState{
ID: kernel.NewID(), Provider: "linuxdo", StateHash: HashToken(state), CodeVerifier: verifier,
NextPath: safeOAuthNext(nextPath), AcceptedTerms: acceptedTerms, ExpiresAt: time.Now().Add(10 * time.Minute),
}); err != nil {
record.ID, record.Provider, record.StateHash, record.CodeVerifier, record.ExpiresAt = kernel.NewID(), "linuxdo", HashToken(state), verifier, time.Now().Add(10*time.Minute)
if err := s.repo.CreateOAuthState(record); err != nil {
return "", err
}
authorizeURL, err := url.Parse(setting.AuthorizationURL)
@@ -183,7 +214,25 @@ func (s *Service) BeginLinuxDOLogin(nextPath string, acceptedTerms bool) (string
return authorizeURL.String(), nil
}
func (s *Service) CompleteLinuxDOLogin(stateValue string, code string) (*LinuxDOCallbackResult, error) {
func (s *Service) CompleteLinuxDOLogin(stateValue string, code string, cookieValue string, contexts ...LoginContext) (result *LinuxDOCallbackResult, err error) {
var context LoginContext
if len(contexts) > 0 {
context = contexts[0]
}
event := NewLoginLog("linuxdo", "login", context)
logFailure := true
defer func() {
if err != nil && logFailure {
err = s.recordLoginFailure(event, err)
}
}()
if strings.TrimSpace(stateValue) != "" {
reauth, stateErr := s.LinuxDOReauthenticationState(stateValue)
if stateErr != nil {
return nil, stateErr
}
logFailure = !reauth
}
if strings.TrimSpace(stateValue) == "" || strings.TrimSpace(code) == "" {
return nil, kernel.BadAuthRequest("Linux.do 登录回调缺少必要参数")
}
@@ -194,10 +243,28 @@ func (s *Service) CompleteLinuxDOLogin(stateValue string, code string) (*LinuxDO
if err != nil {
return nil, err
}
logFailure = state.ReauthUserID == "" && state.ReauthSessionID == ""
_, setting, err := s.readLinuxDOSetting()
if err != nil {
return nil, err
}
if !setting.Enabled {
return nil, kernel.Forbidden("Linux.do 登录尚未启用")
}
var reauthUser *model.User
var reauthSession *model.AuthSession
if state.ReauthUserID != "" || state.ReauthSessionID != "" {
if cookieValue == "" {
return nil, kernel.Unauthorized("重新验证需要当前登录会话")
}
reauthUser, reauthSession, err = s.profileSession(&model.User{ID: state.ReauthUserID}, cookieValue)
if err != nil {
return nil, err
}
if reauthSession.ID != state.ReauthSessionID {
return nil, kernel.Unauthorized("重新验证与当前登录会话不匹配")
}
}
accessToken, err := exchangeLinuxDOCode(setting, code, state.CodeVerifier)
if err != nil {
return nil, err
@@ -210,6 +277,12 @@ func (s *Service) CompleteLinuxDOLogin(stateValue string, code string) (*LinuxDO
if subject == "" {
return nil, errors.New("Linux.do 用户信息缺少稳定用户 ID")
}
if reauthUser != nil {
if err := s.repo.ReauthenticateLinuxDOSession(reauthUser, reauthSession, subject, time.Now()); err != nil {
return nil, profileWriteError(err)
}
return &LinuxDOCallbackResult{Next: "/settings?section=profile&reauth=success", Reauthenticated: true}, nil
}
providerUsername := profileString(profile, setting.UsernameField)
displayName := kernel.FirstNonEmpty(profileString(profile, setting.DisplayNameField), providerUsername, "Linux.do 用户")
avatarURL := profileString(profile, setting.AvatarField)
@@ -227,6 +300,7 @@ func (s *Service) CompleteLinuxDOLogin(stateValue string, code string) (*LinuxDO
return nil, err
}
} else if errors.Is(err, gorm.ErrRecordNotFound) {
event.Source = "registration"
policy, policyErr := s.verificationPolicy()
if policyErr != nil {
return nil, policyErr
@@ -254,6 +328,7 @@ func (s *Service) CompleteLinuxDOLogin(stateValue string, code string) (*LinuxDO
} else {
return nil, err
}
loginLogUser(event, user)
if user.Status != model.UserStatusActive {
return nil, kernel.Forbidden("该账号已被禁用")
}
@@ -266,8 +341,7 @@ func (s *Service) CompleteLinuxDOLogin(stateValue string, code string) (*LinuxDO
if err := s.repo.UpdateUserLoginTime(user.ID, now); err != nil {
return nil, err
}
s.host.RecordActivity(user.ID, "login", 1)
session, err := s.createAuthSession(user)
session, err := s.createAuthSession(user, event)
if err != nil {
return nil, err
}