mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-08-01 16:20:59 +00:00
- Enable merge UI for linking channel contacts to tenant_users - Contact → tenant_user resolution with cached lookup (60s TTL) - MCP per-user credentials via user-keyed connection pool - Secure CLI per-user credentials with AES-256-GCM encryption - Unified UserPickerCombobox searching contacts + tenant_users - Group contact collection with chat title in all channels - Group permission inheritance via wildcard user_id="*" - Fix heartbeat using wrong userID in group chats - Filter internal senders from contact collection - Add contact_type column (user/group) to channel_contacts - SQLite schema v2 migration for desktop edition
148 lines
4.8 KiB
Go
148 lines
4.8 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
func (h *SecureCLIHandler) handleListUserCredentials(w http.ResponseWriter, r *http.Request) {
|
|
binaryID, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
locale := store.LocaleFromContext(r.Context())
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID)})
|
|
return
|
|
}
|
|
creds, err := h.store.ListUserCredentials(r.Context(), binaryID)
|
|
if err != nil {
|
|
locale := store.LocaleFromContext(r.Context())
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgInternalError, err.Error())})
|
|
return
|
|
}
|
|
// Return without encrypted env for listing (only user_id + timestamps)
|
|
type entry struct {
|
|
ID uuid.UUID `json:"id"`
|
|
BinaryID uuid.UUID `json:"binary_id"`
|
|
UserID string `json:"user_id"`
|
|
HasEnv bool `json:"has_env"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
entries := make([]entry, 0, len(creds))
|
|
for _, c := range creds {
|
|
entries = append(entries, entry{
|
|
ID: c.ID,
|
|
BinaryID: c.BinaryID,
|
|
UserID: c.UserID,
|
|
HasEnv: len(c.EncryptedEnv) > 0,
|
|
CreatedAt: c.CreatedAt,
|
|
UpdatedAt: c.UpdatedAt,
|
|
})
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"user_credentials": entries})
|
|
}
|
|
|
|
func (h *SecureCLIHandler) handleGetUserCredentials(w http.ResponseWriter, r *http.Request) {
|
|
binaryID, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
locale := store.LocaleFromContext(r.Context())
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID)})
|
|
return
|
|
}
|
|
userID := r.PathValue("userId")
|
|
if userID == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "user_id required"})
|
|
return
|
|
}
|
|
|
|
cred, err := h.store.GetUserCredentials(r.Context(), binaryID, userID)
|
|
if err != nil {
|
|
locale := store.LocaleFromContext(r.Context())
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgInternalError, err.Error())})
|
|
return
|
|
}
|
|
if cred == nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
|
|
return
|
|
}
|
|
|
|
// Return decrypted env as JSON object (admin-only endpoint)
|
|
var envObj any
|
|
if len(cred.EncryptedEnv) > 0 {
|
|
_ = json.Unmarshal(cred.EncryptedEnv, &envObj)
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"user_id": cred.UserID,
|
|
"env": envObj,
|
|
})
|
|
}
|
|
|
|
func (h *SecureCLIHandler) handleSetUserCredentials(w http.ResponseWriter, r *http.Request) {
|
|
binaryID, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
locale := store.LocaleFromContext(r.Context())
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID)})
|
|
return
|
|
}
|
|
userID := r.PathValue("userId")
|
|
if userID == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "user_id required"})
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
Env json.RawMessage `json:"env"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON body"})
|
|
return
|
|
}
|
|
if len(body.Env) == 0 {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "env is required"})
|
|
return
|
|
}
|
|
|
|
// Validate env is a JSON object
|
|
var envCheck map[string]string
|
|
if err := json.Unmarshal(body.Env, &envCheck); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "env must be a JSON object with string values"})
|
|
return
|
|
}
|
|
|
|
if err := h.store.SetUserCredentials(r.Context(), binaryID, userID, body.Env); err != nil {
|
|
locale := store.LocaleFromContext(r.Context())
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgInternalError, err.Error())})
|
|
return
|
|
}
|
|
|
|
h.emitCacheInvalidate("")
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (h *SecureCLIHandler) handleDeleteUserCredentials(w http.ResponseWriter, r *http.Request) {
|
|
binaryID, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
locale := store.LocaleFromContext(r.Context())
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID)})
|
|
return
|
|
}
|
|
userID := r.PathValue("userId")
|
|
if userID == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "user_id required"})
|
|
return
|
|
}
|
|
|
|
if err := h.store.DeleteUserCredentials(r.Context(), binaryID, userID); err != nil {
|
|
locale := store.LocaleFromContext(r.Context())
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgInternalError, err.Error())})
|
|
return
|
|
}
|
|
|
|
h.emitCacheInvalidate("")
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|