mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
fix(web/config): persist Discord token updates from channel settings (#2024)
* fix: save Discord token updates from channel settings - preserve secret fields from PUT/PATCH /api/config payloads via setters - include _token edit fields in channel save payload construction - add regression test for Discord token patch flow (issue #2005) * fix: resolve shadow lint warnings in config secret mapping * fix(web/api): adapt config secret patch path after #2068 --------- Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
This commit is contained in:
@@ -52,6 +52,11 @@ func (h *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var raw map[string]any
|
||||
if err = json.Unmarshal(body, &raw); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if execAllowRemoteOmitted(body) {
|
||||
cfg.Tools.Exec.AllowRemote = config.DefaultConfig().Tools.Exec.AllowRemote
|
||||
}
|
||||
@@ -63,6 +68,7 @@ func (h *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, fmt.Sprintf("Failed to apply security config: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
applyConfigSecretsFromMap(&cfg, raw)
|
||||
|
||||
if errs := validateConfig(&cfg); len(errs) > 0 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -159,6 +165,7 @@ func (h *Handler) handlePatchConfig(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, fmt.Sprintf("Failed to apply security config: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
applyConfigSecretsFromMap(&newCfg, base)
|
||||
|
||||
if errs := validateConfig(&newCfg); len(errs) > 0 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -325,3 +332,146 @@ func mergeMap(dst, src map[string]any) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func asMapField(value map[string]any, key string) (map[string]any, bool) {
|
||||
raw, exists := value[key]
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
m, isMap := raw.(map[string]any)
|
||||
return m, isMap
|
||||
}
|
||||
|
||||
func getSecretString(m map[string]any, key string) (string, bool) {
|
||||
if raw, exists := m[key]; exists {
|
||||
s, isString := raw.(string)
|
||||
if isString {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
if raw, exists := m["_"+key]; exists {
|
||||
s, isString := raw.(string)
|
||||
if isString {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func applyConfigSecretsFromMap(cfg *config.Config, raw map[string]any) {
|
||||
channels, hasChannels := asMapField(raw, "channels")
|
||||
if hasChannels {
|
||||
if telegram, hasTelegram := asMapField(channels, "telegram"); hasTelegram {
|
||||
if token, hasToken := getSecretString(telegram, "token"); hasToken {
|
||||
cfg.Channels.Telegram.SetToken(token)
|
||||
}
|
||||
}
|
||||
if feishu, hasFeishu := asMapField(channels, "feishu"); hasFeishu {
|
||||
if appSecret, hasAppSecret := getSecretString(feishu, "app_secret"); hasAppSecret {
|
||||
cfg.Channels.Feishu.AppSecret.Set(appSecret)
|
||||
}
|
||||
if encryptKey, hasEncryptKey := getSecretString(feishu, "encrypt_key"); hasEncryptKey {
|
||||
cfg.Channels.Feishu.EncryptKey.Set(encryptKey)
|
||||
}
|
||||
if verificationToken, hasVerificationToken := getSecretString(
|
||||
feishu,
|
||||
"verification_token",
|
||||
); hasVerificationToken {
|
||||
cfg.Channels.Feishu.VerificationToken.Set(verificationToken)
|
||||
}
|
||||
}
|
||||
if discord, hasDiscord := asMapField(channels, "discord"); hasDiscord {
|
||||
if token, hasToken := getSecretString(discord, "token"); hasToken {
|
||||
cfg.Channels.Discord.Token.Set(token)
|
||||
}
|
||||
}
|
||||
if weixin, hasWeixin := asMapField(channels, "weixin"); hasWeixin {
|
||||
if token, hasToken := getSecretString(weixin, "token"); hasToken {
|
||||
cfg.Channels.Weixin.SetToken(token)
|
||||
}
|
||||
}
|
||||
if qq, hasQQ := asMapField(channels, "qq"); hasQQ {
|
||||
if appSecret, hasAppSecret := getSecretString(qq, "app_secret"); hasAppSecret {
|
||||
cfg.Channels.QQ.AppSecret.Set(appSecret)
|
||||
}
|
||||
}
|
||||
if dingtalk, hasDingTalk := asMapField(channels, "dingtalk"); hasDingTalk {
|
||||
if clientSecret, hasClientSecret := getSecretString(dingtalk, "client_secret"); hasClientSecret {
|
||||
cfg.Channels.DingTalk.ClientSecret.Set(clientSecret)
|
||||
}
|
||||
}
|
||||
if slack, hasSlack := asMapField(channels, "slack"); hasSlack {
|
||||
if botToken, hasBotToken := getSecretString(slack, "bot_token"); hasBotToken {
|
||||
cfg.Channels.Slack.BotToken.Set(botToken)
|
||||
}
|
||||
if appToken, hasAppToken := getSecretString(slack, "app_token"); hasAppToken {
|
||||
cfg.Channels.Slack.AppToken.Set(appToken)
|
||||
}
|
||||
}
|
||||
if matrix, hasMatrix := asMapField(channels, "matrix"); hasMatrix {
|
||||
if accessToken, hasAccessToken := getSecretString(matrix, "access_token"); hasAccessToken {
|
||||
cfg.Channels.Matrix.AccessToken.Set(accessToken)
|
||||
}
|
||||
}
|
||||
if line, hasLine := asMapField(channels, "line"); hasLine {
|
||||
if channelSecret, hasChannelSecret := getSecretString(line, "channel_secret"); hasChannelSecret {
|
||||
cfg.Channels.LINE.ChannelSecret.Set(channelSecret)
|
||||
}
|
||||
if channelAccessToken, hasChannelAccessToken := getSecretString(
|
||||
line,
|
||||
"channel_access_token",
|
||||
); hasChannelAccessToken {
|
||||
cfg.Channels.LINE.ChannelAccessToken.Set(channelAccessToken)
|
||||
}
|
||||
}
|
||||
if onebot, hasOneBot := asMapField(channels, "onebot"); hasOneBot {
|
||||
if accessToken, hasAccessToken := getSecretString(onebot, "access_token"); hasAccessToken {
|
||||
cfg.Channels.OneBot.AccessToken.Set(accessToken)
|
||||
}
|
||||
}
|
||||
if wecom, hasWeCom := asMapField(channels, "wecom"); hasWeCom {
|
||||
if secret, hasSecret := getSecretString(wecom, "secret"); hasSecret {
|
||||
cfg.Channels.WeCom.SetSecret(secret)
|
||||
}
|
||||
}
|
||||
if pico, hasPico := asMapField(channels, "pico"); hasPico {
|
||||
if token, hasToken := getSecretString(pico, "token"); hasToken {
|
||||
cfg.Channels.Pico.SetToken(token)
|
||||
}
|
||||
}
|
||||
if irc, hasIRC := asMapField(channels, "irc"); hasIRC {
|
||||
if password, hasPassword := getSecretString(irc, "password"); hasPassword {
|
||||
cfg.Channels.IRC.Password.Set(password)
|
||||
}
|
||||
if nickservPassword, hasNickservPassword := getSecretString(irc, "nickserv_password"); hasNickservPassword {
|
||||
cfg.Channels.IRC.NickServPassword.Set(nickservPassword)
|
||||
}
|
||||
if saslPassword, hasSASLPassword := getSecretString(irc, "sasl_password"); hasSASLPassword {
|
||||
cfg.Channels.IRC.SASLPassword.Set(saslPassword)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tools, hasTools := asMapField(raw, "tools")
|
||||
if !hasTools {
|
||||
return
|
||||
}
|
||||
skills, hasSkills := asMapField(tools, "skills")
|
||||
if !hasSkills {
|
||||
return
|
||||
}
|
||||
if github, hasGithub := asMapField(skills, "github"); hasGithub {
|
||||
if token, hasToken := getSecretString(github, "token"); hasToken {
|
||||
cfg.Tools.Skills.Github.Token.Set(token)
|
||||
}
|
||||
}
|
||||
registries, hasRegistries := asMapField(skills, "registries")
|
||||
if !hasRegistries {
|
||||
return
|
||||
}
|
||||
if clawHub, hasClawHub := asMapField(registries, "clawhub"); hasClawHub {
|
||||
if authToken, hasAuthToken := getSecretString(clawHub, "auth_token"); hasAuthToken {
|
||||
cfg.Tools.Skills.Registries.ClawHub.AuthToken.Set(authToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +251,42 @@ func TestHandlePatchConfig_SucceedsWhenPicoTokenInSecurityOnly(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePatchConfig_SavesDiscordTokenFromPayload(t *testing.T) {
|
||||
configPath, cleanup := setupOAuthTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
h := NewHandler(configPath)
|
||||
mux := http.NewServeMux()
|
||||
h.RegisterRoutes(mux)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/config", bytes.NewBufferString(`{
|
||||
"channels": {
|
||||
"discord": {
|
||||
"enabled": true,
|
||||
"token": "discord-test-token"
|
||||
}
|
||||
}
|
||||
}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("PATCH /api/config status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
|
||||
cfg, err := config.LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
if !cfg.Channels.Discord.Enabled {
|
||||
t.Fatal("discord should be enabled after PATCH")
|
||||
}
|
||||
if got := cfg.Channels.Discord.Token.String(); got != "discord-test-token" {
|
||||
t.Fatalf("discord token = %q, want %q", got, "discord-test-token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePatchConfig_AllowsInvalidDenyRegexPatternsWhenDenyPatternsDisabled(t *testing.T) {
|
||||
configPath, cleanup := setupOAuthTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -62,10 +62,8 @@ function asBool(value: unknown): boolean {
|
||||
|
||||
function buildEditConfig(config: ChannelConfig): ChannelConfig {
|
||||
const edit: ChannelConfig = { ...config }
|
||||
for (const secretKey of Object.keys(SECRET_FIELD_MAP)) {
|
||||
if (secretKey in config) {
|
||||
edit[SECRET_FIELD_MAP[secretKey]] = ""
|
||||
}
|
||||
for (const editKey of Object.values(SECRET_FIELD_MAP)) {
|
||||
edit[editKey] = ""
|
||||
}
|
||||
return edit
|
||||
}
|
||||
@@ -94,17 +92,22 @@ function buildSavePayload(
|
||||
for (const [key, value] of Object.entries(editConfig)) {
|
||||
if (key.startsWith("_")) continue
|
||||
if (key === "enabled") continue
|
||||
|
||||
if (key in SECRET_FIELD_MAP) {
|
||||
const editKey = SECRET_FIELD_MAP[key]
|
||||
const incoming = asString(editConfig[editKey])
|
||||
payload[key] = incoming !== "" ? incoming : value
|
||||
continue
|
||||
}
|
||||
if (key in SECRET_FIELD_MAP) continue
|
||||
|
||||
payload[key] = value
|
||||
}
|
||||
|
||||
for (const [secretKey, editKey] of Object.entries(SECRET_FIELD_MAP)) {
|
||||
const incoming = asString(editConfig[editKey])
|
||||
if (incoming !== "") {
|
||||
payload[secretKey] = incoming
|
||||
continue
|
||||
}
|
||||
if (secretKey in editConfig) {
|
||||
payload[secretKey] = editConfig[secretKey]
|
||||
}
|
||||
}
|
||||
|
||||
if (channel.name === "whatsapp_native") {
|
||||
payload.use_native = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user