数据源和 oss
This commit is contained in:
+20
-5
@@ -35,6 +35,10 @@ type UserListFilter struct {
|
||||
Desc bool
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Driver, Host, Port, User, Password, Name, Path, Config, AdminPassword string
|
||||
}
|
||||
|
||||
type Authority struct {
|
||||
AuthorityID uint
|
||||
AuthorityName string
|
||||
@@ -64,7 +68,7 @@ type Menu struct {
|
||||
|
||||
type SystemRepo interface {
|
||||
IsInitialized(context.Context) (bool, error)
|
||||
Initialize(context.Context) error
|
||||
Initialize(context.Context, *DatabaseConfig) error
|
||||
FindUserByUsername(context.Context, string) (*User, error)
|
||||
FindUserByID(context.Context, uint) (*User, error)
|
||||
MenusByAuthority(context.Context, uint) ([]*Menu, error)
|
||||
@@ -72,11 +76,12 @@ type SystemRepo interface {
|
||||
CreateUser(context.Context, *User) (*User, error)
|
||||
UpdateUser(context.Context, *User) error
|
||||
DeleteUser(context.Context, uint) error
|
||||
UpdatePassword(context.Context, uint, string) error
|
||||
UpdatePassword(context.Context, uint, string, bool) error
|
||||
SetUserAuthorities(context.Context, uint, []uint) error
|
||||
SetUserAuthority(context.Context, uint, uint) error
|
||||
SetUserSetting(context.Context, uint, map[string]any) error
|
||||
ListAuthorities(context.Context) ([]*Authority, error)
|
||||
PersistConfig(context.Context) error
|
||||
}
|
||||
|
||||
type SystemUsecase struct {
|
||||
@@ -102,7 +107,13 @@ func (uc *SystemUsecase) CacheDelete(ctx context.Context, key string) error {
|
||||
func (uc *SystemUsecase) IsInitialized(ctx context.Context) (bool, error) {
|
||||
return uc.repo.IsInitialized(ctx)
|
||||
}
|
||||
func (uc *SystemUsecase) Initialize(ctx context.Context) error { return uc.repo.Initialize(ctx) }
|
||||
func (uc *SystemUsecase) Initialize(ctx context.Context, config *DatabaseConfig) error {
|
||||
if config == nil || len(config.AdminPassword) < 6 {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
return uc.repo.Initialize(ctx, config)
|
||||
}
|
||||
func (uc *SystemUsecase) PersistConfig(ctx context.Context) error { return uc.repo.PersistConfig(ctx) }
|
||||
|
||||
func (uc *SystemUsecase) Login(ctx context.Context, username, password string) (*User, error) {
|
||||
u, err := uc.repo.FindUserByUsername(ctx, username)
|
||||
@@ -142,7 +153,7 @@ func (uc *SystemUsecase) ResetPassword(ctx context.Context, id uint, password st
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return uc.repo.UpdatePassword(ctx, id, string(hash))
|
||||
return uc.repo.UpdatePassword(ctx, id, string(hash), false)
|
||||
}
|
||||
func (uc *SystemUsecase) ChangePassword(ctx context.Context, id uint, oldPassword, newPassword string) error {
|
||||
user, err := uc.repo.FindUserByID(ctx, id)
|
||||
@@ -152,7 +163,11 @@ func (uc *SystemUsecase) ChangePassword(ctx context.Context, id uint, oldPasswor
|
||||
if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(oldPassword)) != nil {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
return uc.ResetPassword(ctx, id, newPassword)
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return uc.repo.UpdatePassword(ctx, id, string(hash), true)
|
||||
}
|
||||
func (uc *SystemUsecase) Authorities(ctx context.Context) ([]*Authority, error) {
|
||||
return uc.repo.ListAuthorities(ctx)
|
||||
|
||||
Generated
+482
-40
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.1
|
||||
// protoc (unknown)
|
||||
// source: conf/conf.proto
|
||||
|
||||
package conf
|
||||
@@ -188,12 +188,15 @@ func (x *Data) GetRedis() *Data_Redis {
|
||||
|
||||
// AdminBackend contains settings for the administration HTTP transport.
|
||||
type AdminBackend struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RouterPrefix string `protobuf:"bytes,1,opt,name=router_prefix,json=routerPrefix,proto3" json:"router_prefix,omitempty"`
|
||||
Jwt *AdminBackend_JWT `protobuf:"bytes,2,opt,name=jwt,proto3" json:"jwt,omitempty"`
|
||||
Captcha *AdminBackend_Captcha `protobuf:"bytes,3,opt,name=captcha,proto3" json:"captcha,omitempty"`
|
||||
Local *AdminBackend_Local `protobuf:"bytes,4,opt,name=local,proto3" json:"local,omitempty"`
|
||||
Email *AdminBackend_Email `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RouterPrefix string `protobuf:"bytes,1,opt,name=router_prefix,json=routerPrefix,proto3" json:"router_prefix,omitempty"`
|
||||
Jwt *AdminBackend_JWT `protobuf:"bytes,2,opt,name=jwt,proto3" json:"jwt,omitempty"`
|
||||
Captcha *AdminBackend_Captcha `protobuf:"bytes,3,opt,name=captcha,proto3" json:"captcha,omitempty"`
|
||||
Local *AdminBackend_Local `protobuf:"bytes,4,opt,name=local,proto3" json:"local,omitempty"`
|
||||
Email *AdminBackend_Email `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"`
|
||||
Storage *AdminBackend_Storage `protobuf:"bytes,6,opt,name=storage,proto3" json:"storage,omitempty"`
|
||||
// ConfigPath is populated by the entrypoint and is not required in YAML.
|
||||
ConfigPath string `protobuf:"bytes,7,opt,name=config_path,json=configPath,proto3" json:"config_path,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -263,6 +266,20 @@ func (x *AdminBackend) GetEmail() *AdminBackend_Email {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AdminBackend) GetStorage() *AdminBackend_Storage {
|
||||
if x != nil {
|
||||
return x.Storage
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AdminBackend) GetConfigPath() string {
|
||||
if x != nil {
|
||||
return x.ConfigPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Server_HTTP struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Network string `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"`
|
||||
@@ -387,6 +404,13 @@ type Data_Database struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Driver string `protobuf:"bytes,1,opt,name=driver,proto3" json:"driver,omitempty"`
|
||||
Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"`
|
||||
Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"`
|
||||
Port string `protobuf:"bytes,4,opt,name=port,proto3" json:"port,omitempty"`
|
||||
User string `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"`
|
||||
Password string `protobuf:"bytes,6,opt,name=password,proto3" json:"password,omitempty"`
|
||||
Name string `protobuf:"bytes,7,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Config string `protobuf:"bytes,8,opt,name=config,proto3" json:"config,omitempty"`
|
||||
Path string `protobuf:"bytes,9,opt,name=path,proto3" json:"path,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -435,6 +459,55 @@ func (x *Data_Database) GetSource() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Data_Database) GetHost() string {
|
||||
if x != nil {
|
||||
return x.Host
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Data_Database) GetPort() string {
|
||||
if x != nil {
|
||||
return x.Port
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Data_Database) GetUser() string {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Data_Database) GetPassword() string {
|
||||
if x != nil {
|
||||
return x.Password
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Data_Database) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Data_Database) GetConfig() string {
|
||||
if x != nil {
|
||||
return x.Config
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Data_Database) GetPath() string {
|
||||
if x != nil {
|
||||
return x.Path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Data_Redis struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Network string `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"`
|
||||
@@ -791,6 +864,316 @@ func (x *AdminBackend_Email) GetIsLoginAuth() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type AdminBackend_Storage struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Qiniu *AdminBackend_Qiniu `protobuf:"bytes,2,opt,name=qiniu,proto3" json:"qiniu,omitempty"`
|
||||
AliyunOss *AdminBackend_ObjectStore `protobuf:"bytes,3,opt,name=aliyun_oss,json=aliyunOss,proto3" json:"aliyun_oss,omitempty"`
|
||||
HuaweiObs *AdminBackend_ObjectStore `protobuf:"bytes,4,opt,name=huawei_obs,json=huaweiObs,proto3" json:"huawei_obs,omitempty"`
|
||||
TencentCos *AdminBackend_ObjectStore `protobuf:"bytes,5,opt,name=tencent_cos,json=tencentCos,proto3" json:"tencent_cos,omitempty"`
|
||||
AwsS3 *AdminBackend_ObjectStore `protobuf:"bytes,6,opt,name=aws_s3,json=awsS3,proto3" json:"aws_s3,omitempty"`
|
||||
CloudflareR2 *AdminBackend_ObjectStore `protobuf:"bytes,7,opt,name=cloudflare_r2,json=cloudflareR2,proto3" json:"cloudflare_r2,omitempty"`
|
||||
Minio *AdminBackend_ObjectStore `protobuf:"bytes,8,opt,name=minio,proto3" json:"minio,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Storage) Reset() {
|
||||
*x = AdminBackend_Storage{}
|
||||
mi := &file_conf_conf_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Storage) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AdminBackend_Storage) ProtoMessage() {}
|
||||
|
||||
func (x *AdminBackend_Storage) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_conf_conf_proto_msgTypes[12]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AdminBackend_Storage.ProtoReflect.Descriptor instead.
|
||||
func (*AdminBackend_Storage) Descriptor() ([]byte, []int) {
|
||||
return file_conf_conf_proto_rawDescGZIP(), []int{3, 4}
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Storage) GetType() string {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Storage) GetQiniu() *AdminBackend_Qiniu {
|
||||
if x != nil {
|
||||
return x.Qiniu
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Storage) GetAliyunOss() *AdminBackend_ObjectStore {
|
||||
if x != nil {
|
||||
return x.AliyunOss
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Storage) GetHuaweiObs() *AdminBackend_ObjectStore {
|
||||
if x != nil {
|
||||
return x.HuaweiObs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Storage) GetTencentCos() *AdminBackend_ObjectStore {
|
||||
if x != nil {
|
||||
return x.TencentCos
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Storage) GetAwsS3() *AdminBackend_ObjectStore {
|
||||
if x != nil {
|
||||
return x.AwsS3
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Storage) GetCloudflareR2() *AdminBackend_ObjectStore {
|
||||
if x != nil {
|
||||
return x.CloudflareR2
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Storage) GetMinio() *AdminBackend_ObjectStore {
|
||||
if x != nil {
|
||||
return x.Minio
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AdminBackend_Qiniu struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Zone string `protobuf:"bytes,1,opt,name=zone,proto3" json:"zone,omitempty"`
|
||||
Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"`
|
||||
BaseUrl string `protobuf:"bytes,3,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"`
|
||||
AccessKey string `protobuf:"bytes,4,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"`
|
||||
SecretKey string `protobuf:"bytes,5,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"`
|
||||
UseHttps bool `protobuf:"varint,6,opt,name=use_https,json=useHttps,proto3" json:"use_https,omitempty"`
|
||||
UseCdnDomains bool `protobuf:"varint,7,opt,name=use_cdn_domains,json=useCdnDomains,proto3" json:"use_cdn_domains,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Qiniu) Reset() {
|
||||
*x = AdminBackend_Qiniu{}
|
||||
mi := &file_conf_conf_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Qiniu) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AdminBackend_Qiniu) ProtoMessage() {}
|
||||
|
||||
func (x *AdminBackend_Qiniu) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_conf_conf_proto_msgTypes[13]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AdminBackend_Qiniu.ProtoReflect.Descriptor instead.
|
||||
func (*AdminBackend_Qiniu) Descriptor() ([]byte, []int) {
|
||||
return file_conf_conf_proto_rawDescGZIP(), []int{3, 5}
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Qiniu) GetZone() string {
|
||||
if x != nil {
|
||||
return x.Zone
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Qiniu) GetBucket() string {
|
||||
if x != nil {
|
||||
return x.Bucket
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Qiniu) GetBaseUrl() string {
|
||||
if x != nil {
|
||||
return x.BaseUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Qiniu) GetAccessKey() string {
|
||||
if x != nil {
|
||||
return x.AccessKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Qiniu) GetSecretKey() string {
|
||||
if x != nil {
|
||||
return x.SecretKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Qiniu) GetUseHttps() bool {
|
||||
if x != nil {
|
||||
return x.UseHttps
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *AdminBackend_Qiniu) GetUseCdnDomains() bool {
|
||||
if x != nil {
|
||||
return x.UseCdnDomains
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ObjectStore covers S3-compatible configuration used by public cloud
|
||||
// providers and self-hosted MinIO.
|
||||
type AdminBackend_ObjectStore struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Endpoint string `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
|
||||
Region string `protobuf:"bytes,2,opt,name=region,proto3" json:"region,omitempty"`
|
||||
Bucket string `protobuf:"bytes,3,opt,name=bucket,proto3" json:"bucket,omitempty"`
|
||||
AccessKey string `protobuf:"bytes,4,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"`
|
||||
SecretKey string `protobuf:"bytes,5,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"`
|
||||
BaseUrl string `protobuf:"bytes,6,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"`
|
||||
PathPrefix string `protobuf:"bytes,7,opt,name=path_prefix,json=pathPrefix,proto3" json:"path_prefix,omitempty"`
|
||||
UseSsl bool `protobuf:"varint,8,opt,name=use_ssl,json=useSsl,proto3" json:"use_ssl,omitempty"`
|
||||
ForcePathStyle bool `protobuf:"varint,9,opt,name=force_path_style,json=forcePathStyle,proto3" json:"force_path_style,omitempty"`
|
||||
AccountId string `protobuf:"bytes,10,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AdminBackend_ObjectStore) Reset() {
|
||||
*x = AdminBackend_ObjectStore{}
|
||||
mi := &file_conf_conf_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AdminBackend_ObjectStore) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AdminBackend_ObjectStore) ProtoMessage() {}
|
||||
|
||||
func (x *AdminBackend_ObjectStore) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_conf_conf_proto_msgTypes[14]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AdminBackend_ObjectStore.ProtoReflect.Descriptor instead.
|
||||
func (*AdminBackend_ObjectStore) Descriptor() ([]byte, []int) {
|
||||
return file_conf_conf_proto_rawDescGZIP(), []int{3, 6}
|
||||
}
|
||||
|
||||
func (x *AdminBackend_ObjectStore) GetEndpoint() string {
|
||||
if x != nil {
|
||||
return x.Endpoint
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AdminBackend_ObjectStore) GetRegion() string {
|
||||
if x != nil {
|
||||
return x.Region
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AdminBackend_ObjectStore) GetBucket() string {
|
||||
if x != nil {
|
||||
return x.Bucket
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AdminBackend_ObjectStore) GetAccessKey() string {
|
||||
if x != nil {
|
||||
return x.AccessKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AdminBackend_ObjectStore) GetSecretKey() string {
|
||||
if x != nil {
|
||||
return x.SecretKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AdminBackend_ObjectStore) GetBaseUrl() string {
|
||||
if x != nil {
|
||||
return x.BaseUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AdminBackend_ObjectStore) GetPathPrefix() string {
|
||||
if x != nil {
|
||||
return x.PathPrefix
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AdminBackend_ObjectStore) GetUseSsl() bool {
|
||||
if x != nil {
|
||||
return x.UseSsl
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *AdminBackend_ObjectStore) GetForcePathStyle() bool {
|
||||
if x != nil {
|
||||
return x.ForcePathStyle
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *AdminBackend_ObjectStore) GetAccountId() string {
|
||||
if x != nil {
|
||||
return x.AccountId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_conf_conf_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_conf_conf_proto_rawDesc = "" +
|
||||
@@ -811,24 +1194,34 @@ const file_conf_conf_proto_rawDesc = "" +
|
||||
"\x04GRPC\x12\x18\n" +
|
||||
"\anetwork\x18\x01 \x01(\tR\anetwork\x12\x12\n" +
|
||||
"\x04addr\x18\x02 \x01(\tR\x04addr\x123\n" +
|
||||
"\atimeout\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\atimeout\"\xdd\x02\n" +
|
||||
"\atimeout\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\atimeout\"\xf6\x03\n" +
|
||||
"\x04Data\x125\n" +
|
||||
"\bdatabase\x18\x01 \x01(\v2\x19.kratos.api.Data.DatabaseR\bdatabase\x12,\n" +
|
||||
"\x05redis\x18\x02 \x01(\v2\x16.kratos.api.Data.RedisR\x05redis\x1a:\n" +
|
||||
"\x05redis\x18\x02 \x01(\v2\x16.kratos.api.Data.RedisR\x05redis\x1a\xd2\x01\n" +
|
||||
"\bDatabase\x12\x16\n" +
|
||||
"\x06driver\x18\x01 \x01(\tR\x06driver\x12\x16\n" +
|
||||
"\x06source\x18\x02 \x01(\tR\x06source\x1a\xb3\x01\n" +
|
||||
"\x06source\x18\x02 \x01(\tR\x06source\x12\x12\n" +
|
||||
"\x04host\x18\x03 \x01(\tR\x04host\x12\x12\n" +
|
||||
"\x04port\x18\x04 \x01(\tR\x04port\x12\x12\n" +
|
||||
"\x04user\x18\x05 \x01(\tR\x04user\x12\x1a\n" +
|
||||
"\bpassword\x18\x06 \x01(\tR\bpassword\x12\x12\n" +
|
||||
"\x04name\x18\a \x01(\tR\x04name\x12\x16\n" +
|
||||
"\x06config\x18\b \x01(\tR\x06config\x12\x12\n" +
|
||||
"\x04path\x18\t \x01(\tR\x04path\x1a\xb3\x01\n" +
|
||||
"\x05Redis\x12\x18\n" +
|
||||
"\anetwork\x18\x01 \x01(\tR\anetwork\x12\x12\n" +
|
||||
"\x04addr\x18\x02 \x01(\tR\x04addr\x12<\n" +
|
||||
"\fread_timeout\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\vreadTimeout\x12>\n" +
|
||||
"\rwrite_timeout\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\fwriteTimeout\"\xfd\x06\n" +
|
||||
"\rwrite_timeout\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\fwriteTimeout\"\xd1\x0f\n" +
|
||||
"\fAdminBackend\x12#\n" +
|
||||
"\rrouter_prefix\x18\x01 \x01(\tR\frouterPrefix\x12.\n" +
|
||||
"\x03jwt\x18\x02 \x01(\v2\x1c.kratos.api.AdminBackend.JWTR\x03jwt\x12:\n" +
|
||||
"\acaptcha\x18\x03 \x01(\v2 .kratos.api.AdminBackend.CaptchaR\acaptcha\x124\n" +
|
||||
"\x05local\x18\x04 \x01(\v2\x1e.kratos.api.AdminBackend.LocalR\x05local\x124\n" +
|
||||
"\x05email\x18\x05 \x01(\v2\x1e.kratos.api.AdminBackend.EmailR\x05email\x1a\xb8\x01\n" +
|
||||
"\x05email\x18\x05 \x01(\v2\x1e.kratos.api.AdminBackend.EmailR\x05email\x12:\n" +
|
||||
"\astorage\x18\x06 \x01(\v2 .kratos.api.AdminBackend.StorageR\astorage\x12\x1f\n" +
|
||||
"\vconfig_path\x18\a \x01(\tR\n" +
|
||||
"configPath\x1a\xb8\x01\n" +
|
||||
"\x03JWT\x12\x1f\n" +
|
||||
"\vsigning_key\x18\x01 \x01(\tR\n" +
|
||||
"signingKey\x12<\n" +
|
||||
@@ -855,7 +1248,45 @@ const file_conf_conf_proto_rawDesc = "" +
|
||||
"\bnickname\x18\x05 \x01(\tR\bnickname\x12\x12\n" +
|
||||
"\x04port\x18\x06 \x01(\x05R\x04port\x12\x15\n" +
|
||||
"\x06is_ssl\x18\a \x01(\bR\x05isSsl\x12\"\n" +
|
||||
"\ris_login_auth\x18\b \x01(\bR\visLoginAuthB\x18Z\x16kra/internal/conf;confb\x06proto3"
|
||||
"\ris_login_auth\x18\b \x01(\bR\visLoginAuth\x1a\xe8\x03\n" +
|
||||
"\aStorage\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x124\n" +
|
||||
"\x05qiniu\x18\x02 \x01(\v2\x1e.kratos.api.AdminBackend.QiniuR\x05qiniu\x12C\n" +
|
||||
"\n" +
|
||||
"aliyun_oss\x18\x03 \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\taliyunOss\x12C\n" +
|
||||
"\n" +
|
||||
"huawei_obs\x18\x04 \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\thuaweiObs\x12E\n" +
|
||||
"\vtencent_cos\x18\x05 \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\n" +
|
||||
"tencentCos\x12;\n" +
|
||||
"\x06aws_s3\x18\x06 \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\x05awsS3\x12I\n" +
|
||||
"\rcloudflare_r2\x18\a \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\fcloudflareR2\x12:\n" +
|
||||
"\x05minio\x18\b \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\x05minio\x1a\xd1\x01\n" +
|
||||
"\x05Qiniu\x12\x12\n" +
|
||||
"\x04zone\x18\x01 \x01(\tR\x04zone\x12\x16\n" +
|
||||
"\x06bucket\x18\x02 \x01(\tR\x06bucket\x12\x19\n" +
|
||||
"\bbase_url\x18\x03 \x01(\tR\abaseUrl\x12\x1d\n" +
|
||||
"\n" +
|
||||
"access_key\x18\x04 \x01(\tR\taccessKey\x12\x1d\n" +
|
||||
"\n" +
|
||||
"secret_key\x18\x05 \x01(\tR\tsecretKey\x12\x1b\n" +
|
||||
"\tuse_https\x18\x06 \x01(\bR\buseHttps\x12&\n" +
|
||||
"\x0fuse_cdn_domains\x18\a \x01(\bR\ruseCdnDomains\x1a\xb5\x02\n" +
|
||||
"\vObjectStore\x12\x1a\n" +
|
||||
"\bendpoint\x18\x01 \x01(\tR\bendpoint\x12\x16\n" +
|
||||
"\x06region\x18\x02 \x01(\tR\x06region\x12\x16\n" +
|
||||
"\x06bucket\x18\x03 \x01(\tR\x06bucket\x12\x1d\n" +
|
||||
"\n" +
|
||||
"access_key\x18\x04 \x01(\tR\taccessKey\x12\x1d\n" +
|
||||
"\n" +
|
||||
"secret_key\x18\x05 \x01(\tR\tsecretKey\x12\x19\n" +
|
||||
"\bbase_url\x18\x06 \x01(\tR\abaseUrl\x12\x1f\n" +
|
||||
"\vpath_prefix\x18\a \x01(\tR\n" +
|
||||
"pathPrefix\x12\x17\n" +
|
||||
"\ause_ssl\x18\b \x01(\bR\x06useSsl\x12(\n" +
|
||||
"\x10force_path_style\x18\t \x01(\bR\x0eforcePathStyle\x12\x1d\n" +
|
||||
"\n" +
|
||||
"account_id\x18\n" +
|
||||
" \x01(\tR\taccountIdB\x18Z\x16kra/internal/conf;confb\x06proto3"
|
||||
|
||||
var (
|
||||
file_conf_conf_proto_rawDescOnce sync.Once
|
||||
@@ -869,21 +1300,24 @@ func file_conf_conf_proto_rawDescGZIP() []byte {
|
||||
return file_conf_conf_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_conf_conf_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
||||
var file_conf_conf_proto_msgTypes = make([]protoimpl.MessageInfo, 15)
|
||||
var file_conf_conf_proto_goTypes = []any{
|
||||
(*Bootstrap)(nil), // 0: kratos.api.Bootstrap
|
||||
(*Server)(nil), // 1: kratos.api.Server
|
||||
(*Data)(nil), // 2: kratos.api.Data
|
||||
(*AdminBackend)(nil), // 3: kratos.api.AdminBackend
|
||||
(*Server_HTTP)(nil), // 4: kratos.api.Server.HTTP
|
||||
(*Server_GRPC)(nil), // 5: kratos.api.Server.GRPC
|
||||
(*Data_Database)(nil), // 6: kratos.api.Data.Database
|
||||
(*Data_Redis)(nil), // 7: kratos.api.Data.Redis
|
||||
(*AdminBackend_JWT)(nil), // 8: kratos.api.AdminBackend.JWT
|
||||
(*AdminBackend_Captcha)(nil), // 9: kratos.api.AdminBackend.Captcha
|
||||
(*AdminBackend_Local)(nil), // 10: kratos.api.AdminBackend.Local
|
||||
(*AdminBackend_Email)(nil), // 11: kratos.api.AdminBackend.Email
|
||||
(*durationpb.Duration)(nil), // 12: google.protobuf.Duration
|
||||
(*Bootstrap)(nil), // 0: kratos.api.Bootstrap
|
||||
(*Server)(nil), // 1: kratos.api.Server
|
||||
(*Data)(nil), // 2: kratos.api.Data
|
||||
(*AdminBackend)(nil), // 3: kratos.api.AdminBackend
|
||||
(*Server_HTTP)(nil), // 4: kratos.api.Server.HTTP
|
||||
(*Server_GRPC)(nil), // 5: kratos.api.Server.GRPC
|
||||
(*Data_Database)(nil), // 6: kratos.api.Data.Database
|
||||
(*Data_Redis)(nil), // 7: kratos.api.Data.Redis
|
||||
(*AdminBackend_JWT)(nil), // 8: kratos.api.AdminBackend.JWT
|
||||
(*AdminBackend_Captcha)(nil), // 9: kratos.api.AdminBackend.Captcha
|
||||
(*AdminBackend_Local)(nil), // 10: kratos.api.AdminBackend.Local
|
||||
(*AdminBackend_Email)(nil), // 11: kratos.api.AdminBackend.Email
|
||||
(*AdminBackend_Storage)(nil), // 12: kratos.api.AdminBackend.Storage
|
||||
(*AdminBackend_Qiniu)(nil), // 13: kratos.api.AdminBackend.Qiniu
|
||||
(*AdminBackend_ObjectStore)(nil), // 14: kratos.api.AdminBackend.ObjectStore
|
||||
(*durationpb.Duration)(nil), // 15: google.protobuf.Duration
|
||||
}
|
||||
var file_conf_conf_proto_depIdxs = []int32{
|
||||
1, // 0: kratos.api.Bootstrap.server:type_name -> kratos.api.Server
|
||||
@@ -897,18 +1331,26 @@ var file_conf_conf_proto_depIdxs = []int32{
|
||||
9, // 8: kratos.api.AdminBackend.captcha:type_name -> kratos.api.AdminBackend.Captcha
|
||||
10, // 9: kratos.api.AdminBackend.local:type_name -> kratos.api.AdminBackend.Local
|
||||
11, // 10: kratos.api.AdminBackend.email:type_name -> kratos.api.AdminBackend.Email
|
||||
12, // 11: kratos.api.Server.HTTP.timeout:type_name -> google.protobuf.Duration
|
||||
12, // 12: kratos.api.Server.GRPC.timeout:type_name -> google.protobuf.Duration
|
||||
12, // 13: kratos.api.Data.Redis.read_timeout:type_name -> google.protobuf.Duration
|
||||
12, // 14: kratos.api.Data.Redis.write_timeout:type_name -> google.protobuf.Duration
|
||||
12, // 15: kratos.api.AdminBackend.JWT.expires_time:type_name -> google.protobuf.Duration
|
||||
12, // 16: kratos.api.AdminBackend.JWT.buffer_time:type_name -> google.protobuf.Duration
|
||||
12, // 17: kratos.api.AdminBackend.Captcha.store_expiration:type_name -> google.protobuf.Duration
|
||||
18, // [18:18] is the sub-list for method output_type
|
||||
18, // [18:18] is the sub-list for method input_type
|
||||
18, // [18:18] is the sub-list for extension type_name
|
||||
18, // [18:18] is the sub-list for extension extendee
|
||||
0, // [0:18] is the sub-list for field type_name
|
||||
12, // 11: kratos.api.AdminBackend.storage:type_name -> kratos.api.AdminBackend.Storage
|
||||
15, // 12: kratos.api.Server.HTTP.timeout:type_name -> google.protobuf.Duration
|
||||
15, // 13: kratos.api.Server.GRPC.timeout:type_name -> google.protobuf.Duration
|
||||
15, // 14: kratos.api.Data.Redis.read_timeout:type_name -> google.protobuf.Duration
|
||||
15, // 15: kratos.api.Data.Redis.write_timeout:type_name -> google.protobuf.Duration
|
||||
15, // 16: kratos.api.AdminBackend.JWT.expires_time:type_name -> google.protobuf.Duration
|
||||
15, // 17: kratos.api.AdminBackend.JWT.buffer_time:type_name -> google.protobuf.Duration
|
||||
15, // 18: kratos.api.AdminBackend.Captcha.store_expiration:type_name -> google.protobuf.Duration
|
||||
13, // 19: kratos.api.AdminBackend.Storage.qiniu:type_name -> kratos.api.AdminBackend.Qiniu
|
||||
14, // 20: kratos.api.AdminBackend.Storage.aliyun_oss:type_name -> kratos.api.AdminBackend.ObjectStore
|
||||
14, // 21: kratos.api.AdminBackend.Storage.huawei_obs:type_name -> kratos.api.AdminBackend.ObjectStore
|
||||
14, // 22: kratos.api.AdminBackend.Storage.tencent_cos:type_name -> kratos.api.AdminBackend.ObjectStore
|
||||
14, // 23: kratos.api.AdminBackend.Storage.aws_s3:type_name -> kratos.api.AdminBackend.ObjectStore
|
||||
14, // 24: kratos.api.AdminBackend.Storage.cloudflare_r2:type_name -> kratos.api.AdminBackend.ObjectStore
|
||||
14, // 25: kratos.api.AdminBackend.Storage.minio:type_name -> kratos.api.AdminBackend.ObjectStore
|
||||
26, // [26:26] is the sub-list for method output_type
|
||||
26, // [26:26] is the sub-list for method input_type
|
||||
26, // [26:26] is the sub-list for extension type_name
|
||||
26, // [26:26] is the sub-list for extension extendee
|
||||
0, // [0:26] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_conf_conf_proto_init() }
|
||||
@@ -922,7 +1364,7 @@ func file_conf_conf_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_conf_conf_proto_rawDesc), len(file_conf_conf_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 12,
|
||||
NumMessages: 15,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@@ -30,6 +30,13 @@ message Data {
|
||||
message Database {
|
||||
string driver = 1;
|
||||
string source = 2;
|
||||
string host = 3;
|
||||
string port = 4;
|
||||
string user = 5;
|
||||
string password = 6;
|
||||
string name = 7;
|
||||
string config = 8;
|
||||
string path = 9;
|
||||
}
|
||||
message Redis {
|
||||
string network = 1;
|
||||
@@ -48,6 +55,9 @@ message AdminBackend {
|
||||
Captcha captcha = 3;
|
||||
Local local = 4;
|
||||
Email email = 5;
|
||||
Storage storage = 6;
|
||||
// ConfigPath is populated by the entrypoint and is not required in YAML.
|
||||
string config_path = 7;
|
||||
|
||||
message JWT {
|
||||
string signing_key = 1;
|
||||
@@ -78,4 +88,40 @@ message AdminBackend {
|
||||
bool is_ssl = 7;
|
||||
bool is_login_auth = 8;
|
||||
}
|
||||
|
||||
message Storage {
|
||||
string type = 1;
|
||||
Qiniu qiniu = 2;
|
||||
ObjectStore aliyun_oss = 3;
|
||||
ObjectStore huawei_obs = 4;
|
||||
ObjectStore tencent_cos = 5;
|
||||
ObjectStore aws_s3 = 6;
|
||||
ObjectStore cloudflare_r2 = 7;
|
||||
ObjectStore minio = 8;
|
||||
}
|
||||
|
||||
message Qiniu {
|
||||
string zone = 1;
|
||||
string bucket = 2;
|
||||
string base_url = 3;
|
||||
string access_key = 4;
|
||||
string secret_key = 5;
|
||||
bool use_https = 6;
|
||||
bool use_cdn_domains = 7;
|
||||
}
|
||||
|
||||
// ObjectStore covers S3-compatible configuration used by public cloud
|
||||
// providers and self-hosted MinIO.
|
||||
message ObjectStore {
|
||||
string endpoint = 1;
|
||||
string region = 2;
|
||||
string bucket = 3;
|
||||
string access_key = 4;
|
||||
string secret_key = 5;
|
||||
string base_url = 6;
|
||||
string path_prefix = 7;
|
||||
bool use_ssl = 8;
|
||||
bool force_path_style = 9;
|
||||
string account_id = 10;
|
||||
}
|
||||
}
|
||||
|
||||
+50
-92
@@ -2,126 +2,84 @@ package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/go-kratos/aip-go/ents"
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/biz"
|
||||
"kra/internal/data/ent"
|
||||
"kra/internal/data/ent/admin"
|
||||
)
|
||||
|
||||
func convertAdmin(po *ent.Admin) *biz.Admin {
|
||||
return &biz.Admin{
|
||||
ID: po.ID,
|
||||
Name: po.Name,
|
||||
Email: po.Email,
|
||||
Avatar: po.Avatar,
|
||||
Access: po.Access,
|
||||
Password: po.Password,
|
||||
CreateTime: po.CreateTime,
|
||||
UpdateTime: po.UpdateTime,
|
||||
type adminPO struct {
|
||||
ID int64 `gorm:"primaryKey;autoIncrement"`
|
||||
Name string `gorm:"uniqueIndex"`
|
||||
Email string `gorm:"uniqueIndex"`
|
||||
Password string
|
||||
Access string
|
||||
Avatar string
|
||||
CreateTime time.Time
|
||||
UpdateTime time.Time
|
||||
}
|
||||
|
||||
func (adminPO) TableName() string { return "admins" }
|
||||
func convertAdmin(po *adminPO) *biz.Admin {
|
||||
return &biz.Admin{ID: po.ID, Name: po.Name, Email: po.Email, Password: po.Password, Access: po.Access, Avatar: po.Avatar, CreateTime: po.CreateTime, UpdateTime: po.UpdateTime}
|
||||
}
|
||||
|
||||
type adminRepo struct{ data *Data }
|
||||
|
||||
func NewAdminRepo(data *Data) biz.AdminRepo { return &adminRepo{data: data} }
|
||||
func (r *adminRepo) one(ctx context.Context, query *gorm.DB) (*biz.Admin, error) {
|
||||
var po adminPO
|
||||
if err := query.WithContext(ctx).First(&po).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, biz.ErrAdminNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return convertAdmin(&po), nil
|
||||
}
|
||||
|
||||
type adminRepo struct {
|
||||
data *Data
|
||||
}
|
||||
|
||||
// NewAdminRepo creates a new AdminRepo instance.
|
||||
func NewAdminRepo(data *Data) biz.AdminRepo {
|
||||
return &adminRepo{
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *adminRepo) FindByID(ctx context.Context, id int64) (*biz.Admin, error) {
|
||||
po, err := r.data.db.Admin.Get(ctx, id)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
return nil, biz.ErrAdminNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return convertAdmin(po), nil
|
||||
return r.one(ctx, r.data.gormDB.Where("id = ?", id))
|
||||
}
|
||||
|
||||
func (r *adminRepo) FindByName(ctx context.Context, name string) (*biz.Admin, error) {
|
||||
po, err := r.data.db.Admin.Query().Where(admin.NameEQ(name)).Only(ctx)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
return nil, biz.ErrAdminNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return convertAdmin(po), nil
|
||||
return r.one(ctx, r.data.gormDB.Where("name = ?", name))
|
||||
}
|
||||
|
||||
func (r *adminRepo) FindByEmail(ctx context.Context, email string) (*biz.Admin, error) {
|
||||
po, err := r.data.db.Admin.Query().Where(admin.EmailEQ(email)).Only(ctx)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
return nil, biz.ErrAdminNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return convertAdmin(po), nil
|
||||
return r.one(ctx, r.data.gormDB.Where("email = ?", email))
|
||||
}
|
||||
|
||||
func (r *adminRepo) ListAdmins(ctx context.Context, opts ...biz.ListOption) ([]*biz.Admin, error) {
|
||||
o := biz.ListOptions{Limit: 20}
|
||||
for _, opt := range opts {
|
||||
opt(&o)
|
||||
}
|
||||
pos, err := r.data.db.Admin.Query().
|
||||
Where(ents.ApplyFilter(o.Filter)).
|
||||
Order(ents.ApplyOrderBy(o.OrderBy)).
|
||||
Offset(o.Offset).
|
||||
Limit(o.Limit).
|
||||
All(ctx)
|
||||
if err != nil {
|
||||
var pos []adminPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Offset(o.Offset).Limit(o.Limit).Order("id asc").Find(&pos).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var admins []*biz.Admin
|
||||
for _, po := range pos {
|
||||
admins = append(admins, convertAdmin(po))
|
||||
out := make([]*biz.Admin, 0, len(pos))
|
||||
for i := range pos {
|
||||
out = append(out, convertAdmin(&pos[i]))
|
||||
}
|
||||
return admins, nil
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *adminRepo) CreateAdmin(ctx context.Context, admin *biz.Admin) (*biz.Admin, error) {
|
||||
po, err := r.data.db.Admin.Create().
|
||||
SetName(admin.Name).
|
||||
SetEmail(admin.Email).
|
||||
SetAvatar(admin.Avatar).
|
||||
SetAccess(admin.Access).
|
||||
SetPassword(admin.Password).
|
||||
SetCreateTime(time.Now()).
|
||||
SetUpdateTime(time.Now()).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
func (r *adminRepo) CreateAdmin(ctx context.Context, a *biz.Admin) (*biz.Admin, error) {
|
||||
now := time.Now()
|
||||
po := adminPO{Name: a.Name, Email: a.Email, Password: a.Password, Access: a.Access, Avatar: a.Avatar, CreateTime: now, UpdateTime: now}
|
||||
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertAdmin(po), nil
|
||||
return convertAdmin(&po), nil
|
||||
}
|
||||
|
||||
func (r *adminRepo) UpdateAdmin(ctx context.Context, admin *biz.Admin) (*biz.Admin, error) {
|
||||
update := r.data.db.Admin.UpdateOneID(admin.ID).
|
||||
SetName(admin.Name).
|
||||
SetEmail(admin.Email).
|
||||
SetAccess(admin.Access).
|
||||
SetAvatar(admin.Avatar).
|
||||
SetUpdateTime(time.Now())
|
||||
// Only update the password if it's not empty
|
||||
if admin.Password != "" {
|
||||
update.SetPassword(admin.Password)
|
||||
func (r *adminRepo) UpdateAdmin(ctx context.Context, a *biz.Admin) (*biz.Admin, error) {
|
||||
values := map[string]any{"name": a.Name, "email": a.Email, "access": a.Access, "avatar": a.Avatar, "update_time": time.Now()}
|
||||
if a.Password != "" {
|
||||
values["password"] = a.Password
|
||||
}
|
||||
po, err := update.Save(ctx)
|
||||
if err != nil {
|
||||
if err := r.data.gormDB.WithContext(ctx).Model(&adminPO{}).Where("id = ?", a.ID).Updates(values).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertAdmin(po), nil
|
||||
return r.FindByID(ctx, a.ID)
|
||||
}
|
||||
|
||||
func (r *adminRepo) DeleteAdmin(ctx context.Context, id int64) error {
|
||||
return r.data.db.Admin.DeleteOneID(id).Exec(ctx)
|
||||
return r.data.gormDB.WithContext(ctx).Delete(&adminPO{}, id).Error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func protoMap(message proto.Message) (map[string]any, error) {
|
||||
raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var value map[string]any
|
||||
if err = json.Unmarshal(raw, &value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
delete(value, "config_path")
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func setYAMLMapping(node *yaml.Node, key string, value any) error {
|
||||
if node.Kind == yaml.DocumentNode {
|
||||
node = node.Content[0]
|
||||
}
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return fmt.Errorf("configuration root is not a mapping")
|
||||
}
|
||||
raw, err := yaml.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var replacement yaml.Node
|
||||
if err = yaml.Unmarshal(raw, &replacement); err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
if node.Content[i].Value == key {
|
||||
node.Content[i+1] = replacement.Content[0]
|
||||
return nil
|
||||
}
|
||||
}
|
||||
node.Content = append(node.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: key}, replacement.Content[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Data) persistConfig() error {
|
||||
if d.admin == nil || d.admin.ConfigPath == "" {
|
||||
return nil
|
||||
}
|
||||
raw, err := os.ReadFile(d.admin.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var document yaml.Node
|
||||
if err = yaml.Unmarshal(raw, &document); err != nil {
|
||||
return err
|
||||
}
|
||||
dataValue, err := protoMap(d.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
adminValue, err := protoMap(d.admin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = setYAMLMapping(&document, "data", dataValue); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = setYAMLMapping(&document, "admin", adminValue); err != nil {
|
||||
return err
|
||||
}
|
||||
output, err := yaml.Marshal(&document)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.MkdirAll(filepath.Dir(d.admin.ConfigPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
temporary, err := os.CreateTemp(filepath.Dir(d.admin.ConfigPath), ".kra-config-*.yaml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempName := temporary.Name()
|
||||
defer os.Remove(tempName)
|
||||
if _, err = temporary.Write(output); err != nil {
|
||||
_ = temporary.Close()
|
||||
return err
|
||||
}
|
||||
if err = temporary.Chmod(0o600); err != nil {
|
||||
_ = temporary.Close()
|
||||
return err
|
||||
}
|
||||
if err = temporary.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tempName, d.admin.ConfigPath)
|
||||
}
|
||||
+55
-48
@@ -4,55 +4,47 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/google/wire"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/conf"
|
||||
"kra/internal/data/ent"
|
||||
"kra/internal/data/ent/migrate"
|
||||
)
|
||||
|
||||
// ProviderSet is data providers.
|
||||
var ProviderSet = wire.NewSet(NewData, NewAdminRepo, NewSystemRepo, NewAccessRepo, NewSettingsRepo, NewVersionRepo, NewExportRepo, NewAuditRepo, NewTaskRepo, NewMediaRepo, NewAnnouncementRepo, NewEmailRepo, NewCache, NewFileStorage)
|
||||
|
||||
// Data is a struct that contains the database client.
|
||||
type Data struct {
|
||||
db *ent.Client
|
||||
gormDB *gorm.DB
|
||||
redis *redis.Client
|
||||
mu sync.RWMutex
|
||||
gormDB *gorm.DB
|
||||
redis *redis.Client
|
||||
database *conf.Data_Database
|
||||
config *conf.Data
|
||||
admin *conf.AdminBackend
|
||||
}
|
||||
|
||||
// NewData creates a new Data instance.
|
||||
func NewData(c *conf.Data) (*Data, func(), error) {
|
||||
func NewData(c *conf.Data, admin *conf.AdminBackend) (*Data, func(), error) {
|
||||
if c == nil || c.Database == nil {
|
||||
return nil, nil, fmt.Errorf("database configuration is required")
|
||||
}
|
||||
db, err := ent.Open(c.Database.Driver, c.Database.Source)
|
||||
d := &Data{database: c.Database, config: c, admin: admin}
|
||||
db, err := openDatabase(c.Database, false)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("open ent database: %w", err)
|
||||
}
|
||||
gormDB, err := gorm.Open(mysql.Open(c.Database.Source), &gorm.Config{})
|
||||
if err != nil {
|
||||
_ = db.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
// The announcement module owns its table lifecycle independently of the
|
||||
// first-run database initializer.
|
||||
if err = gormDB.AutoMigrate(&announcementPO{}); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, nil, fmt.Errorf("migrate announcement table: %w", err)
|
||||
}
|
||||
var redisClient *redis.Client
|
||||
if c.Redis != nil && c.Redis.Addr != "" {
|
||||
options := &redis.Options{Addr: c.Redis.Addr}
|
||||
if c.Redis.Network != "" {
|
||||
options.Network = c.Redis.Network
|
||||
// The initialization endpoint must remain available when the configured
|
||||
// target database has not been created yet.
|
||||
log.Printf("configured database unavailable before initialization: %v", err)
|
||||
db, err = openFallbackDatabase()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("open bootstrap database: %w", err)
|
||||
}
|
||||
}
|
||||
d.gormDB = db
|
||||
if err = db.AutoMigrate(&announcementPO{}, &adminPO{}); err != nil {
|
||||
return nil, nil, fmt.Errorf("migrate bootstrap tables: %w", err)
|
||||
}
|
||||
if c.Redis != nil && c.Redis.Addr != "" {
|
||||
options := &redis.Options{Addr: c.Redis.Addr, Network: c.Redis.Network}
|
||||
if c.Redis.ReadTimeout != nil {
|
||||
options.ReadTimeout = c.Redis.ReadTimeout.AsDuration()
|
||||
}
|
||||
@@ -65,27 +57,42 @@ func NewData(c *conf.Data) (*Data, func(), error) {
|
||||
log.Printf("redis unavailable, using in-memory cache: %v", pingErr)
|
||||
_ = candidate.Close()
|
||||
} else {
|
||||
redisClient = candidate
|
||||
d.redis = candidate
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
if os.Getenv("DEPLOY_ENV") == "dev" {
|
||||
// Enable debug mode for detailed logging.
|
||||
db = db.Debug()
|
||||
// Run the auto migration tool.
|
||||
if err = db.Schema.Create(context.Background(), migrate.WithDropIndex(true)); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
cleanup := func() {
|
||||
_ = db.Close()
|
||||
if redisClient != nil {
|
||||
_ = redisClient.Close()
|
||||
d.mu.RLock()
|
||||
db := d.gormDB
|
||||
d.mu.RUnlock()
|
||||
if sqlDB, closeErr := db.DB(); closeErr == nil {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
if d.redis != nil {
|
||||
_ = d.redis.Close()
|
||||
}
|
||||
}
|
||||
return &Data{
|
||||
db: db,
|
||||
gormDB: gormDB,
|
||||
redis: redisClient,
|
||||
}, cleanup, nil
|
||||
return d, cleanup, nil
|
||||
}
|
||||
|
||||
func (d *Data) switchDatabase(config *conf.Data_Database) error {
|
||||
db, err := openDatabase(config, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.mu.Lock()
|
||||
old := d.gormDB
|
||||
d.gormDB = db
|
||||
d.database = config
|
||||
d.config.Database = config
|
||||
d.mu.Unlock()
|
||||
if err := d.persistConfig(); err != nil {
|
||||
return fmt.Errorf("persist database configuration: %w", err)
|
||||
}
|
||||
if old != nil {
|
||||
if sqlDB, e := old.DB(); e == nil {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
oracle "github.com/dzwvip/gorm-oracle"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlserver"
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/conf"
|
||||
)
|
||||
|
||||
var databaseNamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_$-]*$`)
|
||||
|
||||
func normalizedDriver(driver string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(driver)) {
|
||||
case "postgres", "postgresql", "pgsql":
|
||||
return "pgsql"
|
||||
case "sqlserver", "mssql":
|
||||
return "mssql"
|
||||
case "sqlite", "sqlite3":
|
||||
return "sqlite"
|
||||
case "oracle":
|
||||
return "oracle"
|
||||
default:
|
||||
return "mysql"
|
||||
}
|
||||
}
|
||||
|
||||
func databaseDSN(c *conf.Data_Database, name string) (string, error) {
|
||||
if c.Source != "" && c.Host == "" && c.Path == "" {
|
||||
return c.Source, nil
|
||||
}
|
||||
driver := normalizedDriver(c.Driver)
|
||||
if name == "" {
|
||||
name = c.Name
|
||||
}
|
||||
host := c.Host
|
||||
if host == "" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
switch driver {
|
||||
case "mysql":
|
||||
port := c.Port
|
||||
if port == "" {
|
||||
port = "3306"
|
||||
}
|
||||
query := c.Config
|
||||
if query == "" {
|
||||
query = "timeout=5s&parseTime=True&loc=Local&charset=utf8mb4"
|
||||
}
|
||||
return fmt.Sprintf("%s:%s@tcp(%s)/%s?%s", c.User, c.Password, net.JoinHostPort(host, port), name, query), nil
|
||||
case "pgsql":
|
||||
port := c.Port
|
||||
if port == "" {
|
||||
port = "5432"
|
||||
}
|
||||
extra := c.Config
|
||||
if extra == "" {
|
||||
extra = "sslmode=disable TimeZone=Local"
|
||||
}
|
||||
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s %s", host, port, c.User, c.Password, name, extra), nil
|
||||
case "mssql":
|
||||
port := c.Port
|
||||
if port == "" {
|
||||
port = "1433"
|
||||
}
|
||||
q := url.Values{"database": {name}, "encrypt": {"disable"}}
|
||||
return (&url.URL{Scheme: "sqlserver", User: url.UserPassword(c.User, c.Password), Host: net.JoinHostPort(host, port), RawQuery: q.Encode()}).String(), nil
|
||||
case "oracle":
|
||||
port := c.Port
|
||||
if port == "" {
|
||||
port = "1521"
|
||||
}
|
||||
return fmt.Sprintf("oracle://%s:%s@%s/%s?%s", url.PathEscape(c.User), url.PathEscape(c.Password), net.JoinHostPort(host, port), url.PathEscape(name), c.Config), nil
|
||||
case "sqlite":
|
||||
path := c.Path
|
||||
if path == "" {
|
||||
path = "."
|
||||
}
|
||||
if name == "" {
|
||||
name = "kra"
|
||||
}
|
||||
if filepath.Ext(name) == "" {
|
||||
name += ".db"
|
||||
}
|
||||
return filepath.Join(path, name), nil
|
||||
}
|
||||
return "", fmt.Errorf("unsupported database driver %q", c.Driver)
|
||||
}
|
||||
|
||||
func openWithDriver(driver, dsn string) (*gorm.DB, error) {
|
||||
switch normalizedDriver(driver) {
|
||||
case "mysql":
|
||||
return gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
case "pgsql":
|
||||
return gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
case "mssql":
|
||||
return gorm.Open(sqlserver.Open(dsn), &gorm.Config{})
|
||||
case "oracle":
|
||||
return gorm.Open(oracle.Open(dsn), &gorm.Config{})
|
||||
case "sqlite":
|
||||
return gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported database driver %q", driver)
|
||||
}
|
||||
}
|
||||
|
||||
func openDatabase(c *conf.Data_Database, create bool) (*gorm.DB, error) {
|
||||
driver := normalizedDriver(c.Driver)
|
||||
if driver == "sqlite" {
|
||||
dsn, err := databaseDSN(c, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = os.MkdirAll(filepath.Dir(dsn), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return openWithDriver(driver, dsn)
|
||||
}
|
||||
if create && driver != "oracle" {
|
||||
if !databaseNamePattern.MatchString(c.Name) {
|
||||
return nil, fmt.Errorf("invalid database name %q", c.Name)
|
||||
}
|
||||
bootstrap := ""
|
||||
switch driver {
|
||||
case "pgsql":
|
||||
bootstrap = "postgres"
|
||||
case "mssql":
|
||||
bootstrap = "master"
|
||||
}
|
||||
dsn, err := databaseDSN(c, bootstrap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
adminDB, err := openWithDriver(driver, dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect database server: %w", err)
|
||||
}
|
||||
var statement string
|
||||
switch driver {
|
||||
case "mysql":
|
||||
statement = "CREATE DATABASE IF NOT EXISTS `" + c.Name + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"
|
||||
case "pgsql":
|
||||
var count int64
|
||||
if err = adminDB.Raw("SELECT count(*) FROM pg_database WHERE datname = ?", c.Name).Scan(&count).Error; err == nil && count == 0 {
|
||||
statement = `CREATE DATABASE "` + c.Name + `"`
|
||||
}
|
||||
case "mssql":
|
||||
statement = "IF DB_ID(N'" + c.Name + "') IS NULL CREATE DATABASE [" + c.Name + "]"
|
||||
}
|
||||
if statement != "" {
|
||||
err = adminDB.Exec(statement).Error
|
||||
}
|
||||
if sqlDB, e := adminDB.DB(); e == nil {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create database: %w", err)
|
||||
}
|
||||
}
|
||||
dsn, err := databaseDSN(c, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return openWithDriver(driver, dsn)
|
||||
}
|
||||
|
||||
func openFallbackDatabase() (*gorm.DB, error) {
|
||||
return openWithDriver("sqlite", "file:kra-bootstrap?mode=memory&cache=shared")
|
||||
}
|
||||
@@ -21,6 +21,33 @@ type fileStorage struct {
|
||||
}
|
||||
|
||||
func NewFileStorage(config *conf.AdminBackend) (biz.FileStorage, error) {
|
||||
storageType := "local"
|
||||
if config != nil && config.Storage != nil && config.Storage.Type != "" {
|
||||
storageType = strings.ToLower(config.Storage.Type)
|
||||
}
|
||||
if storageType == "qiniu" {
|
||||
return newQiniuStorage(config.Storage.Qiniu)
|
||||
}
|
||||
if storageType != "local" {
|
||||
var object *conf.AdminBackend_ObjectStore
|
||||
switch storageType {
|
||||
case "aliyun-oss":
|
||||
object = config.Storage.AliyunOss
|
||||
case "huawei-obs":
|
||||
object = config.Storage.HuaweiObs
|
||||
case "tencent-cos":
|
||||
object = config.Storage.TencentCos
|
||||
case "aws-s3":
|
||||
object = config.Storage.AwsS3
|
||||
case "cloudflare-r2":
|
||||
object = config.Storage.CloudflareR2
|
||||
case "minio":
|
||||
object = config.Storage.Minio
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported storage type %q", storageType)
|
||||
}
|
||||
return newS3Storage(storageType, object)
|
||||
}
|
||||
root, prefix := "uploads/file", "uploads/file"
|
||||
if config != nil && config.Local != nil {
|
||||
if config.Local.StorePath != "" {
|
||||
@@ -37,6 +64,43 @@ func NewFileStorage(config *conf.AdminBackend) (biz.FileStorage, error) {
|
||||
return &fileStorage{root: root, urlPrefix: "/" + strings.Trim(prefix, "/")}, nil
|
||||
}
|
||||
|
||||
func composeFiles(ctx context.Context, storage biz.FileStorage, names []string, destination string) (*biz.StoredFile, string, error) {
|
||||
reader, writer := io.Pipe()
|
||||
hash := md5.New()
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
for _, name := range names {
|
||||
if ctx.Err() != nil {
|
||||
errCh <- ctx.Err()
|
||||
return
|
||||
}
|
||||
file, err := storage.Open(ctx, name)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
_, err = io.Copy(io.MultiWriter(writer, hash), file)
|
||||
_ = file.Close()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
errCh <- nil
|
||||
}()
|
||||
stored, err := storage.Put(ctx, destination, reader)
|
||||
composeErr := <-errCh
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if composeErr != nil {
|
||||
_ = storage.Delete(ctx, destination)
|
||||
return nil, "", composeErr
|
||||
}
|
||||
return stored, hex.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func (s *fileStorage) resolve(name string) (string, error) {
|
||||
clean := filepath.Clean(strings.TrimPrefix(name, "/"))
|
||||
if clean == "." || filepath.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
|
||||
@@ -88,40 +152,7 @@ func (s *fileStorage) Delete(ctx context.Context, name string) error {
|
||||
}
|
||||
|
||||
func (s *fileStorage) Compose(ctx context.Context, names []string, destination string) (*biz.StoredFile, string, error) {
|
||||
reader, writer := io.Pipe()
|
||||
hash := md5.New()
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
for _, name := range names {
|
||||
if ctx.Err() != nil {
|
||||
errCh <- ctx.Err()
|
||||
return
|
||||
}
|
||||
file, err := s.Open(ctx, name)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
_, err = io.Copy(io.MultiWriter(writer, hash), file)
|
||||
_ = file.Close()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
errCh <- nil
|
||||
}()
|
||||
stored, err := s.Put(ctx, destination, reader)
|
||||
composeErr := <-errCh
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if composeErr != nil {
|
||||
_ = s.Delete(ctx, destination)
|
||||
return nil, "", composeErr
|
||||
}
|
||||
return stored, hex.EncodeToString(hash.Sum(nil)), nil
|
||||
return composeFiles(ctx, s, names, destination)
|
||||
}
|
||||
func (s *fileStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
path, err := s.resolve(strings.TrimSuffix(prefix, "/") + "/placeholder")
|
||||
|
||||
@@ -143,7 +143,7 @@ func (r *mediaRepo) DeleteMedia(ctx context.Context, id uint) error {
|
||||
}
|
||||
func (r *mediaRepo) MediaKeyReferences(ctx context.Context, key string) (int64, error) {
|
||||
var count int64
|
||||
err := r.data.gormDB.WithContext(ctx).Model(&mediaPO{}).Where("`key` = ?", key).Count(&count).Error
|
||||
err := r.data.gormDB.WithContext(ctx).Model(&mediaPO{}).Where(map[string]any{"key": key}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
func (r *mediaRepo) CreateMediaBatch(ctx context.Context, items []*biz.MediaFile) error {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qiniu/go-sdk/v7/auth/qbox"
|
||||
qstorage "github.com/qiniu/go-sdk/v7/storage"
|
||||
"kra/internal/biz"
|
||||
"kra/internal/conf"
|
||||
)
|
||||
|
||||
type qiniuStorage struct {
|
||||
config *conf.AdminBackend_Qiniu
|
||||
upload *qstorage.FormUploader
|
||||
manager *qstorage.BucketManager
|
||||
mac *qbox.Mac
|
||||
token string
|
||||
}
|
||||
|
||||
func newQiniuStorage(config *conf.AdminBackend_Qiniu) (biz.FileStorage, error) {
|
||||
if config == nil || config.Bucket == "" || config.AccessKey == "" || config.SecretKey == "" {
|
||||
return nil, fmt.Errorf("qiniu storage configuration is incomplete")
|
||||
}
|
||||
cfg := qstorage.Config{UseHTTPS: config.UseHttps, UseCdnDomains: config.UseCdnDomains}
|
||||
switch config.Zone {
|
||||
case "ZoneHuadong":
|
||||
cfg.Zone = &qstorage.ZoneHuadong
|
||||
case "ZoneHuabei":
|
||||
cfg.Zone = &qstorage.ZoneHuabei
|
||||
case "ZoneHuanan":
|
||||
cfg.Zone = &qstorage.ZoneHuanan
|
||||
case "ZoneBeimei":
|
||||
cfg.Zone = &qstorage.ZoneBeimei
|
||||
case "ZoneXinjiapo":
|
||||
cfg.Zone = &qstorage.ZoneXinjiapo
|
||||
}
|
||||
mac := qbox.NewMac(config.AccessKey, config.SecretKey)
|
||||
policy := qstorage.PutPolicy{Scope: config.Bucket}
|
||||
token := policy.UploadToken(mac)
|
||||
return &qiniuStorage{config: config, upload: qstorage.NewFormUploader(&cfg), manager: qstorage.NewBucketManager(mac, &cfg), mac: mac, token: token}, nil
|
||||
}
|
||||
func (s *qiniuStorage) file(key string, size int64) *biz.StoredFile {
|
||||
return &biz.StoredFile{Name: path.Base(key), Path: key, URL: strings.TrimSuffix(s.config.BaseUrl, "/") + "/" + key, Size: size}
|
||||
}
|
||||
func (s *qiniuStorage) Put(ctx context.Context, name string, reader io.Reader) (*biz.StoredFile, error) {
|
||||
temporary, err := os.CreateTemp("", "kra-qiniu-upload-*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
temporaryName := temporary.Name()
|
||||
defer os.Remove(temporaryName)
|
||||
size, err := io.Copy(temporary, reader)
|
||||
if err != nil {
|
||||
_ = temporary.Close()
|
||||
return nil, err
|
||||
}
|
||||
if _, err = temporary.Seek(0, io.SeekStart); err != nil {
|
||||
_ = temporary.Close()
|
||||
return nil, err
|
||||
}
|
||||
defer temporary.Close()
|
||||
ret := qstorage.PutRet{}
|
||||
if err = s.upload.Put(ctx, &ret, s.token, strings.TrimPrefix(name, "/"), temporary, size, &qstorage.PutExtra{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.file(ret.Key, size), nil
|
||||
}
|
||||
func (s *qiniuStorage) Open(ctx context.Context, name string) (io.ReadCloser, error) {
|
||||
downloadURL := qstorage.MakePrivateURL(s.mac, strings.TrimSuffix(s.config.BaseUrl, "/"), strings.TrimPrefix(name, "/"), time.Now().Add(time.Hour).Unix())
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode >= 300 {
|
||||
_ = resp.Body.Close()
|
||||
return nil, fmt.Errorf("qiniu download failed: %s", resp.Status)
|
||||
}
|
||||
return resp.Body, nil
|
||||
}
|
||||
func (s *qiniuStorage) Delete(ctx context.Context, name string) error {
|
||||
return s.manager.Delete(s.config.Bucket, strings.TrimPrefix(name, "/"))
|
||||
}
|
||||
func (s *qiniuStorage) Compose(ctx context.Context, names []string, destination string) (*biz.StoredFile, string, error) {
|
||||
return composeFiles(ctx, s, names, destination)
|
||||
}
|
||||
func (s *qiniuStorage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
for {
|
||||
items, _, more, err := s.List(ctx, prefix, "", 1000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range items {
|
||||
if err = s.Delete(ctx, item.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !more {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
func (s *qiniuStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
|
||||
if limit < 1 || limit > 1000 {
|
||||
limit = 100
|
||||
}
|
||||
entries, _, marker, more, err := s.manager.ListFiles(s.config.Bucket, prefix, "", cursor, limit)
|
||||
if err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
out := make([]*biz.StoredFile, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
out = append(out, s.file(entry.Key, entry.Fsize))
|
||||
}
|
||||
return out, marker, more, nil
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"kra/internal/biz"
|
||||
"kra/internal/conf"
|
||||
)
|
||||
|
||||
type s3Storage struct {
|
||||
client *minio.Client
|
||||
bucket, baseURL, prefix string
|
||||
}
|
||||
|
||||
func newS3Storage(provider string, config *conf.AdminBackend_ObjectStore) (biz.FileStorage, error) {
|
||||
if config == nil || config.Bucket == "" || config.AccessKey == "" || config.SecretKey == "" {
|
||||
return nil, fmt.Errorf("%s storage configuration is incomplete", provider)
|
||||
}
|
||||
endpoint := config.Endpoint
|
||||
if endpoint == "" {
|
||||
switch provider {
|
||||
case "aws-s3":
|
||||
endpoint = "s3." + config.Region + ".amazonaws.com"
|
||||
case "cloudflare-r2":
|
||||
endpoint = config.AccountId + ".r2.cloudflarestorage.com"
|
||||
case "aliyun-oss":
|
||||
endpoint = "oss-" + config.Region + ".aliyuncs.com"
|
||||
case "huawei-obs":
|
||||
endpoint = "obs." + config.Region + ".myhuaweicloud.com"
|
||||
case "tencent-cos":
|
||||
endpoint = "cos." + config.Region + ".myqcloud.com"
|
||||
}
|
||||
}
|
||||
secure := config.UseSsl
|
||||
if parsed, err := url.Parse(endpoint); err == nil && parsed.Host != "" {
|
||||
secure = parsed.Scheme == "https"
|
||||
endpoint = parsed.Host
|
||||
}
|
||||
if endpoint == "" {
|
||||
return nil, fmt.Errorf("%s endpoint is required", provider)
|
||||
}
|
||||
client, err := minio.New(strings.TrimSuffix(endpoint, "/"), &minio.Options{Creds: credentials.NewStaticV4(config.AccessKey, config.SecretKey, ""), Secure: secure, Region: config.Region, BucketLookup: func() minio.BucketLookupType {
|
||||
if config.ForcePathStyle {
|
||||
return minio.BucketLookupPath
|
||||
}
|
||||
return minio.BucketLookupAuto
|
||||
}()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s3Storage{client: client, bucket: config.Bucket, baseURL: strings.TrimSuffix(config.BaseUrl, "/"), prefix: strings.Trim(config.PathPrefix, "/")}, nil
|
||||
}
|
||||
func (s *s3Storage) key(name string) string {
|
||||
if s.prefix == "" {
|
||||
return strings.TrimPrefix(name, "/")
|
||||
}
|
||||
return path.Join(s.prefix, strings.TrimPrefix(name, "/"))
|
||||
}
|
||||
func (s *s3Storage) unkey(key string) string {
|
||||
return strings.TrimPrefix(strings.TrimPrefix(key, s.prefix), "/")
|
||||
}
|
||||
func (s *s3Storage) file(key string, size int64) *biz.StoredFile {
|
||||
name := s.unkey(key)
|
||||
rawURL := s.baseURL + "/" + key
|
||||
if s.baseURL == "" {
|
||||
rawURL = key
|
||||
}
|
||||
return &biz.StoredFile{Name: path.Base(name), Path: name, URL: rawURL, Size: size}
|
||||
}
|
||||
func (s *s3Storage) Put(ctx context.Context, name string, reader io.Reader) (*biz.StoredFile, error) {
|
||||
key := s.key(name)
|
||||
info, err := s.client.PutObject(ctx, s.bucket, key, reader, -1, minio.PutObjectOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.file(key, info.Size), nil
|
||||
}
|
||||
func (s *s3Storage) Open(ctx context.Context, name string) (io.ReadCloser, error) {
|
||||
obj, err := s.client.GetObject(ctx, s.bucket, s.key(name), minio.GetObjectOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = obj.Stat(); err != nil {
|
||||
_ = obj.Close()
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
func (s *s3Storage) Delete(ctx context.Context, name string) error {
|
||||
return s.client.RemoveObject(ctx, s.bucket, s.key(name), minio.RemoveObjectOptions{})
|
||||
}
|
||||
func (s *s3Storage) Compose(ctx context.Context, names []string, destination string) (*biz.StoredFile, string, error) {
|
||||
return composeFiles(ctx, s, names, destination)
|
||||
}
|
||||
func (s *s3Storage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||
items := s.client.ListObjects(ctx, s.bucket, minio.ListObjectsOptions{Prefix: s.key(prefix), Recursive: true})
|
||||
for item := range items {
|
||||
if item.Err != nil {
|
||||
return item.Err
|
||||
}
|
||||
if err := s.client.RemoveObject(ctx, s.bucket, item.Key, minio.RemoveObjectOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s *s3Storage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
|
||||
if limit < 1 || limit > 1000 {
|
||||
limit = 100
|
||||
}
|
||||
items := s.client.ListObjects(ctx, s.bucket, minio.ListObjectsOptions{Prefix: s.key(prefix), Recursive: true, StartAfter: s.key(cursor)})
|
||||
out := make([]*biz.StoredFile, 0, limit+1)
|
||||
for item := range items {
|
||||
if item.Err != nil {
|
||||
return nil, "", false, item.Err
|
||||
}
|
||||
out = append(out, s.file(item.Key, item.Size))
|
||||
if len(out) > limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
more := len(out) > limit
|
||||
if more {
|
||||
out = out[:limit]
|
||||
}
|
||||
next := ""
|
||||
if len(out) > 0 {
|
||||
next = out[len(out)-1].Path
|
||||
}
|
||||
return out, next, more, nil
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"kra/internal/biz"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type dictionaryPO struct {
|
||||
@@ -334,7 +335,7 @@ func (r *settingsRepo) FindParameter(ctx context.Context, id uint, key string) (
|
||||
if id != 0 {
|
||||
err = db.First(&po, id).Error
|
||||
} else {
|
||||
err = db.Where("`key` = ?", key).First(&po).Error
|
||||
err = db.Where(map[string]any{"key": key}).First(&po).Error
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -354,7 +355,7 @@ func (r *settingsRepo) ListParameters(ctx context.Context, page, size int, q *bi
|
||||
db = db.Where("name LIKE ?", "%"+q.Name+"%")
|
||||
}
|
||||
if q.Key != "" {
|
||||
db = db.Where("`key` LIKE ?", "%"+q.Key+"%")
|
||||
db = db.Where(clause.Like{Column: clause.Column{Name: "key"}, Value: "%" + q.Key + "%"})
|
||||
}
|
||||
}
|
||||
var total int64
|
||||
|
||||
+18
-5
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/conf"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -87,14 +88,22 @@ func (authorityMenuPO) TableName() string { return "sys_authority_menus" }
|
||||
|
||||
type systemRepo struct{ data *Data }
|
||||
|
||||
func NewSystemRepo(data *Data) biz.SystemRepo { return &systemRepo{data: data} }
|
||||
func NewSystemRepo(data *Data) biz.SystemRepo { return &systemRepo{data: data} }
|
||||
func (r *systemRepo) PersistConfig(context.Context) error { return r.data.persistConfig() }
|
||||
|
||||
func (r *systemRepo) IsInitialized(ctx context.Context) (bool, error) {
|
||||
return r.data.gormDB.WithContext(ctx).Migrator().HasTable(&userPO{}), nil
|
||||
}
|
||||
|
||||
func (r *systemRepo) Initialize(ctx context.Context) error {
|
||||
func (r *systemRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig) error {
|
||||
config := &conf.Data_Database{Driver: input.Driver, Host: input.Host, Port: input.Port, User: input.User, Password: input.Password, Name: input.Name, Path: input.Path, Config: input.Config}
|
||||
if err := r.data.switchDatabase(config); err != nil {
|
||||
return err
|
||||
}
|
||||
db := r.data.gormDB.WithContext(ctx)
|
||||
if err := db.AutoMigrate(&adminPO{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &menuButtonPO{}, &authorityButtonPO{}, &departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{}, &dictionaryPO{}, &dictionaryDetailPO{}, ¶meterPO{}, &apiTokenPO{}, &securityConfigPO{}, &versionPO{}, &exportTemplatePO{}, &exportConditionPO{}, &exportJoinPO{}, &operationPO{}, &loginLogPO{}, &dataAccessLogPO{}, &errorRecordPO{}, &taskPO{}, &taskLogPO{}, &mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{}, &announcementPO{}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -140,7 +149,7 @@ func (r *systemRepo) Initialize(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(input.AdminPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -349,9 +358,13 @@ func (r *systemRepo) DeleteUser(ctx context.Context, id uint) error {
|
||||
return tx.Delete(&userPO{}, id).Error
|
||||
})
|
||||
}
|
||||
func (r *systemRepo) UpdatePassword(ctx context.Context, id uint, password string) error {
|
||||
func (r *systemRepo) UpdatePassword(ctx context.Context, id uint, password string, clearMustChange bool) error {
|
||||
now := time.Now()
|
||||
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Updates(map[string]any{"password": password, "password_updated_at": now, "must_change_password": false}).Error
|
||||
updates := map[string]any{"password": password, "password_updated_at": now}
|
||||
if clearMustChange {
|
||||
updates["must_change_password"] = false
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Updates(updates).Error
|
||||
}
|
||||
func (r *systemRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
|
||||
var pos []authorityPO
|
||||
|
||||
@@ -62,7 +62,7 @@ func redactJSON(raw []byte) string {
|
||||
}
|
||||
func operationAudit(svc *service.AuditService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if c.Request.Method == "GET" || c.Request.Method == "HEAD" || c.Request.Method == "OPTIONS" {
|
||||
if isBootstrapPath(c.Request.URL.Path) || c.Request.Method == "GET" || c.Request.Method == "HEAD" || c.Request.Method == "OPTIONS" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
+35
-5
@@ -34,7 +34,8 @@ func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.System
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
engine := gin.New()
|
||||
engine.Use(ginRequestMeta(), emailErrorAlert(emails, logger), gin.Recovery(), securityRateLimit(svc, settings), operationAudit(audit))
|
||||
if config != nil && config.Local != nil && config.Local.StorePath != "" {
|
||||
localStorage := config == nil || config.Storage == nil || config.Storage.Type == "" || config.Storage.Type == "local"
|
||||
if localStorage && config != nil && config.Local != nil && config.Local.StorePath != "" {
|
||||
pathPrefix := "/" + strings.Trim(config.Local.PathPrefix, "/")
|
||||
if pathPrefix != "/" {
|
||||
engine.StaticFS(pathPrefix, filesOnly{FileSystem: http.Dir(config.Local.StorePath)})
|
||||
@@ -51,11 +52,11 @@ func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.System
|
||||
})
|
||||
registerPublicRoutes(public, engine, config, svc, settings, access, audit)
|
||||
private := engine.Group(prefix)
|
||||
private.Use(jwtAuth(config, settings), accessControl(access, audit))
|
||||
private.Use(jwtAuth(config, settings), mustChangePasswordGuard(), accessControl(access, audit))
|
||||
registerPrivateRoutes(private, svc)
|
||||
registerAccessRoutes(private, engine, access)
|
||||
registerSettingsRoutes(private, public, settings)
|
||||
registerSystemConfigRoutes(private, config, settings)
|
||||
registerSystemConfigRoutes(private, config, svc, settings)
|
||||
registerVersionRoutes(private, versions)
|
||||
registerExportRoutes(private, public, svc, exports)
|
||||
registerAuditRoutes(private, public, audit)
|
||||
@@ -117,6 +118,22 @@ func currentClaims(c *gin.Context) *adminauth.Claims {
|
||||
return result
|
||||
}
|
||||
|
||||
func mustChangePasswordGuard() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
claims := currentClaims(c)
|
||||
if claims == nil || !claims.MustChangePwd {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
path := strings.TrimSuffix(c.Request.URL.Path, "/")
|
||||
if strings.HasSuffix(path, "/user/changePassword") || strings.HasSuffix(path, "/user/getUserInfo") || strings.HasSuffix(path, "/jwt/jsonInBlacklist") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
c.AbortWithStatusJSON(http.StatusConflict, response{Code: codePasswordChangeRequired, Data: gin.H{"needChangePassword": true}, Msg: "请先修改初始密码"})
|
||||
}
|
||||
}
|
||||
|
||||
func registerPublicRoutes(group *gin.RouterGroup, engine *gin.Engine, config *conf.AdminBackend, svc *service.SystemService, settings *service.SettingsService, access *service.AccessService, audit *service.AuditService) {
|
||||
expiration := 3 * time.Minute
|
||||
keyLong, width, height := 6, 240, 80
|
||||
@@ -221,8 +238,13 @@ func registerPublicRoutes(group *gin.RouterGroup, engine *gin.Engine, config *co
|
||||
writeResult(c, codeSuccess, gin.H{"needInit": !initialized}, message)
|
||||
})
|
||||
initGroup.POST("/initdb", func(c *gin.Context) {
|
||||
if err := svc.Initialize(c.Request.Context()); err != nil {
|
||||
fail(c, "自动创建数据库失败")
|
||||
var input service.DatabaseInit
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
fail(c, "数据库初始化参数无效")
|
||||
return
|
||||
}
|
||||
if err := svc.Initialize(c.Request.Context(), &input); err != nil {
|
||||
fail(c, "自动创建数据库失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
routes := engine.Routes()
|
||||
@@ -261,6 +283,10 @@ func incrementCached(ctx context.Context, svc *service.SystemService, key string
|
||||
}
|
||||
func securityRateLimit(svc *service.SystemService, settings *service.SettingsService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if isBootstrapPath(c.Request.URL.Path) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
cfg, err := settings.CurrentSecurity(c.Request.Context())
|
||||
if err != nil || cfg == nil || !cfg.LimitEnable {
|
||||
c.Next()
|
||||
@@ -281,6 +307,10 @@ func securityRateLimit(svc *service.SystemService, settings *service.SettingsSer
|
||||
}
|
||||
}
|
||||
|
||||
func isBootstrapPath(path string) bool {
|
||||
return strings.HasSuffix(path, "/health") || strings.Contains(path, "/init/")
|
||||
}
|
||||
|
||||
type captchaStore struct {
|
||||
service *service.SystemService
|
||||
expiration time.Duration
|
||||
|
||||
@@ -3,15 +3,19 @@ package server
|
||||
import (
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/conf"
|
||||
"kra/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBackend, settings *service.SettingsService) {
|
||||
func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBackend, systemService *service.SystemService, settings *service.SettingsService) {
|
||||
security := group.Group("/securityConfig")
|
||||
security.GET("/getSecurityConfig", func(c *gin.Context) {
|
||||
value, err := settings.Security(c.Request.Context())
|
||||
@@ -53,6 +57,11 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken
|
||||
if config.Email != nil {
|
||||
email = gin.H{"to": config.Email.To, "from": config.Email.From, "host": config.Email.Host, "secret": "******", "nickname": config.Email.Nickname, "port": config.Email.Port, "is-ssl": config.Email.IsSsl, "is-loginauth": config.Email.IsLoginAuth}
|
||||
}
|
||||
if config.Storage != nil {
|
||||
storage := proto.Clone(config.Storage).(*conf.AdminBackend_Storage)
|
||||
maskStorageSecrets(storage)
|
||||
admin["storage"] = storage
|
||||
}
|
||||
}
|
||||
writeResult(c, codeSuccess, gin.H{"config": gin.H{"admin": admin, "email": email}}, "获取成功")
|
||||
})
|
||||
@@ -69,6 +78,7 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken
|
||||
StorePath string `json:"storePath"`
|
||||
PathPrefix string `json:"pathPrefix"`
|
||||
} `json:"local"`
|
||||
Storage *conf.AdminBackend_Storage `json:"storage"`
|
||||
} `json:"admin"`
|
||||
Email *struct {
|
||||
To string `json:"to"`
|
||||
@@ -104,6 +114,10 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken
|
||||
config.Local.PathPrefix = req.Config.Admin.Local.PathPrefix
|
||||
}
|
||||
}
|
||||
if req.Config.Admin.Storage != nil {
|
||||
preserveStorageSecrets(req.Config.Admin.Storage, config.Storage)
|
||||
config.Storage = req.Config.Admin.Storage
|
||||
}
|
||||
if config.Email != nil && req.Config.Email != nil {
|
||||
config.Email.To = req.Config.Email.To
|
||||
config.Email.From = req.Config.Email.From
|
||||
@@ -117,12 +131,18 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken
|
||||
}
|
||||
}
|
||||
}
|
||||
ok(c)
|
||||
if err := systemService.PersistConfig(c.Request.Context()); err != nil {
|
||||
fail(c, "配置保存失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
writeResult(c, codeSuccess, gin.H{"restartRequired": true}, "配置已保存,重启服务后完整生效")
|
||||
})
|
||||
system.POST("/reloadSystem", func(c *gin.Context) {
|
||||
writeResult(c, codeSuccess, gin.H{"reload": false, "restartRequired": true}, "配置已在当前进程部分生效;路由与存储配置需重启服务")
|
||||
})
|
||||
system.POST("/reloadSystem", func(c *gin.Context) { writeResult(c, codeSuccess, gin.H{"reload": true}, "配置已热更新") })
|
||||
system.POST("/getServerInfo", func(c *gin.Context) {
|
||||
var mem runtime.MemStats
|
||||
runtime.ReadMemStats(&mem)
|
||||
cpuPercent, _ := cpu.Percent(200*time.Millisecond, false)
|
||||
memory, _ := mem.VirtualMemory()
|
||||
disk := []gin.H{}
|
||||
var stat syscall.Statfs_t
|
||||
if syscall.Statfs(".", &stat) == nil {
|
||||
@@ -135,7 +155,42 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken
|
||||
}
|
||||
disk = append(disk, gin.H{"mountPoint": ".", "usedMb": used / 1024 / 1024, "usedGb": used / 1024 / 1024 / 1024, "totalMb": total / 1024 / 1024, "totalGb": total / 1024 / 1024 / 1024, "usedPercent": percent})
|
||||
}
|
||||
server := gin.H{"os": gin.H{"goos": runtime.GOOS, "numCpu": runtime.NumCPU(), "compiler": runtime.Compiler, "goVersion": runtime.Version(), "numGoroutine": runtime.NumGoroutine()}, "cpu": gin.H{"cpus": []float64{}, "cores": runtime.NumCPU()}, "ram": gin.H{"usedMb": mem.Alloc / 1024 / 1024, "totalMb": mem.Sys / 1024 / 1024, "usedPercent": 0}, "disk": disk}
|
||||
usedMB, totalMB, memoryPercent := uint64(0), uint64(0), float64(0)
|
||||
if memory != nil {
|
||||
usedMB, totalMB, memoryPercent = memory.Used/1024/1024, memory.Total/1024/1024, memory.UsedPercent
|
||||
}
|
||||
server := gin.H{"os": gin.H{"goos": runtime.GOOS, "numCpu": runtime.NumCPU(), "compiler": runtime.Compiler, "goVersion": runtime.Version(), "numGoroutine": runtime.NumGoroutine()}, "cpu": gin.H{"cpus": cpuPercent, "cores": runtime.NumCPU()}, "ram": gin.H{"usedMb": usedMB, "totalMb": totalMB, "usedPercent": memoryPercent}, "disk": disk}
|
||||
writeResult(c, codeSuccess, gin.H{"server": server}, "获取成功")
|
||||
})
|
||||
}
|
||||
|
||||
func objectStores(storage *conf.AdminBackend_Storage) []*conf.AdminBackend_ObjectStore {
|
||||
if storage == nil {
|
||||
return nil
|
||||
}
|
||||
return []*conf.AdminBackend_ObjectStore{storage.AliyunOss, storage.HuaweiObs, storage.TencentCos, storage.AwsS3, storage.CloudflareR2, storage.Minio}
|
||||
}
|
||||
func maskStorageSecrets(storage *conf.AdminBackend_Storage) {
|
||||
if storage.Qiniu != nil {
|
||||
storage.Qiniu.SecretKey = "******"
|
||||
}
|
||||
for _, item := range objectStores(storage) {
|
||||
if item != nil {
|
||||
item.SecretKey = "******"
|
||||
}
|
||||
}
|
||||
}
|
||||
func preserveStorageSecrets(next, current *conf.AdminBackend_Storage) {
|
||||
if next == nil || current == nil {
|
||||
return
|
||||
}
|
||||
if next.Qiniu != nil && current.Qiniu != nil && (next.Qiniu.SecretKey == "" || next.Qiniu.SecretKey == "******") {
|
||||
next.Qiniu.SecretKey = current.Qiniu.SecretKey
|
||||
}
|
||||
nextItems, currentItems := objectStores(next), objectStores(current)
|
||||
for i := range nextItems {
|
||||
if nextItems[i] != nil && currentItems[i] != nil && (nextItems[i].SecretKey == "" || nextItems[i].SecretKey == "******") {
|
||||
nextItems[i].SecretKey = currentItems[i].SecretKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,30 @@ func NewSystemService(uc *biz.SystemUsecase, config *conf.AdminBackend, settings
|
||||
func (s *SystemService) IsInitialized(ctx context.Context) (bool, error) {
|
||||
return s.uc.IsInitialized(ctx)
|
||||
}
|
||||
func (s *SystemService) Initialize(ctx context.Context) error { return s.uc.Initialize(ctx) }
|
||||
func (s *SystemService) PersistConfig(ctx context.Context) error { return s.uc.PersistConfig(ctx) }
|
||||
|
||||
type DatabaseInit struct {
|
||||
DBType string `json:"dbType"`
|
||||
Host string `json:"host"`
|
||||
Port string `json:"port"`
|
||||
UserName string `json:"userName"`
|
||||
Password string `json:"password"`
|
||||
DBName string `json:"dbName"`
|
||||
DBPath string `json:"dbPath"`
|
||||
Template string `json:"template"`
|
||||
AdminPassword string `json:"adminPassword"`
|
||||
}
|
||||
|
||||
func (s *SystemService) Initialize(ctx context.Context, input *DatabaseInit) error {
|
||||
config := ""
|
||||
switch input.DBType {
|
||||
case "mysql":
|
||||
config = "timeout=5s&parseTime=True&loc=Local&charset=utf8mb4"
|
||||
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})
|
||||
}
|
||||
|
||||
func (s *SystemService) CacheGet(ctx context.Context, key string) (string, bool, error) {
|
||||
return s.uc.CacheGet(ctx, key)
|
||||
|
||||
Reference in New Issue
Block a user