Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions meteor-backend/server/org-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,15 +170,19 @@ export async function getAccessibleOrgIds(userId) {

/**
* True if `userId` has team-admin authority on `team` — either because they
* are listed in `team.admins`, or because they own the organization the team
* belongs to. Org owners get full team-admin authority on every team in
* their org (rename, delete, invite, remove member, set role/password,
* approve/decline join requests, manage invitations).
* are listed in `team.admins`, or because they are an owner/admin of the
* organization the team belongs to. Org owners and admins get full team-admin
* authority on every team in their org (rename, delete, invite, remove member,
* set role/password, approve/decline join requests, manage invitations).
*/
export async function isTeamAdminOrOrgOwner(team, userId) {
if (team.admins.includes(userId)) return true;
if (!team.orgId || !isValidId(team.orgId)) return false;
// Check modern org_members collection first (owner or admin both qualify)
const membership = await rawDb().collection('org_members').findOne({ orgId: team.orgId, userId });
if (membership?.role === 'owner' || membership?.role === 'admin') return true;
// Fallback to legacy owners/admins arrays on the org document
const org = await rawDb().collection('organizations').findOne({ _id: new ObjectId(team.orgId) });
return !!org?.owners?.includes(userId);
return !!org?.owners?.includes(userId) || !!org?.admins?.includes(userId);
}

7 changes: 6 additions & 1 deletion meteor-backend/server/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ async function resolveOrgRoleForTeam(userId, team) {
const membership = await rawDb().collection('org_members').findOne({ orgId: team.orgId, userId });
if (membership?.role === 'owner') return 'owner';
if (membership?.role === 'admin') return 'admin';
// Fallback to legacy owners/admins arrays for data predating org_members migration
const org = await rawDb().collection('organizations').findOne({ _id: new ObjectId(team.orgId) });
if ((org?.owners ?? []).includes(userId)) return 'owner';
if ((org?.admins ?? []).includes(userId)) return 'admin';
return 'member';
}

Expand Down Expand Up @@ -118,7 +122,8 @@ export async function buildTeamAbility(userId, teamId) {
orgIds: team.orgId ? [team.orgId] : [],
enterpriseIds: enterpriseScope.enterpriseId ? [enterpriseScope.enterpriseId] : [],
isEnterpriseElevated: enterpriseScope.elevated,
teamAdminIds: (team.admins ?? []).includes(userId) ? [teamId] : [],
// Org owners/admins have team-admin authority on all teams in their org
teamAdminIds: (team.admins ?? []).includes(userId) || isOrgElevated ? [teamId] : [],
});

return { team, scoped, ability };
Expand Down
42 changes: 34 additions & 8 deletions meteor-backend/server/teams.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,26 +110,52 @@ function toPublicTeam(team) {
};
}

Meteor.publish('teams.byUser', function () {
Meteor.publish('teams.byUser', async function () {
if (!this.userId) return this.ready();
const userId = this.userId;
return Teams.find({ members: userId });
// Also surface all teams in orgs where this user is an owner or admin
const elevatedMemberships = await rawDb()
.collection('org_members')
.find({ userId, role: { $in: ['owner', 'admin'] } })
.toArray();
const elevatedOrgIds = elevatedMemberships.map((m) => m.orgId);
const filter =
elevatedOrgIds.length > 0
? { $or: [{ members: userId }, { orgId: { $in: elevatedOrgIds }, isPersonal: { $ne: true } }] }
: { members: userId };
return Teams.find(filter);
});

Meteor.methods({
async 'teams.list'() {
const identity = await requireIdentity(this);
const teams = await Teams.find({ members: identity.userId }).fetchAsync();
const userId = identity.userId;

// Include all teams in orgs where this user is an owner or admin
const elevatedMemberships = await rawDb()
.collection('org_members')
.find({ userId, role: { $in: ['owner', 'admin'] } })
.toArray();
const elevatedOrgIds = elevatedMemberships.map((m) => m.orgId);
const teamFilter =
elevatedOrgIds.length > 0
? { $or: [{ members: userId }, { orgId: { $in: elevatedOrgIds }, isPersonal: { $ne: true } }] }
: { members: userId };
const teams = await Teams.find(teamFilter).fetchAsync();

const userPending = await TeamJoinRequests.rawCollection()
.find({ userId: identity.userId, status: 'pending' })
.find({ userId, status: 'pending' })
.sort({ requestedAt: -1 })
.toArray();

const adminTeamIds = teams.filter((t) => t.admins?.includes(identity.userId)).map((t) => {
const id = t._id?.toHexString ? t._id.toHexString() : String(t._id);
return id;
});
// Org owners/admins see pending join requests for all their teams
const adminTeamIds = teams
.filter((t) => {
const id = t._id?.toHexString ? t._id.toHexString() : String(t._id);
if (t.admins?.includes(userId)) return true;
return elevatedOrgIds.includes(t.orgId);
})
.map((t) => (t._id?.toHexString ? t._id.toHexString() : String(t._id)));

let adminPending = [];
if (adminTeamIds.length > 0) {
Expand Down
Loading