Skip to content

Commit 202d860

Browse files
committed
feat(auth): support fine-grained personal access tokens
Signed-off-by: Adam Setch <[email protected]>
1 parent 6a43886 commit 202d860

File tree

5 files changed

+138
-18
lines changed

5 files changed

+138
-18
lines changed

src/routes/LoginWithPersonalAccessToken.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe('routes/LoginWithPersonalAccessToken.tsx', () => {
8989
target: { value: '' },
9090
});
9191

92-
fireEvent.click(screen.getByText('Generate a PAT'));
92+
fireEvent.click(screen.getByText('Generate a PAT (classic)'));
9393

9494
expect(openExternalLinkMock).toHaveBeenCalledTimes(0);
9595
});
@@ -107,7 +107,7 @@ describe('routes/LoginWithPersonalAccessToken.tsx', () => {
107107
</AppContext.Provider>,
108108
);
109109

110-
fireEvent.click(screen.getByText('Generate a PAT'));
110+
fireEvent.click(screen.getByText('Generate a PAT (classic)'));
111111

112112
expect(openExternalLinkMock).toHaveBeenCalledTimes(1);
113113
});

src/routes/LoginWithPersonalAccessToken.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import { AppContext } from '../context/App';
1010
import { type Hostname, Size, type Token } from '../types';
1111
import type { LoginPersonalAccessTokenOptions } from '../utils/auth/types';
1212
import {
13-
getNewTokenURL,
13+
getNewClassicTokenURL,
14+
getNewFineGrainedTokenURL,
15+
isValidFineGrainedToken,
1416
isValidHostname,
1517
isValidToken,
1618
} from '../utils/auth/utils';
@@ -37,7 +39,10 @@ export const validate = (values: IValues): IFormErrors => {
3739

3840
if (!values.token) {
3941
errors.token = 'Required';
40-
} else if (!isValidToken(values.token)) {
42+
} else if (
43+
!isValidToken(values.token) &&
44+
!isValidFineGrainedToken(values.token)
45+
) {
4146
errors.token = 'Invalid token.';
4247
}
4348

@@ -65,13 +70,23 @@ export const LoginWithPersonalAccessToken: FC = () => {
6570
</div>
6671
<div className="mt-3">
6772
<Button
68-
label="Generate a PAT"
73+
label="Generate a PAT (classic)"
6974
disabled={!values.hostname}
7075
icon={{ icon: KeyIcon, size: Size.XSMALL }}
71-
url={getNewTokenURL(values.hostname)}
76+
url={getNewClassicTokenURL(values.hostname)}
7277
size="xs"
7378
>
74-
Generate a PAT
79+
Generate a PAT (classic)
80+
</Button>{' '}
81+
or{' '}
82+
<Button
83+
label="Generate a PAT (fine-grained)"
84+
disabled={!values.hostname}
85+
icon={{ icon: KeyIcon, size: Size.XSMALL }}
86+
url={getNewFineGrainedTokenURL(values.hostname)}
87+
size="xs"
88+
>
89+
Generate a PAT (fine-grained)
7590
</Button>
7691
<span className="mx-1">
7792
on GitHub then paste your{' '}

src/routes/__snapshots__/LoginWithPersonalAccessToken.test.tsx.snap

Lines changed: 52 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/utils/auth/utils.test.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ import type {
1818
import * as apiRequests from '../api/request';
1919
import type { AuthMethod } from './types';
2020
import * as auth from './utils';
21-
import { getNewOAuthAppURL, getNewTokenURL } from './utils';
21+
import {
22+
getNewClassicTokenURL,
23+
getNewFineGrainedTokenURL,
24+
getNewOAuthAppURL,
25+
} from './utils';
2226

2327
const browserWindow = new remote.BrowserWindow();
2428

@@ -283,24 +287,42 @@ describe('utils/auth/utils.ts', () => {
283287
).toBe('https://github.com/settings');
284288
});
285289

286-
describe('getNewTokenURL', () => {
287-
it('should generate new PAT url - github cloud', () => {
290+
describe('getNewClassicTokenURL', () => {
291+
it('should generate new PAT (classic) url - github cloud', () => {
288292
expect(
289-
getNewTokenURL('github.com' as Hostname).startsWith(
293+
getNewClassicTokenURL('github.com' as Hostname).startsWith(
290294
'https://github.com/settings/tokens/new',
291295
),
292296
).toBeTruthy();
293297
});
294298

295-
it('should generate new PAT url - github server', () => {
299+
it('should generate new PAT (classic) url - github server', () => {
296300
expect(
297-
getNewTokenURL('github.gitify.io' as Hostname).startsWith(
301+
getNewClassicTokenURL('github.gitify.io' as Hostname).startsWith(
298302
'https://github.gitify.io/settings/tokens/new',
299303
),
300304
).toBeTruthy();
301305
});
302306
});
303307

308+
describe('getNewFineGrainedTokenURL', () => {
309+
it('should generate new PAT (fine-grained) url - github cloud', () => {
310+
expect(
311+
getNewFineGrainedTokenURL('github.com' as Hostname).startsWith(
312+
'https://github.com/settings/personal-access-tokens/new',
313+
),
314+
).toBeTruthy();
315+
});
316+
317+
it('should generate new PAT (fine-grained) url - github server', () => {
318+
expect(
319+
getNewFineGrainedTokenURL('github.gitify.io' as Hostname).startsWith(
320+
'https://github.gitify.io/settings/personal-access-tokens/new',
321+
),
322+
).toBeTruthy();
323+
});
324+
});
325+
304326
describe('getNewOAuthAppURL', () => {
305327
it('should generate new oauth app url - github cloud', () => {
306328
expect(
@@ -369,6 +391,26 @@ describe('utils/auth/utils.ts', () => {
369391
});
370392
});
371393

394+
describe('isValidFineGrainedToken', () => {
395+
it('should validate fine-grained token - valid', () => {
396+
expect(
397+
auth.isValidFineGrainedToken(
398+
'github_pat_1234567890123456789012_asdfghjklPOIUYTREWQ0987654321asdfghjklasdfghjklasdfghjkl123' as Token,
399+
),
400+
).toBeTruthy();
401+
});
402+
403+
it('should validate fine-grained token - empty', () => {
404+
expect(auth.isValidFineGrainedToken('' as Token)).toBeFalsy();
405+
});
406+
407+
it('should validate fine-grained token - invalid', () => {
408+
expect(
409+
auth.isValidFineGrainedToken('1234567890asdfg' as Token),
410+
).toBeFalsy();
411+
});
412+
});
413+
372414
describe('hasAccounts', () => {
373415
it('should return true', () => {
374416
expect(auth.hasAccounts(mockAuth)).toBeTruthy();

src/utils/auth/utils.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ export function getDeveloperSettingsURL(account: Account): Link {
194194
return settingsURL.toString() as Link;
195195
}
196196

197-
export function getNewTokenURL(hostname: Hostname): Link {
197+
export function getNewClassicTokenURL(hostname: Hostname): Link {
198198
const date = format(new Date(), 'PP p');
199199
const newTokenURL = new URL(`https://${hostname}/settings/tokens/new`);
200200
newTokenURL.searchParams.append('description', `Gitify (Created on ${date})`);
@@ -203,6 +203,17 @@ export function getNewTokenURL(hostname: Hostname): Link {
203203
return newTokenURL.toString() as Link;
204204
}
205205

206+
export function getNewFineGrainedTokenURL(hostname: Hostname): Link {
207+
const date = format(new Date(), 'PP p');
208+
const newTokenURL = new URL(
209+
`https://${hostname}/settings/personal-access-tokens/new`,
210+
);
211+
newTokenURL.searchParams.append('description', `Gitify (Created on ${date})`);
212+
newTokenURL.searchParams.append('scopes', Constants.AUTH_SCOPE.join(','));
213+
214+
return newTokenURL.toString() as Link;
215+
}
216+
206217
export function getNewOAuthAppURL(hostname: Hostname): Link {
207218
const date = format(new Date(), 'PP p');
208219
const newOAuthAppURL = new URL(
@@ -238,6 +249,10 @@ export function isValidToken(token: Token) {
238249
return /^[A-Z0-9_]{40}$/i.test(token);
239250
}
240251

252+
export function isValidFineGrainedToken(token: Token) {
253+
return /^github_pat_[A-Z0-9]{22}_[A-Z0-9]{59}$/i.test(token);
254+
}
255+
241256
export function getAccountUUID(account: Account): string {
242257
return btoa(`${account.hostname}-${account.user.id}-${account.method}`);
243258
}

0 commit comments

Comments
 (0)