@nocobase/cli 2.2.0-beta.7 → 2.2.0-beta.9
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/assets/env-proxy/nginx/app.conf.tpl +23 -0
- package/assets/env-proxy/nginx/nocobase.conf.tpl +5 -0
- package/assets/env-proxy/nginx/snippets/dist-location.conf +5 -0
- package/assets/env-proxy/nginx/snippets/gzip.conf +17 -0
- package/assets/env-proxy/nginx/snippets/log-format-http.conf +13 -0
- package/assets/env-proxy/nginx/snippets/maps-http.conf +14 -0
- package/assets/env-proxy/nginx/snippets/mime-types.conf +98 -0
- package/assets/env-proxy/nginx/snippets/proxy-location.conf +18 -0
- package/assets/env-proxy/nginx/snippets/spa-location.conf +6 -0
- package/assets/env-proxy/nginx/snippets/uploads-location.conf +21 -0
- package/dist/commands/app/start.js +4 -1
- package/dist/commands/install.js +53 -138
- package/dist/commands/proxy/caddy/generate.js +93 -7
- package/dist/commands/proxy/nginx/generate.js +98 -7
- package/dist/commands/revision/create.js +1 -1
- package/dist/commands/self/update.js +2 -2
- package/dist/commands/source/download.js +16 -12
- package/dist/lib/app-managed-resources.js +3 -4
- package/dist/lib/auth-store.js +82 -6
- package/dist/lib/cli-config.js +71 -1
- package/dist/lib/docker-image.js +94 -6
- package/dist/lib/env-auth.js +291 -45
- package/dist/lib/env-config.js +5 -0
- package/dist/lib/env-proxy-config.js +48 -0
- package/dist/lib/env-proxy.js +164 -58
- package/dist/lib/prompt-validators.js +1 -1
- package/dist/lib/proxy-caddy.js +77 -9
- package/dist/lib/proxy-nginx.js +71 -11
- package/dist/lib/run-npm.js +4 -0
- package/dist/lib/self-manager.js +251 -46
- package/dist/lib/startup-update.js +1 -1
- package/dist/locale/en-US.json +2 -2
- package/dist/locale/zh-CN.json +2 -2
- package/package.json +3 -2
package/dist/lib/env-auth.js
CHANGED
|
@@ -22,9 +22,45 @@ const OAUTH_FETCH_TIMEOUT_MS = 15_000;
|
|
|
22
22
|
const OAUTH_FETCH_RETRY_DELAYS_MS = [500, 1_000, 2_000];
|
|
23
23
|
const DEFAULT_OAUTH_SCOPE = 'openid api offline_access';
|
|
24
24
|
const DEFAULT_CLIENT_NAME = 'NocoBase CLI';
|
|
25
|
+
const DEVICE_CODE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
26
|
+
const DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5;
|
|
25
27
|
function normalizeBaseUrl(baseUrl) {
|
|
26
28
|
return baseUrl.replace(/\/+$/, '');
|
|
27
29
|
}
|
|
30
|
+
function buildDeviceVerificationPathFromApiBaseUrl(apiBaseUrl) {
|
|
31
|
+
const url = new URL(apiBaseUrl);
|
|
32
|
+
const subappMatch = url.pathname.match(/^(.*)\/api\/__app\/([^/]+)\/?$/);
|
|
33
|
+
if (subappMatch) {
|
|
34
|
+
const publicPath = (subappMatch[1] || '').replace(/\/+$/, '');
|
|
35
|
+
return `${publicPath}/apps/${subappMatch[2]}/idpOAuth/device`;
|
|
36
|
+
}
|
|
37
|
+
const appMatch = url.pathname.match(/^(.*)\/api\/?$/);
|
|
38
|
+
if (appMatch) {
|
|
39
|
+
const publicPath = (appMatch[1] || '').replace(/\/+$/, '');
|
|
40
|
+
return `${publicPath}/idpOAuth/device`;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
export function resolveDeviceVerificationUrlForApiBaseUrl(verificationUrl, apiBaseUrl) {
|
|
45
|
+
try {
|
|
46
|
+
const devicePath = buildDeviceVerificationPathFromApiBaseUrl(apiBaseUrl);
|
|
47
|
+
if (!devicePath) {
|
|
48
|
+
return verificationUrl;
|
|
49
|
+
}
|
|
50
|
+
const originalUrl = new URL(verificationUrl);
|
|
51
|
+
if (!originalUrl.pathname.endsWith('/idpOAuth/device')) {
|
|
52
|
+
return verificationUrl;
|
|
53
|
+
}
|
|
54
|
+
const publicUrl = new URL(apiBaseUrl);
|
|
55
|
+
publicUrl.pathname = devicePath;
|
|
56
|
+
publicUrl.search = originalUrl.search;
|
|
57
|
+
publicUrl.hash = originalUrl.hash;
|
|
58
|
+
return publicUrl.toString();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return verificationUrl;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
28
64
|
export function getOauthMetadataUrl(baseUrl) {
|
|
29
65
|
return `${normalizeBaseUrl(baseUrl)}/.well-known/oauth-authorization-server`;
|
|
30
66
|
}
|
|
@@ -129,6 +165,16 @@ function getOauthFetchRetryDelays() {
|
|
|
129
165
|
}
|
|
130
166
|
return OAUTH_FETCH_RETRY_DELAYS_MS;
|
|
131
167
|
}
|
|
168
|
+
function getDevicePollIntervalMs(intervalSeconds) {
|
|
169
|
+
const override = process.env.NB_CLI_OAUTH_DEVICE_POLL_INTERVAL_MS;
|
|
170
|
+
if (override !== undefined) {
|
|
171
|
+
const delay = Number(override);
|
|
172
|
+
if (Number.isFinite(delay) && delay >= 0) {
|
|
173
|
+
return Math.max(1, delay);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return Math.max(1, intervalSeconds ?? DEFAULT_DEVICE_POLL_INTERVAL_SECONDS) * 1000;
|
|
177
|
+
}
|
|
132
178
|
function formatOauthRetryMessage(options) {
|
|
133
179
|
return `${options.operation} failed (${options.reason}). Retrying ${options.attempt}/${options.maxAttempts}...`;
|
|
134
180
|
}
|
|
@@ -223,6 +269,31 @@ async function registerOauthClient(metadata, redirectUri, options = {}) {
|
|
|
223
269
|
if (!metadata.registration_endpoint) {
|
|
224
270
|
throw new Error('OAuth server does not expose a dynamic client registration endpoint.');
|
|
225
271
|
}
|
|
272
|
+
let body;
|
|
273
|
+
if (options.deviceFlow) {
|
|
274
|
+
body = {
|
|
275
|
+
client_name: DEFAULT_CLIENT_NAME,
|
|
276
|
+
application_type: 'native',
|
|
277
|
+
token_endpoint_auth_method: 'none',
|
|
278
|
+
grant_types: [DEVICE_CODE_GRANT_TYPE, 'refresh_token'],
|
|
279
|
+
response_types: [],
|
|
280
|
+
scope: DEFAULT_OAUTH_SCOPE,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
if (!redirectUri) {
|
|
285
|
+
throw new Error('OAuth redirect URI is required for browser sign-in.');
|
|
286
|
+
}
|
|
287
|
+
body = {
|
|
288
|
+
client_name: DEFAULT_CLIENT_NAME,
|
|
289
|
+
application_type: 'native',
|
|
290
|
+
token_endpoint_auth_method: 'none',
|
|
291
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
292
|
+
response_types: ['code'],
|
|
293
|
+
scope: DEFAULT_OAUTH_SCOPE,
|
|
294
|
+
redirect_uris: [redirectUri],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
226
297
|
let response;
|
|
227
298
|
try {
|
|
228
299
|
response = await fetchWithOauthRetry(metadata.registration_endpoint, {
|
|
@@ -231,15 +302,7 @@ async function registerOauthClient(metadata, redirectUri, options = {}) {
|
|
|
231
302
|
accept: 'application/json',
|
|
232
303
|
'content-type': 'application/json',
|
|
233
304
|
},
|
|
234
|
-
body: JSON.stringify(
|
|
235
|
-
client_name: DEFAULT_CLIENT_NAME,
|
|
236
|
-
application_type: 'native',
|
|
237
|
-
token_endpoint_auth_method: 'none',
|
|
238
|
-
grant_types: ['authorization_code', 'refresh_token'],
|
|
239
|
-
response_types: ['code'],
|
|
240
|
-
scope: DEFAULT_OAUTH_SCOPE,
|
|
241
|
-
redirect_uris: [redirectUri],
|
|
242
|
-
}),
|
|
305
|
+
body: JSON.stringify(body),
|
|
243
306
|
}, {
|
|
244
307
|
operation: 'Registering OAuth client',
|
|
245
308
|
onRetry: (message) => updateTask(message),
|
|
@@ -594,6 +657,10 @@ async function maybeOpenBrowser(url) {
|
|
|
594
657
|
cleanup,
|
|
595
658
|
};
|
|
596
659
|
}
|
|
660
|
+
let browserOpener = maybeOpenBrowser;
|
|
661
|
+
export function setOauthBrowserOpenerForTests(opener) {
|
|
662
|
+
browserOpener = opener || maybeOpenBrowser;
|
|
663
|
+
}
|
|
597
664
|
async function createLoopbackServer(state) {
|
|
598
665
|
const result = await new Promise((resolve, reject) => {
|
|
599
666
|
const server = createServer((req, res) => {
|
|
@@ -706,6 +773,109 @@ async function exchangeAuthorizationCode(options) {
|
|
|
706
773
|
}
|
|
707
774
|
return data;
|
|
708
775
|
}
|
|
776
|
+
async function requestDeviceAuthorization(options) {
|
|
777
|
+
if (!options.metadata.device_authorization_endpoint) {
|
|
778
|
+
throw new Error('OAuth server does not expose a device authorization endpoint.');
|
|
779
|
+
}
|
|
780
|
+
const body = new URLSearchParams({
|
|
781
|
+
client_id: options.clientId,
|
|
782
|
+
scope: options.scope,
|
|
783
|
+
resource: options.resource,
|
|
784
|
+
});
|
|
785
|
+
let response;
|
|
786
|
+
try {
|
|
787
|
+
response = await fetchWithOauthRetry(options.metadata.device_authorization_endpoint, {
|
|
788
|
+
method: 'POST',
|
|
789
|
+
headers: {
|
|
790
|
+
accept: 'application/json',
|
|
791
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
792
|
+
},
|
|
793
|
+
body,
|
|
794
|
+
}, {
|
|
795
|
+
operation: 'Starting OAuth device authorization',
|
|
796
|
+
onRetry: (message) => updateTask(message),
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
throw new Error(formatOauthFetchFailure('Failed to start OAuth device authorization.', {
|
|
801
|
+
envName: options.envName,
|
|
802
|
+
baseUrl: options.baseUrl,
|
|
803
|
+
url: options.metadata.device_authorization_endpoint,
|
|
804
|
+
rawMessage: error instanceof Error ? error.message : String(error),
|
|
805
|
+
}));
|
|
806
|
+
}
|
|
807
|
+
const data = await parseJsonResponse(response);
|
|
808
|
+
if (!response.ok) {
|
|
809
|
+
throw new Error(formatOauthError('Failed to start OAuth device authorization', data, response.status));
|
|
810
|
+
}
|
|
811
|
+
if (!data ||
|
|
812
|
+
typeof data !== 'object' ||
|
|
813
|
+
typeof data.device_code !== 'string' ||
|
|
814
|
+
typeof data.user_code !== 'string' ||
|
|
815
|
+
typeof data.verification_uri !== 'string') {
|
|
816
|
+
throw new Error('OAuth device authorization response is missing device_code, user_code, or verification_uri.');
|
|
817
|
+
}
|
|
818
|
+
return data;
|
|
819
|
+
}
|
|
820
|
+
async function pollDeviceToken(options) {
|
|
821
|
+
let intervalMs = getDevicePollIntervalMs(options.interval);
|
|
822
|
+
const deadline = Date.now() + Math.max(1, options.expiresIn ?? OAUTH_LOGIN_TIMEOUT_MS / 1000) * 1000;
|
|
823
|
+
while (Date.now() < deadline) {
|
|
824
|
+
await sleep(intervalMs);
|
|
825
|
+
const body = new URLSearchParams({
|
|
826
|
+
grant_type: DEVICE_CODE_GRANT_TYPE,
|
|
827
|
+
client_id: options.clientId,
|
|
828
|
+
device_code: options.deviceCode,
|
|
829
|
+
});
|
|
830
|
+
let response;
|
|
831
|
+
try {
|
|
832
|
+
response = await fetchWithOauthRetry(options.metadata.token_endpoint, {
|
|
833
|
+
method: 'POST',
|
|
834
|
+
headers: {
|
|
835
|
+
accept: 'application/json',
|
|
836
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
837
|
+
},
|
|
838
|
+
body,
|
|
839
|
+
}, {
|
|
840
|
+
operation: 'Polling OAuth device authorization',
|
|
841
|
+
onRetry: (message) => updateTask(message),
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
catch (error) {
|
|
845
|
+
throw new Error(formatOauthFetchFailure('Failed to poll OAuth device authorization.', {
|
|
846
|
+
envName: options.envName,
|
|
847
|
+
baseUrl: options.baseUrl,
|
|
848
|
+
url: options.metadata.token_endpoint,
|
|
849
|
+
rawMessage: error instanceof Error ? error.message : String(error),
|
|
850
|
+
}));
|
|
851
|
+
}
|
|
852
|
+
const data = await parseJsonResponse(response);
|
|
853
|
+
if (response.ok) {
|
|
854
|
+
if (!data || typeof data !== 'object' || typeof data.access_token !== 'string') {
|
|
855
|
+
throw new Error('OAuth token response is missing access_token.');
|
|
856
|
+
}
|
|
857
|
+
return data;
|
|
858
|
+
}
|
|
859
|
+
const oauthError = typeof data === 'object' && data ? String(data.error || '') : '';
|
|
860
|
+
if (oauthError === 'authorization_pending') {
|
|
861
|
+
updateTask(`Waiting for you to approve device sign-in for "${options.envName}"...`);
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
if (oauthError === 'slow_down') {
|
|
865
|
+
intervalMs += 5_000;
|
|
866
|
+
updateTask(`OAuth server asked us to slow down. Polling again in ${Math.ceil(intervalMs / 1000)}s...`);
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
if (oauthError === 'expired_token') {
|
|
870
|
+
throw new Error(`OAuth device sign-in expired. Run \`nb env auth ${options.envName}\` to try again.`);
|
|
871
|
+
}
|
|
872
|
+
if (oauthError === 'access_denied') {
|
|
873
|
+
throw new Error('OAuth device sign-in was denied.');
|
|
874
|
+
}
|
|
875
|
+
throw new Error(formatOauthError('Failed to poll OAuth device authorization', data, response.status));
|
|
876
|
+
}
|
|
877
|
+
throw new Error(`OAuth device sign-in timed out. Run \`nb env auth ${options.envName}\` to try again.`);
|
|
878
|
+
}
|
|
709
879
|
async function refreshOauthAccessToken(options) {
|
|
710
880
|
if (!options.auth.refreshToken || !options.auth.clientId) {
|
|
711
881
|
throw new Error(`OAuth session for env "${options.envName}" cannot be refreshed. Run \`nb env auth ${options.envName}\`.`);
|
|
@@ -862,41 +1032,79 @@ export async function authenticateEnvWithBasic(options) {
|
|
|
862
1032
|
}
|
|
863
1033
|
return token;
|
|
864
1034
|
}
|
|
865
|
-
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1035
|
+
async function authenticateEnvWithOauthDevice(options) {
|
|
1036
|
+
const resource = getOauthResource(options.metadata.issuer);
|
|
1037
|
+
let cleanupBrowserOpenTarget;
|
|
1038
|
+
try {
|
|
1039
|
+
updateTask(`Preparing device sign-in for "${options.envName}"...`);
|
|
1040
|
+
const registration = await registerOauthClient(options.metadata, undefined, {
|
|
1041
|
+
envName: options.envName,
|
|
1042
|
+
baseUrl: options.baseUrl,
|
|
1043
|
+
deviceFlow: true,
|
|
1044
|
+
});
|
|
1045
|
+
const deviceAuthorization = await requestDeviceAuthorization({
|
|
1046
|
+
metadata: options.metadata,
|
|
1047
|
+
clientId: registration.clientId,
|
|
1048
|
+
scope: DEFAULT_OAUTH_SCOPE,
|
|
1049
|
+
resource,
|
|
1050
|
+
envName: options.envName,
|
|
1051
|
+
baseUrl: options.baseUrl,
|
|
1052
|
+
});
|
|
1053
|
+
stopTask();
|
|
1054
|
+
const verificationUrl = resolveDeviceVerificationUrlForApiBaseUrl(deviceAuthorization.verification_uri_complete || deviceAuthorization.verification_uri, options.baseUrl);
|
|
1055
|
+
const browser = await browserOpener(verificationUrl);
|
|
1056
|
+
cleanupBrowserOpenTarget = browser.cleanup;
|
|
1057
|
+
if (!browser.opened) {
|
|
1058
|
+
printWarningBlock('We could not open your browser automatically. Open the URL below to approve device sign-in:');
|
|
1059
|
+
}
|
|
1060
|
+
else {
|
|
1061
|
+
printInfo(`Opening device sign-in for "${options.envName}" in your browser.`);
|
|
1062
|
+
printInfo('If the browser does not open automatically, open this URL manually:');
|
|
1063
|
+
}
|
|
1064
|
+
printInfo(verificationUrl);
|
|
1065
|
+
printInfo(`Enter code: ${deviceAuthorization.user_code}`);
|
|
1066
|
+
updateTask(`Waiting for you to approve device sign-in for "${options.envName}"...`);
|
|
1067
|
+
const tokenResponse = await pollDeviceToken({
|
|
1068
|
+
metadata: options.metadata,
|
|
1069
|
+
clientId: registration.clientId,
|
|
1070
|
+
deviceCode: deviceAuthorization.device_code,
|
|
1071
|
+
expiresIn: deviceAuthorization.expires_in,
|
|
1072
|
+
interval: deviceAuthorization.interval,
|
|
1073
|
+
envName: options.envName,
|
|
1074
|
+
baseUrl: options.baseUrl,
|
|
1075
|
+
});
|
|
1076
|
+
if (!tokenResponse.refresh_token) {
|
|
1077
|
+
printWarning('Sign-in succeeded, but no refresh token was returned. You may need to sign in again when this session expires.');
|
|
1078
|
+
}
|
|
1079
|
+
await setEnvOauthSession(options.envName, {
|
|
1080
|
+
type: 'oauth',
|
|
1081
|
+
accessToken: tokenResponse.access_token,
|
|
1082
|
+
refreshToken: tokenResponse.refresh_token,
|
|
1083
|
+
expiresAt: calculateExpiresAt(tokenResponse.expires_in),
|
|
1084
|
+
scope: tokenResponse.scope || DEFAULT_OAUTH_SCOPE,
|
|
1085
|
+
issuer: options.metadata.issuer,
|
|
1086
|
+
clientId: registration.clientId,
|
|
1087
|
+
resource,
|
|
1088
|
+
}, { scope: options.scope });
|
|
880
1089
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
});
|
|
1090
|
+
finally {
|
|
1091
|
+
await cleanupBrowserOpenTarget?.().catch(() => undefined);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
async function authenticateEnvWithOauthBrowser(options) {
|
|
887
1095
|
const state = encodeBase64Url(crypto.randomBytes(16));
|
|
888
1096
|
const { codeVerifier, codeChallenge } = buildPkcePair();
|
|
889
1097
|
const callback = await createLoopbackServer(state);
|
|
890
|
-
const resource = getOauthResource(metadata.issuer);
|
|
1098
|
+
const resource = getOauthResource(options.metadata.issuer);
|
|
891
1099
|
let cleanupBrowserOpenTarget;
|
|
892
1100
|
try {
|
|
893
1101
|
printVerbose(`OAuth callback listener ready at ${callback.redirectUri}`);
|
|
894
|
-
updateTask(`Preparing secure browser sign-in for "${envName}"...`);
|
|
895
|
-
const registration = await registerOauthClient(metadata, callback.redirectUri, {
|
|
896
|
-
envName,
|
|
897
|
-
baseUrl,
|
|
1102
|
+
updateTask(`Preparing secure browser sign-in for "${options.envName}"...`);
|
|
1103
|
+
const registration = await registerOauthClient(options.metadata, callback.redirectUri, {
|
|
1104
|
+
envName: options.envName,
|
|
1105
|
+
baseUrl: options.baseUrl,
|
|
898
1106
|
});
|
|
899
|
-
const authorizationUrl = new URL(metadata.authorization_endpoint);
|
|
1107
|
+
const authorizationUrl = new URL(options.metadata.authorization_endpoint);
|
|
900
1108
|
authorizationUrl.searchParams.set('response_type', 'code');
|
|
901
1109
|
authorizationUrl.searchParams.set('client_id', registration.clientId);
|
|
902
1110
|
authorizationUrl.searchParams.set('redirect_uri', callback.redirectUri);
|
|
@@ -906,8 +1114,8 @@ export async function authenticateEnvWithOauth(options) {
|
|
|
906
1114
|
authorizationUrl.searchParams.set('code_challenge', codeChallenge);
|
|
907
1115
|
authorizationUrl.searchParams.set('code_challenge_method', 'S256');
|
|
908
1116
|
authorizationUrl.searchParams.set('resource', resource);
|
|
909
|
-
updateTask(`Waiting for you to finish signing in for "${envName}"...`);
|
|
910
|
-
const browser = await
|
|
1117
|
+
updateTask(`Waiting for you to finish signing in for "${options.envName}"...`);
|
|
1118
|
+
const browser = await browserOpener(authorizationUrl.toString());
|
|
911
1119
|
cleanupBrowserOpenTarget = browser.cleanup;
|
|
912
1120
|
if (!browser.opened) {
|
|
913
1121
|
printWarningBlock('We could not open your browser automatically. Open the URL below to continue signing in:');
|
|
@@ -918,7 +1126,7 @@ export async function authenticateEnvWithOauth(options) {
|
|
|
918
1126
|
}
|
|
919
1127
|
printInfo(authorizationUrl.toString());
|
|
920
1128
|
const code = await new Promise((resolve, reject) => {
|
|
921
|
-
const timeout = setTimeout(() => reject(new Error(`OAuth sign-in timed out after 5 minutes. Run \`nb env auth ${envName}\` to try again.`)), OAUTH_LOGIN_TIMEOUT_MS);
|
|
1129
|
+
const timeout = setTimeout(() => reject(new Error(`OAuth sign-in timed out after 5 minutes. Run \`nb env auth ${options.envName}\` to try again.`)), OAUTH_LOGIN_TIMEOUT_MS);
|
|
922
1130
|
timeout.unref?.();
|
|
923
1131
|
callback
|
|
924
1132
|
.waitForCode()
|
|
@@ -931,27 +1139,27 @@ export async function authenticateEnvWithOauth(options) {
|
|
|
931
1139
|
reject(error);
|
|
932
1140
|
});
|
|
933
1141
|
});
|
|
934
|
-
updateTask(`Finishing sign-in for "${envName}"...`);
|
|
1142
|
+
updateTask(`Finishing sign-in for "${options.envName}"...`);
|
|
935
1143
|
const tokenResponse = await exchangeAuthorizationCode({
|
|
936
|
-
metadata,
|
|
1144
|
+
metadata: options.metadata,
|
|
937
1145
|
clientId: registration.clientId,
|
|
938
1146
|
redirectUri: callback.redirectUri,
|
|
939
1147
|
code,
|
|
940
1148
|
codeVerifier,
|
|
941
1149
|
resource,
|
|
942
|
-
envName,
|
|
943
|
-
baseUrl,
|
|
1150
|
+
envName: options.envName,
|
|
1151
|
+
baseUrl: options.baseUrl,
|
|
944
1152
|
});
|
|
945
1153
|
if (!tokenResponse.refresh_token) {
|
|
946
1154
|
printWarning('Sign-in succeeded, but no refresh token was returned. You may need to sign in again when this session expires.');
|
|
947
1155
|
}
|
|
948
|
-
await setEnvOauthSession(envName, {
|
|
1156
|
+
await setEnvOauthSession(options.envName, {
|
|
949
1157
|
type: 'oauth',
|
|
950
1158
|
accessToken: tokenResponse.access_token,
|
|
951
1159
|
refreshToken: tokenResponse.refresh_token,
|
|
952
1160
|
expiresAt: calculateExpiresAt(tokenResponse.expires_in),
|
|
953
1161
|
scope: tokenResponse.scope || DEFAULT_OAUTH_SCOPE,
|
|
954
|
-
issuer: metadata.issuer,
|
|
1162
|
+
issuer: options.metadata.issuer,
|
|
955
1163
|
clientId: registration.clientId,
|
|
956
1164
|
resource,
|
|
957
1165
|
}, { scope: options.scope });
|
|
@@ -961,3 +1169,41 @@ export async function authenticateEnvWithOauth(options) {
|
|
|
961
1169
|
await callback.close().catch(() => undefined);
|
|
962
1170
|
}
|
|
963
1171
|
}
|
|
1172
|
+
export async function authenticateEnvWithOauth(options) {
|
|
1173
|
+
const envName = options.envName ?? (await getCurrentEnvName({ scope: options.scope }));
|
|
1174
|
+
const env = await getEnv(envName, { scope: options.scope });
|
|
1175
|
+
const baseUrl = env?.baseUrl;
|
|
1176
|
+
if (!baseUrl) {
|
|
1177
|
+
throw new Error([
|
|
1178
|
+
env
|
|
1179
|
+
? `Environment "${envName}" does not have an API base URL yet.`
|
|
1180
|
+
: `Environment "${envName}" has not been set up yet.`,
|
|
1181
|
+
env
|
|
1182
|
+
? `Run \`nb env update ${envName} --api-base-url <url>\` to finish setting it up.`
|
|
1183
|
+
: `Run \`nb init --ui --env ${envName}\` first.`,
|
|
1184
|
+
]
|
|
1185
|
+
.filter(Boolean)
|
|
1186
|
+
.join('\n'));
|
|
1187
|
+
}
|
|
1188
|
+
printVerbose(`Starting OAuth sign-in for env "${envName}" using ${baseUrl}`);
|
|
1189
|
+
updateTask(`Checking sign-in settings for "${envName}"...`);
|
|
1190
|
+
const metadata = await fetchOauthServerMetadata(baseUrl, {
|
|
1191
|
+
envName,
|
|
1192
|
+
onRetry: (message) => updateTask(message),
|
|
1193
|
+
});
|
|
1194
|
+
if (metadata.device_authorization_endpoint) {
|
|
1195
|
+
await authenticateEnvWithOauthDevice({
|
|
1196
|
+
envName,
|
|
1197
|
+
baseUrl,
|
|
1198
|
+
metadata,
|
|
1199
|
+
scope: options.scope,
|
|
1200
|
+
});
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
await authenticateEnvWithOauthBrowser({
|
|
1204
|
+
envName,
|
|
1205
|
+
baseUrl,
|
|
1206
|
+
metadata,
|
|
1207
|
+
scope: options.scope,
|
|
1208
|
+
});
|
|
1209
|
+
}
|
package/dist/lib/env-config.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
|
+
import { normalizeEnvProxyConfig } from './env-proxy-config.js';
|
|
9
10
|
import { resolveAppPublicPath } from './app-public-path.js';
|
|
10
11
|
const STRING_ENV_CONFIG_KEYS = [
|
|
11
12
|
'source',
|
|
@@ -110,5 +111,9 @@ export function buildStoredEnvConfig(input) {
|
|
|
110
111
|
if ((authType === 'basic' || authType === 'token') && accessToken) {
|
|
111
112
|
envConfig.accessToken = accessToken;
|
|
112
113
|
}
|
|
114
|
+
const proxy = normalizeEnvProxyConfig(input.proxy);
|
|
115
|
+
if (proxy) {
|
|
116
|
+
envConfig.proxy = proxy;
|
|
117
|
+
}
|
|
113
118
|
return envConfig;
|
|
114
119
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
function normalizeOptionalString(value) {
|
|
10
|
+
const normalized = String(value ?? '').trim();
|
|
11
|
+
return normalized || undefined;
|
|
12
|
+
}
|
|
13
|
+
function normalizeOptionalPort(value) {
|
|
14
|
+
const normalized = normalizeOptionalString(value);
|
|
15
|
+
if (!normalized || !/^\d+$/.test(normalized)) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const port = Number(normalized);
|
|
19
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return port;
|
|
23
|
+
}
|
|
24
|
+
export function normalizeEnvProxyProviderConfig(value) {
|
|
25
|
+
if (!value || typeof value !== 'object') {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return { ...value };
|
|
29
|
+
}
|
|
30
|
+
export function normalizeEnvProxyConfig(value) {
|
|
31
|
+
if (!value || typeof value !== 'object') {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const proxy = value;
|
|
35
|
+
const host = normalizeOptionalString(proxy.host);
|
|
36
|
+
const port = normalizeOptionalPort(proxy.port);
|
|
37
|
+
const nginx = normalizeEnvProxyProviderConfig(proxy.nginx);
|
|
38
|
+
const caddy = normalizeEnvProxyProviderConfig(proxy.caddy);
|
|
39
|
+
if (!host && port === undefined && !nginx && !caddy) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
...(host ? { host } : {}),
|
|
44
|
+
...(port !== undefined ? { port } : {}),
|
|
45
|
+
...(nginx ? { nginx } : {}),
|
|
46
|
+
...(caddy ? { caddy } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|