206 lines
7.7 KiB
Go
206 lines
7.7 KiB
Go
package adminweb
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.zheromiku.top/Weknora/dobotai-platform/internal/dingtalkconfig"
|
|
"gitea.zheromiku.top/Weknora/dobotai-platform/internal/secretstore"
|
|
"gitea.zheromiku.top/Weknora/dobotai-platform/internal/settings"
|
|
)
|
|
|
|
type ConfigRepository interface {
|
|
ActiveDingTalkConfig(context.Context, string) (string, string, bool, error)
|
|
CandidateDingTalkConfig(context.Context, string) (string, string, error)
|
|
SaveDingTalkConfig(context.Context, string, string, string, bool) error
|
|
MarkDingTalkCandidateFailed(context.Context, string, string, string) error
|
|
AdministratorCount(context.Context, string) (int, error)
|
|
ActiveBindings(context.Context, string) (map[string]settings.ResourceBinding, error)
|
|
CreateBootstrapChallenge(context.Context, string, string, time.Time) error
|
|
ConfirmBootstrap(context.Context, string) error
|
|
}
|
|
type AdministratorRepository interface {
|
|
ListAdministrators(context.Context, string) ([]settings.Administrator, error)
|
|
DisableAdministrator(context.Context, string, string) error
|
|
}
|
|
type CandidateActivator interface{ ActivateCandidate(context.Context) error }
|
|
type OperationalStatusProvider interface {
|
|
Components(context.Context) ([]Component, error)
|
|
}
|
|
type CredentialVerifier interface {
|
|
Verify(context.Context, string, string) dingtalkconfig.Result
|
|
}
|
|
type ControlService struct {
|
|
tenant string
|
|
repo ConfigRepository
|
|
secrets *secretstore.Store
|
|
verifier CredentialVerifier
|
|
activator CandidateActivator
|
|
status OperationalStatusProvider
|
|
}
|
|
|
|
func NewControlService(tenant string, repo ConfigRepository, secrets *secretstore.Store, verifier CredentialVerifier) (*ControlService, error) {
|
|
if strings.TrimSpace(tenant) == "" || repo == nil || secrets == nil || verifier == nil {
|
|
return nil, errors.New("admin control dependencies are required")
|
|
}
|
|
return &ControlService{tenant: strings.TrimSpace(tenant), repo: repo, secrets: secrets, verifier: verifier}, nil
|
|
}
|
|
func (s *ControlService) Snapshot(ctx context.Context) (Snapshot, error) {
|
|
id, ref, verified, err := s.repo.ActiveDingTalkConfig(ctx, s.tenant)
|
|
if err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
admins, err := s.repo.AdministratorCount(ctx, s.tenant)
|
|
if err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
bindings, err := s.repo.ActiveBindings(ctx, s.tenant)
|
|
if err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
administratorViews := []AdministratorView{}
|
|
if administratorRepo, ok := s.repo.(AdministratorRepository); ok {
|
|
administrators, listErr := administratorRepo.ListAdministrators(ctx, s.tenant)
|
|
if listErr != nil {
|
|
return Snapshot{}, listErr
|
|
}
|
|
activeCount := 0
|
|
for _, administrator := range administrators {
|
|
if administrator.Status == "active" {
|
|
activeCount++
|
|
}
|
|
}
|
|
for _, administrator := range administrators {
|
|
administratorViews = append(administratorViews, AdministratorView{
|
|
StaffID: administrator.StaffID, StaffIDMasked: maskID(administrator.StaffID),
|
|
DisplayName: administrator.DisplayName, Department: administrator.Department,
|
|
Role: administrator.Role, Status: administrator.Status,
|
|
CanDisable: administrator.Status == "active" && activeCount > 1,
|
|
})
|
|
}
|
|
}
|
|
state := settings.DetermineState(settings.Snapshot{ClientID: id, SecretConfigured: s.secrets.Configured(ref), CredentialsVerified: verified, AdministratorCount: admins})
|
|
now := time.Now()
|
|
components := []Component{
|
|
{Name: "配置仓库", State: "ok", Code: "repository_ok", Detail: "PostgreSQL配置查询成功", LastSuccess: now, CheckedAt: now},
|
|
{Name: "钉钉OAuth", State: status(verified), Code: ternaryCode(verified, "oauth_verified", "oauth_unconfigured"), Detail: "当前应用凭据验证状态", CheckedAt: now},
|
|
{Name: "管理员", State: countStatus(admins), Code: ternaryCode(admins > 0, "admin_configured", "admin_missing"), Detail: itoa(admins) + " 位有效管理员", CheckedAt: now},
|
|
{Name: "技术支持群绑定", State: bindingStatus(bindings, "support_group"), Code: bindingCode(bindings, "support_group"), Detail: "仅显示是否存在active绑定", CheckedAt: now},
|
|
{Name: "问题AI表格绑定", State: bindingStatus(bindings, "question_table"), Code: bindingCode(bindings, "question_table"), Detail: "仅显示是否存在active绑定", CheckedAt: now},
|
|
{Name: "FAQ表格绑定", State: bindingStatus(bindings, "faq_table"), Code: bindingCode(bindings, "faq_table"), Detail: "仅显示是否存在active绑定", CheckedAt: now},
|
|
}
|
|
if s.status != nil {
|
|
operational, probeErr := s.status.Components(ctx)
|
|
if probeErr != nil {
|
|
components = append(components, Component{Name: "运行探针", State: "error", Code: "component_probe_failed", Detail: "运行状态探针失败;未显示原始错误", CheckedAt: time.Now()})
|
|
} else {
|
|
components = append(components, operational...)
|
|
}
|
|
}
|
|
return Snapshot{SystemState: string(state), ClientIDMasked: maskID(id), SecretConfigured: s.secrets.Configured(ref), Components: components, Administrators: administratorViews}, nil
|
|
}
|
|
func (s *ControlService) VerifyCredentials(ctx context.Context, id, secret string) Verification {
|
|
r := s.verifier.Verify(ctx, id, secret)
|
|
return Verification{OK: r.OK, Code: r.Code, CheckedAt: r.CheckedAt}
|
|
}
|
|
func (s *ControlService) SaveCredentials(ctx context.Context, id, secret string) error {
|
|
_, oldRef, _ := s.repo.CandidateDingTalkConfig(ctx, s.tenant)
|
|
ref, err := s.secrets.Put("dingtalk", secret)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = s.repo.SaveDingTalkConfig(ctx, s.tenant, strings.TrimSpace(id), ref, false); err != nil {
|
|
_ = s.secrets.Delete(ref)
|
|
return err
|
|
}
|
|
if oldRef != "" && oldRef != ref {
|
|
_ = s.secrets.Delete(oldRef)
|
|
}
|
|
return nil
|
|
}
|
|
func (s *ControlService) SetCandidateActivator(activator CandidateActivator) { s.activator = activator }
|
|
func (s *ControlService) SetOperationalStatusProvider(provider OperationalStatusProvider) {
|
|
s.status = provider
|
|
}
|
|
func (s *ControlService) ActivateCandidate(ctx context.Context) error {
|
|
if s.activator == nil {
|
|
return errors.New("candidate activator unavailable")
|
|
}
|
|
_, ref, err := s.repo.CandidateDingTalkConfig(ctx, s.tenant)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = s.activator.ActivateCandidate(ctx); err != nil {
|
|
_ = s.repo.MarkDingTalkCandidateFailed(ctx, s.tenant, ref, "activation_failed")
|
|
return errors.New("candidate activation failed")
|
|
}
|
|
return nil
|
|
}
|
|
func (s *ControlService) CreateBootstrap(ctx context.Context, code string) error {
|
|
return s.repo.CreateBootstrapChallenge(ctx, s.tenant, code, time.Now().Add(10*time.Minute))
|
|
}
|
|
func (s *ControlService) ConfirmBootstrap(ctx context.Context) error {
|
|
return s.repo.ConfirmBootstrap(ctx, s.tenant)
|
|
}
|
|
func (s *ControlService) DisableAdministrator(ctx context.Context, staffID string) error {
|
|
staffID = strings.TrimSpace(staffID)
|
|
if staffID == "" {
|
|
return settings.ErrAdministratorNotFound
|
|
}
|
|
repo, ok := s.repo.(AdministratorRepository)
|
|
if !ok {
|
|
return errors.New("administrator management unavailable")
|
|
}
|
|
return repo.DisableAdministrator(ctx, s.tenant, staffID)
|
|
}
|
|
func maskID(v string) string {
|
|
v = strings.TrimSpace(v)
|
|
if len(v) <= 6 {
|
|
return v
|
|
}
|
|
return v[:3] + "***" + v[len(v)-3:]
|
|
}
|
|
func status(ok bool) string {
|
|
if ok {
|
|
return "ok"
|
|
}
|
|
return "unconfigured"
|
|
}
|
|
func countStatus(n int) string {
|
|
if n > 0 {
|
|
return "ok"
|
|
}
|
|
return "unconfigured"
|
|
}
|
|
func bindingStatus(m map[string]settings.ResourceBinding, k string) string {
|
|
if _, ok := m[k]; ok {
|
|
return "configured"
|
|
}
|
|
return "unconfigured"
|
|
}
|
|
func bindingCode(m map[string]settings.ResourceBinding, k string) string {
|
|
if _, ok := m[k]; ok {
|
|
return "binding_active"
|
|
}
|
|
return "binding_missing"
|
|
}
|
|
func ternaryCode(ok bool, success, failure string) string {
|
|
if ok {
|
|
return success
|
|
}
|
|
return failure
|
|
}
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
b := make([]byte, 0, 8)
|
|
for n > 0 {
|
|
b = append([]byte{byte('0' + n%10)}, b...)
|
|
n /= 10
|
|
}
|
|
return string(b)
|
|
}
|