Files
MindSift/internal/storage/account.go
T
sandrodachandClaude Opus 5.5 0e9fd1b3ad
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m27s
feat(account): danger zone, restore import, larger API key field on phones
- Danger zone in settings: delete all own data or the account, both with
  password confirmation; the last server admin cannot delete the account
- DELETE /user/data and /user/account; PurgeUserData removes everything a
  user owns in one transaction (others' tasks in shared lists stay theirs),
  also used by admin user deletion
- MindSift import: 'Hinzufügen' or 'Wiederherstellen' (mode=restore, wipes
  own data only after the file is fully validated, confirm checkbox)
- API key name field and button 44 px on phones (flex-1 collapsed them)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 07:13:29 +01:00

93 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package storage
import (
"errors"
"fmt"
)
// ErrLastAdmin protects the server from losing its last administrator.
var ErrLastAdmin = errors.New("Der letzte Server-Administrator kann nicht gelöscht werden. Bitte ernenne vorher ein anderes Konto zum Administrator.")
// PurgeUserData deletes everything the user owns – thoughts and tasks, notes (with blocks, history and
// shares), folders, shared folders, task lists and lists – in one transaction. The account, its
// settings, themes, API keys and group memberships stay. Other people's tasks in the user's task lists
// become their own personal tasks, as when a single list is deleted.
func (s *Store) PurgeUserData(userID string) error {
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
ownDocs := `(SELECT id FROM documents WHERE user_id = ?)`
ownTaskLists := `(SELECT id FROM task_lists WHERE user_id = ?)`
ownFolders := `(SELECT id FROM workspace_shared_folders WHERE owner_id = ?)`
stmts := []struct {
q string
args int // how many times userID is passed
}{
// Task lists: keep the tasks of others, drop shares and the lists
{`UPDATE thoughts SET task_list_id = '' WHERE task_list_id IN ` + ownTaskLists + ` AND user_id != ?`, 2},
{`DELETE FROM task_list_shares WHERE task_list_id IN ` + ownTaskLists, 1},
{`DELETE FROM task_lists WHERE user_id = ?`, 1},
// Tasks of others linked to blocks of the user's notes lose the link
{`UPDATE thoughts SET converted_type = '', converted_ref_id = '' WHERE converted_type = 'document_block'
AND substr(converted_ref_id, 1, instr(converted_ref_id, ':') - 1) IN ` + ownDocs, 1},
{`DELETE FROM thoughts WHERE user_id = ?`, 1},
// Shared folders owned by the user
{`UPDATE documents SET shared_folder_id = '', sub_folder_path = '' WHERE shared_folder_id IN ` + ownFolders, 1},
{`DELETE FROM workspace_folder_mounts WHERE folder_id IN ` + ownFolders, 1},
{`DELETE FROM workspace_folder_shares WHERE folder_id IN ` + ownFolders, 1},
{`DELETE FROM workspace_folders WHERE shared_folder_id IN ` + ownFolders, 1},
{`DELETE FROM workspace_shared_folders WHERE owner_id = ?`, 1},
// Notes (history tables cascade with the document)
{`DELETE FROM blocks WHERE document_id IN ` + ownDocs, 1},
{`DELETE FROM block_tags WHERE document_id IN ` + ownDocs, 1},
{`DELETE FROM doc_user_folders WHERE doc_id IN ` + ownDocs, 1},
{`DELETE FROM doc_team_shares WHERE doc_id IN ` + ownDocs, 1},
{`DELETE FROM doc_shares WHERE doc_id IN ` + ownDocs, 1},
{`DELETE FROM documents WHERE user_id = ?`, 1},
// The user's own placement of notes, folders, pins and tag pages
{`DELETE FROM doc_user_folders WHERE user_id = ?`, 1},
{`DELETE FROM doc_pins WHERE user_id = ?`, 1},
{`DELETE FROM workspace_folders WHERE user_id = ?`, 1},
{`DELETE FROM tag_pages WHERE user_id = ?`, 1},
// Lists (items cascade)
{`DELETE FROM list_shares WHERE list_id IN (SELECT id FROM lists WHERE user_id = ?)`, 1},
{`DELETE FROM lists WHERE user_id = ?`, 1},
}
for _, st := range stmts {
args := make([]interface{}, st.args)
for i := range args {
args[i] = userID
}
if _, err := tx.Exec(st.q, args...); err != nil {
return fmt.Errorf("Daten löschen: %w", err)
}
}
if err := tx.Commit(); err != nil {
return err
}
s.PruneTaskAssignees()
return nil
}
// IsLastAdmin reports whether userID is an administrator and no other one exists.
func (s *Store) IsLastAdmin(userID string) bool {
var role string
if err := s.db.QueryRow(`SELECT role FROM users WHERE id = ?`, userID).Scan(&role); err != nil || role != "admin" {
return false
}
var others int
_ = s.db.QueryRow(`SELECT COUNT(*) FROM users WHERE role = 'admin' AND id != ?`, userID).Scan(&others)
return others == 0
}
// DeleteOwnAccount removes the user with all data; refused for the last administrator.
func (s *Store) DeleteOwnAccount(userID string) error {
if s.IsLastAdmin(userID) {
return ErrLastAdmin
}
return s.DeleteUser(userID)
}