feat: edit API keys (name, rights); quick dump text above its buttons; inline note badges
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m33s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m33s
- PATCH /keys/{id} renames a key or changes its scopes; the secret stays. Settings → API gets an
edit button per key with the same switches as for a new key.
- Quick dump: a multiline text gets the full width, the buttons move to their own row below; the
bar only goes back to one line once the text is clearly shorter (no flip-flopping).
- Notes: the "Erweitert"/"Unteraufgabe" badges sit right after the checkbox text.
v0.38.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6f2eb08489
commit
1e3ac9786f
14 files changed
+266
-76
No files matched your search
@@ -126,3 +126,28 @@ func TestFilterTasks(t *testing.T) {
|
||||
t.Error("unknown status should fail")
|
||||
}
|
||||
}
|
||||
|
||||
// A key can be renamed and get other rights; the secret stays, other users' keys are out of reach.
|
||||
func TestUpdateAPIKey(t *testing.T) {
|
||||
srv, store, token := setupTestServer(t)
|
||||
router := srv.Router()
|
||||
claims, _ := auth.ValidateToken(testJWTSecret, token)
|
||||
key, _ := store.CreateAPIKey(claims.UserID, "Home Assistant", []string{"webhook:triage"})
|
||||
|
||||
rec := sessionCall(router, http.MethodPatch, "/api/v1/keys/"+key.ID, token, `{"name":"Claude Code","scopes":["tasks:read","tasks:write"]}`, "")
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("update: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if code, _ := keyRequest(t, router, key.RawKey, http.MethodGet, "/api/v1/tasks", ""); code != http.StatusOK {
|
||||
t.Fatalf("same secret should now read tasks: %d", code)
|
||||
}
|
||||
keys, _ := store.GetAPIKeys(claims.UserID)
|
||||
if keys[0].Name != "Claude Code" {
|
||||
t.Fatalf("name not changed: %+v", keys[0])
|
||||
}
|
||||
other, _ := store.CreateUser("other@example.com", "hash", "Other", "member")
|
||||
foreign, _ := store.CreateAPIKey(other.ID, "Fremd", nil)
|
||||
if rec := sessionCall(router, http.MethodPatch, "/api/v1/keys/"+foreign.ID, token, `{"name":"x"}`, ""); rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("foreign key must not be changed: %d", rec.Code)
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,29 @@ func (s *Server) handleCreateAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusCreated, key)
|
||||
}
|
||||
|
||||
// handleUpdateAPIKey renames a key or changes its rights; the key itself stays the same.
|
||||
func (s *Server) handleUpdateAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
var req struct {
|
||||
Name *string `json:"name"`
|
||||
Scopes *[]string `json:"scopes"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
key, err := s.store.UpdateAPIKey(userID, chi.URLParam(r, "id"), req.Name, req.Scopes)
|
||||
if errors.Is(err, storage.ErrAPIKeyNotFound) {
|
||||
writeError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, key)
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
keyID := chi.URLParam(r, "id")
|
||||
|
||||
@@ -304,6 +304,7 @@ func (s *Server) Router() http.Handler {
|
||||
// API Keys Management
|
||||
r.Get("/api/v1/keys", s.handleGetAPIKeys)
|
||||
r.Post("/api/v1/keys", s.handleCreateAPIKey)
|
||||
r.Patch("/api/v1/keys/{id}", s.handleUpdateAPIKey)
|
||||
r.Delete("/api/v1/keys/{id}", s.handleDeleteAPIKey)
|
||||
|
||||
// Staff Routes (Server-Admin)
|
||||
|
||||
@@ -93,6 +93,42 @@ func (s *Store) GetAPIKeys(userID string) ([]domain.APIKey, error) {
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// UpdateAPIKey changes name and/or rights of an own key; the secret itself never changes. nil keeps
|
||||
// a value. Returns ErrAPIKeyNotFound for keys of other users.
|
||||
func (s *Store) UpdateAPIKey(userID, keyID string, name *string, scopes *[]string) (*domain.APIKey, error) {
|
||||
if name != nil {
|
||||
n := strings.TrimSpace(*name)
|
||||
if n == "" {
|
||||
return nil, errors.New("Der Name darf nicht leer sein")
|
||||
}
|
||||
if res, err := s.db.Exec(`UPDATE api_keys SET name = ? WHERE id = ? AND user_id = ?`, n, keyID, userID); err != nil {
|
||||
return nil, err
|
||||
} else if c, _ := res.RowsAffected(); c == 0 {
|
||||
return nil, ErrAPIKeyNotFound
|
||||
}
|
||||
}
|
||||
if scopes != nil {
|
||||
joined := strings.Join(NormalizeScopes(*scopes), ",")
|
||||
if res, err := s.db.Exec(`UPDATE api_keys SET scopes = ? WHERE id = ? AND user_id = ?`, joined, keyID, userID); err != nil {
|
||||
return nil, err
|
||||
} else if c, _ := res.RowsAffected(); c == 0 {
|
||||
return nil, ErrAPIKeyNotFound
|
||||
}
|
||||
}
|
||||
keys, err := s.GetAPIKeys(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range keys {
|
||||
if keys[i].ID == keyID {
|
||||
return &keys[i], nil
|
||||
}
|
||||
}
|
||||
return nil, ErrAPIKeyNotFound
|
||||
}
|
||||
|
||||
var ErrAPIKeyNotFound = errors.New("API-Schlüssel nicht gefunden")
|
||||
|
||||
func (s *Store) DeleteAPIKey(userID, keyID string) error {
|
||||
_, err := s.db.Exec(`DELETE FROM api_keys WHERE id = ? AND user_id = ?`, keyID, userID)
|
||||
return err
|
||||
|
||||
Reference in new issue
Block a user