Skip to content

feat(organizations): LinkedIn company-page management (#42) - #126

Merged
Adron merged 1 commit into
parity/queuefrom
issue/42-org-linkedin
Sep 16, 2026
Merged

Adron merged 1 commit into
parity/queuefrom
issue/42-org-linkedin

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

Closes #42. Part of epic #38.

A LinkedIn company-pages section on the organization detail screen, gated on a new
OrgPermissions.canManageLinkedIn — extending #83's matrix rather than adding a second permission
mechanism. It shows credential status, lists discovered pages, assigns a page per member, syncs the
page list, and disconnects behind a confirmation stating the consequence. An organization with no
credential is a first-class state, not an error.

Roles: owner + admin. The help centre says "when an organization's owners or admins connect a
shared LinkedIn credential" — and unlike the member endpoints #83 found unenforced, the server
genuinely enforces it here
: a plain member PUTting an assignment or POSTing a sync gets
403 {"error":"Admin or owner required","code":"forbidden"}. Since only management reads the status,
the whole section follows the stricter rule and a member never issues the request.

Live findings that contradict the help centre — trust these

  • GET …/linkedin/status → {"credential":null,"role":"member"}, not the documented
    {"connected":true,"expiresAt":…}. Non-member → 403 "Not a member of this organization".
  • GET …/linkedin/assignments and GET …/linkedin/sync-pages answer 405 — the help centre's
    table lists GETs that do not exist. The status payload is the only possible source of pages and
    assignments.
  • PUT …/linkedin/assignments takes one {userId, pageId} pair, not a map. {} →
    400 "userId required"; {"userId":…} with no page → 200 {"assigned":false} (clears that
    member); unknown page → 404 "Page not found in this organization"; non-member →
    400 "User is not a member of this organization".
  • POST …/linkedin/sync-pages without a credential → 404 "No active LinkedIn credential…";
    DELETE …/linkedin/credential without one → 404 "No LinkedIn credential found". Both render as
    the not-connected state.

Could not confirm (no reachable account holds an org credential): the connected payload — the
credential object's fields, the page rows, whether assignments ride on it at all — and the org
sync-pages response body. The DTOs are therefore tolerant (connected or presence of
credential; pages/assignments at top level or inside credential; page id under id/pageId,
name under pageName/name/label), and syncLinkedInPages deliberately POSTs then re-reads the
status
, so the unknown sync response shape is never depended on. {"assigned":true} is inferred
from the observed false.

Verification

./gradlew :app:assembleDebug testDebugUnitTest → BUILD SUCCESSFUL, 1467 tests, 0 failures. 34
new: status parse (connected, not-connected, and the documented top-level shape), assignment
round-trip asserting the exact body {"userId":"u1","pageId":"p2"} and the unassign form
{"userId":"u1"}, page-404 and 403 handling, credential delete and "none present", sync
POST-then-status-GET, the role gate, and the section hidden for a member (status never requested)
/ shown for owner and admin. 9 Compose tests compile, not executed (no emulator).

No :app change was needed; nothing outside :feature:organizations was touched.

Live probing — what was created and removed

Created one throwaway organization, "ZZ Android probe 42 (temp)", and ran every write probe
against that org only
. Deleted it afterwards; a follow-up GET returns 404.

Against real organizations only reads were sent, plus two calls the server rejected before touching
anything (PUT …/assignments with {} and POST …/sync-pages, both 403 "Admin or owner required").
No credential was removed and no assignment changed on any real organization.
(Independently re-verified: four visible orgs, the probe org 404s.)

Risks

  • The assignment UI renders one row per loaded member; a very large organization would want paging
    (members already load with limit=20).
  • If the connected status payload turns out not to carry assignments, the UI will show "Not
    assigned" until a page is chosen — assignments set in-session are tracked locally but not
    re-derivable on reload. Worth re-probing once an organization actually connects LinkedIn.
  • Reconnecting still requires the browser OAuth flow (OAuth: link a social account from Android via Custom Tabs #39, blocked).

…ssignments

An organization's LinkedIn section on the detail screen, shown only to a role
that may manage it: owner and admin. That is the help centre's rule ("when an
organization's owners or admins connect a shared LinkedIn credential") and the
server's — verified live, `PUT .../linkedin/assignments` and
`POST .../linkedin/sync-pages` answer a plain member
`403 {"error":"Admin or owner required"}`. The capability is a new
`OrgPermissions.canManageLinkedIn`, so #83's role matrix stays the one place
roles are decided.

The section reads `GET .../linkedin/status`, assigns a member to a company page
(or clears it) through `PUT .../linkedin/assignments`, refreshes the page list
with `POST .../linkedin/sync-pages`, and disconnects the credential with
`DELETE .../linkedin/credential` behind a confirmation that states the cost: the
organization can no longer post to its pages and every assignment is cleared.

An organization with no credential is a first-class state, not an error. Live,
`GET .../linkedin/status` answers `{"credential":null,"role":"owner"}` for one,
and the 404s `No LinkedIn credential found` / `No active LinkedIn credential for
this organization` mean the same thing — all three render as "not connected"
with nothing to sync or disconnect.

The assignment body is one live-verified `{userId, pageId}` pair per call, not
the "map" the help centre describes: `{}` answers `400 "userId required"`, a
`userId` with no `pageId` answers `200 {"assigned":false}` and so clears that
member's page, an unknown page answers `404 "Page not found in this
organization"`, and a stranger `400 "User is not a member of this
organization"`. `GET` on `.../assignments` and `.../sync-pages` answer 405, so
the documented read endpoints do not exist and the status payload is the only
source of pages and assignments.

The connected payload itself was not observable (no reachable account holds an
org credential), so its wire model is deliberately tolerant: `connected` or the
presence of `credential`, pages and assignments at either the top level or
inside `credential`, and a page read under either field naming.

Tests: status parse for connected and not-connected payloads, the assignment
round-trip asserting the exact request body (and the unassign form that omits
the page), the credential delete and its "none present" answer, sync posting
then re-reading the status, the role gate in `OrgPermissions`, and Compose
coverage that the section is hidden for a member, shown for an owner or admin,
and that disconnecting only fires after the confirmation.

Closes #42
@Adron
Adron merged commit 8b14bc5 into parity/queue Sep 16, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant