mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 15:06:32 +00:00
feat(collections): per-member permission roles on shared lists (#1081)
The owner now assigns each member a role — viewer (read + copy-to-trip only), editor (default: add + edit places) or admin (full incl. delete). The owner is always full. Existing members default to editor via migration 152, so nothing regresses. - Server: role column on collection_members (migration 152 + schema); roleOf + assertCanEdit (save/update/status/list-meta) + assertCanDelete (delete) layered on assertAccess; sendInvite takes a role; new setMemberRole (owner-only) + POST members/role; members payload carries each role. - Client: Share modal gains a role picker on invite and a per-member role select for the owner (read-only role badge for others); the page hides add / edit / status / move / delete for roles that can't perform them (server still enforces). - Roles in all 22 locales; service + controller tests for the new gating.
This commit is contained in:
@@ -6,6 +6,13 @@ export const COLLECTION_STATUSES = ['idea', 'want', 'visited'] as const;
|
||||
export const collectionStatusSchema = z.enum(COLLECTION_STATUSES).catch('idea').default('idea');
|
||||
export type CollectionStatus = (typeof COLLECTION_STATUSES)[number];
|
||||
|
||||
/** Per-member permission on a shared list. viewer = read + copy-to-trip only;
|
||||
* editor (default) = add + edit places; admin = full incl. delete. The owner
|
||||
* is always full and is not a member row. */
|
||||
export const COLLECTION_ROLES = ['viewer', 'editor', 'admin'] as const;
|
||||
export const collectionRoleSchema = z.enum(COLLECTION_ROLES).catch('editor').default('editor');
|
||||
export type CollectionRole = (typeof COLLECTION_ROLES)[number];
|
||||
|
||||
/** A user-added link on a list or a saved place (stored as a JSON array). */
|
||||
export const collectionLinkSchema = z.object({
|
||||
label: z.string().max(120).optional(),
|
||||
@@ -55,6 +62,7 @@ export const collectionMemberSchema = z.object({
|
||||
email: z.string().optional(),
|
||||
avatar: z.string().nullable().optional(),
|
||||
status: z.enum(['pending', 'accepted']),
|
||||
role: collectionRoleSchema.optional(),
|
||||
is_owner: z.boolean().optional(),
|
||||
});
|
||||
export type CollectionMember = z.infer<typeof collectionMemberSchema>;
|
||||
@@ -157,6 +165,7 @@ export type CollectionCopyToTripRequest = z.infer<typeof collectionCopyToTripReq
|
||||
export const collectionInviteRequestSchema = z.object({
|
||||
collection_id: z.number(),
|
||||
user_id: z.number(),
|
||||
role: collectionRoleSchema.optional(),
|
||||
});
|
||||
export type CollectionInviteRequest = z.infer<typeof collectionInviteRequestSchema>;
|
||||
|
||||
@@ -176,6 +185,14 @@ export const collectionRemoveMemberRequestSchema = z.object({
|
||||
});
|
||||
export type CollectionRemoveMemberRequest = z.infer<typeof collectionRemoveMemberRequestSchema>;
|
||||
|
||||
/** Owner changes an accepted member's permission role. */
|
||||
export const collectionSetMemberRoleRequestSchema = z.object({
|
||||
collection_id: z.number(),
|
||||
user_id: z.number(),
|
||||
role: collectionRoleSchema,
|
||||
});
|
||||
export type CollectionSetMemberRoleRequest = z.infer<typeof collectionSetMemberRoleRequestSchema>;
|
||||
|
||||
// ── Responses ─────────────────────────────────────────────────────────────
|
||||
export const collectionListResponseSchema = z.object({
|
||||
collections: z.array(collectionSchema),
|
||||
|
||||
Reference in New Issue
Block a user