feat: update template
This commit is contained in:
42
internal/service/admin/access_log.go
Normal file
42
internal/service/admin/access_log.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"server/internal/db"
|
||||
"server/internal/db/sqlc"
|
||||
"server/internal/model/common"
|
||||
)
|
||||
|
||||
type AccessLogService struct {
|
||||
store *db.Store
|
||||
}
|
||||
|
||||
func NewAccessLogService(store *db.Store) *AccessLogService {
|
||||
return &AccessLogService{
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AccessLogService) List(ctx context.Context, p *common.Pagination) (*common.PageResult[sqlc.ListAccessLogsRow], error) {
|
||||
params := sqlc.ListAccessLogsParams{
|
||||
Limit: p.PageSize,
|
||||
Offset: (p.Page - 1) * p.PageSize,
|
||||
}
|
||||
|
||||
total, err := s.store.CountAccessLogs(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list, err := s.store.ListAccessLogs(ctx, params)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &common.PageResult[sqlc.ListAccessLogsRow]{
|
||||
Total: total,
|
||||
List: list,
|
||||
}, nil
|
||||
}
|
||||
@@ -2,10 +2,13 @@ package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"server/internal/config"
|
||||
"server/internal/db"
|
||||
"server/internal/db/sqlc"
|
||||
"server/internal/middleware"
|
||||
"server/internal/model/auth"
|
||||
"server/internal/model/enum"
|
||||
"server/internal/model/request"
|
||||
"server/internal/model/response"
|
||||
"server/internal/pkg/cache"
|
||||
@@ -13,6 +16,7 @@ import (
|
||||
"server/internal/pkg/errs"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -46,15 +50,31 @@ func comparePasswordHash(passwordHash, inputPassword string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(inputPassword))
|
||||
}
|
||||
|
||||
func (s *AuthService) GetAuthState(ctx context.Context, userID int32) (*sqlc.GetUserAuthStateRow, error) {
|
||||
// 过期时间使用最短窗口的那个 也就是access_token的过期时间
|
||||
return cache.GetOrSetJSON[*sqlc.GetUserAuthStateRow](ctx, s.cache, cachekey.UserAuthState(userID), s.cfg.JWTConfig.Expire, func() (*sqlc.GetUserAuthStateRow, error) {
|
||||
state, err := s.store.GetUserAuthState(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &state, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AuthService) discardRefreshToken(ctx context.Context, userID int32, hash string) {
|
||||
_ = s.cache.Del(ctx, cachekey.AuthRefresh(hash))
|
||||
_ = s.cache.SRem(ctx, cachekey.AuthRefreshUser(userID), hash)
|
||||
}
|
||||
|
||||
func (s *AuthService) Login(ctx context.Context, req request.LoginRequest) (*response.LoginResponse, error) {
|
||||
user, err := s.store.GetUserByAccount(ctx, req.Account)
|
||||
user, err := s.store.GetUser(ctx, sqlc.GetUserParams{Account: req.Account})
|
||||
|
||||
if err != nil {
|
||||
return nil, errs.ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// 此处判断如果用户id不为1 且状态为0表示用户已被禁用
|
||||
if user.ID != 1 && user.Status == 0 {
|
||||
// 判断用户不为超管 且状态为0表示用户已被禁用
|
||||
if !auth.IsAdmin(user.ID) && user.Status == 0 {
|
||||
return nil, errs.ErrUserDisabled
|
||||
}
|
||||
|
||||
@@ -62,14 +82,14 @@ func (s *AuthService) Login(ctx context.Context, req request.LoginRequest) (*res
|
||||
return nil, errs.ErrInvalidCredentials
|
||||
}
|
||||
|
||||
accessToken, accessTokenExp, err := s.jwt.GenerateAccessToken(user.ID)
|
||||
accessToken, accessTokenExp, err := s.jwt.GenerateAccessToken(user.ID, user.TokenVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errs.ErrInternalServer
|
||||
}
|
||||
|
||||
refreshToken, err := s.jwt.GenerateRefreshToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errs.ErrInternalServer
|
||||
}
|
||||
|
||||
// 哈希
|
||||
@@ -79,28 +99,29 @@ func (s *AuthService) Login(ctx context.Context, req request.LoginRequest) (*res
|
||||
|
||||
refreshTokenExp := now.Add(s.cfg.JWTConfig.RefreshExpire)
|
||||
refreshTokenRecord := &auth.RefreshTokenRecord{
|
||||
UserID: user.ID,
|
||||
CreatedAt: now,
|
||||
ExpiresAt: refreshTokenExp,
|
||||
UserID: user.ID,
|
||||
TokenVersion: user.TokenVersion,
|
||||
CreatedAt: now,
|
||||
ExpiresAt: refreshTokenExp,
|
||||
}
|
||||
|
||||
// 存入redis
|
||||
err = s.cache.SetJSON(ctx, cachekey.AuthRefresh(hash), refreshTokenRecord, s.cfg.JWTConfig.RefreshExpire)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errs.ErrInternalServer
|
||||
}
|
||||
|
||||
// 反向索引
|
||||
err = s.cache.SAdd(ctx, cachekey.AuthRefreshUser(user.ID), hash)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errs.ErrInternalServer
|
||||
}
|
||||
|
||||
// 给反向索引设置过期时间 这个过期时间需要覆盖最后一个token的过期时间 所以用最新的就行了
|
||||
if err = s.cache.Expire(ctx, cachekey.AuthRefreshUser(user.ID), s.cfg.JWTConfig.RefreshExpire); err != nil {
|
||||
return nil, err
|
||||
return nil, errs.ErrInternalServer
|
||||
}
|
||||
|
||||
return &response.LoginResponse{
|
||||
@@ -121,26 +142,7 @@ func (s *AuthService) Logout(ctx context.Context, refreshToken string) error {
|
||||
}
|
||||
|
||||
if ok {
|
||||
// 清理反向索引
|
||||
if err = s.cache.SRem(ctx, cachekey.AuthRefreshUser(user.UserID), hash); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 清理当前登录的token
|
||||
if err = s.cache.Del(ctx, cachekey.AuthRefresh(hash)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AuthService) GetActiveSysUser(ctx context.Context, id int32) error {
|
||||
var err error
|
||||
|
||||
_, err = s.store.GetActiveUserByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
s.discardRefreshToken(ctx, user.UserID, hash)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -152,25 +154,45 @@ func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*r
|
||||
// 从redis中获取数据
|
||||
record, ok, err := cache.GetJSON[auth.RefreshTokenRecord](ctx, s.cache, cachekey.AuthRefresh(hash))
|
||||
if err != nil {
|
||||
return nil, errs.ErrInvalidRefreshToken
|
||||
// redis 错误返回500
|
||||
return nil, errs.ErrInternalServer
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return nil, errs.ErrInvalidRefreshToken
|
||||
}
|
||||
|
||||
// 拿到用户信息
|
||||
authState, err := s.GetAuthState(ctx, record.UserID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
// 用户已被删除 惰性清理
|
||||
s.discardRefreshToken(ctx, record.UserID, hash)
|
||||
return nil, errs.ErrInvalidRefreshToken
|
||||
}
|
||||
|
||||
return nil, errs.ErrInternalServer
|
||||
}
|
||||
|
||||
// 如果不是超级用户 需要判断用户状态
|
||||
if record.UserID != 1 {
|
||||
err = s.GetActiveSysUser(ctx, record.UserID)
|
||||
if err != nil {
|
||||
if !auth.IsAdmin(record.UserID) {
|
||||
if enum.Status(authState.Status) == enum.StatusDisabled {
|
||||
// 用户被禁用 清理redis缓存
|
||||
s.discardRefreshToken(ctx, record.UserID, hash)
|
||||
return nil, errs.ErrInvalidRefreshToken
|
||||
}
|
||||
}
|
||||
|
||||
// 比对token version 如果不相等 此时 惰性清理掉redis中的缓存
|
||||
if authState.TokenVersion != record.TokenVersion {
|
||||
s.discardRefreshToken(ctx, record.UserID, hash)
|
||||
return nil, errs.ErrInvalidRefreshToken
|
||||
}
|
||||
|
||||
// 获取新的access token
|
||||
accessToken, accessTokenExp, err := s.jwt.GenerateAccessToken(record.UserID)
|
||||
accessToken, accessTokenExp, err := s.jwt.GenerateAccessToken(record.UserID, authState.TokenVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errs.ErrInternalServer
|
||||
}
|
||||
|
||||
return &response.LoginResponse{
|
||||
|
||||
@@ -2,45 +2,93 @@ package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"image"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"server/internal/config"
|
||||
"server/internal/db"
|
||||
"server/internal/db/sqlc"
|
||||
"server/internal/model/common"
|
||||
"server/internal/model/request"
|
||||
"server/internal/pkg/errs"
|
||||
"server/internal/pkg/httputil"
|
||||
"server/internal/pkg/safego"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
)
|
||||
|
||||
type FileService struct {
|
||||
store *db.Store
|
||||
store *db.Store
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewFileService(store *db.Store) *FileService {
|
||||
func NewFileService(store *db.Store, config *config.Config) *FileService {
|
||||
return &FileService{
|
||||
store: store,
|
||||
store: store,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// MakeSavedDir 创建目录并返回
|
||||
func MakeSavedDir(folder string) (string, error) {
|
||||
func MakeSavedDir(uploadDir, folder string) (string, error) {
|
||||
rootDir, err := os.Getwd()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
uploadDir := filepath.Join(rootDir, "uploads", folder)
|
||||
dir := filepath.Join(rootDir, uploadDir, folder)
|
||||
|
||||
if err = os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
if err = os.MkdirAll(dir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return uploadDir, nil
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
func (s *FileService) List(ctx context.Context, p *common.Pagination) (*common.PageResult[sqlc.File], error) {
|
||||
func MakeFilePath(rootDir string, uploadDir string, filePath string) string {
|
||||
return filepath.Join(rootDir, uploadDir, filePath)
|
||||
}
|
||||
|
||||
// isWebp 判断是否为 WebP 魔数(RIFF+WEBP),DetectContentType 不识别需手动补
|
||||
func isWebp(b []byte) bool {
|
||||
return len(b) >= 12 &&
|
||||
string(b[0:4]) == "RIFF" &&
|
||||
string(b[8:12]) == "WEBP"
|
||||
}
|
||||
|
||||
// detectMime 检测文件类型 因为从header里获取的可能是伪造的
|
||||
func detectMime(file multipart.File) (string, error) {
|
||||
buf := make([]byte, 512)
|
||||
|
||||
n, err := file.Read(buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mimeType := http.DetectContentType(buf[:n])
|
||||
|
||||
// 补识别 DetectContentType 漏掉的格式
|
||||
if mimeType == "application/octet-stream" && isWebp(buf[:n]) {
|
||||
mimeType = "image/webp"
|
||||
}
|
||||
|
||||
// 回到文件开头
|
||||
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return mimeType, nil
|
||||
}
|
||||
|
||||
func (s *FileService) List(ctx context.Context, p *common.Pagination) (*common.PageResult[sqlc.ListFilesRow], error) {
|
||||
params := sqlc.ListFilesParams{
|
||||
Limit: p.PageSize,
|
||||
Offset: (p.Page - 1) * p.PageSize,
|
||||
@@ -58,12 +106,37 @@ func (s *FileService) List(ctx context.Context, p *common.Pagination) (*common.P
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &common.PageResult[sqlc.File]{
|
||||
return &common.PageResult[sqlc.ListFilesRow]{
|
||||
List: list,
|
||||
Total: total,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// sanitizeOriginalName 过滤文件名中的 < > 和控制字符,防 HTML 注入
|
||||
func sanitizeOriginalName(name string) string {
|
||||
var b strings.Builder
|
||||
b.Grow(len(name))
|
||||
|
||||
for _, r := range name {
|
||||
switch {
|
||||
case r == '<' || r == '>':
|
||||
continue
|
||||
case r < 0x20 || r == 0x7f:
|
||||
continue // 控制字符
|
||||
default:
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
|
||||
// 限长 255 个字符
|
||||
runes := []rune(b.String())
|
||||
if len(runes) > 255 {
|
||||
runes = runes[:255]
|
||||
}
|
||||
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
func (s *FileService) Upload(ctx context.Context, folder string, file *multipart.FileHeader) (*sqlc.CreateFileRow, error) {
|
||||
// 生成文件名
|
||||
fileID, err := gonanoid.New()
|
||||
@@ -71,7 +144,7 @@ func (s *FileService) Upload(ctx context.Context, folder string, file *multipart
|
||||
return nil, err
|
||||
}
|
||||
|
||||
savedDir, err := MakeSavedDir(folder)
|
||||
savedDir, err := MakeSavedDir(s.config.File.UploadDir, folder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -89,6 +162,44 @@ func (s *FileService) Upload(ctx context.Context, folder string, file *multipart
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 检测类型
|
||||
mimeType, err := detectMime(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 内容与扩展名必须匹配,防改后缀绕过
|
||||
if !request.MatchUploadMime(file.Filename, mimeType) {
|
||||
return nil, errs.ErrFileTypeNotAllowed
|
||||
}
|
||||
|
||||
var imageMeta *sqlc.CreateFileImageMetadataParams
|
||||
|
||||
// 图片需能解码,否则拒绝
|
||||
if strings.HasPrefix(mimeType, "image/") {
|
||||
cfg, format, err := image.DecodeConfig(src)
|
||||
|
||||
if err != nil {
|
||||
// webp 标准库无法解码,但 MIME 已验证,跳过元数据
|
||||
if mimeType != "image/webp" {
|
||||
return nil, errs.ErrFileTypeNotAllowed
|
||||
}
|
||||
} else {
|
||||
imageMeta = &sqlc.CreateFileImageMetadataParams{
|
||||
Width: int32(cfg.Width),
|
||||
Height: int32(cfg.Height),
|
||||
Format: format,
|
||||
}
|
||||
}
|
||||
|
||||
// 重置读取位置
|
||||
_, err = src.Seek(0, io.SeekStart)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 创建目标文件
|
||||
dst, err := os.Create(savedPath)
|
||||
if err != nil {
|
||||
@@ -101,20 +212,135 @@ func (s *FileService) Upload(ctx context.Context, folder string, file *multipart
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 净化文件名,全被过滤则用存储名兜底
|
||||
originalName := sanitizeOriginalName(file.Filename)
|
||||
if originalName == "" {
|
||||
originalName = filename
|
||||
}
|
||||
|
||||
params := sqlc.CreateFileParams{
|
||||
FileName: filename,
|
||||
FilePath: filePath,
|
||||
FileUrl: httputil.BuildFileUrl(&filePath),
|
||||
OriginalName: file.Filename,
|
||||
OriginalName: originalName,
|
||||
FolderName: folder,
|
||||
MimeType: file.Header.Get("Content-Type"),
|
||||
MimeType: mimeType,
|
||||
FileSize: file.Size,
|
||||
}
|
||||
|
||||
result, err := s.store.CreateFile(ctx, params)
|
||||
result, err := db.WithTxResult[*sqlc.CreateFileRow](ctx, s.store, func(q *sqlc.Queries) (*sqlc.CreateFileRow, error) {
|
||||
result, err := q.CreateFile(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if imageMeta != nil {
|
||||
meta := *imageMeta
|
||||
meta.FileID = result.ID
|
||||
|
||||
if err = q.CreateFileImageMetadata(ctx, meta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
})
|
||||
|
||||
// 事务失败 清理已保存的文件
|
||||
if err != nil {
|
||||
_ = os.Remove(savedPath)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *FileService) SyncMetadata(ctx context.Context) error {
|
||||
files, err := s.store.ListImageFiles(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
rootDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
metadataList []sqlc.CopyFileImageMetadataParams
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
// 限制并发数量
|
||||
sem := make(chan struct{}, 10)
|
||||
|
||||
for _, f := range files {
|
||||
wg.Add(1)
|
||||
|
||||
safego.Go(func() {
|
||||
defer wg.Done()
|
||||
|
||||
sem <- struct{}{}
|
||||
defer func() {
|
||||
<-sem
|
||||
}()
|
||||
|
||||
absolutePath := MakeFilePath(
|
||||
rootDir,
|
||||
s.config.File.UploadDir,
|
||||
f.FilePath,
|
||||
)
|
||||
|
||||
src, err := os.Open(absolutePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
cfg, format, err := image.DecodeConfig(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
item := sqlc.CopyFileImageMetadataParams{
|
||||
FileID: f.ID,
|
||||
Width: int32(cfg.Width),
|
||||
Height: int32(cfg.Height),
|
||||
Format: format,
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
metadataList = append(metadataList, item)
|
||||
mu.Unlock()
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if len(metadataList) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = s.store.WithTx(ctx, func(q *sqlc.Queries) error {
|
||||
if err = q.TruncateFileImageMetadata(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = q.CopyFileImageMetadata(ctx, metadataList); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@ func (s *MenuService) Create(ctx context.Context, req request.CreateMenuRequest)
|
||||
return err
|
||||
}
|
||||
|
||||
// 清理缓存 这里不清理 超管用户的菜单不会刷新
|
||||
_ = s.cache.DelByPrefix(ctx, cachekey.UserInfoPattern)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,5 +15,6 @@ var Module = fx.Module("sys-services",
|
||||
NewAuthService,
|
||||
NewTagService,
|
||||
NewCategoryService,
|
||||
NewAccessLogService,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -55,7 +55,7 @@ func (s *PostService) Create(ctx context.Context, req request.CreatePostRequest)
|
||||
postID, err := db.WithTxResult(ctx, s.store, func(q *sqlc.Queries) (int32, error) {
|
||||
params := sqlc.CreatePostParams{
|
||||
Title: req.Title,
|
||||
CoverID: &req.CoverID,
|
||||
CoverID: req.CoverID,
|
||||
Slug: req.Slug,
|
||||
Content: req.Content,
|
||||
Summary: req.Summary,
|
||||
@@ -103,7 +103,6 @@ func (s *PostService) Update(ctx context.Context, id int32, req request.UpdatePo
|
||||
return s.store.WithTx(ctx, func(q *sqlc.Queries) error {
|
||||
params := sqlc.UpdatePostParams{
|
||||
Title: req.Title,
|
||||
CoverID: req.CoverID,
|
||||
Slug: req.Slug,
|
||||
Content: req.Content,
|
||||
Summary: req.Summary,
|
||||
@@ -113,6 +112,13 @@ func (s *PostService) Update(ctx context.Context, id int32, req request.UpdatePo
|
||||
ID: id,
|
||||
}
|
||||
|
||||
if req.CoverID.Set {
|
||||
params.UpdateCoverID = true
|
||||
if req.CoverID.Valid {
|
||||
params.CoverID = &req.CoverID.Value
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := q.UpdatePost(ctx, params)
|
||||
if err != nil {
|
||||
return dberr.MapUniqueViolation(err, dberr.PostSlugKey, errs.ErrSlugAlreadyExists)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"server/internal/db"
|
||||
"server/internal/db/sqlc"
|
||||
"server/internal/middleware"
|
||||
"server/internal/model/auth"
|
||||
"server/internal/model/common"
|
||||
"server/internal/model/request"
|
||||
"server/internal/model/response"
|
||||
@@ -31,7 +32,7 @@ func NewUserService(store *db.Store, jwt *middleware.JWTMiddleware, cache *cache
|
||||
}
|
||||
}
|
||||
|
||||
// clearUserCache
|
||||
// clearUserCache 清理单个用户缓存 权限 info 和 鉴权状态
|
||||
func (s *UserService) clearUserCache(ctx context.Context, id int32) error {
|
||||
if err := s.cache.Del(ctx, cachekey.UserApiPermissions(id)); err != nil {
|
||||
return err
|
||||
@@ -41,6 +42,9 @@ func (s *UserService) clearUserCache(ctx context.Context, id int32) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.cache.Del(ctx, cachekey.UserAuthState(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -64,19 +68,29 @@ func (s *UserService) clearRefreshToken(ctx context.Context, id int32) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// forceLogout 强制下线:bump 版本 + 清缓存(含 auth_state)+ 清刷新令牌
|
||||
func (s *UserService) forceLogout(ctx context.Context, id int32) error {
|
||||
if err := s.store.IncrementUserTokenVersion(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.clearUserCache(ctx, id)
|
||||
_ = s.clearRefreshToken(ctx, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserService) GetCurrentUser(ctx context.Context, id int32, isAdmin bool) (*response.UserInfo, error) {
|
||||
return cache.GetOrSetJSON[*response.UserInfo](ctx, s.cache, cachekey.UserInfo(id), 0, func() (*response.UserInfo, error) {
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
var (
|
||||
user sqlc.GetUserByIDRow
|
||||
user sqlc.GetUserRow
|
||||
roles []sqlc.SysRole
|
||||
menus []sqlc.SysMenu
|
||||
permissions []*string
|
||||
)
|
||||
|
||||
g.Go(func() error {
|
||||
u, err := s.store.GetUserByID(ctx, id)
|
||||
u, err := s.store.GetUser(ctx, sqlc.GetUserParams{ID: id})
|
||||
if err != nil {
|
||||
return dberr.MapNoRows(err, errs.ErrUserNotFound)
|
||||
}
|
||||
@@ -136,8 +150,8 @@ func (s *UserService) GetCurrentUser(ctx context.Context, id int32, isAdmin bool
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果用户被禁用 返回错误 超管不用管状态
|
||||
if user.ID != 1 && user.Status != 1 {
|
||||
// 判断用户不为超管 且状态为0表示用户已被禁用
|
||||
if !auth.IsAdmin(user.ID) && user.Status == 0 {
|
||||
return nil, errs.ErrUserDisabled
|
||||
}
|
||||
|
||||
@@ -174,7 +188,7 @@ func (s *UserService) List(ctx context.Context, p request.SearchUserParams) (*co
|
||||
|
||||
func (s *UserService) ListRoles(ctx context.Context, id int32) ([]sqlc.SysRole, error) {
|
||||
// 先查询用户是否存在
|
||||
_, err := s.store.GetUserByID(ctx, id)
|
||||
_, err := s.store.GetUser(ctx, sqlc.GetUserParams{ID: id})
|
||||
if err != nil {
|
||||
return nil, dberr.MapNoRows(err, errs.ErrUserNotFound)
|
||||
}
|
||||
@@ -203,6 +217,19 @@ func (s *UserService) Create(ctx context.Context, req request.CreateUserRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserService) KickUser(ctx context.Context, id int32) error {
|
||||
return s.forceLogout(ctx, id)
|
||||
}
|
||||
|
||||
func (s *UserService) KickAllUsers(ctx context.Context) error {
|
||||
if err := s.store.IncrementTokenVersionForAllUsers(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
// 清理所有的token和状态
|
||||
_ = s.cache.DelByPrefix(ctx, cachekey.AuthRefreshPattern)
|
||||
return s.cache.DelByPrefix(ctx, cachekey.UserAuthStatePattern)
|
||||
}
|
||||
|
||||
func (s *UserService) Update(ctx context.Context, id int32, req request.UpdateUserRequest) error {
|
||||
user := sqlc.UpdateUserParams{
|
||||
Username: req.Username,
|
||||
@@ -227,7 +254,8 @@ func (s *UserService) Update(ctx context.Context, id int32, req request.UpdateUs
|
||||
|
||||
// 如果将用户的状态修改为0,则清除用户刷新令牌
|
||||
if req.Status != nil && *req.Status == 0 {
|
||||
_ = s.clearRefreshToken(ctx, id)
|
||||
// 踢下线
|
||||
_ = s.forceLogout(ctx, id)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -235,7 +263,7 @@ func (s *UserService) Update(ctx context.Context, id int32, req request.UpdateUs
|
||||
|
||||
func (s *UserService) SetRoles(ctx context.Context, userID int32, req request.SetUserRolesRequest) error {
|
||||
// 先查询用户是否存在
|
||||
_, err := s.store.GetUserByID(ctx, userID)
|
||||
_, err := s.store.GetUser(ctx, sqlc.GetUserParams{ID: userID})
|
||||
if err != nil {
|
||||
return dberr.MapNoRows(err, errs.ErrUserNotFound)
|
||||
}
|
||||
@@ -292,13 +320,15 @@ func (s *UserService) UpdatePassword(ctx context.Context, id int32, req request.
|
||||
}
|
||||
|
||||
// 下线当前用户
|
||||
_ = s.clearRefreshToken(ctx, id)
|
||||
if err = s.forceLogout(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserService) Delete(ctx context.Context, id int32) error {
|
||||
isAdmin := middleware.UserIsAdmin(id)
|
||||
isAdmin := auth.IsAdmin(id)
|
||||
|
||||
if isAdmin {
|
||||
return errs.ErrCannotDeleteSuperAdmin
|
||||
|
||||
Reference in New Issue
Block a user