feat(library): protect Daedalus workspace-scoped libraries from manual deletion
All checks were successful
CVE Scan & Docker Build / security-scan (push) Successful in 49s
CVE Scan & Docker Build / build-and-push (push) Successful in 2m13s

- Add guard in `library_delete` view to block deletion of libraries
  owned by a Daedalus workspace, redirecting with an error message
- Disable the Delete button in `library_detail.html` for workspace-
  scoped libraries and show a warning alert explaining managed ownership
- Add a "Daedalus workspace" badge in both `library_detail.html` and
  `library_list.html` to visually identify workspace-owned libraries

Prevents state desync between Mnemosyne and Daedalus by ensuring
workspace-scoped libraries can only be removed via the Daedalus
workspace DELETE API endpoint.
This commit is contained in:
2026-05-08 06:55:07 -04:00
parent 3c7f85cba0
commit d11ee72527
3 changed files with 49 additions and 2 deletions

View File

@@ -10,17 +10,46 @@
<div class="flex justify-between items-start mb-6">
<div>
<h1 class="text-3xl font-bold">{{ library.name }}</h1>
<div class="badge badge-primary mt-2">{{ library.library_type }}</div>
<div class="flex flex-wrap gap-2 mt-2">
<div class="badge badge-primary">{{ library.library_type }}</div>
{% if library.workspace_id %}
<div class="badge badge-warning gap-1"
title="Workspace {{ library.workspace_id }}">
Daedalus workspace
</div>
{% endif %}
</div>
{% if library.description %}
<p class="mt-3 opacity-80">{{ library.description }}</p>
{% endif %}
</div>
<div class="flex gap-2">
<a href="{% url 'library:library-edit' uid=library.uid %}" class="btn btn-sm btn-outline">Edit</a>
{% if library.workspace_id %}
<button type="button" class="btn btn-sm btn-error btn-outline" disabled
title="This library is managed by Daedalus. Delete it from the Daedalus workspace, not here.">
Delete
</button>
{% else %}
<a href="{% url 'library:library-delete' uid=library.uid %}" class="btn btn-sm btn-error btn-outline">Delete</a>
{% endif %}
</div>
</div>
{% if library.workspace_id %}
<div class="alert alert-warning mb-6">
<div>
<div class="font-semibold">Managed by Daedalus</div>
<div class="text-sm opacity-80">
This library was created for Daedalus workspace
<code class="font-mono">{{ library.workspace_id }}</code>.
Items here are owned by the workspace; deleting the workspace in
Daedalus will remove this library. Do not delete it manually.
</div>
</div>
</div>
{% endif %}
<!-- Content-Type Configuration -->
<div class="collapse collapse-arrow bg-base-200 mb-6">
<input type="checkbox" />

View File

@@ -31,7 +31,14 @@
{{ lib.name }}
</a>
</h2>
<div class="flex flex-wrap gap-1">
<div class="badge badge-outline">{{ lib.library_type }}</div>
{% if lib.workspace_id %}
<div class="badge badge-warning gap-1" title="Managed by Daedalus workspace {{ lib.workspace_id }} — do not delete from Mnemosyne.">
Daedalus workspace
</div>
{% endif %}
</div>
{% if lib.description %}
<p class="text-sm opacity-70 mt-2">{{ lib.description|truncatewords:20 }}</p>
{% endif %}

View File

@@ -299,6 +299,17 @@ def library_delete(request, uid):
messages.error(request, f"Library not found: {e}")
return redirect("library:library-list")
# Daedalus owns the lifecycle of workspace-scoped libraries — they can
# only be deleted via DELETE /library/api/workspaces/{workspace_id}/.
# Block the human delete path so a stray click can't desync state.
if lib.workspace_id:
messages.error(
request,
f'"{lib.name}" is managed by Daedalus workspace '
f"{lib.workspace_id}. Delete it from Daedalus, not here.",
)
return redirect("library:library-detail", uid=uid)
if request.method == "POST":
name = lib.name
lib.delete()