294 lines
13 KiB
Go
294 lines
13 KiB
Go
package data
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"kra/internal/biz"
|
|
"kra/internal/conf"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
"google.golang.org/protobuf/proto"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func (r *initializationRepo) PersistConfig(context.Context) error { return r.data.persistConfig() }
|
|
func (r *initializationRepo) PersistAdminConfig(_ context.Context, raw []byte) error {
|
|
currentData, currentAdmin := r.data.runtime.Values()
|
|
next := proto.Clone(currentAdmin).(*conf.AdminBackend)
|
|
if err := (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(raw, next); err != nil {
|
|
return err
|
|
}
|
|
next.ConfigPath = currentAdmin.ConfigPath
|
|
if err := r.data.persistConfigValues(currentData, next); err != nil {
|
|
return err
|
|
}
|
|
// Writing through the management API updates the same in-memory values
|
|
// immediately; the file watcher remains the fallback for external edits.
|
|
r.data.runtime.Replace(currentData, next)
|
|
return nil
|
|
}
|
|
func (r *initializationRepo) PersistRuntimeConfig(_ context.Context, dataRaw, adminRaw []byte) error {
|
|
currentData, currentAdmin := r.data.runtime.Values()
|
|
nextData := proto.Clone(currentData).(*conf.Data)
|
|
nextAdmin := proto.Clone(currentAdmin).(*conf.AdminBackend)
|
|
options := protojson.UnmarshalOptions{DiscardUnknown: true}
|
|
if err := options.Unmarshal(dataRaw, nextData); err != nil {
|
|
return err
|
|
}
|
|
if err := options.Unmarshal(adminRaw, nextAdmin); err != nil {
|
|
return err
|
|
}
|
|
nextAdmin.ConfigPath = currentAdmin.ConfigPath
|
|
if err := r.data.persistConfigValues(nextData, nextAdmin); err != nil {
|
|
return err
|
|
}
|
|
r.data.runtime.Replace(nextData, nextAdmin)
|
|
return nil
|
|
}
|
|
func (r *initializationRepo) ReloadConfig(ctx context.Context) error {
|
|
return r.data.reloadConfig(ctx)
|
|
}
|
|
|
|
func (r *initializationRepo) IsInitialized(ctx context.Context) (bool, error) {
|
|
return r.data.databaseReady.Load(), nil
|
|
}
|
|
|
|
func (r *initializationRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig) error {
|
|
config := &conf.Data_Database{}
|
|
if current := r.data.runtime.Data(); current != nil && current.Database != nil {
|
|
config = proto.Clone(current.Database).(*conf.Data_Database)
|
|
}
|
|
config.Driver = input.Driver
|
|
config.Host = input.Host
|
|
config.Port = input.Port
|
|
config.User = input.User
|
|
config.Password = input.Password
|
|
config.Name = input.Name
|
|
config.Path = input.Path
|
|
config.Config = input.Config
|
|
config.Source = ""
|
|
source, err := databaseDSN(config, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
config.Source = source
|
|
r.data.initMu.Lock()
|
|
defer r.data.initMu.Unlock()
|
|
initialized, err := r.IsInitialized(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if initialized {
|
|
return errors.New("数据库已初始化,无需重复初始化")
|
|
}
|
|
candidate, err := openDatabase(config, true, input.Template, r.data.logger())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
activated := false
|
|
defer func() {
|
|
if !activated {
|
|
if sqlDB, closeErr := candidate.DB(); closeErr == nil {
|
|
_ = sqlDB.Close()
|
|
}
|
|
}
|
|
}()
|
|
db := candidate.WithContext(ctx)
|
|
if err := migrateAll(db); err != nil {
|
|
return err
|
|
}
|
|
if err := db.Transaction(func(tx *gorm.DB) error {
|
|
rootParentID := uint(0)
|
|
authority := authorityPO{AuthorityID: 888, AuthorityName: "超级管理员", ParentID: &rootParentID, DataScope: 1, DefaultRouter: "dashboard"}
|
|
if err := tx.FirstOrCreate(&authority, authorityPO{AuthorityID: 888}).Error; err != nil {
|
|
return err
|
|
}
|
|
// The root role is seeded with parent_id=0. Older Kra databases used
|
|
// NULL, which makes the role disappear from the same root-only queries.
|
|
if err := tx.Model(&authorityPO{}).Where("authority_id = ? AND parent_id IS NULL", 888).Update("parent_id", 0).Error; err != nil {
|
|
return err
|
|
}
|
|
menus := defaultMenus()
|
|
for i := range menus {
|
|
if err := tx.Where("name = ?", menus[i].Name).FirstOrCreate(&menus[i]).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
var persisted []menuPO
|
|
if err := tx.Order("sort asc, id asc").Find(&persisted).Error; err != nil {
|
|
return err
|
|
}
|
|
nameID := make(map[string]uint, len(persisted))
|
|
for _, menu := range persisted {
|
|
nameID[menu.Name] = menu.ID
|
|
}
|
|
for i := range menus {
|
|
if menus[i].ActiveName == "" {
|
|
continue
|
|
}
|
|
if err := tx.Model(&menuPO{}).Where("name = ?", menus[i].Name).Updates(map[string]any{"parent_id": nameID[menus[i].ActiveName], "active_name": ""}).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := tx.Where("sys_authority_authority_id = ?", 888).Delete(&authorityMenuPO{}).Error; err != nil {
|
|
return err
|
|
}
|
|
links := make([]authorityMenuPO, 0, len(persisted))
|
|
for _, menu := range persisted {
|
|
links = append(links, authorityMenuPO{SysAuthorityAuthorityID: 888, SysBaseMenuID: menu.ID})
|
|
}
|
|
if len(links) > 0 {
|
|
if err := tx.Create(&links).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
var count int64
|
|
if err := tx.Model(&userPO{}).Where("username = ?", "admin").Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
if count == 0 {
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(input.AdminPassword), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
now := time.Now()
|
|
user := userPO{UUID: uuid.NewString(), Username: "admin", Password: string(hash), NickName: "超级管理员", AuthorityID: 888, Enable: 1, PasswordUpdatedAt: &now}
|
|
if err := tx.Create(&user).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Create(&userAuthorityPO{SysUserID: user.ID, SysAuthorityAuthorityID: 888}).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
enabled := true
|
|
department := departmentPO{Name: "总公司", ParentID: 0, Ancestors: "0", Sort: 0, Status: &enabled}
|
|
if err := tx.Where("name = ?", department.Name).FirstOrCreate(&department).Error; err != nil {
|
|
return err
|
|
}
|
|
for _, position := range []positionPO{{Name: "总经理", Code: "CEO", Sort: 1, Status: &enabled}, {Name: "普通员工", Code: "STAFF", Sort: 2, Status: &enabled}} {
|
|
if err := tx.Where("code = ?", position.Code).FirstOrCreate(&position).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
security := defaultSecurityConfig()
|
|
if err := tx.FirstOrCreate(&security, securityConfigPO{ID: 1}).Error; err != nil {
|
|
return err
|
|
}
|
|
exportTemplate := exportTemplatePO{Name: "api", DBTableName: "sys_apis", TemplateID: "api", TemplateInfo: "{\n\"path\":\"路径\",\n\"method\":\"方法(大写)\",\n\"description\":\"方法介绍\",\n\"api_group\":\"方法分组\"\n}"}
|
|
if err := tx.Where("template_id = ?", exportTemplate.TemplateID).FirstOrCreate(&exportTemplate).Error; err != nil {
|
|
return err
|
|
}
|
|
for _, task := range []taskPO{{Name: "ClearDB", Description: "定时清理数据库过期日志(操作记录/JWT黑名单/定时任务执行日志)", Spec: "@daily", ExecutorType: "method", MethodName: "ClearDB", Enabled: true}, {Name: "CleanStaleUploads", Description: "定时清理过期大文件上传会话", Spec: "@hourly", ExecutorType: "method", MethodName: "CleanStaleUploads", Enabled: true}} {
|
|
if err := tx.Where("name = ?", task.Name).FirstOrCreate(&task).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, item := range input.APIs {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
po := apiPO{Path: item.Path, Method: strings.ToUpper(item.Method), Description: item.Description, APIGroup: item.APIGroup}
|
|
if err := tx.Where("path = ? AND method = ?", po.Path, po.Method).FirstOrCreate(&po).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
staticPath := "uploads/file"
|
|
if admin := r.data.runtime.Admin(); admin != nil && admin.Local != nil && strings.Trim(admin.Local.PathPrefix, "/") != "" {
|
|
staticPath = strings.Trim(admin.Local.PathPrefix, "/")
|
|
}
|
|
ignoredAPIs := defaultIgnoredAPIs(staticPath)
|
|
for _, ignored := range ignoredAPIs {
|
|
if err := tx.FirstOrCreate(&ignored, ignored).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
ignoreSet := make(map[string]struct{}, len(ignoredAPIs))
|
|
for _, ignored := range ignoredAPIs {
|
|
ignoreSet[ignored.Method+"\x00"+ignored.Path] = struct{}{}
|
|
}
|
|
var apiRows []apiPO
|
|
if err := tx.Find(&apiRows).Error; err != nil {
|
|
return err
|
|
}
|
|
for _, api := range apiRows {
|
|
if _, ignored := ignoreSet[api.Method+"\x00"+api.Path]; ignored {
|
|
continue
|
|
}
|
|
exists, err := policyExists(tx, 888, api.Path, api.Method)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if exists {
|
|
continue
|
|
}
|
|
rule := newPolicyRule(888, api.Path, api.Method)
|
|
if err := tx.Create(&rule).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
signingKey := uuid.NewString()
|
|
if err := r.data.persistDatabaseConfig(config, signingKey); err != nil {
|
|
return fmt.Errorf("persist database configuration: %w", err)
|
|
}
|
|
r.data.activateDatabase(candidate, config)
|
|
currentData, currentAdmin := r.data.runtime.Values()
|
|
if currentAdmin == nil {
|
|
currentAdmin = &conf.AdminBackend{}
|
|
}
|
|
if currentAdmin.Jwt == nil {
|
|
currentAdmin.Jwt = &conf.AdminBackend_JWT{}
|
|
}
|
|
currentAdmin.Jwt.SigningKey = signingKey
|
|
r.data.runtime.Replace(currentData, currentAdmin)
|
|
activated = true
|
|
return nil
|
|
}
|
|
|
|
func defaultIgnoredAPIs(staticPath string) []ignoredAPIPO {
|
|
staticRoute := "/" + strings.Trim(staticPath, "/") + "/*filepath"
|
|
return []ignoredAPIPO{
|
|
{Method: "GET", Path: "/api/freshCasbin"}, {Method: "GET", Path: "/health"},
|
|
{Method: "GET", Path: "/swagger/*any"},
|
|
{Method: "GET", Path: staticRoute}, {Method: "HEAD", Path: staticRoute},
|
|
{Method: "POST", Path: "/system/reloadSystem"}, {Method: "POST", Path: "/base/login"},
|
|
{Method: "POST", Path: "/base/captcha"}, {Method: "POST", Path: "/init/initdb"},
|
|
{Method: "POST", Path: "/init/checkdb"}, {Method: "GET", Path: "/info/getInfoDataSource"},
|
|
{Method: "GET", Path: "/info/getInfoPublic"},
|
|
}
|
|
}
|
|
|
|
func defaultMenus() []menuPO {
|
|
root := func(path, name, title, icon string, sort int) menuPO {
|
|
return menuPO{Path: path, Name: name, Component: "view/routerHolder.vue", Title: title, Icon: icon, Sort: sort}
|
|
}
|
|
child := func(parent, path, name, component, title, icon string, sort int) menuPO {
|
|
return menuPO{MenuLevel: 1, Path: path, Name: name, Component: component, Title: title, Icon: icon, Sort: sort, ActiveName: parent}
|
|
}
|
|
cachedChild := func(parent, path, name, component, title, icon string, sort int) menuPO {
|
|
value := child(parent, path, name, component, title, icon, sort)
|
|
value.KeepAlive = true
|
|
return value
|
|
}
|
|
return []menuPO{
|
|
{Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Title: "仪表盘", Icon: "odometer", Sort: 1},
|
|
root("permission", "permission", "权限管理", "perm-kra", 2), root("org", "org", "组织管理", "share", 3), root("systemConfig", "systemConfig", "系统设置", "config-kra", 4), root("monitor", "monitor", "运维监控", "monitor-kra", 5), root("media", "media", "媒体管理", "folder-opened", 6), root("extensions", "extensions", "扩展功能", "cherry", 10),
|
|
{Path: "person", Name: "person", Component: "view/person/person.vue", Title: "个人信息", Icon: "postcard", Hidden: true, Sort: 13},
|
|
child("permission", "authority", "authority", "view/superAdmin/authority/authority.vue", "角色管理", "role-kra", 1), cachedChild("permission", "menu", "menu", "view/superAdmin/menu/menu.vue", "菜单管理", "tickets", 2), cachedChild("permission", "api", "api", "view/superAdmin/api/api.vue", "api管理", "api-kra", 3), child("permission", "apiToken", "apiToken", "view/systemTools/apiToken/index.vue", "API Token", "key", 4),
|
|
child("org", "user", "user", "view/superAdmin/user/user.vue", "用户管理", "user", 1), child("org", "department", "department", "view/superAdmin/department/department.vue", "部门管理", "office-building", 2), child("org", "position", "position", "view/superAdmin/position/position.vue", "岗位管理", "postcard", 3),
|
|
child("systemConfig", "system", "system", "view/systemTools/system/system.vue", "配置文件", "config-file-kra", 1), child("systemConfig", "dictionary", "dictionary", "view/superAdmin/dictionary/sysDictionary.vue", "字典管理", "notebook", 2), child("systemConfig", "sysParams", "sysParams", "view/superAdmin/params/sysParams.vue", "参数管理", "set-up", 3), child("systemConfig", "security", "security", "view/system/security/index.vue", "安全配置", "security-kra", 4),
|
|
child("monitor", "operation", "operation", "view/superAdmin/operation/sysOperationRecord.vue", "操作历史", "document", 1), child("monitor", "loginLog", "loginLog", "view/systemTools/loginLog/index.vue", "登录日志", "clock", 2), child("monitor", "sysError", "sysError", "view/systemTools/sysError/sysError.vue", "错误日志", "error-kra", 3), child("monitor", "sysVersion", "sysVersion", "view/systemTools/version/version.vue", "版本管理", "version-kra", 4), child("monitor", "state", "state", "view/system/state.vue", "服务器状态", "server", 5), child("monitor", "dataAccessLog", "dataAccessLog", "view/superAdmin/dataAccessLog/dataAccessLog.vue", "数据权限审计", "warning", 6), child("monitor", "timedTask", "timedTask", "view/systemTools/timedTask/index.vue", "定时任务", "timer", 7), child("monitor", "logViewer", "logViewer", "view/systemTools/logViewer/index.vue", "文件日志", "document", 8),
|
|
child("media", "upload", "upload", "view/media/upload.vue", "媒体库(上传下载)", "upload", 1), child("media", "chunkUpload", "chunkUpload", "view/media/chunkUpload.vue", "大文件上传", "folder-add", 2),
|
|
child("extensions", "email", "email", "modules/email/view/index.vue", "邮件发送", "message", 4), child("extensions", "anInfo", "anInfo", "modules/announcement/view/info.vue", "公告管理", "bell", 5),
|
|
}
|
|
}
|