feat: complete Kra admin backend migration

This commit is contained in:
2026-08-14 00:28:37 +08:00
parent c4bf7ee03b
commit acba9624d7
88 changed files with 1765 additions and 8623 deletions
-232
View File
@@ -1,232 +0,0 @@
package service
import (
"context"
"time"
v1 "kra/api/kratos/admin/v1"
"kra/internal/biz"
"kra/pkg/auth"
"github.com/go-kratos/kratos/v3/errors"
"go.einride.tech/aip/fieldmask"
"go.einride.tech/aip/filtering"
"go.einride.tech/aip/ordering"
"go.einride.tech/aip/pagination"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
)
func convertAdmin(m *biz.Admin) *v1.Admin {
return &v1.Admin{
Id: m.ID,
Name: m.Name,
Email: m.Email,
Avatar: m.Avatar,
Access: m.Access,
CreateTime: timestamppb.New(m.CreateTime),
UpdateTime: timestamppb.New(m.UpdateTime),
}
}
// AdminService is a greeter service.
type AdminService struct {
v1.UnimplementedAdminServiceServer
uc *biz.AdminUsecase
}
// NewAdminService new a greeter service.
func NewAdminService(uc *biz.AdminUsecase) *AdminService {
return &AdminService{uc: uc}
}
// Current implements auth current admin retrieval.
func (s *AdminService) Current(ctx context.Context, req *emptypb.Empty) (*v1.Admin, error) {
a, ok := auth.FromContext(ctx)
if !ok {
return nil, auth.ErrUnauthenticated
}
admin, err := s.uc.GetAdmin(ctx, a.UserID)
if err != nil {
return nil, err
}
return convertAdmin(admin), nil
}
// Login implements auth login.
func (s *AdminService) Login(ctx context.Context, req *v1.LoginRequest) (*v1.Admin, error) {
var (
err error
admin *biz.Admin
)
switch v := req.Identity.(type) {
case *v1.LoginRequest_Username:
admin, err = s.uc.LoginByUsername(ctx, v.Username, req.Password)
if err != nil {
return nil, err
}
case *v1.LoginRequest_Email:
admin, err = s.uc.LoginByEmail(ctx, v.Email, req.Password)
if err != nil {
return nil, err
}
default:
return nil, errors.BadRequest(v1.ErrorReason_INVALID_IDENTITY_TYPE.String(), "unsupported identity type")
}
if err := auth.SetCookie(ctx, admin.ID, admin.Access, time.Now().Add(7*24*time.Hour)); err != nil {
return nil, err
}
return convertAdmin(admin), nil
}
// Logout implements auth logout.
func (s *AdminService) Logout(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) {
a, ok := auth.FromContext(ctx)
if !ok {
return nil, auth.ErrUnauthenticated
}
if err := s.uc.Logout(ctx, a.UserID); err != nil {
return nil, err
}
if err := auth.DeleteCookie(ctx); err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}
// CreateAdmin implements admin creation.
func (s *AdminService) CreateAdmin(ctx context.Context, req *v1.CreateAdminRequest) (*v1.Admin, error) {
a, ok := auth.FromContext(ctx)
if !ok {
return nil, auth.ErrUnauthenticated
}
if !a.HasAdminAccess() {
return nil, auth.ErrPermissionDenied
}
admin, err := s.uc.CreateAdmin(ctx, &biz.Admin{
Name: req.Admin.Name,
Email: req.Admin.Email,
Password: req.Admin.Password,
Avatar: req.Admin.Avatar,
Access: req.Admin.Access,
})
if err != nil {
return nil, err
}
return convertAdmin(admin), nil
}
// UpdateAdmin implements admin update.
func (s *AdminService) UpdateAdmin(ctx context.Context, req *v1.UpdateAdminRequest) (*v1.Admin, error) {
a, ok := auth.FromContext(ctx)
if !ok {
return nil, auth.ErrUnauthenticated
}
if !a.HasAdminAccess() {
return nil, auth.ErrPermissionDenied
}
admin, err := s.GetAdmin(ctx, &v1.GetAdminRequest{Id: req.Admin.Id})
if err != nil {
return nil, err
}
fieldmask.Update(req.UpdateMask, admin, req.Admin)
updated, err := s.uc.UpdateAdmin(ctx, &biz.Admin{
ID: admin.Id,
Name: admin.Name,
Email: admin.Email,
Password: admin.Password,
Avatar: admin.Avatar,
Access: admin.Access,
})
if err != nil {
return nil, err
}
return convertAdmin(updated), nil
}
// DeleteAdmin implements admin deletion.
func (s *AdminService) DeleteAdmin(ctx context.Context, req *v1.DeleteAdminRequest) (*emptypb.Empty, error) {
a, ok := auth.FromContext(ctx)
if !ok {
return nil, auth.ErrUnauthenticated
}
if !a.HasAdminAccess() {
return nil, auth.ErrPermissionDenied
}
if err := s.uc.DeleteAdmin(ctx, req.Id); err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}
// GetAdmin implements admin retrieval.
func (s *AdminService) GetAdmin(ctx context.Context, req *v1.GetAdminRequest) (*v1.Admin, error) {
a, ok := auth.FromContext(ctx)
if !ok {
return nil, auth.ErrUnauthenticated
}
if !a.HasAdminAccess() {
return nil, auth.ErrPermissionDenied
}
admin, err := s.uc.GetAdmin(ctx, req.Id)
if err != nil {
return nil, err
}
return convertAdmin(admin), nil
}
// ListAdmins implements admin listing with filtering, ordering, and pagination.
func (s *AdminService) ListAdmins(ctx context.Context, req *v1.ListAdminsRequest) (*v1.AdminSet, error) {
a, ok := auth.FromContext(ctx)
if !ok {
return nil, auth.ErrUnauthenticated
}
if !a.HasAdminAccess() {
return nil, auth.ErrPermissionDenied
}
declarations, err := filtering.NewDeclarations(
filtering.DeclareStandardFunctions(),
filtering.DeclareIdent("name", filtering.TypeString),
filtering.DeclareIdent("email", filtering.TypeString),
filtering.DeclareIdent("phone", filtering.TypeString),
filtering.DeclareIdent("create_time", filtering.TypeTimestamp),
)
if err != nil {
return nil, err
}
filter, err := filtering.ParseFilter(req, declarations)
if err != nil {
return nil, err
}
pageToken, err := pagination.ParsePageToken(req)
if err != nil {
return nil, err
}
orderBy, err := ordering.ParseOrderBy(req)
if err != nil {
return nil, err
}
if req.PageSize <= 0 {
req.PageSize = 20
}
admins, err := s.uc.ListAdmins(ctx,
biz.ListFilter(filter),
biz.ListOrderBy(orderBy),
biz.ListLimit(int(req.PageSize)),
biz.ListOffset(int(pageToken.Offset)),
)
if err != nil {
return nil, err
}
adminSet := &v1.AdminSet{
Admins: make([]*v1.Admin, 0, len(admins)),
}
if len(admins) >= int(req.PageSize) {
adminSet.NextPageToken = pageToken.Next(req).String()
}
for _, admin := range admins {
adminSet.Admins = append(adminSet.Admins, convertAdmin(admin))
}
return adminSet, nil
}
+1 -1
View File
@@ -3,4 +3,4 @@ package service
import "github.com/google/wire"
// ProviderSet is service providers.
var ProviderSet = wire.NewSet(NewAdminService, NewSystemService, NewAccessService, NewSettingsService, NewVersionService, NewExportService, NewAuditService, NewTaskService, NewMediaService, NewAnnouncementService, NewEmailService)
var ProviderSet = wire.NewSet(NewSystemService, NewAccessService, NewSettingsService, NewVersionService, NewExportService, NewAuditService, NewTaskService, NewMediaService, NewAnnouncementService, NewEmailService)
+13 -3
View File
@@ -162,13 +162,17 @@ func (s *SettingsService) CreateAPIToken(ctx context.Context, userID, authorityI
return "", errors.New("有效天数必须大于0或为-1")
}
secret, issuer := "", "kra"
buffer := 24 * time.Hour
if s.config != nil && s.config.Jwt != nil {
secret = s.config.Jwt.SigningKey
if s.config.Jwt.Issuer != "" {
issuer = s.config.Jwt.Issuer
}
if s.config.Jwt.BufferTime != nil {
buffer = s.config.Jwt.BufferTime.AsDuration()
}
}
token, _, err := adminauth.Generate(secret, issuer, duration, user.ID, authorityID, user.UUID, user.Username, user.NickName, false)
token, _, err := adminauth.Generate(secret, issuer, duration, buffer, user.ID, authorityID, user.UUID, user.Username, user.NickName, false)
if err != nil {
return "", err
}
@@ -190,8 +194,14 @@ func (s *SettingsService) APITokens(ctx context.Context, page, size int, userID
return out, total, nil
}
func (s *SettingsService) DisableAPIToken(ctx context.Context, id uint) error {
_, err := s.Repo().DisableAPIToken(ctx, id)
return err
token, err := s.Repo().DisableAPIToken(ctx, id)
if err != nil {
return err
}
return s.Repo().BlacklistToken(ctx, token)
}
func (s *SettingsService) BlacklistToken(ctx context.Context, token string) error {
return s.Repo().BlacklistToken(ctx, token)
}
func (s *SettingsService) IsTokenDisabled(ctx context.Context, token string) (bool, error) {
return s.Repo().IsTokenDisabled(ctx, token)
+33 -7
View File
@@ -4,6 +4,7 @@ import (
"context"
"time"
"google.golang.org/protobuf/encoding/protojson"
"kra/internal/biz"
"kra/internal/conf"
"kra/pkg/adminauth"
@@ -30,6 +31,14 @@ func (s *SystemService) IsInitialized(ctx context.Context) (bool, error) {
return s.uc.IsInitialized(ctx)
}
func (s *SystemService) PersistConfig(ctx context.Context) error { return s.uc.PersistConfig(ctx) }
func (s *SystemService) PersistAdminConfig(ctx context.Context, value *conf.AdminBackend) error {
raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(value)
if err != nil {
return err
}
return s.uc.PersistAdminConfig(ctx, raw)
}
func (s *SystemService) ReloadConfig(ctx context.Context) error { return s.uc.ReloadConfig(ctx) }
type DatabaseInit struct {
DBType string `json:"dbType"`
@@ -43,7 +52,7 @@ type DatabaseInit struct {
AdminPassword string `json:"adminPassword"`
}
func (s *SystemService) Initialize(ctx context.Context, input *DatabaseInit) error {
func (s *SystemService) Initialize(ctx context.Context, input *DatabaseInit, apis []*biz.API) error {
config := ""
switch input.DBType {
case "mysql":
@@ -51,7 +60,7 @@ func (s *SystemService) Initialize(ctx context.Context, input *DatabaseInit) err
case "pgsql":
config = "sslmode=disable TimeZone=Local"
}
return s.uc.Initialize(ctx, &biz.DatabaseConfig{Driver: input.DBType, Host: input.Host, Port: input.Port, User: input.UserName, Password: input.Password, Name: input.DBName, Path: input.DBPath, Config: config, AdminPassword: input.AdminPassword})
return s.uc.Initialize(ctx, &biz.DatabaseConfig{Driver: input.DBType, Host: input.Host, Port: input.Port, User: input.UserName, Password: input.Password, Name: input.DBName, Path: input.DBPath, Config: config, Template: input.Template, AdminPassword: input.AdminPassword, APIs: apis})
}
func (s *SystemService) CacheGet(ctx context.Context, key string) (string, bool, error) {
@@ -63,6 +72,9 @@ func (s *SystemService) CacheSet(ctx context.Context, key, value string, expirat
func (s *SystemService) CacheDelete(ctx context.Context, key string) error {
return s.uc.CacheDelete(ctx, key)
}
func (s *SystemService) CacheIncrement(ctx context.Context, key string, expiration time.Duration) (int64, error) {
return s.uc.CacheIncrement(ctx, key, expiration)
}
func (s *SystemService) Login(ctx context.Context, username, password string) (*LoginResult, error) {
u, err := s.uc.Login(ctx, username, password)
@@ -75,15 +87,18 @@ func (s *SystemService) Login(ctx context.Context, username, password string) (*
if security, securityErr := s.settings.CurrentSecurity(ctx); securityErr == nil && security.PwdExpireEnable && u.PasswordUpdatedAt != nil && time.Since(*u.PasswordUpdatedAt) > time.Duration(security.PwdExpireDays)*24*time.Hour {
u.MustChangePassword = true
}
expires := 7 * 24 * time.Hour
expires, buffer := 7*24*time.Hour, 24*time.Hour
secret, issuer := "", "kra"
if s.config != nil && s.config.Jwt != nil {
secret, issuer = s.config.Jwt.SigningKey, s.config.Jwt.Issuer
if s.config.Jwt.ExpiresTime != nil {
expires = s.config.Jwt.ExpiresTime.AsDuration()
}
if s.config.Jwt.BufferTime != nil {
buffer = s.config.Jwt.BufferTime.AsDuration()
}
}
token, claims, err := adminauth.Generate(secret, issuer, expires, u.ID, u.AuthorityID, u.UUID, u.Username, u.NickName, u.MustChangePassword)
token, claims, err := adminauth.Generate(secret, issuer, expires, buffer, u.ID, u.AuthorityID, u.UUID, u.Username, u.NickName, u.MustChangePassword)
if err != nil {
return nil, err
}
@@ -212,15 +227,18 @@ func (s *SystemService) SwitchAuthority(ctx context.Context, id, authorityID uin
if err != nil {
return nil, err
}
expires := 7 * 24 * time.Hour
expires, buffer := 7*24*time.Hour, 24*time.Hour
secret, issuer := "", "kra"
if s.config != nil && s.config.Jwt != nil {
secret, issuer = s.config.Jwt.SigningKey, s.config.Jwt.Issuer
if s.config.Jwt.ExpiresTime != nil {
expires = s.config.Jwt.ExpiresTime.AsDuration()
}
if s.config.Jwt.BufferTime != nil {
buffer = s.config.Jwt.BufferTime.AsDuration()
}
}
token, claims, err := adminauth.Generate(secret, issuer, expires, u.ID, authorityID, u.UUID, u.Username, u.NickName, u.MustChangePassword)
token, claims, err := adminauth.Generate(secret, issuer, expires, buffer, u.ID, authorityID, u.UUID, u.Username, u.NickName, u.MustChangePassword)
if err != nil {
return nil, err
}
@@ -260,8 +278,16 @@ func convertMenu(menu *biz.Menu) map[string]any {
children = append(children, convertMenu(child))
}
buttons := make([]map[string]any, 0, len(menu.Buttons))
buttonAuthorities := make(map[string]uint, len(menu.Buttons))
for _, button := range menu.Buttons {
buttons = append(buttons, map[string]any{"ID": button.ID, "name": button.Name, "desc": button.Description, "sysBaseMenuID": button.MenuID})
if button.AuthorityID != 0 {
buttonAuthorities[button.Name] = button.AuthorityID
}
}
return map[string]any{"ID": menu.ID, "parentId": menu.ParentID, "path": menu.Path, "name": menu.Name, "hidden": menu.Hidden, "component": menu.Component, "sort": menu.Sort, "meta": map[string]any{"activeName": menu.ActiveName, "keepAlive": menu.KeepAlive, "defaultMenu": menu.DefaultMenu, "title": menu.Title, "icon": menu.Icon, "closeTab": menu.CloseTab, "transitionType": menu.TransitionType}, "children": children, "parameters": []any{}, "menuBtn": buttons}
parameters := make([]map[string]any, 0, len(menu.Parameters))
for _, parameter := range menu.Parameters {
parameters = append(parameters, map[string]any{"ID": parameter.ID, "SysBaseMenuID": parameter.MenuID, "type": parameter.Type, "key": parameter.Key, "value": parameter.Value})
}
return map[string]any{"ID": menu.ID, "parentId": menu.ParentID, "path": menu.Path, "name": menu.Name, "hidden": menu.Hidden, "component": menu.Component, "sort": menu.Sort, "meta": map[string]any{"activeName": menu.ActiveName, "keepAlive": menu.KeepAlive, "defaultMenu": menu.DefaultMenu, "title": menu.Title, "icon": menu.Icon, "closeTab": menu.CloseTab, "transitionType": menu.TransitionType}, "children": children, "parameters": parameters, "menuBtn": buttons, "btns": buttonAuthorities}
}
+35 -14
View File
@@ -9,6 +9,7 @@ import (
"github.com/robfig/cron/v3"
"io"
"kra/internal/biz"
"kra/internal/conf"
"net"
"net/http"
"net/url"
@@ -16,9 +17,15 @@ import (
"time"
)
type TaskService struct{ uc *biz.TaskUsecase }
type TaskService struct {
uc *biz.TaskUsecase
media *biz.MediaUsecase
config *conf.AdminBackend
}
func NewTaskService(uc *biz.TaskUsecase) *TaskService { return &TaskService{uc: uc} }
func NewTaskService(uc *biz.TaskUsecase, media *biz.MediaUsecase, config *conf.AdminBackend) *TaskService {
return &TaskService{uc: uc, media: media, config: config}
}
func (s *TaskService) Validate(v *biz.TimedTask) error {
if v.Name == "" {
return errors.New("任务名不能为空")
@@ -34,7 +41,7 @@ func (s *TaskService) Validate(v *biz.TimedTask) error {
}
switch v.ExecutorType {
case "method":
if v.MethodName != "ClearDB" {
if v.MethodName != "ClearDB" && v.MethodName != "CleanStaleUploads" {
return errors.New("方法未注册")
}
if len(v.Params) > 0 && !json.Valid(v.Params) {
@@ -60,6 +67,13 @@ func (s *TaskService) Create(ctx context.Context, v *biz.TimedTask) error {
if err := s.Validate(v); err != nil {
return err
}
exists, err := s.uc.Repo().TaskNameExists(ctx, v.Name, 0)
if err != nil {
return err
}
if exists {
return fmt.Errorf("任务名 %s 已存在", v.Name)
}
return s.uc.Repo().CreateTask(ctx, v)
}
func (s *TaskService) Update(ctx context.Context, v *biz.TimedTask) error {
@@ -69,6 +83,13 @@ func (s *TaskService) Update(ctx context.Context, v *biz.TimedTask) error {
if err := s.Validate(v); err != nil {
return err
}
exists, err := s.uc.Repo().TaskNameExists(ctx, v.Name, v.ID)
if err != nil {
return err
}
if exists {
return fmt.Errorf("任务名 %s 已存在", v.Name)
}
return s.uc.Repo().UpdateTask(ctx, v)
}
func (s *TaskService) Delete(ctx context.Context, id uint) error {
@@ -168,17 +189,17 @@ func (s *TaskService) Run(ctx context.Context, v *biz.TimedTask, trigger string)
var err error
switch v.ExecutorType {
case "method":
if v.MethodName == "ClearDB" {
days := 30
var params struct {
Days int `json:"days"`
}
_ = json.Unmarshal(v.Params, &params)
if params.Days > 0 {
days = params.Days
}
err = s.uc.Repo().CleanupLogs(runCtx, time.Now().AddDate(0, 0, -days))
switch v.MethodName {
case "ClearDB":
err = s.uc.Repo().CleanupLogs(runCtx)
log.Output = "过期日志清理完成"
case "CleanStaleUploads":
ttl := 24
if s.config != nil && s.config.Media != nil && s.config.Media.SessionTtl > 0 {
ttl = int(s.config.Media.SessionTtl)
}
err = s.media.CleanupStale(runCtx, ttl)
log.Output = "过期大文件上传会话清理完成"
}
case "http":
log.Output, err = s.runHTTP(runCtx, v)
@@ -190,5 +211,5 @@ func (s *TaskService) Run(ctx context.Context, v *biz.TimedTask, trigger string)
return log
}
func (s *TaskService) RegisteredMethods() []map[string]any {
return []map[string]any{{"name": "ClearDB", "description": "清理数据库过期日志(操作记录、登录日志、定时任务日志)"}}
return []map[string]any{{"name": "ClearDB", "description": "清理数据库过期日志(操作记录、JWT黑名单、定时任务日志)"}, {"name": "CleanStaleUploads", "description": "清理过期大文件上传会话"}}
}