• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gitify-app / gitify / 13017563958

28 Jan 2025 06:57PM UTC coverage: 87.888%. Remained the same
13017563958

Pull #1783

github

web-flow
Merge f718d37b2 into 1244afc90
Pull Request #1783: fix: hover group background color

658 of 733 branches covered (89.77%)

Branch coverage included in aggregate %.

1722 of 1975 relevant lines covered (87.19%)

23.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.78
/src/renderer/utils/auth/utils.ts
1
import { BrowserWindow } from '@electron/remote';
64✔
2
import { format } from 'date-fns';
64✔
3
import semver from 'semver';
64✔
4

5
import { APPLICATION } from '../../../shared/constants';
64✔
6
import { logError, logWarn } from '../../../shared/logger';
64✔
7
import type {
8
  Account,
9
  AuthCode,
10
  AuthState,
11
  ClientID,
12
  GitifyUser,
13
  Hostname,
14
  Link,
15
  Token,
16
} from '../../types';
17
import type { UserDetails } from '../../typesGitHub';
18
import { getAuthenticatedUser } from '../api/client';
64✔
19
import { apiRequest } from '../api/request';
64✔
20
import { Constants } from '../constants';
64✔
21
import { getPlatformFromHostname } from '../helpers';
64✔
22
import type { AuthMethod, AuthResponse, AuthTokenResponse } from './types';
23

24
// TODO - Refactor our OAuth2 flow to use system browser and local app gitify://callback - see #485 #561 #654
25
export function authGitHub(
64✔
26
  authOptions = Constants.DEFAULT_AUTH_OPTIONS,
2✔
27
): Promise<AuthResponse> {
28
  return new Promise((resolve, reject) => {
4✔
29
    // Build the OAuth consent page URL
30
    const authWindow = new BrowserWindow({
4✔
31
      width: 548,
32
      height: 736,
33
      show: true,
34
    });
35

36
    const authUrl = new URL(`https://${authOptions.hostname}`);
4✔
37
    authUrl.pathname = '/login/oauth/authorize';
4✔
38
    authUrl.searchParams.append('client_id', authOptions.clientId);
4✔
39
    authUrl.searchParams.append('scope', Constants.AUTH_SCOPE.toString());
4✔
40

41
    const session = authWindow.webContents.session;
4✔
42
    session.clearStorageData();
4✔
43

44
    authWindow.loadURL(authUrl.toString());
4✔
45

46
    const handleCallback = (url: Link) => {
4✔
47
      const raw_code = /code=([^&]*)/.exec(url) || null;
4✔
48
      const authCode =
49
        raw_code && raw_code.length > 1 ? (raw_code[1] as AuthCode) : null;
4✔
50
      const error = /\?error=(.+)$/.exec(url);
4✔
51
      if (authCode || error) {
4✔
52
        // Close the browser if code found or error
53
        authWindow.destroy();
4✔
54
      }
55
      // If there is a code, proceed to get token from github
56
      if (authCode) {
4✔
57
        resolve({ authCode, authOptions });
2✔
58
      } else if (error) {
2✔
59
        reject(
2✔
60
          "Oops! Something went wrong and we couldn't " +
61
            'log you in using GitHub. Please try again.',
62
        );
63
      }
64
    };
65

66
    // If "Done" button is pressed, hide "Loading"
67
    authWindow.on('close', () => {
4✔
68
      authWindow.destroy();
×
69
    });
70

71
    authWindow.webContents.on(
4✔
72
      'did-fail-load',
73
      (_event, _errorCode, _errorDescription, validatedURL) => {
74
        if (validatedURL.includes(authOptions.hostname)) {
×
75
          authWindow.destroy();
×
76
          reject(
×
77
            `Invalid Hostname. Could not load https://${authOptions.hostname}/.`,
78
          );
79
        }
80
      },
81
    );
82

83
    authWindow.webContents.on('will-redirect', (event, url) => {
4✔
84
      event.preventDefault();
4✔
85
      handleCallback(url as Link);
4✔
86
    });
87

88
    authWindow.webContents.on('will-navigate', (event, url) => {
4✔
89
      event.preventDefault();
×
90
      handleCallback(url as Link);
×
91
    });
92
  });
93
}
94

95
export async function getUserData(
64✔
96
  token: Token,
97
  hostname: Hostname,
98
): Promise<GitifyUser> {
99
  const response: UserDetails = (await getAuthenticatedUser(hostname, token))
10✔
100
    .data;
101

102
  return {
10✔
103
    id: response.id,
104
    login: response.login,
105
    name: response.name,
106
    avatar: response.avatar_url,
107
  };
108
}
109

110
export async function getToken(
64✔
111
  authCode: AuthCode,
112
  authOptions = Constants.DEFAULT_AUTH_OPTIONS,
2✔
113
): Promise<AuthTokenResponse> {
114
  const url =
115
    `https://${authOptions.hostname}/login/oauth/access_token` as Link;
4✔
116
  const data = {
4✔
117
    client_id: authOptions.clientId,
118
    client_secret: authOptions.clientSecret,
119
    code: authCode,
120
  };
121

122
  const response = await apiRequest(url, 'POST', data);
4✔
123
  return {
2✔
124
    hostname: authOptions.hostname,
125
    token: response.data.access_token,
126
  };
127
}
128

129
export async function addAccount(
64✔
130
  auth: AuthState,
131
  method: AuthMethod,
132
  token: Token,
133
  hostname: Hostname,
134
): Promise<AuthState> {
135
  let newAccount = {
10✔
136
    hostname: hostname,
137
    method: method,
138
    platform: getPlatformFromHostname(hostname),
139
    token: token,
140
  } as Account;
141

142
  newAccount = await refreshAccount(newAccount);
10✔
143

144
  return {
8✔
145
    accounts: [...auth.accounts, newAccount],
146
  };
147
}
148

149
export function removeAccount(auth: AuthState, account: Account): AuthState {
64✔
150
  const updatedAccounts = auth.accounts.filter(
4✔
151
    (a) => a.token !== account.token,
8✔
152
  );
153

154
  return {
4✔
155
    accounts: updatedAccounts,
156
  };
157
}
158

159
export async function refreshAccount(account: Account): Promise<Account> {
64✔
160
  try {
12✔
161
    const res = await getAuthenticatedUser(account.hostname, account.token);
12✔
162

163
    // Refresh user data
164
    account.user = {
8✔
165
      id: res.data.id,
166
      login: res.data.login,
167
      name: res.data.name,
168
      avatar: res.data.avatar_url,
169
    };
170

171
    account.version = extractHostVersion(
8✔
172
      res.headers['x-github-enterprise-version'],
173
    );
174

175
    const accountScopes = res.headers['x-oauth-scopes']
8✔
176
      ?.split(',')
177
      .map((scope: string) => scope.trim());
×
178

179
    account.hasRequiredScopes = Constants.AUTH_SCOPE.every((scope) =>
8✔
180
      accountScopes.includes(scope),
8✔
181
    );
182

183
    if (!account.hasRequiredScopes) {
×
184
      logWarn(
×
185
        'refreshAccount',
186
        `account for user ${account.user.login} is missing required scopes`,
187
      );
188
    }
189
  } catch (err) {
190
    logError(
10✔
191
      'refreshAccount',
192
      `failed to refresh account for user ${account.user.login}`,
193
      err,
194
    );
195
  }
196

197
  return account;
10✔
198
}
199

200
export function extractHostVersion(version: string | null): string {
64✔
201
  if (version) {
28✔
202
    return semver.valid(semver.coerce(version));
22✔
203
  }
204

205
  return 'latest';
6✔
206
}
207

208
export function getDeveloperSettingsURL(account: Account): Link {
64✔
209
  const settingsURL = new URL(`https://${account.hostname}`);
12✔
210

211
  switch (account.method) {
12✔
212
    case 'GitHub App':
213
      settingsURL.pathname =
2✔
214
        '/settings/connections/applications/27a352516d3341cee376';
215
      break;
2✔
216
    case 'OAuth App':
217
      settingsURL.pathname = '/settings/developers';
2✔
218
      break;
2✔
219
    case 'Personal Access Token':
220
      settingsURL.pathname = '/settings/tokens';
6✔
221
      break;
6✔
222
    default:
223
      settingsURL.pathname = '/settings';
2✔
224
      break;
2✔
225
  }
226
  return settingsURL.toString() as Link;
12✔
227
}
228

229
export function getNewTokenURL(hostname: Hostname): Link {
64✔
230
  const date = format(new Date(), 'PP p');
6✔
231
  const newTokenURL = new URL(`https://${hostname}/settings/tokens/new`);
6✔
232
  newTokenURL.searchParams.append(
6✔
233
    'description',
234
    `${APPLICATION.NAME} (Created on ${date})`,
235
  );
236
  newTokenURL.searchParams.append('scopes', Constants.AUTH_SCOPE.join(','));
6✔
237

238
  return newTokenURL.toString() as Link;
6✔
239
}
240

241
export function getNewOAuthAppURL(hostname: Hostname): Link {
64✔
242
  const date = format(new Date(), 'PP p');
6✔
243
  const newOAuthAppURL = new URL(
6✔
244
    `https://${hostname}/settings/applications/new`,
245
  );
246
  newOAuthAppURL.searchParams.append(
6✔
247
    'oauth_application[name]',
248
    `${APPLICATION.NAME} (Created on ${date})`,
249
  );
250
  newOAuthAppURL.searchParams.append(
6✔
251
    'oauth_application[url]',
252
    'https://www.gitify.io',
253
  );
254
  newOAuthAppURL.searchParams.append(
6✔
255
    'oauth_application[callback_url]',
256
    'https://www.gitify.io/callback',
257
  );
258

259
  return newOAuthAppURL.toString() as Link;
6✔
260
}
261

262
export function isValidHostname(hostname: Hostname) {
64✔
263
  return /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$/i.test(
28✔
264
    hostname,
265
  );
266
}
267

268
export function isValidClientId(clientId: ClientID) {
64✔
269
  return /^[A-Z0-9_]{20}$/i.test(clientId);
16✔
270
}
271

272
export function isValidToken(token: Token) {
64✔
273
  return /^[A-Z0-9_]{40}$/i.test(token);
26✔
274
}
275

276
export function getAccountUUID(account: Account): string {
64✔
277
  return btoa(`${account.hostname}-${account.user.id}-${account.method}`);
54✔
278
}
279

280
export function hasAccounts(auth: AuthState) {
64✔
281
  return auth.accounts.length > 0;
24✔
282
}
283

284
export function hasMultipleAccounts(auth: AuthState) {
64✔
285
  return auth.accounts.length > 1;
86✔
286
}
287

288
export function formatRequiredScopes() {
64✔
289
  return Constants.AUTH_SCOPE.join(', ');
74✔
290
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc