Skip to content

Commit 1433a80

Browse files
Prompt for an alias when logging in from other commands
1 parent ddc5dbf commit 1433a80

File tree

6 files changed

+105
-116
lines changed

6 files changed

+105
-116
lines changed

packages/cli-kit/src/private/node/session.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import {themeToken} from '../../public/node/context/local.js'
2525
import {partnersRequest} from '../../public/node/api/partners.js'
2626
import {getPartnersToken} from '../../public/node/environment.js'
2727
import {nonRandomUUID} from '../../public/node/crypto.js'
28+
import {renderTextPrompt} from '../../public/node/ui.js'
29+
import {terminalSupportsPrompting} from '../../public/node/system.js'
2830
import {vi, describe, expect, test, beforeEach} from 'vitest'
2931

3032
const futureDate = new Date(2022, 1, 1, 11)
@@ -110,6 +112,8 @@ vi.mock('../../store')
110112
vi.mock('../../public/node/environment.js')
111113
vi.mock('./session/device-authorization')
112114
vi.mock('./conf-store')
115+
vi.mock('../../public/node/ui.js')
116+
vi.mock('../../public/node/system.js')
113117

114118
beforeEach(() => {
115119
vi.spyOn(fqdnModule, 'identityFqdn').mockResolvedValue(fqdn)
@@ -134,21 +138,29 @@ beforeEach(() => {
134138
interval: 5,
135139
})
136140
vi.mocked(pollForDeviceAuthorization).mockResolvedValue(validIdentityToken)
141+
vi.mocked(renderTextPrompt).mockResolvedValue(userId)
142+
vi.mocked(terminalSupportsPrompting).mockReturnValue(true)
137143
})
138144

139145
describe('ensureAuthenticated when previous session is invalid', () => {
140146
test('executes complete auth flow if there is no session', async () => {
141147
// Given
142148
vi.mocked(validateSession).mockResolvedValueOnce('needs_full_auth')
143149
vi.mocked(fetchSessions).mockResolvedValue(undefined)
150+
vi.mocked(renderTextPrompt).mockResolvedValue(userId)
144151

145152
// When
146153
const got = await ensureAuthenticated(defaultApplications)
147154

148155
// Then
149156
expect(exchangeAccessForApplicationTokens).toBeCalled()
150157
expect(refreshAccessToken).not.toBeCalled()
151-
expect(storeSessions).toBeCalledWith(validSessions)
158+
expect(renderTextPrompt).toHaveBeenCalledWith({
159+
message: 'Enter an alias to identify this account',
160+
defaultValue: userId,
161+
allowEmpty: false,
162+
})
163+
expect(storeSessions).toHaveBeenCalledOnce()
152164
expect(got).toEqual(validTokens)
153165

154166
// The userID is cached in memory and the secureStore is not accessed again
@@ -457,6 +469,7 @@ describe('ensureAuthenticated alias functionality', () => {
457469
// Given
458470
vi.mocked(validateSession).mockResolvedValueOnce('needs_full_auth')
459471
vi.mocked(fetchSessions).mockResolvedValue(undefined)
472+
vi.mocked(renderTextPrompt).mockResolvedValue('work-account')
460473
const expectedSessionWithAlias = {
461474
...validSessions,
462475
[fqdn]: {
@@ -522,6 +535,7 @@ describe('ensureAuthenticated alias functionality', () => {
522535
vi.mocked(validateSession).mockResolvedValueOnce('needs_refresh')
523536
vi.mocked(fetchSessions).mockResolvedValue(validSessions)
524537
vi.mocked(refreshAccessToken).mockRejectedValueOnce(tokenResponseError)
538+
vi.mocked(renderTextPrompt).mockResolvedValue('fallback-alias')
525539
const expectedSessionWithAlias = {
526540
...validSessions,
527541
[fqdn]: {

packages/cli-kit/src/private/node/session.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {getIdentityTokenInformation, getPartnersToken} from '../../public/node/e
2222
import {AdminSession} from '../../public/node/session.js'
2323
import {nonRandomUUID} from '../../public/node/crypto.js'
2424
import {isEmpty} from '../../public/common/object.js'
25+
import {renderTextPrompt} from '../../public/node/ui.js'
2526

2627
/**
2728
* A scope supported by the Shopify Admin API.
@@ -211,7 +212,7 @@ ${outputToken.json(applications)}
211212
if (validationResult === 'needs_full_auth') {
212213
throwOnNoPrompt(noPrompt)
213214
outputDebug(outputContent`Initiating the full authentication flow...`)
214-
newSession = await executeCompleteFlow(applications)
215+
newSession = await executeCompleteFlow(applications, alias)
215216
} else if (validationResult === 'needs_refresh' || forceRefresh) {
216217
outputDebug(outputContent`The current session is valid but needs refresh. Refreshing...`)
217218
try {
@@ -220,7 +221,7 @@ ${outputToken.json(applications)}
220221
} catch (error) {
221222
if (error instanceof InvalidGrantError) {
222223
throwOnNoPrompt(noPrompt)
223-
newSession = await executeCompleteFlow(applications)
224+
newSession = await executeCompleteFlow(applications, alias)
224225
} else if (error instanceof InvalidRequestError) {
225226
await sessionStore.remove()
226227
throw new AbortError('\nError validating auth session', "We've cleared the current session, please try again")
@@ -271,8 +272,9 @@ The CLI is currently unable to prompt for reauthentication.`,
271272
* Execute the full authentication flow.
272273
*
273274
* @param applications - An object containing the applications we need to be authenticated with.
275+
* @param alias - Optional alias to use for the session.
274276
*/
275-
async function executeCompleteFlow(applications: OAuthApplications): Promise<Session> {
277+
async function executeCompleteFlow(applications: OAuthApplications, alias?: string): Promise<Session> {
276278
const scopes = getFlattenScopes(applications)
277279
const exchangeScopes = getExchangeScopes(applications)
278280
const store = applications.adminApi?.storeFqdn
@@ -299,12 +301,22 @@ async function executeCompleteFlow(applications: OAuthApplications): Promise<Ses
299301
outputDebug(outputContent`CLI token received. Exchanging it for application tokens...`)
300302
const result = await exchangeAccessForApplicationTokens(identityToken, exchangeScopes, store)
301303

304+
const defaultAlias = alias ?? identityToken.userId
305+
const finalAlias = await renderTextPrompt({
306+
message: 'Enter an alias to identify this account',
307+
defaultValue: defaultAlias,
308+
allowEmpty: false,
309+
})
310+
302311
const session: Session = {
303-
identity: identityToken,
312+
identity: {
313+
...identityToken,
314+
alias: finalAlias,
315+
},
304316
applications: result,
305317
}
306318

307-
outputCompleted('Logged in.')
319+
outputCompleted(`Logged in.`)
308320

309321
return session
310322
}

packages/cli-kit/src/public/node/session-prompt.test.ts

Lines changed: 21 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {promptSessionSelect} from './session-prompt.js'
2-
import {renderSelectPrompt, renderTextPrompt} from './ui.js'
2+
import {renderSelectPrompt} from './ui.js'
33
import {ensureAuthenticatedUser} from './session.js'
44
import {identityFqdn} from './context/fqdn.js'
55
import {setCurrentSessionId} from '../../private/node/conf-store.js'
@@ -45,8 +45,7 @@ describe('promptSessionSelect', () => {
4545
beforeEach(() => {
4646
vi.mocked(identityFqdn).mockResolvedValue('identity.fqdn.com')
4747
vi.mocked(ensureAuthenticatedUser).mockResolvedValue({userId: 'new-user-id'})
48-
vi.mocked(sessionStore.updateSessionAlias).mockResolvedValue()
49-
vi.mocked(renderTextPrompt).mockResolvedValue('new-alias')
48+
vi.mocked(sessionStore.getSessionAlias).mockResolvedValue('new-alias')
5049
vi.mocked(sessionStore.findSessionByAlias).mockResolvedValue(undefined)
5150
})
5251

@@ -59,34 +58,24 @@ describe('promptSessionSelect', () => {
5958

6059
// Then
6160
expect(renderSelectPrompt).not.toHaveBeenCalled()
62-
expect(ensureAuthenticatedUser).toHaveBeenCalledWith({}, {forceNewSession: true})
63-
expect(renderTextPrompt).toHaveBeenCalledWith({
64-
message: 'Enter an alias for this account',
65-
defaultValue: '',
66-
allowEmpty: false,
67-
})
68-
expect(sessionStore.updateSessionAlias).toHaveBeenCalledWith('new-user-id', 'new-alias')
69-
expect(result).toEqual({userId: 'new-user-id'})
61+
expect(ensureAuthenticatedUser).toHaveBeenCalledWith({}, {forceNewSession: true, alias: undefined})
62+
expect(sessionStore.getSessionAlias).toHaveBeenCalledWith('new-user-id')
63+
expect(result).toEqual('new-alias')
7064
})
7165

7266
test('prompts user to create new session with alias when no existing sessions', async () => {
7367
// Given
7468
vi.mocked(sessionStore.fetch).mockResolvedValue(undefined)
75-
vi.mocked(renderTextPrompt).mockResolvedValue('custom-alias')
69+
vi.mocked(sessionStore.getSessionAlias).mockResolvedValue('custom-alias')
7670

7771
// When
7872
const result = await promptSessionSelect('my-alias')
7973

8074
// Then
8175
expect(renderSelectPrompt).not.toHaveBeenCalled()
82-
expect(ensureAuthenticatedUser).toHaveBeenCalledWith({}, {forceNewSession: true})
83-
expect(renderTextPrompt).toHaveBeenCalledWith({
84-
message: 'Enter an alias for this account',
85-
defaultValue: 'my-alias',
86-
allowEmpty: false,
87-
})
88-
expect(sessionStore.updateSessionAlias).toHaveBeenCalledWith('new-user-id', 'custom-alias')
89-
expect(result).toEqual({userId: 'new-user-id'})
76+
expect(ensureAuthenticatedUser).toHaveBeenCalledWith({}, {forceNewSession: true, alias: 'my-alias'})
77+
expect(sessionStore.getSessionAlias).toHaveBeenCalledWith('new-user-id')
78+
expect(result).toEqual('custom-alias')
9079
})
9180

9281
test('shows existing sessions and allows selection', async () => {
@@ -107,7 +96,7 @@ describe('promptSessionSelect', () => {
10796
],
10897
})
10998
expect(setCurrentSessionId).toHaveBeenCalledWith('user1')
110-
expect(result).toEqual({userId: 'user1'})
99+
expect(result).toEqual('Work Account')
111100
})
112101

113102
test('handles missing alias in existing session gracefully', async () => {
@@ -144,7 +133,7 @@ describe('promptSessionSelect', () => {
144133
],
145134
})
146135
expect(setCurrentSessionId).toHaveBeenCalledWith('user3')
147-
expect(result).toEqual({userId: 'user3'})
136+
expect(result).toEqual('user3')
148137
})
149138

150139
test('creates new session when user selects "Log in with a different account"', async () => {
@@ -156,48 +145,24 @@ describe('promptSessionSelect', () => {
156145
const result = await promptSessionSelect()
157146

158147
// Then
159-
expect(ensureAuthenticatedUser).toHaveBeenCalledWith({}, {forceNewSession: true})
160-
expect(renderTextPrompt).toHaveBeenCalledWith({
161-
message: 'Enter an alias for this account',
162-
defaultValue: '',
163-
allowEmpty: false,
164-
})
165-
expect(sessionStore.updateSessionAlias).toHaveBeenCalledWith('new-user-id', 'new-alias')
166-
expect(result).toEqual({userId: 'new-user-id'})
148+
expect(ensureAuthenticatedUser).toHaveBeenCalledWith({}, {forceNewSession: true, alias: undefined})
149+
expect(sessionStore.getSessionAlias).toHaveBeenCalledWith('new-user-id')
150+
expect(result).toEqual('new-alias')
167151
})
168152

169153
test('creates new session with alias when user selects "Log in with a different account"', async () => {
170154
// Given
171155
vi.mocked(sessionStore.fetch).mockResolvedValue(mockSessions)
172156
vi.mocked(renderSelectPrompt).mockResolvedValue('NEW_LOGIN')
173-
vi.mocked(renderTextPrompt).mockResolvedValue('custom-alias')
157+
vi.mocked(sessionStore.getSessionAlias).mockResolvedValue('custom-alias')
174158

175159
// When
176160
const result = await promptSessionSelect('work-alias')
177161

178162
// Then
179-
expect(ensureAuthenticatedUser).toHaveBeenCalledWith({}, {forceNewSession: true})
180-
expect(renderTextPrompt).toHaveBeenCalledWith({
181-
message: 'Enter an alias for this account',
182-
defaultValue: 'work-alias',
183-
allowEmpty: false,
184-
})
185-
expect(sessionStore.updateSessionAlias).toHaveBeenCalledWith('new-user-id', 'custom-alias')
186-
expect(result).toEqual({userId: 'new-user-id'})
187-
})
188-
189-
test('updates alias for existing session when provided', async () => {
190-
// Given
191-
vi.mocked(sessionStore.fetch).mockResolvedValue(mockSessions)
192-
vi.mocked(renderSelectPrompt).mockResolvedValue('user1')
193-
194-
// When
195-
const result = await promptSessionSelect('updated-alias')
196-
197-
// Then
198-
expect(setCurrentSessionId).toHaveBeenCalledWith('user1')
199-
expect(sessionStore.updateSessionAlias).toHaveBeenCalledWith('user1', 'updated-alias')
200-
expect(result).toEqual({userId: 'user1'})
163+
expect(ensureAuthenticatedUser).toHaveBeenCalledWith({}, {forceNewSession: true, alias: 'work-alias'})
164+
expect(sessionStore.getSessionAlias).toHaveBeenCalledWith('new-user-id')
165+
expect(result).toEqual('custom-alias')
201166
})
202167

203168
test('does not update alias for existing session when not provided', async () => {
@@ -211,7 +176,7 @@ describe('promptSessionSelect', () => {
211176
// Then
212177
expect(setCurrentSessionId).toHaveBeenCalledWith('user1')
213178
expect(sessionStore.updateSessionAlias).not.toHaveBeenCalled()
214-
expect(result).toEqual({userId: 'user1'})
179+
expect(result).toEqual('Work Account')
215180
})
216181

217182
test('switches to existing session when alias is provided and found', async () => {
@@ -227,7 +192,7 @@ describe('promptSessionSelect', () => {
227192
expect(setCurrentSessionId).toHaveBeenCalledWith('user1')
228193
expect(renderSelectPrompt).not.toHaveBeenCalled()
229194
expect(ensureAuthenticatedUser).not.toHaveBeenCalled()
230-
expect(result).toEqual({userId: 'user1'})
195+
expect(result).toEqual('Work Account')
231196
})
232197

233198
test('shows session selection when alias is not found', async () => {
@@ -243,6 +208,6 @@ describe('promptSessionSelect', () => {
243208
expect(sessionStore.findSessionByAlias).toHaveBeenCalledWith('Non-existent Alias')
244209
expect(renderSelectPrompt).toHaveBeenCalled()
245210
expect(setCurrentSessionId).toHaveBeenCalledWith('user2')
246-
expect(result).toEqual({userId: 'user2'})
211+
expect(result).toEqual('user2')
247212
})
248213
})

0 commit comments

Comments
 (0)