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

gitify-app / gitify / 13089101461

01 Feb 2025 01:30PM CUT coverage: 87.364% (+0.04%) from 87.327%
13089101461

push

github

web-flow
chore(deps): update tailwindcss monorepo to v4.0.3 (#1802)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

639 of 716 branches covered (89.25%)

Branch coverage included in aggregate %.

1698 of 1959 relevant lines covered (86.68%)

25.09 hits per line

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

92.65
/src/renderer/utils/auth/utils.ts
1
import { format } from 'date-fns';
76✔
2
import semver from 'semver';
76✔
3

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

26
export function authGitHub(
76✔
27
  authOptions = Constants.DEFAULT_AUTH_OPTIONS,
2✔
28
): Promise<AuthResponse> {
29
  return new Promise((resolve, reject) => {
6✔
30
    const authUrl = new URL(`https://${authOptions.hostname}`);
6✔
31
    authUrl.pathname = '/login/oauth/authorize';
6✔
32
    authUrl.searchParams.append('client_id', authOptions.clientId);
6✔
33
    authUrl.searchParams.append('scope', Constants.AUTH_SCOPE.toString());
6✔
34

35
    openExternalLink(authUrl.toString() as Link);
6✔
36

37
    const handleCallback = (callbackUrl: string) => {
6✔
38
      const url = new URL(callbackUrl);
6✔
39

40
      const type = url.hostname;
6✔
41
      const code = url.searchParams.get('code');
6✔
42
      const error = url.searchParams.get('error');
6✔
43
      const errorDescription = url.searchParams.get('error_description');
6✔
44
      const errorUri = url.searchParams.get('error_uri');
6✔
45

46
      if (code && (type === 'auth' || type === 'oauth')) {
6✔
47
        const authMethod: AuthMethod =
48
          type === 'auth' ? 'GitHub App' : 'OAuth App';
4✔
49

50
        resolve({
4✔
51
          authMethod: authMethod,
52
          authCode: code as AuthCode,
53
          authOptions: authOptions,
54
        });
55
      } else if (error) {
2✔
56
        reject(
2✔
57
          `Oops! Something went wrong and we couldn't log you in using GitHub. Please try again. Reason: ${errorDescription} Docs: ${errorUri}`,
58
        );
59
      }
60
    };
61

62
    ipcRenderer.on(
6✔
63
      namespacedEvent('auth-callback'),
64
      (_, callbackUrl: string) => {
65
        handleCallback(callbackUrl);
6✔
66
      },
67
    );
68
  });
69
}
70

71
export async function getUserData(
76✔
72
  token: Token,
73
  hostname: Hostname,
74
): Promise<GitifyUser> {
75
  const response: UserDetails = (await getAuthenticatedUser(hostname, token))
×
76
    .data;
77

78
  return {
×
79
    id: response.id,
80
    login: response.login,
81
    name: response.name,
82
    avatar: response.avatar_url,
83
  };
84
}
85

86
export async function getToken(
76✔
87
  authCode: AuthCode,
88
  authOptions = Constants.DEFAULT_AUTH_OPTIONS,
2✔
89
): Promise<AuthTokenResponse> {
90
  const url =
91
    `https://${authOptions.hostname}/login/oauth/access_token` as Link;
4✔
92
  const data = {
4✔
93
    client_id: authOptions.clientId,
94
    client_secret: authOptions.clientSecret,
95
    code: authCode,
96
  };
97

98
  const response = await apiRequest(url, 'POST', data);
4✔
99
  return {
2✔
100
    hostname: authOptions.hostname,
101
    token: response.data.access_token,
102
  };
103
}
104

105
export async function addAccount(
76✔
106
  auth: AuthState,
107
  method: AuthMethod,
108
  token: Token,
109
  hostname: Hostname,
110
): Promise<AuthState> {
111
  const accountList = auth.accounts;
10✔
112
  const encryptedToken = await encryptValue(token);
10✔
113

114
  let newAccount = {
10✔
115
    hostname: hostname,
116
    method: method,
117
    platform: getPlatformFromHostname(hostname),
118
    token: encryptedToken,
119
  } as Account;
120

121
  newAccount = await refreshAccount(newAccount);
10✔
122
  const newAccountUUID = getAccountUUID(newAccount);
8✔
123

124
  const accountAlreadyExists = accountList.some(
8✔
125
    (a) => getAccountUUID(a) === newAccountUUID,
×
126
  );
127

128
  if (accountAlreadyExists) {
8!
129
    logWarn(
×
130
      'addAccount',
131
      `account for user ${newAccount.user.login} already exists`,
132
    );
133
  } else {
134
    accountList.push(newAccount);
8✔
135
  }
136

137
  return {
8✔
138
    accounts: accountList,
139
  };
140
}
141

142
export function removeAccount(auth: AuthState, account: Account): AuthState {
76✔
143
  const updatedAccounts = auth.accounts.filter(
4✔
144
    (a) => a.token !== account.token,
8✔
145
  );
146

147
  return {
4✔
148
    accounts: updatedAccounts,
149
  };
150
}
151

152
export async function refreshAccount(account: Account): Promise<Account> {
76✔
153
  try {
12✔
154
    const res = await getAuthenticatedUser(account.hostname, account.token);
12✔
155

156
    // Refresh user data
157
    account.user = {
8✔
158
      id: res.data.id,
159
      login: res.data.login,
160
      name: res.data.name,
161
      avatar: res.data.avatar_url,
162
    };
163

164
    account.version = extractHostVersion(
8✔
165
      res.headers['x-github-enterprise-version'],
166
    );
167

168
    const accountScopes = res.headers['x-oauth-scopes']
8✔
169
      ?.split(',')
170
      .map((scope: string) => scope.trim());
×
171

172
    account.hasRequiredScopes = Constants.AUTH_SCOPE.every((scope) =>
8✔
173
      accountScopes.includes(scope),
8✔
174
    );
175

176
    if (!account.hasRequiredScopes) {
×
177
      logWarn(
×
178
        'refreshAccount',
179
        `account for user ${account.user.login} is missing required scopes`,
180
      );
181
    }
182
  } catch (err) {
183
    logError(
10✔
184
      'refreshAccount',
185
      `failed to refresh account for user ${account.user.login}`,
186
      err,
187
    );
188
  }
189

190
  return account;
10✔
191
}
192

193
export function extractHostVersion(version: string | null): string {
76✔
194
  if (version) {
28✔
195
    return semver.valid(semver.coerce(version));
22✔
196
  }
197

198
  return 'latest';
6✔
199
}
200

201
export function getDeveloperSettingsURL(account: Account): Link {
76✔
202
  const settingsURL = new URL(`https://${account.hostname}`);
12✔
203

204
  switch (account.method) {
12✔
205
    case 'GitHub App':
206
      settingsURL.pathname =
2✔
207
        '/settings/connections/applications/27a352516d3341cee376';
208
      break;
2✔
209
    case 'OAuth App':
210
      settingsURL.pathname = '/settings/developers';
2✔
211
      break;
2✔
212
    case 'Personal Access Token':
213
      settingsURL.pathname = '/settings/tokens';
6✔
214
      break;
6✔
215
    default:
216
      settingsURL.pathname = '/settings';
2✔
217
      break;
2✔
218
  }
219
  return settingsURL.toString() as Link;
12✔
220
}
221

222
export function getNewTokenURL(hostname: Hostname): Link {
76✔
223
  const date = format(new Date(), 'PP p');
6✔
224
  const newTokenURL = new URL(`https://${hostname}/settings/tokens/new`);
6✔
225
  newTokenURL.searchParams.append(
6✔
226
    'description',
227
    `${APPLICATION.NAME} (Created on ${date})`,
228
  );
229
  newTokenURL.searchParams.append('scopes', Constants.AUTH_SCOPE.join(','));
6✔
230

231
  return newTokenURL.toString() as Link;
6✔
232
}
233

234
export function getNewOAuthAppURL(hostname: Hostname): Link {
76✔
235
  const date = format(new Date(), 'PP p');
6✔
236
  const newOAuthAppURL = new URL(
6✔
237
    `https://${hostname}/settings/applications/new`,
238
  );
239
  newOAuthAppURL.searchParams.append(
6✔
240
    'oauth_application[name]',
241
    `${APPLICATION.NAME} (Created on ${date})`,
242
  );
243
  newOAuthAppURL.searchParams.append(
6✔
244
    'oauth_application[url]',
245
    'https://gitify.io',
246
  );
247
  newOAuthAppURL.searchParams.append(
6✔
248
    'oauth_application[callback_url]',
249
    'gitify://oauth',
250
  );
251

252
  return newOAuthAppURL.toString() as Link;
6✔
253
}
254

255
export function isValidHostname(hostname: Hostname) {
76✔
256
  return /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$/i.test(
28✔
257
    hostname,
258
  );
259
}
260

261
export function isValidClientId(clientId: ClientID) {
76✔
262
  return /^[A-Z0-9_]{20}$/i.test(clientId);
16✔
263
}
264

265
export function isValidToken(token: Token) {
76✔
266
  return /^[A-Z0-9_]{40}$/i.test(token);
26✔
267
}
268

269
export function getAccountUUID(account: Account): string {
76✔
270
  return btoa(`${account.hostname}-${account.user.id}-${account.method}`);
64✔
271
}
272

273
export function hasAccounts(auth: AuthState) {
76✔
274
  return auth.accounts.length > 0;
24✔
275
}
276

277
export function hasMultipleAccounts(auth: AuthState) {
76✔
278
  return auth.accounts.length > 1;
28✔
279
}
280

281
export function formatRequiredScopes() {
76✔
282
  return Constants.AUTH_SCOPE.join(', ');
74✔
283
}
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