Skip to content

Commit c67e7b3

Browse files
committed
feat: use system browser for oauth flow
Signed-off-by: Adam Setch <[email protected]>
1 parent a1d0ea7 commit c67e7b3

File tree

7 files changed

+72
-123
lines changed

7 files changed

+72
-123
lines changed

config/webpack.config.renderer.base.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ const configuration: webpack.Configuration = {
4242
plugins: [
4343
// Development Keys - See README.md
4444
new webpack.EnvironmentPlugin({
45-
OAUTH_CLIENT_ID: '3fef4433a29c6ad8f22c',
46-
OAUTH_CLIENT_SECRET: '9670de733096c15322183ff17ed0fc8704050379',
45+
OAUTH_CLIENT_ID: 'Ov23liQIkFs5ehQLNzHF',
46+
OAUTH_CLIENT_SECRET: '404b80632292e18419dbd2a6ed25976856e95255',
4747
}),
4848

4949
// Extract CSS into a separate file

src/main/main.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,14 +190,16 @@ app.whenReady().then(async () => {
190190
});
191191
});
192192

193+
// Handle custom protocol URL events for OAuth 2.0 callback
193194
app.on('open-url', (event, url) => {
194195
event.preventDefault();
195-
const code = new URL(url).searchParams.get('code'); // Extract the authorization code
196-
console.log('Authorization Code:', code);
197196

198-
if (code) {
199-
// Exchange the code for an access token
200-
mb.window.webContents.send(namespacedEvent('auth-code'), code);
201-
// exchangeCodeForToken(code);
197+
const link = new URL(url);
198+
199+
const type = link.hostname;
200+
const code = link.searchParams.get('code');
201+
202+
if (code && (type === 'auth' || type === 'oauth')) {
203+
mb.window.webContents.send(namespacedEvent('auth-code'), type, code);
202204
}
203205
});

src/renderer/context/App.tsx

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
type Account,
1818
type AccountNotifications,
1919
type AppearanceSettingsState,
20-
type AuthCode,
2120
type AuthState,
2221
type FilterSettingsState,
2322
type GitifyError,
@@ -114,7 +113,7 @@ export const defaultSettings: SettingsState = {
114113
interface AppContextState {
115114
auth: AuthState;
116115
isLoggedIn: boolean;
117-
loginWithGitHubApp: (authCode: AuthCode) => void;
116+
loginWithGitHubApp: () => void;
118117
loginWithOAuthApp: (data: LoginOAuthAppOptions) => void;
119118
loginWithPersonalAccessToken: (data: LoginPersonalAccessTokenOptions) => void;
120119
logoutFromAccount: (account: Account) => void;
@@ -237,17 +236,14 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
237236
return hasAccounts(auth);
238237
}, [auth]);
239238

240-
const loginWithGitHubApp = useCallback(
241-
async (authCode: AuthCode) => {
242-
// const { authCode } = await authGitHub();
243-
const { token } = await getToken(authCode);
244-
const hostname = Constants.DEFAULT_AUTH_OPTIONS.hostname;
245-
const updatedAuth = await addAccount(auth, 'GitHub App', token, hostname);
246-
setAuth(updatedAuth);
247-
saveState({ auth: updatedAuth, settings });
248-
},
249-
[auth, settings],
250-
);
239+
const loginWithGitHubApp = useCallback(async () => {
240+
const { authCode } = await authGitHub();
241+
const { token } = await getToken(authCode);
242+
const hostname = Constants.DEFAULT_AUTH_OPTIONS.hostname;
243+
const updatedAuth = await addAccount(auth, 'GitHub App', token, hostname);
244+
setAuth(updatedAuth);
245+
saveState({ auth: updatedAuth, settings });
246+
}, [auth, settings]);
251247

252248
const loginWithOAuthApp = useCallback(
253249
async (data: LoginOAuthAppOptions) => {

src/renderer/routes/Accounts.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,20 @@ import {
2222
Text,
2323
} from '@primer/react';
2424

25+
import { logError } from '../../shared/logger';
2526
import { AvatarWithFallback } from '../components/avatars/AvatarWithFallback';
2627
import { Contents } from '../components/layout/Contents';
2728
import { Page } from '../components/layout/Page';
2829
import { Footer } from '../components/primitives/Footer';
2930
import { Header } from '../components/primitives/Header';
3031
import { AppContext } from '../context/App';
31-
import { type Account, type Link, Size } from '../types';
32+
import { type Account, Size } from '../types';
3233
import {
3334
formatRequiredScopes,
3435
getAccountUUID,
3536
refreshAccount,
3637
} from '../utils/auth/utils';
37-
import {
38-
openExternalLink,
39-
updateTrayIcon,
40-
updateTrayTitle,
41-
} from '../utils/comms';
38+
import { updateTrayIcon, updateTrayTitle } from '../utils/comms';
4239
import { getAuthMethodIcon, getPlatformIcon } from '../utils/icons';
4340
import {
4441
openAccountProfile,
@@ -48,7 +45,8 @@ import {
4845
import { saveState } from '../utils/storage';
4946

5047
export const AccountsRoute: FC = () => {
51-
const { auth, settings, logoutFromAccount } = useContext(AppContext);
48+
const { auth, settings, loginWithGitHubApp, logoutFromAccount } =
49+
useContext(AppContext);
5250
const navigate = useNavigate();
5351

5452
const logoutAccount = useCallback(
@@ -67,6 +65,14 @@ export const AccountsRoute: FC = () => {
6765
navigate('/accounts', { replace: true });
6866
}, []);
6967

68+
const loginWithGitHub = useCallback(async () => {
69+
try {
70+
await loginWithGitHubApp();
71+
} catch (err) {
72+
logError('loginWithGitHub', 'failed to login with GitHub', err);
73+
}
74+
}, []);
75+
7076
const loginWithPersonalAccessToken = useCallback(() => {
7177
return navigate('/login-personal-access-token', { replace: true });
7278
}, []);
@@ -231,7 +237,7 @@ export const AccountsRoute: FC = () => {
231237
<ActionMenu.Overlay width="medium">
232238
<ActionList>
233239
<ActionList.Item
234-
onSelect={() => openExternalLink('https://github.com' as Link)}
240+
onSelect={() => loginWithGitHub()}
235241
data-testid="account-add-github"
236242
>
237243
<ActionList.LeadingVisual>

src/renderer/routes/Login.tsx

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { KeyIcon, MarkGithubIcon, PersonIcon } from '@primer/octicons-react';
22
import { Button, Heading, Stack, Text } from '@primer/react';
3-
import { type FC, useContext, useEffect } from 'react';
3+
import { type FC, useCallback, useContext, useEffect } from 'react';
44
import { useNavigate } from 'react-router-dom';
55

6-
import { ipcRenderer } from 'electron';
7-
import { namespacedEvent } from '../../shared/events';
6+
import { logError } from '../../shared/logger';
87
import { LogoIcon } from '../components/icons/LogoIcon';
98
import { Centered } from '../components/layout/Centered';
109
import { AppContext } from '../context/App';
11-
import { type AuthCode, type Link, Size } from '../types';
12-
import { openExternalLink, showWindow } from '../utils/comms';
10+
import { Size } from '../types';
11+
import { showWindow } from '../utils/comms';
1312

1413
export const LoginRoute: FC = () => {
1514
const navigate = useNavigate();
@@ -22,21 +21,14 @@ export const LoginRoute: FC = () => {
2221
}
2322
}, [isLoggedIn]);
2423

25-
useEffect(() => {
26-
ipcRenderer.on(namespacedEvent('auth-code'), (_, authCode: AuthCode) => {
27-
console.log('RENDER AUTH CODE', authCode);
28-
loginWithGitHubApp(authCode);
29-
});
24+
const loginUser = useCallback(() => {
25+
try {
26+
loginWithGitHubApp();
27+
} catch (err) {
28+
logError('loginWithGitHubApp', 'failed to login with GitHub', err);
29+
}
3030
}, [loginWithGitHubApp]);
3131

32-
// const loginUser = useCallback(() => {
33-
// try {
34-
// loginWithGitHubApp();
35-
// } catch (err) {
36-
// logError('loginWithGitHubApp', 'failed to login with GitHub', err);
37-
// }
38-
// }, [loginWithGitHubApp]);
39-
4032
return (
4133
<Centered fullHeight={true}>
4234
<Stack direction="vertical" align="center">
@@ -53,7 +45,7 @@ export const LoginRoute: FC = () => {
5345
<Button
5446
leadingVisual={MarkGithubIcon}
5547
variant="primary"
56-
onClick={() => openExternalLink('https://github.com' as Link)}
48+
onClick={() => loginUser()}
5749
data-testid="login-github"
5850
>
5951
GitHub

src/renderer/utils/auth/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface LoginPersonalAccessTokenOptions {
2222
}
2323

2424
export interface AuthResponse {
25+
authType: AuthMethod;
2526
authCode: AuthCode;
2627
authOptions: LoginOAuthAppOptions;
2728
}

src/renderer/utils/auth/utils.ts

Lines changed: 27 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { BrowserWindow } from '@electron/remote';
21
import { format } from 'date-fns';
32
import semver from 'semver';
43

4+
import { ipcRenderer } from 'electron';
55
import { APPLICATION } from '../../../shared/constants';
6+
import { namespacedEvent } from '../../../shared/events';
67
import { logError, logWarn } from '../../../shared/logger';
78
import type {
89
Account,
@@ -22,80 +23,31 @@ import { Constants } from '../constants';
2223
import { getPlatformFromHostname } from '../helpers';
2324
import type { AuthMethod, AuthResponse, AuthTokenResponse } from './types';
2425

25-
// TODO - Refactor our OAuth2 flow to use system browser and local app gitify://callback - see #485 #561 #654
26-
export function authGitHub(authOptions = Constants.DEFAULT_AUTH_OPTIONS) {
27-
const authUrl = new URL(`https://${authOptions.hostname}`);
28-
authUrl.pathname = '/login/oauth/authorize';
29-
authUrl.searchParams.append('client_id', authOptions.clientId);
30-
authUrl.searchParams.append('scope', Constants.AUTH_SCOPE.toString());
31-
32-
openExternalLink(authUrl.toString() as Link);
33-
34-
// return new Promise((resolve, reject) => {
35-
// // Build the OAuth consent page URL
36-
// const authWindow = new BrowserWindow({
37-
// width: 548,
38-
// height: 736,
39-
// show: true,
40-
// });
41-
42-
// const authUrl = new URL(`https://${authOptions.hostname}`);
43-
// authUrl.pathname = '/login/oauth/authorize';
44-
// authUrl.searchParams.append('client_id', authOptions.clientId);
45-
// authUrl.searchParams.append('scope', Constants.AUTH_SCOPE.toString());
46-
47-
// const session = authWindow.webContents.session;
48-
// session.clearStorageData();
49-
50-
// authWindow.loadURL(authUrl.toString());
51-
52-
// const handleCallback = (url: Link) => {
53-
// const raw_code = /code=([^&]*)/.exec(url) || null;
54-
// const authCode =
55-
// raw_code && raw_code.length > 1 ? (raw_code[1] as AuthCode) : null;
56-
// const error = /\?error=(.+)$/.exec(url);
57-
// if (authCode || error) {
58-
// // Close the browser if code found or error
59-
// authWindow.destroy();
60-
// }
61-
// // If there is a code, proceed to get token from github
62-
// if (authCode) {
63-
// resolve({ authCode, authOptions });
64-
// } else if (error) {
65-
// reject(
66-
// "Oops! Something went wrong and we couldn't " +
67-
// 'log you in using GitHub. Please try again.',
68-
// );
69-
// }
70-
// };
71-
72-
// // If "Done" button is pressed, hide "Loading"
73-
// authWindow.on('close', () => {
74-
// authWindow.destroy();
75-
// });
76-
77-
// authWindow.webContents.on(
78-
// 'did-fail-load',
79-
// (_event, _errorCode, _errorDescription, validatedURL) => {
80-
// if (validatedURL.includes(authOptions.hostname)) {
81-
// authWindow.destroy();
82-
// reject(
83-
// `Invalid Hostname. Could not load https://${authOptions.hostname}/.`,
84-
// );
85-
// }
86-
// },
87-
// );
88-
89-
// authWindow.webContents.on('will-redirect', (event, url) => {
90-
// event.preventDefault();
91-
// handleCallback(url as Link);
92-
// });
93-
94-
// authWindow.webContents.on('will-navigate', (event, url) => {
95-
// event.preventDefault();
96-
// handleCallback(url as Link);
97-
// });
98-
// });
26+
export function authGitHub(
27+
authOptions = Constants.DEFAULT_AUTH_OPTIONS,
28+
): Promise<AuthResponse> {
29+
return new Promise((resolve) => {
30+
// // Build the OAuth consent page URL
31+
const authUrl = new URL(`https://${authOptions.hostname}`);
32+
authUrl.pathname = '/login/oauth/authorize';
33+
authUrl.searchParams.append('client_id', authOptions.clientId);
34+
authUrl.searchParams.append('scope', Constants.AUTH_SCOPE.toString());
35+
36+
openExternalLink(authUrl.toString() as Link);
37+
38+
ipcRenderer.on(
39+
namespacedEvent('auth-code'),
40+
(_, authType: 'auth' | 'oauth', authCode: AuthCode) => {
41+
const type: AuthMethod =
42+
authType === 'auth' ? 'GitHub App' : 'OAuth App';
43+
handleCallback(type, authCode);
44+
},
45+
);
46+
47+
const handleCallback = (authType: AuthMethod, authCode: AuthCode) => {
48+
resolve({ authType, authCode, authOptions });
49+
};
50+
});
9951
}
10052

10153
export async function getUserData(

0 commit comments

Comments
 (0)