refactor(storage): one definition of task list access (FLAW-20)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m56s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m56s
accessibleTaskListsSQL in tasklists.go defines which task lists a user can see; GetTaskLists, GetTaskListByID and the task queries use it instead of three copies of the same subquery. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0fdf46635b
commit
cab5a79f6d
3 files changed
+25
-27
No files matched your search
+4
-3
@@ -28,8 +28,8 @@ Schweregrade: **Hoch** (Datenabfluss oder Rechteumgehung möglich), **Mittel** (
|
|||||||
| FLAW-16 | Setup | `/auth/setup` ist bis zum ersten Konto offen; Demo-Modus legt `admin/admin` an | Mittel |
|
| FLAW-16 | Setup | `/auth/setup` ist bis zum ersten Konto offen; Demo-Modus legt `admin/admin` an | Mittel |
|
||||||
| FLAW-17 | Workspace | ~~Bearbeiter-Freigaben für Notizen nicht wählbar~~ | **Behoben** |
|
| FLAW-17 | Workspace | ~~Bearbeiter-Freigaben für Notizen nicht wählbar~~ | **Behoben** |
|
||||||
| FLAW-18 | Suche | ~~Nur eigene Gedanken und Dokumente, `LIKE` ohne Escaping, keine FTS~~ | **Behoben** |
|
| FLAW-18 | Suche | ~~Nur eigene Gedanken und Dokumente, `LIKE` ohne Escaping, keine FTS~~ | **Behoben** |
|
||||||
| FLAW-19 | Qualität | Frontend-Tests nur für den Outliner, `lint` ist ein Platzhalter | Niedrig |
|
| FLAW-19 | Qualität | ~~Frontend-Tests nur für den Outliner, `lint` ist ein Platzhalter~~ | **Behoben** |
|
||||||
| FLAW-20 | Qualität | ~~Sehr große Dateien~~; duplizierte Zugriffslogik | Niedrig |
|
| FLAW-20 | Qualität | ~~Große Go-Dateien, duplizierte Zugriffslogik~~; große Frontend-Dateien | Niedrig |
|
||||||
| FLAW-21 | Repo | ~~`LICENSE` fehlt, obwohl README MIT angibt~~ | **Behoben** |
|
| FLAW-21 | Repo | ~~`LICENSE` fehlt, obwohl README MIT angibt~~ | **Behoben** |
|
||||||
| FLAW-22 | Netzwerk | ~~Token in der URL (WS/SSE), CORS `*`, `CheckOrigin` immer `true`~~ | **Behoben** |
|
| FLAW-22 | Netzwerk | ~~Token in der URL (WS/SSE), CORS `*`, `CheckOrigin` immer `true`~~ | **Behoben** |
|
||||||
| FLAW-23 | SQLite | ~~`SQLITE_BUSY` bei vielen gleichzeitigen Schreibern~~ | **Behoben** |
|
| FLAW-23 | SQLite | ~~`SQLITE_BUSY` bei vielen gleichzeitigen Schreibern~~ | **Behoben** |
|
||||||
@@ -162,12 +162,13 @@ Schweregrade: **Hoch** (Datenabfluss oder Rechteumgehung möglich), **Mittel** (
|
|||||||
- **Fundstellen:** `internal/storage/search.go`, `internal/api/server.go`, `frontend/src/components/CommandPalette.tsx`.
|
- **Fundstellen:** `internal/storage/search.go`, `internal/api/server.go`, `frontend/src/components/CommandPalette.tsx`.
|
||||||
|
|
||||||
### FLAW-19: Geringe Frontend-Testabdeckung
|
### FLAW-19: Geringe Frontend-Testabdeckung
|
||||||
|
- **Status: Behoben.** `npm run lint` ist echtes ESLint (`eslint.config.js`: empfohlene JS-/TypeScript-Regeln und React-Hook-Regeln) und läuft im Image-Build; Fehler brechen ihn ab, 24 `exhaustive-deps`-Hinweise bleiben Warnungen. ESLint fand zwei echte Fehler: `TaskDetailModal` und `ManageColumnsModal` riefen Hooks nach `if (!isOpen) return null` auf. Neue Tests: Aufgaben-Dialog (Öffnen/Schließen, Datum/Uhrzeit, Lesemodus), Personen-Stapel geteilter Inhalte, Papierkorb, API-Key-Scopes, Passwortregel, Offline-Warteschlange, Fälligkeiten (insgesamt 90 Frontend-Tests).
|
||||||
- **Stand:** Vitest mit jsdom und Testing Library ist eingerichtet (`npm run test:run`) und läuft beim Image-Build. Getestet sind bisher nur Outliner-Funktionen (Backspace-Zusammenführen, Cursor-Position beim Klicken).
|
- **Stand:** Vitest mit jsdom und Testing Library ist eingerichtet (`npm run test:run`) und läuft beim Image-Build. Getestet sind bisher nur Outliner-Funktionen (Backspace-Zusammenführen, Cursor-Position beim Klicken).
|
||||||
- **Problem:** Aufgaben, Listen, Freigabe-Dialoge und Berechtigungsanzeigen sind ungetestet. `"lint"` in [`frontend/package.json`](../frontend/package.json) ist weiterhin ein Platzhalter (`echo`).
|
- **Problem:** Aufgaben, Listen, Freigabe-Dialoge und Berechtigungsanzeigen sind ungetestet. `"lint"` in [`frontend/package.json`](../frontend/package.json) ist weiterhin ein Platzhalter (`echo`).
|
||||||
- **Maßnahme:** Tests für kritische Pfade ergänzen, ESLint als echtes `lint`-Skript.
|
- **Maßnahme:** Tests für kritische Pfade ergänzen, ESLint als echtes `lint`-Skript.
|
||||||
|
|
||||||
### FLAW-20: Dateigröße und Duplikate
|
### FLAW-20: Dateigröße und Duplikate
|
||||||
- **Teilweise behoben:** `sqlite.go` und `server.go` sind nach Bereichen in eigene Dateien aufgeteilt (reine Verschiebung, Verhalten unverändert; siehe [architecture.md](architecture.md#verzeichnisse)). Offen: große Frontend-Dateien und die doppelte Zugriffsabfrage.
|
- **Teilweise behoben:** `sqlite.go` und `server.go` sind nach Bereichen in eigene Dateien aufgeteilt (reine Verschiebung, Verhalten unverändert; siehe [architecture.md](architecture.md#verzeichnisse)). Der Lesezugriff auf Aufgabenlisten ist an einer Stelle definiert (`accessibleTaskListsSQL` in `tasklists.go`), die drei Kopien sind weg. Offen: `WorkspaceView.tsx` (≈ 2.200 Zeilen), `TasksView.tsx` (≈ 2.400) und `api.ts` (≈ 1.600) sind weiterhin groß.
|
||||||
- `internal/storage/sqlite.go` (≈ 5.600 Zeilen), `internal/api/server.go` (≈ 3.000), `WorkspaceView.tsx` (≈ 2.300), `TasksView.tsx` (≈ 2.500), `api.ts` (≈ 1.500).
|
- `internal/storage/sqlite.go` (≈ 5.600 Zeilen), `internal/api/server.go` (≈ 3.000), `WorkspaceView.tsx` (≈ 2.300), `TasksView.tsx` (≈ 2.500), `api.ts` (≈ 1.500).
|
||||||
- Die Zugriffsabfrage für Aufgabenlisten steht dreimal fast identisch im Code (`accessibleTaskListIDsQuery`, `GetTaskLists`, `GetTaskListByID`). Dokumentzugriff ist ähnlich verteilt. Änderungen an Berechtigungen müssen an mehreren Stellen erfolgen und werden leicht vergessen.
|
- Die Zugriffsabfrage für Aufgabenlisten steht dreimal fast identisch im Code (`accessibleTaskListIDsQuery`, `GetTaskLists`, `GetTaskListByID`). Dokumentzugriff ist ähnlich verteilt. Änderungen an Berechtigungen müssen an mehreren Stellen erfolgen und werden leicht vergessen.
|
||||||
- **Maßnahme:** In Teilmodule zerlegen (Auth, Dokumente, Aufgaben, Listen, Teams), Zugriffsprüfung zentralisieren.
|
- **Maßnahme:** In Teilmodule zerlegen (Auth, Dokumente, Aufgaben, Listen, Teams), Zugriffsprüfung zentralisieren.
|
||||||
|
|||||||
@@ -214,6 +214,21 @@ func (s *Store) queryUserIDs(query string, args ...interface{}) []string {
|
|||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// accessibleTaskListsSQL selects the IDs of every task list a user can see: own lists and lists shared
|
||||||
|
// with the user directly or through a group. It takes the user ID three times (accessibleTaskListArgs).
|
||||||
|
// This is the one definition of task list access for reading; GetTaskListPermission adds the role.
|
||||||
|
const accessibleTaskListsSQL = `SELECT id FROM task_lists
|
||||||
|
WHERE user_id = ?
|
||||||
|
OR id IN (
|
||||||
|
SELECT task_list_id FROM task_list_shares
|
||||||
|
WHERE (target_type = 'user' AND target_id = ?)
|
||||||
|
OR (target_type = 'team' AND target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
|
||||||
|
)`
|
||||||
|
|
||||||
|
func accessibleTaskListArgs(userID string) []interface{} {
|
||||||
|
return []interface{}{userID, userID, userID}
|
||||||
|
}
|
||||||
|
|
||||||
// taskListAccessSQL selects every user ID with access to a task list: owner, direct shares and members of
|
// taskListAccessSQL selects every user ID with access to a task list: owner, direct shares and members of
|
||||||
// shared groups. %[1]s is the list ID expression ("?" or a correlated column).
|
// shared groups. %[1]s is the list ID expression ("?" or a correlated column).
|
||||||
const taskListAccessSQL = `SELECT user_id FROM task_lists WHERE id = %[1]s
|
const taskListAccessSQL = `SELECT user_id FROM task_lists WHERE id = %[1]s
|
||||||
@@ -412,14 +427,9 @@ func (s *Store) GetTaskLists(userID string) ([]domain.TaskList, error) {
|
|||||||
rows, err := s.db.Query(
|
rows, err := s.db.Query(
|
||||||
`SELECT id, user_id, space_id, title, icon, color, COALESCE(columns, '[]'), created_at, updated_at
|
`SELECT id, user_id, space_id, title, icon, color, COALESCE(columns, '[]'), created_at, updated_at
|
||||||
FROM task_lists
|
FROM task_lists
|
||||||
WHERE user_id = ?
|
WHERE id IN (`+accessibleTaskListsSQL+`)
|
||||||
OR id IN (
|
|
||||||
SELECT task_list_id FROM task_list_shares
|
|
||||||
WHERE (target_type = 'user' AND target_id = ?)
|
|
||||||
OR (target_type = 'team' AND target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
|
|
||||||
)
|
|
||||||
ORDER BY updated_at DESC`,
|
ORDER BY updated_at DESC`,
|
||||||
userID, userID, userID,
|
accessibleTaskListArgs(userID)...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []domain.TaskList{}, err
|
return []domain.TaskList{}, err
|
||||||
@@ -470,16 +480,9 @@ func (s *Store) GetTaskListByID(userID, listID string) (*domain.TaskList, error)
|
|||||||
var colsJSON string
|
var colsJSON string
|
||||||
err := s.db.QueryRow(
|
err := s.db.QueryRow(
|
||||||
`SELECT id, user_id, space_id, title, icon, color, COALESCE(columns, '[]'), created_at, updated_at
|
`SELECT id, user_id, space_id, title, icon, color, COALESCE(columns, '[]'), created_at, updated_at
|
||||||
FROM task_lists
|
FROM task_lists
|
||||||
WHERE id = ? AND (
|
WHERE id = ? AND id IN (`+accessibleTaskListsSQL+`)`,
|
||||||
user_id = ?
|
append([]interface{}{listID}, accessibleTaskListArgs(userID)...)...,
|
||||||
OR id IN (
|
|
||||||
SELECT task_list_id FROM task_list_shares
|
|
||||||
WHERE (target_type = 'user' AND target_id = ?)
|
|
||||||
OR (target_type = 'team' AND target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
|
|
||||||
)
|
|
||||||
)`,
|
|
||||||
listID, userID, userID, userID,
|
|
||||||
).Scan(&tl.ID, &tl.UserID, &tl.SpaceID, &tl.Title, &tl.Icon, &tl.Color, &colsJSON, &tl.CreatedAt, &tl.UpdatedAt)
|
).Scan(&tl.ID, &tl.UserID, &tl.SpaceID, &tl.Title, &tl.Icon, &tl.Color, &colsJSON, &tl.CreatedAt, &tl.UpdatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -131,13 +131,7 @@ func scanThought(rows interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) accessibleTaskListIDsQuery() string {
|
func (s *Store) accessibleTaskListIDsQuery() string {
|
||||||
return `SELECT id FROM task_lists
|
return accessibleTaskListsSQL
|
||||||
WHERE user_id = ?
|
|
||||||
OR id IN (
|
|
||||||
SELECT task_list_id FROM task_list_shares
|
|
||||||
WHERE (target_type = 'user' AND target_id = ?)
|
|
||||||
OR (target_type = 'team' AND target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
|
|
||||||
)`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetThoughtByID(userID, thoughtID string) (*domain.Thought, error) {
|
func (s *Store) GetThoughtByID(userID, thoughtID string) (*domain.Thought, error) {
|
||||||
|
|||||||
Reference in new issue
Block a user