dbdocs 0.19.0 → 1.0.0
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/README.md +5 -2
- package/oclif.manifest.json +10 -3
- package/package.json +3 -3
- package/src/assets/callback-error.html +109 -0
- package/src/assets/callback-success.html +73 -0
- package/src/commands/login.js +218 -48
- package/src/errors/BaseError.js +10 -0
- package/src/errors/common-errors.js +12 -0
- package/src/services/auth/portal/index.js +206 -0
- package/src/services/auth/portal/server.js +106 -0
- package/src/user_data.json +1 -1
- package/src/utils/browser.js +13 -0
- package/src/utils/constants.js +34 -0
- package/src/utils/helper.js +7 -0
- package/src/vars.js +15 -0
- package/src/vars.js.staging +15 -0
package/README.md
CHANGED
|
@@ -119,10 +119,13 @@ login to dbdocs
|
|
|
119
119
|
|
|
120
120
|
```bash
|
|
121
121
|
USAGE
|
|
122
|
-
$ dbdocs login
|
|
122
|
+
$ dbdocs login [--legacy]
|
|
123
|
+
|
|
124
|
+
FLAGS
|
|
125
|
+
--legacy Use legacy login methods: Email OTP or Google/GitHub with manual token copy
|
|
123
126
|
|
|
124
127
|
DESCRIPTION
|
|
125
|
-
login
|
|
128
|
+
login to dbdocs
|
|
126
129
|
```
|
|
127
130
|
|
|
128
131
|
## `dbdocs logout`
|
package/oclif.manifest.json
CHANGED
|
@@ -123,8 +123,15 @@
|
|
|
123
123
|
"login": {
|
|
124
124
|
"aliases": [],
|
|
125
125
|
"args": {},
|
|
126
|
-
"description": "login to dbdocs
|
|
127
|
-
"flags": {
|
|
126
|
+
"description": "login to dbdocs",
|
|
127
|
+
"flags": {
|
|
128
|
+
"legacy": {
|
|
129
|
+
"description": "Use legacy login methods: Email OTP or Google/GitHub with manual token copy",
|
|
130
|
+
"name": "legacy",
|
|
131
|
+
"allowNo": false,
|
|
132
|
+
"type": "boolean"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
128
135
|
"hasDynamicHelp": false,
|
|
129
136
|
"hiddenAliases": [],
|
|
130
137
|
"id": "login",
|
|
@@ -379,5 +386,5 @@
|
|
|
379
386
|
]
|
|
380
387
|
}
|
|
381
388
|
},
|
|
382
|
-
"version": "0.
|
|
389
|
+
"version": "1.0.0"
|
|
383
390
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dbdocs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"author": "@holistics",
|
|
5
5
|
"bin": {
|
|
6
6
|
"dbdocs": "./bin/run"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@dbml/connector": "5.3.
|
|
10
|
-
"@dbml/core": "5.3.
|
|
9
|
+
"@dbml/connector": "5.3.1",
|
|
10
|
+
"@dbml/core": "5.3.1",
|
|
11
11
|
"@oclif/core": "1.26.2",
|
|
12
12
|
"@oclif/plugin-help": "6.2.32",
|
|
13
13
|
"axios": "^1.12.0",
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>dbdocs CLI - Authentication Failed</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
justify-content: center;
|
|
19
|
+
min-height: 100vh;
|
|
20
|
+
background: #f9fafb;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.container {
|
|
24
|
+
background: white;
|
|
25
|
+
border-radius: 6px;
|
|
26
|
+
border: 1px solid #eff0f3;
|
|
27
|
+
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
28
|
+
padding: 32px;
|
|
29
|
+
width: 100%;
|
|
30
|
+
max-width: 400px;
|
|
31
|
+
text-align: center;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.logo-container {
|
|
35
|
+
margin-bottom: 16px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.logo {
|
|
39
|
+
height: 48px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
h1 {
|
|
43
|
+
color: #ff4f46;
|
|
44
|
+
font-size: 24px;
|
|
45
|
+
font-weight: 600;
|
|
46
|
+
margin-bottom: 20px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.error-box {
|
|
50
|
+
background: #ffeef1;
|
|
51
|
+
border: 1px solid #fea5a6;
|
|
52
|
+
border-radius: 6px;
|
|
53
|
+
padding: 16px;
|
|
54
|
+
text-align: left;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.error-title {
|
|
58
|
+
font-weight: 600;
|
|
59
|
+
color: #ff4f46;
|
|
60
|
+
margin-bottom: 8px;
|
|
61
|
+
font-size: 16px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.error-message {
|
|
65
|
+
font-size: 14px;
|
|
66
|
+
color: #ff4f46;
|
|
67
|
+
line-height: 1.5;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
p {
|
|
71
|
+
color: #718096;
|
|
72
|
+
line-height: 1.6;
|
|
73
|
+
font-size: 15px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
strong {
|
|
77
|
+
color: #2d3748;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.footer {
|
|
81
|
+
margin-top: 24px;
|
|
82
|
+
padding-top: 24px;
|
|
83
|
+
border-top: 1px solid #e2e8f0;
|
|
84
|
+
color: #a0aec0;
|
|
85
|
+
font-size: 13px;
|
|
86
|
+
}
|
|
87
|
+
</style>
|
|
88
|
+
</head>
|
|
89
|
+
<body>
|
|
90
|
+
<div class="container">
|
|
91
|
+
<div class="logo-container">
|
|
92
|
+
<svg class="logo" width="155" height="155" viewBox="0 0 155 155" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
93
|
+
<path d="M155 29C155 12.9837 142.016 0 126 0H29C12.9837 0 0 12.9837 0 29V126C0 142.016 12.9837 155 29 155H126C142.016 155 155 142.016 155 126V29Z" fill="#0246CC"/>
|
|
94
|
+
<path d="M77.5 54.6521C102.065 54.6521 121.978 47.8098 121.978 39.3694C121.978 30.929 102.065 24.0867 77.5 24.0867C52.9354 24.0867 33.0218 30.929 33.0218 39.3694C33.0218 47.8098 52.9354 54.6521 77.5 54.6521Z" fill="white"/>
|
|
95
|
+
<path d="M33.0399 76.4867V91.6759C33.0399 96.6868 50.3603 103.965 77.5001 103.965C104.64 103.965 121.96 96.6868 121.96 91.6759V76.4867C112.817 82.4469 95.0968 85.5314 77.5001 85.5314C59.9034 85.5314 42.1828 82.4469 33.0399 76.4867Z" fill="#287EFF"/>
|
|
96
|
+
<path d="M33.0399 50.5847V67.5282C33.0399 72.5391 50.3603 79.8172 77.5001 79.8172C104.64 79.8172 121.96 72.5391 121.96 67.5282V50.5847C113.453 57.1317 97.1229 61.3837 77.5001 61.3837C57.8772 61.3837 41.5476 57.1317 33.0399 50.5847Z" fill="#96C0FF"/>
|
|
97
|
+
<path d="M33.0399 101.065V116.254C33.0399 121.265 50.3603 128.543 77.5001 128.543C104.64 128.543 121.96 121.265 121.96 116.254V101.065C112.817 107.025 95.0968 110.11 77.5001 110.11C59.9034 110.11 42.1828 107.025 33.0399 101.065Z" fill="#0258ED"/>
|
|
98
|
+
</svg>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<h1>Authentication Failed</h1>
|
|
102
|
+
|
|
103
|
+
<div class="error-box">
|
|
104
|
+
<p class="error-title">Invalid redirect URL</p>
|
|
105
|
+
<p class="error-message">The authentication callback could not be completed.</p>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</body>
|
|
109
|
+
</html>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>dbdocs CLI - Completing Authentication</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
justify-content: center;
|
|
19
|
+
min-height: 100vh;
|
|
20
|
+
background: #f9fafb;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.container {
|
|
24
|
+
background: white;
|
|
25
|
+
border-radius: 6px;
|
|
26
|
+
border: 1px solid #eff0f3;
|
|
27
|
+
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
28
|
+
padding: 32px;
|
|
29
|
+
width: 100%;
|
|
30
|
+
max-width: 400px;
|
|
31
|
+
text-align: center;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.logo-container {
|
|
35
|
+
margin-bottom: 16px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.logo {
|
|
39
|
+
height: 48px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
h1 {
|
|
43
|
+
color: #048cff;
|
|
44
|
+
font-size: 24px;
|
|
45
|
+
font-weight: 600;
|
|
46
|
+
margin-bottom: 16px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
p {
|
|
50
|
+
color: #323740;
|
|
51
|
+
line-height: 1.6;
|
|
52
|
+
font-size: 14px;
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
55
|
+
</head>
|
|
56
|
+
<body>
|
|
57
|
+
<div class="container">
|
|
58
|
+
<div class="logo-container">
|
|
59
|
+
<svg class="logo" width="155" height="155" viewBox="0 0 155 155" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
60
|
+
<path d="M155 29C155 12.9837 142.016 0 126 0H29C12.9837 0 0 12.9837 0 29V126C0 142.016 12.9837 155 29 155H126C142.016 155 155 142.016 155 126V29Z" fill="#0246CC"/>
|
|
61
|
+
<path d="M77.5 54.6521C102.065 54.6521 121.978 47.8098 121.978 39.3694C121.978 30.929 102.065 24.0867 77.5 24.0867C52.9354 24.0867 33.0218 30.929 33.0218 39.3694C33.0218 47.8098 52.9354 54.6521 77.5 54.6521Z" fill="white"/>
|
|
62
|
+
<path d="M33.0399 76.4867V91.6759C33.0399 96.6868 50.3603 103.965 77.5001 103.965C104.64 103.965 121.96 96.6868 121.96 91.6759V76.4867C112.817 82.4469 95.0968 85.5314 77.5001 85.5314C59.9034 85.5314 42.1828 82.4469 33.0399 76.4867Z" fill="#287EFF"/>
|
|
63
|
+
<path d="M33.0399 50.5847V67.5282C33.0399 72.5391 50.3603 79.8172 77.5001 79.8172C104.64 79.8172 121.96 72.5391 121.96 67.5282V50.5847C113.453 57.1317 97.1229 61.3837 77.5001 61.3837C57.8772 61.3837 41.5476 57.1317 33.0399 50.5847Z" fill="#96C0FF"/>
|
|
64
|
+
<path d="M33.0399 101.065V116.254C33.0399 121.265 50.3603 128.543 77.5001 128.543C104.64 128.543 121.96 121.265 121.96 116.254V101.065C112.817 107.025 95.0968 110.11 77.5001 110.11C59.9034 110.11 42.1828 107.025 33.0399 101.065Z" fill="#0258ED"/>
|
|
65
|
+
</svg>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<h1>Completing Authentication</h1>
|
|
69
|
+
|
|
70
|
+
<p>You can close this window and return to your terminal to continue.</p>
|
|
71
|
+
</div>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|
package/src/commands/login.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { Command } = require('@oclif/core');
|
|
1
|
+
const { Command, Flags } = require('@oclif/core');
|
|
2
2
|
const open = require('open');
|
|
3
3
|
const inquirer = require('inquirer').default;
|
|
4
4
|
const axios = require('axios');
|
|
@@ -7,7 +7,12 @@ const netrc = require('netrc-parser').default;
|
|
|
7
7
|
const { vars } = require('../vars');
|
|
8
8
|
const { isValidEmail } = require('../validators/email');
|
|
9
9
|
const { isValidOtp } = require('../validators/otp');
|
|
10
|
-
const { LOGIN_METHODS } = require('../utils/constants');
|
|
10
|
+
const { LOGIN_METHODS, LOGIN_STRATEGIES, PORTAL_ERROR_CODES, PORTAL_LOGIN_CONFIGS } = require('../utils/constants');
|
|
11
|
+
const { initAuthClient } = require('../services/auth/portal');
|
|
12
|
+
const { sleep } = require('../utils/helper');
|
|
13
|
+
const { openUrlInExternalBrowser } = require('../utils/browser');
|
|
14
|
+
|
|
15
|
+
const TIMEOUT_BEFORE_OPEN_BROWSER_MS = 500;
|
|
11
16
|
|
|
12
17
|
async function askForOtp (spinner, shortLivedToken, email) {
|
|
13
18
|
const cliSpinner = spinner;
|
|
@@ -69,10 +74,106 @@ async function loginViaEmail (spinner) {
|
|
|
69
74
|
return token;
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
function getPortalErrorMessage(errorCode, errorDescription = '') {
|
|
78
|
+
// If we have errorDescription, use it (this is the main display)
|
|
79
|
+
if (errorDescription) return errorDescription;
|
|
80
|
+
|
|
81
|
+
// Otherwise, fallback to default mapped messages
|
|
82
|
+
const errorMessages = {
|
|
83
|
+
[PORTAL_ERROR_CODES.loginTimeout]: 'Login timed out after 5 minutes. Please try again.',
|
|
84
|
+
[PORTAL_ERROR_CODES.loginCancelled]: 'Login cancelled.',
|
|
85
|
+
[PORTAL_ERROR_CODES.stateMismatch]: 'Security validation failed. Please try again.',
|
|
86
|
+
[PORTAL_ERROR_CODES.invalidCallback]: 'Invalid authentication callback received. Please try again.',
|
|
87
|
+
[PORTAL_ERROR_CODES.exchangeFailed]: 'Failed to exchange authorization code. Please try again.',
|
|
88
|
+
[PORTAL_ERROR_CODES.loopbackServerError]: 'Failed to start callback server. Please try again.',
|
|
89
|
+
[PORTAL_ERROR_CODES.unknownError]: 'Login failed. Please try again.',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return errorMessages[errorCode] || 'An unexpected error occurred. Please try again.';
|
|
93
|
+
}
|
|
94
|
+
|
|
72
95
|
class LoginCommand extends Command {
|
|
73
|
-
|
|
74
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Detect which login strategy to use
|
|
98
|
+
* Priority: CLI flag > env var > default (portal)
|
|
99
|
+
* @param {boolean} legacyFlag - The --legacy flag value
|
|
100
|
+
* @param {string} loginStrategy - The login strategy from environment variable
|
|
101
|
+
* @returns {'portal' | 'legacy'}
|
|
102
|
+
*/
|
|
103
|
+
detectStrategy (legacyFlag, loginStrategy) {
|
|
104
|
+
if (legacyFlag) return LOGIN_STRATEGIES.legacy;
|
|
105
|
+
|
|
106
|
+
return loginStrategy;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Execute portal login flow (OAuth 2.0 with PKCE)
|
|
111
|
+
* @param {ora.Ora} spinner - The ora spinner instance
|
|
112
|
+
* @returns {Promise<string>} Access token
|
|
113
|
+
* @throws {Error} If login fails (after displaying error to user)
|
|
114
|
+
*/
|
|
115
|
+
async executePortalLogin (spinner) {
|
|
116
|
+
let authClient = null;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
this.log('Preparing authentication...');
|
|
120
|
+
|
|
121
|
+
authClient = await initAuthClient({
|
|
122
|
+
loginUrl: vars.loginUrl,
|
|
123
|
+
loginApiUrl: vars.loginApiUrl,
|
|
124
|
+
clientId: PORTAL_LOGIN_CONFIGS.clientId,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const loginUrl = authClient.getLoginUrl();
|
|
128
|
+
|
|
129
|
+
spinner.text = 'Attempting to open authentication page in your browser...';
|
|
130
|
+
spinner.start();
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await sleep(TIMEOUT_BEFORE_OPEN_BROWSER_MS);
|
|
134
|
+
await openUrlInExternalBrowser(loginUrl);
|
|
135
|
+
|
|
136
|
+
spinner.succeed();
|
|
137
|
+
} catch (err) {
|
|
138
|
+
spinner.warn();
|
|
139
|
+
spinner.warn(`Could not open browser automatically. Please navigate to:\n${loginUrl}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
spinner.text = 'Waiting for authentication... (Press CTRL+C to cancel)';
|
|
143
|
+
spinner.start();
|
|
144
|
+
|
|
145
|
+
const { accessToken } = await authClient.obtainTokens();
|
|
146
|
+
|
|
147
|
+
spinner.text = 'Authentication successful';
|
|
148
|
+
spinner.succeed();
|
|
149
|
+
|
|
150
|
+
return accessToken;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const errorMessage = error.code
|
|
153
|
+
? getPortalErrorMessage(error.code, error.errorDescription)
|
|
154
|
+
: `Login failed: ${error.message || 'Unknown error'}`;
|
|
155
|
+
|
|
156
|
+
this.error(errorMessage);
|
|
157
|
+
} finally {
|
|
158
|
+
// Always cleanup
|
|
159
|
+
if (authClient) {
|
|
160
|
+
authClient.cleanup();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Execute legacy login flow (Email OTP or Social)
|
|
167
|
+
* @param {ora.Ora} spinner - The ora spinner instance
|
|
168
|
+
* @returns {Promise<string>} Access token
|
|
169
|
+
* @throws {Error} If login fails (after displaying error to user)
|
|
170
|
+
*/
|
|
171
|
+
async executeLegacyLogin (spinner) {
|
|
75
172
|
try {
|
|
173
|
+
// Show deprecation warning
|
|
174
|
+
this.warn('⚠️ You are using the legacy login. You can remove the --legacy flag for automatic browser authentication.');
|
|
175
|
+
|
|
176
|
+
// Prompt for login method
|
|
76
177
|
const loginMethodAnswer = await inquirer.prompt([
|
|
77
178
|
{
|
|
78
179
|
type: 'rawlist',
|
|
@@ -103,69 +204,138 @@ class LoginCommand extends Command {
|
|
|
103
204
|
authToken = answer.authToken;
|
|
104
205
|
}
|
|
105
206
|
|
|
106
|
-
|
|
107
|
-
|
|
207
|
+
return authToken;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
// Handle legacy-specific errors
|
|
210
|
+
let errorMessage = error.message || 'Login failed. Please try again.';
|
|
211
|
+
|
|
212
|
+
if (error.response) {
|
|
213
|
+
const { error: apiError } = error.response.data;
|
|
214
|
+
switch (apiError.name) {
|
|
215
|
+
case 'OtpInvalidError':
|
|
216
|
+
errorMessage = 'Invalid OTP. Please login again.';
|
|
217
|
+
break;
|
|
218
|
+
|
|
219
|
+
case 'OtpUsedError':
|
|
220
|
+
errorMessage = 'OTP used recently. Please login again after couple minutes.';
|
|
221
|
+
break;
|
|
222
|
+
|
|
223
|
+
case 'EmailDeliveryFailedError':
|
|
224
|
+
errorMessage = 'Email delivery failed. Please login again.';
|
|
225
|
+
break;
|
|
226
|
+
|
|
227
|
+
default:
|
|
228
|
+
errorMessage = error.message || 'Login failed. Please try again.';
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.error(errorMessage);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Verify token with API
|
|
239
|
+
* @param {string} token - Access token to verify
|
|
240
|
+
* @returns {Promise<object>} User account object
|
|
241
|
+
* @throws {Error} If token is invalid
|
|
242
|
+
*/
|
|
243
|
+
async verifyToken (token) {
|
|
244
|
+
try {
|
|
108
245
|
const { data: { account } } = await axios.get(`${vars.apiUrl}/account`, {
|
|
109
246
|
headers: {
|
|
110
|
-
Authorization:
|
|
247
|
+
Authorization: token,
|
|
111
248
|
'Authorization-Method': 'login',
|
|
112
249
|
},
|
|
113
250
|
});
|
|
251
|
+
|
|
252
|
+
return account;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (error.response) {
|
|
255
|
+
const { error: apiError } = error.response.data;
|
|
256
|
+
switch (apiError.name) {
|
|
257
|
+
case 'TokenExpiredError':
|
|
258
|
+
throw new Error('Your token has expired. Please login again.');
|
|
259
|
+
|
|
260
|
+
case 'InvalidAuthToken':
|
|
261
|
+
throw new Error('Invalid token. Please login again.');
|
|
262
|
+
|
|
263
|
+
default:
|
|
264
|
+
throw new Error('Failed to verify token. Please try again.');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Store token to ~/.netrc file
|
|
274
|
+
* @param {string} token - Access token to store
|
|
275
|
+
* @param {object} user - User account object with email
|
|
276
|
+
* @returns {Promise<void>}
|
|
277
|
+
*/
|
|
278
|
+
async storeToken (token, user) {
|
|
279
|
+
const { apiHost } = vars;
|
|
280
|
+
await netrc.load();
|
|
281
|
+
|
|
282
|
+
const previousEntry = netrc.machines[apiHost];
|
|
283
|
+
if (!previousEntry) {
|
|
284
|
+
netrc.machines[apiHost] = {};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
netrc.machines[apiHost].login = user.email;
|
|
288
|
+
netrc.machines[apiHost].password = token;
|
|
289
|
+
await netrc.save();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async run () {
|
|
293
|
+
const { flags } = await this.parse(LoginCommand);
|
|
294
|
+
const spinner = ora({});
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
// 1. Detect strategy
|
|
298
|
+
const strategy = this.detectStrategy(flags.legacy, vars.loginStrategy);
|
|
299
|
+
|
|
300
|
+
// 2. Execute strategy (returns token)
|
|
301
|
+
let accessToken;
|
|
302
|
+
if (strategy === LOGIN_STRATEGIES.legacy) {
|
|
303
|
+
accessToken = await this.executeLegacyLogin(spinner);
|
|
304
|
+
} else {
|
|
305
|
+
accessToken = await this.executePortalLogin(spinner);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 3. Verify token
|
|
309
|
+
spinner.text = 'Validate token';
|
|
310
|
+
spinner.start();
|
|
311
|
+
const user = await this.verifyToken(accessToken);
|
|
114
312
|
spinner.succeed();
|
|
115
313
|
|
|
314
|
+
// 4. Store token
|
|
116
315
|
spinner.text = 'Save credential';
|
|
117
316
|
spinner.start();
|
|
118
|
-
|
|
119
|
-
await netrc.load();
|
|
120
|
-
const previousEntry = netrc.machines[apiHost];
|
|
121
|
-
if (!previousEntry) {
|
|
122
|
-
netrc.machines[apiHost] = {};
|
|
123
|
-
}
|
|
124
|
-
netrc.machines[apiHost].login = account.email;
|
|
125
|
-
netrc.machines[apiHost].password = authToken;
|
|
126
|
-
await netrc.save();
|
|
317
|
+
await this.storeToken(accessToken, user);
|
|
127
318
|
spinner.succeed();
|
|
128
319
|
|
|
129
320
|
this.log('\nDone.');
|
|
130
|
-
} catch (
|
|
321
|
+
} catch (error) {
|
|
322
|
+
// Cleanup spinner if still running
|
|
131
323
|
if (spinner.isSpinning) {
|
|
132
324
|
spinner.fail();
|
|
133
325
|
}
|
|
134
|
-
let message = err.message || 'Something wrong :( Please try again.';
|
|
135
|
-
if (err.response) {
|
|
136
|
-
const { error } = err.response.data;
|
|
137
|
-
switch (error.name) {
|
|
138
|
-
case 'TokenExpiredError':
|
|
139
|
-
message = 'Your token has expired. Please login again.';
|
|
140
|
-
break;
|
|
141
|
-
|
|
142
|
-
case 'InvalidAuthToken':
|
|
143
|
-
message = 'Invalid token. Please login again.';
|
|
144
|
-
break;
|
|
145
326
|
|
|
146
|
-
|
|
147
|
-
message = 'Invalid OTP. Please login again.';
|
|
148
|
-
break;
|
|
149
|
-
|
|
150
|
-
case 'OtpUsedError':
|
|
151
|
-
message = 'OTP used recently. Please login again after couple minutes.';
|
|
152
|
-
break;
|
|
153
|
-
|
|
154
|
-
case 'EmailDeliveryFailedError':
|
|
155
|
-
message = 'Email delivery failed. Please login again.';
|
|
156
|
-
break;
|
|
157
|
-
|
|
158
|
-
default:
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
this.error(message);
|
|
327
|
+
this.error(error.message || 'Something wrong. Please try again.');
|
|
163
328
|
}
|
|
164
329
|
}
|
|
165
330
|
}
|
|
166
331
|
|
|
167
|
-
LoginCommand.description =
|
|
168
|
-
|
|
169
|
-
|
|
332
|
+
LoginCommand.description = 'login to dbdocs';
|
|
333
|
+
|
|
334
|
+
LoginCommand.flags = {
|
|
335
|
+
legacy: Flags.boolean({
|
|
336
|
+
description: 'Use legacy login methods: Email OTP or Google/GitHub with manual token copy',
|
|
337
|
+
default: false,
|
|
338
|
+
}),
|
|
339
|
+
};
|
|
170
340
|
|
|
171
341
|
module.exports = LoginCommand;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const BaseError = require('./BaseError');
|
|
2
|
+
|
|
3
|
+
class PortalAuthenticationError extends BaseError {
|
|
4
|
+
constructor(code, errorDescription = '') {
|
|
5
|
+
super(code, code); // Base message is just the code (internal use)
|
|
6
|
+
this.errorDescription = errorDescription; // This is the main display message
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
PortalAuthenticationError,
|
|
12
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
const { randomBytes, createHash } = require('crypto');
|
|
2
|
+
const axios = require('axios');
|
|
3
|
+
const { startLoopbackServer } = require('./server');
|
|
4
|
+
const { PORTAL_ERROR_CODES, PORTAL_LOGIN_CONFIGS } = require('../../../utils/constants');
|
|
5
|
+
const { PortalAuthenticationError } = require('../../../errors/common-errors');
|
|
6
|
+
|
|
7
|
+
const STATE_BYTES_LENGTH = 32;
|
|
8
|
+
const CODE_VERIFIER_BYTES_LENGTH = 32;
|
|
9
|
+
|
|
10
|
+
const generateLoginState = () => randomBytes(STATE_BYTES_LENGTH).toString('base64url');
|
|
11
|
+
|
|
12
|
+
const generateCodeVerifier = () => randomBytes(CODE_VERIFIER_BYTES_LENGTH).toString('base64url');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
*
|
|
16
|
+
* @param {string} codeVerifier
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
const generateCodeChallenge = (codeVerifier) => {
|
|
20
|
+
const hash = createHash('sha256');
|
|
21
|
+
hash.update(codeVerifier);
|
|
22
|
+
|
|
23
|
+
return hash.digest().toString('base64url');
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build portal login URL with OAuth parameters
|
|
28
|
+
* @param {{ baseUrl: string, redirectUri: string, codeChallenge: string, state: string, clientId: string }} params
|
|
29
|
+
* @returns {URL}
|
|
30
|
+
*/
|
|
31
|
+
const generateLoginUrl = (params) => {
|
|
32
|
+
const {
|
|
33
|
+
baseUrl,
|
|
34
|
+
redirectUri,
|
|
35
|
+
codeChallenge,
|
|
36
|
+
state,
|
|
37
|
+
clientId,
|
|
38
|
+
} = params;
|
|
39
|
+
|
|
40
|
+
const url = new URL('/login', baseUrl);
|
|
41
|
+
url.searchParams.set('client_id', clientId);
|
|
42
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
43
|
+
url.searchParams.set('response_type', 'code');
|
|
44
|
+
url.searchParams.set('code_challenge', codeChallenge);
|
|
45
|
+
url.searchParams.set('state', state);
|
|
46
|
+
url.searchParams.set('prompt', 'select_account');
|
|
47
|
+
|
|
48
|
+
return url;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Exchange authorization code for access token
|
|
53
|
+
* @param {{ apiHost: string, code: string, codeVerifier: string, redirectUri: string, clientId: string }} params
|
|
54
|
+
* @returns {Promise<{ accessToken: string }>}
|
|
55
|
+
* @throws {PortalAuthenticationError} with code 'EXCHANGE_FAILED'
|
|
56
|
+
*/
|
|
57
|
+
async function exchangeCodeForToken(params) {
|
|
58
|
+
const { apiHost, code, codeVerifier, redirectUri, clientId } = params;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const { data } = await axios.post(`${apiHost}/portal/auth/token`, {
|
|
62
|
+
grantType: 'authorization_code',
|
|
63
|
+
redirectUri,
|
|
64
|
+
clientId,
|
|
65
|
+
code,
|
|
66
|
+
codeVerifier,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return data;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (axios.isAxiosError(error)) {
|
|
72
|
+
// We let the description be empty since there is a mapping function that has a fallback for it
|
|
73
|
+
let description = '';
|
|
74
|
+
if (axios.isAxiosError(error)) {
|
|
75
|
+
if (error.response) {
|
|
76
|
+
description = error.response.data?.message || '';
|
|
77
|
+
} else {
|
|
78
|
+
description = error.message || '';
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
description = error.message || '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new PortalAuthenticationError(
|
|
85
|
+
PORTAL_ERROR_CODES.exchangeFailed,
|
|
86
|
+
description,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
throw new PortalAuthenticationError(
|
|
91
|
+
PORTAL_ERROR_CODES.exchangeFailed,
|
|
92
|
+
error.message || '',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Initialize OAuth client for portal login
|
|
99
|
+
* Returns a client object with methods to get login URL and exchange token
|
|
100
|
+
*
|
|
101
|
+
* @param {{ loginUrl: string, loginApiUrl: string, clientId: string }} options
|
|
102
|
+
* @returns {Promise<{
|
|
103
|
+
* getLoginUrl: () => string,
|
|
104
|
+
* obtainTokens: () => Promise<{ accessToken: string }>,
|
|
105
|
+
* cleanup: () => void
|
|
106
|
+
* }>}
|
|
107
|
+
*/
|
|
108
|
+
async function initAuthClient(options) {
|
|
109
|
+
const { loginUrl, loginApiUrl, clientId } = options;
|
|
110
|
+
|
|
111
|
+
const codeVerifier = generateCodeVerifier();
|
|
112
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
113
|
+
const state = generateLoginState();
|
|
114
|
+
|
|
115
|
+
// Start loopback server with configuration
|
|
116
|
+
const { port, close: closeLoopbackServer, loginCompletePromise } = await startLoopbackServer({
|
|
117
|
+
address: PORTAL_LOGIN_CONFIGS.loopbackServer.address,
|
|
118
|
+
port: PORTAL_LOGIN_CONFIGS.loopbackServer.autoAssignedPort,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const redirectUri = `http://${PORTAL_LOGIN_CONFIGS.loopbackServer.address}:${port}/callback`;
|
|
122
|
+
|
|
123
|
+
const portalLoginUrl = generateLoginUrl({
|
|
124
|
+
baseUrl: loginUrl,
|
|
125
|
+
redirectUri,
|
|
126
|
+
codeChallenge,
|
|
127
|
+
state,
|
|
128
|
+
clientId,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const getLoginUrl = () => portalLoginUrl.toString();
|
|
132
|
+
|
|
133
|
+
const obtainTokens = async () => {
|
|
134
|
+
let timeoutId;
|
|
135
|
+
let sigintHandler;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
// Prevent infinite waiting for the browser response
|
|
139
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
140
|
+
timeoutId = setTimeout(() => {
|
|
141
|
+
reject(
|
|
142
|
+
new PortalAuthenticationError(PORTAL_ERROR_CODES.loginTimeout)
|
|
143
|
+
)
|
|
144
|
+
}, PORTAL_LOGIN_CONFIGS.loginTimeoutMs);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Create cancellation promise for user termination (Ctrl+C)
|
|
148
|
+
const cancellationPromise = new Promise((_, reject) => {
|
|
149
|
+
sigintHandler = () => {
|
|
150
|
+
reject(
|
|
151
|
+
new PortalAuthenticationError(PORTAL_ERROR_CODES.loginCancelled)
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
process.once('SIGINT', sigintHandler);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const { code, state: returnedState } = await Promise.race([
|
|
158
|
+
loginCompletePromise,
|
|
159
|
+
timeoutPromise,
|
|
160
|
+
cancellationPromise,
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
// Validate state to prevent CSRF attacks
|
|
164
|
+
if (state !== returnedState) {
|
|
165
|
+
throw new PortalAuthenticationError(PORTAL_ERROR_CODES.stateMismatch);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Exchange authorization code for access token
|
|
169
|
+
const { accessToken } = await exchangeCodeForToken({
|
|
170
|
+
apiHost: loginApiUrl,
|
|
171
|
+
code,
|
|
172
|
+
codeVerifier,
|
|
173
|
+
redirectUri,
|
|
174
|
+
clientId,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return { accessToken };
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error instanceof PortalAuthenticationError) throw error;
|
|
180
|
+
|
|
181
|
+
throw new PortalAuthenticationError(
|
|
182
|
+
PORTAL_ERROR_CODES.unknownError,
|
|
183
|
+
error.message || '',
|
|
184
|
+
);
|
|
185
|
+
} finally {
|
|
186
|
+
// Cleanup resources
|
|
187
|
+
if (timeoutId) {
|
|
188
|
+
clearTimeout(timeoutId);
|
|
189
|
+
}
|
|
190
|
+
if (sigintHandler) {
|
|
191
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
192
|
+
}
|
|
193
|
+
closeLoopbackServer();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
getLoginUrl,
|
|
199
|
+
obtainTokens,
|
|
200
|
+
cleanup: () => { closeLoopbackServer() },
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
initAuthClient,
|
|
206
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { createServer } = require('http');
|
|
4
|
+
|
|
5
|
+
const { PortalAuthenticationError } = require('../../../errors/common-errors');
|
|
6
|
+
const { PORTAL_ERROR_CODES } = require('../../../utils/constants');
|
|
7
|
+
|
|
8
|
+
// Load HTML templates once at module load
|
|
9
|
+
const errorHtml = fs.readFileSync(path.join(__dirname, '../../../assets', 'callback-error.html'), 'utf-8');
|
|
10
|
+
const successHtml = fs.readFileSync(path.join(__dirname, '../../../assets', 'callback-success.html'), 'utf-8');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_HTTP_HEADERS = {
|
|
13
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Start a local HTTP loopback server to receive OAuth callback
|
|
18
|
+
* @param {Object} options - Server configuration options
|
|
19
|
+
* @param {string} options.address - Address to bind to (default: '127.0.0.1')
|
|
20
|
+
* @param {number} options.port - Port to listen on (0 = auto-assign)
|
|
21
|
+
* @returns {Promise<{ port: number, loginCompletePromise: Promise<{code: string, state: string}>, close: Function }>}
|
|
22
|
+
*/
|
|
23
|
+
const startLoopbackServer = ({ address, port } = {}) => {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
let isServerClosed = false;
|
|
26
|
+
let loginResolve = null;
|
|
27
|
+
let loginReject = null;
|
|
28
|
+
|
|
29
|
+
const loginCompletePromise = new Promise((res, rej) => {
|
|
30
|
+
loginResolve = res;
|
|
31
|
+
loginReject = rej;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const server = createServer();
|
|
35
|
+
|
|
36
|
+
const closeServer = () => {
|
|
37
|
+
if (!isServerClosed && server) {
|
|
38
|
+
isServerClosed = true;
|
|
39
|
+
|
|
40
|
+
// The success or error page could keep the connection alive and prevent the server from closing
|
|
41
|
+
// So we need to close all connections before closing the server
|
|
42
|
+
server.closeAllConnections();
|
|
43
|
+
server.close();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
server.on('error', (err) => {
|
|
48
|
+
closeServer();
|
|
49
|
+
reject(
|
|
50
|
+
new PortalAuthenticationError(
|
|
51
|
+
PORTAL_ERROR_CODES.loopbackServerError,
|
|
52
|
+
err.message || '',
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
server.on('request', (req, res) => {
|
|
58
|
+
const reqUrl = new URL(req.url, `http://${req.headers.host}`);
|
|
59
|
+
|
|
60
|
+
const { pathname } = reqUrl;
|
|
61
|
+
switch (pathname) {
|
|
62
|
+
case '/callback': {
|
|
63
|
+
const code = reqUrl.searchParams.get('code');
|
|
64
|
+
const state = reqUrl.searchParams.get('state');
|
|
65
|
+
|
|
66
|
+
if (!code || !state) {
|
|
67
|
+
res.writeHead(400, DEFAULT_HTTP_HEADERS);
|
|
68
|
+
res.end(errorHtml);
|
|
69
|
+
|
|
70
|
+
closeServer();
|
|
71
|
+
loginReject(new PortalAuthenticationError(PORTAL_ERROR_CODES.invalidCallback));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
res.writeHead(200, DEFAULT_HTTP_HEADERS);
|
|
76
|
+
res.end(successHtml);
|
|
77
|
+
|
|
78
|
+
loginResolve({ code, state });
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
default: {
|
|
82
|
+
res.writeHead(404, DEFAULT_HTTP_HEADERS);
|
|
83
|
+
res.end();
|
|
84
|
+
closeServer();
|
|
85
|
+
loginReject(new PortalAuthenticationError(
|
|
86
|
+
PORTAL_ERROR_CODES.invalidCallback,
|
|
87
|
+
));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Listen on specified address and port
|
|
94
|
+
server.listen(port, address, () => {
|
|
95
|
+
resolve({
|
|
96
|
+
port: server.address().port,
|
|
97
|
+
loginCompletePromise,
|
|
98
|
+
close: closeServer,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
startLoopbackServer,
|
|
106
|
+
};
|
package/src/user_data.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"BUILD_COUNT":
|
|
1
|
+
{"BUILD_COUNT":2}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const open = require('open');
|
|
2
|
+
/**
|
|
3
|
+
* Open URL in default browser
|
|
4
|
+
* @param {string} url - URL to open
|
|
5
|
+
* @returns {Promise<void>}
|
|
6
|
+
*/
|
|
7
|
+
const openUrlInExternalBrowser = async (url) => {
|
|
8
|
+
await open(url, { wait: false });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
openUrlInExternalBrowser,
|
|
13
|
+
};
|
package/src/utils/constants.js
CHANGED
|
@@ -43,6 +43,37 @@ const HOST_CONFIG_STATUS = {
|
|
|
43
43
|
inactive: 'inactive',
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
const LOGIN_STRATEGIES = {
|
|
47
|
+
legacy: 'legacy',
|
|
48
|
+
portal: 'portal',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Portal authentication error codes
|
|
53
|
+
* Used for OAuth 2.0 flow errors
|
|
54
|
+
*/
|
|
55
|
+
const PORTAL_ERROR_CODES = {
|
|
56
|
+
loginTimeout: 'LOGIN_TIMEOUT',
|
|
57
|
+
loginCancelled: 'LOGIN_CANCELLED',
|
|
58
|
+
stateMismatch: 'STATE_MISMATCH',
|
|
59
|
+
invalidCallback: 'INVALID_CALLBACK',
|
|
60
|
+
exchangeFailed: 'EXCHANGE_FAILED',
|
|
61
|
+
loopbackServerError: 'LOOPBACK_SERVER_ERROR',
|
|
62
|
+
unknownError: 'UNKNOWN_ERROR',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const PORTAL_LOGIN_CONFIGS = {
|
|
66
|
+
clientId: 'dbdocs-cli',
|
|
67
|
+
loginTimeoutMs: 5 * 60 * 1000, // 5 minutes
|
|
68
|
+
loopbackServer: {
|
|
69
|
+
// It's recommended to use 127.0.0.1 instead of localhost since the localhost can be overriden on the host machine
|
|
70
|
+
// See https://www.rfc-editor.org/rfc/rfc8252#section-8.3
|
|
71
|
+
address: '127.0.0.1',
|
|
72
|
+
// For node http server, port 0 means the OS will assign a random one
|
|
73
|
+
autoAssignedPort: 0,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
46
77
|
module.exports = {
|
|
47
78
|
PROJECT_GENERAL_ACCESS_TYPE,
|
|
48
79
|
PROJECT_SHARING_TEXT,
|
|
@@ -52,4 +83,7 @@ module.exports = {
|
|
|
52
83
|
WINDOW_FILE_PATH_REGEX,
|
|
53
84
|
UNIX_FILE_PATH_REGEX,
|
|
54
85
|
HOST_CONFIG_STATUS,
|
|
86
|
+
LOGIN_STRATEGIES,
|
|
87
|
+
PORTAL_ERROR_CODES,
|
|
88
|
+
PORTAL_LOGIN_CONFIGS,
|
|
55
89
|
};
|
package/src/utils/helper.js
CHANGED
|
@@ -15,8 +15,15 @@ const parseProjectName = (projectName) => {
|
|
|
15
15
|
|
|
16
16
|
const getProjectUrl = (hostUrl, orgName, projectUrl) => `${hostUrl}/${encodeURIComponent(orgName)}/${projectUrl}`;
|
|
17
17
|
|
|
18
|
+
const sleep = async (miliseconds) => new Promise((resolve) => {
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
resolve(undefined);
|
|
21
|
+
}, miliseconds);
|
|
22
|
+
});
|
|
23
|
+
|
|
18
24
|
module.exports = {
|
|
19
25
|
getIsPublicValueFromBuildFlag,
|
|
20
26
|
getProjectUrl,
|
|
21
27
|
parseProjectName,
|
|
28
|
+
sleep,
|
|
22
29
|
};
|
package/src/vars.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { LOGIN_STRATEGIES } = require("./utils/constants");
|
|
2
|
+
|
|
1
3
|
/* eslint-disable class-methods-use-this */
|
|
2
4
|
class Vars {
|
|
3
5
|
get host () {
|
|
@@ -23,6 +25,19 @@ class Vars {
|
|
|
23
25
|
get envApiHost () {
|
|
24
26
|
return process.env.DBDOCS_API_HOST;
|
|
25
27
|
}
|
|
28
|
+
|
|
29
|
+
get loginStrategy () {
|
|
30
|
+
const envLoginStrategy = process.env.DBDOCS_LOGIN_STRATEGY || '';
|
|
31
|
+
return Object.values(LOGIN_STRATEGIES).includes(envLoginStrategy) ? envLoginStrategy : LOGIN_STRATEGIES.portal;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get loginUrl () {
|
|
35
|
+
return process.env.DBDOCS_LOGIN_URL || 'https://dbdiagram.io';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get loginApiUrl () {
|
|
39
|
+
return process.env.DBDOCS_LOGIN_API_URL || this.apiUrl;
|
|
40
|
+
}
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
module.exports.Vars = Vars;
|
package/src/vars.js.staging
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { LOGIN_STRATEGIES } = require("./utils/constants");
|
|
2
|
+
|
|
1
3
|
/* eslint-disable class-methods-use-this */
|
|
2
4
|
// Copied from `vars.js` and replace the 5th and 9th lines with staging's endpoints
|
|
3
5
|
class Vars {
|
|
@@ -24,6 +26,19 @@ class Vars {
|
|
|
24
26
|
get envApiHost () {
|
|
25
27
|
return process.env.DBDOCS_API_HOST;
|
|
26
28
|
}
|
|
29
|
+
|
|
30
|
+
get loginStrategy () {
|
|
31
|
+
const envLoginStrategy = process.env.DBDOCS_LOGIN_STRATEGY || '';
|
|
32
|
+
return Object.values(LOGIN_STRATEGIES).includes(envLoginStrategy) ? envLoginStrategy : LOGIN_STRATEGIES.portal;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get loginUrl () {
|
|
36
|
+
return process.env.DBDOCS_LOGIN_URL || 'https://staging.dbdiagram.io';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get loginApiUrl () {
|
|
40
|
+
return process.env.DBDOCS_LOGIN_API_URL || this.apiUrl;
|
|
41
|
+
}
|
|
27
42
|
}
|
|
28
43
|
|
|
29
44
|
module.exports.Vars = Vars;
|