feat(organizations): role-aware actions, visibility, and three wire fixes (#83) - #115
Merged
Merged
Conversation
The detail screen offered Edit and Delete to everyone, including non-members (#81 flagged this), and offered every role chip and a Remove button on every member row regardless of what the signed-in user could actually do. Calls that the server would refuse were simply sent. Model the documented role rules once, in OrgPermissions, quoting /help/organizations rather than inventing a hierarchy: - owner: full control; the only role that may delete the organization - admin: add/remove members and change roles, except an owner's, and may not hand out ownership - member: basic access — the roster is visible but read-only - non-member: nothing; the members endpoint is members-only (403) - a system organization ("The Public") can be neither left nor deleted The detail screen now renders only the permitted affordances: the overflow disappears entirely when nothing is available, the member picker is owners/admins only, role chips are limited to the roles the viewer may assign, and Remove is hidden where it would be refused. The server stays authoritative — hiding is UX, not enforcement — so every rejection still surfaces, and the last-owner rules are explained before and after the request for demote and remove, matching how #81 handled leave. Visibility is stated on the header (with what public/private mean) and remains editable by the roles permitted to edit the organization. Three wire bugs found while probing the live API, all fixed: - isPublic was serialised as a string on both create and update; the server answers 500 to a string, so visibility never round-tripped - the PUT echo carries no userRole/memberCount, so mapping it straight through made the editor look like a non-member and hid the actions they had just used; the repository re-reads instead - isSystem was not modelled at all, so "The Public" offered Leave Tests: a role x action matrix in OrgPermissionsTest, per-role view model coverage including a non-member seeing no Edit/Delete, last-owner demote and remove explained both before the call and on the server's rejection, visibility read and round-tripped, plus Compose coverage of the same rules on the rendered screen. Closes #83
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #83. Part of epic #80.
The role model came from the help centre, verbatim
/help/organizationsstates it plainly, and the wording is used as-is:That settles the matrix in
OrgPermissions.kt: add member / edit org = owner+admin; delete org =owner only; change-role and remove = owner on anyone, admin on anyone except an owner and never
granting owner; view members = any member; join = non-member of a public org; leave = any member of
a non-system org.
What changed
items and disappears entirely when a role has none — a non-member gets no menu at all.
(an admin gets no Owner chip, and only a read-only label on an owner's row); Remove is hidden where
it would be refused; a plain member sees a read-only roster.
invite-only; an owner or admin adds members", plus the viewer's role and a system-org marker.
PUT /api/organizations/{id}, fieldisPublic, owner+admin.offered no demotion or removal and gets an inline explanation, and the server's
Cannot demote the last owner/Cannot remove the last owner400s map to the same text. Guardslive in the ViewModel too, so hiding is genuinely UX and not the only barrier.
isSystemhandled: the help centre says you cannot leave "The Public", so system orgs offerneither Leave nor Delete.
Three live-verified wire bugs found and fixed
isPublicwas serialised as a string on both create and update. The server answers500 {"error":"Internal server error"}to"isPublic":"false"and200to"isPublic":false.Visibility never round-tripped from the app at all. Now a real JSON boolean.
PUTecho omitsuserRoleandmemberCount. Mapping it straight through made the ownerwho had just edited look like a non-member — hiding the very actions they had just used.
updateOrganizationnow re-reads.isSystemwas not modelled, which is why Leave was offered on "The Public". Added to the DTO,entity (Room v1→v2, destructive migration already configured), mapper and domain.
Verification
./gradlew :app:assembleDebug testDebugUnitTest→ BUILD SUCCESSFUL.:feature:organizations:testDebugUnitTest --rerun-tasks→ 93 tests, 0 failures. NewOrgPermissionsTesthas one case per role per action (15), plus 13 ViewModel cases, booleanwire-body assertions, and the Compose test rewritten for the rendered role rules (compiles, not
executed — no emulator).
No
:appchange was needed.Live-API cleanup
One throwaway organization
zz-parity-probe-83was created to probe PUT/role behaviour and wasdeleted; a second create attempt returned 500 and created nothing. Verified afterwards that
GET /api/organizationslists only the four real orgs andGET /api/user/organizationsonly theaccount's original three memberships. Probes against other people's orgs used a non-existent user id
so nothing could be mutated; nothing was changed on "The Public" or any real organization.
(Independently re-verified after the fact.)
Notes and risks
POSTing to
/membersgot a 500 (FK failure), not a 403, and PUT/DELETE on a member returned404 "User is not a member of this organization"rather than a permission error. So the UIgating is the practical protection here — worth knowing, and arguably worth a server-side fix.
membereverywhere).It follows the help centre: edit = owner+admin, delete = owner only. If that is wrong,
OrgPermissionsis the single place to change it.active/ suspend is not implemented. The docs mention suspending a member, and the serverrequires
rolealongsideactive({"active":false}alone →400 "Valid role (owner, admin, or member) is required"). Out of scope here, captured for later.