package service import ( "context" "fmt" "strings" ) // OAuth provider 配置键;client secret 以 AES-GCM 密文落库。 const ( settingOauthOidcIssuer = "oauth_oidc_issuer" settingOauthOidcClientID = "oauth_oidc_client_id" settingOauthOidcClientSecret = "oauth_oidc_client_secret" settingOauthOidcDisplayName = "oauth_oidc_display_name" settingOauthOidcDisabled = "oauth_oidc_disabled" settingOauthGithubClientID = "oauth_github_client_id" settingOauthGithubClientSecret = "oauth_github_client_secret" settingOauthGithubDisplayName = "oauth_github_display_name" settingOauthGithubDisabled = "oauth_github_disabled" ) // OAuthProvidersView 是 OAuth provider 配置视图;绝不返回 secret 明文。 type OAuthProvidersView struct { OidcIssuer string `json:"oidcIssuer"` OidcClientID string `json:"oidcClientId"` OidcSecretSet bool `json:"oidcSecretSet"` OidcDisplayName string `json:"oidcDisplayName"` OidcDisabled bool `json:"oidcDisabled"` GithubClientID string `json:"githubClientId"` GithubSecretSet bool `json:"githubSecretSet"` GithubDisplayName string `json:"githubDisplayName"` GithubDisabled bool `json:"githubDisabled"` } // UpdateOAuthInput 是 provider 配置的部分更新:所有字段 nil=沿用已存值, // 只落库出现的字段;并发编辑不同 provider 因此互不回滚(2026-07-22 审查 #18)。 // secret 非 nil 覆盖(空串清除),绝不回读。 type UpdateOAuthInput struct { OidcIssuer *string `json:"oidcIssuer"` OidcClientID *string `json:"oidcClientId"` OidcClientSecret *string `json:"oidcClientSecret"` OidcDisplayName *string `json:"oidcDisplayName"` OidcDisabled *bool `json:"oidcDisabled"` GithubClientID *string `json:"githubClientId"` GithubClientSecret *string `json:"githubClientSecret"` GithubDisplayName *string `json:"githubDisplayName"` GithubDisabled *bool `json:"githubDisabled"` } // OAuthView 返回脱敏后的 provider 配置。 func (s *SettingService) OAuthView(ctx context.Context) (OAuthProvidersView, error) { var view OAuthProvidersView vals, err := s.getMany(ctx, settingOauthOidcIssuer, settingOauthOidcClientID, settingOauthOidcClientSecret, settingOauthOidcDisplayName, settingOauthOidcDisabled, settingOauthGithubClientID, settingOauthGithubClientSecret, settingOauthGithubDisplayName, settingOauthGithubDisabled) if err != nil { return view, err } view.OidcIssuer = vals[settingOauthOidcIssuer] view.OidcClientID = vals[settingOauthOidcClientID] view.OidcSecretSet = vals[settingOauthOidcClientSecret] != "" view.OidcDisplayName = vals[settingOauthOidcDisplayName] view.OidcDisabled = vals[settingOauthOidcDisabled] == "1" view.GithubClientID = vals[settingOauthGithubClientID] view.GithubSecretSet = vals[settingOauthGithubClientSecret] != "" view.GithubDisplayName = vals[settingOauthGithubDisplayName] view.GithubDisabled = vals[settingOauthGithubDisabled] == "1" return view, nil } // boolFlag 把开关序列化为 settings 存储值。 func boolFlag(on bool) string { if on { return "1" } return "" } // trimPtr / issuerPtr / flagPtr 把补丁字段规范化为存储值;nil 表示未出现不写。 func trimPtr(p *string) *string { if p == nil { return nil } v := strings.TrimSpace(*p) return &v } func issuerPtr(p *string) *string { if p == nil { return nil } v := strings.TrimRight(strings.TrimSpace(*p), "/") return &v } func flagPtr(p *bool) *string { if p == nil { return nil } v := boolFlag(*p) return &v } // UpdateOAuth 部分更新 provider 配置:只落库非 nil 字段; // issuer 规范化去尾斜杠,secret 加密落库(空串清除)。 func (s *SettingService) UpdateOAuth(ctx context.Context, in UpdateOAuthInput) error { writes := []struct { key string val *string }{ {settingOauthOidcIssuer, issuerPtr(in.OidcIssuer)}, {settingOauthOidcClientID, trimPtr(in.OidcClientID)}, {settingOauthOidcDisplayName, trimPtr(in.OidcDisplayName)}, {settingOauthOidcDisabled, flagPtr(in.OidcDisabled)}, {settingOauthGithubClientID, trimPtr(in.GithubClientID)}, {settingOauthGithubDisplayName, trimPtr(in.GithubDisplayName)}, {settingOauthGithubDisabled, flagPtr(in.GithubDisabled)}, } for _, w := range writes { if w.val == nil { continue } if err := s.set(ctx, w.key, *w.val); err != nil { return err } } if err := s.saveOAuthSecret(ctx, settingOauthOidcClientSecret, in.OidcClientSecret); err != nil { return err } return s.saveOAuthSecret(ctx, settingOauthGithubClientSecret, in.GithubClientSecret) } // saveOAuthSecret 加密保存 secret;nil 沿用,空串清除。 func (s *SettingService) saveOAuthSecret(ctx context.Context, key string, secret *string) error { if secret == nil { return nil } if *secret == "" { return s.set(ctx, key, "") } enc, err := s.cipher.EncryptString(*secret) if err != nil { return fmt.Errorf("encrypt oauth secret: %w", err) } return s.set(ctx, key, enc) } // oauthClient 返回 provider 的 clientID/明文 secret/issuer(仅 oidc);未配置时 clientID 为空。 func (s *SettingService) oauthClient(ctx context.Context, provider string) (clientID, secret, issuer string, err error) { idKey, secKey := settingOauthGithubClientID, settingOauthGithubClientSecret if provider == "oidc" { idKey, secKey = settingOauthOidcClientID, settingOauthOidcClientSecret if issuer, err = s.get(ctx, settingOauthOidcIssuer); err != nil { return } } if clientID, err = s.get(ctx, idKey); err != nil { return } enc, err := s.get(ctx, secKey) if err != nil || enc == "" { return } if secret, err = s.cipher.DecryptString(enc); err != nil { err = fmt.Errorf("decrypt oauth secret: %w", err) } return } // oauthProviderMeta 返回 provider 的展示名(空则给默认名)与禁用态。 func (s *SettingService) oauthProviderMeta(ctx context.Context, provider string) (display string, disabled bool, err error) { nameKey, offKey, def := settingOauthGithubDisplayName, settingOauthGithubDisabled, "GitHub" if provider == "oidc" { nameKey, offKey, def = settingOauthOidcDisplayName, settingOauthOidcDisabled, "OIDC 单点登录" } if display, err = s.get(ctx, nameKey); err != nil { return } if display == "" { display = def } off, err := s.get(ctx, offKey) disabled = off == "1" return }