Skip to content

Commit

Permalink
Org Admin - team lead perms
Browse files Browse the repository at this point in the history
  • Loading branch information
jmtaber129 committed Nov 28, 2023
1 parent ad6ac29 commit fa267da
Show file tree
Hide file tree
Showing 12 changed files with 72 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import MenuItemLabel from '../MenuItemLabel'
interface Props {
isLead: boolean
isViewerLead: boolean
isViewerOrgAdmin: boolean
teamMember: TeamMemberAvatarMenu_teamMember$key
menuProps: MenuProps
handleNavigate?: () => void
Expand All @@ -27,6 +28,7 @@ const StyledLabel = styled(MenuItemLabel)({
const TeamMemberAvatarMenu = (props: Props) => {
const {
isViewerLead,
isViewerOrgAdmin,
teamMember: teamMemberRef,
menuProps,
togglePromote,
Expand All @@ -48,17 +50,18 @@ const TeamMemberAvatarMenu = (props: Props) => {
const {preferredName, userId} = teamMember
const {viewerId} = atmosphere
const isSelf = userId === viewerId
const isViewerTeamAdmin = isViewerLead || isViewerOrgAdmin

return (
<Menu ariaLabel={'Select what to do with this team member'} {...menuProps}>
{isViewerLead && !isSelf && (
{isViewerTeamAdmin && (!isSelf || !isViewerLead) && (
<MenuItem
key='promote'
onClick={togglePromote}
label={<StyledLabel>Promote {preferredName} to Team Lead</StyledLabel>}
/>
)}
{isViewerLead && !isSelf && (
{isViewerTeamAdmin && !isSelf && (
<MenuItem
key='remove'
onClick={toggleRemove}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const ManageTeamList = (props: Props) => {
graphql`
fragment ManageTeamList_team on Team {
isLead
isOrgAdmin
teamMembers(sortBy: "preferredName") {
id
preferredName
Expand All @@ -35,14 +36,15 @@ const ManageTeamList = (props: Props) => {
`,
props.team
)
const {isLead: isViewerLead, teamMembers} = team
const {isLead: isViewerLead, isOrgAdmin: isViewerOrgAdmin, teamMembers} = team
return (
<List>
{teamMembers.map((teamMember) => {
return (
<ManageTeamMember
key={teamMember.id}
isViewerLead={isViewerLead}
isViewerOrgAdmin={isViewerOrgAdmin}
manageTeamMemberId={manageTeamMemberId}
teamMember={teamMember}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,13 @@ const TeamMemberAvatarMenu = lazyPreload(

interface Props {
isViewerLead: boolean
isViewerOrgAdmin: boolean
manageTeamMemberId?: string | null
teamMember: ManageTeamMember_teamMember$key
}

const ManageTeamMember = (props: Props) => {
const {isViewerLead, manageTeamMemberId} = props
const {isViewerLead, isViewerOrgAdmin, manageTeamMemberId} = props
const teamMember = useFragment(
graphql`
fragment ManageTeamMember_teamMember on TeamMember {
Expand All @@ -88,19 +89,23 @@ const ManageTeamMember = (props: Props) => {
...RemoveTeamMemberModal_teamMember
id
isLead
isOrgAdmin
preferredName
picture
userId
}
`,
props.teamMember
)
const {id: teamMemberId, isLead, preferredName, picture, userId} = teamMember
const {id: teamMemberId, isLead, isOrgAdmin, preferredName, picture, userId} = teamMember
const atmosphere = useAtmosphere()
const {viewerId} = atmosphere
const isSelf = userId === viewerId
const isSelectedAvatar = manageTeamMemberId === teamMemberId
const showMenuButton = (isViewerLead && !isSelf) || (!isViewerLead && isSelf)
const showMenuButton =
(isViewerOrgAdmin && ((isSelf && !isViewerLead) || !isLead)) ||
(isViewerLead && !isSelf && !isOrgAdmin) ||
(!isViewerLead && isSelf)
const {
closePortal: closePromote,
togglePortal: togglePromote,
Expand All @@ -121,7 +126,11 @@ const ManageTeamMember = (props: Props) => {
<Avatar size={24} picture={picture} />
<Content>
<Name>{preferredName}</Name>
<TeamLeadLabel isLead={isLead}>Team Lead</TeamLeadLabel>
<TeamLeadLabel isLead={isLead || isOrgAdmin}>
{isLead && 'Team Lead'}
{isLead && isOrgAdmin && ', '}
{isOrgAdmin && 'Org Admin'}
</TeamLeadLabel>
</Content>
<StyledButton
showMenuButton={showMenuButton}
Expand All @@ -138,6 +147,7 @@ const ManageTeamMember = (props: Props) => {
menuProps={menuProps}
isLead={isLead}
isViewerLead={isViewerLead}
isViewerOrgAdmin={isViewerOrgAdmin}
teamMember={teamMember}
togglePromote={togglePromote}
toggleRemove={toggleRemove}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const query = graphql`
teamMemberId: id
userId
isLead
isOrgAdmin
isSelf
preferredName
email
Expand All @@ -73,7 +74,7 @@ const TeamSettings = (props: Props) => {
const viewerTeamMember = teamMembers.find((m) => m.isSelf)
// if kicked out, the component might reload before the redirect occurs
if (!viewerTeamMember) return null
const {isLead: viewerIsLead} = viewerTeamMember
const {isLead: viewerIsLead, isOrgAdmin: viewerIsOrgAdmin} = viewerTeamMember
const lead = teamMembers.find((m) => m.isLead)
const contact = lead ?? {email: 'love@parabol.co', preferredName: 'Parabol Support'}
return (
Expand All @@ -93,7 +94,7 @@ const TeamSettings = (props: Props) => {
</StyledRow>
</Panel>
)}
{viewerIsLead ? (
{viewerIsLead || viewerIsOrgAdmin ? (
<Panel label='Danger Zone'>
<PanelRow>
<ArchiveTeam team={team!} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const OrgTeamsRow = (props: Props) => {
teamMembers {
id
isLead
isOrgAdmin
isSelf
email
}
Expand All @@ -30,7 +31,9 @@ const OrgTeamsRow = (props: Props) => {
const {id: teamId, teamMembers, name} = team
const teamMembersCount = teamMembers.length
const teamLeadEmail = teamMembers.find((member) => member.isLead)?.email ?? ''
const isViewerTeamLead = teamMembers.some((member) => member.isSelf && member.isLead)
const isViewerTeamLead = teamMembers.some(
(member) => member.isSelf && (member.isLead || member.isOrgAdmin)
)
return (
<Row>
<div className='flex w-full flex-col px-4 py-1'>
Expand Down
2 changes: 1 addition & 1 deletion packages/server/graphql/mutations/archiveTeam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default {

// AUTH
const viewerId = getUserId(authToken)
if (!(await isTeamLead(viewerId, teamId)) && !isSuperUser(authToken)) {
if (!(await isTeamLead(viewerId, teamId, dataLoader)) && !isSuperUser(authToken)) {
return standardError(new Error('Not team lead'), {userId: viewerId})
}

Expand Down
2 changes: 1 addition & 1 deletion packages/server/graphql/mutations/removeTeamMember.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default {
const {userId, teamId} = fromTeamMemberId(teamMemberId)
const isSelf = viewerId === userId
if (!isSelf) {
if (!(await isTeamLead(viewerId, teamId))) {
if (!(await isTeamLead(viewerId, teamId, dataLoader))) {
return standardError(new Error('Not team lead'), {userId: viewerId})
}
}
Expand Down
5 changes: 5 additions & 0 deletions packages/server/graphql/public/typeDefs/Team.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ type Team {
"""
isLead: Boolean!

"""
true if the viewer is an admin for the team's org, else false
"""
isOrgAdmin: Boolean!

"""
The team-specific settings for running all available types of meetings
"""
Expand Down
5 changes: 5 additions & 0 deletions packages/server/graphql/public/typeDefs/_legacy.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,11 @@ type TeamMember {
"""
isLead: Boolean!

"""
Is user an admin of the team's org?
"""
isOrgAdmin: Boolean!

"""
true if the user prefers to not vote during a poker meeting
"""
Expand Down
9 changes: 8 additions & 1 deletion packages/server/graphql/public/types/Team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,14 @@ const Team: TeamResolvers = {
tier: ({tier, trialStartDate}) => {
return getFeatureTier({tier, trialStartDate})
},
billingTier: ({tier}) => tier
billingTier: ({tier}) => tier,
isOrgAdmin: async ({orgId}, _args, {authToken, dataLoader}) => {
const viewerId = getUserId(authToken)
const organizationUser = await dataLoader
.get('organizationUsersByUserIdOrgId')
.load({userId: viewerId, orgId})
return organizationUser?.role === 'ORG_ADMIN'
}
}

export default Team
13 changes: 13 additions & 0 deletions packages/server/graphql/public/types/TeamMember.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {TeamMemberResolvers} from '../resolverTypes'

const TeamMember: TeamMemberResolvers = {
isOrgAdmin: async ({teamId, userId}, _args, {dataLoader}) => {
const team = await dataLoader.get('teams').loadNonNull(teamId)
const organizationUser = await dataLoader
.get('organizationUsersByUserIdOrgId')
.load({userId, orgId: team.orgId})
return organizationUser?.role === 'ORG_ADMIN'
}
}

export default TeamMember
12 changes: 10 additions & 2 deletions packages/server/utils/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,18 @@ export const isTeamMember = (authToken: AuthToken, teamId: string) => {
// .run()
// }

export const isTeamLead = async (userId: string, teamId: string) => {
export const isTeamLead = async (userId: string, teamId: string, dataLoader: DataLoaderWorker) => {
const r = await getRethink()
const teamMemberId = toTeamMemberId(teamId, userId)
return r.table('TeamMember').get(teamMemberId)('isLead').default(false).run()
if (await r.table('TeamMember').get(teamMemberId)('isLead').default(false).run()) {
return true
}

const team = await dataLoader.get('teams').loadNonNull(teamId)
const organizationUser = await dataLoader
.get('organizationUsersByUserIdOrgId')
.load({userId, orgId: team.orgId})
return organizationUser?.role === 'ORG_ADMIN'
}

interface Options {
Expand Down

0 comments on commit fa267da

Please sign in to comment.