MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m16s
API keys reach tasks and lists over plain REST too (GET /task-lists, GET /tasks with
status/due filters, PATCH/DELETE /thoughts/{id}, GET /lists, list items), guarded by a route
table; notes and settings stay closed. Rights hold for every access alike (webhook:task also
allows create_task over MCP, tasks:write also task delivery).
Tasks, thoughts and list items record which key created them and which one changed them last
(migration 13 replaces agent_mark; origin stays after the user's own edits and survives the
triage into a list item). OriginBadge shows the key name with a webhook or robot icon, details
on hover and tap, and replaces the Webhook and KI badges everywhere, now also on list items.
Settings: category "API" with rights grouped by tasks, lists and inbox and two tabs
(curl / smart home with a Home Assistant sensor, AI agents over MCP). Docs, help, README and
changelog updated; v0.30.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
3.6 KiB
Go
118 lines
3.6 KiB
Go
package storage
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/sandrodach/mindsift/internal/auth"
|
|
"github.com/sandrodach/mindsift/internal/domain"
|
|
)
|
|
|
|
// --- API KEYS & WEBHOOKS ---
|
|
|
|
// Rights of an API key. They hold the same over every access: REST (curl, smart home) and the MCP
|
|
// endpoint for AI agents. The ids of the first three come from the time they only delivered data
|
|
// (webhooks) and stay for existing keys. Notes are never reachable with a key.
|
|
const (
|
|
ScopeTriage = "webhook:triage" // thoughts into the inbox
|
|
ScopeTask = "webhook:task" // create tasks
|
|
ScopeList = "webhook:list" // add items to lists
|
|
|
|
ScopeTasksRead = "tasks:read" // task lists and tasks
|
|
ScopeTasksWrite = "tasks:write" // create, change, complete and delete tasks
|
|
ScopeListsRead = "lists:read" // lists and their items
|
|
ScopeListsWrite = "lists:write" // add, change and check off items
|
|
)
|
|
|
|
// AllScopes in display order.
|
|
var AllScopes = []string{ScopeTriage, ScopeTask, ScopeList, ScopeTasksRead, ScopeTasksWrite, ScopeListsRead, ScopeListsWrite}
|
|
|
|
// NormalizeScopes keeps the known scopes in a fixed order; none at all means triage only.
|
|
func NormalizeScopes(scopes []string) []string {
|
|
want := map[string]bool{}
|
|
for _, sc := range scopes {
|
|
want[strings.ToLower(strings.TrimSpace(sc))] = true
|
|
}
|
|
out := []string{}
|
|
for _, sc := range AllScopes {
|
|
if want[sc] {
|
|
out = append(out, sc)
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
out = []string{ScopeTriage}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *Store) CreateAPIKey(userID, name string, scopes []string) (*domain.APIKey, error) {
|
|
if strings.TrimSpace(name) == "" {
|
|
name = "Smarthome / Webhook Key"
|
|
}
|
|
rawKey, prefix, hash := auth.GenerateAPIKey()
|
|
k := &domain.APIKey{
|
|
ID: domain.NewID("key"),
|
|
UserID: userID,
|
|
Name: strings.TrimSpace(name),
|
|
KeyPrefix: prefix,
|
|
KeyHash: hash,
|
|
RawKey: rawKey,
|
|
Scopes: strings.Join(NormalizeScopes(scopes), ","),
|
|
CreatedAt: domain.NowISO(),
|
|
}
|
|
|
|
_, err := s.db.Exec(
|
|
`INSERT INTO api_keys (id, user_id, name, key_prefix, key_hash, scopes, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
k.ID, k.UserID, k.Name, k.KeyPrefix, k.KeyHash, k.Scopes, k.CreatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return k, nil
|
|
}
|
|
|
|
func (s *Store) GetAPIKeys(userID string) ([]domain.APIKey, error) {
|
|
rows, err := s.db.Query(
|
|
`SELECT id, user_id, name, key_prefix, scopes, last_used_at, created_at
|
|
FROM api_keys WHERE user_id = ? ORDER BY created_at DESC`,
|
|
userID,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
keys := make([]domain.APIKey, 0)
|
|
for rows.Next() {
|
|
var k domain.APIKey
|
|
if err := rows.Scan(&k.ID, &k.UserID, &k.Name, &k.KeyPrefix, &k.Scopes, &k.LastUsedAt, &k.CreatedAt); err == nil {
|
|
keys = append(keys, k)
|
|
}
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (s *Store) ValidateAPIKeyAndGetUser(rawKey string) (*domain.User, error) {
|
|
u, _, _, err := s.ValidateAPIKey(rawKey)
|
|
return u, err
|
|
}
|
|
|
|
// ValidateAPIKey returns the owner of a key, the key's scopes and its id.
|
|
func (s *Store) ValidateAPIKey(rawKey string) (*domain.User, []string, string, error) {
|
|
hash := auth.HashAPIKey(strings.TrimSpace(rawKey))
|
|
var keyID, userID, scopes string
|
|
err := s.db.QueryRow(`SELECT id, user_id, scopes FROM api_keys WHERE key_hash = ?`, hash).Scan(&keyID, &userID, &scopes)
|
|
if err != nil {
|
|
return nil, nil, "", errors.New("invalid API key")
|
|
}
|
|
_, _ = s.db.Exec(`UPDATE api_keys SET last_used_at = ? WHERE id = ?`, domain.NowISO(), keyID)
|
|
u, err := s.GetUserByID(userID)
|
|
return u, NormalizeScopes(strings.Split(scopes, ",")), keyID, err
|
|
}
|