Skip to content

Commit

Permalink
feat: token command
Browse files Browse the repository at this point in the history
  • Loading branch information
RostiMelk committed Dec 5, 2024
1 parent ff90128 commit 591f90c
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 93 deletions.
1 change: 1 addition & 0 deletions packages/@sanity/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@sanity/telemetry": "^0.7.7",
"@sanity/util": "3.65.1",
"chalk": "^4.1.2",
"cli-table3": "^0.6.5",
"debug": "^4.3.4",
"decompress": "^4.2.0",
"esbuild": "0.21.5",
Expand Down
51 changes: 51 additions & 0 deletions packages/@sanity/cli/src/actions/token/createToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {randomBytes} from 'node:crypto'

import {debug} from '../../debug'
import {type CliCommandArguments, type CliCommandContext} from '../../types'
import {API_VERSION, fetchRoles, selectProject} from './tokenUtils'

export async function createToken(
args: CliCommandArguments,
context: CliCommandContext,
): Promise<void> {
const {output, cliConfig, prompt, apiClient} = context
const {print} = output
const client = apiClient({requireUser: true, requireProject: false})

const projectId = cliConfig?.api?.projectId || (await selectProject(client, prompt))
const roles = await fetchRoles(client, projectId)

debug('Filtering roles that apply to robots')
const robotRoles = roles.filter((role) => role.appliesToRobots)
const roleChoices = robotRoles.map((role) => ({
value: role.name,
name: `${role.title} (${role.name})`,
}))

debug('Prompting for token role')
const roleName = await prompt.single({
message: 'Choose access level for the token:',
type: 'list',
choices: roleChoices,
default: 'viewer',
})

debug('Prompting for token label')
const selectedRole = robotRoles.find((role) => role.name === roleName)
const label = await prompt.single({
message: 'Give this token a descriptive name:',
type: 'input',
default: `${selectedRole?.title} token (${randomBytes(2).toString('hex')})`,
})

debug('Creating token')
const {key} = await client.config({apiVersion: API_VERSION}).request<{key: string}>({
uri: `/projects/${projectId}/tokens`,
method: 'POST',
body: {label, roleName},
})

print("New token created. Make sure to copy it now - you won't see it again:")
print('')
print(key)
}
56 changes: 56 additions & 0 deletions packages/@sanity/cli/src/actions/token/deleteToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {debug} from '../../debug'
import {type CliCommandArguments, type CliCommandContext} from '../../types'
import {fetchTokens, selectProject} from './tokenUtils'

export async function deleteToken(
args: CliCommandArguments,
context: CliCommandContext,
): Promise<void> {
const {output, cliConfig, prompt, apiClient} = context
const {print} = output
const client = apiClient({requireUser: true, requireProject: false})

const projectId = cliConfig?.api?.projectId || (await selectProject(client, prompt))
let tokenId = args.argsWithoutOptions[0]

if (!tokenId) {
const tokens = await fetchTokens(client, projectId)

if (tokens.length === 0) {
print('No tokens found in project')
return
}

tokens.sort((a, b) => b.createdAt.localeCompare(a.createdAt))

debug('No token ID provided, showing list of choices')
const tokenChoices = tokens.map((token) => ({
value: token.id,
name: `${token.label} (created ${new Date(token.createdAt).toLocaleString()})`,
}))

tokenId = await prompt.single({
message: 'Select token to delete',
type: 'list',
choices: tokenChoices,
})
}

const confirm = await prompt.single({
message: 'Are you sure you want to delete this token?',
type: 'confirm',
})

if (!confirm) {
print('Token deletion cancelled')
return
}

debug('Deleting token:', tokenId)
await client.config({apiVersion: 'v2021-06-07'}).request({
uri: `/projects/${projectId}/tokens/${tokenId}`,
method: 'DELETE',
})

print('Token deleted successfully')
}
39 changes: 39 additions & 0 deletions packages/@sanity/cli/src/actions/token/listTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Table from 'cli-table3'

import {type CliCommandArguments, type CliCommandContext} from '../../types'
import {fetchTokens, selectProject} from './tokenUtils'

export async function listTokens(
args: CliCommandArguments,
context: CliCommandContext,
): Promise<void> {
const {output, cliConfig, prompt, apiClient} = context
const {print} = output
const client = apiClient({requireUser: true, requireProject: false})

const projectId = cliConfig?.api?.projectId || (await selectProject(client, prompt))
const tokens = await fetchTokens(client, projectId)

if (tokens.length === 0) {
print('No tokens found in project')
return
}

const sortedTokens = tokens.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)

const table = new Table({
head: ['ID', 'Label', 'Roles', 'Created'],
colWidths: [16, 32, 18, 14],
style: {head: []}, // Remove the default header style
})

for (const token of sortedTokens) {
const roles = token.roles.map((role) => role.title).join(', ')
const date = new Date(token.createdAt).toLocaleDateString()
table.push([token.id, token.label, roles, date])
}

print(table.toString())
}
63 changes: 63 additions & 0 deletions packages/@sanity/cli/src/actions/token/tokenUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {type SanityClient} from '@sanity/client'

import {debug} from '../../debug'
import {type CliPrompter} from '../../types'

export const API_VERSION = 'v2021-06-07'

export type Token = {
id: string
label: string
projectUserId: string
createdAt: string
roles: Array<{
name: string
title: string
}>
}

export type Role = {
name: string
title: string
description: string
isCustom: boolean
projectId: string
appliesToUsers: boolean
appliesToRobots: boolean
}

export async function selectProject(client: SanityClient, prompt: CliPrompter): Promise<string> {
debug('No project ID in config, fetching projects list')
const projects = await client.projects
.list({includeMembers: false})
.then((allProjects) => allProjects.sort((a, b) => b.createdAt.localeCompare(a.createdAt)))

debug(`User has ${projects.length} project(s) already, showing list of choices`)

const projectChoices = projects.map((project) => ({
value: project.id,
name: `${project.displayName} [${project.id}]`,
}))

return prompt.single({
message: 'Select project to use',
type: 'list',
choices: projectChoices,
})
}

export async function fetchTokens(client: SanityClient, projectId: string): Promise<Token[]> {
debug('Fetching tokens for project:', projectId)
return client.config({apiVersion: API_VERSION}).request<Token[]>({
uri: `/projects/${projectId}/tokens`,
method: 'GET',
})
}

export async function fetchRoles(client: SanityClient, projectId: string): Promise<Role[]> {
debug('Fetching roles for project:', projectId)
return client.config({apiVersion: API_VERSION}).request<Role[]>({
uri: `/projects/${projectId}/roles`,
method: 'GET',
})
}
8 changes: 8 additions & 0 deletions packages/@sanity/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import disableTelemetryCommand from './telemetry/disableTelemetryCommand'
import enableTelemetryCommand from './telemetry/enableTelemetryCommand'
import telemetryGroup from './telemetry/telemetryGroup'
import telemetryStatusCommand from './telemetry/telemetryStatusCommand'
import tokenCreateCommand from './token/tokenCreateCommand'
import tokenDeleteCommand from './token/tokenDeleteCommand'
import tokenGroup from './token/tokenGroup'
import tokenListCommand from './token/tokenListCommand'
import generateTypegenCommand from './typegen/generateTypesCommand'
import typegenGroup from './typegen/typegenGroup'
import upgradeCommand from './upgrade/upgradeCommand'
Expand All @@ -41,4 +45,8 @@ export const baseCommands: (CliCommandDefinition | CliCommandGroupDefinition)[]
telemetryStatusCommand,
generateTypegenCommand,
typegenGroup,
tokenGroup,
tokenCreateCommand,
tokenDeleteCommand,
tokenListCommand,
]
13 changes: 13 additions & 0 deletions packages/@sanity/cli/src/commands/token/tokenCreateCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {createToken} from '../../actions/token/createToken'
import {type CliCommandDefinition} from '../../types'

const tokenCommand: CliCommandDefinition = {
name: 'create',
group: 'token',
signature: '',
helpText: 'Create a new API token for project',
description: 'Create a new API token for project',
action: createToken,
}

export default tokenCommand
13 changes: 13 additions & 0 deletions packages/@sanity/cli/src/commands/token/tokenDeleteCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {deleteToken} from '../../actions/token/deleteToken'
import {type CliCommandDefinition} from '../../types'

const tokenDeleteCommand: CliCommandDefinition = {
name: 'delete',
group: 'token',
signature: '[id]',
helpText: 'Delete an API token from project',
description: 'Delete an API token from project',
action: deleteToken,
}

export default tokenDeleteCommand
10 changes: 10 additions & 0 deletions packages/@sanity/cli/src/commands/token/tokenGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {type CliCommandGroupDefinition} from '../../types'

const tokenGroup: CliCommandGroupDefinition = {
name: 'token',
signature: '[COMMAND]',
isGroupRoot: true,
description: 'Manages project API tokens',
}

export default tokenGroup
24 changes: 24 additions & 0 deletions packages/@sanity/cli/src/commands/token/tokenListCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {listTokens} from '../../actions/token/listTokens'
import {type CliCommandDefinition} from '../../types'

type Token = {
id: string
label: string
projectUserId: string
createdAt: string
roles: Array<{
name: string
title: string
}>
}

const tokenListCommand: CliCommandDefinition = {
name: 'list',
group: 'token',
signature: '',
helpText: 'List all API tokens in project',
description: 'List all API tokens in project',
action: listTokens,
}

export default tokenListCommand
Loading

0 comments on commit 591f90c

Please sign in to comment.