Files
WeKnora/internal/im/credentials.go
T
wizardchen 6a7e41f806 feat(im): describe IM platforms and credentials from the backend
Each IM platform now declares its connection modes, thread support,
console links and a credential config schema in internal/im/platforms.go.
Fields used by one mode carry x-visible-if on "$mode", which config
schemas read from a context (configschema.ValidateInContext, the
SchemaForm context prop) instead of from sibling fields.

GET /api/v1/im-channels/platforms serves the registered platforms, and
IMChannelPanel renders modes, links and credentials from it, dropping
the per-platform v-if blocks. WeChat keeps its QR binding flow; its
bound fields are declared hidden so the token is still treated as a
secret.

Fix IM channel edits wiping credentials: list responses stopped
carrying credentials in 7099a7e18, so the editor sent an empty map that
replaced the stored one. GET /api/v1/im-channels/:id (Admin) now returns
the channel with secrets redacted, and updates merge credentials key by
key, keeping secrets sent empty or as ***.
2026-09-25 20:55:09 +08:00

111 lines
2.8 KiB
Go

package im
import (
"encoding/json"
"strings"
"github.com/Tencent/WeKnora/internal/plugin/configschema"
)
// ParseCredentials parses the JSONB credentials field into a map.
func ParseCredentials(data []byte) (map[string]any, error) {
if len(data) == 0 {
return map[string]any{}, nil
}
var creds map[string]any
if err := json.Unmarshal(data, &creds); err != nil {
return nil, err
}
return creds, nil
}
// GetString safely extracts a string value from a credentials map.
func GetString(creds map[string]any, key string) string {
if v, ok := creds[key]; ok {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetBool reads a boolean from JSON credentials (bool, string "true"/"1", or non-zero number).
func GetBool(creds map[string]any, key string) bool {
v, ok := creds[key]
if !ok {
return false
}
switch x := v.(type) {
case bool:
return x
case string:
s := strings.TrimSpace(strings.ToLower(x))
return s == "true" || s == "1" || s == "yes"
case float64:
return x != 0
case int:
return x != 0
default:
return false
}
}
// secretKeys returns the credential keys a platform's schema marks secret.
func secretKeys(platform string) map[string]bool {
out := map[string]bool{}
if s := LookupPlatformInfo(platform).ConfigSchema; s != nil {
for key, prop := range s.Properties {
if prop.Secret {
out[key] = true
}
}
}
return out
}
// RedactCredentials returns a channel's credentials for the editor: plain
// fields as stored, set secrets replaced by configschema.RedactedPlaceholder.
func RedactCredentials(platform string, raw []byte) (map[string]any, error) {
creds, err := ParseCredentials(raw)
if err != nil {
return nil, err
}
secrets := secretKeys(platform)
for key, v := range creds {
if s, ok := v.(string); ok && s != "" && secrets[key] {
creds[key] = configschema.RedactedPlaceholder
}
}
return creds, nil
}
// MergeCredentials applies credentials sent by the editor to the stored ones.
// Keys the update leaves out keep their stored value, and so does a secret
// sent empty or as the redaction placeholder, since the editor never saw it.
// A plain field sent empty is cleared. An empty update changes nothing, which
// keeps an edit that never loaded the credentials from wiping them.
func MergeCredentials(platform string, stored, incoming []byte) ([]byte, error) {
update, err := ParseCredentials(incoming)
if err != nil {
return nil, err
}
if len(update) == 0 {
return stored, nil
}
out, err := ParseCredentials(stored)
if err != nil {
// Unreadable stored credentials cannot be merged into; take the update.
out = map[string]any{}
}
secrets := secretKeys(platform)
for key, v := range update {
if secrets[key] {
if s, ok := v.(string); v == nil || (ok && (s == "" || s == configschema.RedactedPlaceholder)) {
continue
}
}
out[key] = v
}
return json.Marshal(out)
}