@anmiles/google-api-wrapper 18.0.3 → 19.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nycrc.json +27 -0
- package/CHANGELOG.md +32 -21
- package/README.md +1 -1
- package/cspell.json +22 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +5 -11
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +5 -6
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/auth.d.ts +2 -9
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +3 -23
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/credentials/generator.d.ts +4 -0
- package/dist/lib/credentials/generator.d.ts.map +1 -0
- package/dist/lib/credentials/generator.js +60 -0
- package/dist/lib/credentials/generator.js.map +1 -0
- package/dist/lib/credentials/index.d.ts +5 -0
- package/dist/lib/credentials/index.d.ts.map +1 -0
- package/dist/lib/credentials/index.js +42 -0
- package/dist/lib/credentials/index.js.map +1 -0
- package/dist/lib/credentials/validator.d.ts +7 -0
- package/dist/lib/credentials/validator.d.ts.map +1 -0
- package/dist/lib/credentials/validator.js +20 -0
- package/dist/lib/credentials/validator.js.map +1 -0
- package/dist/lib/login.d.ts +3 -0
- package/dist/lib/login.d.ts.map +1 -0
- package/dist/lib/login.js +19 -0
- package/dist/lib/login.js.map +1 -0
- package/dist/lib/profiles.d.ts +4 -12
- package/dist/lib/profiles.d.ts.map +1 -1
- package/dist/lib/profiles.js +8 -11
- package/dist/lib/profiles.js.map +1 -1
- package/dist/lib/renderer.d.ts +7 -25
- package/dist/lib/renderer.d.ts.map +1 -1
- package/dist/lib/renderer.js +26 -23
- package/dist/lib/renderer.js.map +1 -1
- package/dist/lib/scopes.d.ts +2 -0
- package/dist/lib/scopes.d.ts.map +1 -0
- package/dist/lib/scopes.js +22 -0
- package/dist/lib/scopes.js.map +1 -0
- package/dist/lib/secrets.d.ts +2 -28
- package/dist/lib/secrets.d.ts.map +1 -1
- package/dist/lib/secrets.js +15 -115
- package/dist/lib/secrets.js.map +1 -1
- package/dist/lib/utils/paths.d.ts +7 -0
- package/dist/lib/utils/paths.d.ts.map +1 -0
- package/dist/lib/{paths.js → utils/paths.js} +6 -8
- package/dist/lib/utils/paths.js.map +1 -0
- package/dist/templates/auth.html +1 -1
- package/dist/templates/index.html +6 -1
- package/dist/templates/page.html +1 -2
- package/dist/templates/script.html +0 -0
- package/{src/templates/css.html → dist/templates/style.html} +2 -2
- package/dist/types/options.d.ts +2 -3
- package/dist/types/options.d.ts.map +1 -1
- package/dist/types/secrets.d.ts +1 -2
- package/dist/types/secrets.d.ts.map +1 -1
- package/eslint.config.mts +43 -0
- package/jest.config.js +9 -9
- package/package.json +40 -30
- package/src/index.ts +3 -2
- package/src/lib/__tests__/__snapshots__/renderer.test.ts.snap +273 -0
- package/src/lib/__tests__/__snapshots__/scopes.test.ts.snap +6 -0
- package/src/lib/__tests__/__snapshots__/secrets.test.ts.snap +38 -0
- package/src/lib/__tests__/api.test.ts +72 -74
- package/src/lib/__tests__/auth.test.ts +38 -114
- package/src/lib/__tests__/login.test.ts +71 -0
- package/src/lib/__tests__/profiles.test.ts +50 -93
- package/src/lib/__tests__/renderer.test.ts +16 -89
- package/src/lib/__tests__/scopes.test.ts +41 -0
- package/src/lib/__tests__/secrets.test.ts +47 -541
- package/src/lib/api.ts +19 -21
- package/src/lib/auth.ts +5 -25
- package/src/lib/credentials/__tests__/generator.test.ts +249 -0
- package/src/lib/credentials/__tests__/index.test.ts +213 -0
- package/src/lib/credentials/__tests__/validator.test.ts +43 -0
- package/src/lib/credentials/generator.ts +70 -0
- package/src/lib/credentials/index.ts +50 -0
- package/src/lib/credentials/validator.ts +29 -0
- package/src/lib/login.ts +22 -0
- package/src/lib/profiles.ts +9 -12
- package/src/lib/renderer.ts +32 -27
- package/src/lib/scopes.ts +18 -0
- package/src/lib/secrets.ts +21 -141
- package/src/lib/utils/paths.ts +30 -0
- package/src/templates/auth.html +1 -1
- package/src/templates/index.html +6 -1
- package/src/templates/page.html +1 -2
- package/src/templates/script.html +0 -0
- package/{dist/templates/css.html → src/templates/style.html} +2 -2
- package/src/types/options.ts +5 -7
- package/src/types/secrets.ts +8 -10
- package/tsconfig.build.json +6 -0
- package/tsconfig.json +0 -5
- package/tsconfig.test.json +1 -1
- package/.eslintignore +0 -2
- package/.eslintrc.js +0 -30
- package/.vscode/settings.json +0 -9
- package/coverage.config.js +0 -8
- package/dist/lib/paths.d.ts +0 -16
- package/dist/lib/paths.d.ts.map +0 -1
- package/dist/lib/paths.js.map +0 -1
- package/src/lib/__tests__/paths.test.ts +0 -77
- package/src/lib/paths.ts +0 -32
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
|
|
3
|
+
import { warn } from '@anmiles/logger';
|
|
4
|
+
import type GoogleApis from 'googleapis';
|
|
5
|
+
import { open } from 'out-url';
|
|
6
|
+
import enableDestroy from 'server-destroy';
|
|
7
|
+
|
|
8
|
+
import type { AuthOptions } from '../../types/options';
|
|
9
|
+
import { renderAuth, renderDone } from '../renderer';
|
|
10
|
+
import { getScopes } from '../scopes';
|
|
11
|
+
|
|
12
|
+
const port = 6006;
|
|
13
|
+
const host = `localhost:${port}`;
|
|
14
|
+
const startURI = `http://${host}/`;
|
|
15
|
+
const serverRetryInterval = 1000;
|
|
16
|
+
|
|
17
|
+
export async function generateCredentials(
|
|
18
|
+
profile: string,
|
|
19
|
+
auth: GoogleApis.Auth.OAuth2Client,
|
|
20
|
+
options?: AuthOptions,
|
|
21
|
+
prompt?: GoogleApis.Auth.GenerateAuthUrlOpts['prompt'],
|
|
22
|
+
): Promise<GoogleApis.Auth.Credentials> {
|
|
23
|
+
const scope = options?.scopes ?? getScopes();
|
|
24
|
+
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const authUrl = auth.generateAuthUrl({
|
|
27
|
+
access_type: 'offline',
|
|
28
|
+
prompt,
|
|
29
|
+
scope,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const server = http.createServer();
|
|
33
|
+
enableDestroy(server);
|
|
34
|
+
|
|
35
|
+
server.on('request', (request, response) => {
|
|
36
|
+
if (!request.url) {
|
|
37
|
+
response.end('');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const url = new URL(`http://${request.headers.host}${request.url}`);
|
|
42
|
+
const code = url.searchParams.get('code');
|
|
43
|
+
|
|
44
|
+
if (!code) {
|
|
45
|
+
response.end(renderAuth({ profile, authUrl, scope }));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
response.end(renderDone({ profile }));
|
|
50
|
+
server.destroy();
|
|
51
|
+
|
|
52
|
+
void (async () => {
|
|
53
|
+
const { tokens } = await auth.getToken(code);
|
|
54
|
+
resolve(tokens);
|
|
55
|
+
})();
|
|
56
|
+
});
|
|
57
|
+
server.on('error', (error: NodeJS.ErrnoException) => {
|
|
58
|
+
if (error.code === 'EADDRINUSE') {
|
|
59
|
+
setTimeout(() => server.listen(port), serverRetryInterval);
|
|
60
|
+
} else {
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
server.once('listening', () => {
|
|
65
|
+
warn('Please check your browser for further actions');
|
|
66
|
+
void open(startURI);
|
|
67
|
+
});
|
|
68
|
+
server.listen(port);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
import type GoogleApis from 'googleapis';
|
|
4
|
+
|
|
5
|
+
import type { AuthOptions } from '../../types/options';
|
|
6
|
+
import { getCredentialsFile } from '../utils/paths';
|
|
7
|
+
|
|
8
|
+
import { generateCredentials } from './generator';
|
|
9
|
+
import { validateCredentials } from './validator';
|
|
10
|
+
|
|
11
|
+
export async function getCredentials(profile: string, auth: GoogleApis.Common.OAuth2Client, options?: AuthOptions): Promise<GoogleApis.Auth.Credentials> {
|
|
12
|
+
const credentialsFile = getCredentialsFile(profile);
|
|
13
|
+
|
|
14
|
+
if (options?.temporary) {
|
|
15
|
+
const credentials = await generateCredentials(profile, auth, options);
|
|
16
|
+
const validationResult = validateCredentials(credentials, options);
|
|
17
|
+
|
|
18
|
+
if (!validationResult.isValid) {
|
|
19
|
+
throw new Error(validationResult.validationError);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return credentials;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return fs.getJSONAsync(credentialsFile, async () => {
|
|
26
|
+
const refreshToken = fs.existsSync(credentialsFile)
|
|
27
|
+
? fs.readJSON<GoogleApis.Auth.Credentials>(credentialsFile).refresh_token
|
|
28
|
+
: undefined;
|
|
29
|
+
|
|
30
|
+
const credentials = await generateCredentials(profile, auth, options,
|
|
31
|
+
refreshToken
|
|
32
|
+
? undefined
|
|
33
|
+
: 'consent',
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
refresh_token: refreshToken,
|
|
38
|
+
...credentials,
|
|
39
|
+
};
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
41
|
+
}, async (credentials) => validateCredentials(credentials));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function deleteCredentials(profile: string): void {
|
|
45
|
+
const credentialsFile = getCredentialsFile(profile);
|
|
46
|
+
|
|
47
|
+
if (fs.existsSync(credentialsFile)) {
|
|
48
|
+
fs.rmSync(credentialsFile);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type GoogleApis from 'googleapis';
|
|
2
|
+
|
|
3
|
+
import type { AuthOptions } from '../../types/options';
|
|
4
|
+
|
|
5
|
+
const tokenExpiration = 7 * 24 * 60 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
export function validateCredentials(
|
|
8
|
+
credentials: GoogleApis.Auth.Credentials,
|
|
9
|
+
options?: AuthOptions,
|
|
10
|
+
): { isValid: boolean; validationError?: string } {
|
|
11
|
+
|
|
12
|
+
if (!credentials.access_token) {
|
|
13
|
+
return { isValid: false, validationError: 'Credentials does not have access_token' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!credentials.refresh_token && !options?.temporary) {
|
|
17
|
+
return { isValid: false, validationError: 'Credentials does not have refresh_token' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!credentials.expiry_date) {
|
|
21
|
+
return { isValid: false, validationError: 'Credentials does not have expiry_date' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (new Date().getTime() - credentials.expiry_date >= tokenExpiration) {
|
|
25
|
+
return { isValid: false, validationError: 'Credentials expired' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { isValid: true };
|
|
29
|
+
}
|
package/src/lib/login.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { info, warn } from '@anmiles/logger';
|
|
2
|
+
|
|
3
|
+
import type { AuthOptions, CommonOptions } from '../types/options';
|
|
4
|
+
|
|
5
|
+
import { getAuth } from './auth';
|
|
6
|
+
import { getProfiles } from './profiles';
|
|
7
|
+
|
|
8
|
+
export async function login(profile?: string, options?: AuthOptions & CommonOptions): Promise<void> {
|
|
9
|
+
const profiles = getProfiles().filter((p) => !profile || p === profile);
|
|
10
|
+
|
|
11
|
+
for (const profile of profiles) {
|
|
12
|
+
if (!options?.hideProgress) {
|
|
13
|
+
warn(`${profile} - logging in...`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
await getAuth(profile, options);
|
|
17
|
+
|
|
18
|
+
if (!options?.hideProgress) {
|
|
19
|
+
info(`${profile} - logged in successfully`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/lib/profiles.ts
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
|
|
2
3
|
import '@anmiles/prototypes';
|
|
3
|
-
import { getProfilesFile } from './paths';
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { getProfilesFile } from './utils/paths';
|
|
6
6
|
|
|
7
|
-
function getProfiles(): string[] {
|
|
7
|
+
export function getProfiles(): string[] {
|
|
8
8
|
const profilesFile = getProfilesFile();
|
|
9
9
|
return fs.getJSON(profilesFile, () => []);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function setProfiles(profiles: string[]): void {
|
|
12
|
+
export function setProfiles(profiles: string[]): void {
|
|
13
13
|
const profilesFile = getProfilesFile();
|
|
14
14
|
fs.writeJSON(profilesFile, profiles);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function createProfile(profile?: string): void {
|
|
17
|
+
export function createProfile(profile?: string): void {
|
|
18
18
|
if (!profile) {
|
|
19
19
|
throw new Error('Usage: `npm run create <profile>` where `profile` - is any profile name you want');
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const existingProfiles =
|
|
22
|
+
const existingProfiles = getProfiles();
|
|
23
23
|
|
|
24
24
|
if (existingProfiles.includes(profile)) {
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
existingProfiles.push(profile);
|
|
29
|
-
|
|
29
|
+
setProfiles(existingProfiles);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function filterProfiles(profile?: string): string[] {
|
|
33
|
-
const existingProfiles =
|
|
32
|
+
export function filterProfiles(profile?: string): string[] {
|
|
33
|
+
const existingProfiles = getProfiles();
|
|
34
34
|
|
|
35
35
|
if (existingProfiles.length === 0) {
|
|
36
36
|
throw new Error('Please `npm run create` at least one profile');
|
|
@@ -46,6 +46,3 @@ function filterProfiles(profile?: string): string[] {
|
|
|
46
46
|
|
|
47
47
|
throw new Error(`Profile '${profile}' does not exist`);
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
export { getProfiles, setProfiles, createProfile, filterProfiles };
|
|
51
|
-
export default { getProfiles, setProfiles, createProfile, filterProfiles };
|
package/src/lib/renderer.ts
CHANGED
|
@@ -1,48 +1,56 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
|
|
3
|
-
import { getTemplateFile } from './paths';
|
|
4
|
-
|
|
5
|
-
const templates = {
|
|
6
|
-
index : [ 'page' ] as const,
|
|
7
|
-
page : [ '
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
|
|
3
|
+
import { getTemplateFile } from './utils/paths';
|
|
4
|
+
|
|
5
|
+
export const templates = {
|
|
6
|
+
index : [ 'style', 'page', 'script' ] as const,
|
|
7
|
+
page : [ 'content' ] as const,
|
|
8
|
+
style : [ ] as const,
|
|
9
|
+
script: [ ] as const,
|
|
10
|
+
auth : [ 'profile', 'authUrl', 'scopesList' ] as const,
|
|
10
11
|
scope : [ 'type', 'title', 'name' ] as const,
|
|
11
12
|
done : [ 'profile' ] as const,
|
|
12
13
|
} as const;
|
|
13
14
|
|
|
14
15
|
type TemplateName = keyof typeof templates;
|
|
15
16
|
|
|
16
|
-
const allHTML = {} as Record<TemplateName, string>;
|
|
17
|
+
const allHTML = {} as Record<TemplateName, string>; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
|
17
18
|
|
|
18
|
-
function renderAuth({ profile, authUrl, scope }: { profile
|
|
19
|
+
export function renderAuth({ profile, authUrl, scope }: { profile: string; authUrl: string; scope: string[] }): string {
|
|
19
20
|
const scopesList = scope.map((s) => render('scope', {
|
|
20
|
-
name
|
|
21
|
-
title
|
|
22
|
-
type
|
|
21
|
+
name : s.split('/').pop()!,
|
|
22
|
+
title: s.endsWith('.readonly') ? 'Readonly (cannot change or delete your data)' : 'Writable (can change or delete your data)',
|
|
23
|
+
type : s.endsWith('.readonly') ? 'readonly' : '',
|
|
23
24
|
})).join('\n');
|
|
24
25
|
|
|
25
|
-
const
|
|
26
|
+
const style = render('style', {});
|
|
27
|
+
const script = render('script', {});
|
|
26
28
|
const content = render('auth', { profile, authUrl, scopesList });
|
|
27
|
-
const page = render('page', {
|
|
28
|
-
return render('index', { page });
|
|
29
|
+
const page = render('page', { content });
|
|
30
|
+
return render('index', { style, page, script });
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
function renderDone({ profile }: { profile
|
|
32
|
-
const
|
|
33
|
+
export function renderDone({ profile }: { profile: string }): string {
|
|
34
|
+
const style = render('style', {});
|
|
35
|
+
const script = render('script', {});
|
|
33
36
|
const content = render('done', { profile });
|
|
34
|
-
const page = render('page', {
|
|
35
|
-
return render('index', { page });
|
|
37
|
+
const page = render('page', { content });
|
|
38
|
+
return render('index', { style, page, script });
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
// TODO: Use react
|
|
39
42
|
function render<T extends TemplateName>(templateName: T, values: Record<typeof templates[T][number], string | undefined>): string {
|
|
40
|
-
let html =
|
|
43
|
+
let html = getTemplate(templateName);
|
|
41
44
|
const allValues = values as Record<typeof templates[TemplateName][number], string | undefined>;
|
|
42
45
|
|
|
43
46
|
for (const variable of templates[templateName]) {
|
|
44
|
-
const value = allValues[variable]
|
|
45
|
-
|
|
47
|
+
const value = allValues[variable];
|
|
48
|
+
|
|
49
|
+
if (typeof value === 'undefined') {
|
|
50
|
+
throw new Error(`Missing required value '${variable}' while rendering template '${templateName}'`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
html = html.replaceAll(`\${${variable}}`, value);
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
return html;
|
|
@@ -51,12 +59,9 @@ function render<T extends TemplateName>(templateName: T, values: Record<typeof t
|
|
|
51
59
|
function getTemplate(templateName: TemplateName): string {
|
|
52
60
|
if (!(templateName in allHTML)) {
|
|
53
61
|
const file = getTemplateFile(templateName);
|
|
54
|
-
const template = fs.readFileSync(file).toString();
|
|
62
|
+
const template = fs.readFileSync(file).toString().trim();
|
|
55
63
|
allHTML[templateName] = template;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
return allHTML[templateName];
|
|
59
67
|
}
|
|
60
|
-
|
|
61
|
-
export { templates, renderAuth, renderDone };
|
|
62
|
-
export default { templates, render, getTemplate, renderAuth, renderDone };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
import { getScopesFile } from './utils/paths';
|
|
4
|
+
|
|
5
|
+
export function getScopes(): string[] {
|
|
6
|
+
const scopesFile = getScopesFile();
|
|
7
|
+
const scopes = fs.getJSON<string[]>(scopesFile, () => {
|
|
8
|
+
throw new Error(getScopesError(scopesFile));
|
|
9
|
+
});
|
|
10
|
+
return scopes;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getScopesError(scopesFile: string): string {
|
|
14
|
+
return [
|
|
15
|
+
`File ${scopesFile} not found!`,
|
|
16
|
+
`This application had to have pre-defined file ${scopesFile} that will declare needed scopes`,
|
|
17
|
+
].join('\n');
|
|
18
|
+
}
|
package/src/lib/secrets.ts
CHANGED
|
@@ -1,154 +1,37 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
|
|
3
|
-
import { open } from 'out-url';
|
|
4
|
-
import enableDestroy from 'server-destroy';
|
|
5
|
-
import type GoogleApis from 'googleapis';
|
|
6
|
-
import { warn } from '@anmiles/logger';
|
|
7
|
-
import type { Secrets } from '../types/secrets';
|
|
8
|
-
import type { AuthOptions } from '../types/options';
|
|
2
|
+
|
|
9
3
|
import '@anmiles/prototypes';
|
|
10
|
-
import { getScopesFile, getSecretsFile, getCredentialsFile } from './paths';
|
|
11
|
-
import { renderAuth, renderDone } from './renderer';
|
|
12
4
|
|
|
13
|
-
import
|
|
5
|
+
import type { Secrets } from '../types/secrets';
|
|
14
6
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const startURI = `http://${host}/`;
|
|
18
|
-
const callbackURI = `http://${host}/oauthcallback`;
|
|
19
|
-
const tokenExpiration = 7 * 24 * 60 * 60 * 1000;
|
|
20
|
-
const serverRetryInterval = 1000;
|
|
7
|
+
import { getScopes } from './scopes';
|
|
8
|
+
import { getSecretsFile } from './utils/paths';
|
|
21
9
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
10
|
+
const port = 6006;
|
|
11
|
+
const host = `localhost:${port}`;
|
|
12
|
+
const callbackURI = `http://${host}/oauthcallback`;
|
|
13
|
+
|
|
14
|
+
function checkSecrets(secretsObject: Secrets): true {
|
|
15
|
+
if (secretsObject.web.redirect_uris[0] === callbackURI) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`Error in credentials file: redirect URI should be ${callbackURI}`);
|
|
28
19
|
}
|
|
29
20
|
|
|
30
|
-
function getSecrets(profile: string): Secrets {
|
|
21
|
+
export function getSecrets(profile: string): Secrets {
|
|
31
22
|
const secretsFile = getSecretsFile(profile);
|
|
32
23
|
const secretsObject = fs.getJSON<Secrets>(secretsFile, () => {
|
|
33
|
-
throw new Error(
|
|
24
|
+
throw new Error(getSecretsError(secretsFile));
|
|
34
25
|
});
|
|
35
|
-
|
|
26
|
+
checkSecrets(secretsObject);
|
|
36
27
|
return secretsObject;
|
|
37
28
|
}
|
|
38
29
|
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
if (options?.temporary) {
|
|
43
|
-
return secrets.createCredentials(profile, auth, options);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return fs.getJSONAsync(credentialsFile, async () => {
|
|
47
|
-
const refreshToken = fs.existsSync(credentialsFile) ? fs.readJSON<GoogleApis.Auth.Credentials>(credentialsFile).refresh_token : undefined;
|
|
48
|
-
const credentials = await secrets.createCredentials(profile, auth, options, refreshToken ? undefined : 'consent');
|
|
49
|
-
return { refresh_token : refreshToken, ...credentials };
|
|
50
|
-
}, secrets.validateCredentials);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// eslint-disable-next-line @typescript-eslint/require-await -- pass sync function into async context
|
|
54
|
-
async function validateCredentials(credentials: GoogleApis.Auth.Credentials): Promise<{ isValid : boolean; validationError? : string }> {
|
|
55
|
-
if (!credentials.access_token) {
|
|
56
|
-
return { isValid : false, validationError : 'Credentials does not have access_token' };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (!credentials.refresh_token) {
|
|
60
|
-
return { isValid : false, validationError : 'Credentials does not have refresh_token' };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (!credentials.expiry_date) {
|
|
64
|
-
return { isValid : false, validationError : 'Credentials does not have expiry_date' };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (new Date().getTime() - credentials.expiry_date >= tokenExpiration) {
|
|
68
|
-
return { isValid : false, validationError : 'Credentials expired' };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return { isValid : true };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function createCredentials(profile: string, auth: GoogleApis.Auth.OAuth2Client, options?: AuthOptions, prompt?: GoogleApis.Auth.GenerateAuthUrlOpts['prompt']): Promise<GoogleApis.Auth.Credentials> {
|
|
75
|
-
const scope = options?.scopes ?? secrets.getScopes();
|
|
76
|
-
|
|
77
|
-
return new Promise((resolve) => {
|
|
78
|
-
const authUrl = auth.generateAuthUrl({
|
|
79
|
-
access_type : 'offline',
|
|
80
|
-
prompt,
|
|
81
|
-
scope,
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const server = http.createServer();
|
|
85
|
-
enableDestroy(server);
|
|
30
|
+
function getSecretsError(secretsFile: string): string {
|
|
31
|
+
const relativePath = secretsFile.replace('\\', '/');
|
|
86
32
|
|
|
87
|
-
server.on('request', (request, response) => {
|
|
88
|
-
if (!request.url) {
|
|
89
|
-
response.end('');
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const url = new URL(`http://${request.headers.host}${request.url}`);
|
|
94
|
-
const code = url.searchParams.get('code');
|
|
95
|
-
|
|
96
|
-
if (!code) {
|
|
97
|
-
response.end(renderAuth({ profile, authUrl, scope }));
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
response.end(renderDone({ profile }));
|
|
102
|
-
server.destroy();
|
|
103
|
-
|
|
104
|
-
void (async () => {
|
|
105
|
-
const { tokens } = await auth.getToken(code);
|
|
106
|
-
resolve(tokens);
|
|
107
|
-
})();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
server.on('error', (error: NodeJS.ErrnoException) => {
|
|
111
|
-
if (error.code === 'EADDRINUSE') {
|
|
112
|
-
setTimeout(() => server.listen(port), serverRetryInterval);
|
|
113
|
-
} else {
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
server.once('listening', () => {
|
|
119
|
-
warn('Please check your browser for further actions');
|
|
120
|
-
void open(startURI);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
server.listen(port);
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function deleteCredentials(profile: string): void {
|
|
128
|
-
const credentialsFile = getCredentialsFile(profile);
|
|
129
|
-
|
|
130
|
-
if (fs.existsSync(credentialsFile)) {
|
|
131
|
-
fs.rmSync(credentialsFile);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function checkSecrets(profile: string, secretsObject: Secrets, secretsFile: string): true {
|
|
136
|
-
if (secretsObject.web.redirect_uris[0] === callbackURI) {
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
throw new Error(`Error in credentials file: redirect URI should be ${callbackURI}.\n${secrets.getSecretsError(profile, secretsFile)}`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function getScopesError(scopesFile: string): string {
|
|
143
|
-
return [
|
|
144
|
-
`File ${scopesFile} not found!`,
|
|
145
|
-
`This application had to have pre-defined file ${scopesFile} that will declare needed scopes`,
|
|
146
|
-
].join('\n');
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function getSecretsError(profile: string, secretsFile: string): string {
|
|
150
33
|
return [
|
|
151
|
-
`File ${
|
|
34
|
+
`File ${relativePath} not found!`,
|
|
152
35
|
'Here is how to obtain it:',
|
|
153
36
|
'\tGo to https://console.cloud.google.com/projectcreate',
|
|
154
37
|
'\t\tChoose project name',
|
|
@@ -166,7 +49,7 @@ function getSecretsError(profile: string, secretsFile: string): string {
|
|
|
166
49
|
'\t\t\t\tSpecify your email as user support email and as developer contact information on the very bottom',
|
|
167
50
|
'\t\t\t\tClick "Save and continue"',
|
|
168
51
|
'\t\t\tClick "Add or remove scopes"',
|
|
169
|
-
`\t\t\t\tAdd scopes: ${
|
|
52
|
+
`\t\t\t\tAdd scopes: ${getScopes().join(',')}`,
|
|
170
53
|
'\t\t\t\tClick "Save and continue"',
|
|
171
54
|
'\t\t\tClick "Add users"',
|
|
172
55
|
'\t\t\t\tAdd your email',
|
|
@@ -178,10 +61,7 @@ function getSecretsError(profile: string, secretsFile: string): string {
|
|
|
178
61
|
'\t\t\t\tSpecify app name, i.e. "NodeJS"',
|
|
179
62
|
`\t\t\t\tAdd authorized redirect URI: ${callbackURI}`,
|
|
180
63
|
'\t\t\t\tClick "CREATE"',
|
|
181
|
-
`\t\t\t\tClick "DOWNLOAD JSON" and download credentials to
|
|
64
|
+
`\t\t\t\tClick "DOWNLOAD JSON" and download credentials to ${relativePath}`,
|
|
182
65
|
'Then start this script again',
|
|
183
66
|
].join('\n');
|
|
184
67
|
}
|
|
185
|
-
|
|
186
|
-
export { getSecrets, getCredentials, deleteCredentials };
|
|
187
|
-
export default { getScopes, getSecrets, getCredentials, validateCredentials, createCredentials, deleteCredentials, checkSecrets, getSecretsError, getScopesError };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
import type { templates } from '../renderer';
|
|
4
|
+
|
|
5
|
+
const dirPaths = {
|
|
6
|
+
input : 'input',
|
|
7
|
+
secrets : 'secrets',
|
|
8
|
+
// TODO: Remove this hack after moving to React
|
|
9
|
+
templates: path.relative(process.cwd(), path.join(__dirname, '../../templates')),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function getProfilesFile(): string {
|
|
13
|
+
return path.join(dirPaths.input, 'profiles.json');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getScopesFile(): string {
|
|
17
|
+
return 'scopes.json';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getSecretsFile(profile: string): string {
|
|
21
|
+
return path.join(dirPaths.secrets, `${profile}.json`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getCredentialsFile(profile: string): string {
|
|
25
|
+
return path.join(dirPaths.secrets, `${profile}.credentials.json`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getTemplateFile(templateName: keyof typeof templates): string {
|
|
29
|
+
return path.join(dirPaths.templates, `${templateName}.html`);
|
|
30
|
+
}
|
package/src/templates/auth.html
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<h1>Welcome ${profile}!</h1>
|
|
2
2
|
<p>Please authorize:</p>
|
|
3
3
|
<ul>
|
|
4
|
-
|
|
4
|
+
${scopesList}
|
|
5
5
|
</ul>
|
|
6
6
|
<a id="button" href="${authUrl}">Continue</a>
|
|
7
7
|
<script type="text/javascript">document.addEventListener('DOMContentLoaded', function(){ document.getElementById('button').focus(); });</script>
|
package/src/templates/index.html
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
|
-
<title>Google API sign-in</title>
|
|
4
|
+
<title>Google API sign-in'</title>
|
|
5
5
|
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
6
7
|
</head>
|
|
7
8
|
<body>
|
|
8
9
|
|
|
10
|
+
${style}
|
|
11
|
+
|
|
9
12
|
${page}
|
|
10
13
|
|
|
14
|
+
${script}
|
|
15
|
+
|
|
11
16
|
</body>
|
|
12
17
|
</html>
|
package/src/templates/page.html
CHANGED
|
File without changes
|
package/src/types/options.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
interface CommonOptions {
|
|
2
|
-
hideProgress
|
|
1
|
+
export interface CommonOptions {
|
|
2
|
+
hideProgress?: boolean;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
interface AuthOptions {
|
|
6
|
-
temporary
|
|
7
|
-
scopes
|
|
5
|
+
export interface AuthOptions {
|
|
6
|
+
temporary?: boolean;
|
|
7
|
+
scopes?: string[];
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
export type { CommonOptions, AuthOptions };
|
package/src/types/secrets.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
interface Secrets {
|
|
1
|
+
export interface Secrets {
|
|
2
2
|
web: {
|
|
3
|
-
client_id
|
|
4
|
-
project_id
|
|
5
|
-
auth_uri
|
|
6
|
-
token_uri
|
|
7
|
-
auth_provider_x509_cert_url
|
|
8
|
-
client_secret
|
|
9
|
-
redirect_uris
|
|
3
|
+
client_id: `${string}.apps.googleusercontent.com`;
|
|
4
|
+
project_id: string;
|
|
5
|
+
auth_uri: 'https://accounts.google.com/o/oauth2/auth';
|
|
6
|
+
token_uri: 'https://oauth2.googleapis.com/token';
|
|
7
|
+
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs';
|
|
8
|
+
client_secret: string;
|
|
9
|
+
redirect_uris: string[];
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
export type { Secrets };
|