From 963ad4eabc5da241e64dff919c981cf1c38c0899 Mon Sep 17 00:00:00 2001 From: yokowu <18836617@qq.com> Date: Sun, 13 Jul 2025 00:05:39 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat(model):=20=E5=A2=9E=E5=8A=A0=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/cmd/server/wire_gen.go | 14 ++++++------- backend/domain/model.go | 1 + backend/domain/proxy.go | 1 - backend/go.mod | 1 + backend/go.sum | 2 ++ backend/internal/model/repo/model.go | 26 +++++++++++++++++++++++-- backend/internal/proxy/usecase/proxy.go | 9 +++++---- 7 files changed, 40 insertions(+), 14 deletions(-) diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index d7c5c26..c5c2997 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -17,14 +17,14 @@ import ( v1_4 "github.com/chaitin/MonkeyCode/backend/internal/dashboard/handler/v1" repo6 "github.com/chaitin/MonkeyCode/backend/internal/dashboard/repo" usecase5 "github.com/chaitin/MonkeyCode/backend/internal/dashboard/usecase" - repo3 "github.com/chaitin/MonkeyCode/backend/internal/extension/repo" + repo4 "github.com/chaitin/MonkeyCode/backend/internal/extension/repo" usecase2 "github.com/chaitin/MonkeyCode/backend/internal/extension/usecase" "github.com/chaitin/MonkeyCode/backend/internal/middleware" v1_2 "github.com/chaitin/MonkeyCode/backend/internal/model/handler/http/v1" - repo4 "github.com/chaitin/MonkeyCode/backend/internal/model/repo" + repo2 "github.com/chaitin/MonkeyCode/backend/internal/model/repo" usecase3 "github.com/chaitin/MonkeyCode/backend/internal/model/usecase" "github.com/chaitin/MonkeyCode/backend/internal/openai/handler/v1" - repo2 "github.com/chaitin/MonkeyCode/backend/internal/openai/repo" + repo3 "github.com/chaitin/MonkeyCode/backend/internal/openai/repo" "github.com/chaitin/MonkeyCode/backend/internal/openai/usecase" "github.com/chaitin/MonkeyCode/backend/internal/proxy" "github.com/chaitin/MonkeyCode/backend/internal/proxy/repo" @@ -54,17 +54,17 @@ func newServer() (*Server, error) { return nil, err } proxyRepo := repo.NewProxyRepo(client) - proxyUsecase := usecase.NewProxyUsecase(proxyRepo) + modelRepo := repo2.NewModelRepo(client) + proxyUsecase := usecase.NewProxyUsecase(proxyRepo, modelRepo) domainProxy := proxy.NewLLMProxy(proxyUsecase, configConfig, slogLogger) - openAIRepo := repo2.NewOpenAIRepo(client) + openAIRepo := repo3.NewOpenAIRepo(client) openAIUsecase := openai.NewOpenAIUsecase(configConfig, openAIRepo, slogLogger) - extensionRepo := repo3.NewExtensionRepo(client) + extensionRepo := repo4.NewExtensionRepo(client) extensionUsecase := usecase2.NewExtensionUsecase(extensionRepo, configConfig, slogLogger) proxyMiddleware := middleware.NewProxyMiddleware(proxyUsecase) redisClient := store.NewRedisCli(configConfig) activeMiddleware := middleware.NewActiveMiddleware(redisClient, slogLogger) v1Handler := v1.NewV1Handler(slogLogger, web, domainProxy, openAIUsecase, extensionUsecase, proxyMiddleware, activeMiddleware, configConfig) - modelRepo := repo4.NewModelRepo(client) modelUsecase := usecase3.NewModelUsecase(slogLogger, modelRepo, configConfig) sessionSession := session.NewSession(configConfig) authMiddleware := middleware.NewAuthMiddleware(sessionSession, slogLogger) diff --git a/backend/domain/model.go b/backend/domain/model.go index 583f26b..a64e778 100644 --- a/backend/domain/model.go +++ b/backend/domain/model.go @@ -20,6 +20,7 @@ type ModelUsecase interface { } type ModelRepo interface { + GetWithCache(ctx context.Context, modelType consts.ModelType) (*db.Model, error) List(ctx context.Context) (*AllModelResp, error) Create(ctx context.Context, m *CreateModelReq) (*db.Model, error) Update(ctx context.Context, id string, fn func(tx *db.Tx, old *db.Model, up *db.ModelUpdateOne) error) (*db.Model, error) diff --git a/backend/domain/proxy.go b/backend/domain/proxy.go index a7a2d25..d344a98 100644 --- a/backend/domain/proxy.go +++ b/backend/domain/proxy.go @@ -10,7 +10,6 @@ import ( "github.com/chaitin/MonkeyCode/backend/db" ) -// Proxy LLM API代理接口 type Proxy interface { AcceptCompletion(ctx context.Context, req *AcceptCompletionReq) error HandleCompletion(ctx context.Context, w http.ResponseWriter, req CompletionRequest) diff --git a/backend/go.mod b/backend/go.mod index 1855d9f..85acd2d 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -10,6 +10,7 @@ require ( github.com/google/wire v0.6.0 github.com/labstack/echo/v4 v4.13.4 github.com/lib/pq v1.10.9 + github.com/patrickmn/go-cache v2.1.0+incompatible github.com/redis/go-redis/v9 v9.7.3 github.com/rokku-c/go-openai v1.35.7-fix2 github.com/spf13/viper v1.20.1 diff --git a/backend/go.sum b/backend/go.sum index 9f2f330..b26474e 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -115,6 +115,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/backend/internal/model/repo/model.go b/backend/internal/model/repo/model.go index cbc1616..faac2c9 100644 --- a/backend/internal/model/repo/model.go +++ b/backend/internal/model/repo/model.go @@ -6,6 +6,7 @@ import ( "entgo.io/ent/dialect/sql" "github.com/google/uuid" + "github.com/patrickmn/go-cache" "github.com/chaitin/MonkeyCode/backend/consts" "github.com/chaitin/MonkeyCode/backend/db" @@ -18,11 +19,30 @@ import ( ) type ModelRepo struct { - db *db.Client + db *db.Client + cache *cache.Cache } func NewModelRepo(db *db.Client) domain.ModelRepo { - return &ModelRepo{db: db} + cache := cache.New(24*time.Hour, 10*time.Minute) + return &ModelRepo{db: db, cache: cache} +} + +func (r *ModelRepo) GetWithCache(ctx context.Context, modelType consts.ModelType) (*db.Model, error) { + if v, ok := r.cache.Get(string(modelType)); ok { + return v.(*db.Model), nil + } + + m, err := r.db.Model.Query(). + Where(model.ModelType(modelType)). + Where(model.Status(consts.ModelStatusActive)). + Only(ctx) + if err != nil { + return nil, err + } + + r.cache.Set(string(modelType), m, 24*time.Hour) + return m, nil } func (r *ModelRepo) Create(ctx context.Context, m *domain.CreateModelReq) (*db.Model, error) { @@ -40,6 +60,7 @@ func (r *ModelRepo) Create(ctx context.Context, m *domain.CreateModelReq) (*db.M status = consts.ModelStatusActive } + r.cache.Delete(string(m.ModelType)) return r.db.Model.Create(). SetUserID(uid). SetModelName(m.ModelName). @@ -62,6 +83,7 @@ func (r *ModelRepo) Update(ctx context.Context, id string, fn func(tx *db.Tx, ol if err != nil { return err } + r.cache.Delete(string(old.ModelType)) up := tx.Model.UpdateOneID(old.ID) if err := fn(tx, old, up); err != nil { diff --git a/backend/internal/proxy/usecase/proxy.go b/backend/internal/proxy/usecase/proxy.go index 2a39f54..065e506 100644 --- a/backend/internal/proxy/usecase/proxy.go +++ b/backend/internal/proxy/usecase/proxy.go @@ -10,11 +10,12 @@ import ( ) type ProxyUsecase struct { - repo domain.ProxyRepo + repo domain.ProxyRepo + modelRepo domain.ModelRepo } -func NewProxyUsecase(repo domain.ProxyRepo) domain.ProxyUsecase { - return &ProxyUsecase{repo: repo} +func NewProxyUsecase(repo domain.ProxyRepo, modelRepo domain.ModelRepo) domain.ProxyUsecase { + return &ProxyUsecase{repo: repo, modelRepo: modelRepo} } func (p *ProxyUsecase) Record(ctx context.Context, record *domain.RecordParam) error { @@ -23,7 +24,7 @@ func (p *ProxyUsecase) Record(ctx context.Context, record *domain.RecordParam) e // SelectModelWithLoadBalancing implements domain.ProxyUsecase. func (p *ProxyUsecase) SelectModelWithLoadBalancing(modelName string, modelType consts.ModelType) (*domain.Model, error) { - model, err := p.repo.SelectModelWithLoadBalancing(modelName, modelType) + model, err := p.modelRepo.GetWithCache(context.Background(), modelType) if err != nil { return nil, err } From 253c6d93ec106d7964d9650e65d728120842ad07 Mon Sep 17 00:00:00 2001 From: yokowu <18836617@qq.com> Date: Sun, 13 Jul 2025 09:46:43 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat(proxy):=20=E5=A4=8D=E7=94=A8client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/proxy/proxy.go | 14 ++++++++++++-- backend/pkg/request/ops.go | 6 ++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/backend/internal/proxy/proxy.go b/backend/internal/proxy/proxy.go index c0a24e5..0e61c3b 100644 --- a/backend/internal/proxy/proxy.go +++ b/backend/internal/proxy/proxy.go @@ -442,7 +442,12 @@ func (p *LLMProxy) handleCompletion(ctx context.Context, w http.ResponseWriter, startTime := time.Now() - client := request.NewClient(u.Scheme, u.Host, 30*time.Second) + client := request.NewClient( + u.Scheme, + u.Host, + 30*time.Second, + request.WithClient(p.client), + ) client.SetDebug(p.cfg.Debug) resp, err := request.Post[openai.CompletionResponse](client, u.Path, req, request.WithHeader(request.Header{ "Authorization": "Bearer " + m.APIKey, @@ -753,7 +758,12 @@ func (p *LLMProxy) handleChatCompletion(ctx context.Context, w http.ResponseWrit mode := req.Metadata["mode"] taskID := req.Metadata["task_id"] - client := request.NewClient(u.Scheme, u.Host, 30*time.Second) + client := request.NewClient( + u.Scheme, + u.Host, + 30*time.Second, + request.WithClient(p.client), + ) resp, err := request.Post[openai.ChatCompletionResponse](client, u.Path, req, request.WithHeader(request.Header{ "Authorization": "Bearer " + m.APIKey, })) diff --git a/backend/pkg/request/ops.go b/backend/pkg/request/ops.go index 9a5930e..08ea7b4 100644 --- a/backend/pkg/request/ops.go +++ b/backend/pkg/request/ops.go @@ -12,6 +12,12 @@ func WithDebug() ReqOpt { } } +func WithClient(client *http.Client) ReqOpt { + return func(c *Client) { + c.client = client + } +} + func WithTransport(tr *http.Transport) ReqOpt { return func(c *Client) { c.tr = tr From 320aeb29857e46a26e2acd3c18c14cb4ad0fad69 Mon Sep 17 00:00:00 2001 From: yokowu <18836617@qq.com> Date: Wed, 9 Jul 2025 18:13:15 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat(oauth):=20=E6=94=AF=E6=8C=81=E6=A0=87?= =?UTF-8?q?=E5=87=86OAuth2.0=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/consts/user.go | 4 +- backend/db/migrate/schema.go | 5 +- backend/db/mutation.go | 213 +++++++---------- backend/db/runtime/runtime.go | 8 +- backend/db/setting.go | 59 +++-- backend/db/setting/setting.go | 32 +-- backend/db/setting/where.go | 179 +-------------- backend/db/setting_create.go | 214 ++++++------------ backend/db/setting_update.go | 147 ++++-------- backend/docs/swagger.json | 149 ++++++++++-- backend/domain/oauth.go | 31 +++ backend/domain/user.go | 105 +++++++-- backend/ent/schema/setting.go | 7 +- backend/ent/types/types.go | 21 ++ backend/errcode/errcode.go | 16 +- backend/errcode/locale.zh.toml | 14 +- backend/internal/user/handler/v1/user.go | 15 +- backend/internal/user/repo/user.go | 82 ++++--- backend/internal/user/usecase/user.go | 142 ++++++++++-- .../000006_alter_settings_table.down.sql | 0 .../000006_alter_settings_table.up.sql | 6 + backend/pkg/oauth/custom.go | 96 ++++++++ backend/pkg/oauth/oauth.go | 2 + 23 files changed, 821 insertions(+), 726 deletions(-) create mode 100644 backend/ent/types/types.go create mode 100644 backend/migration/000006_alter_settings_table.down.sql create mode 100644 backend/migration/000006_alter_settings_table.up.sql create mode 100644 backend/pkg/oauth/custom.go diff --git a/backend/consts/user.go b/backend/consts/user.go index 36089c9..64cc308 100644 --- a/backend/consts/user.go +++ b/backend/consts/user.go @@ -21,12 +21,14 @@ type UserPlatform string const ( UserPlatformEmail UserPlatform = "email" UserPlatformDingTalk UserPlatform = "dingtalk" + UserPlatformCustom UserPlatform = "custom" ) type OAuthKind string const ( - OAuthKindSignUpOrIn OAuthKind = "signup_or_in" + OAuthKindInvite OAuthKind = "invite" + OAuthKindLogin OAuthKind = "login" ) type InviteCodeStatus string diff --git a/backend/db/migrate/schema.go b/backend/db/migrate/schema.go index 33faaab..232087d 100644 --- a/backend/db/migrate/schema.go +++ b/backend/db/migrate/schema.go @@ -243,9 +243,8 @@ var ( {Name: "enable_sso", Type: field.TypeBool, Default: false}, {Name: "force_two_factor_auth", Type: field.TypeBool, Default: false}, {Name: "disable_password_login", Type: field.TypeBool, Default: false}, - {Name: "enable_dingtalk_oauth", Type: field.TypeBool, Default: false}, - {Name: "dingtalk_client_id", Type: field.TypeString, Nullable: true}, - {Name: "dingtalk_client_secret", Type: field.TypeString, Nullable: true}, + {Name: "dingtalk_oauth", Type: field.TypeJSON, Nullable: true}, + {Name: "custom_oauth", Type: field.TypeJSON, Nullable: true}, {Name: "created_at", Type: field.TypeTime}, {Name: "updated_at", Type: field.TypeTime}, } diff --git a/backend/db/mutation.go b/backend/db/mutation.go index 0140a20..3debddc 100644 --- a/backend/db/mutation.go +++ b/backend/db/mutation.go @@ -31,6 +31,7 @@ import ( "github.com/chaitin/MonkeyCode/backend/db/user" "github.com/chaitin/MonkeyCode/backend/db/useridentity" "github.com/chaitin/MonkeyCode/backend/db/userloginhistory" + "github.com/chaitin/MonkeyCode/backend/ent/types" "github.com/google/uuid" ) @@ -8816,9 +8817,8 @@ type SettingMutation struct { enable_sso *bool force_two_factor_auth *bool disable_password_login *bool - enable_dingtalk_oauth *bool - dingtalk_client_id *string - dingtalk_client_secret *string + dingtalk_oauth **types.DingtalkOAuth + custom_oauth **types.CustomOAuth created_at *time.Time updated_at *time.Time clearedFields map[string]struct{} @@ -9039,138 +9039,102 @@ func (m *SettingMutation) ResetDisablePasswordLogin() { m.disable_password_login = nil } -// SetEnableDingtalkOauth sets the "enable_dingtalk_oauth" field. -func (m *SettingMutation) SetEnableDingtalkOauth(b bool) { - m.enable_dingtalk_oauth = &b +// SetDingtalkOauth sets the "dingtalk_oauth" field. +func (m *SettingMutation) SetDingtalkOauth(to *types.DingtalkOAuth) { + m.dingtalk_oauth = &to } -// EnableDingtalkOauth returns the value of the "enable_dingtalk_oauth" field in the mutation. -func (m *SettingMutation) EnableDingtalkOauth() (r bool, exists bool) { - v := m.enable_dingtalk_oauth +// DingtalkOauth returns the value of the "dingtalk_oauth" field in the mutation. +func (m *SettingMutation) DingtalkOauth() (r *types.DingtalkOAuth, exists bool) { + v := m.dingtalk_oauth if v == nil { return } return *v, true } -// OldEnableDingtalkOauth returns the old "enable_dingtalk_oauth" field's value of the Setting entity. +// OldDingtalkOauth returns the old "dingtalk_oauth" field's value of the Setting entity. // If the Setting object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *SettingMutation) OldEnableDingtalkOauth(ctx context.Context) (v bool, err error) { +func (m *SettingMutation) OldDingtalkOauth(ctx context.Context) (v *types.DingtalkOAuth, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldEnableDingtalkOauth is only allowed on UpdateOne operations") + return v, errors.New("OldDingtalkOauth is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldEnableDingtalkOauth requires an ID field in the mutation") + return v, errors.New("OldDingtalkOauth requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldEnableDingtalkOauth: %w", err) + return v, fmt.Errorf("querying old value for OldDingtalkOauth: %w", err) } - return oldValue.EnableDingtalkOauth, nil + return oldValue.DingtalkOauth, nil } -// ResetEnableDingtalkOauth resets all changes to the "enable_dingtalk_oauth" field. -func (m *SettingMutation) ResetEnableDingtalkOauth() { - m.enable_dingtalk_oauth = nil +// ClearDingtalkOauth clears the value of the "dingtalk_oauth" field. +func (m *SettingMutation) ClearDingtalkOauth() { + m.dingtalk_oauth = nil + m.clearedFields[setting.FieldDingtalkOauth] = struct{}{} } -// SetDingtalkClientID sets the "dingtalk_client_id" field. -func (m *SettingMutation) SetDingtalkClientID(s string) { - m.dingtalk_client_id = &s -} - -// DingtalkClientID returns the value of the "dingtalk_client_id" field in the mutation. -func (m *SettingMutation) DingtalkClientID() (r string, exists bool) { - v := m.dingtalk_client_id - if v == nil { - return - } - return *v, true -} - -// OldDingtalkClientID returns the old "dingtalk_client_id" field's value of the Setting entity. -// If the Setting object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *SettingMutation) OldDingtalkClientID(ctx context.Context) (v string, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldDingtalkClientID is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldDingtalkClientID requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldDingtalkClientID: %w", err) - } - return oldValue.DingtalkClientID, nil -} - -// ClearDingtalkClientID clears the value of the "dingtalk_client_id" field. -func (m *SettingMutation) ClearDingtalkClientID() { - m.dingtalk_client_id = nil - m.clearedFields[setting.FieldDingtalkClientID] = struct{}{} -} - -// DingtalkClientIDCleared returns if the "dingtalk_client_id" field was cleared in this mutation. -func (m *SettingMutation) DingtalkClientIDCleared() bool { - _, ok := m.clearedFields[setting.FieldDingtalkClientID] +// DingtalkOauthCleared returns if the "dingtalk_oauth" field was cleared in this mutation. +func (m *SettingMutation) DingtalkOauthCleared() bool { + _, ok := m.clearedFields[setting.FieldDingtalkOauth] return ok } -// ResetDingtalkClientID resets all changes to the "dingtalk_client_id" field. -func (m *SettingMutation) ResetDingtalkClientID() { - m.dingtalk_client_id = nil - delete(m.clearedFields, setting.FieldDingtalkClientID) +// ResetDingtalkOauth resets all changes to the "dingtalk_oauth" field. +func (m *SettingMutation) ResetDingtalkOauth() { + m.dingtalk_oauth = nil + delete(m.clearedFields, setting.FieldDingtalkOauth) } -// SetDingtalkClientSecret sets the "dingtalk_client_secret" field. -func (m *SettingMutation) SetDingtalkClientSecret(s string) { - m.dingtalk_client_secret = &s +// SetCustomOauth sets the "custom_oauth" field. +func (m *SettingMutation) SetCustomOauth(to *types.CustomOAuth) { + m.custom_oauth = &to } -// DingtalkClientSecret returns the value of the "dingtalk_client_secret" field in the mutation. -func (m *SettingMutation) DingtalkClientSecret() (r string, exists bool) { - v := m.dingtalk_client_secret +// CustomOauth returns the value of the "custom_oauth" field in the mutation. +func (m *SettingMutation) CustomOauth() (r *types.CustomOAuth, exists bool) { + v := m.custom_oauth if v == nil { return } return *v, true } -// OldDingtalkClientSecret returns the old "dingtalk_client_secret" field's value of the Setting entity. +// OldCustomOauth returns the old "custom_oauth" field's value of the Setting entity. // If the Setting object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *SettingMutation) OldDingtalkClientSecret(ctx context.Context) (v string, err error) { +func (m *SettingMutation) OldCustomOauth(ctx context.Context) (v *types.CustomOAuth, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldDingtalkClientSecret is only allowed on UpdateOne operations") + return v, errors.New("OldCustomOauth is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldDingtalkClientSecret requires an ID field in the mutation") + return v, errors.New("OldCustomOauth requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldDingtalkClientSecret: %w", err) + return v, fmt.Errorf("querying old value for OldCustomOauth: %w", err) } - return oldValue.DingtalkClientSecret, nil + return oldValue.CustomOauth, nil } -// ClearDingtalkClientSecret clears the value of the "dingtalk_client_secret" field. -func (m *SettingMutation) ClearDingtalkClientSecret() { - m.dingtalk_client_secret = nil - m.clearedFields[setting.FieldDingtalkClientSecret] = struct{}{} +// ClearCustomOauth clears the value of the "custom_oauth" field. +func (m *SettingMutation) ClearCustomOauth() { + m.custom_oauth = nil + m.clearedFields[setting.FieldCustomOauth] = struct{}{} } -// DingtalkClientSecretCleared returns if the "dingtalk_client_secret" field was cleared in this mutation. -func (m *SettingMutation) DingtalkClientSecretCleared() bool { - _, ok := m.clearedFields[setting.FieldDingtalkClientSecret] +// CustomOauthCleared returns if the "custom_oauth" field was cleared in this mutation. +func (m *SettingMutation) CustomOauthCleared() bool { + _, ok := m.clearedFields[setting.FieldCustomOauth] return ok } -// ResetDingtalkClientSecret resets all changes to the "dingtalk_client_secret" field. -func (m *SettingMutation) ResetDingtalkClientSecret() { - m.dingtalk_client_secret = nil - delete(m.clearedFields, setting.FieldDingtalkClientSecret) +// ResetCustomOauth resets all changes to the "custom_oauth" field. +func (m *SettingMutation) ResetCustomOauth() { + m.custom_oauth = nil + delete(m.clearedFields, setting.FieldCustomOauth) } // SetCreatedAt sets the "created_at" field. @@ -9279,7 +9243,7 @@ func (m *SettingMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *SettingMutation) Fields() []string { - fields := make([]string, 0, 8) + fields := make([]string, 0, 7) if m.enable_sso != nil { fields = append(fields, setting.FieldEnableSSO) } @@ -9289,14 +9253,11 @@ func (m *SettingMutation) Fields() []string { if m.disable_password_login != nil { fields = append(fields, setting.FieldDisablePasswordLogin) } - if m.enable_dingtalk_oauth != nil { - fields = append(fields, setting.FieldEnableDingtalkOauth) + if m.dingtalk_oauth != nil { + fields = append(fields, setting.FieldDingtalkOauth) } - if m.dingtalk_client_id != nil { - fields = append(fields, setting.FieldDingtalkClientID) - } - if m.dingtalk_client_secret != nil { - fields = append(fields, setting.FieldDingtalkClientSecret) + if m.custom_oauth != nil { + fields = append(fields, setting.FieldCustomOauth) } if m.created_at != nil { fields = append(fields, setting.FieldCreatedAt) @@ -9318,12 +9279,10 @@ func (m *SettingMutation) Field(name string) (ent.Value, bool) { return m.ForceTwoFactorAuth() case setting.FieldDisablePasswordLogin: return m.DisablePasswordLogin() - case setting.FieldEnableDingtalkOauth: - return m.EnableDingtalkOauth() - case setting.FieldDingtalkClientID: - return m.DingtalkClientID() - case setting.FieldDingtalkClientSecret: - return m.DingtalkClientSecret() + case setting.FieldDingtalkOauth: + return m.DingtalkOauth() + case setting.FieldCustomOauth: + return m.CustomOauth() case setting.FieldCreatedAt: return m.CreatedAt() case setting.FieldUpdatedAt: @@ -9343,12 +9302,10 @@ func (m *SettingMutation) OldField(ctx context.Context, name string) (ent.Value, return m.OldForceTwoFactorAuth(ctx) case setting.FieldDisablePasswordLogin: return m.OldDisablePasswordLogin(ctx) - case setting.FieldEnableDingtalkOauth: - return m.OldEnableDingtalkOauth(ctx) - case setting.FieldDingtalkClientID: - return m.OldDingtalkClientID(ctx) - case setting.FieldDingtalkClientSecret: - return m.OldDingtalkClientSecret(ctx) + case setting.FieldDingtalkOauth: + return m.OldDingtalkOauth(ctx) + case setting.FieldCustomOauth: + return m.OldCustomOauth(ctx) case setting.FieldCreatedAt: return m.OldCreatedAt(ctx) case setting.FieldUpdatedAt: @@ -9383,26 +9340,19 @@ func (m *SettingMutation) SetField(name string, value ent.Value) error { } m.SetDisablePasswordLogin(v) return nil - case setting.FieldEnableDingtalkOauth: - v, ok := value.(bool) + case setting.FieldDingtalkOauth: + v, ok := value.(*types.DingtalkOAuth) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetEnableDingtalkOauth(v) + m.SetDingtalkOauth(v) return nil - case setting.FieldDingtalkClientID: - v, ok := value.(string) + case setting.FieldCustomOauth: + v, ok := value.(*types.CustomOAuth) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetDingtalkClientID(v) - return nil - case setting.FieldDingtalkClientSecret: - v, ok := value.(string) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetDingtalkClientSecret(v) + m.SetCustomOauth(v) return nil case setting.FieldCreatedAt: v, ok := value.(time.Time) @@ -9448,11 +9398,11 @@ func (m *SettingMutation) AddField(name string, value ent.Value) error { // mutation. func (m *SettingMutation) ClearedFields() []string { var fields []string - if m.FieldCleared(setting.FieldDingtalkClientID) { - fields = append(fields, setting.FieldDingtalkClientID) + if m.FieldCleared(setting.FieldDingtalkOauth) { + fields = append(fields, setting.FieldDingtalkOauth) } - if m.FieldCleared(setting.FieldDingtalkClientSecret) { - fields = append(fields, setting.FieldDingtalkClientSecret) + if m.FieldCleared(setting.FieldCustomOauth) { + fields = append(fields, setting.FieldCustomOauth) } return fields } @@ -9468,11 +9418,11 @@ func (m *SettingMutation) FieldCleared(name string) bool { // error if the field is not defined in the schema. func (m *SettingMutation) ClearField(name string) error { switch name { - case setting.FieldDingtalkClientID: - m.ClearDingtalkClientID() + case setting.FieldDingtalkOauth: + m.ClearDingtalkOauth() return nil - case setting.FieldDingtalkClientSecret: - m.ClearDingtalkClientSecret() + case setting.FieldCustomOauth: + m.ClearCustomOauth() return nil } return fmt.Errorf("unknown Setting nullable field %s", name) @@ -9491,14 +9441,11 @@ func (m *SettingMutation) ResetField(name string) error { case setting.FieldDisablePasswordLogin: m.ResetDisablePasswordLogin() return nil - case setting.FieldEnableDingtalkOauth: - m.ResetEnableDingtalkOauth() - return nil - case setting.FieldDingtalkClientID: - m.ResetDingtalkClientID() + case setting.FieldDingtalkOauth: + m.ResetDingtalkOauth() return nil - case setting.FieldDingtalkClientSecret: - m.ResetDingtalkClientSecret() + case setting.FieldCustomOauth: + m.ResetCustomOauth() return nil case setting.FieldCreatedAt: m.ResetCreatedAt() diff --git a/backend/db/runtime/runtime.go b/backend/db/runtime/runtime.go index c1ba6bb..447abef 100644 --- a/backend/db/runtime/runtime.go +++ b/backend/db/runtime/runtime.go @@ -232,16 +232,12 @@ func init() { settingDescDisablePasswordLogin := settingFields[3].Descriptor() // setting.DefaultDisablePasswordLogin holds the default value on creation for the disable_password_login field. setting.DefaultDisablePasswordLogin = settingDescDisablePasswordLogin.Default.(bool) - // settingDescEnableDingtalkOauth is the schema descriptor for enable_dingtalk_oauth field. - settingDescEnableDingtalkOauth := settingFields[4].Descriptor() - // setting.DefaultEnableDingtalkOauth holds the default value on creation for the enable_dingtalk_oauth field. - setting.DefaultEnableDingtalkOauth = settingDescEnableDingtalkOauth.Default.(bool) // settingDescCreatedAt is the schema descriptor for created_at field. - settingDescCreatedAt := settingFields[7].Descriptor() + settingDescCreatedAt := settingFields[6].Descriptor() // setting.DefaultCreatedAt holds the default value on creation for the created_at field. setting.DefaultCreatedAt = settingDescCreatedAt.Default.(func() time.Time) // settingDescUpdatedAt is the schema descriptor for updated_at field. - settingDescUpdatedAt := settingFields[8].Descriptor() + settingDescUpdatedAt := settingFields[7].Descriptor() // setting.DefaultUpdatedAt holds the default value on creation for the updated_at field. setting.DefaultUpdatedAt = settingDescUpdatedAt.Default.(func() time.Time) // setting.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. diff --git a/backend/db/setting.go b/backend/db/setting.go index 51bcc21..9188159 100644 --- a/backend/db/setting.go +++ b/backend/db/setting.go @@ -3,6 +3,7 @@ package db import ( + "encoding/json" "fmt" "strings" "time" @@ -10,6 +11,7 @@ import ( "entgo.io/ent" "entgo.io/ent/dialect/sql" "github.com/chaitin/MonkeyCode/backend/db/setting" + "github.com/chaitin/MonkeyCode/backend/ent/types" "github.com/google/uuid" ) @@ -24,12 +26,10 @@ type Setting struct { ForceTwoFactorAuth bool `json:"force_two_factor_auth,omitempty"` // DisablePasswordLogin holds the value of the "disable_password_login" field. DisablePasswordLogin bool `json:"disable_password_login,omitempty"` - // EnableDingtalkOauth holds the value of the "enable_dingtalk_oauth" field. - EnableDingtalkOauth bool `json:"enable_dingtalk_oauth,omitempty"` - // DingtalkClientID holds the value of the "dingtalk_client_id" field. - DingtalkClientID string `json:"dingtalk_client_id,omitempty"` - // DingtalkClientSecret holds the value of the "dingtalk_client_secret" field. - DingtalkClientSecret string `json:"dingtalk_client_secret,omitempty"` + // DingtalkOauth holds the value of the "dingtalk_oauth" field. + DingtalkOauth *types.DingtalkOAuth `json:"dingtalk_oauth,omitempty"` + // CustomOauth holds the value of the "custom_oauth" field. + CustomOauth *types.CustomOAuth `json:"custom_oauth,omitempty"` // CreatedAt holds the value of the "created_at" field. CreatedAt time.Time `json:"created_at,omitempty"` // UpdatedAt holds the value of the "updated_at" field. @@ -42,10 +42,10 @@ func (*Setting) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case setting.FieldEnableSSO, setting.FieldForceTwoFactorAuth, setting.FieldDisablePasswordLogin, setting.FieldEnableDingtalkOauth: + case setting.FieldDingtalkOauth, setting.FieldCustomOauth: + values[i] = new([]byte) + case setting.FieldEnableSSO, setting.FieldForceTwoFactorAuth, setting.FieldDisablePasswordLogin: values[i] = new(sql.NullBool) - case setting.FieldDingtalkClientID, setting.FieldDingtalkClientSecret: - values[i] = new(sql.NullString) case setting.FieldCreatedAt, setting.FieldUpdatedAt: values[i] = new(sql.NullTime) case setting.FieldID: @@ -89,23 +89,21 @@ func (s *Setting) assignValues(columns []string, values []any) error { } else if value.Valid { s.DisablePasswordLogin = value.Bool } - case setting.FieldEnableDingtalkOauth: - if value, ok := values[i].(*sql.NullBool); !ok { - return fmt.Errorf("unexpected type %T for field enable_dingtalk_oauth", values[i]) - } else if value.Valid { - s.EnableDingtalkOauth = value.Bool - } - case setting.FieldDingtalkClientID: - if value, ok := values[i].(*sql.NullString); !ok { - return fmt.Errorf("unexpected type %T for field dingtalk_client_id", values[i]) - } else if value.Valid { - s.DingtalkClientID = value.String + case setting.FieldDingtalkOauth: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field dingtalk_oauth", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &s.DingtalkOauth); err != nil { + return fmt.Errorf("unmarshal field dingtalk_oauth: %w", err) + } } - case setting.FieldDingtalkClientSecret: - if value, ok := values[i].(*sql.NullString); !ok { - return fmt.Errorf("unexpected type %T for field dingtalk_client_secret", values[i]) - } else if value.Valid { - s.DingtalkClientSecret = value.String + case setting.FieldCustomOauth: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field custom_oauth", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &s.CustomOauth); err != nil { + return fmt.Errorf("unmarshal field custom_oauth: %w", err) + } } case setting.FieldCreatedAt: if value, ok := values[i].(*sql.NullTime); !ok { @@ -164,14 +162,11 @@ func (s *Setting) String() string { builder.WriteString("disable_password_login=") builder.WriteString(fmt.Sprintf("%v", s.DisablePasswordLogin)) builder.WriteString(", ") - builder.WriteString("enable_dingtalk_oauth=") - builder.WriteString(fmt.Sprintf("%v", s.EnableDingtalkOauth)) - builder.WriteString(", ") - builder.WriteString("dingtalk_client_id=") - builder.WriteString(s.DingtalkClientID) + builder.WriteString("dingtalk_oauth=") + builder.WriteString(fmt.Sprintf("%v", s.DingtalkOauth)) builder.WriteString(", ") - builder.WriteString("dingtalk_client_secret=") - builder.WriteString(s.DingtalkClientSecret) + builder.WriteString("custom_oauth=") + builder.WriteString(fmt.Sprintf("%v", s.CustomOauth)) builder.WriteString(", ") builder.WriteString("created_at=") builder.WriteString(s.CreatedAt.Format(time.ANSIC)) diff --git a/backend/db/setting/setting.go b/backend/db/setting/setting.go index 6a8fd21..d0037bd 100644 --- a/backend/db/setting/setting.go +++ b/backend/db/setting/setting.go @@ -19,12 +19,10 @@ const ( FieldForceTwoFactorAuth = "force_two_factor_auth" // FieldDisablePasswordLogin holds the string denoting the disable_password_login field in the database. FieldDisablePasswordLogin = "disable_password_login" - // FieldEnableDingtalkOauth holds the string denoting the enable_dingtalk_oauth field in the database. - FieldEnableDingtalkOauth = "enable_dingtalk_oauth" - // FieldDingtalkClientID holds the string denoting the dingtalk_client_id field in the database. - FieldDingtalkClientID = "dingtalk_client_id" - // FieldDingtalkClientSecret holds the string denoting the dingtalk_client_secret field in the database. - FieldDingtalkClientSecret = "dingtalk_client_secret" + // FieldDingtalkOauth holds the string denoting the dingtalk_oauth field in the database. + FieldDingtalkOauth = "dingtalk_oauth" + // FieldCustomOauth holds the string denoting the custom_oauth field in the database. + FieldCustomOauth = "custom_oauth" // FieldCreatedAt holds the string denoting the created_at field in the database. FieldCreatedAt = "created_at" // FieldUpdatedAt holds the string denoting the updated_at field in the database. @@ -39,9 +37,8 @@ var Columns = []string{ FieldEnableSSO, FieldForceTwoFactorAuth, FieldDisablePasswordLogin, - FieldEnableDingtalkOauth, - FieldDingtalkClientID, - FieldDingtalkClientSecret, + FieldDingtalkOauth, + FieldCustomOauth, FieldCreatedAt, FieldUpdatedAt, } @@ -63,8 +60,6 @@ var ( DefaultForceTwoFactorAuth bool // DefaultDisablePasswordLogin holds the default value on creation for the "disable_password_login" field. DefaultDisablePasswordLogin bool - // DefaultEnableDingtalkOauth holds the default value on creation for the "enable_dingtalk_oauth" field. - DefaultEnableDingtalkOauth bool // DefaultCreatedAt holds the default value on creation for the "created_at" field. DefaultCreatedAt func() time.Time // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. @@ -96,21 +91,6 @@ func ByDisablePasswordLogin(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldDisablePasswordLogin, opts...).ToFunc() } -// ByEnableDingtalkOauth orders the results by the enable_dingtalk_oauth field. -func ByEnableDingtalkOauth(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldEnableDingtalkOauth, opts...).ToFunc() -} - -// ByDingtalkClientID orders the results by the dingtalk_client_id field. -func ByDingtalkClientID(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldDingtalkClientID, opts...).ToFunc() -} - -// ByDingtalkClientSecret orders the results by the dingtalk_client_secret field. -func ByDingtalkClientSecret(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldDingtalkClientSecret, opts...).ToFunc() -} - // ByCreatedAt orders the results by the created_at field. func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() diff --git a/backend/db/setting/where.go b/backend/db/setting/where.go index b2db507..1ec5ce7 100644 --- a/backend/db/setting/where.go +++ b/backend/db/setting/where.go @@ -70,21 +70,6 @@ func DisablePasswordLogin(v bool) predicate.Setting { return predicate.Setting(sql.FieldEQ(FieldDisablePasswordLogin, v)) } -// EnableDingtalkOauth applies equality check predicate on the "enable_dingtalk_oauth" field. It's identical to EnableDingtalkOauthEQ. -func EnableDingtalkOauth(v bool) predicate.Setting { - return predicate.Setting(sql.FieldEQ(FieldEnableDingtalkOauth, v)) -} - -// DingtalkClientID applies equality check predicate on the "dingtalk_client_id" field. It's identical to DingtalkClientIDEQ. -func DingtalkClientID(v string) predicate.Setting { - return predicate.Setting(sql.FieldEQ(FieldDingtalkClientID, v)) -} - -// DingtalkClientSecret applies equality check predicate on the "dingtalk_client_secret" field. It's identical to DingtalkClientSecretEQ. -func DingtalkClientSecret(v string) predicate.Setting { - return predicate.Setting(sql.FieldEQ(FieldDingtalkClientSecret, v)) -} - // CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. func CreatedAt(v time.Time) predicate.Setting { return predicate.Setting(sql.FieldEQ(FieldCreatedAt, v)) @@ -125,164 +110,24 @@ func DisablePasswordLoginNEQ(v bool) predicate.Setting { return predicate.Setting(sql.FieldNEQ(FieldDisablePasswordLogin, v)) } -// EnableDingtalkOauthEQ applies the EQ predicate on the "enable_dingtalk_oauth" field. -func EnableDingtalkOauthEQ(v bool) predicate.Setting { - return predicate.Setting(sql.FieldEQ(FieldEnableDingtalkOauth, v)) -} - -// EnableDingtalkOauthNEQ applies the NEQ predicate on the "enable_dingtalk_oauth" field. -func EnableDingtalkOauthNEQ(v bool) predicate.Setting { - return predicate.Setting(sql.FieldNEQ(FieldEnableDingtalkOauth, v)) -} - -// DingtalkClientIDEQ applies the EQ predicate on the "dingtalk_client_id" field. -func DingtalkClientIDEQ(v string) predicate.Setting { - return predicate.Setting(sql.FieldEQ(FieldDingtalkClientID, v)) -} - -// DingtalkClientIDNEQ applies the NEQ predicate on the "dingtalk_client_id" field. -func DingtalkClientIDNEQ(v string) predicate.Setting { - return predicate.Setting(sql.FieldNEQ(FieldDingtalkClientID, v)) -} - -// DingtalkClientIDIn applies the In predicate on the "dingtalk_client_id" field. -func DingtalkClientIDIn(vs ...string) predicate.Setting { - return predicate.Setting(sql.FieldIn(FieldDingtalkClientID, vs...)) -} - -// DingtalkClientIDNotIn applies the NotIn predicate on the "dingtalk_client_id" field. -func DingtalkClientIDNotIn(vs ...string) predicate.Setting { - return predicate.Setting(sql.FieldNotIn(FieldDingtalkClientID, vs...)) -} - -// DingtalkClientIDGT applies the GT predicate on the "dingtalk_client_id" field. -func DingtalkClientIDGT(v string) predicate.Setting { - return predicate.Setting(sql.FieldGT(FieldDingtalkClientID, v)) -} - -// DingtalkClientIDGTE applies the GTE predicate on the "dingtalk_client_id" field. -func DingtalkClientIDGTE(v string) predicate.Setting { - return predicate.Setting(sql.FieldGTE(FieldDingtalkClientID, v)) -} - -// DingtalkClientIDLT applies the LT predicate on the "dingtalk_client_id" field. -func DingtalkClientIDLT(v string) predicate.Setting { - return predicate.Setting(sql.FieldLT(FieldDingtalkClientID, v)) -} - -// DingtalkClientIDLTE applies the LTE predicate on the "dingtalk_client_id" field. -func DingtalkClientIDLTE(v string) predicate.Setting { - return predicate.Setting(sql.FieldLTE(FieldDingtalkClientID, v)) -} - -// DingtalkClientIDContains applies the Contains predicate on the "dingtalk_client_id" field. -func DingtalkClientIDContains(v string) predicate.Setting { - return predicate.Setting(sql.FieldContains(FieldDingtalkClientID, v)) -} - -// DingtalkClientIDHasPrefix applies the HasPrefix predicate on the "dingtalk_client_id" field. -func DingtalkClientIDHasPrefix(v string) predicate.Setting { - return predicate.Setting(sql.FieldHasPrefix(FieldDingtalkClientID, v)) -} - -// DingtalkClientIDHasSuffix applies the HasSuffix predicate on the "dingtalk_client_id" field. -func DingtalkClientIDHasSuffix(v string) predicate.Setting { - return predicate.Setting(sql.FieldHasSuffix(FieldDingtalkClientID, v)) -} - -// DingtalkClientIDIsNil applies the IsNil predicate on the "dingtalk_client_id" field. -func DingtalkClientIDIsNil() predicate.Setting { - return predicate.Setting(sql.FieldIsNull(FieldDingtalkClientID)) -} - -// DingtalkClientIDNotNil applies the NotNil predicate on the "dingtalk_client_id" field. -func DingtalkClientIDNotNil() predicate.Setting { - return predicate.Setting(sql.FieldNotNull(FieldDingtalkClientID)) -} - -// DingtalkClientIDEqualFold applies the EqualFold predicate on the "dingtalk_client_id" field. -func DingtalkClientIDEqualFold(v string) predicate.Setting { - return predicate.Setting(sql.FieldEqualFold(FieldDingtalkClientID, v)) -} - -// DingtalkClientIDContainsFold applies the ContainsFold predicate on the "dingtalk_client_id" field. -func DingtalkClientIDContainsFold(v string) predicate.Setting { - return predicate.Setting(sql.FieldContainsFold(FieldDingtalkClientID, v)) -} - -// DingtalkClientSecretEQ applies the EQ predicate on the "dingtalk_client_secret" field. -func DingtalkClientSecretEQ(v string) predicate.Setting { - return predicate.Setting(sql.FieldEQ(FieldDingtalkClientSecret, v)) -} - -// DingtalkClientSecretNEQ applies the NEQ predicate on the "dingtalk_client_secret" field. -func DingtalkClientSecretNEQ(v string) predicate.Setting { - return predicate.Setting(sql.FieldNEQ(FieldDingtalkClientSecret, v)) -} - -// DingtalkClientSecretIn applies the In predicate on the "dingtalk_client_secret" field. -func DingtalkClientSecretIn(vs ...string) predicate.Setting { - return predicate.Setting(sql.FieldIn(FieldDingtalkClientSecret, vs...)) -} - -// DingtalkClientSecretNotIn applies the NotIn predicate on the "dingtalk_client_secret" field. -func DingtalkClientSecretNotIn(vs ...string) predicate.Setting { - return predicate.Setting(sql.FieldNotIn(FieldDingtalkClientSecret, vs...)) -} - -// DingtalkClientSecretGT applies the GT predicate on the "dingtalk_client_secret" field. -func DingtalkClientSecretGT(v string) predicate.Setting { - return predicate.Setting(sql.FieldGT(FieldDingtalkClientSecret, v)) -} - -// DingtalkClientSecretGTE applies the GTE predicate on the "dingtalk_client_secret" field. -func DingtalkClientSecretGTE(v string) predicate.Setting { - return predicate.Setting(sql.FieldGTE(FieldDingtalkClientSecret, v)) -} - -// DingtalkClientSecretLT applies the LT predicate on the "dingtalk_client_secret" field. -func DingtalkClientSecretLT(v string) predicate.Setting { - return predicate.Setting(sql.FieldLT(FieldDingtalkClientSecret, v)) -} - -// DingtalkClientSecretLTE applies the LTE predicate on the "dingtalk_client_secret" field. -func DingtalkClientSecretLTE(v string) predicate.Setting { - return predicate.Setting(sql.FieldLTE(FieldDingtalkClientSecret, v)) -} - -// DingtalkClientSecretContains applies the Contains predicate on the "dingtalk_client_secret" field. -func DingtalkClientSecretContains(v string) predicate.Setting { - return predicate.Setting(sql.FieldContains(FieldDingtalkClientSecret, v)) -} - -// DingtalkClientSecretHasPrefix applies the HasPrefix predicate on the "dingtalk_client_secret" field. -func DingtalkClientSecretHasPrefix(v string) predicate.Setting { - return predicate.Setting(sql.FieldHasPrefix(FieldDingtalkClientSecret, v)) -} - -// DingtalkClientSecretHasSuffix applies the HasSuffix predicate on the "dingtalk_client_secret" field. -func DingtalkClientSecretHasSuffix(v string) predicate.Setting { - return predicate.Setting(sql.FieldHasSuffix(FieldDingtalkClientSecret, v)) -} - -// DingtalkClientSecretIsNil applies the IsNil predicate on the "dingtalk_client_secret" field. -func DingtalkClientSecretIsNil() predicate.Setting { - return predicate.Setting(sql.FieldIsNull(FieldDingtalkClientSecret)) +// DingtalkOauthIsNil applies the IsNil predicate on the "dingtalk_oauth" field. +func DingtalkOauthIsNil() predicate.Setting { + return predicate.Setting(sql.FieldIsNull(FieldDingtalkOauth)) } -// DingtalkClientSecretNotNil applies the NotNil predicate on the "dingtalk_client_secret" field. -func DingtalkClientSecretNotNil() predicate.Setting { - return predicate.Setting(sql.FieldNotNull(FieldDingtalkClientSecret)) +// DingtalkOauthNotNil applies the NotNil predicate on the "dingtalk_oauth" field. +func DingtalkOauthNotNil() predicate.Setting { + return predicate.Setting(sql.FieldNotNull(FieldDingtalkOauth)) } -// DingtalkClientSecretEqualFold applies the EqualFold predicate on the "dingtalk_client_secret" field. -func DingtalkClientSecretEqualFold(v string) predicate.Setting { - return predicate.Setting(sql.FieldEqualFold(FieldDingtalkClientSecret, v)) +// CustomOauthIsNil applies the IsNil predicate on the "custom_oauth" field. +func CustomOauthIsNil() predicate.Setting { + return predicate.Setting(sql.FieldIsNull(FieldCustomOauth)) } -// DingtalkClientSecretContainsFold applies the ContainsFold predicate on the "dingtalk_client_secret" field. -func DingtalkClientSecretContainsFold(v string) predicate.Setting { - return predicate.Setting(sql.FieldContainsFold(FieldDingtalkClientSecret, v)) +// CustomOauthNotNil applies the NotNil predicate on the "custom_oauth" field. +func CustomOauthNotNil() predicate.Setting { + return predicate.Setting(sql.FieldNotNull(FieldCustomOauth)) } // CreatedAtEQ applies the EQ predicate on the "created_at" field. diff --git a/backend/db/setting_create.go b/backend/db/setting_create.go index 069217b..5fda099 100644 --- a/backend/db/setting_create.go +++ b/backend/db/setting_create.go @@ -13,6 +13,7 @@ import ( "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" "github.com/chaitin/MonkeyCode/backend/db/setting" + "github.com/chaitin/MonkeyCode/backend/ent/types" "github.com/google/uuid" ) @@ -66,45 +67,15 @@ func (sc *SettingCreate) SetNillableDisablePasswordLogin(b *bool) *SettingCreate return sc } -// SetEnableDingtalkOauth sets the "enable_dingtalk_oauth" field. -func (sc *SettingCreate) SetEnableDingtalkOauth(b bool) *SettingCreate { - sc.mutation.SetEnableDingtalkOauth(b) +// SetDingtalkOauth sets the "dingtalk_oauth" field. +func (sc *SettingCreate) SetDingtalkOauth(to *types.DingtalkOAuth) *SettingCreate { + sc.mutation.SetDingtalkOauth(to) return sc } -// SetNillableEnableDingtalkOauth sets the "enable_dingtalk_oauth" field if the given value is not nil. -func (sc *SettingCreate) SetNillableEnableDingtalkOauth(b *bool) *SettingCreate { - if b != nil { - sc.SetEnableDingtalkOauth(*b) - } - return sc -} - -// SetDingtalkClientID sets the "dingtalk_client_id" field. -func (sc *SettingCreate) SetDingtalkClientID(s string) *SettingCreate { - sc.mutation.SetDingtalkClientID(s) - return sc -} - -// SetNillableDingtalkClientID sets the "dingtalk_client_id" field if the given value is not nil. -func (sc *SettingCreate) SetNillableDingtalkClientID(s *string) *SettingCreate { - if s != nil { - sc.SetDingtalkClientID(*s) - } - return sc -} - -// SetDingtalkClientSecret sets the "dingtalk_client_secret" field. -func (sc *SettingCreate) SetDingtalkClientSecret(s string) *SettingCreate { - sc.mutation.SetDingtalkClientSecret(s) - return sc -} - -// SetNillableDingtalkClientSecret sets the "dingtalk_client_secret" field if the given value is not nil. -func (sc *SettingCreate) SetNillableDingtalkClientSecret(s *string) *SettingCreate { - if s != nil { - sc.SetDingtalkClientSecret(*s) - } +// SetCustomOauth sets the "custom_oauth" field. +func (sc *SettingCreate) SetCustomOauth(to *types.CustomOAuth) *SettingCreate { + sc.mutation.SetCustomOauth(to) return sc } @@ -189,10 +160,6 @@ func (sc *SettingCreate) defaults() { v := setting.DefaultDisablePasswordLogin sc.mutation.SetDisablePasswordLogin(v) } - if _, ok := sc.mutation.EnableDingtalkOauth(); !ok { - v := setting.DefaultEnableDingtalkOauth - sc.mutation.SetEnableDingtalkOauth(v) - } if _, ok := sc.mutation.CreatedAt(); !ok { v := setting.DefaultCreatedAt() sc.mutation.SetCreatedAt(v) @@ -214,9 +181,6 @@ func (sc *SettingCreate) check() error { if _, ok := sc.mutation.DisablePasswordLogin(); !ok { return &ValidationError{Name: "disable_password_login", err: errors.New(`db: missing required field "Setting.disable_password_login"`)} } - if _, ok := sc.mutation.EnableDingtalkOauth(); !ok { - return &ValidationError{Name: "enable_dingtalk_oauth", err: errors.New(`db: missing required field "Setting.enable_dingtalk_oauth"`)} - } if _, ok := sc.mutation.CreatedAt(); !ok { return &ValidationError{Name: "created_at", err: errors.New(`db: missing required field "Setting.created_at"`)} } @@ -271,17 +235,13 @@ func (sc *SettingCreate) createSpec() (*Setting, *sqlgraph.CreateSpec) { _spec.SetField(setting.FieldDisablePasswordLogin, field.TypeBool, value) _node.DisablePasswordLogin = value } - if value, ok := sc.mutation.EnableDingtalkOauth(); ok { - _spec.SetField(setting.FieldEnableDingtalkOauth, field.TypeBool, value) - _node.EnableDingtalkOauth = value - } - if value, ok := sc.mutation.DingtalkClientID(); ok { - _spec.SetField(setting.FieldDingtalkClientID, field.TypeString, value) - _node.DingtalkClientID = value + if value, ok := sc.mutation.DingtalkOauth(); ok { + _spec.SetField(setting.FieldDingtalkOauth, field.TypeJSON, value) + _node.DingtalkOauth = value } - if value, ok := sc.mutation.DingtalkClientSecret(); ok { - _spec.SetField(setting.FieldDingtalkClientSecret, field.TypeString, value) - _node.DingtalkClientSecret = value + if value, ok := sc.mutation.CustomOauth(); ok { + _spec.SetField(setting.FieldCustomOauth, field.TypeJSON, value) + _node.CustomOauth = value } if value, ok := sc.mutation.CreatedAt(); ok { _spec.SetField(setting.FieldCreatedAt, field.TypeTime, value) @@ -379,51 +339,39 @@ func (u *SettingUpsert) UpdateDisablePasswordLogin() *SettingUpsert { return u } -// SetEnableDingtalkOauth sets the "enable_dingtalk_oauth" field. -func (u *SettingUpsert) SetEnableDingtalkOauth(v bool) *SettingUpsert { - u.Set(setting.FieldEnableDingtalkOauth, v) +// SetDingtalkOauth sets the "dingtalk_oauth" field. +func (u *SettingUpsert) SetDingtalkOauth(v *types.DingtalkOAuth) *SettingUpsert { + u.Set(setting.FieldDingtalkOauth, v) return u } -// UpdateEnableDingtalkOauth sets the "enable_dingtalk_oauth" field to the value that was provided on create. -func (u *SettingUpsert) UpdateEnableDingtalkOauth() *SettingUpsert { - u.SetExcluded(setting.FieldEnableDingtalkOauth) +// UpdateDingtalkOauth sets the "dingtalk_oauth" field to the value that was provided on create. +func (u *SettingUpsert) UpdateDingtalkOauth() *SettingUpsert { + u.SetExcluded(setting.FieldDingtalkOauth) return u } -// SetDingtalkClientID sets the "dingtalk_client_id" field. -func (u *SettingUpsert) SetDingtalkClientID(v string) *SettingUpsert { - u.Set(setting.FieldDingtalkClientID, v) +// ClearDingtalkOauth clears the value of the "dingtalk_oauth" field. +func (u *SettingUpsert) ClearDingtalkOauth() *SettingUpsert { + u.SetNull(setting.FieldDingtalkOauth) return u } -// UpdateDingtalkClientID sets the "dingtalk_client_id" field to the value that was provided on create. -func (u *SettingUpsert) UpdateDingtalkClientID() *SettingUpsert { - u.SetExcluded(setting.FieldDingtalkClientID) +// SetCustomOauth sets the "custom_oauth" field. +func (u *SettingUpsert) SetCustomOauth(v *types.CustomOAuth) *SettingUpsert { + u.Set(setting.FieldCustomOauth, v) return u } -// ClearDingtalkClientID clears the value of the "dingtalk_client_id" field. -func (u *SettingUpsert) ClearDingtalkClientID() *SettingUpsert { - u.SetNull(setting.FieldDingtalkClientID) +// UpdateCustomOauth sets the "custom_oauth" field to the value that was provided on create. +func (u *SettingUpsert) UpdateCustomOauth() *SettingUpsert { + u.SetExcluded(setting.FieldCustomOauth) return u } -// SetDingtalkClientSecret sets the "dingtalk_client_secret" field. -func (u *SettingUpsert) SetDingtalkClientSecret(v string) *SettingUpsert { - u.Set(setting.FieldDingtalkClientSecret, v) - return u -} - -// UpdateDingtalkClientSecret sets the "dingtalk_client_secret" field to the value that was provided on create. -func (u *SettingUpsert) UpdateDingtalkClientSecret() *SettingUpsert { - u.SetExcluded(setting.FieldDingtalkClientSecret) - return u -} - -// ClearDingtalkClientSecret clears the value of the "dingtalk_client_secret" field. -func (u *SettingUpsert) ClearDingtalkClientSecret() *SettingUpsert { - u.SetNull(setting.FieldDingtalkClientSecret) +// ClearCustomOauth clears the value of the "custom_oauth" field. +func (u *SettingUpsert) ClearCustomOauth() *SettingUpsert { + u.SetNull(setting.FieldCustomOauth) return u } @@ -541,59 +489,45 @@ func (u *SettingUpsertOne) UpdateDisablePasswordLogin() *SettingUpsertOne { }) } -// SetEnableDingtalkOauth sets the "enable_dingtalk_oauth" field. -func (u *SettingUpsertOne) SetEnableDingtalkOauth(v bool) *SettingUpsertOne { - return u.Update(func(s *SettingUpsert) { - s.SetEnableDingtalkOauth(v) - }) -} - -// UpdateEnableDingtalkOauth sets the "enable_dingtalk_oauth" field to the value that was provided on create. -func (u *SettingUpsertOne) UpdateEnableDingtalkOauth() *SettingUpsertOne { - return u.Update(func(s *SettingUpsert) { - s.UpdateEnableDingtalkOauth() - }) -} - -// SetDingtalkClientID sets the "dingtalk_client_id" field. -func (u *SettingUpsertOne) SetDingtalkClientID(v string) *SettingUpsertOne { +// SetDingtalkOauth sets the "dingtalk_oauth" field. +func (u *SettingUpsertOne) SetDingtalkOauth(v *types.DingtalkOAuth) *SettingUpsertOne { return u.Update(func(s *SettingUpsert) { - s.SetDingtalkClientID(v) + s.SetDingtalkOauth(v) }) } -// UpdateDingtalkClientID sets the "dingtalk_client_id" field to the value that was provided on create. -func (u *SettingUpsertOne) UpdateDingtalkClientID() *SettingUpsertOne { +// UpdateDingtalkOauth sets the "dingtalk_oauth" field to the value that was provided on create. +func (u *SettingUpsertOne) UpdateDingtalkOauth() *SettingUpsertOne { return u.Update(func(s *SettingUpsert) { - s.UpdateDingtalkClientID() + s.UpdateDingtalkOauth() }) } -// ClearDingtalkClientID clears the value of the "dingtalk_client_id" field. -func (u *SettingUpsertOne) ClearDingtalkClientID() *SettingUpsertOne { +// ClearDingtalkOauth clears the value of the "dingtalk_oauth" field. +func (u *SettingUpsertOne) ClearDingtalkOauth() *SettingUpsertOne { return u.Update(func(s *SettingUpsert) { - s.ClearDingtalkClientID() + s.ClearDingtalkOauth() }) } -// SetDingtalkClientSecret sets the "dingtalk_client_secret" field. -func (u *SettingUpsertOne) SetDingtalkClientSecret(v string) *SettingUpsertOne { +// SetCustomOauth sets the "custom_oauth" field. +func (u *SettingUpsertOne) SetCustomOauth(v *types.CustomOAuth) *SettingUpsertOne { return u.Update(func(s *SettingUpsert) { - s.SetDingtalkClientSecret(v) + s.SetCustomOauth(v) }) } -// UpdateDingtalkClientSecret sets the "dingtalk_client_secret" field to the value that was provided on create. -func (u *SettingUpsertOne) UpdateDingtalkClientSecret() *SettingUpsertOne { +// UpdateCustomOauth sets the "custom_oauth" field to the value that was provided on create. +func (u *SettingUpsertOne) UpdateCustomOauth() *SettingUpsertOne { return u.Update(func(s *SettingUpsert) { - s.UpdateDingtalkClientSecret() + s.UpdateCustomOauth() }) } -// ClearDingtalkClientSecret clears the value of the "dingtalk_client_secret" field. -func (u *SettingUpsertOne) ClearDingtalkClientSecret() *SettingUpsertOne { +// ClearCustomOauth clears the value of the "custom_oauth" field. +func (u *SettingUpsertOne) ClearCustomOauth() *SettingUpsertOne { return u.Update(func(s *SettingUpsert) { - s.ClearDingtalkClientSecret() + s.ClearCustomOauth() }) } @@ -882,59 +816,45 @@ func (u *SettingUpsertBulk) UpdateDisablePasswordLogin() *SettingUpsertBulk { }) } -// SetEnableDingtalkOauth sets the "enable_dingtalk_oauth" field. -func (u *SettingUpsertBulk) SetEnableDingtalkOauth(v bool) *SettingUpsertBulk { - return u.Update(func(s *SettingUpsert) { - s.SetEnableDingtalkOauth(v) - }) -} - -// UpdateEnableDingtalkOauth sets the "enable_dingtalk_oauth" field to the value that was provided on create. -func (u *SettingUpsertBulk) UpdateEnableDingtalkOauth() *SettingUpsertBulk { - return u.Update(func(s *SettingUpsert) { - s.UpdateEnableDingtalkOauth() - }) -} - -// SetDingtalkClientID sets the "dingtalk_client_id" field. -func (u *SettingUpsertBulk) SetDingtalkClientID(v string) *SettingUpsertBulk { +// SetDingtalkOauth sets the "dingtalk_oauth" field. +func (u *SettingUpsertBulk) SetDingtalkOauth(v *types.DingtalkOAuth) *SettingUpsertBulk { return u.Update(func(s *SettingUpsert) { - s.SetDingtalkClientID(v) + s.SetDingtalkOauth(v) }) } -// UpdateDingtalkClientID sets the "dingtalk_client_id" field to the value that was provided on create. -func (u *SettingUpsertBulk) UpdateDingtalkClientID() *SettingUpsertBulk { +// UpdateDingtalkOauth sets the "dingtalk_oauth" field to the value that was provided on create. +func (u *SettingUpsertBulk) UpdateDingtalkOauth() *SettingUpsertBulk { return u.Update(func(s *SettingUpsert) { - s.UpdateDingtalkClientID() + s.UpdateDingtalkOauth() }) } -// ClearDingtalkClientID clears the value of the "dingtalk_client_id" field. -func (u *SettingUpsertBulk) ClearDingtalkClientID() *SettingUpsertBulk { +// ClearDingtalkOauth clears the value of the "dingtalk_oauth" field. +func (u *SettingUpsertBulk) ClearDingtalkOauth() *SettingUpsertBulk { return u.Update(func(s *SettingUpsert) { - s.ClearDingtalkClientID() + s.ClearDingtalkOauth() }) } -// SetDingtalkClientSecret sets the "dingtalk_client_secret" field. -func (u *SettingUpsertBulk) SetDingtalkClientSecret(v string) *SettingUpsertBulk { +// SetCustomOauth sets the "custom_oauth" field. +func (u *SettingUpsertBulk) SetCustomOauth(v *types.CustomOAuth) *SettingUpsertBulk { return u.Update(func(s *SettingUpsert) { - s.SetDingtalkClientSecret(v) + s.SetCustomOauth(v) }) } -// UpdateDingtalkClientSecret sets the "dingtalk_client_secret" field to the value that was provided on create. -func (u *SettingUpsertBulk) UpdateDingtalkClientSecret() *SettingUpsertBulk { +// UpdateCustomOauth sets the "custom_oauth" field to the value that was provided on create. +func (u *SettingUpsertBulk) UpdateCustomOauth() *SettingUpsertBulk { return u.Update(func(s *SettingUpsert) { - s.UpdateDingtalkClientSecret() + s.UpdateCustomOauth() }) } -// ClearDingtalkClientSecret clears the value of the "dingtalk_client_secret" field. -func (u *SettingUpsertBulk) ClearDingtalkClientSecret() *SettingUpsertBulk { +// ClearCustomOauth clears the value of the "custom_oauth" field. +func (u *SettingUpsertBulk) ClearCustomOauth() *SettingUpsertBulk { return u.Update(func(s *SettingUpsert) { - s.ClearDingtalkClientSecret() + s.ClearCustomOauth() }) } diff --git a/backend/db/setting_update.go b/backend/db/setting_update.go index 6d57184..13d233f 100644 --- a/backend/db/setting_update.go +++ b/backend/db/setting_update.go @@ -13,6 +13,7 @@ import ( "entgo.io/ent/schema/field" "github.com/chaitin/MonkeyCode/backend/db/predicate" "github.com/chaitin/MonkeyCode/backend/db/setting" + "github.com/chaitin/MonkeyCode/backend/ent/types" ) // SettingUpdate is the builder for updating Setting entities. @@ -71,57 +72,27 @@ func (su *SettingUpdate) SetNillableDisablePasswordLogin(b *bool) *SettingUpdate return su } -// SetEnableDingtalkOauth sets the "enable_dingtalk_oauth" field. -func (su *SettingUpdate) SetEnableDingtalkOauth(b bool) *SettingUpdate { - su.mutation.SetEnableDingtalkOauth(b) +// SetDingtalkOauth sets the "dingtalk_oauth" field. +func (su *SettingUpdate) SetDingtalkOauth(to *types.DingtalkOAuth) *SettingUpdate { + su.mutation.SetDingtalkOauth(to) return su } -// SetNillableEnableDingtalkOauth sets the "enable_dingtalk_oauth" field if the given value is not nil. -func (su *SettingUpdate) SetNillableEnableDingtalkOauth(b *bool) *SettingUpdate { - if b != nil { - su.SetEnableDingtalkOauth(*b) - } - return su -} - -// SetDingtalkClientID sets the "dingtalk_client_id" field. -func (su *SettingUpdate) SetDingtalkClientID(s string) *SettingUpdate { - su.mutation.SetDingtalkClientID(s) - return su -} - -// SetNillableDingtalkClientID sets the "dingtalk_client_id" field if the given value is not nil. -func (su *SettingUpdate) SetNillableDingtalkClientID(s *string) *SettingUpdate { - if s != nil { - su.SetDingtalkClientID(*s) - } +// ClearDingtalkOauth clears the value of the "dingtalk_oauth" field. +func (su *SettingUpdate) ClearDingtalkOauth() *SettingUpdate { + su.mutation.ClearDingtalkOauth() return su } -// ClearDingtalkClientID clears the value of the "dingtalk_client_id" field. -func (su *SettingUpdate) ClearDingtalkClientID() *SettingUpdate { - su.mutation.ClearDingtalkClientID() +// SetCustomOauth sets the "custom_oauth" field. +func (su *SettingUpdate) SetCustomOauth(to *types.CustomOAuth) *SettingUpdate { + su.mutation.SetCustomOauth(to) return su } -// SetDingtalkClientSecret sets the "dingtalk_client_secret" field. -func (su *SettingUpdate) SetDingtalkClientSecret(s string) *SettingUpdate { - su.mutation.SetDingtalkClientSecret(s) - return su -} - -// SetNillableDingtalkClientSecret sets the "dingtalk_client_secret" field if the given value is not nil. -func (su *SettingUpdate) SetNillableDingtalkClientSecret(s *string) *SettingUpdate { - if s != nil { - su.SetDingtalkClientSecret(*s) - } - return su -} - -// ClearDingtalkClientSecret clears the value of the "dingtalk_client_secret" field. -func (su *SettingUpdate) ClearDingtalkClientSecret() *SettingUpdate { - su.mutation.ClearDingtalkClientSecret() +// ClearCustomOauth clears the value of the "custom_oauth" field. +func (su *SettingUpdate) ClearCustomOauth() *SettingUpdate { + su.mutation.ClearCustomOauth() return su } @@ -210,20 +181,17 @@ func (su *SettingUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := su.mutation.DisablePasswordLogin(); ok { _spec.SetField(setting.FieldDisablePasswordLogin, field.TypeBool, value) } - if value, ok := su.mutation.EnableDingtalkOauth(); ok { - _spec.SetField(setting.FieldEnableDingtalkOauth, field.TypeBool, value) - } - if value, ok := su.mutation.DingtalkClientID(); ok { - _spec.SetField(setting.FieldDingtalkClientID, field.TypeString, value) + if value, ok := su.mutation.DingtalkOauth(); ok { + _spec.SetField(setting.FieldDingtalkOauth, field.TypeJSON, value) } - if su.mutation.DingtalkClientIDCleared() { - _spec.ClearField(setting.FieldDingtalkClientID, field.TypeString) + if su.mutation.DingtalkOauthCleared() { + _spec.ClearField(setting.FieldDingtalkOauth, field.TypeJSON) } - if value, ok := su.mutation.DingtalkClientSecret(); ok { - _spec.SetField(setting.FieldDingtalkClientSecret, field.TypeString, value) + if value, ok := su.mutation.CustomOauth(); ok { + _spec.SetField(setting.FieldCustomOauth, field.TypeJSON, value) } - if su.mutation.DingtalkClientSecretCleared() { - _spec.ClearField(setting.FieldDingtalkClientSecret, field.TypeString) + if su.mutation.CustomOauthCleared() { + _spec.ClearField(setting.FieldCustomOauth, field.TypeJSON) } if value, ok := su.mutation.CreatedAt(); ok { _spec.SetField(setting.FieldCreatedAt, field.TypeTime, value) @@ -295,57 +263,27 @@ func (suo *SettingUpdateOne) SetNillableDisablePasswordLogin(b *bool) *SettingUp return suo } -// SetEnableDingtalkOauth sets the "enable_dingtalk_oauth" field. -func (suo *SettingUpdateOne) SetEnableDingtalkOauth(b bool) *SettingUpdateOne { - suo.mutation.SetEnableDingtalkOauth(b) +// SetDingtalkOauth sets the "dingtalk_oauth" field. +func (suo *SettingUpdateOne) SetDingtalkOauth(to *types.DingtalkOAuth) *SettingUpdateOne { + suo.mutation.SetDingtalkOauth(to) return suo } -// SetNillableEnableDingtalkOauth sets the "enable_dingtalk_oauth" field if the given value is not nil. -func (suo *SettingUpdateOne) SetNillableEnableDingtalkOauth(b *bool) *SettingUpdateOne { - if b != nil { - suo.SetEnableDingtalkOauth(*b) - } - return suo -} - -// SetDingtalkClientID sets the "dingtalk_client_id" field. -func (suo *SettingUpdateOne) SetDingtalkClientID(s string) *SettingUpdateOne { - suo.mutation.SetDingtalkClientID(s) - return suo -} - -// SetNillableDingtalkClientID sets the "dingtalk_client_id" field if the given value is not nil. -func (suo *SettingUpdateOne) SetNillableDingtalkClientID(s *string) *SettingUpdateOne { - if s != nil { - suo.SetDingtalkClientID(*s) - } +// ClearDingtalkOauth clears the value of the "dingtalk_oauth" field. +func (suo *SettingUpdateOne) ClearDingtalkOauth() *SettingUpdateOne { + suo.mutation.ClearDingtalkOauth() return suo } -// ClearDingtalkClientID clears the value of the "dingtalk_client_id" field. -func (suo *SettingUpdateOne) ClearDingtalkClientID() *SettingUpdateOne { - suo.mutation.ClearDingtalkClientID() +// SetCustomOauth sets the "custom_oauth" field. +func (suo *SettingUpdateOne) SetCustomOauth(to *types.CustomOAuth) *SettingUpdateOne { + suo.mutation.SetCustomOauth(to) return suo } -// SetDingtalkClientSecret sets the "dingtalk_client_secret" field. -func (suo *SettingUpdateOne) SetDingtalkClientSecret(s string) *SettingUpdateOne { - suo.mutation.SetDingtalkClientSecret(s) - return suo -} - -// SetNillableDingtalkClientSecret sets the "dingtalk_client_secret" field if the given value is not nil. -func (suo *SettingUpdateOne) SetNillableDingtalkClientSecret(s *string) *SettingUpdateOne { - if s != nil { - suo.SetDingtalkClientSecret(*s) - } - return suo -} - -// ClearDingtalkClientSecret clears the value of the "dingtalk_client_secret" field. -func (suo *SettingUpdateOne) ClearDingtalkClientSecret() *SettingUpdateOne { - suo.mutation.ClearDingtalkClientSecret() +// ClearCustomOauth clears the value of the "custom_oauth" field. +func (suo *SettingUpdateOne) ClearCustomOauth() *SettingUpdateOne { + suo.mutation.ClearCustomOauth() return suo } @@ -464,20 +402,17 @@ func (suo *SettingUpdateOne) sqlSave(ctx context.Context) (_node *Setting, err e if value, ok := suo.mutation.DisablePasswordLogin(); ok { _spec.SetField(setting.FieldDisablePasswordLogin, field.TypeBool, value) } - if value, ok := suo.mutation.EnableDingtalkOauth(); ok { - _spec.SetField(setting.FieldEnableDingtalkOauth, field.TypeBool, value) - } - if value, ok := suo.mutation.DingtalkClientID(); ok { - _spec.SetField(setting.FieldDingtalkClientID, field.TypeString, value) + if value, ok := suo.mutation.DingtalkOauth(); ok { + _spec.SetField(setting.FieldDingtalkOauth, field.TypeJSON, value) } - if suo.mutation.DingtalkClientIDCleared() { - _spec.ClearField(setting.FieldDingtalkClientID, field.TypeString) + if suo.mutation.DingtalkOauthCleared() { + _spec.ClearField(setting.FieldDingtalkOauth, field.TypeJSON) } - if value, ok := suo.mutation.DingtalkClientSecret(); ok { - _spec.SetField(setting.FieldDingtalkClientSecret, field.TypeString, value) + if value, ok := suo.mutation.CustomOauth(); ok { + _spec.SetField(setting.FieldCustomOauth, field.TypeJSON, value) } - if suo.mutation.DingtalkClientSecretCleared() { - _spec.ClearField(setting.FieldDingtalkClientSecret, field.TypeString) + if suo.mutation.CustomOauthCleared() { + _spec.ClearField(setting.FieldCustomOauth, field.TypeJSON) } if value, ok := suo.mutation.CreatedAt(); ok { _spec.SetField(setting.FieldCreatedAt, field.TypeTime, value) diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index e20e3a3..8f27fa5 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -17,7 +17,7 @@ "application/json" ], "tags": [ - "User" + "Admin" ], "summary": "创建管理员", "operationId": "create-admin", @@ -64,7 +64,7 @@ "application/json" ], "tags": [ - "User" + "Admin" ], "summary": "删除管理员", "operationId": "delete-admin", @@ -109,7 +109,7 @@ "application/json" ], "tags": [ - "User" + "Admin" ], "summary": "获取管理员用户列表", "operationId": "list-admin-user", @@ -165,7 +165,7 @@ "application/json" ], "tags": [ - "User" + "Admin" ], "summary": "管理员登录", "operationId": "admin-login", @@ -212,7 +212,7 @@ "application/json" ], "tags": [ - "User" + "Admin" ], "summary": "获取管理员登录历史", "operationId": "admin-login-history", @@ -268,7 +268,7 @@ "application/json" ], "tags": [ - "User" + "Admin" ], "summary": "获取系统设置", "operationId": "get-setting", @@ -302,7 +302,7 @@ "application/json" ], "tags": [ - "User" + "Admin" ], "summary": "更新系统设置", "operationId": "update-setting", @@ -1604,15 +1604,23 @@ "summary": "用户 OAuth 登录或注册", "operationId": "user-oauth-signup-or-in", "parameters": [ + { + "type": "string", + "description": "邀请码", + "name": "inviate_code", + "in": "query" + }, { "enum": [ "email", - "dingtalk" + "dingtalk", + "custom" ], "type": "string", "x-enum-varnames": [ "UserPlatformEmail", - "UserPlatformDingTalk" + "UserPlatformDingTalk", + "UserPlatformCustom" ], "description": "第三方平台 dingtalk", "name": "platform", @@ -1947,11 +1955,13 @@ "type": "string", "enum": [ "email", - "dingtalk" + "dingtalk", + "custom" ], "x-enum-varnames": [ "UserPlatformEmail", - "UserPlatformDingTalk" + "UserPlatformDingTalk", + "UserPlatformCustom" ] }, "consts.UserStatus": { @@ -2289,6 +2299,75 @@ } } }, + "domain.CustomOAuth": { + "type": "object", + "properties": { + "access_token_url": { + "description": "自定义OAuth访问令牌URL", + "type": "string" + }, + "authorize_url": { + "description": "自定义OAuth授权URL", + "type": "string" + }, + "avatar_field": { + "description": "用户信息回包中的头像URL字段名", + "type": "string" + }, + "client_id": { + "description": "自定义客户端ID", + "type": "string" + }, + "client_secret": { + "description": "自定义客户端密钥", + "type": "string" + }, + "email_field": { + "description": "用户信息回包中的邮箱字段名", + "type": "string" + }, + "enable": { + "description": "自定义OAuth开关", + "type": "boolean" + }, + "id_field": { + "description": "用户信息回包中的ID字段名", + "type": "string" + }, + "name_field": { + "description": "用户信息回包中的用户名字段名", + "type": "string" + }, + "scopes": { + "description": "自定义OAuth Scope列表", + "type": "array", + "items": { + "type": "string" + } + }, + "userinfo_url": { + "description": "自定义OAuth用户信息URL", + "type": "string" + } + } + }, + "domain.DingtalkOAuth": { + "type": "object", + "properties": { + "client_id": { + "description": "钉钉客户端ID", + "type": "string" + }, + "client_secret": { + "description": "钉钉客户端密钥", + "type": "string" + }, + "enable": { + "description": "钉钉OAuth开关", + "type": "boolean" + } + } + }, "domain.IPInfo": { "type": "object", "properties": { @@ -2695,14 +2774,26 @@ "description": "创建时间", "type": "integer" }, + "custom_oauth": { + "description": "自定义OAuth接入", + "allOf": [ + { + "$ref": "#/definitions/domain.CustomOAuth" + } + ] + }, + "dingtalk_oauth": { + "description": "钉钉OAuth接入", + "allOf": [ + { + "$ref": "#/definitions/domain.DingtalkOAuth" + } + ] + }, "disable_password_login": { "description": "是否禁用密码登录", "type": "boolean" }, - "enable_dingtalk_oauth": { - "description": "是否开启钉钉OAuth", - "type": "boolean" - }, "enable_sso": { "description": "是否开启SSO", "type": "boolean" @@ -2893,22 +2984,26 @@ "domain.UpdateSettingReq": { "type": "object", "properties": { - "dingtalk_client_id": { - "description": "钉钉客户端ID", - "type": "string" + "custom_oauth": { + "description": "自定义OAuth配置", + "allOf": [ + { + "$ref": "#/definitions/domain.CustomOAuth" + } + ] }, - "dingtalk_client_secret": { - "description": "钉钉客户端密钥", - "type": "string" + "dingtalk_oauth": { + "description": "钉钉OAuth配置", + "allOf": [ + { + "$ref": "#/definitions/domain.DingtalkOAuth" + } + ] }, "disable_password_login": { "description": "是否禁用密码登录", "type": "boolean" }, - "enable_dingtalk_oauth": { - "description": "是否开启钉钉OAuth", - "type": "boolean" - }, "enable_sso": { "description": "是否开启SSO", "type": "boolean" @@ -2946,6 +3041,10 @@ "domain.User": { "type": "object", "properties": { + "avatar_url": { + "description": "头像URL", + "type": "string" + }, "created_at": { "description": "创建时间", "type": "integer" diff --git a/backend/domain/oauth.go b/backend/domain/oauth.go index 776053c..76c0357 100644 --- a/backend/domain/oauth.go +++ b/backend/domain/oauth.go @@ -13,6 +13,13 @@ type OAuthConfig struct { ClientID string ClientSecret string RedirectURI string + Scope string + AuthorizeURL string + TokenURL string + UserInfoURL string + IDField string + NameField string + AvatarField string } type OAuthUserInfo struct { @@ -27,6 +34,14 @@ type OAuthSignUpOrInReq struct { Platform consts.UserPlatform `json:"platform" query:"platform" validate:"required"` // 第三方平台 dingtalk SessionID string `json:"session_id" query:"session_id"` // 会话ID RedirectURL string `json:"redirect_url" query:"redirect_url"` // 登录成功后跳转的 URL + InviteCode string `json:"inviate_code" query:"inviate_code"` // 邀请码 +} + +func (o OAuthSignUpOrInReq) OAuthKind() consts.OAuthKind { + if o.InviteCode == "" { + return consts.OAuthKindLogin + } + return consts.OAuthKindInvite } type OAuthCallbackReq struct { @@ -43,4 +58,20 @@ type OAuthState struct { SessionID string `json:"session_id"` // 会话ID Platform consts.UserPlatform `json:"platform" query:"platform" validate:"required"` // 第三方平台 dingtalk RedirectURL string `json:"redirect_url" query:"redirect_url"` // 登录成功后跳转的 URL + InviteCode string `json:"inviate_code"` // 邀请码 +} + +type OAuthAccessToken struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int64 `json:"expires_in"` + Scope string `json:"scope"` +} + +type GetAccessTokenReq struct { + GrantType string `json:"grant_type"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + Code string `json:"code"` + RedirectURL string `json:"redirect_uri"` } diff --git a/backend/domain/user.go b/backend/domain/user.go index 9854cb8..fcb7b66 100644 --- a/backend/domain/user.go +++ b/backend/domain/user.go @@ -7,6 +7,7 @@ import ( "github.com/chaitin/MonkeyCode/backend/consts" "github.com/chaitin/MonkeyCode/backend/db" + "github.com/chaitin/MonkeyCode/backend/ent/types" "github.com/chaitin/MonkeyCode/backend/pkg/cvt" ) @@ -48,8 +49,9 @@ type UserRepo interface { UserLoginHistory(ctx context.Context, page *web.Pagination) ([]*db.UserLoginHistory, *db.PageInfo, error) AdminLoginHistory(ctx context.Context, page *web.Pagination) ([]*db.AdminLoginHistory, *db.PageInfo, error) GetSetting(ctx context.Context) (*db.Setting, error) - UpdateSetting(ctx context.Context, fn func(*db.SettingUpdateOne)) (*db.Setting, error) - SignUpOrIn(ctx context.Context, platform consts.UserPlatform, req *OAuthUserInfo) (*db.User, error) + UpdateSetting(ctx context.Context, fn func(*db.Setting, *db.SettingUpdateOne)) (*db.Setting, error) + OAuthRegister(ctx context.Context, platform consts.UserPlatform, inviteCode string, req *OAuthUserInfo) (*db.User, error) + OAuthLogin(ctx context.Context, platform consts.UserPlatform, req *OAuthUserInfo) (*db.User, error) } type UpdateUserReq struct { @@ -202,6 +204,7 @@ type User struct { Email string `json:"email"` // 邮箱 TwoStepAuth bool `json:"two_step_auth"` // 是否开启两步验证 Status consts.UserStatus `json:"status"` // 用户状态 active: 正常 locked: 锁定 inactive: 禁用 + AvatarURL string `json:"avatar_url"` // 头像URL CreatedAt int64 `json:"created_at"` // 创建时间 LastActiveAt int64 `json:"last_active_at"` // 最后活跃时间 } @@ -215,6 +218,7 @@ func (u *User) From(e *db.User) *User { u.Username = e.Username u.Email = e.Email u.Status = e.Status + u.AvatarURL = e.AvatarURL u.CreatedAt = e.CreatedAt.Unix() return u @@ -249,21 +253,91 @@ type VSCodeSession struct { } type UpdateSettingReq struct { - EnableSSO *bool `json:"enable_sso"` // 是否开启SSO - ForceTwoFactorAuth *bool `json:"force_two_factor_auth"` // 是否强制两步验证 - DisablePasswordLogin *bool `json:"disable_password_login"` // 是否禁用密码登录 - EnableDingtalkOAuth *bool `json:"enable_dingtalk_oauth"` // 是否开启钉钉OAuth - DingtalkClientID *string `json:"dingtalk_client_id"` // 钉钉客户端ID - DingtalkClientSecret *string `json:"dingtalk_client_secret"` // 钉钉客户端密钥 + EnableSSO *bool `json:"enable_sso"` // 是否开启SSO + ForceTwoFactorAuth *bool `json:"force_two_factor_auth"` // 是否强制两步验证 + DisablePasswordLogin *bool `json:"disable_password_login"` // 是否禁用密码登录 + DingtalkOAuth *DingtalkOAuthReq `json:"dingtalk_oauth"` // 钉钉OAuth配置 + CustomOAuth *CustomOAuthReq `json:"custom_oauth"` // 自定义OAuth配置 +} + +type DingtalkOAuthReq struct { + Enable *bool `json:"enable"` // 钉钉OAuth开关 + ClientID *string `json:"client_id"` // 钉钉客户端ID + ClientSecret *string `json:"client_secret"` // 钉钉客户端密钥 +} + +type DingtalkOAuth struct { + Enable bool `json:"enable"` // 钉钉OAuth开关 + ClientID string `json:"client_id"` // 钉钉客户端ID + ClientSecret string `json:"client_secret"` // 钉钉客户端密钥 +} + +func (d *DingtalkOAuth) From(e *types.DingtalkOAuth) *DingtalkOAuth { + if e == nil { + d.Enable = false + return d + } + + d.Enable = e.Enable + d.ClientID = e.ClientID + return d +} + +type CustomOAuthReq struct { + Enable *bool `json:"enable"` // 自定义OAuth开关 + ClientID *string `json:"client_id"` // 自定义客户端ID + ClientSecret *string `json:"client_secret"` // 自定义客户端密钥 + AuthorizeURL *string `json:"authorize_url"` // 自定义OAuth授权URL + AccessTokenURL *string `json:"access_token_url"` // 自定义OAuth访问令牌URL + UserInfoURL *string `json:"userinfo_url"` // 自定义OAuth用户信息URL + Scopes []string `json:"scopes"` // 自定义OAuth Scope列表 + IDField *string `json:"id_field"` // 用户信息回包中的ID字段名 + NameField *string `json:"name_field"` // 用户信息回包中的用户名字段名 + AvatarField *string `json:"avatar_field"` // 用户信息回包中的头像URL字段名 + EmailField *string `json:"email_field"` // 用户信息回包中的邮箱字段名 +} + +type CustomOAuth struct { + Enable bool `json:"enable"` // 自定义OAuth开关 + ClientID string `json:"client_id"` // 自定义客户端ID + ClientSecret string `json:"client_secret"` // 自定义客户端密钥 + AuthorizeURL string `json:"authorize_url"` // 自定义OAuth授权URL + AccessTokenURL string `json:"access_token_url"` // 自定义OAuth访问令牌URL + UserInfoURL string `json:"userinfo_url"` // 自定义OAuth用户信息URL + Scopes []string `json:"scopes"` // 自定义OAuth Scope列表 + IDField string `json:"id_field"` // 用户信息回包中的ID字段名 + NameField string `json:"name_field"` // 用户信息回包中的用户名字段名 + AvatarField string `json:"avatar_field"` // 用户信息回包中的头像URL字段名 + EmailField string `json:"email_field"` // 用户信息回包中的邮箱字段名 +} + +func (c *CustomOAuth) From(e *types.CustomOAuth) *CustomOAuth { + if e == nil { + c.Enable = false + return c + } + + c.Enable = e.Enable + c.ClientID = e.ClientID + c.AuthorizeURL = e.AuthorizeURL + c.AccessTokenURL = e.AccessTokenURL + c.UserInfoURL = e.UserInfoURL + c.Scopes = e.Scopes + c.IDField = e.IDField + c.NameField = e.NameField + c.AvatarField = e.AvatarField + c.EmailField = e.EmailField + return c } type Setting struct { - EnableSSO bool `json:"enable_sso"` // 是否开启SSO - ForceTwoFactorAuth bool `json:"force_two_factor_auth"` // 是否强制两步验证 - DisablePasswordLogin bool `json:"disable_password_login"` // 是否禁用密码登录 - EnableDingtalkOAuth bool `json:"enable_dingtalk_oauth"` // 是否开启钉钉OAuth - CreatedAt int64 `json:"created_at"` // 创建时间 - UpdatedAt int64 `json:"updated_at"` // 更新时间 + EnableSSO bool `json:"enable_sso"` // 是否开启SSO + ForceTwoFactorAuth bool `json:"force_two_factor_auth"` // 是否强制两步验证 + DisablePasswordLogin bool `json:"disable_password_login"` // 是否禁用密码登录 + DingtalkOAuth DingtalkOAuth `json:"dingtalk_oauth"` // 钉钉OAuth接入 + CustomOAuth CustomOAuth `json:"custom_oauth"` // 自定义OAuth接入 + CreatedAt int64 `json:"created_at"` // 创建时间 + UpdatedAt int64 `json:"updated_at"` // 更新时间 } func (s *Setting) From(e *db.Setting) *Setting { @@ -274,7 +348,8 @@ func (s *Setting) From(e *db.Setting) *Setting { s.EnableSSO = e.EnableSSO s.ForceTwoFactorAuth = e.ForceTwoFactorAuth s.DisablePasswordLogin = e.DisablePasswordLogin - s.EnableDingtalkOAuth = e.EnableDingtalkOauth + s.DingtalkOAuth = *cvt.From(e.DingtalkOauth, &DingtalkOAuth{}) + s.CustomOAuth = *cvt.From(e.CustomOauth, &CustomOAuth{}) s.CreatedAt = e.CreatedAt.Unix() s.UpdatedAt = e.UpdatedAt.Unix() diff --git a/backend/ent/schema/setting.go b/backend/ent/schema/setting.go index d8e85ee..d631634 100644 --- a/backend/ent/schema/setting.go +++ b/backend/ent/schema/setting.go @@ -9,6 +9,8 @@ import ( "entgo.io/ent/schema/field" "github.com/google/uuid" + + "github.com/chaitin/MonkeyCode/backend/ent/types" ) // Setting holds the schema definition for the Setting entity. @@ -31,9 +33,8 @@ func (Setting) Fields() []ent.Field { field.Bool("enable_sso").Default(false), field.Bool("force_two_factor_auth").Default(false), field.Bool("disable_password_login").Default(false), - field.Bool("enable_dingtalk_oauth").Default(false), - field.String("dingtalk_client_id").Optional(), - field.String("dingtalk_client_secret").Optional(), + field.JSON("dingtalk_oauth", &types.DingtalkOAuth{}).Optional(), + field.JSON("custom_oauth", &types.CustomOAuth{}).Optional(), field.Time("created_at").Default(time.Now), field.Time("updated_at").Default(time.Now).UpdateDefault(time.Now), } diff --git a/backend/ent/types/types.go b/backend/ent/types/types.go new file mode 100644 index 0000000..06f6cd5 --- /dev/null +++ b/backend/ent/types/types.go @@ -0,0 +1,21 @@ +package types + +type DingtalkOAuth struct { + Enable bool `json:"enable"` // 钉钉OAuth开关 + ClientID string `json:"client_id"` // 钉钉客户端ID + ClientSecret string `json:"client_secret"` // 钉钉客户端密钥 +} + +type CustomOAuth struct { + Enable bool `json:"enable"` // 自定义OAuth开关 + ClientID string `json:"client_id"` // 自定义客户端ID + ClientSecret string `json:"client_secret"` // 自定义客户端密钥 + AuthorizeURL string `json:"authorize_url"` // 自定义OAuth授权URL + AccessTokenURL string `json:"access_token_url"` // 自定义OAuth访问令牌URL + UserInfoURL string `json:"userinfo_url"` // 自定义OAuth用户信息URL + Scopes []string `json:"scopes"` // 自定义OAuth Scope列表 + IDField string `json:"id_field"` // 用户信息回包中的ID字段名 + NameField string `json:"name_field"` // 用户信息回包中的用户名字段名` + AvatarField string `json:"avatar_field"` // 用户信息回包中的头像URL字段名` + EmailField string `json:"email_field"` // 用户信息回包中的邮箱字段名 +} diff --git a/backend/errcode/errcode.go b/backend/errcode/errcode.go index a825b9c..23f0c3d 100644 --- a/backend/errcode/errcode.go +++ b/backend/errcode/errcode.go @@ -10,10 +10,14 @@ import ( var LocalFS embed.FS var ( - ErrPermission = web.NewBadRequestErr("err-permission") - ErrUserNotFound = web.NewBadRequestErr("err-user-not-found") - ErrPassword = web.NewBadRequestErr("err-password") - ErrInviteCodeInvalid = web.NewBadRequestErr("err-invite-code-invalid") - ErrEmailInvalid = web.NewBadRequestErr("err-email-invalid") - ErrOAuthStateInvalid = web.NewBadRequestErr("err-oauth-state-invalid") + ErrPermission = web.NewBadRequestErr("err-permission") + ErrUserNotFound = web.NewBadRequestErr("err-user-not-found") + ErrPassword = web.NewBadRequestErr("err-password") + ErrInviteCodeInvalid = web.NewBadRequestErr("err-invite-code-invalid") + ErrEmailInvalid = web.NewBadRequestErr("err-email-invalid") + ErrOAuthStateInvalid = web.NewBadRequestErr("err-oauth-state-invalid") + ErrUnsupportedPlatform = web.NewBadRequestErr("err-unsupported-platform") + ErrNotInvited = web.NewBadRequestErr("err-not-invited") + ErrDingtalkNotEnabled = web.NewBadRequestErr("err-dingtalk-not-enabled") + ErrCustomNotEnabled = web.NewBadRequestErr("err-custom-not-enabled") ) diff --git a/backend/errcode/locale.zh.toml b/backend/errcode/locale.zh.toml index 5423c71..6000dd0 100644 --- a/backend/errcode/locale.zh.toml +++ b/backend/errcode/locale.zh.toml @@ -14,4 +14,16 @@ other = "邀请码无效" other = "邮箱格式错误" [err-oauth-state-invalid] -other = "OAuth 状态无效" \ No newline at end of file +other = "OAuth 状态无效" + +[err-unsupported-platform] +other = "不支持的平台" + +[err-not-invited] +other = "未被邀请" + +[err-dingtalk-not-enabled] +other = "钉钉未启用" + +[err-custom-not-enabled] +other = "OAuth未启用" \ No newline at end of file diff --git a/backend/internal/user/handler/v1/user.go b/backend/internal/user/handler/v1/user.go index 5f05b44..e532b0c 100644 --- a/backend/internal/user/handler/v1/user.go +++ b/backend/internal/user/handler/v1/user.go @@ -58,6 +58,7 @@ func NewUserHandler( admin.GET("/login-history", web.BaseHandler(u.AdminLoginHistory, web.WithPage())) admin.DELETE("/delete", web.BaseHandler(u.DeleteAdmin)) + // user g := w.Group("/api/v1/user") g.GET("/oauth/signup-or-in", web.BindHandler(u.OAuthSignUpOrIn)) g.GET("/oauth/callback", web.BindHandler(u.OAuthCallback)) @@ -165,7 +166,7 @@ func (h *UserHandler) Delete(c *web.Context) error { // DeleteAdmin 删除管理员 // -// @Tags User +// @Tags Admin // @Summary 删除管理员 // @Description 删除管理员 // @ID delete-admin @@ -184,7 +185,7 @@ func (h *UserHandler) DeleteAdmin(c *web.Context) error { // AdminLogin 管理员登录 // -// @Tags User +// @Tags Admin // @Summary 管理员登录 // @Description 管理员登录 // @ID admin-login @@ -285,7 +286,7 @@ func (h *UserHandler) Register(c *web.Context, req domain.RegisterReq) error { // CreateAdmin 创建管理员 // -// @Tags User +// @Tags Admin // @Summary 创建管理员 // @Description 创建管理员 // @ID create-admin @@ -308,7 +309,7 @@ func (h *UserHandler) CreateAdmin(c *web.Context, req domain.CreateAdminReq) err // AdminList 获取管理员用户列表 // -// @Tags User +// @Tags Admin // @Summary 获取管理员用户列表 // @Description 获取管理员用户列表 // @ID list-admin-user @@ -327,7 +328,7 @@ func (h *UserHandler) AdminList(c *web.Context) error { // AdminLoginHistory 获取管理员登录历史 // -// @Tags User +// @Tags Admin // @Summary 获取管理员登录历史 // @Description 获取管理员登录历史 // @ID admin-login-history @@ -346,7 +347,7 @@ func (h *UserHandler) AdminLoginHistory(c *web.Context) error { // GetSetting 获取系统设置 // -// @Tags User +// @Tags Admin // @Summary 获取系统设置 // @Description 获取系统设置 // @ID get-setting @@ -364,7 +365,7 @@ func (h *UserHandler) GetSetting(c *web.Context) error { // UpdateSetting 更新系统设置 // -// @Tags User +// @Tags Admin // @Summary 更新系统设置 // @Description 更新系统设置 // @ID update-setting diff --git a/backend/internal/user/repo/user.go b/backend/internal/user/repo/user.go index 2706e98..b8a85d9 100644 --- a/backend/internal/user/repo/user.go +++ b/backend/internal/user/repo/user.go @@ -3,6 +3,7 @@ package repo import ( "context" "errors" + "fmt" "time" "github.com/google/uuid" @@ -17,6 +18,7 @@ import ( "github.com/chaitin/MonkeyCode/backend/db/user" "github.com/chaitin/MonkeyCode/backend/db/useridentity" "github.com/chaitin/MonkeyCode/backend/domain" + "github.com/chaitin/MonkeyCode/backend/errcode" "github.com/chaitin/MonkeyCode/backend/pkg/entx" ) @@ -68,32 +70,40 @@ func (r *UserRepo) GetByName(ctx context.Context, username string) (*db.User, er func (r *UserRepo) ValidateInviteCode(ctx context.Context, code string) (*db.InviteCode, error) { var res *db.InviteCode err := entx.WithTx(ctx, r.db, func(tx *db.Tx) error { - ic, err := tx.InviteCode.Query().Where(invitecode.Code(code)).Only(ctx) + ic, err := r.innerValidateInviteCode(ctx, tx, code) if err != nil { return err } - - if ic.ExpiredAt.Before(time.Now()) { - return errors.New("invite code has expired") - } - if ic.Status == consts.InviteCodeStatusUsed { - return errors.New("invite code has been used") - } - - ic, err = tx.InviteCode.UpdateOneID(ic.ID). - SetStatus(consts.InviteCodeStatusUsed). - Save(ctx) - - if err != nil { - return err - } - res = ic return nil }) return res, err } +func (r *UserRepo) innerValidateInviteCode(ctx context.Context, tx *db.Tx, code string) (*db.InviteCode, error) { + ic, err := tx.InviteCode.Query().Where(invitecode.Code(code)).Only(ctx) + if err != nil { + return nil, err + } + + if ic.ExpiredAt.Before(time.Now()) { + return nil, errors.New("invite code has expired") + } + if ic.Status == consts.InviteCodeStatusUsed { + return nil, errors.New("invite code has been used") + } + + ic, err = tx.InviteCode.UpdateOneID(ic.ID). + SetStatus(consts.InviteCodeStatusUsed). + Save(ctx) + + if err != nil { + return nil, err + } + + return ic, nil +} + func (r *UserRepo) CreateUser(ctx context.Context, user *db.User) (*db.User, error) { return r.db.User.Create(). SetUsername(user.Username). @@ -184,18 +194,23 @@ func (r *UserRepo) GetSetting(ctx context.Context) (*db.Setting, error) { return s, nil } -func (r *UserRepo) UpdateSetting(ctx context.Context, fn func(*db.SettingUpdateOne)) (*db.Setting, error) { - var s *db.Setting +func (r *UserRepo) UpdateSetting(ctx context.Context, fn func(*db.Setting, *db.SettingUpdateOne)) (*db.Setting, error) { + var res *db.Setting err := entx.WithTx(ctx, r.db, func(tx *db.Tx) error { s, err := tx.Setting.Query().First(ctx) if err != nil { return err } up := tx.Setting.UpdateOneID(s.ID) - fn(up) - return up.Exec(ctx) + fn(s, up) + s, err = up.Save(ctx) + if err != nil { + return err + } + res = s + return nil }) - return s, err + return res, err } func (r *UserRepo) Update(ctx context.Context, id string, fn func(*db.UserUpdateOne) error) (*db.User, error) { @@ -241,16 +256,19 @@ func (r *UserRepo) DeleteAdmin(ctx context.Context, id string) error { return r.db.Admin.DeleteOne(admin).Exec(ctx) } -func (r *UserRepo) SignUpOrIn(ctx context.Context, platform consts.UserPlatform, req *domain.OAuthUserInfo) (*db.User, error) { +func (r *UserRepo) OAuthRegister(ctx context.Context, platform consts.UserPlatform, inviteCode string, req *domain.OAuthUserInfo) (*db.User, error) { var u *db.User err := entx.WithTx(ctx, r.db, func(tx *db.Tx) error { - ui, err := tx.UserIdentity.Query(). + if _, err := r.innerValidateInviteCode(ctx, tx, inviteCode); err != nil { + return errcode.ErrInviteCodeInvalid.Wrap(err) + } + + _, err := tx.UserIdentity.Query(). WithUser(). Where(useridentity.Platform(platform), useridentity.IdentityID(req.ID)). First(ctx) if err == nil { - u = ui.Edges.User - return nil + return fmt.Errorf("user already exists for platform %s and identity ID %s", platform, req.ID) } if !db.IsNotFound(err) { return err @@ -282,3 +300,15 @@ func (r *UserRepo) SignUpOrIn(ctx context.Context, platform consts.UserPlatform, }) return u, err } + +func (r *UserRepo) OAuthLogin(ctx context.Context, platform consts.UserPlatform, req *domain.OAuthUserInfo) (*db.User, error) { + ui, err := r.db.UserIdentity.Query(). + WithUser(). + Where(useridentity.Platform(platform), useridentity.IdentityID(req.ID)). + Where(useridentity.HasUser()). + Only(ctx) + if err != nil { + return nil, errcode.ErrNotInvited.Wrap(err) + } + return ui.Edges.User, nil +} diff --git a/backend/internal/user/usecase/user.go b/backend/internal/user/usecase/user.go index 3d2f957..a39c8f8 100644 --- a/backend/internal/user/usecase/user.go +++ b/backend/internal/user/usecase/user.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "net/url" + "strings" "time" "github.com/google/uuid" @@ -14,6 +15,7 @@ import ( "github.com/GoYoko/web" "github.com/chaitin/MonkeyCode/backend/consts" + "github.com/chaitin/MonkeyCode/backend/ent/types" "github.com/chaitin/MonkeyCode/backend/pkg/cvt" "github.com/chaitin/MonkeyCode/backend/pkg/oauth" @@ -268,24 +270,65 @@ func (u *UserUsecase) GetSetting(ctx context.Context) (*domain.Setting, error) { } func (u *UserUsecase) UpdateSetting(ctx context.Context, req *domain.UpdateSettingReq) (*domain.Setting, error) { - s, err := u.repo.UpdateSetting(ctx, func(s *db.SettingUpdateOne) { + s, err := u.repo.UpdateSetting(ctx, func(old *db.Setting, up *db.SettingUpdateOne) { if req.EnableSSO != nil { - s.SetEnableSSO(*req.EnableSSO) + up.SetEnableSSO(*req.EnableSSO) } if req.ForceTwoFactorAuth != nil { - s.SetForceTwoFactorAuth(*req.ForceTwoFactorAuth) + up.SetForceTwoFactorAuth(*req.ForceTwoFactorAuth) } if req.DisablePasswordLogin != nil { - s.SetDisablePasswordLogin(*req.DisablePasswordLogin) + up.SetDisablePasswordLogin(*req.DisablePasswordLogin) } - if req.EnableDingtalkOAuth != nil { - s.SetEnableDingtalkOauth(*req.EnableDingtalkOAuth) - } - if req.DingtalkClientID != nil { - s.SetDingtalkClientID(*req.DingtalkClientID) + if req.DingtalkOAuth != nil { + dingtalk := cvt.NilWithDefault(old.DingtalkOauth, &types.DingtalkOAuth{}) + if req.DingtalkOAuth.Enable != nil { + dingtalk.Enable = *req.DingtalkOAuth.Enable + } + if req.DingtalkOAuth.ClientID != nil { + dingtalk.ClientID = *req.DingtalkOAuth.ClientID + } + if req.DingtalkOAuth.ClientSecret != nil { + dingtalk.ClientSecret = *req.DingtalkOAuth.ClientSecret + } + up.SetDingtalkOauth(dingtalk) } - if req.DingtalkClientSecret != nil { - s.SetDingtalkClientSecret(*req.DingtalkClientSecret) + if req.CustomOAuth != nil { + custom := cvt.NilWithDefault(old.CustomOauth, &types.CustomOAuth{}) + if req.CustomOAuth.Enable != nil { + custom.Enable = *req.CustomOAuth.Enable + } + if req.CustomOAuth.ClientID != nil { + custom.ClientID = *req.CustomOAuth.ClientID + } + if req.CustomOAuth.ClientSecret != nil { + custom.ClientSecret = *req.CustomOAuth.ClientSecret + } + if req.CustomOAuth.AuthorizeURL != nil { + custom.AuthorizeURL = *req.CustomOAuth.AuthorizeURL + } + if req.CustomOAuth.AccessTokenURL != nil { + custom.AccessTokenURL = *req.CustomOAuth.AccessTokenURL + } + if req.CustomOAuth.UserInfoURL != nil { + custom.UserInfoURL = *req.CustomOAuth.UserInfoURL + } + if req.CustomOAuth.Scopes != nil { + custom.Scopes = req.CustomOAuth.Scopes + } + if req.CustomOAuth.IDField != nil { + custom.IDField = *req.CustomOAuth.IDField + } + if req.CustomOAuth.NameField != nil { + custom.NameField = *req.CustomOAuth.NameField + } + if req.CustomOAuth.AvatarField != nil { + custom.AvatarField = *req.CustomOAuth.AvatarField + } + if req.CustomOAuth.EmailField != nil { + custom.EmailField = *req.CustomOAuth.EmailField + } + up.SetCustomOauth(custom) } }) if err != nil { @@ -335,8 +378,26 @@ func (u *UserUsecase) OAuthSignUpOrIn(ctx context.Context, req *domain.OAuthSign switch req.Platform { case consts.UserPlatformDingTalk: - cfg.ClientID = setting.DingtalkClientID - cfg.ClientSecret = setting.DingtalkClientSecret + if setting.DingtalkOauth == nil || !setting.DingtalkOauth.Enable { + return nil, errcode.ErrDingtalkNotEnabled + } + cfg.ClientID = setting.DingtalkOauth.ClientID + cfg.ClientSecret = setting.DingtalkOauth.ClientSecret + case consts.UserPlatformCustom: + if setting.CustomOauth == nil || !setting.CustomOauth.Enable { + return nil, errcode.ErrCustomNotEnabled + } + cfg.ClientID = setting.CustomOauth.ClientID + cfg.ClientSecret = setting.CustomOauth.ClientSecret + cfg.AuthorizeURL = setting.CustomOauth.AuthorizeURL + cfg.Scope = strings.Join(setting.CustomOauth.Scopes, " ") + cfg.TokenURL = setting.CustomOauth.AccessTokenURL + cfg.UserInfoURL = setting.CustomOauth.UserInfoURL + cfg.IDField = setting.CustomOauth.IDField + cfg.NameField = setting.CustomOauth.NameField + cfg.AvatarField = setting.CustomOauth.AvatarField + default: + return nil, errcode.ErrUnsupportedPlatform } oauth, err := oauth.NewOAuther(cfg) @@ -347,9 +408,10 @@ func (u *UserUsecase) OAuthSignUpOrIn(ctx context.Context, req *domain.OAuthSign session := &domain.OAuthState{ SessionID: req.SessionID, - Kind: consts.OAuthKindSignUpOrIn, + Kind: req.OAuthKind(), Platform: req.Platform, RedirectURL: req.RedirectURL, + InviteCode: req.InviteCode, } b, err := json.Marshal(session) if err != nil { @@ -375,38 +437,74 @@ func (u *UserUsecase) OAuthCallback(ctx context.Context, req *domain.OAuthCallba } switch session.Kind { - case consts.OAuthKindSignUpOrIn: - return u.OAuthSignUpOrInCallback(ctx, req, &session) + case consts.OAuthKindInvite: + return u.WithOAuthCallback(ctx, req, &session, func(ctx context.Context, s *domain.OAuthState, oui *domain.OAuthUserInfo) (*db.User, error) { + return u.repo.OAuthRegister(ctx, s.Platform, s.InviteCode, oui) + }) + + case consts.OAuthKindLogin: + return u.WithOAuthCallback(ctx, req, &session, func(ctx context.Context, s *domain.OAuthState, oui *domain.OAuthUserInfo) (*db.User, error) { + return u.repo.OAuthLogin(ctx, s.Platform, oui) + }) default: return "", errcode.ErrOAuthStateInvalid } } -func (u *UserUsecase) OAuthSignUpOrInCallback(ctx context.Context, req *domain.OAuthCallbackReq, session *domain.OAuthState) (string, error) { +func (u *UserUsecase) FetchUserInfo(ctx context.Context, req *domain.OAuthCallbackReq, session *domain.OAuthState) (*domain.OAuthUserInfo, error) { setting, err := u.repo.GetSetting(ctx) if err != nil { - return "", err + return nil, err } cfg := domain.OAuthConfig{ Debug: u.cfg.Debug, Platform: session.Platform, } + switch session.Platform { case consts.UserPlatformDingTalk: - cfg.ClientID = setting.DingtalkClientID - cfg.ClientSecret = setting.DingtalkClientSecret + if setting.DingtalkOauth == nil || !setting.DingtalkOauth.Enable { + return nil, errcode.ErrDingtalkNotEnabled + } + cfg.ClientID = setting.DingtalkOauth.ClientID + cfg.ClientSecret = setting.DingtalkOauth.ClientSecret + case consts.UserPlatformCustom: + if setting.CustomOauth == nil || !setting.CustomOauth.Enable { + return nil, errcode.ErrCustomNotEnabled + } + cfg.ClientID = setting.CustomOauth.ClientID + cfg.ClientSecret = setting.CustomOauth.ClientSecret + cfg.AuthorizeURL = setting.CustomOauth.AuthorizeURL + cfg.Scope = strings.Join(setting.CustomOauth.Scopes, " ") + cfg.TokenURL = setting.CustomOauth.AccessTokenURL + cfg.UserInfoURL = setting.CustomOauth.UserInfoURL + cfg.IDField = setting.CustomOauth.IDField + cfg.NameField = setting.CustomOauth.NameField + cfg.AvatarField = setting.CustomOauth.AvatarField + default: + return nil, errcode.ErrUnsupportedPlatform } oauth, err := oauth.NewOAuther(cfg) if err != nil { - return "", err + return nil, err } userInfo, err := oauth.GetUserInfo(req.Code) + if err != nil { + return nil, err + } + return userInfo, nil +} + +type OAuthUserRepoHandle func(context.Context, *domain.OAuthState, *domain.OAuthUserInfo) (*db.User, error) + +func (u *UserUsecase) WithOAuthCallback(ctx context.Context, req *domain.OAuthCallbackReq, session *domain.OAuthState, handle OAuthUserRepoHandle) (string, error) { + info, err := u.FetchUserInfo(ctx, req, session) if err != nil { return "", err } - user, err := u.repo.SignUpOrIn(ctx, session.Platform, userInfo) + user, err := handle(ctx, session, info) if err != nil { return "", err } diff --git a/backend/migration/000006_alter_settings_table.down.sql b/backend/migration/000006_alter_settings_table.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/backend/migration/000006_alter_settings_table.up.sql b/backend/migration/000006_alter_settings_table.up.sql new file mode 100644 index 0000000..09c2289 --- /dev/null +++ b/backend/migration/000006_alter_settings_table.up.sql @@ -0,0 +1,6 @@ +ALTER TABLE settings DROP COLUMN IF EXISTS enable_dingtalk_oauth; +ALTER TABLE settings DROP COLUMN IF EXISTS dingtalk_client_id; +ALTER TABLE settings DROP COLUMN IF EXISTS dingtalk_client_secret; + +ALTER TABLE settings ADD COLUMN dingtalk_oauth JSONB DEFAULT '{}'::JSONB; +ALTER TABLE settings ADD COLUMN custom_oauth JSONB DEFAULT '{}'::JSONB; \ No newline at end of file diff --git a/backend/pkg/oauth/custom.go b/backend/pkg/oauth/custom.go new file mode 100644 index 0000000..229e404 --- /dev/null +++ b/backend/pkg/oauth/custom.go @@ -0,0 +1,96 @@ +package oauth + +import ( + "fmt" + "net/url" + "strings" + "time" + + "github.com/google/uuid" + + "github.com/chaitin/MonkeyCode/backend/domain" + "github.com/chaitin/MonkeyCode/backend/pkg/request" +) + +type CustomOAuth struct { + cfg domain.OAuthConfig +} + +func NewCustomOAuth(config domain.OAuthConfig) domain.OAuther { + c := &CustomOAuth{ + cfg: config, + } + + return c +} + +// GetAuthorizeURL implements domain.OAuther. +func (c *CustomOAuth) GetAuthorizeURL() (string, string) { + state := uuid.NewString() + url := fmt.Sprintf("%s?response_type=code&client_id=%s&state=%s&redirect_uri=%s", c.cfg.AuthorizeURL, c.cfg.ClientID, state, c.cfg.RedirectURI) + return state, url +} + +// GetUserInfo implements domain.OAuther. +func (c *CustomOAuth) GetUserInfo(code string) (*domain.OAuthUserInfo, error) { + accessToken, err := c.getAccessToken(code) + if err != nil { + return nil, err + } + info, err := c.getUserInfo(accessToken) + if err != nil { + return nil, err + } + return &domain.OAuthUserInfo{ + ID: fmt.Sprint(info[c.cfg.IDField]), + AvatarURL: fmt.Sprint(info[c.cfg.AvatarField]), + Name: fmt.Sprint(info[c.cfg.NameField]), + }, nil +} + +func (c *CustomOAuth) getAccessToken(code string) (string, error) { + u, err := url.Parse(c.cfg.TokenURL) + if err != nil { + return "", fmt.Errorf("[CustomOAuth] 无效的Token URL: %w", err) + } + client := request.NewClient(u.Scheme, u.Host, 30*time.Second) + client.SetDebug(c.cfg.Debug) + req := domain.GetAccessTokenReq{ + GrantType: "authorization_code", + Code: code, + RedirectURL: c.cfg.RedirectURI, + ClientID: c.cfg.ClientID, + ClientSecret: c.cfg.ClientSecret, + } + resp, err := request.Post[domain.OAuthAccessToken](client, u.Path, req, request.WithHeader(request.Header{ + "Accept": "application/json", + })) + if err != nil { + return "", fmt.Errorf("[CustomOAuth] 获取access token失败: %w", err) + } + return resp.AccessToken, nil +} + +type UserInfo map[string]any + +func (c *CustomOAuth) getUserInfo(accessToken string) (UserInfo, error) { + u, err := url.Parse(c.cfg.UserInfoURL) + if err != nil { + return nil, fmt.Errorf("[CustomOAuth] 无效的UseInfo URL: %w", err) + } + client := request.NewClient(u.Scheme, u.Host, 30*time.Second) + client.SetDebug(c.cfg.Debug) + h := request.Header{ + "Authorization": fmt.Sprintf("Bearer %s", accessToken), + } + if strings.Contains(c.cfg.UserInfoURL, "github") { + h["Accept"] = "application/vnd.github.v3+json" + } + + resp, err := request.Get[UserInfo](client, u.Path, request.WithHeader(h)) + if err != nil { + return nil, fmt.Errorf("[CustomOAuth] 获取用户信息失败: %w", err) + } + + return *resp, nil +} diff --git a/backend/pkg/oauth/oauth.go b/backend/pkg/oauth/oauth.go index 16c83b4..6977849 100644 --- a/backend/pkg/oauth/oauth.go +++ b/backend/pkg/oauth/oauth.go @@ -11,6 +11,8 @@ func NewOAuther(config domain.OAuthConfig) (domain.OAuther, error) { switch config.Platform { case consts.UserPlatformDingTalk: return NewDingTalk(config), nil + case consts.UserPlatformCustom: + return NewCustomOAuth(config), nil default: return nil, fmt.Errorf("unsupported platform: %s", config.Platform) } From 0776e5eb54f3718d25d6413a5dd96db3977f41d1 Mon Sep 17 00:00:00 2001 From: Gavan <994259213@qq.com> Date: Thu, 10 Jul 2025 18:01:58 +0800 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20oauth,=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=AC=AC=E4=B8=89=E6=96=B9=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/src/api/types.ts | 119 ++++-- ui/src/components/form/index.tsx | 19 +- ui/src/components/header/Bread.tsx | 164 +++++---- ui/src/components/markDown/code.tsx | 2 +- ui/src/components/markDown/diff.tsx | 2 +- ui/src/components/sidebar/index.tsx | 40 ++- ui/src/pages/auth/index.tsx | 35 +- ui/src/pages/chat/index.tsx | 3 +- .../completion/completionDetailModal.tsx | 2 +- .../pages/dashboard/components/memberInfo.tsx | 22 +- .../dashboard/components/memberStatistic.tsx | 1 + ui/src/pages/invite/index.tsx | 58 ++- .../pages/{user => user-management}/index.tsx | 25 +- .../inviteUserModal.tsx | 0 .../loginHistory.tsx | 0 .../memberManage.tsx | 0 .../thirdPartyLoginSettingModal.tsx | 309 ++++++++++++++++ ui/src/pages/user/chat/chatDetailModal.tsx | 149 ++++++++ ui/src/pages/user/chat/index.tsx | 170 +++++++++ .../user/completion/completionDetailModal.tsx | 176 +++++++++ ui/src/pages/user/completion/constant.ts | 67 ++++ ui/src/pages/user/completion/index.tsx | 237 ++++++++++++ .../dashboard/components/memberStatistic.tsx | 212 +++++++++++ ui/src/pages/user/dashboard/index.tsx | 77 ++++ ui/src/pages/user/login/index.tsx | 340 ++++++++++++++++++ .../user/thirdPartyLoginSettingModal.tsx | 215 ----------- ui/src/router.tsx | 41 ++- 27 files changed, 2111 insertions(+), 374 deletions(-) rename ui/src/pages/{user => user-management}/index.tsx (82%) rename ui/src/pages/{user => user-management}/inviteUserModal.tsx (100%) rename ui/src/pages/{user => user-management}/loginHistory.tsx (100%) rename ui/src/pages/{user => user-management}/memberManage.tsx (100%) create mode 100644 ui/src/pages/user-management/thirdPartyLoginSettingModal.tsx create mode 100644 ui/src/pages/user/chat/chatDetailModal.tsx create mode 100644 ui/src/pages/user/chat/index.tsx create mode 100644 ui/src/pages/user/completion/completionDetailModal.tsx create mode 100644 ui/src/pages/user/completion/constant.ts create mode 100644 ui/src/pages/user/completion/index.tsx create mode 100644 ui/src/pages/user/dashboard/components/memberStatistic.tsx create mode 100644 ui/src/pages/user/dashboard/index.tsx create mode 100644 ui/src/pages/user/login/index.tsx delete mode 100644 ui/src/pages/user/thirdPartyLoginSettingModal.tsx diff --git a/ui/src/api/types.ts b/ui/src/api/types.ts index 25966d0..b31047e 100644 --- a/ui/src/api/types.ts +++ b/ui/src/api/types.ts @@ -1,3 +1,4 @@ +/* eslint-disable */ /* tslint:disable */ // @ts-nocheck /* @@ -10,37 +11,38 @@ */ export enum ConstsUserStatus { - UserStatusActive = 'active', - UserStatusInactive = 'inactive', - UserStatusLocked = 'locked', + UserStatusActive = "active", + UserStatusInactive = "inactive", + UserStatusLocked = "locked", } export enum ConstsUserPlatform { - UserPlatformEmail = 'email', - UserPlatformDingTalk = 'dingtalk', + UserPlatformEmail = "email", + UserPlatformDingTalk = "dingtalk", + UserPlatformCustom = "custom", } export enum ConstsModelType { - ModelTypeLLM = 'llm', - ModelTypeCoder = 'coder', - ModelTypeEmbedding = 'embedding', - ModelTypeAudio = 'audio', - ModelTypeReranker = 'reranker', + ModelTypeLLM = "llm", + ModelTypeCoder = "coder", + ModelTypeEmbedding = "embedding", + ModelTypeAudio = "audio", + ModelTypeReranker = "reranker", } export enum ConstsModelStatus { - ModelStatusActive = 'active', - ModelStatusInactive = 'inactive', + ModelStatusActive = "active", + ModelStatusInactive = "inactive", } export enum ConstsChatRole { - ChatRoleUser = 'user', - ChatRoleAssistant = 'assistant', + ChatRoleUser = "user", + ChatRoleAssistant = "assistant", } export enum ConstsAdminStatus { - AdminStatusActive = 'active', - AdminStatusInactive = 'inactive', + AdminStatusActive = "active", + AdminStatusInactive = "inactive", } export interface DomainAcceptCompletionReq { @@ -355,8 +357,22 @@ export interface DomainRegisterReq { export interface DomainSetting { /** 创建时间 */ created_at?: number; + /** 自定义OAuth访问令牌URL */ + custom_oauth_access_token_url?: string; + /** 自定义OAuth授权URL */ + custom_oauth_authorize_url?: string; + /** 自定义OAuth客户端ID */ + custom_oauth_client_id?: string; + /** 自定义OAuth Scope列表 */ + custom_oauth_scopes?: string[]; + /** 自定义OAuth用户信息URL */ + custom_oauth_userinfo_url?: string; + /** 钉钉客户端ID */ + dingtalk_client_id?: string; /** 是否禁用密码登录 */ disable_password_login?: boolean; + /** 是否开启自定义OAuth */ + enable_custom_oauth?: boolean; /** 是否开启钉钉OAuth */ enable_dingtalk_oauth?: boolean; /** 是否开启SSO */ @@ -445,12 +461,26 @@ export interface DomainUpdateModelReq { } export interface DomainUpdateSettingReq { + /** 自定义OAuth访问令牌URL */ + custom_oauth_access_token_url?: string; + /** 自定义OAuth授权URL */ + custom_oauth_authorize_url?: string; + /** 自定义OAuth客户端ID */ + custom_oauth_client_id?: string; + /** 自定义OAuth客户端密钥 */ + custom_oauth_client_secret?: string; + /** 自定义OAuth Scope列表 */ + custom_oauth_scopes?: string[]; + /** 自定义OAuth用户信息URL */ + custom_oauth_userinfo_url?: string; /** 钉钉客户端ID */ dingtalk_client_id?: string; /** 钉钉客户端密钥 */ dingtalk_client_secret?: string; /** 是否禁用密码登录 */ disable_password_login?: boolean; + /** 是否开启自定义OAuth */ + enable_custom_oauth?: boolean; /** 是否开启钉钉OAuth */ enable_dingtalk_oauth?: boolean; /** 是否开启SSO */ @@ -639,11 +669,15 @@ export interface GetCategoryStatDashboardParams { * 持续时间 (小时或天数)` * @min 24 * @max 90 + * @default 90 */ duration?: number; - /** 精度: "hour", "day" */ - precision: 'hour' | 'day'; - /** 用户ID,可jj */ + /** + * 精度: "hour", "day" + * @default "day" + */ + precision: "hour" | "day"; + /** 用户ID,可选参数 */ user_id?: string; } @@ -652,11 +686,15 @@ export interface GetTimeStatDashboardParams { * 持续时间 (小时或天数)` * @min 24 * @max 90 + * @default 90 */ duration?: number; - /** 精度: "hour", "day" */ - precision: 'hour' | 'day'; - /** 用户ID,可jj */ + /** + * 精度: "hour", "day" + * @default "day" + */ + precision: "hour" | "day"; + /** 用户ID,可选参数 */ user_id?: string; } @@ -665,11 +703,15 @@ export interface GetUserCodeRankDashboardParams { * 持续时间 (小时或天数)` * @min 24 * @max 90 + * @default 90 */ duration?: number; - /** 精度: "hour", "day" */ - precision: 'hour' | 'day'; - /** 用户ID,可jj */ + /** + * 精度: "hour", "day" + * @default "day" + */ + precision: "hour" | "day"; + /** 用户ID,可选参数 */ user_id?: string; } @@ -678,8 +720,15 @@ export interface GetUserEventsDashboardParams { * 持续时间 (小时或天数)` * @min 24 * @max 90 + * @default 90 + */ + duration?: number; + /** + * 精度: "hour", "day" + * @default "day" */ - /** 用户ID,可jj */ + precision: "hour" | "day"; + /** 用户ID,可选参数 */ user_id?: string; } @@ -693,22 +742,26 @@ export interface GetUserStatDashboardParams { * 持续时间 (小时或天数)` * @min 24 * @max 90 + * @default 90 */ duration?: number; - /** 精度: "hour", "day" */ - precision: 'hour' | 'day'; - /** 用户ID,可jj */ + /** + * 精度: "hour", "day" + * @default "day" + */ + precision: "hour" | "day"; + /** 用户ID,可选参数 */ user_id?: string; } export interface GetMyModelListParams { /** 模型类型 llm:对话模型 coder:代码模型 */ - model_type?: 'llm' | 'coder' | 'embedding' | 'audio' | 'reranker'; + model_type?: "llm" | "coder" | "embedding" | "audio" | "reranker"; } export interface GetGetTokenUsageParams { /** 模型类型 llm:对话模型 coder:代码模型 */ - model_type: 'llm' | 'coder' | 'embedding' | 'audio' | 'reranker'; + model_type: "llm" | "coder" | "embedding" | "audio" | "reranker"; } export interface DeleteDeleteUserParams { @@ -740,8 +793,10 @@ export interface GetUserOauthCallbackParams { } export interface GetUserOauthSignupOrInParams { + /** 邀请码 */ + inviate_code?: string; /** 第三方平台 dingtalk */ - platform: 'email' | 'dingtalk'; + platform: "email" | "dingtalk" | "custom"; /** 登录成功后跳转的 URL */ redirect_url?: string; /** 会话ID */ diff --git a/ui/src/components/form/index.tsx b/ui/src/components/form/index.tsx index d5d37b4..646717b 100644 --- a/ui/src/components/form/index.tsx +++ b/ui/src/components/form/index.tsx @@ -1,4 +1,4 @@ -import { styled, FormLabel } from '@mui/material'; +import { styled, FormLabel, Box } from '@mui/material'; export const StyledFormLabel = styled(FormLabel)(({ theme }) => ({ display: 'block', @@ -10,3 +10,20 @@ export const StyledFormLabel = styled(FormLabel)(({ theme }) => ({ fontSize: 14, }, })); + +export const FormItem = ({ + label, + children, + required, +}: { + label: string; + children: React.ReactNode; + required?: boolean; +}) => { + return ( + + {label} + {children} + + ); +}; diff --git a/ui/src/components/header/Bread.tsx b/ui/src/components/header/Bread.tsx index 82f14a0..fbf2691 100644 --- a/ui/src/components/header/Bread.tsx +++ b/ui/src/components/header/Bread.tsx @@ -1,111 +1,121 @@ import KeyboardArrowRightRoundedIcon from '@mui/icons-material/KeyboardArrowRightRounded'; -import { Box, Stack, useTheme } from '@mui/material'; -import React, { useEffect, useState } from 'react'; +import { Box, Stack, Typography } from '@mui/material'; +import { useMemo } from 'react'; import { NavLink, useLocation } from 'react-router-dom'; -const HomeBread = { title: '工作台', to: '/' }; -const OtherBread = { +const ADMIN_BREADCRUMB_MAP: Record = { dashboard: { title: '仪表盘', to: '/' }, chat: { title: '对话记录', to: '/chat' }, completion: { title: '补全记录', to: '/completion' }, model: { title: '模型管理', to: '/model' }, - user: { title: '成员管理', to: '/user' }, + 'user-management': { title: '成员管理', to: '/user-management' }, admin: { title: '管理员', to: '/admin' }, }; +const USER_BREADCRUMB_MAP: Record = { + dashboard: { title: '仪表盘', to: '/user/dashboard' }, + chat: { title: '对话记录', to: '/user/chat' }, + completion: { title: '补全记录', to: '/user/completion' }, +}; + const Bread = () => { - const theme = useTheme(); const { pathname } = useLocation(); - const [breads, setBreads] = useState< - { title: React.ReactNode; to: string }[] - >([]); - useEffect(() => { - const curBreads: { title: React.ReactNode; to: string }[] = [ + const breadcrumbs = useMemo(() => { + const pathParts = pathname.split('/').filter(Boolean); + + const generatedCrumbs = pathParts + .map((part) => { + if (pathname.startsWith('/user/')) { + return USER_BREADCRUMB_MAP[part]; + } + return ADMIN_BREADCRUMB_MAP[part]; + }) + .filter(Boolean); + + return [ { - title: ( - - MonkeyCode - - ), - to: '/dashboard', + title: 'MonkeyCode', + to: pathname.startsWith('/user/') ? '/user/dashboard' : '/dashboard', }, + ...generatedCrumbs, ]; - if (pathname === '/') { - curBreads.push(HomeBread); - } else { - const pieces = pathname.split('/').filter((it) => it !== ''); - pieces.forEach((it) => { - const bread = OtherBread[it as keyof typeof OtherBread]; - if (bread) { - curBreads.push(bread); - } - }); - } - // if (pageName) { - // curBreads.push({ title: pageName, to: 'custom' }) - // } - setBreads(curBreads); }, [pathname]); return ( - {/* */} - {breads.map((it, idx) => { - return ( - { + const isLast = index === breadcrumbs.length - 1; - ...(idx === breads.length - 1 && { fontWeight: 'bold' }), - }} - > - {idx !== 0 && ( + const crumbContent = ( + + {index > 0 && ( )} - {it.to === 'custom' ? ( - - {it.title} - - ) : ( - - - {it.title} - - - )} + + {crumb.title} + ); + + if (isLast) { + return ( + + {crumbContent} + + ); + } + + if (crumb.to === 'custom') { + return ( + + {crumbContent} + + ); + } + + return ( + + + {crumbContent} + + + ); })} ); diff --git a/ui/src/components/markDown/code.tsx b/ui/src/components/markDown/code.tsx index 8df87c7..648732b 100644 --- a/ui/src/components/markDown/code.tsx +++ b/ui/src/components/markDown/code.tsx @@ -108,7 +108,7 @@ const Code = ({ accessibilitySupport: 'off', bracketPairColorization: { enabled: false }, matchBrackets: 'never', - lineNumbers: 'on', + lineNumbers: 'off', verticalScrollbarSize: 0, horizontalScrollbarSize: 0, scrollbar: { diff --git a/ui/src/components/markDown/diff.tsx b/ui/src/components/markDown/diff.tsx index f2e7fc1..073bb68 100644 --- a/ui/src/components/markDown/diff.tsx +++ b/ui/src/components/markDown/diff.tsx @@ -61,7 +61,7 @@ const Diff: React.FC = ({ fontSize: 14, scrollBeyondLastLine: false, wordWrap: 'off', - lineNumbers: 'on', + lineNumbers: 'off', glyphMargin: false, folding: false, overviewRulerLanes: 0, diff --git a/ui/src/components/sidebar/index.tsx b/ui/src/components/sidebar/index.tsx index c470836..98ccd9d 100644 --- a/ui/src/components/sidebar/index.tsx +++ b/ui/src/components/sidebar/index.tsx @@ -2,13 +2,12 @@ import Logo from '@/assets/images/logo.png'; import { alpha, Box, Button, Stack, useTheme, styled } from '@mui/material'; import { Icon } from '@c-x/ui'; import { NavLink, useLocation } from 'react-router-dom'; -import Avatar from '../avatar'; import { Modal } from '@c-x/ui'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import Qrcode from '@/assets/images/qrcode.png'; import Version from './version'; -const menus = [ +const ADMIN_MENUS = [ { label: '仪表盘', value: '/dashboard', @@ -46,8 +45,8 @@ const menus = [ }, { label: '成员管理', - value: '/user', - pathname: 'user', + value: '/user-management', + pathname: 'user-management', icon: 'icon-yonghuguanli1', show: true, }, @@ -60,6 +59,30 @@ const menus = [ }, ]; +const USER_MENUS = [ + { + label: '仪表盘', + value: '/user/dashboard', + pathname: '/user/dashboard', + icon: 'icon-yibiaopan', + show: true, + }, + { + label: '对话记录', + value: '/user/chat', + pathname: '/user/chat', + icon: 'icon-duihuajilu1', + show: true, + }, + { + label: '补全记录', + value: '/user/completion', + pathname: '/user/completion', + icon: 'icon-buquanjilu', + show: true, + }, +]; + const SidebarButton = styled(Button)(({ theme }) => ({ fontSize: 14, flexShrink: 0, @@ -81,6 +104,13 @@ const Sidebar = () => { const { pathname } = useLocation(); const theme = useTheme(); const [showQrcode, setShowQrcode] = useState(false); + const menus = useMemo(() => { + if (pathname.startsWith('/user/')) { + return USER_MENUS; + } + return ADMIN_MENUS; + }, [pathname]); + return ( { new AestheticFluidBg(BACKGROUND_CONFIG); }, []); + const oauthEnable = useMemo(() => { + return ( + loginSetting.enable_custom_oauth || loginSetting.enable_dingtalk_oauth + ); + }, [loginSetting]); + // 渲染用户名输入框 const renderUsernameField = () => ( { ); - const onDingdingLogin = () => { + const onOauthLogin = (platform: 'dingtalk' | 'custom') => { getUserOauthSignupOrIn({ - platform: 'dingtalk', + platform, redirect_url: window.location.origin + window.location.pathname, // @ts-ignore session_id: searchParams.get('session_id') || null, @@ -278,15 +284,28 @@ const AuthPage = () => { }); }; - const dingdingLogin = () => { + const oauthLogin = () => { return ( 使用其他方式登录 - - - + {loginSetting.enable_dingtalk_oauth && ( + onOauthLogin('dingtalk')} + > + + + )} + {loginSetting.enable_custom_oauth && ( + onOauthLogin('custom')} + > + + + )} ); }; @@ -322,7 +341,7 @@ const AuthPage = () => { {!loginSetting.disable_password_login && renderLoginForm()} - {loginSetting.enable_dingtalk_oauth && dingdingLogin()} + {oauthEnable && oauthLogin()} ); diff --git a/ui/src/pages/chat/index.tsx b/ui/src/pages/chat/index.tsx index 5cadcb3..c9909af 100644 --- a/ui/src/pages/chat/index.tsx +++ b/ui/src/pages/chat/index.tsx @@ -35,6 +35,7 @@ const Chat = () => { useEffect(() => { fetchData(); + // eslint-disable-next-line }, [page, size]); const columns: ColumnsType = [ @@ -102,7 +103,7 @@ const Chat = () => { }; return ( - { value ? workModeMap[value]['name'] : '未知' } + {value ? workModeMap[value]['name'] : '未知'} ); }, diff --git a/ui/src/pages/completion/completionDetailModal.tsx b/ui/src/pages/completion/completionDetailModal.tsx index 7f52357..a1fa4f8 100644 --- a/ui/src/pages/completion/completionDetailModal.tsx +++ b/ui/src/pages/completion/completionDetailModal.tsx @@ -123,7 +123,7 @@ const ChatDetailModal = ({ fontSize: 14, scrollBeyondLastLine: false, wordWrap: 'on', - lineNumbers: 'on', + lineNumbers: 'off', glyphMargin: false, folding: false, overviewRulerLanes: 0, diff --git a/ui/src/pages/dashboard/components/memberInfo.tsx b/ui/src/pages/dashboard/components/memberInfo.tsx index cbd4af3..7b1ef53 100644 --- a/ui/src/pages/dashboard/components/memberInfo.tsx +++ b/ui/src/pages/dashboard/components/memberInfo.tsx @@ -54,8 +54,8 @@ const MemberInfo = ({ }: { data: DomainUserHeatmapResp; memberData: DomainUser | null; - userList: DomainUser[]; - onMemberChange: (data: DomainUser) => void; + userList?: DomainUser[]; + onMemberChange?: (data: DomainUser) => void; }) => { const theme = useTheme(); const [blockSize, setBlockSize] = useState(8); @@ -104,12 +104,12 @@ const MemberInfo = ({ open={open} onClose={handleClose} > - {userList.map((item) => ( + {userList?.map((item) => ( { - onMemberChange(item); + onMemberChange?.(item); handleClose(); }} sx={{ @@ -127,12 +127,14 @@ const MemberInfo = ({ sx={{ mb: 1 }} > - - - + {userList && ( + + + + )} {memberData?.username} diff --git a/ui/src/pages/dashboard/components/memberStatistic.tsx b/ui/src/pages/dashboard/components/memberStatistic.tsx index 8d81f65..8a7767e 100644 --- a/ui/src/pages/dashboard/components/memberStatistic.tsx +++ b/ui/src/pages/dashboard/components/memberStatistic.tsx @@ -42,6 +42,7 @@ const MemberStatistic = ({ () => getUserEventsDashboard({ user_id: id || '', + precision: timeDuration.precision, }), { refreshDeps: [id], diff --git a/ui/src/pages/invite/index.tsx b/ui/src/pages/invite/index.tsx index ef77696..b26151c 100644 --- a/ui/src/pages/invite/index.tsx +++ b/ui/src/pages/invite/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import Logo from '@/assets/images/logo.png'; @@ -20,6 +20,7 @@ import { IconButton, CircularProgress, Stack, + Divider, } from '@mui/material'; import { useRequest } from 'ahooks'; import { @@ -134,19 +135,54 @@ const Invite = () => { }); }, []); - const onDingdingLogin = () => { + const onOauthLogin = (platform: 'dingtalk' | 'custom') => { getUserOauthSignupOrIn({ - platform: 'dingtalk', + platform, redirect_url: `${window.location.origin}/invite/${id}/2`, + inviate_code: id, }).then((res) => { - window.location.href = res.url!; + if (res.url) { + window.location.href = res.url; + } }); }; + const oauthEnable = useMemo(() => { + return ( + loginSetting.enable_custom_oauth || loginSetting.enable_dingtalk_oauth + ); + }, [loginSetting]); + + const oauthLogin = () => { + return ( + + + 使用以下方式注册 + + {loginSetting.enable_dingtalk_oauth && ( + + )} + {loginSetting.enable_custom_oauth && ( + onOauthLogin('custom')} + > + + + )} + + ); + }; + const renderStepContent = () => { switch (activeStep) { case 1: - return !loginSetting.enable_dingtalk_oauth ? ( + return !oauthEnable ? ( @@ -285,17 +321,7 @@ const Invite = () => { ) : ( - - - + oauthLogin() ); case 2: diff --git a/ui/src/pages/user/index.tsx b/ui/src/pages/user-management/index.tsx similarity index 82% rename from ui/src/pages/user/index.tsx rename to ui/src/pages/user-management/index.tsx index edc8cf8..89d149d 100644 --- a/ui/src/pages/user/index.tsx +++ b/ui/src/pages/user-management/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import Card from '@/components/card'; import { Grid2 as Grid, @@ -30,6 +30,15 @@ const StyledLabel = styled('div')(({ theme }) => ({ color: theme.vars.palette.text.primary, })); +const OAUTH_LOGIN_TYPE_KEYS = ['enable_custom_oauth', 'enable_dingtalk_oauth']; + +const OAUTH_LOGIN_TYPE_LABELS = { + enable_custom_oauth: '已开启 OAuth 登录', + enable_dingtalk_oauth: '已开启钉钉登录', +}; + +type OAUTH_LOGIN_TYPE_KEYS = keyof typeof OAUTH_LOGIN_TYPE_LABELS; + const User = () => { const [thirdPartyLoginSettingModalOpen, setThirdPartyLoginSettingModalOpen] = useState(false); @@ -39,6 +48,7 @@ const User = () => { force_two_factor_auth: false, disable_password_login: false, enable_dingtalk_oauth: false, + enable_custom_oauth: false, }, refresh, } = useRequest(getGetSetting); @@ -51,6 +61,15 @@ const User = () => { }, }); + const oauthLabel = useMemo(() => { + const key = OAUTH_LOGIN_TYPE_KEYS.find( + (key) => data[key as OAUTH_LOGIN_TYPE_KEYS] + ); + return key + ? OAUTH_LOGIN_TYPE_LABELS[key as OAUTH_LOGIN_TYPE_KEYS] + : '未开启'; + }, [data]); + return ( @@ -84,12 +103,12 @@ const User = () => { component='span' sx={{ ml: 2, - color: data.enable_dingtalk_oauth ? 'success.main' : 'gray', + color: oauthLabel ? 'success.main' : 'gray', fontWeight: 400, fontSize: 13, }} > - {data.enable_dingtalk_oauth ? '已开启钉钉登录' : '未开启'} + {oauthLabel} +); + +const ThirdPartyLoginSettingModal = ({ + open, + onCancel, + settingData, + onOk, +}: { + open: boolean; + onCancel: () => void; + settingData: DomainSetting; + onOk: () => void; +}) => { + const { + control, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + defaultValues: { + dingtalk_client_id: '', + dingtalk_client_secret: '', + custom_oauth_access_token_url: '', + custom_oauth_authorize_url: '', + custom_oauth_client_id: '', + custom_oauth_client_secret: '', + }, + }); + + const [loginType, setLoginType] = useState( + settingData?.enable_dingtalk_oauth ? 'dingding' : 'none' + ); + + useEffect(() => { + if (open) { + reset(); + } + }, [open]); + + useEffect(() => { + if (settingData?.enable_dingtalk_oauth) { + setLoginType('dingding'); + reset( + { + dingtalk_client_id: settingData.dingtalk_client_id, + }, + { + keepValues: true, + } + ); + } + if (settingData?.enable_custom_oauth) { + setLoginType('oauth'); + reset( + { + custom_oauth_access_token_url: + settingData.custom_oauth_access_token_url, + custom_oauth_authorize_url: settingData.custom_oauth_authorize_url, + custom_oauth_client_id: settingData.custom_oauth_client_id, + }, + { + keepValues: true, + } + ); + } + }, [settingData]); + + const onSubmit = handleSubmit((data) => { + let params: DomainUpdateSettingReq = {}; + if (loginType === 'none') { + params = { + enable_dingtalk_oauth: false, + enable_custom_oauth: false, + }; + } else if (loginType === 'dingding') { + params = { + enable_dingtalk_oauth: true, + enable_custom_oauth: false, + dingtalk_client_id: data.dingtalk_client_id, + dingtalk_client_secret: data.dingtalk_client_secret, + }; + } else if (loginType === 'oauth') { + params = { + enable_custom_oauth: true, + enable_dingtalk_oauth: false, + custom_oauth_access_token_url: data.custom_oauth_access_token_url, + custom_oauth_authorize_url: data.custom_oauth_authorize_url, + custom_oauth_client_id: data.custom_oauth_client_id, + custom_oauth_client_secret: data.custom_oauth_client_secret, + }; + } + + putUpdateSetting(params).then(() => { + message.success('设置成功'); + onCancel(); + onOk(); + }); + }); + + const dingdingForm = () => { + return ( + <> + + ( + + )} + /> + + + ( + + )} + /> + + + ); + }; + + const oauthForm = () => { + return ( + <> + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + + ); + }; + + return ( + + + {loginOptions.map((option) => ( + + ))} + + + {loginType === 'dingding' && dingdingForm()} + {loginType === 'oauth' && oauthForm()} + + + ); +}; + +export default ThirdPartyLoginSettingModal; diff --git a/ui/src/pages/user/chat/chatDetailModal.tsx b/ui/src/pages/user/chat/chatDetailModal.tsx new file mode 100644 index 0000000..6e61428 --- /dev/null +++ b/ui/src/pages/user/chat/chatDetailModal.tsx @@ -0,0 +1,149 @@ +import Avatar from '@/components/avatar'; +import Card from '@/components/card'; +import { getChatInfo } from '@/api/Billing'; +import MarkDown from '@/components/markDown'; +import { Ellipsis, Modal } from '@c-x/ui'; +import { styled } from '@mui/material'; +import logo from '@/assets/images/logo.png'; + +import { useEffect, useState } from 'react'; +import { DomainChatContent, DomainChatRecord } from '@/api/types'; + +const StyledChatList = styled('div')(() => ({ + borderRadius: 4, + padding: 24, + minHeight: 400, + maxHeight: 600, + overflowY: 'auto', +})); + +const StyledChatRow = styled('div', { + shouldForwardProp: (prop) => prop !== 'isUser', +})<{ isUser: boolean }>(({ isUser, theme }) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: isUser ? 'flex-end' : 'flex-start', + gap: theme.spacing(1), + marginBottom: theme.spacing(2), +})); + +const StyledChatUser = styled('div', { + shouldForwardProp: (prop) => prop !== 'isUser', +})<{ isUser: boolean }>(({ isUser }) => ({ + display: 'flex', + flexDirection: isUser ? 'row-reverse' : 'row', + alignItems: 'center', + position: 'relative', +})); + +const StyledChatName = styled('div')(({ theme }) => ({ + color: theme.vars.palette.text.primary, + fontSize: '14px', + fontWeight: 500, +})); + +const StyledChatAvatar = styled('div', { + shouldForwardProp: (prop) => prop !== 'isUser', +})<{ isUser: boolean }>(({ isUser }) => ({ + margin: isUser ? '0 0 0 12px' : '0 12px 0 0', + display: 'flex', + alignItems: 'flex-start', + position: 'relative', + top: 0, +})); + +const StyledChatBubble = styled('div', { + shouldForwardProp: (prop) => prop !== 'isUser', +})<{ isUser: boolean }>(({ isUser }) => ({ + background: isUser ? '#e6f7ff' : '#f5f5f5', + margin: isUser ? '0 36px 0 0' : '0 0 0 36px', + borderRadius: 12, + padding: '8px 12px', + minHeight: 36, + maxWidth: 1040, + wordBreak: 'break-word', + position: 'relative', +})); + +const ChatDetailModal = ({ + data, + open, + onClose, +}: { + data?: DomainChatRecord; + open: boolean; + onClose: () => void; +}) => { + const [content, setContent] = useState([]); + + const getChatDetailModal = () => { + if (!data) return; + getChatInfo({ id: data.id! }).then((res) => { + setContent(res.contents || []); + }); + }; + + useEffect(() => { + if (open) getChatDetailModal(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data, open]); + + return ( + + 对话记录-{data?.user?.username} + + } + sx={{ + '.MuiDialog-paper': { + maxWidth: 1300, + }, + }} + width={1200} + open={open} + onCancel={onClose} + footer={null} + > + + + {content.map((item, idx) => { + const isUser = item.role === 'user'; + const name = isUser ? data?.user?.username : 'MonkeyCode'; + const msg = item.content || ''; + return ( + + + + + + {name} + + + + + + ); + })} + + + + ); +}; + +export default ChatDetailModal; diff --git a/ui/src/pages/user/chat/index.tsx b/ui/src/pages/user/chat/index.tsx new file mode 100644 index 0000000..c9909af --- /dev/null +++ b/ui/src/pages/user/chat/index.tsx @@ -0,0 +1,170 @@ +import React, { useState, useEffect } from 'react'; +import { Table } from '@c-x/ui'; +import { getListChatRecord } from '@/api/Billing'; +import dayjs from 'dayjs'; + +import Card from '@/components/card'; +import { Box } from '@mui/material'; +import StyledLabel from '@/components/label'; + +import ChatDetailModal from './chatDetailModal'; +import { ColumnsType } from '@c-x/ui/dist/Table'; +import { DomainChatRecord, DomainUser } from '@/api/types'; +import { addCommasToNumber } from '@/utils'; +import User from '@/components/user'; + +const Chat = () => { + const [page, setPage] = useState(1); + const [size, setSize] = useState(20); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(false); + const [dataSource, setDataSource] = useState([]); + const [chatDetailModal, setChatDetailModal] = useState< + DomainChatRecord | undefined + >(); + const fetchData = async () => { + setLoading(true); + const res = await getListChatRecord({ + page: page, + size: size, + }); + setLoading(false); + setTotal(res?.total_count || 0); + setDataSource(res.records || []); + }; + + useEffect(() => { + fetchData(); + // eslint-disable-next-line + }, [page, size]); + + const columns: ColumnsType = [ + { + dataIndex: 'user', + title: '成员', + width: 260, + render(value: DomainUser) { + return ( + + ); + }, + }, + { + dataIndex: 'question', + title: '任务内容', + render(value: string, record) { + const cleanValue = value?.replace(/<\/?task>/g, '') || value; + return ( + setChatDetailModal(record)} + sx={{ + cursor: 'pointer', + color: 'info.main', + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + }} + > + {cleanValue || '无标题任务'} + + ); + }, + }, + { + dataIndex: 'work_mode', + title: '工作模式', + width: 120, + render(value: DomainChatRecord['work_mode']) { + const workModeMap: Record> = { + code: { + name: '编程模式', + color: 'warning', + }, + ask: { + name: '问答模式', + color: 'info', + }, + architect: { + name: '架构模式', + color: 'success', + }, + debug: { + name: '调试模式', + color: 'error', + }, + orchestrator: { + name: '编排模式', + color: 'info', + }, + }; + return ( + + {value ? workModeMap[value]['name'] : '未知'} + + ); + }, + }, + { + dataIndex: 'input_tokens', + title: '输入 Token', + width: 150, + render(value: number) { + return addCommasToNumber(value); + }, + }, + { + dataIndex: 'output_tokens', + title: '输出 Token', + width: 150, + render(value: number) { + return addCommasToNumber(value); + }, + }, + { + dataIndex: 'created_at', + title: '时间', + width: 180, + render(value: number) { + return dayjs.unix(value).format('YYYY-MM-DD HH:mm:ss'); + }, + }, + ]; + return ( + + { + setPage(page); + setSize(size); + }, + }} + /> + setChatDetailModal(undefined)} + data={chatDetailModal} + /> + + ); +}; + +export default Chat; diff --git a/ui/src/pages/user/completion/completionDetailModal.tsx b/ui/src/pages/user/completion/completionDetailModal.tsx new file mode 100644 index 0000000..a1fa4f8 --- /dev/null +++ b/ui/src/pages/user/completion/completionDetailModal.tsx @@ -0,0 +1,176 @@ +import Card from '@/components/card'; +import { getCompletionInfo } from '@/api/Billing'; +import { Modal } from '@c-x/ui'; +import MonacoEditor from '@monaco-editor/react'; + +import { useEffect, useState, useRef } from 'react'; +import { DomainCompletionRecord } from '@/api/types'; +import { getBaseLanguageId } from '@/utils'; + +// 删除 <|im_start|> 和 <|im_end|> 及其间内容的工具函数 +const removeImBlocks = (text: string) => { + // 匹配前后可能的换行符 + return text.replace( + /(^[ \t]*\r?\n)?<\|im_start\|>[\s\S]*?<\|im_end\|>(\r?\n)?/g, + '' + ); +}; + +const ChatDetailModal = ({ + data, + open, + onClose, +}: { + data?: DomainCompletionRecord; + open: boolean; + onClose: () => void; +}) => { + const [editorValue, setEditorValue] = useState(''); + const editorRef = useRef(null); + const [editorReady, setEditorReady] = useState(false); + const [highlightInfo, setHighlightInfo] = useState(null); + + const getChatDetailModal = () => { + if (!data) return; + getCompletionInfo({ id: data.id! }).then((res) => { + // 先去除 <|im_start|> 和 <|im_end|> 及其间内容 + const rawPrompt = removeImBlocks(res.prompt || ''); + const content = res.content || ''; + // 找到三个特殊标记的位置 + const prefixTag = '<|fim_prefix|>'; + const suffixTag = '<|fim_suffix|>'; + const middleTag = '<|fim_middle|>'; + const prefixIdx = rawPrompt.indexOf(prefixTag); + const suffixIdx = rawPrompt.indexOf(suffixTag); + const middleIdx = rawPrompt.indexOf(middleTag); + // 去掉特殊标记 + const prompt = rawPrompt + .replace(prefixTag, '') + .replace(suffixTag, '') + .replace(middleTag, ''); + // 重新定位插入点(因为去掉了前面的 tag,位置会变) + // 计算插入点:suffixTag 在原始 prompt 的位置,去掉 prefixTag 后的 offset + let insertIdx = suffixIdx; + if (prefixIdx !== -1 && prefixIdx < suffixIdx) { + insertIdx -= prefixTag.length; + } + if (middleIdx !== -1 && middleIdx < suffixIdx) { + insertIdx -= middleTag.length; + } + // 插入 content + const newValue = + prompt.slice(0, insertIdx) + content + prompt.slice(insertIdx); + setEditorValue(newValue); + // 计算高亮范围(行列) + const before = newValue.slice(0, insertIdx); + const contentLines = content.split('\n'); + const beforeLines = before.split('\n'); + const startLine = beforeLines.length; + const startColumn = beforeLines[beforeLines.length - 1].length + 1; + const endLine = startLine + contentLines.length - 1; + const endColumn = + contentLines.length === 1 + ? startColumn + content.length + : contentLines[contentLines.length - 1].length + 1; + setHighlightInfo({ startLine, startColumn, endLine, endColumn }); + }); + }; + + useEffect(() => { + if (editorReady && highlightInfo && editorRef.current) { + editorRef.current.deltaDecorations( + [], + [ + { + range: { + startLineNumber: highlightInfo.startLine, + startColumn: highlightInfo.startColumn, + endLineNumber: highlightInfo.endLine, + endColumn: highlightInfo.endColumn, + }, + options: { + inlineClassName: 'completion-highlight', + }, + }, + ] + ); + } + }, [editorReady, highlightInfo, editorValue]); + + useEffect(() => { + if (open) getChatDetailModal(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data, open]); + + return ( + + +
+ { + editorRef.current = editor; + setEditorReady(true); + // 隐藏光标 + const editorDom = editor.getDomNode(); + if (editorDom) { + const style = document.createElement('style'); + style.innerHTML = `.monaco-editor .cursor { display: none !important; }`; + editorDom.appendChild(style); + } + }} + /> +
+ +
+
+ ); +}; + +export default ChatDetailModal; diff --git a/ui/src/pages/user/completion/constant.ts b/ui/src/pages/user/completion/constant.ts new file mode 100644 index 0000000..3d62dfb --- /dev/null +++ b/ui/src/pages/user/completion/constant.ts @@ -0,0 +1,67 @@ +export const LANG_OPTIONS = [ + 'JavaScript', + 'JavaScriptReact', + 'TypeScript', + 'TypeScriptReact', + 'Python', + 'Java', + 'C', + 'C++', + 'C#', + 'Go', + 'PHP', + 'Ruby', + 'Swift', + 'Kotlin', + 'Rust', + 'Dart', + 'Objective-C', + 'Scala', + 'Perl', + 'R', + 'Shell Script', + 'PowerShell', + 'HTML', + 'CSS', + 'SCSS', + 'Less', + 'JSON', + 'YAML', + 'XML', + 'Markdown', + 'SQL', + 'GraphQL', + 'Dockerfile', + 'Makefile', + 'Lua', + 'Haskell', + 'Elixir', + 'Erlang', + 'F#', + 'Groovy', + 'Visual Basic', + 'Assembly', + 'Matlab', + 'Fortran', + 'COBOL', + 'Prolog', + 'Scheme', + 'Lisp', + 'Julia', + 'SASS', + 'TOML', + 'INI', + 'LaTeX', + 'CMake', + 'Batch', + 'CoffeeScript', + 'Crystal', + 'OCaml', + 'Nim', + 'ReScript', + 'Solidity', + 'Vue', + 'Svelte', + 'JSX', + 'TSX', +]; diff --git a/ui/src/pages/user/completion/index.tsx b/ui/src/pages/user/completion/index.tsx new file mode 100644 index 0000000..cf85150 --- /dev/null +++ b/ui/src/pages/user/completion/index.tsx @@ -0,0 +1,237 @@ +import { useState, useEffect } from 'react'; +import { DomainCompletionRecord, DomainUser } from '@/api/types'; +import { getListCompletionRecord } from '@/api/Billing'; +import { useRequest } from 'ahooks'; +import { Table } from '@c-x/ui'; +import Card from '@/components/card'; +import { + Box, + Stack, + MenuItem, + Select, + FormControl, + InputLabel, + Autocomplete, + TextField, +} from '@mui/material'; +import { getListUser } from '@/api/User'; +import dayjs from 'dayjs'; +import { useDebounceFn } from 'ahooks'; +import { ColumnsType } from '@c-x/ui/dist/Table'; +import { addCommasToNumber } from '@/utils'; +import CompletionDetailModal from './completionDetailModal'; +import StyledLabel from '@/components/label'; +import { LANG_OPTIONS } from './constant'; +import User from '@/components/user'; + +const Completion = () => { + const [page, setPage] = useState(1); + const [size, setSize] = useState(20); + const [total, setTotal] = useState(0); + const [dataSource, setDataSource] = useState([]); + const [loading, setLoading] = useState(false); + const [completionDetailModal, setCompletionDetailModal] = useState< + DomainCompletionRecord | undefined + >(); + + // 新增筛选项 state + const [filterUser, setFilterUser] = useState(''); + const [filterLang, setFilterLang] = useState(''); + const [filterAccept, setFilterAccept] = useState< + 'accepted' | 'unaccepted' | '' + >('accepted'); + + const { data: userOptions = { users: [] } } = useRequest(() => + getListUser({ + page: 1, + size: 10, + }) + ); + + useEffect(() => { + setPage(1); // 筛选变化时重置页码 + fetchData({ + page: 1, + language: filterLang, + author: filterUser, + is_accept: filterAccept, + }); + }, [filterUser, filterLang, filterAccept]); + + const fetchData = async (params: { + page?: number; + size?: number; + language?: string; + author?: string; + is_accept?: 'accepted' | 'unaccepted' | ''; + }) => { + setLoading(true); + const isAccept = params.is_accept || filterAccept; + const res = await getListCompletionRecord({ + page: params.page || page, + size: params.size || size, + language: params.language || filterLang, + author: params.author || filterUser, + is_accept: + isAccept === 'accepted' + ? true + : isAccept === 'unaccepted' + ? false + : undefined, + }); + setLoading(false); + setTotal(res?.total_count || 0); + setDataSource(res.records || []); + }; + + const columns: ColumnsType = [ + { + dataIndex: 'user', + title: '成员', + render(value: DomainUser) { + return ( + + ); + }, + }, + { + dataIndex: 'task', + title: '补全内容', + width: 150, + render(_, record) { + return ( + setCompletionDetailModal(record)} + sx={{ color: 'info.main', cursor: 'pointer' }} + > + 点击查看 + + ); + }, + }, + { + dataIndex: 'is_accept', + title: '是否采纳', + width: 130, + render(value: boolean) { + const color = value ? 'success' : 'default'; + return ( + {value ? '已采纳' : '未采纳'} + ); + }, + }, + { + dataIndex: 'program_language', + title: '编程语言', + width: 160, + }, + { + dataIndex: 'input_tokens', + title: '输入 Token', + width: 140, + render(value: number) { + return addCommasToNumber(value); + }, + }, + { + dataIndex: 'output_tokens', + title: '输出 Token', + width: 140, + render(value: number) { + return addCommasToNumber(value); + }, + }, + { + dataIndex: 'created_at', + title: '时间', + width: 200, + render(value: number) { + return dayjs.unix(value).format('YYYY-MM-DD HH:mm:ss'); + }, + }, + ]; + + const debounceSetFilterLang = useDebounceFn( + (val: string) => setFilterLang(val), + { + wait: 500, + } + ); + + return ( + + + option || ''} + value={filterLang || ''} + freeSolo + onChange={(_, newValue) => { + setFilterLang(newValue ? String(newValue) : ''); + }} + onInputChange={(_, newInputValue) => + debounceSetFilterLang.run(newInputValue) + } + renderInput={(params) => } + clearOnEscape + /> + + 是否采纳 + + + +
{ + setPage(page); + setSize(size); + fetchData({ + page, + size, + }); + }, + }} + /> + + setCompletionDetailModal(undefined)} + data={completionDetailModal} + /> + + ); +}; + +export default Completion; diff --git a/ui/src/pages/user/dashboard/components/memberStatistic.tsx b/ui/src/pages/user/dashboard/components/memberStatistic.tsx new file mode 100644 index 0000000..2ab70f2 --- /dev/null +++ b/ui/src/pages/user/dashboard/components/memberStatistic.tsx @@ -0,0 +1,212 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Grid2 as Grid } from '@mui/material'; +import { useParams } from 'react-router-dom'; +import MemberInfo from '@/pages/dashboard/components/memberInfo'; +import PieCharts from '@/pages/dashboard/components/pieCharts'; +import LineCharts from '@/pages/dashboard/components/lineCharts'; +import { RecentActivityCard } from '@/pages/dashboard/components/statisticCard'; +import { useRequest } from 'ahooks'; +import { + getUserEventsDashboard, + getUserStatDashboard, + getUserHeatmapDashboard, +} from '@/api/Dashboard'; +import { StyledHighlight } from '@/pages/dashboard/components/globalStatistic'; +import { getRecent90DaysData, getRecent24HoursData } from '@/utils'; +import { DomainUser } from '@/api/types'; +import { TimeRange } from '../index'; + +interface TimeDuration { + duration: number; + precision: 'day' | 'hour'; +} + +const MemberStatistic = ({ + memberData, + timeRange, +}: { + memberData: DomainUser | null; + timeRange: TimeRange; +}) => { + const [timeDuration, setTimeDuration] = useState({ + duration: timeRange === '90d' ? 90 : 24, + precision: timeRange === '90d' ? 'day' : 'hour', + }); + + const { id } = useParams(); + const { data: userEvents } = useRequest( + () => + getUserEventsDashboard({ + user_id: id || '', + precision: timeDuration.precision, + }), + { + refreshDeps: [id], + manual: false, + ready: !!id, + } + ); + const { data: userStat } = useRequest( + () => + getUserStatDashboard({ + user_id: id || '', + ...timeDuration, + }), + { + refreshDeps: [id, timeDuration], + manual: false, + ready: !!id, + } + ); + const { data: userHeatmap } = useRequest( + () => + getUserHeatmapDashboard({ + user_id: id || '', + }), + { + refreshDeps: [id], + manual: false, + ready: !!id, + } + ); + + useEffect(() => { + setTimeDuration({ + duration: timeRange === '90d' ? 90 : 24, + precision: timeRange === '90d' ? 'day' : 'hour', + }); + }, [timeRange]); + + const getRangeData = ( + data: Record[], + timeRange: TimeRange, + label: { keyLabel?: string; valueLabel?: string } = { valueLabel: 'value' } + ) => { + return timeRange === '90d' + ? getRecent90DaysData(data, label) + : getRecent24HoursData(data, label); + }; + + const { + chatChartData, + codeCompletionChartData, + codeLineChartData, + acceptedPerChartData, + } = useMemo(() => { + const { + accepted_per = [], + chats = [], + code_completions = [], + lines_of_code = [], + } = userStat || {}; + const label = { valueLabel: 'value' }; + const chatChartData = getRangeData(chats, timeRange, label); + const codeCompletionChartData = getRangeData( + code_completions, + timeRange, + label + ); + const codeLineChartData = getRangeData(lines_of_code, timeRange, label); + const acceptedPerChartData = getRangeData(accepted_per, timeRange, label); + return { + chatChartData, + codeCompletionChartData, + codeLineChartData, + acceptedPerChartData, + }; + }, [userStat]); + return ( + + + + + + + + + + + + + + + + + + {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共 + {userStat?.total_chats || 0} + 个对话任务 + + } + /> + + + + {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共 + + {userStat?.total_completions || 0} + + 个补全任务 + + } + /> + + + + {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}共修改 + + {userStat?.total_lines_of_code || 0} + + 行代码 + + } + /> + + + + {timeRange === '90d' ? '最近 90 天' : '最近 24 小时'}平均采纳率为 + + {(userStat?.total_accepted_per || 0).toFixed(2)} + + % + + } + /> + + + ); +}; + +export default MemberStatistic; diff --git a/ui/src/pages/user/dashboard/index.tsx b/ui/src/pages/user/dashboard/index.tsx new file mode 100644 index 0000000..015ff98 --- /dev/null +++ b/ui/src/pages/user/dashboard/index.tsx @@ -0,0 +1,77 @@ +import { useEffect, useMemo, useState } from 'react'; +import { getListUser } from '@/api/User'; +import { Stack, MenuItem, Select } from '@mui/material'; + +import { useRequest } from 'ahooks'; +import MemberStatistic from './components/memberStatistic'; +import { useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; +import { DomainUser } from '@/api/types'; + +export type TimeRange = '90d' | '24h'; + +const Dashboard = () => { + const navigate = useNavigate(); + const { tab, id } = useParams(); + const [tabValue, setTabValue] = useState(tab || 'global'); + const [memberData, setMemberData] = useState(null); + const [timeRange, setTimeRange] = useState('24h'); + + const { data: userData, refresh } = useRequest( + () => + getListUser({ + page: 1, + size: 99999, + }), + { + manual: true, + onSuccess: (res) => { + if (id) { + setMemberData(res.users?.find((item) => item.id === id) || null); + } else { + setMemberData(res.users?.[0] || null); + navigate(`/dashboard/member/${res.users?.[0]?.id}`); + } + }, + } + ); + const userList = useMemo(() => { + return userData?.users || []; + }, [userData]); + useEffect(() => { + if (tabValue === 'member') { + refresh(); + } + }, [tabValue]); + + const onMemberChange = (data: DomainUser) => { + setMemberData(data); + navigate(`/dashboard/member/${data.id}`); + }; + + return ( + + + + + + + + ); +}; + +export default Dashboard; diff --git a/ui/src/pages/user/login/index.tsx b/ui/src/pages/user/login/index.tsx new file mode 100644 index 0000000..6cbd09f --- /dev/null +++ b/ui/src/pages/user/login/index.tsx @@ -0,0 +1,340 @@ +import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import Logo from '@/assets/images/logo.png'; +import { + Box, + Button, + TextField, + Typography, + Container, + Paper, + CircularProgress, + Grid2 as Grid, + InputAdornment, + IconButton, + Divider, + Stack, +} from '@mui/material'; +import { Icon, message } from '@c-x/ui'; + +import { getRedirectUrl } from '@/utils'; + +// @ts-ignore +import { AestheticFluidBg } from '@/assets/jsm/AestheticFluidBg.module.js'; + +import { useSearchParams } from 'react-router-dom'; +import { postLogin, getUserOauthSignupOrIn, getGetSetting } from '@/api/User'; + +import { useForm, Controller } from 'react-hook-form'; +import { styled } from '@mui/material/styles'; +import { useRequest } from 'ahooks'; + +// 样式化组件 +const StyledContainer = styled(Container)(({ theme }) => ({ + minHeight: '100vh', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + maxWidth: '100% !important', + background: theme.palette.background.paper, +})); + +const StyledPaper = styled(Paper)(({ theme }) => ({ + position: 'relative', + zIndex: 9, + padding: theme.spacing(4), + background: 'rgba(255, 255, 255, 0.85)', + backdropFilter: 'blur(10px)', + width: 458, + borderRadius: theme.spacing(2), + boxShadow: + '0px 0px 4px 0px rgba(54,59,76,0.1), 0px 20px 40px 0px rgba(54,59,76,0.1)', +})); + +const LogoContainer = styled(Box)(({ theme }) => ({ + textAlign: 'center', + marginBottom: theme.spacing(4), +})); + +const LogoImage = styled('img')({ + width: 48, + height: 48, +}); + +const LogoTitle = styled(Typography)(({ theme }) => ({ + fontSize: 28, + fontWeight: 'bold', + color: theme.palette.primary.main, +})); + +const StyledTextField = styled(TextField)(({ theme }) => ({ + '.MuiInputBase-root': { + backgroundColor: '#fff', + paddingLeft: '20px', + }, + '.MuiInputBase-input': { + paddingTop: '16px', + paddingBottom: '16px', + fontSize: 14, + }, +})); + +const StyledButton = styled(Button)(({ theme }) => ({ + height: 48, + textTransform: 'none', +})); + +const IconWrapper = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: theme.palette.text.primary, + marginRight: theme.spacing(2), + fontSize: 16, +})); + +const TogglePasswordIcon = styled(Icon)({ + fontSize: 20, +}); + +// 表单数据类型 +interface LoginFormData { + username: string; + password: string; +} + +// 背景动画配置 +const BACKGROUND_CONFIG = { + dom: 'box', + colors: ['#FDFDFD', '#DDDDDD', '#BBBBBB', '#555555', '#343434', '#010101'], + loop: true, +} as const; + +const UserLogin = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [showPassword, setShowPassword] = useState(false); + + const [searchParams] = useSearchParams(); + const { data: loginSetting = {} } = useRequest(getGetSetting); + + const { + control, + handleSubmit, + formState: { errors }, + } = useForm(); + + // 切换密码显示状态 + const togglePasswordVisibility = useCallback(() => { + setShowPassword((prev) => !prev); + }, []); + + // 处理登录表单提交 + const onSubmit = useCallback( + async (data: LoginFormData) => { + setLoading(true); + setError(null); + + try { + const sessionId = searchParams.get('session_id'); + if (!sessionId) { + message.error('缺少会话ID参数'); + return; + } + + // 用户登录 + const loginResult = await postLogin({ + ...data, + session_id: sessionId, + }); + + window.location.href = loginResult.redirect_url!; + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : '登录失败,请重试'; + setError(errorMessage); + console.error('登录失败:', err); + } finally { + setLoading(false); + } + }, + [searchParams] + ); + + // 初始化背景动画 + useEffect(() => { + new AestheticFluidBg(BACKGROUND_CONFIG); + }, []); + + const oauthEnable = useMemo(() => { + return ( + loginSetting.enable_custom_oauth || loginSetting.enable_dingtalk_oauth + ); + }, [loginSetting]); + + // 渲染用户名输入框 + const renderUsernameField = () => ( + ( + + + + ), + }, + }} + /> + )} + /> + ); + + // 渲染密码输入框 + const renderPasswordField = () => ( + ( + + + + ), + endAdornment: ( + + + + + + ), + }, + }} + /> + )} + /> + ); + + // 渲染登录按钮 + const renderLoginButton = () => ( + + + {loading ? : '登录'} + + + ); + + const onOauthLogin = (platform: 'dingtalk' | 'custom') => { + const redirectUrl = getRedirectUrl(); + getUserOauthSignupOrIn({ + platform, + redirect_url: redirectUrl.href, + }).then((res) => { + if (res.url) { + window.location.href = res.url; + } + }); + }; + + const oauthLogin = () => { + return ( + + + 使用其他方式登录 + + {loginSetting.enable_dingtalk_oauth && ( + onOauthLogin('dingtalk')} + > + + + )} + {loginSetting.enable_custom_oauth && ( + onOauthLogin('custom')} + > + + + )} + + ); + }; + + // 渲染登录表单 + const renderLoginForm = () => ( + <> + + + {renderUsernameField()} + {renderPasswordField()} + + {renderLoginButton()} + + + + ); + + useEffect(() => { + const redirect_url = searchParams.get('redirect_url'); + if (redirect_url) { + window.location.href = redirect_url; + } + }, []); + + return ( + + + + + + Monkey Code + + + {!loginSetting.disable_password_login && renderLoginForm()} + {oauthEnable && oauthLogin()} + + + ); +}; + +export default UserLogin; diff --git a/ui/src/pages/user/thirdPartyLoginSettingModal.tsx b/ui/src/pages/user/thirdPartyLoginSettingModal.tsx deleted file mode 100644 index bf57abc..0000000 --- a/ui/src/pages/user/thirdPartyLoginSettingModal.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import { Button, Radio, Stack, Box, TextField } from '@mui/material'; -import { Modal, Icon, message } from '@c-x/ui'; -import { useState, useEffect } from 'react'; -import { useForm, Controller } from 'react-hook-form'; -import { StyledFormLabel } from '@/components/form'; -import { putUpdateSetting } from '@/api/User'; -import { DomainSetting } from '@/api/types'; - -type LoginType = 'dingding' | 'wechat' | 'feishu' | 'oauth' | 'none'; - -const ThirdPartyLoginSettingModal = ({ - open, - onCancel, - settingData, - onOk, -}: { - open: boolean; - onCancel: () => void; - settingData: DomainSetting; - onOk: () => void; -}) => { - const { - control, - handleSubmit, - reset, - formState: { errors }, - } = useForm({ - defaultValues: { - dingtalk_client_id: '', - dingtalk_client_secret: '', - // title: '', - }, - }); - - const [loginType, setLoginType] = useState( - settingData?.enable_dingtalk_oauth ? 'dingding' : 'none' - ); - - useEffect(() => { - if (open) { - reset(); - } - }, [open]); - - useEffect(() => { - if (settingData?.enable_dingtalk_oauth) { - setLoginType('dingding'); - } - }, [settingData]); - - const onSubmit = handleSubmit((data) => { - if (loginType === 'none') { - putUpdateSetting({ ...data, enable_dingtalk_oauth: false }).then(() => { - message.success('设置成功'); - onCancel(); - onOk(); - }); - } - if (loginType === 'dingding') { - putUpdateSetting({ ...data, enable_dingtalk_oauth: true }).then(() => { - message.success('设置成功'); - onCancel(); - onOk(); - }); - } - }); - - return ( - - - - - - - - - {loginType === 'dingding' && ( - - - Client ID - ( - - )} - /> - - - Client Secret - ( - - )} - /> - - {/* - 标题名称,默认为 身份认证-钉钉登录 - ( - { - field.onChange(e.target.value); - }} - /> - )} - /> - */} - - )} - - ); -}; - -export default ThirdPartyLoginSettingModal; diff --git a/ui/src/router.tsx b/ui/src/router.tsx index cc50378..7c29808 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -32,18 +32,26 @@ const Dashboard = LazyLoadable(lazy(() => import('@/pages/dashboard'))); const Chat = LazyLoadable(lazy(() => import('@/pages/chat'))); const Completion = LazyLoadable(lazy(() => import('@/pages/completion'))); const Model = LazyLoadable(lazy(() => import('@/pages/model'))); -const User = LazyLoadable(lazy(() => import('@/pages/user'))); +const User = LazyLoadable(lazy(() => import('@/pages/user-management'))); const Admin = LazyLoadable(lazy(() => import('@/pages/admin'))); const Invite = LazyLoadable(lazy(() => import('@/pages/invite'))); const Auth = LazyLoadable(lazy(() => import('@/pages/auth'))); const Login = LazyLoadable(lazy(() => import('@/pages/login'))); +const UserLogin = LazyLoadable(lazy(() => import('@/pages/user/login'))); const Expectation = LazyLoadable(lazy(() => import('@/pages/expectation'))); +const UserChat = LazyLoadable(lazy(() => import('@/pages/user/chat'))); +const UserCompletion = LazyLoadable( + lazy(() => import('@/pages/user/completion')) +); + +const UserDashboard = LazyLoadable( + lazy(() => import('@/pages/user/dashboard')) +); const routerConfig = [ { path: '/', element: , - redirect: '/dashboard', children: [ { index: true, @@ -70,7 +78,7 @@ const routerConfig = [ element: , }, { - path: 'user', + path: 'user-management', element: , }, { @@ -79,6 +87,29 @@ const routerConfig = [ }, ], }, + + { + path: '/user', + element: , + children: [ + { + index: true, + element: , + }, + { + path: 'dashboard', + element: , + }, + { + path: 'chat', + element: , + }, + { + path: 'completion', + element: , + }, + ], + }, { path: '/invite/:id/:step?', element: , @@ -87,6 +118,10 @@ const routerConfig = [ path: '/auth', element: , }, + { + path: '/user/login', + element: , + }, { path: '/login', element: , From bede408e41332dcede7b02f15e101b05049eb101 Mon Sep 17 00:00:00 2001 From: Gavan <994259213@qq.com> Date: Mon, 14 Jul 2025 14:04:25 +0800 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20oauth=20=E6=B7=BB=E5=8A=A0=E5=AD=97?= =?UTF-8?q?=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/src/api/Admin.ts | 232 ++++++++++++++ ui/src/api/User.ts | 215 ------------- ui/src/api/index.ts | 1 + ui/src/api/types.ts | 80 ++--- ui/src/pages/admin/adminTable.tsx | 4 +- ui/src/pages/admin/loginHistory.tsx | 2 +- ui/src/pages/auth/index.tsx | 19 +- ui/src/pages/invite/index.tsx | 22 +- ui/src/pages/login/index.tsx | 2 +- ui/src/pages/user-management/index.tsx | 25 +- .../thirdPartyLoginSettingModal.tsx | 283 +++++++++++++++--- ui/src/pages/user/dashboard/index.tsx | 7 +- ui/src/pages/user/login/index.tsx | 19 +- 13 files changed, 564 insertions(+), 347 deletions(-) create mode 100644 ui/src/api/Admin.ts diff --git a/ui/src/api/Admin.ts b/ui/src/api/Admin.ts new file mode 100644 index 0000000..4eaed3c --- /dev/null +++ b/ui/src/api/Admin.ts @@ -0,0 +1,232 @@ +/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +/* + * --------------------------------------------------------------- + * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## + * ## ## + * ## AUTHOR: acacode ## + * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## + * --------------------------------------------------------------- + */ + +import request, { ContentType, RequestParams } from "./httpClient"; +import { + DeleteDeleteAdminParams, + DomainAdminUser, + DomainCreateAdminReq, + DomainListAdminLoginHistoryResp, + DomainListAdminUserResp, + DomainLoginReq, + DomainSetting, + DomainUpdateSettingReq, + GetAdminLoginHistoryParams, + GetListAdminUserParams, + WebResp, +} from "./types"; + +/** + * @description 创建管理员 + * + * @tags Admin + * @name PostCreateAdmin + * @summary 创建管理员 + * @request POST:/api/v1/admin/create + * @response `200` `(WebResp & { + data?: DomainAdminUser, + +})` OK + */ + +export const postCreateAdmin = ( + param: DomainCreateAdminReq, + params: RequestParams = {}, +) => + request< + WebResp & { + data?: DomainAdminUser; + } + >({ + path: `/api/v1/admin/create`, + method: "POST", + body: param, + type: ContentType.Json, + format: "json", + ...params, + }); + +/** + * @description 删除管理员 + * + * @tags Admin + * @name DeleteDeleteAdmin + * @summary 删除管理员 + * @request DELETE:/api/v1/admin/delete + * @response `200` `(WebResp & { + data?: Record, + +})` OK + */ + +export const deleteDeleteAdmin = ( + query: DeleteDeleteAdminParams, + params: RequestParams = {}, +) => + request< + WebResp & { + data?: Record; + } + >({ + path: `/api/v1/admin/delete`, + method: "DELETE", + query: query, + type: ContentType.Json, + format: "json", + ...params, + }); + +/** + * @description 获取管理员用户列表 + * + * @tags Admin + * @name GetListAdminUser + * @summary 获取管理员用户列表 + * @request GET:/api/v1/admin/list + * @response `200` `(WebResp & { + data?: DomainListAdminUserResp, + +})` OK + */ + +export const getListAdminUser = ( + query: GetListAdminUserParams, + params: RequestParams = {}, +) => + request< + WebResp & { + data?: DomainListAdminUserResp; + } + >({ + path: `/api/v1/admin/list`, + method: "GET", + query: query, + type: ContentType.Json, + format: "json", + ...params, + }); + +/** + * @description 管理员登录 + * + * @tags Admin + * @name PostAdminLogin + * @summary 管理员登录 + * @request POST:/api/v1/admin/login + * @response `200` `(WebResp & { + data?: DomainAdminUser, + +})` OK + */ + +export const postAdminLogin = ( + param: DomainLoginReq, + params: RequestParams = {}, +) => + request< + WebResp & { + data?: DomainAdminUser; + } + >({ + path: `/api/v1/admin/login`, + method: "POST", + body: param, + type: ContentType.Json, + format: "json", + ...params, + }); + +/** + * @description 获取管理员登录历史 + * + * @tags Admin + * @name GetAdminLoginHistory + * @summary 获取管理员登录历史 + * @request GET:/api/v1/admin/login-history + * @response `200` `(WebResp & { + data?: DomainListAdminLoginHistoryResp, + +})` OK + */ + +export const getAdminLoginHistory = ( + query: GetAdminLoginHistoryParams, + params: RequestParams = {}, +) => + request< + WebResp & { + data?: DomainListAdminLoginHistoryResp; + } + >({ + path: `/api/v1/admin/login-history`, + method: "GET", + query: query, + type: ContentType.Json, + format: "json", + ...params, + }); + +/** + * @description 获取系统设置 + * + * @tags Admin + * @name GetGetSetting + * @summary 获取系统设置 + * @request GET:/api/v1/admin/setting + * @response `200` `(WebResp & { + data?: DomainSetting, + +})` OK + */ + +export const getGetSetting = (params: RequestParams = {}) => + request< + WebResp & { + data?: DomainSetting; + } + >({ + path: `/api/v1/admin/setting`, + method: "GET", + type: ContentType.Json, + format: "json", + ...params, + }); + +/** + * @description 更新系统设置 + * + * @tags Admin + * @name PutUpdateSetting + * @summary 更新系统设置 + * @request PUT:/api/v1/admin/setting + * @response `200` `(WebResp & { + data?: DomainSetting, + +})` OK + */ + +export const putUpdateSetting = ( + param: DomainUpdateSettingReq, + params: RequestParams = {}, +) => + request< + WebResp & { + data?: DomainSetting; + } + >({ + path: `/api/v1/admin/setting`, + method: "PUT", + body: param, + type: ContentType.Json, + format: "json", + ...params, + }); diff --git a/ui/src/api/User.ts b/ui/src/api/User.ts index 8646910..7ebbd1a 100644 --- a/ui/src/api/User.ts +++ b/ui/src/api/User.ts @@ -12,25 +12,16 @@ import request, { ContentType, RequestParams } from "./httpClient"; import { - DeleteDeleteAdminParams, DeleteDeleteUserParams, - DomainAdminUser, - DomainCreateAdminReq, DomainInviteResp, - DomainListAdminLoginHistoryResp, - DomainListAdminUserResp, DomainListLoginHistoryResp, DomainListUserResp, DomainLoginReq, DomainLoginResp, DomainOAuthURLResp, DomainRegisterReq, - DomainSetting, - DomainUpdateSettingReq, DomainUpdateUserReq, DomainUser, - GetAdminLoginHistoryParams, - GetListAdminUserParams, GetListUserParams, GetLoginHistoryParams, GetUserOauthCallbackParams, @@ -38,212 +29,6 @@ import { WebResp, } from "./types"; -/** - * @description 创建管理员 - * - * @tags User - * @name PostCreateAdmin - * @summary 创建管理员 - * @request POST:/api/v1/admin/create - * @response `200` `(WebResp & { - data?: DomainAdminUser, - -})` OK - */ - -export const postCreateAdmin = ( - param: DomainCreateAdminReq, - params: RequestParams = {}, -) => - request< - WebResp & { - data?: DomainAdminUser; - } - >({ - path: `/api/v1/admin/create`, - method: "POST", - body: param, - type: ContentType.Json, - format: "json", - ...params, - }); - -/** - * @description 删除管理员 - * - * @tags User - * @name DeleteDeleteAdmin - * @summary 删除管理员 - * @request DELETE:/api/v1/admin/delete - * @response `200` `(WebResp & { - data?: Record, - -})` OK - */ - -export const deleteDeleteAdmin = ( - query: DeleteDeleteAdminParams, - params: RequestParams = {}, -) => - request< - WebResp & { - data?: Record; - } - >({ - path: `/api/v1/admin/delete`, - method: "DELETE", - query: query, - type: ContentType.Json, - format: "json", - ...params, - }); - -/** - * @description 获取管理员用户列表 - * - * @tags User - * @name GetListAdminUser - * @summary 获取管理员用户列表 - * @request GET:/api/v1/admin/list - * @response `200` `(WebResp & { - data?: DomainListAdminUserResp, - -})` OK - */ - -export const getListAdminUser = ( - query: GetListAdminUserParams, - params: RequestParams = {}, -) => - request< - WebResp & { - data?: DomainListAdminUserResp; - } - >({ - path: `/api/v1/admin/list`, - method: "GET", - query: query, - type: ContentType.Json, - format: "json", - ...params, - }); - -/** - * @description 管理员登录 - * - * @tags User - * @name PostAdminLogin - * @summary 管理员登录 - * @request POST:/api/v1/admin/login - * @response `200` `(WebResp & { - data?: DomainAdminUser, - -})` OK - */ - -export const postAdminLogin = ( - param: DomainLoginReq, - params: RequestParams = {}, -) => - request< - WebResp & { - data?: DomainAdminUser; - } - >({ - path: `/api/v1/admin/login`, - method: "POST", - body: param, - type: ContentType.Json, - format: "json", - ...params, - }); - -/** - * @description 获取管理员登录历史 - * - * @tags User - * @name GetAdminLoginHistory - * @summary 获取管理员登录历史 - * @request GET:/api/v1/admin/login-history - * @response `200` `(WebResp & { - data?: DomainListAdminLoginHistoryResp, - -})` OK - */ - -export const getAdminLoginHistory = ( - query: GetAdminLoginHistoryParams, - params: RequestParams = {}, -) => - request< - WebResp & { - data?: DomainListAdminLoginHistoryResp; - } - >({ - path: `/api/v1/admin/login-history`, - method: "GET", - query: query, - type: ContentType.Json, - format: "json", - ...params, - }); - -/** - * @description 获取系统设置 - * - * @tags User - * @name GetGetSetting - * @summary 获取系统设置 - * @request GET:/api/v1/admin/setting - * @response `200` `(WebResp & { - data?: DomainSetting, - -})` OK - */ - -export const getGetSetting = (params: RequestParams = {}) => - request< - WebResp & { - data?: DomainSetting; - } - >({ - path: `/api/v1/admin/setting`, - method: "GET", - type: ContentType.Json, - format: "json", - ...params, - }); - -/** - * @description 更新系统设置 - * - * @tags User - * @name PutUpdateSetting - * @summary 更新系统设置 - * @request PUT:/api/v1/admin/setting - * @response `200` `(WebResp & { - data?: DomainSetting, - -})` OK - */ - -export const putUpdateSetting = ( - param: DomainUpdateSettingReq, - params: RequestParams = {}, -) => - request< - WebResp & { - data?: DomainSetting; - } - >({ - path: `/api/v1/admin/setting`, - method: "PUT", - body: param, - type: ContentType.Json, - format: "json", - ...params, - }); - /** * @description 下载VSCode插件 * diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index 482f2a8..c9ce7b6 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -1,3 +1,4 @@ +export * from './Admin' export * from './Billing' export * from './Dashboard' export * from './Model' diff --git a/ui/src/api/types.ts b/ui/src/api/types.ts index b31047e..7b73905 100644 --- a/ui/src/api/types.ts +++ b/ui/src/api/types.ts @@ -185,6 +185,40 @@ export interface DomainCreateModelReq { provider?: string; } +export interface DomainCustomOAuth { + /** 自定义OAuth访问令牌URL */ + access_token_url?: string; + /** 自定义OAuth授权URL */ + authorize_url?: string; + /** 用户信息回包中的头像URL字段名` */ + avatar_field?: string; + /** 自定义客户端ID */ + client_id?: string; + /** 自定义客户端密钥 */ + client_secret?: string; + /** 用户信息回包中的邮箱字段名 */ + email_field?: string; + /** 自定义OAuth开关 */ + enable?: boolean; + /** 用户信息回包中的ID字段名 */ + id_field?: string; + /** 用户信息回包中的用户名字段名` */ + name_field?: string; + /** 自定义OAuth Scope列表 */ + scopes?: string[]; + /** 自定义OAuth用户信息URL */ + userinfo_url?: string; +} + +export interface DomainDingtalkOAuth { + /** 钉钉客户端ID */ + client_id?: string; + /** 钉钉客户端密钥 */ + client_secret?: string; + /** 钉钉OAuth开关 */ + enable?: boolean; +} + export interface DomainIPInfo { /** ASN */ asn?: string; @@ -357,24 +391,12 @@ export interface DomainRegisterReq { export interface DomainSetting { /** 创建时间 */ created_at?: number; - /** 自定义OAuth访问令牌URL */ - custom_oauth_access_token_url?: string; - /** 自定义OAuth授权URL */ - custom_oauth_authorize_url?: string; - /** 自定义OAuth客户端ID */ - custom_oauth_client_id?: string; - /** 自定义OAuth Scope列表 */ - custom_oauth_scopes?: string[]; - /** 自定义OAuth用户信息URL */ - custom_oauth_userinfo_url?: string; - /** 钉钉客户端ID */ - dingtalk_client_id?: string; + /** 自定义OAuth接入 */ + custom_oauth?: DomainCustomOAuth; + /** 钉钉OAuth接入 */ + dingtalk_oauth?: DomainDingtalkOAuth; /** 是否禁用密码登录 */ disable_password_login?: boolean; - /** 是否开启自定义OAuth */ - enable_custom_oauth?: boolean; - /** 是否开启钉钉OAuth */ - enable_dingtalk_oauth?: boolean; /** 是否开启SSO */ enable_sso?: boolean; /** 是否强制两步验证 */ @@ -461,28 +483,12 @@ export interface DomainUpdateModelReq { } export interface DomainUpdateSettingReq { - /** 自定义OAuth访问令牌URL */ - custom_oauth_access_token_url?: string; - /** 自定义OAuth授权URL */ - custom_oauth_authorize_url?: string; - /** 自定义OAuth客户端ID */ - custom_oauth_client_id?: string; - /** 自定义OAuth客户端密钥 */ - custom_oauth_client_secret?: string; - /** 自定义OAuth Scope列表 */ - custom_oauth_scopes?: string[]; - /** 自定义OAuth用户信息URL */ - custom_oauth_userinfo_url?: string; - /** 钉钉客户端ID */ - dingtalk_client_id?: string; - /** 钉钉客户端密钥 */ - dingtalk_client_secret?: string; + /** 自定义OAuth配置 */ + custom_oauth?: DomainCustomOAuth; + /** 钉钉OAuth配置 */ + dingtalk_oauth?: DomainDingtalkOAuth; /** 是否禁用密码登录 */ disable_password_login?: boolean; - /** 是否开启自定义OAuth */ - enable_custom_oauth?: boolean; - /** 是否开启钉钉OAuth */ - enable_dingtalk_oauth?: boolean; /** 是否开启SSO */ enable_sso?: boolean; /** 是否强制两步验证 */ @@ -499,6 +505,8 @@ export interface DomainUpdateUserReq { } export interface DomainUser { + /** 头像URL */ + avatar_url?: string; /** 创建时间 */ created_at?: number; /** 邮箱 */ diff --git a/ui/src/pages/admin/adminTable.tsx b/ui/src/pages/admin/adminTable.tsx index 9df63c6..310a03f 100644 --- a/ui/src/pages/admin/adminTable.tsx +++ b/ui/src/pages/admin/adminTable.tsx @@ -7,9 +7,9 @@ import { TextField, Paper, } from '@mui/material'; -import { postCreateAdmin } from '@/api/User'; +import { postCreateAdmin } from '@/api/Admin'; import { CopyToClipboard } from 'react-copy-to-clipboard'; -import { deleteDeleteAdmin, getListAdminUser } from '@/api/User'; +import { deleteDeleteAdmin, getListAdminUser } from '@/api/Admin'; import { Table, Modal, message } from '@c-x/ui'; import { ColumnsType } from '@c-x/ui/dist/Table'; import { useRequest } from 'ahooks'; diff --git a/ui/src/pages/admin/loginHistory.tsx b/ui/src/pages/admin/loginHistory.tsx index 1018254..0e3d770 100644 --- a/ui/src/pages/admin/loginHistory.tsx +++ b/ui/src/pages/admin/loginHistory.tsx @@ -3,7 +3,7 @@ import { Stack, Box } from '@mui/material'; import { Table } from '@c-x/ui'; import dayjs from 'dayjs'; import { useRequest } from 'ahooks'; -import { getAdminLoginHistory } from '@/api/User'; +import { getAdminLoginHistory } from '@/api/Admin'; import { ColumnsType } from '@c-x/ui/dist/Table'; import { DomainListAdminLoginHistoryResp } from '@/api/types'; import User from '@/components/user'; diff --git a/ui/src/pages/auth/index.tsx b/ui/src/pages/auth/index.tsx index 392723d..0ce0912 100644 --- a/ui/src/pages/auth/index.tsx +++ b/ui/src/pages/auth/index.tsx @@ -21,11 +21,13 @@ import { Icon, message } from '@c-x/ui'; import { AestheticFluidBg } from '@/assets/jsm/AestheticFluidBg.module.js'; import { useSearchParams } from 'react-router-dom'; -import { postLogin, getUserOauthSignupOrIn, getGetSetting } from '@/api/User'; +import { postLogin, getUserOauthSignupOrIn } from '@/api/User'; +import { getGetSetting } from '@/api/Admin'; import { useForm, Controller } from 'react-hook-form'; import { styled } from '@mui/material/styles'; import { useRequest } from 'ahooks'; +import { DomainSetting } from '@/api/types'; // 样式化组件 const StyledContainer = styled(Container)(({ theme }) => ({ @@ -114,8 +116,9 @@ const AuthPage = () => { const [showPassword, setShowPassword] = useState(false); const [searchParams] = useSearchParams(); - const { data: loginSetting = {} } = useRequest(getGetSetting); - + const { data: loginSetting = {} as DomainSetting } = + useRequest(getGetSetting); + const { custom_oauth = {}, dingtalk_oauth = {} } = loginSetting; const { control, handleSubmit, @@ -175,10 +178,8 @@ const AuthPage = () => { }, []); const oauthEnable = useMemo(() => { - return ( - loginSetting.enable_custom_oauth || loginSetting.enable_dingtalk_oauth - ); - }, [loginSetting]); + return custom_oauth.enable || dingtalk_oauth.enable; + }, [custom_oauth, dingtalk_oauth]); // 渲染用户名输入框 const renderUsernameField = () => ( @@ -290,7 +291,7 @@ const AuthPage = () => { 使用其他方式登录 - {loginSetting.enable_dingtalk_oauth && ( + {dingtalk_oauth.enable && ( onOauthLogin('dingtalk')} @@ -298,7 +299,7 @@ const AuthPage = () => { )} - {loginSetting.enable_custom_oauth && ( + {custom_oauth.enable && ( onOauthLogin('custom')} diff --git a/ui/src/pages/invite/index.tsx b/ui/src/pages/invite/index.tsx index b26151c..4613312 100644 --- a/ui/src/pages/invite/index.tsx +++ b/ui/src/pages/invite/index.tsx @@ -23,12 +23,10 @@ import { Divider, } from '@mui/material'; import { useRequest } from 'ahooks'; -import { - postRegister, - getUserOauthSignupOrIn, - getGetSetting, -} from '@/api/User'; +import { postRegister, getUserOauthSignupOrIn } from '@/api/User'; +import { getGetSetting } from '@/api/Admin'; import { Icon } from '@c-x/ui'; +import { DomainSetting } from '@/api/types'; import DownloadIcon from '@mui/icons-material/Download'; import MenuBookIcon from '@mui/icons-material/MenuBook'; @@ -92,7 +90,9 @@ const StyledTextField = styled(TextField)(({ theme }) => ({ const Invite = () => { const { id, step } = useParams(); const [showPassword, setShowPassword] = useState(false); - const { data: loginSetting = {} } = useRequest(getGetSetting); + const { data: loginSetting = {} as DomainSetting } = + useRequest(getGetSetting); + const { custom_oauth = {}, dingtalk_oauth = {} } = loginSetting; const { control, handleSubmit, @@ -148,10 +148,8 @@ const Invite = () => { }; const oauthEnable = useMemo(() => { - return ( - loginSetting.enable_custom_oauth || loginSetting.enable_dingtalk_oauth - ); - }, [loginSetting]); + return custom_oauth.enable || dingtalk_oauth.enable; + }, [custom_oauth, dingtalk_oauth]); const oauthLogin = () => { return ( @@ -159,7 +157,7 @@ const Invite = () => { 使用以下方式注册 - {loginSetting.enable_dingtalk_oauth && ( + {dingtalk_oauth.enable && ( )} - {loginSetting.enable_custom_oauth && ( + {custom_oauth.enable && ( onOauthLogin('custom')} diff --git a/ui/src/pages/login/index.tsx b/ui/src/pages/login/index.tsx index 7e3b548..6a2263c 100644 --- a/ui/src/pages/login/index.tsx +++ b/ui/src/pages/login/index.tsx @@ -12,7 +12,7 @@ import { InputAdornment, IconButton, } from '@mui/material'; -import { postAdminLogin } from '@/api/User'; +import { postAdminLogin } from '@/api/Admin'; import { useForm, Controller } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { styled } from '@mui/material/styles'; diff --git a/ui/src/pages/user-management/index.tsx b/ui/src/pages/user-management/index.tsx index 89d149d..b5db601 100644 --- a/ui/src/pages/user-management/index.tsx +++ b/ui/src/pages/user-management/index.tsx @@ -8,9 +8,8 @@ import { Button, Box, } from '@mui/material'; -import { Icon, Modal } from '@c-x/ui'; import { useRequest } from 'ahooks'; -import { getGetSetting, putUpdateSetting } from '@/api/User'; +import { getGetSetting, putUpdateSetting } from '@/api/Admin'; import MemberManage from './memberManage'; import LoginHistory from './loginHistory'; import { message } from '@c-x/ui'; @@ -30,11 +29,11 @@ const StyledLabel = styled('div')(({ theme }) => ({ color: theme.vars.palette.text.primary, })); -const OAUTH_LOGIN_TYPE_KEYS = ['enable_custom_oauth', 'enable_dingtalk_oauth']; +const OAUTH_LOGIN_TYPE_KEYS = ['dingtalk_oauth', 'custom_oauth']; const OAUTH_LOGIN_TYPE_LABELS = { - enable_custom_oauth: '已开启 OAuth 登录', - enable_dingtalk_oauth: '已开启钉钉登录', + custom_oauth: '已开启 OAuth 登录', + dingtalk_oauth: '已开启钉钉登录', }; type OAUTH_LOGIN_TYPE_KEYS = keyof typeof OAUTH_LOGIN_TYPE_LABELS; @@ -42,16 +41,7 @@ type OAUTH_LOGIN_TYPE_KEYS = keyof typeof OAUTH_LOGIN_TYPE_LABELS; const User = () => { const [thirdPartyLoginSettingModalOpen, setThirdPartyLoginSettingModalOpen] = useState(false); - const { - data = { - enable_sso: false, - force_two_factor_auth: false, - disable_password_login: false, - enable_dingtalk_oauth: false, - enable_custom_oauth: false, - }, - refresh, - } = useRequest(getGetSetting); + const { data, refresh } = useRequest(getGetSetting); const { runAsync: updateSetting } = useRequest(putUpdateSetting, { manual: true, @@ -62,8 +52,9 @@ const User = () => { }); const oauthLabel = useMemo(() => { + if (!data) return '未开启'; const key = OAUTH_LOGIN_TYPE_KEYS.find( - (key) => data[key as OAUTH_LOGIN_TYPE_KEYS] + (key) => data[key as OAUTH_LOGIN_TYPE_KEYS]?.enable ); return key ? OAUTH_LOGIN_TYPE_LABELS[key as OAUTH_LOGIN_TYPE_KEYS] @@ -127,7 +118,7 @@ const User = () => { setThirdPartyLoginSettingModalOpen(false)} - settingData={data} + settingData={data || {}} onOk={() => { refresh(); }} diff --git a/ui/src/pages/user-management/thirdPartyLoginSettingModal.tsx b/ui/src/pages/user-management/thirdPartyLoginSettingModal.tsx index 427990d..866bc77 100644 --- a/ui/src/pages/user-management/thirdPartyLoginSettingModal.tsx +++ b/ui/src/pages/user-management/thirdPartyLoginSettingModal.tsx @@ -1,9 +1,16 @@ -import { Button, Radio, Stack, Box, TextField } from '@mui/material'; -import { Modal, Icon, message } from '@c-x/ui'; +import { + Button, + Radio, + Stack, + TextField, + Autocomplete, + Chip, +} from '@mui/material'; +import { Modal, message } from '@c-x/ui'; import { useState, useEffect } from 'react'; import { useForm, Controller } from 'react-hook-form'; import { FormItem } from '@/components/form'; -import { putUpdateSetting } from '@/api/User'; +import { putUpdateSetting } from '@/api/Admin'; import { DomainSetting, DomainUpdateSettingReq } from '@/api/types'; type LoginType = 'dingding' | 'wechat' | 'feishu' | 'oauth' | 'none'; @@ -59,22 +66,33 @@ const ThirdPartyLoginSettingModal = ({ control, handleSubmit, reset, + watch, formState: { errors }, } = useForm({ defaultValues: { dingtalk_client_id: '', dingtalk_client_secret: '', - custom_oauth_access_token_url: '', - custom_oauth_authorize_url: '', - custom_oauth_client_id: '', - custom_oauth_client_secret: '', + access_token_url: '', + authorize_url: '', + client_id: '', + client_secret: '', + id_field: '', + name_field: '', + scopes: [] as string[], + avatar_field: '', + userinfo_url: '', + email_field: '', }, }); const [loginType, setLoginType] = useState( - settingData?.enable_dingtalk_oauth ? 'dingding' : 'none' + settingData?.dingtalk_oauth?.enable ? 'dingding' : 'none' ); + const [scopeInputValue, setScopeInputValue] = useState(''); + + const userInfoUrl = watch('userinfo_url'); + useEffect(() => { if (open) { reset(); @@ -82,25 +100,31 @@ const ThirdPartyLoginSettingModal = ({ }, [open]); useEffect(() => { - if (settingData?.enable_dingtalk_oauth) { + if (settingData?.dingtalk_oauth?.enable) { setLoginType('dingding'); reset( { - dingtalk_client_id: settingData.dingtalk_client_id, + dingtalk_client_id: settingData.dingtalk_oauth.client_id, + dingtalk_client_secret: settingData.dingtalk_oauth.client_secret, }, { keepValues: true, } ); } - if (settingData?.enable_custom_oauth) { + if (settingData?.custom_oauth?.enable) { setLoginType('oauth'); reset( { - custom_oauth_access_token_url: - settingData.custom_oauth_access_token_url, - custom_oauth_authorize_url: settingData.custom_oauth_authorize_url, - custom_oauth_client_id: settingData.custom_oauth_client_id, + access_token_url: settingData.custom_oauth.access_token_url, + authorize_url: settingData.custom_oauth.authorize_url, + client_id: settingData.custom_oauth.client_id, + id_field: settingData.custom_oauth.id_field, + name_field: settingData.custom_oauth.name_field, + scopes: settingData.custom_oauth.scopes || [], + avatar_field: settingData.custom_oauth.avatar_field, + userinfo_url: settingData.custom_oauth.userinfo_url, + email_field: settingData.custom_oauth.email_field, }, { keepValues: true, @@ -113,24 +137,42 @@ const ThirdPartyLoginSettingModal = ({ let params: DomainUpdateSettingReq = {}; if (loginType === 'none') { params = { - enable_dingtalk_oauth: false, - enable_custom_oauth: false, + dingtalk_oauth: { + enable: false, + }, + custom_oauth: { + enable: false, + }, }; } else if (loginType === 'dingding') { params = { - enable_dingtalk_oauth: true, - enable_custom_oauth: false, - dingtalk_client_id: data.dingtalk_client_id, - dingtalk_client_secret: data.dingtalk_client_secret, + dingtalk_oauth: { + enable: true, + client_id: data.dingtalk_client_id, + client_secret: data.dingtalk_client_secret, + }, + custom_oauth: { + enable: false, + }, }; } else if (loginType === 'oauth') { params = { - enable_custom_oauth: true, - enable_dingtalk_oauth: false, - custom_oauth_access_token_url: data.custom_oauth_access_token_url, - custom_oauth_authorize_url: data.custom_oauth_authorize_url, - custom_oauth_client_id: data.custom_oauth_client_id, - custom_oauth_client_secret: data.custom_oauth_client_secret, + dingtalk_oauth: { + enable: false, + }, + custom_oauth: { + enable: true, + access_token_url: data.access_token_url, + authorize_url: data.authorize_url, + client_id: data.client_id, + client_secret: data.client_secret, + id_field: data.id_field, + name_field: data.name_field, + scopes: data.scopes, + avatar_field: data.avatar_field, + userinfo_url: data.userinfo_url, + email_field: data.email_field, + }, }; } @@ -201,15 +243,15 @@ const ThirdPartyLoginSettingModal = ({ rules={{ required: 'Access Token URL 不能为空', }} - name='custom_oauth_access_token_url' + name='access_token_url' render={({ field }) => ( )} /> @@ -217,7 +259,7 @@ const ThirdPartyLoginSettingModal = ({ )} /> @@ -236,7 +278,7 @@ const ThirdPartyLoginSettingModal = ({ )} /> @@ -255,7 +297,7 @@ const ThirdPartyLoginSettingModal = ({ + )} + /> + + + + { + if (value.length === 0) { + return 'Scope 不能为空'; + } + return true; + }, + }} + render={({ field }) => ( + { + field.onChange(value); + }} + onInputChange={(_, value) => { + setScopeInputValue(value); + }} + size='small' + freeSolo + renderTags={(value: readonly string[], getTagProps) => + value.map((option: string, index: number) => { + const { key, ...tagProps } = getTagProps({ index }); + const label = `${option}`; + return ( + + ); + }) + } + renderInput={(params) => ( + { + // 失去焦点时自动添加当前输入的值 + const trimmedValue = scopeInputValue.trim(); + if (trimmedValue && !field.value.includes(trimmedValue)) { + field.onChange([...field.value, trimmedValue]); + // 清空输入框 + setScopeInputValue(''); + } + }} + /> + )} + /> + )} + /> + + + ( + )} /> + {userInfoUrl && ( + <> + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + + )} ); }; diff --git a/ui/src/pages/user/dashboard/index.tsx b/ui/src/pages/user/dashboard/index.tsx index 015ff98..9185d88 100644 --- a/ui/src/pages/user/dashboard/index.tsx +++ b/ui/src/pages/user/dashboard/index.tsx @@ -64,12 +64,7 @@ const Dashboard = () => { - + ); }; diff --git a/ui/src/pages/user/login/index.tsx b/ui/src/pages/user/login/index.tsx index 6cbd09f..0e2184a 100644 --- a/ui/src/pages/user/login/index.tsx +++ b/ui/src/pages/user/login/index.tsx @@ -22,11 +22,13 @@ import { getRedirectUrl } from '@/utils'; import { AestheticFluidBg } from '@/assets/jsm/AestheticFluidBg.module.js'; import { useSearchParams } from 'react-router-dom'; -import { postLogin, getUserOauthSignupOrIn, getGetSetting } from '@/api/User'; +import { postLogin, getUserOauthSignupOrIn } from '@/api/User'; +import { getGetSetting } from '@/api/Admin'; import { useForm, Controller } from 'react-hook-form'; import { styled } from '@mui/material/styles'; import { useRequest } from 'ahooks'; +import { DomainSetting } from '@/api/types'; // 样式化组件 const StyledContainer = styled(Container)(({ theme }) => ({ @@ -115,8 +117,9 @@ const UserLogin = () => { const [showPassword, setShowPassword] = useState(false); const [searchParams] = useSearchParams(); - const { data: loginSetting = {} } = useRequest(getGetSetting); - + const { data: loginSetting = {} as DomainSetting } = + useRequest(getGetSetting); + const { custom_oauth = {}, dingtalk_oauth = {} } = loginSetting; const { control, handleSubmit, @@ -166,10 +169,8 @@ const UserLogin = () => { }, []); const oauthEnable = useMemo(() => { - return ( - loginSetting.enable_custom_oauth || loginSetting.enable_dingtalk_oauth - ); - }, [loginSetting]); + return custom_oauth.enable || dingtalk_oauth.enable; + }, [custom_oauth, dingtalk_oauth]); // 渲染用户名输入框 const renderUsernameField = () => ( @@ -280,7 +281,7 @@ const UserLogin = () => { 使用其他方式登录 - {loginSetting.enable_dingtalk_oauth && ( + {dingtalk_oauth.enable && ( onOauthLogin('dingtalk')} @@ -288,7 +289,7 @@ const UserLogin = () => { )} - {loginSetting.enable_custom_oauth && ( + {custom_oauth.enable && ( onOauthLogin('custom')} From 586372f4c57b2713b97bb1744a9d2fcfcf4ad48f Mon Sep 17 00:00:00 2001 From: Gavan <994259213@qq.com> Date: Mon, 14 Jul 2025 14:50:14 +0800 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20=E4=BF=AE=E6=94=B9=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/src/pages/auth/index.tsx | 6 ++++-- ui/src/pages/invite/index.tsx | 6 ++++-- ui/src/pages/user/login/index.tsx | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/ui/src/pages/auth/index.tsx b/ui/src/pages/auth/index.tsx index 0ce0912..e611418 100644 --- a/ui/src/pages/auth/index.tsx +++ b/ui/src/pages/auth/index.tsx @@ -178,8 +178,10 @@ const AuthPage = () => { }, []); const oauthEnable = useMemo(() => { - return custom_oauth.enable || dingtalk_oauth.enable; - }, [custom_oauth, dingtalk_oauth]); + return ( + loginSetting.custom_oauth?.enable || loginSetting.dingtalk_oauth?.enable + ); + }, [loginSetting]); // 渲染用户名输入框 const renderUsernameField = () => ( diff --git a/ui/src/pages/invite/index.tsx b/ui/src/pages/invite/index.tsx index 4613312..9dc76b7 100644 --- a/ui/src/pages/invite/index.tsx +++ b/ui/src/pages/invite/index.tsx @@ -148,8 +148,10 @@ const Invite = () => { }; const oauthEnable = useMemo(() => { - return custom_oauth.enable || dingtalk_oauth.enable; - }, [custom_oauth, dingtalk_oauth]); + return ( + loginSetting.custom_oauth?.enable || loginSetting.dingtalk_oauth?.enable + ); + }, [loginSetting]); const oauthLogin = () => { return ( diff --git a/ui/src/pages/user/login/index.tsx b/ui/src/pages/user/login/index.tsx index 0e2184a..c47141d 100644 --- a/ui/src/pages/user/login/index.tsx +++ b/ui/src/pages/user/login/index.tsx @@ -169,8 +169,10 @@ const UserLogin = () => { }, []); const oauthEnable = useMemo(() => { - return custom_oauth.enable || dingtalk_oauth.enable; - }, [custom_oauth, dingtalk_oauth]); + return ( + loginSetting.custom_oauth?.enable || loginSetting.dingtalk_oauth?.enable + ); + }, [loginSetting]); // 渲染用户名输入框 const renderUsernameField = () => ( From f5a1c92cd84a85a3d91ed281b58e5650fba68ab5 Mon Sep 17 00:00:00 2001 From: Gavan <994259213@qq.com> Date: Mon, 14 Jul 2025 15:07:50 +0800 Subject: [PATCH 7/7] =?UTF-8?q?ci:=20=E4=BF=AE=E6=94=B9=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=20node=20=E5=92=8C=20pnpm=20=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/frontend-ci-cd.yml | 8 ++++---- ui/Makefile | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 ui/Makefile diff --git a/.github/workflows/frontend-ci-cd.yml b/.github/workflows/frontend-ci-cd.yml index 3980766..e94c03b 100644 --- a/.github/workflows/frontend-ci-cd.yml +++ b/.github/workflows/frontend-ci-cd.yml @@ -3,7 +3,7 @@ name: Frontend CI/CD on: push: tags: - - "v[0-9]+.[0-9]+.[0-9]+*" + - 'v[0-9]+.[0-9]+.[0-9]+*' paths: - 'ui/**' - '.github/workflows/frontend-ci-cd.yml' @@ -29,12 +29,12 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '20.19.0' - name: Set up pnpm uses: pnpm/action-setup@v2 with: - version: 8 + version: 10.12.1 - name: Get version id: get_version @@ -130,4 +130,4 @@ jobs: ${{ env.REGISTRY }}/frontend:${{ needs.build.outputs.version }} ${{ env.REGISTRY }}/frontend:latest cache-from: type=gha - cache-to: type=gha,mode=max \ No newline at end of file + cache-to: type=gha,mode=max diff --git a/ui/Makefile b/ui/Makefile new file mode 100644 index 0000000..59557ef --- /dev/null +++ b/ui/Makefile @@ -0,0 +1,21 @@ +PLATFORM=linux/amd64 +TAG=main +REGISTRY=monkeycode + + +# 构建前端代码 +build: + pnpm run build + +# 构建并加载到本地Docker +image: build + docker buildx build \ + -f .Dockerfile \ + --platform ${PLATFORM} \ + --tag ${REGISTRY}/frontend:${TAG} \ + --load \ + . + +save: image + docker save -o /tmp/monkeycode_frontend.tar monkeycode/frontend:main + \ No newline at end of file pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy