CI & Build / Python lint (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
Android / Build, or is the channel already serving this? (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Failing after 54s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m6s
Desktop (Tauri) / Update manifest (push) Canceled after 0s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Canceled after 2m25s
Desktop (Tauri) / Tauri desktop (Linux) (push) Canceled after 2m33s
The ACL has gated every read since M0, but nothing could write a share. Server: - shares_api: GET /api/users/directory (everyone but you, signed-in only), GET/POST /api/notes/<id>/shares and DELETE …/shares/<share_id>, owner only. Sharing again with the same person changes the permission (ON CONFLICT on the new unique index). - acl.visible_to_user takes permission=; granted_to and shared_ids feed the serializer. - Edit covers body and checklist (_get_editable). Everything else stays _get_owned. A view share's write is a 404 like a stranger's (#1984). An editor's PATCH naming anything but body is a 403. - Serialized notes carry permission, shared and shared_by. A recipient never gets the owner's labels, and a #tag an editor types files under the owner's (it always went to note.owner_id). - ?shared=with_me, also allowed in saved views. Trash and reminders are the owner's. purge_note drops the note's shares. - Migration 0034: one share per note and person (and per group), permission limited to view and edit, an index for "shared with me". Web: - ShareDialog (one, mounted by the shell): pick a member, Can view or Can edit, change or remove existing shares, with loading, error and empty states. - Card: "Shared by X" or "Shared" chip; owner-only actions and reminder buttons hidden for recipients; checkboxes inert at view. - Editor: read-only at view; text and checklist only at edit; Share button for the owner. - FilterBar: Shared with me. Repo seam gains `shares`; the offline desktop shows none of it (#5175 brings sharing there). Tests: owner, recipient and stranger across reads, every write at view and edit, tag filing, unshare, trash, delete and validation; web unit tests for the facet and permission helpers. #5174. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
51 lines
1.6 KiB
Python
51 lines
1.6 KiB
Python
"""shares: one grant per note and person, and only the permissions the app knows
|
|
|
|
Revision ID: 0034
|
|
Revises: 0033
|
|
Create Date: 2026-10-07
|
|
|
|
Nothing wrote a share until #5174, so the table never needed these. Now the Share
|
|
dialog does:
|
|
- A unique index on (resource_type, resource_id, shared_with_user_id) where a user is
|
|
the target, so sharing a note with someone twice updates the one grant rather than
|
|
stacking two (the same for a group, ahead of step 15).
|
|
- A check that `permission` is `view` or `edit`.
|
|
- An index on `shared_with_user_id` for "Shared with me".
|
|
|
|
## Downgrade
|
|
|
|
Drops the indexes and the check. The rows stay.
|
|
"""
|
|
from alembic import op
|
|
|
|
revision = "0034"
|
|
down_revision = "0033"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_index(
|
|
"uq_shares_resource_user",
|
|
"shares",
|
|
["resource_type", "resource_id", "shared_with_user_id"],
|
|
unique=True,
|
|
postgresql_where="shared_with_user_id IS NOT NULL",
|
|
)
|
|
op.create_index(
|
|
"uq_shares_resource_group",
|
|
"shares",
|
|
["resource_type", "resource_id", "shared_with_group_id"],
|
|
unique=True,
|
|
postgresql_where="shared_with_group_id IS NOT NULL",
|
|
)
|
|
op.create_index("ix_shares_user", "shares", ["shared_with_user_id"])
|
|
op.create_check_constraint("ck_shares_permission", "shares", "permission IN ('view', 'edit')")
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_constraint("ck_shares_permission", "shares", type_="check")
|
|
op.drop_index("ix_shares_user", table_name="shares")
|
|
op.drop_index("uq_shares_resource_group", table_name="shares")
|
|
op.drop_index("uq_shares_resource_user", table_name="shares")
|