mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-15 02:33:15 -06:00
fix(auth/me): broaden non-admin accessible_workspaces to include org_owner/org_admin paths
The non-admin branch of /me's accessible_workspaces query drove from workspace_members, so users with org_owner or org_admin on an organization but no direct workspace_members row were missing those workspaces from their /me response - and therefore from the switcher dropdown. Mirrors the access logic in accessibleWorkspaceIds() (lib/tenancy.js) while keeping the full-row SELECT shape /me needs. Verified end-to-end with switcher-test@local.test acting as org_owner of Acme Studios with no workspace_members row on Studio B - Studio B now appears in /me's accessible_workspaces with workspace_role: null, can_admin: true. Also updates the stale TODO comment in tenancy.js that flagged this exact gap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3294525f4c
commit
d2a3bdfd15
|
|
@ -142,10 +142,9 @@ function resolveTenancy(req, res, next) {
|
||||||
// - direct workspace_members rows
|
// - direct workspace_members rows
|
||||||
// - any workspace in an org where they are org_owner / org_admin
|
// - any workspace in an org where they are org_owner / org_admin
|
||||||
// - platform_admin / superadmin: every workspace in the system
|
// - platform_admin / superadmin: every workspace in the system
|
||||||
// Used by socket.io rooms (Phase 2.3) to scope outbound broadcasts. Also a
|
// Used by socket.io rooms (Phase 2.3) to scope outbound broadcasts. /me's
|
||||||
// candidate to broaden /me's accessible_workspaces query - currently /me only
|
// accessible_workspaces query mirrors this access logic but selects full rows
|
||||||
// returns direct workspace_members for non-admins, missing the org-admin
|
// rather than reusing this helper (different shape needs).
|
||||||
// path. Future cleanup tracked in the handoff doc.
|
|
||||||
function accessibleWorkspaceIds(userId, role) {
|
function accessibleWorkspaceIds(userId, role) {
|
||||||
if (!userId) return [];
|
if (!userId) return [];
|
||||||
if (role === 'platform_admin' || role === 'superadmin') {
|
if (role === 'platform_admin' || role === 'superadmin') {
|
||||||
|
|
|
||||||
|
|
@ -295,11 +295,14 @@ function getMicrosoftProfile(accessToken) {
|
||||||
router.get('/me', requireAuth, resolveTenancy, (req, res) => {
|
router.get('/me', requireAuth, resolveTenancy, (req, res) => {
|
||||||
// Platform admins see every workspace in the system (via the LEFT JOIN they
|
// Platform admins see every workspace in the system (via the LEFT JOIN they
|
||||||
// still get their own workspace_role for direct memberships; NULL elsewhere,
|
// still get their own workspace_role for direct memberships; NULL elsewhere,
|
||||||
// matching accessContext's actingAs semantics). Regular users see only
|
// matching accessContext's actingAs semantics). Regular users see every
|
||||||
// workspaces they have a direct workspace_members row in. Role is read from
|
// workspace they can reach via either path: direct workspace_members row, OR
|
||||||
// the signed JWT (not user-supplied), so non-admins cannot reach the admin
|
// org_owner / org_admin on the parent organization. Mirrors the access
|
||||||
// branch. No cap on the admin list yet - revisit at 50+ workspaces when
|
// logic in accessibleWorkspaceIds() (lib/tenancy.js); kept as a separate
|
||||||
// dropdown UX without search starts to degrade.
|
// query rather than reusing it because /me needs full row shape, not just
|
||||||
|
// IDs. Role is read from the signed JWT (not user-supplied), so non-admins
|
||||||
|
// cannot reach the admin branch. No cap on the admin list yet - revisit at
|
||||||
|
// 50+ workspaces when dropdown UX without search starts to degrade.
|
||||||
//
|
//
|
||||||
// Each accessible_workspaces entry also carries `can_admin: bool` so the
|
// Each accessible_workspaces entry also carries `can_admin: bool` so the
|
||||||
// UI can render admin affordances (rename pencil etc.) only where the
|
// UI can render admin affordances (rename pencil etc.) only where the
|
||||||
|
|
@ -325,11 +328,12 @@ router.get('/me', requireAuth, resolveTenancy, (req, res) => {
|
||||||
SELECT w.id, w.name, w.organization_id, o.name AS organization_name,
|
SELECT w.id, w.name, w.organization_id, o.name AS organization_name,
|
||||||
wm.role AS workspace_role, om.role AS org_role,
|
wm.role AS workspace_role, om.role AS org_role,
|
||||||
(SELECT COUNT(*) FROM devices WHERE workspace_id = w.id) AS device_count
|
(SELECT COUNT(*) FROM devices WHERE workspace_id = w.id) AS device_count
|
||||||
FROM workspace_members wm
|
FROM workspaces w
|
||||||
JOIN workspaces w ON w.id = wm.workspace_id
|
|
||||||
JOIN organizations o ON o.id = w.organization_id
|
JOIN organizations o ON o.id = w.organization_id
|
||||||
|
LEFT JOIN workspace_members wm ON wm.workspace_id = w.id AND wm.user_id = ?
|
||||||
LEFT JOIN organization_members om ON om.organization_id = w.organization_id AND om.user_id = ?
|
LEFT JOIN organization_members om ON om.organization_id = w.organization_id AND om.user_id = ?
|
||||||
WHERE wm.user_id = ?
|
WHERE wm.user_id IS NOT NULL
|
||||||
|
OR (om.user_id IS NOT NULL AND om.role IN ('org_owner', 'org_admin'))
|
||||||
ORDER BY o.name, w.name
|
ORDER BY o.name, w.name
|
||||||
`).all(req.user.id, req.user.id);
|
`).all(req.user.id, req.user.id);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue