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

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

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
package app
import (
"errors"
"net"
"strings"
"time"
"yingce/backend/internal/kernel"
"yingce/backend/internal/model"
"yingce/backend/internal/repository"
"gorm.io/gorm"
)
type LoginLogQuery struct{ Keyword, Event, Method, Result, IP, From, To string }
type LoginLogPage struct {
Logs []model.LoginLog `json:"logs"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
func (s *Service) AdminLoginLogs(actor *model.User, query LoginLogQuery, page, pageSize int) (*LoginLogPage, error) {
if err := s.RequireAdmin(actor); err != nil {
return nil, err
}
filter := repository.LoginLogFilter{Keyword: strings.TrimSpace(query.Keyword), Event: strings.TrimSpace(query.Event), Method: strings.TrimSpace(query.Method), Result: strings.TrimSpace(query.Result), IP: strings.TrimSpace(query.IP)}
for _, enum := range []struct {
value string
options []string
}{
{filter.Event, []string{"", "login", "logout"}},
{filter.Result, []string{"", "success", "failed"}},
{filter.Method, []string{"", "password", "email", "sms", "linuxdo", "verification", "unknown"}},
} {
valid := false
for _, option := range enum.options {
if option == enum.value {
valid = true
break
}
}
if !valid {
return nil, kernel.BadAuthRequest("登录日志筛选参数无效")
}
}
if len([]rune(filter.Keyword)) > 160 || (filter.IP != "" && net.ParseIP(filter.IP) == nil) {
return nil, kernel.BadAuthRequest("关键词或 IP 参数无效")
}
for _, bound := range []struct {
raw string
value **time.Time
}{{query.From, &filter.From}, {query.To, &filter.To}} {
if bound.raw == "" {
continue
}
parsed, err := time.Parse(time.RFC3339, bound.raw)
if err != nil {
return nil, kernel.BadAuthRequest("时间必须使用 RFC3339 格式")
}
*bound.value = &parsed
}
if filter.From != nil && filter.To != nil && filter.From.After(*filter.To) {
return nil, kernel.BadAuthRequest("开始时间不能晚于结束时间")
}
page, pageSize = normalizeAdminPage(page, pageSize)
logs, total, err := s.repo.LoginLogs(filter, pageSize, (page-1)*pageSize)
if err != nil {
return nil, err
}
return &LoginLogPage{Logs: logs, Total: total, Page: page, PageSize: pageSize}, nil
}
func (s *Service) AdminLoginLog(actor *model.User, id string) (*model.LoginLog, error) {
if err := s.RequireAdmin(actor); err != nil {
return nil, err
}
event, err := s.repo.LoginLog(strings.TrimSpace(id))
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, kernel.NotFound("登录日志不存在")
}
return event, err
}
func (s *Service) RecordRejectedLogin(method string, context LoginContext, status int) error {
return s.authDomain().RecordRejectedLogin(method, context, status)
}
+5 -4
View File
@@ -14,6 +14,7 @@ type (
EmailCodeCooldownError = auth.EmailCodeCooldownError
RegisterRequest = auth.RegisterRequest
LoginRequest = auth.LoginRequest
LoginContext = auth.LoginContext
PublicAuthSettings = auth.PublicAuthSettings
AuthSessionResult = auth.AuthSessionResult
AuthUser = auth.AuthUser
@@ -138,8 +139,8 @@ func (s *Service) LoginRateLimitSubject(account string) (string, error) {
return s.authDomain().LoginRateLimitSubject(account)
}
func (s *Service) Logout(cookieValue string) error {
return s.authDomain().Logout(cookieValue)
func (s *Service) Logout(cookieValue string, contexts ...LoginContext) error {
return s.authDomain().Logout(cookieValue, contexts...)
}
func (s *Service) CurrentUser(cookieValue string) (*model.User, error) {
@@ -206,8 +207,8 @@ func (s *Service) BeginLinuxDOLogin(nextPath string, acceptedTerms bool) (string
return s.authDomain().BeginLinuxDOLogin(nextPath, acceptedTerms)
}
func (s *Service) CompleteLinuxDOLogin(stateValue string, code string) (*LinuxDOCallbackResult, error) {
return s.authDomain().CompleteLinuxDOLogin(stateValue, code)
func (s *Service) CompleteLinuxDOLogin(stateValue string, code string, cookieValue string, contexts ...LoginContext) (*LinuxDOCallbackResult, error) {
return s.authDomain().CompleteLinuxDOLogin(stateValue, code, cookieValue, contexts...)
}
func (s *Service) AdminLibTVSetting(actor *model.User) (*PublicLibTVSetting, error) {
+33
View File
@@ -0,0 +1,33 @@
package app
import (
"context"
"yingce/backend/internal/auth"
"yingce/backend/internal/model"
)
type UserProfile = auth.UserProfile
type ProfilePasswordRequest = auth.ProfilePasswordRequest
type ProfileEmailCodeRequest = auth.ProfileEmailCodeRequest
type ProfileEmailChangeRequest = auth.ProfileEmailChangeRequest
type ProfileEmailChangeResult = auth.ProfileEmailChangeResult
type VerificationTicket = auth.VerificationTicket
func (s *Service) UserProfile(actor *model.User, cookie string) (*UserProfile, error) {
return s.authDomain().UserProfile(actor, cookie)
}
func (s *Service) ChangeProfilePassword(actor *model.User, cookie string, req ProfilePasswordRequest) error {
return s.authDomain().ChangeProfilePassword(actor, cookie, req)
}
func (s *Service) SendProfileEmailCode(ctx context.Context, actor *model.User, cookie string, req ProfileEmailCodeRequest) (*VerificationTicket, error) {
return s.authDomain().SendProfileEmailCode(ctx, actor, cookie, req)
}
func (s *Service) ChangeProfileEmail(actor *model.User, cookie string, req ProfileEmailChangeRequest) (*ProfileEmailChangeResult, error) {
return s.authDomain().ChangeProfileEmail(actor, cookie, req)
}
func (s *Service) BeginLinuxDOReauthentication(actor *model.User, cookie string) (string, error) {
return s.authDomain().BeginLinuxDOReauthentication(actor, cookie)
}
func (s *Service) LinuxDOReauthenticationState(stateValue string) (bool, error) {
return s.authDomain().LinuxDOReauthenticationState(stateValue)
}
+23
View File
@@ -0,0 +1,23 @@
package app
import (
"mime/multipart"
"yingce/backend/internal/model"
)
func (s *Service) UpdateProfileAvatar(actor *model.User, header *multipart.FileHeader) (*AuthUser, error) {
return s.authDomain().UpdateProfileAvatar(actor, header)
}
func (s *Service) UseDefaultProfileAvatar(actor *model.User) (*AuthUser, error) {
return s.authDomain().UseDefaultProfileAvatar(actor)
}
func (s *Service) SelectDefaultProfileAvatar(actor *model.User, preset string) (*AuthUser, error) {
return s.authDomain().SelectDefaultProfileAvatar(actor, preset)
}
func (s *Service) ProfileAvatarContent(actor *model.User, targetUserID string) ([]byte, error) {
return s.authDomain().ProfileAvatarContent(actor, targetUserID)
}