Skip to content

Commit 3f6ed26

Browse files
authored
refactor(auth): use @octokit/oauth-methods (#2523)
* refactor(auth): migrate to octokit/oauth-method library Signed-off-by: Adam Setch <adam.setch@outlook.com> * refactor(auth): migrate to octokit/oauth-method library Signed-off-by: Adam Setch <adam.setch@outlook.com> * refactor(auth): migrate to octokit/oauth-method library Signed-off-by: Adam Setch <adam.setch@outlook.com> * refactor(auth): migrate to octokit/oauth-method library Signed-off-by: Adam Setch <adam.setch@outlook.com> * refactor(auth): migrate to octokit/oauth-method library Signed-off-by: Adam Setch <adam.setch@outlook.com> * refactor(auth): migrate to octokit/oauth-method library Signed-off-by: Adam Setch <adam.setch@outlook.com> * move to shared constant Signed-off-by: Adam Setch <adam.setch@outlook.com> --------- Signed-off-by: Adam Setch <adam.setch@outlook.com>
1 parent 1bc6439 commit 3f6ed26

File tree

18 files changed

+216
-137
lines changed

18 files changed

+216
-137
lines changed

jest.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ const config: Config = {
1414
},
1515
// Allow transforming specific ESM packages in node_modules that ship untranspiled ESM.
1616
// @primer/* libraries rely on lit and @lit-labs/react internally for some components.
17+
// @octokit/* libraries rely on universal-user-agent internally.
1718
// We also include GitHub web components that ship ESM-only builds.
1819
transformIgnorePatterns: [
19-
'node_modules/(?!(?:@primer/react|@primer/primitives|@primer/octicons-react|@lit-labs/react|lit|@github/relative-time-element|@github/tab-container-element)/)',
20+
'node_modules/(?!(?:@primer/react|@primer/primitives|@primer/octicons-react|@lit-labs/react|lit|@github/relative-time-element|@github/tab-container-element|@octokit/oauth-methods|@octokit/oauth-authorization-url|@octokit/request|@octokit/request-error|@octokit/endpoint|universal-user-agent)/)',
2021
],
2122
moduleNameMapper: {
2223
// Force CommonJS build for http adapter to be available.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@
8484
"@electron/notarize": "3.1.1",
8585
"@graphql-codegen/cli": "6.1.1",
8686
"@graphql-codegen/schema-ast": "5.0.0",
87+
"@octokit/oauth-methods": "6.0.2",
8788
"@octokit/openapi-types": "27.0.0",
89+
"@octokit/request": "10.0.7",
8890
"@parcel/watcher": "2.5.4",
8991
"@primer/css": "22.1.0",
9092
"@primer/octicons-react": "19.21.2",

pnpm-lock.yaml

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

src/main/menu.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ export default class MenuBuilder {
7878
{
7979
label: 'Visit Repository',
8080
click: () => {
81-
shell.openExternal(`https://github.com/${APPLICATION.REPO_SLUG}`);
81+
shell.openExternal(
82+
`${APPLICATION.GITHUB_BASE_URL}/${APPLICATION.REPO_SLUG}`,
83+
);
8284
},
8385
},
8486
{

src/renderer/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ClientID, ClientSecret, Hostname, Link } from './types';
2+
import type { LoginOAuthAppOptions } from './utils/auth/types';
23

34
export const Constants = {
45
STORAGE_KEY: 'gitify-storage',
@@ -13,7 +14,7 @@ export const Constants = {
1314
hostname: 'github.com' as Hostname,
1415
clientId: process.env.OAUTH_CLIENT_ID as ClientID,
1516
clientSecret: process.env.OAUTH_CLIENT_SECRET as ClientSecret,
16-
},
17+
} satisfies LoginOAuthAppOptions,
1718

1819
GITHUB_API_BASE_URL: 'https://api.github.com',
1920
GITHUB_API_GRAPHQL_URL: 'https://api.github.com/graphql',

src/renderer/context/App.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ import type {
3939
import { headNotifications } from '../utils/api/client';
4040
import {
4141
addAccount,
42-
authGitHub,
42+
exchangeAuthCodeForAccessToken,
4343
getAccountUUID,
44-
getToken,
4544
hasAccounts,
45+
performGitHubOAuth,
4646
refreshAccount,
4747
removeAccount,
4848
} from '../utils/auth/utils';
@@ -395,28 +395,45 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
395395
return hasAccounts(auth);
396396
}, [auth]);
397397

398+
/**
399+
* Login with GitHub App.
400+
*
401+
* Note: although we call this "Login with GitHub App", this function actually
402+
* authenticates via a predefined "Gitify" GitHub OAuth App.
403+
*/
398404
const loginWithGitHubApp = useCallback(async () => {
399-
const { authCode } = await authGitHub();
400-
const { token } = await getToken(authCode);
405+
const { authCode } = await performGitHubOAuth();
406+
const token = await exchangeAuthCodeForAccessToken(authCode);
401407
const hostname = Constants.DEFAULT_AUTH_OPTIONS.hostname;
402408

403409
const updatedAuth = await addAccount(auth, 'GitHub App', token, hostname);
404410

405411
persistAuth(updatedAuth);
406412
}, [auth, persistAuth]);
407413

414+
/**
415+
* Login with custom GitHub OAuth App.
416+
*/
408417
const loginWithOAuthApp = useCallback(
409418
async (data: LoginOAuthAppOptions) => {
410-
const { authOptions, authCode } = await authGitHub(data);
411-
const { token, hostname } = await getToken(authCode, authOptions);
419+
const { authOptions, authCode } = await performGitHubOAuth(data);
420+
const token = await exchangeAuthCodeForAccessToken(authCode, authOptions);
412421

413-
const updatedAuth = await addAccount(auth, 'OAuth App', token, hostname);
422+
const updatedAuth = await addAccount(
423+
auth,
424+
'OAuth App',
425+
token,
426+
authOptions.hostname,
427+
);
414428

415429
persistAuth(updatedAuth);
416430
},
417431
[auth, persistAuth],
418432
);
419433

434+
/**
435+
* Login with Personal Access Token (PAT).
436+
*/
420437
const loginWithPersonalAccessToken = useCallback(
421438
async ({ token, hostname }: LoginPersonalAccessTokenOptions) => {
422439
const encryptedToken = (await encryptValue(token)) as Token;

src/renderer/utils/api/graphql/generated/gql.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ type Documents = {
2020
"query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n ...IssueDetails\n }\n }\n}\n\nfragment IssueDetails on Issue {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}": typeof types.FetchIssueByNumberDocument,
2121
"query FetchMergedDetailsTemplate($ownerINDEX: String!, $nameINDEX: String!, $numberINDEX: Int!, $isDiscussionNotificationINDEX: Boolean!, $isIssueNotificationINDEX: Boolean!, $isPullRequestNotificationINDEX: Boolean!, $lastComments: Int, $lastThreadedComments: Int, $lastReplies: Int, $lastReviews: Int, $firstLabels: Int, $firstClosingIssues: Int, $includeIsAnswered: Boolean!) {\n ...MergedDetailsQueryTemplate\n}\n\nfragment MergedDetailsQueryTemplate on Query {\n repository(owner: $ownerINDEX, name: $nameINDEX) {\n discussion(number: $numberINDEX) @include(if: $isDiscussionNotificationINDEX) {\n ...DiscussionDetails\n }\n issue(number: $numberINDEX) @include(if: $isIssueNotificationINDEX) {\n ...IssueDetails\n }\n pullRequest(number: $numberINDEX) @include(if: $isPullRequestNotificationINDEX) {\n ...PullRequestDetails\n }\n }\n}": typeof types.FetchMergedDetailsTemplateDocument,
2222
"query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n ...PullRequestDetails\n }\n }\n}\n\nfragment PullRequestDetails on PullRequest {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: $lastReviews) {\n totalCount\n nodes {\n ...PullRequestReviewFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: $firstClosingIssues) {\n nodes {\n number\n }\n }\n}\n\nfragment PullRequestReviewFields on PullRequestReview {\n state\n author {\n login\n }\n}": typeof types.FetchPullRequestByNumberDocument,
23-
"query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatarUrl\n }\n}": typeof types.FetchAuthenticatedUserDetailsDocument,
23+
"query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatar: avatarUrl\n }\n}": typeof types.FetchAuthenticatedUserDetailsDocument,
2424
};
2525
const documents: Documents = {
2626
"fragment AuthorFields on Actor {\n login\n htmlUrl: url\n avatarUrl: avatarUrl\n type: __typename\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.AuthorFieldsFragmentDoc,
2727
"query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastThreadedComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n ...DiscussionDetails\n }\n }\n}\n\nfragment DiscussionDetails on Discussion {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastThreadedComments) {\n totalCount\n nodes {\n ...DiscussionCommentFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}\n\nfragment DiscussionCommentFields on DiscussionComment {\n ...CommentFields\n replies(last: $lastReplies) {\n totalCount\n nodes {\n ...CommentFields\n }\n }\n}": types.FetchDiscussionByNumberDocument,
2828
"query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n ...IssueDetails\n }\n }\n}\n\nfragment IssueDetails on Issue {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}": types.FetchIssueByNumberDocument,
2929
"query FetchMergedDetailsTemplate($ownerINDEX: String!, $nameINDEX: String!, $numberINDEX: Int!, $isDiscussionNotificationINDEX: Boolean!, $isIssueNotificationINDEX: Boolean!, $isPullRequestNotificationINDEX: Boolean!, $lastComments: Int, $lastThreadedComments: Int, $lastReplies: Int, $lastReviews: Int, $firstLabels: Int, $firstClosingIssues: Int, $includeIsAnswered: Boolean!) {\n ...MergedDetailsQueryTemplate\n}\n\nfragment MergedDetailsQueryTemplate on Query {\n repository(owner: $ownerINDEX, name: $nameINDEX) {\n discussion(number: $numberINDEX) @include(if: $isDiscussionNotificationINDEX) {\n ...DiscussionDetails\n }\n issue(number: $numberINDEX) @include(if: $isIssueNotificationINDEX) {\n ...IssueDetails\n }\n pullRequest(number: $numberINDEX) @include(if: $isPullRequestNotificationINDEX) {\n ...PullRequestDetails\n }\n }\n}": types.FetchMergedDetailsTemplateDocument,
3030
"query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n ...PullRequestDetails\n }\n }\n}\n\nfragment PullRequestDetails on PullRequest {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: $lastReviews) {\n totalCount\n nodes {\n ...PullRequestReviewFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: $firstClosingIssues) {\n nodes {\n number\n }\n }\n}\n\nfragment PullRequestReviewFields on PullRequestReview {\n state\n author {\n login\n }\n}": types.FetchPullRequestByNumberDocument,
31-
"query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatarUrl\n }\n}": types.FetchAuthenticatedUserDetailsDocument,
31+
"query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatar: avatarUrl\n }\n}": types.FetchAuthenticatedUserDetailsDocument,
3232
};
3333

3434
/**
@@ -54,7 +54,7 @@ export function graphql(source: "query FetchPullRequestByNumber($owner: String!,
5454
/**
5555
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
5656
*/
57-
export function graphql(source: "query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatarUrl\n }\n}"): typeof import('./graphql').FetchAuthenticatedUserDetailsDocument;
57+
export function graphql(source: "query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatar: avatarUrl\n }\n}"): typeof import('./graphql').FetchAuthenticatedUserDetailsDocument;
5858

5959

6060
export function graphql(source: string) {

src/renderer/utils/api/graphql/generated/graphql.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35855,7 +35855,7 @@ export type PullRequestReviewFieldsFragment = { __typename?: 'PullRequestReview'
3585535855
export type FetchAuthenticatedUserDetailsQueryVariables = Exact<{ [key: string]: never; }>;
3585635856

3585735857

35858-
export type FetchAuthenticatedUserDetailsQuery = { __typename?: 'Query', viewer: { __typename?: 'User', id: string, name?: string | null, login: string, avatarUrl: any } };
35858+
export type FetchAuthenticatedUserDetailsQuery = { __typename?: 'Query', viewer: { __typename?: 'User', id: string, name?: string | null, login: string, avatar: any } };
3585935859

3586035860
export class TypedDocumentString<TResult, TVariables>
3586135861
extends String
@@ -36528,7 +36528,7 @@ export const FetchAuthenticatedUserDetailsDocument = new TypedDocumentString(`
3652836528
id
3652936529
name
3653036530
login
36531-
avatarUrl
36531+
avatar: avatarUrl
3653236532
}
3653336533
}
3653436534
`) as unknown as TypedDocumentString<FetchAuthenticatedUserDetailsQuery, FetchAuthenticatedUserDetailsQueryVariables>;

src/renderer/utils/api/graphql/user.graphql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ query FetchAuthenticatedUserDetails {
33
id
44
name
55
login
6-
avatarUrl
6+
avatar: avatarUrl
77
}
88
}

src/renderer/utils/api/request.test.ts

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import type { Link } from '../../types';
1111

1212
import { FetchAuthenticatedUserDetailsDocument } from './graphql/generated/graphql';
1313
import {
14-
apiRequest,
1514
apiRequestAuth,
1615
getHeaders,
1716
performGraphQLRequest,
@@ -29,33 +28,6 @@ describe('renderer/utils/api/request.ts', () => {
2928
jest.clearAllMocks();
3029
});
3130

32-
describe('apiRequest', () => {
33-
it('should make a request with the correct parameters', async () => {
34-
const data = { key: 'value' };
35-
36-
await apiRequest(url, method, data);
37-
38-
expect(axios).toHaveBeenCalledWith({
39-
method,
40-
url,
41-
data,
42-
headers: mockNoAuthHeaders,
43-
});
44-
});
45-
46-
it('should make a request with the correct parameters and default data', async () => {
47-
const data = {};
48-
await apiRequest(url, method);
49-
50-
expect(axios).toHaveBeenCalledWith({
51-
method,
52-
url,
53-
data,
54-
headers: mockNoAuthHeaders,
55-
});
56-
});
57-
});
58-
5931
describe('apiRequestAuth', () => {
6032
afterEach(() => {
6133
jest.clearAllMocks();

0 commit comments

Comments
 (0)