Skip to content

Commit 615224e

Browse files
feat: support for external alternative origins (#1216)
* feat: support for external alternative origins * feat: show external alternative origins * fix: support empty alternative origins * feat: load empty alternative origins * chore: fmt
1 parent b1f90d0 commit 615224e

File tree

9 files changed

+148
-13
lines changed

9 files changed

+148
-13
lines changed

src/frontend/src/lib/components/auth/AuthConfig.svelte

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
let derivationOrigin = $derived(
3838
fromNullishNullable(fromNullishNullable(config?.internet_identity)?.derivation_origin)
3939
);
40+
let externalAlternativeOrigins = $derived(
41+
fromNullishNullable(
42+
fromNullishNullable(config?.internet_identity)?.external_alternative_origins
43+
)
44+
);
4045
4146
const loadConfig = async () => {
4247
const result = await getAuthConfig({
@@ -91,7 +96,7 @@
9196
<div class="card-container with-title">
9297
<span class="title">{$i18n.core.config}</span>
9398

94-
<div class="columns-3 fit-column-1">
99+
<div class="content">
95100
<div>
96101
{#if supportConfig}
97102
<div in:fade>
@@ -107,6 +112,18 @@
107112
{/if}
108113
</Value>
109114
</div>
115+
116+
{#if nonNullish(externalAlternativeOrigins)}
117+
<div in:fade>
118+
<Value>
119+
{#snippet label()}
120+
{$i18n.authentication.external_alternative_origins}
121+
{/snippet}
122+
123+
<p>{externalAlternativeOrigins.join(',')}</p>
124+
</Value>
125+
</div>
126+
{/if}
110127
{/if}
111128

112129
{#if supportSettings}
@@ -135,7 +152,13 @@
135152
{/if}
136153

137154
<style lang="scss">
155+
@use '../../styles/mixins/text';
156+
138157
.card-container {
139158
min-height: 166px;
140159
}
160+
161+
p {
162+
@include text.truncate;
163+
}
141164
</style>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<script lang="ts">
2+
import { fromNullable, fromNullishNullable, isEmptyString } from '@dfinity/utils';
3+
import { onMount, type SvelteComponent } from 'svelte';
4+
import type { AuthenticationConfig } from '$declarations/satellite/satellite.did';
5+
import Collapsible from '$lib/components/ui/Collapsible.svelte';
6+
import Input from '$lib/components/ui/Input.svelte';
7+
import Value from '$lib/components/ui/Value.svelte';
8+
import { i18n } from '$lib/stores/i18n.store';
9+
10+
interface Props {
11+
config: AuthenticationConfig | undefined;
12+
externalAlternativeOrigins: string;
13+
}
14+
15+
let { config, externalAlternativeOrigins = $bindable('') }: Props = $props();
16+
17+
let externalAlternativeOriginsInput = $state(
18+
(
19+
fromNullable(
20+
fromNullishNullable(config?.internet_identity)?.external_alternative_origins ?? []
21+
) ?? []
22+
).join(',')
23+
);
24+
25+
$effect(() => {
26+
externalAlternativeOrigins = externalAlternativeOriginsInput;
27+
});
28+
29+
let collapsibleRef: (SvelteComponent & { open: () => void; close: () => void }) | undefined =
30+
$state(undefined);
31+
32+
onMount(() => {
33+
if (isEmptyString(externalAlternativeOriginsInput)) {
34+
return;
35+
}
36+
37+
collapsibleRef?.open();
38+
});
39+
</script>
40+
41+
<Collapsible bind:this={collapsibleRef}>
42+
<svelte:fragment slot="header">{$i18n.core.advanced_options}</svelte:fragment>
43+
44+
<div>
45+
<Value>
46+
{#snippet label()}
47+
{$i18n.authentication.external_alternative_origins}
48+
{/snippet}
49+
50+
<Input
51+
inputType="text"
52+
name="destination"
53+
required={false}
54+
placeholder={$i18n.authentication.external_alternative_origins_placeholder}
55+
bind:value={externalAlternativeOriginsInput}
56+
/>
57+
</Value>
58+
</div>
59+
</Collapsible>

src/frontend/src/lib/components/auth/AuthConfigForm.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { fade } from 'svelte/transition';
44
import type { Satellite } from '$declarations/mission_control/mission_control.did';
55
import type { AuthenticationConfig, Rule } from '$declarations/satellite/satellite.did';
6+
import AuthConfigAdvancedOptions from '$lib/components/auth/AuthConfigAdvancedOptions.svelte';
67
import Input from '$lib/components/ui/Input.svelte';
78
import Value from '$lib/components/ui/Value.svelte';
89
import Warning from '$lib/components/ui/Warning.svelte';
@@ -17,6 +18,7 @@
1718
satellite: Satellite;
1819
rule: Rule | undefined;
1920
maxTokens: number | undefined;
21+
externalAlternativeOrigins: string;
2022
onsubmit: ($event: SubmitEvent) => Promise<void>;
2123
}
2224
@@ -26,7 +28,8 @@
2628
config,
2729
satellite,
2830
rule,
29-
maxTokens = $bindable(undefined)
31+
maxTokens = $bindable(undefined),
32+
externalAlternativeOrigins = $bindable('')
3033
}: Props = $props();
3134
3235
let satelliteUrl: URL | null = $derived(
@@ -121,6 +124,8 @@
121124
</div>
122125
{/if}
123126

127+
<AuthConfigAdvancedOptions {config} bind:externalAlternativeOrigins />
128+
124129
{#if warnDerivationOrigin}
125130
<div class="warn" in:fade>
126131
<Warning>{$i18n.authentication.main_domain_warn}</Warning>

src/frontend/src/lib/components/modals/AuthConfigModal.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
3131
let selectedDerivationOrigin = $state<URL | undefined>(undefined);
3232
33+
let externalAlternativeOrigins = $state('');
34+
3335
let step: 'init' | 'in_progress' | 'ready' | 'error' = $state('init');
3436
3537
const handleSubmit = async ($event: SubmitEvent) => {
@@ -44,6 +46,7 @@
4446
maxTokens,
4547
rule,
4648
derivationOrigin: selectedDerivationOrigin,
49+
externalAlternativeOrigins,
4750
config
4851
});
4952
@@ -78,6 +81,7 @@
7881
onsubmit={handleSubmit}
7982
bind:maxTokens
8083
bind:selectedDerivationOrigin
84+
bind:externalAlternativeOrigins
8185
/>
8286
{/if}
8387
</Modal>

src/frontend/src/lib/i18n/en.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,9 @@
313313
"main_domain": "Main domain (\"derivation origin\")",
314314
"not_configured": "Not configured",
315315
"edit_configuration": "Update the configuration of your authentication using the form below.",
316-
"main_domain_warn": "You are about to change your main domain. Please ensure this is the desired action, as all existing users will no longer be recognized by your app."
316+
"main_domain_warn": "You are about to change your main domain. Please ensure this is the desired action, as all existing users will no longer be recognized by your app.",
317+
"external_alternative_origins": "External alternative origins",
318+
"external_alternative_origins_placeholder": "A comma-separated list of external domains to allow for derivation"
317319
},
318320
"datastore": {
319321
"title": "Datastore",
@@ -544,7 +546,8 @@
544546
"auth_settings_not_loaded": "The authentication's settings are not yet loaded.",
545547
"auth_rate_config_max_tokens": "A maximal number of updates per minute is required.",
546548
"auth_rate_config_update": "Unexpected error(s) while trying to update your authentication settings.",
547-
"auth_domain_config": "Unexpected error(s) while trying to update your authentication configuration",
549+
"auth_domain_config": "Unexpected error(s) while trying to update your authentication configuration.",
550+
"auth_external_alternative_origins": "External alternative origins are malformed. Please provide a valid domain without a protocol.",
548551
"mission_control_not_loaded": "Your mission control is not yet loaded.",
549552
"mission_control_settings_not_loaded": "The settings for the mission control are not yet loaded.",
550553
"mission_control_user_data_not_loaded": "Your data (metadata, config, etc) for the mission control are not yet loaded.",

src/frontend/src/lib/i18n/zh-cn.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,9 @@
313313
"main_domain": "主域名(\"derivation origin\")",
314314
"not_configured": "未配置",
315315
"edit_configuration": "用如下表格更新你的认证配置.",
316-
"main_domain_warn": "你将要修改主域名. 请再次确认, 现有所有用户将无法识别由于修改."
316+
"main_domain_warn": "你将要修改主域名. 请再次确认, 现有所有用户将无法识别由于修改.",
317+
"external_alternative_origins": "External alternative origins",
318+
"external_alternative_origins_placeholder": "A comma-separated list of external domains to allow for derivation"
317319
},
318320
"datastore": {
319321
"title": "数据池",
@@ -544,7 +546,8 @@
544546
"auth_settings_not_loaded": "认证设置未被加载.",
545547
"auth_rate_config_max_tokens": "每分钟最多更新操作数量是必须的.",
546548
"auth_rate_config_update": "更改认证配置出现异常错误.",
547-
"auth_domain_config": "更新认证配置是出现异常错误",
549+
"auth_domain_config": "更新认证配置是出现异常错误.",
550+
"auth_external_alternative_origins": "External alternative origins are malformed. Please provide a valid domain without a protocol.",
548551
"mission_control_not_loaded": "Your mission control is not yet loaded.",
549552
"mission_control_settings_not_loaded": "The settings for the mission control are not yet loaded.",
550553
"mission_control_user_data_not_loaded": "Your data (metadata, config, etc) for the mission control are not yet loaded.",

src/frontend/src/lib/services/auth.config.services.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { toasts } from '$lib/stores/toasts.store';
1313
import type { OptionIdentity } from '$lib/types/itentity';
1414
import type { Option } from '$lib/types/utils';
1515
import {
16+
assertExternalAlternativeOrigins,
1617
buildDeleteAuthenticationConfig,
1718
buildSetAuthenticationConfig
1819
} from '$lib/utils/auth.config.utils';
@@ -22,6 +23,7 @@ import {
2223
fromNullishNullable,
2324
isNullish,
2425
nonNullish,
26+
notEmptyString,
2527
toNullable
2628
} from '@dfinity/utils';
2729
import { compare } from 'semver';
@@ -32,6 +34,7 @@ interface UpdateAuthConfigParams {
3234
rule: Rule | undefined;
3335
config: AuthenticationConfig | undefined;
3436
maxTokens: number | undefined;
37+
externalAlternativeOrigins: string;
3538
derivationOrigin: Option<URL>;
3639
identity: OptionIdentity;
3740
}
@@ -42,6 +45,7 @@ export const updateAuthConfig = async ({
4245
config,
4346
maxTokens,
4447
derivationOrigin,
48+
externalAlternativeOrigins,
4549
identity
4650
}: UpdateAuthConfigParams): Promise<{ success: 'ok' | 'cancelled' | 'error'; err?: unknown }> => {
4751
const labels = get(i18n);
@@ -51,6 +55,16 @@ export const updateAuthConfig = async ({
5155
return { success: 'error' };
5256
}
5357

58+
const externalOrigins = notEmptyString(externalAlternativeOrigins)
59+
? externalAlternativeOrigins.split(',').map((origin) => origin.trim())
60+
: [];
61+
const { valid } = assertExternalAlternativeOrigins(externalOrigins);
62+
63+
if (!valid) {
64+
toasts.error({ text: labels.errors.auth_external_alternative_origins });
65+
return { success: 'error' };
66+
}
67+
5468
const { result: resultRule } = await updateRule({
5569
satellite,
5670
maxTokens,
@@ -65,6 +79,7 @@ export const updateAuthConfig = async ({
6579
const { result: resultConfig } = await updateConfig({
6680
satellite,
6781
derivationOrigin,
82+
externalOrigins,
6883
config,
6984
identity
7085
});
@@ -80,9 +95,10 @@ const updateConfig = async ({
8095
satellite: { satellite_id: satelliteId },
8196
config,
8297
derivationOrigin,
98+
externalOrigins,
8399
identity
84100
}: Pick<UpdateAuthConfigParams, 'config' | 'derivationOrigin' | 'satellite'> &
85-
Required<Pick<UpdateAuthConfigParams, 'identity'>>): Promise<{
101+
Required<Pick<UpdateAuthConfigParams, 'identity'>> & { externalOrigins: string[] }): Promise<{
86102
result: 'skip' | 'success' | 'error';
87103
err?: unknown;
88104
}> => {
@@ -99,7 +115,7 @@ const updateConfig = async ({
99115

100116
const editConfig = nonNullish(derivationOrigin)
101117
? // We use the host in the backend satellite which parse the url with https to generate the /.well-known/ii-alternative-origins
102-
buildSetAuthenticationConfig({ config, domainName: derivationOrigin.host })
118+
buildSetAuthenticationConfig({ config, domainName: derivationOrigin.host, externalOrigins })
103119
: nonNullish(config)
104120
? buildDeleteAuthenticationConfig(config)
105121
: undefined;

src/frontend/src/lib/types/i18n.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,8 @@ interface I18nAuthentication {
323323
not_configured: string;
324324
edit_configuration: string;
325325
main_domain_warn: string;
326+
external_alternative_origins: string;
327+
external_alternative_origins_placeholder: string;
326328
}
327329

328330
interface I18nDatastore {
@@ -561,6 +563,7 @@ interface I18nErrors {
561563
auth_rate_config_max_tokens: string;
562564
auth_rate_config_update: string;
563565
auth_domain_config: string;
566+
auth_external_alternative_origins: string;
564567
mission_control_not_loaded: string;
565568
mission_control_settings_not_loaded: string;
566569
mission_control_user_data_not_loaded: string;

src/frontend/src/lib/utils/auth.config.utils.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@ import { fromNullable, isNullish, nonNullish, toNullable } from '@dfinity/utils'
33

44
export const buildSetAuthenticationConfig = ({
55
config,
6-
domainName
6+
domainName,
7+
externalOrigins
78
}: {
89
config: AuthenticationConfig | undefined;
910
domainName: string;
10-
}): AuthenticationConfig =>
11-
isNullish(config)
11+
externalOrigins?: string[];
12+
}): AuthenticationConfig => {
13+
const external_alternative_origins: [] | [string[]] =
14+
isNullish(externalOrigins) || externalOrigins.length === 0
15+
? toNullable()
16+
: toNullable(externalOrigins);
17+
18+
return isNullish(config)
1219
? {
1320
internet_identity: [
1421
{
1522
derivation_origin: [domainName],
16-
external_alternative_origins: toNullable()
23+
external_alternative_origins
1724
}
1825
]
1926
}
@@ -24,11 +31,12 @@ export const buildSetAuthenticationConfig = ({
2431
{
2532
...fromNullable(config.internet_identity),
2633
derivation_origin: [domainName],
27-
external_alternative_origins: toNullable()
34+
external_alternative_origins
2835
}
2936
]
3037
})
3138
};
39+
};
3240

3341
export const buildDeleteAuthenticationConfig = (
3442
config: AuthenticationConfig
@@ -44,3 +52,14 @@ export const buildDeleteAuthenticationConfig = (
4452
]
4553
})
4654
});
55+
56+
export const assertExternalAlternativeOrigins = (externalOrigins: string[]): { valid: boolean } => {
57+
const invalidUrl = externalOrigins.find((origin) => URL.parse(`https://${origin}`) === null);
58+
59+
const containsProtocol = (origin: string): boolean => /^[a-zA-Z]+:\/\//.test(origin);
60+
const invalidProtocol = externalOrigins.find(containsProtocol);
61+
62+
return {
63+
valid: externalOrigins.length === 0 || (isNullish(invalidUrl) && isNullish(invalidProtocol))
64+
};
65+
};

0 commit comments

Comments
 (0)