Skip to content

Commit de872d3

Browse files
committed
fix(app): add @ prefix when constructing scope:team for npm operations
npm team/access operations require `@scope:team` format but the frontend was sending `scope:team` without the `@` prefix. This was obscured by the fact that the admin operations aren't quite functional yet for most orgs as OTP isn't supported, and by the fact that `listTeamUsers` only fails with this incorrect format when it goes through the CLI, which is only the case for the authenticated user's own orgs.
1 parent e829139 commit de872d3

5 files changed

Lines changed: 40 additions & 9 deletions

File tree

app/components/OrgMembersPanel.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { NewOperation } from '~/composables/useConnector'
3+
import { buildScopeTeam } from '~/utils/npm'
34
45
const props = defineProps<{
56
orgName: string
@@ -144,10 +145,10 @@ async function loadTeamMemberships() {
144145
try {
145146
const teamsResult = await listOrgTeams(props.orgName)
146147
if (teamsResult) {
147-
// Teams come as "org:team" format
148+
// Teams come as "org:team" format from npm, need @scope:team for API calls
148149
const teamPromises = teamsResult.map(async (fullTeamName: string) => {
149150
const teamName = fullTeamName.replace(`${props.orgName}:`, '')
150-
const membersResult = await listTeamUsers(fullTeamName)
151+
const membersResult = await listTeamUsers(buildScopeTeam(props.orgName, teamName))
151152
if (membersResult) {
152153
teamMembers.value[teamName] = membersResult
153154
}
@@ -183,7 +184,7 @@ async function handleAddMember() {
183184
// Second operation: add user to team (if a team is selected)
184185
// This depends on the org operation completing first
185186
if (newTeam.value && addedOrgOp) {
186-
const scopeTeam = `${props.orgName}:${newTeam.value}`
187+
const scopeTeam = buildScopeTeam(props.orgName, newTeam.value)
187188
const teamOperation: NewOperation = {
188189
type: 'team:add-user',
189190
params: {

app/components/OrgTeamsPanel.vue

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { NewOperation } from '~/composables/useConnector'
3+
import { buildScopeTeam } from '~/utils/npm'
34
45
const props = defineProps<{
56
orgName: string
@@ -103,7 +104,7 @@ async function loadTeamUsers(teamName: string) {
103104
isLoadingUsers.value[teamName] = true
104105
105106
try {
106-
const scopeTeam = `${props.orgName}:${teamName}`
107+
const scopeTeam = buildScopeTeam(props.orgName, teamName)
107108
const result = await listTeamUsers(scopeTeam)
108109
if (result) {
109110
teamUsers.value[teamName] = result
@@ -135,7 +136,7 @@ async function handleCreateTeam() {
135136
isCreatingTeam.value = true
136137
try {
137138
const teamName = newTeamName.value.trim()
138-
const scopeTeam = `${props.orgName}:${teamName}`
139+
const scopeTeam = buildScopeTeam(props.orgName, teamName)
139140
const operation: NewOperation = {
140141
type: 'team:create',
141142
params: { scopeTeam },
@@ -153,7 +154,7 @@ async function handleCreateTeam() {
153154
154155
// Destroy team
155156
async function handleDestroyTeam(teamName: string) {
156-
const scopeTeam = `${props.orgName}:${teamName}`
157+
const scopeTeam = buildScopeTeam(props.orgName, teamName)
157158
const operation: NewOperation = {
158159
type: 'team:destroy',
159160
params: { scopeTeam },
@@ -171,7 +172,7 @@ async function handleAddUser(teamName: string) {
171172
isAddingUser.value = true
172173
try {
173174
const username = newUserUsername.value.trim().replace(/^@/, '')
174-
const scopeTeam = `${props.orgName}:${teamName}`
175+
const scopeTeam = buildScopeTeam(props.orgName, teamName)
175176
176177
let dependsOnId: string | undefined
177178
@@ -213,7 +214,7 @@ async function handleAddUser(teamName: string) {
213214
214215
// Remove user from team
215216
async function handleRemoveUser(teamName: string, username: string) {
216-
const scopeTeam = `${props.orgName}:${teamName}`
217+
const scopeTeam = buildScopeTeam(props.orgName, teamName)
217218
const operation: NewOperation = {
218219
type: 'team:rm-user',
219220
params: { scopeTeam, user: username },

app/components/PackageAccessControls.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { NewOperation } from '~/composables/useConnector'
3+
import { buildScopeTeam } from '~/utils/npm'
34
45
const props = defineProps<{
56
packageName: string
@@ -96,7 +97,7 @@ async function handleGrantAccess() {
9697
9798
isGranting.value = true
9899
try {
99-
const scopeTeam = `${orgName.value}:${selectedTeam.value}`
100+
const scopeTeam = buildScopeTeam(orgName.value, selectedTeam.value)
100101
const operation: NewOperation = {
101102
type: 'access:grant',
102103
params: {

app/utils/npm.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Constructs a scope:team string in the format expected by npm.
3+
* npm operations require the format @scope:team (with @ prefix).
4+
*
5+
* @param orgName - The organization name (without @)
6+
* @param teamName - The team name
7+
* @returns The scope:team string in @scope:team format
8+
*/
9+
export function buildScopeTeam(orgName: string, teamName: string): string {
10+
return `@${orgName}:${teamName}`
11+
}

test/unit/npm-utils.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { buildScopeTeam } from '../../app/utils/npm'
4+
import { validateScopeTeam } from '../../cli/src/npm-client'
5+
6+
describe('buildScopeTeam', () => {
7+
it('constructs scope:team with @ prefix', () => {
8+
expect(buildScopeTeam('netlify', 'developers')).toBe('@netlify:developers')
9+
expect(buildScopeTeam('nuxt', 'core')).toBe('@nuxt:core')
10+
})
11+
12+
it('produces format accepted by validateScopeTeam', () => {
13+
expect(() => validateScopeTeam(buildScopeTeam('netlify', 'developers'))).not.toThrow()
14+
expect(() => validateScopeTeam(buildScopeTeam('nuxt', 'core'))).not.toThrow()
15+
expect(() => validateScopeTeam(buildScopeTeam('my-org', 'my-team'))).not.toThrow()
16+
})
17+
})

0 commit comments

Comments
 (0)