mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 00:02:03 +00:00
feat: add custom oauth support (#2336)
* custom oauth * all * revert ee changes * fix frontend
This commit is contained in:
@@ -182,7 +182,7 @@ pub fn build_oauth_clients(
|
||||
.as_ref()
|
||||
.map(|c| (x.0.clone(), (x.1, c.clone())))
|
||||
}))
|
||||
.map(|(k, (client_params, config))| {
|
||||
.filter_map(|(k, (client_params, config))| {
|
||||
let named_client = build_basic_client(
|
||||
k.clone(),
|
||||
config.clone(),
|
||||
@@ -191,17 +191,25 @@ pub fn build_oauth_clients(
|
||||
base_url,
|
||||
None,
|
||||
);
|
||||
(
|
||||
named_client.0,
|
||||
ClientWithScopes {
|
||||
client: named_client.1,
|
||||
scopes: config.scopes.unwrap_or(vec![]),
|
||||
extra_params: config.extra_params,
|
||||
extra_params_callback: config.extra_params_callback,
|
||||
allowed_domains: client_params.allowed_domains.clone(),
|
||||
userinfo_url: config.userinfo_url,
|
||||
},
|
||||
)
|
||||
named_client
|
||||
.map(|named_client| {
|
||||
(
|
||||
named_client.0,
|
||||
ClientWithScopes {
|
||||
client: named_client.1,
|
||||
scopes: config.scopes.unwrap_or(vec![]),
|
||||
extra_params: config.extra_params,
|
||||
extra_params_callback: config.extra_params_callback,
|
||||
allowed_domains: client_params.allowed_domains.clone(),
|
||||
userinfo_url: config.userinfo_url,
|
||||
},
|
||||
)
|
||||
})
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error building oauth client {k}: {e}");
|
||||
e
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -213,7 +221,7 @@ pub fn build_oauth_clients(
|
||||
.as_ref()
|
||||
.map(|c| (x.0.clone(), (x.1, c.clone())))
|
||||
}))
|
||||
.map(|(k, (client_params, config))| {
|
||||
.filter_map(|(k, (client_params, config))| {
|
||||
let named_client = build_basic_client(
|
||||
k.clone(),
|
||||
config.clone(),
|
||||
@@ -226,43 +234,61 @@ pub fn build_oauth_clients(
|
||||
None
|
||||
},
|
||||
);
|
||||
(
|
||||
named_client.0,
|
||||
ClientWithScopes {
|
||||
client: named_client.1,
|
||||
scopes: config.scopes.unwrap_or(vec![]),
|
||||
extra_params: config.extra_params,
|
||||
extra_params_callback: config.extra_params_callback,
|
||||
allowed_domains: None,
|
||||
userinfo_url: None,
|
||||
},
|
||||
)
|
||||
named_client
|
||||
.map(|named_client| {
|
||||
(
|
||||
named_client.0,
|
||||
ClientWithScopes {
|
||||
client: named_client.1,
|
||||
scopes: config.scopes.unwrap_or(vec![]),
|
||||
extra_params: config.extra_params,
|
||||
extra_params_callback: config.extra_params_callback,
|
||||
allowed_domains: None,
|
||||
userinfo_url: None,
|
||||
},
|
||||
)
|
||||
})
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error building oauth client {k}: {e}");
|
||||
e
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let slack = oauths.get("slack").map(|v| {
|
||||
build_basic_client(
|
||||
"slack".to_string(),
|
||||
OAuthConfig {
|
||||
auth_url: "https://slack.com/oauth/authorize".to_string(),
|
||||
token_url: "https://slack.com/api/oauth.access".to_string(),
|
||||
userinfo_url: None,
|
||||
scopes: None,
|
||||
extra_params: None,
|
||||
extra_params_callback: None,
|
||||
req_body_auth: None,
|
||||
},
|
||||
v.clone(),
|
||||
false,
|
||||
base_url,
|
||||
Some(format!("{base_url}/oauth/callback_slack")),
|
||||
)
|
||||
.1
|
||||
});
|
||||
|
||||
Ok(AllClients { logins, connects, slack })
|
||||
let slack = oauths
|
||||
.get("slack")
|
||||
.map(|v| {
|
||||
build_basic_client(
|
||||
"slack".to_string(),
|
||||
OAuthConfig {
|
||||
auth_url: "https://slack.com/oauth/authorize".to_string(),
|
||||
token_url: "https://slack.com/api/oauth.access".to_string(),
|
||||
userinfo_url: None,
|
||||
scopes: None,
|
||||
extra_params: None,
|
||||
extra_params_callback: None,
|
||||
req_body_auth: None,
|
||||
},
|
||||
v.clone(),
|
||||
false,
|
||||
base_url,
|
||||
Some(format!("{base_url}/oauth/callback_slack")),
|
||||
)
|
||||
.map(|x| x.1)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error building oauth slack client: {e}");
|
||||
e
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.flatten();
|
||||
let all_clients = AllClients { logins, connects, slack };
|
||||
tracing::info!("Final oauth config: {all_clients:#?}");
|
||||
Ok(all_clients)
|
||||
}
|
||||
|
||||
use anyhow::anyhow;
|
||||
pub fn build_basic_client(
|
||||
name: String,
|
||||
config: OAuthConfig,
|
||||
@@ -270,9 +296,11 @@ pub fn build_basic_client(
|
||||
login: bool,
|
||||
base_url: &str,
|
||||
override_callback: Option<String>,
|
||||
) -> (String, OClient) {
|
||||
let auth_url = Url::parse(&config.auth_url).expect("Invalid authorization endpoint URL");
|
||||
let token_url = Url::parse(&config.token_url).expect("Invalid token endpoint URL");
|
||||
) -> error::Result<(String, OClient)> {
|
||||
let auth_url = Url::parse(&config.auth_url)
|
||||
.map_err(|e| anyhow!("Invalid authorization endpoint URL: {e}"))?;
|
||||
let token_url =
|
||||
Url::parse(&config.token_url).map_err(|e| anyhow!("Invalid token endpoint URL: {e}"))?;
|
||||
|
||||
let redirect_url = if login {
|
||||
format!("{base_url}/user/login_callback/{name}")
|
||||
@@ -287,9 +315,12 @@ pub fn build_basic_client(
|
||||
client.set_auth_type(AuthType::RequestBody);
|
||||
}
|
||||
client.set_client_secret(client_params.secret.clone());
|
||||
client.set_redirect_url(Url::parse(&redirect_url).expect("Invalid redirect URL"));
|
||||
client.set_redirect_url(
|
||||
Url::parse(&redirect_url).map_err(|e| anyhow!("Invalid redirect URL: {e}"))?,
|
||||
);
|
||||
|
||||
// Set up the config for the Github OAuth2 process.
|
||||
(name.to_string(), client)
|
||||
Ok((name.to_string(), client))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
|
||||
@@ -57,12 +57,10 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { oauthStore, workspaceStore } from '$lib/stores'
|
||||
import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import IconedResourceType from './IconedResourceType.svelte'
|
||||
import { OauthService, ResourceService, VariableService, type TokenResponse } from '$lib/gen'
|
||||
import { emptyString, truncateRev } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import Path from './Path.svelte'
|
||||
import { Button, Drawer, Skeleton } from './common'
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
@@ -71,10 +69,11 @@
|
||||
import autosize from 'svelte-autosize'
|
||||
import WhitelistIp from './WhitelistIp.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import OauthScopes from './OauthScopes.svelte'
|
||||
|
||||
export let newPageOAuth = false
|
||||
|
||||
const nativeLanguagesCategory = ['postgresql', 'mysql', 'bigquery', 'snowflake', 'graphql'];
|
||||
const nativeLanguagesCategory = ['postgresql', 'mysql', 'bigquery', 'snowflake', 'graphql']
|
||||
|
||||
let filter = ''
|
||||
let manual = false
|
||||
@@ -195,14 +194,14 @@
|
||||
linkedSecret: undefined
|
||||
}
|
||||
])
|
||||
const filteredNativeLanguages = filteredConnectsManual?.filter(([key, _]) =>
|
||||
nativeLanguagesCategory.includes(key)
|
||||
);
|
||||
const filteredNativeLanguages = filteredConnectsManual?.filter(([key, _]) =>
|
||||
nativeLanguagesCategory.includes(key)
|
||||
)
|
||||
|
||||
filteredConnectsManual = [
|
||||
...(filteredNativeLanguages ?? []),
|
||||
...(filteredConnectsManual ?? []).filter(([key, _]) => !nativeLanguagesCategory.includes(key))
|
||||
];
|
||||
filteredConnectsManual = [
|
||||
...(filteredNativeLanguages ?? []),
|
||||
...(filteredConnectsManual ?? []).filter(([key, _]) => !nativeLanguagesCategory.includes(key))
|
||||
]
|
||||
}
|
||||
|
||||
async function next() {
|
||||
@@ -397,93 +396,62 @@
|
||||
{#if manual == false && resource_type != ''}
|
||||
<h3>Scopes</h3>
|
||||
{#if !manual && resource_type != ''}
|
||||
{#each scopes as v}
|
||||
<div class="flex flex-row max-w-md mb-2">
|
||||
<input type="text" bind:value={v} />
|
||||
<Button
|
||||
variant="border"
|
||||
color="red"
|
||||
size="xs"
|
||||
btnClasses="mx-6"
|
||||
on:click={() => {
|
||||
scopes = scopes.filter((el) => el != v)
|
||||
}}
|
||||
>
|
||||
<Icon data={faMinus} />
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="flex items-center mt-1">
|
||||
<Button
|
||||
variant="border"
|
||||
color="blue"
|
||||
hover="yo"
|
||||
size="sm"
|
||||
endIcon={{ icon: faPlus }}
|
||||
on:click={() => {
|
||||
scopes = scopes.concat('')
|
||||
}}
|
||||
>
|
||||
Add item
|
||||
</Button>
|
||||
<span class="ml-2 text-sm text-tertiary">
|
||||
({(scopes ?? []).length} item{(scopes ?? []).length > 1 ? 's' : ''})
|
||||
</span>
|
||||
</div>
|
||||
<OauthScopes bind:scopes />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<h2 class="mt-8 mb-4">Others</h2>
|
||||
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-x-2 gap-y-1 items-center mb-2">
|
||||
{#if filteredConnectsManual}
|
||||
{#each filteredConnectsManual as [key, _]}
|
||||
{#if nativeLanguagesCategory.includes(key)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={key === resource_type ? 'blue' : 'light'}
|
||||
btnClasses={key === resource_type ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
manual = true;
|
||||
resource_type = key;
|
||||
next();
|
||||
dispatch('click');
|
||||
}}
|
||||
>
|
||||
<IconedResourceType name={key} after={true} width="20px" height="20px" />
|
||||
</Button>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-x-2 gap-y-1 items-center mb-2">
|
||||
{#if filteredConnectsManual}
|
||||
{#each filteredConnectsManual as [key, _]}
|
||||
{#if nativeLanguagesCategory.includes(key)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={key === resource_type ? 'blue' : 'light'}
|
||||
btnClasses={key === resource_type ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
manual = true
|
||||
resource_type = key
|
||||
next()
|
||||
dispatch('click')
|
||||
}}
|
||||
>
|
||||
<IconedResourceType name={key} after={true} width="20px" height="20px" />
|
||||
</Button>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<h2 class="mt-8 mb-4"></h2>
|
||||
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-x-2 gap-y-1 items-center mb-2">
|
||||
{#if filteredConnectsManual}
|
||||
{#each filteredConnectsManual as [key, _]}
|
||||
{#if !nativeLanguagesCategory.includes(key)} <!-- Exclude specific items -->
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={key === resource_type ? 'blue' : 'light'}
|
||||
btnClasses={key === resource_type ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
manual = true;
|
||||
resource_type = key;
|
||||
next();
|
||||
dispatch('click');
|
||||
}}
|
||||
>
|
||||
<IconedResourceType name={key} after={true} width="20px" height="20px" />
|
||||
</Button>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
{#each new Array(9) as _}
|
||||
<Skeleton layout={[[2]]} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<h2 class="mt-8 mb-4" />
|
||||
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-x-2 gap-y-1 items-center mb-2">
|
||||
{#if filteredConnectsManual}
|
||||
{#each filteredConnectsManual as [key, _]}
|
||||
{#if !nativeLanguagesCategory.includes(key)}
|
||||
<!-- Exclude specific items -->
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={key === resource_type ? 'blue' : 'light'}
|
||||
btnClasses={key === resource_type ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
manual = true
|
||||
resource_type = key
|
||||
next()
|
||||
dispatch('click')
|
||||
}}
|
||||
>
|
||||
<IconedResourceType name={key} after={true} width="20px" height="20px" />
|
||||
</Button>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
{#each new Array(9) as _}
|
||||
<Skeleton layout={[[2]]} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{:else if step == 2 && manual}
|
||||
<Path
|
||||
bind:error={pathError}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
import OauthExtraParams from './OauthExtraParams.svelte'
|
||||
import OauthScopes from './OauthScopes.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
export let connect_config = {
|
||||
scopes: [],
|
||||
auth_url: '',
|
||||
token_url: '',
|
||||
req_body_auth: false,
|
||||
extra_params: {},
|
||||
extra_params_callback: {}
|
||||
}
|
||||
|
||||
$: if (!connect_config) {
|
||||
connect_config = {
|
||||
scopes: [],
|
||||
auth_url: '',
|
||||
token_url: '',
|
||||
req_body_auth: false,
|
||||
extra_params: {},
|
||||
extra_params_callback: {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm">Auth URL</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://github.com/login/oauth/authorize"
|
||||
bind:value={connect_config.auth_url}
|
||||
/>
|
||||
</label>
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm">Token URL</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://github.com/login/oauth/access_token"
|
||||
bind:value={connect_config.token_url}
|
||||
/>
|
||||
</label>
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm">Scopes</span>
|
||||
<OauthScopes bind:scopes={connect_config.scopes} />
|
||||
</label>
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm"
|
||||
>Extra Query Args for Authorize Request <Tooltip
|
||||
>Not needed in most cases. Examples of uses: google apis require the 2 extra args
|
||||
"access_type=offline&prompt=consent"</Tooltip
|
||||
></span
|
||||
>
|
||||
<OauthExtraParams bind:extra_params={connect_config.extra_params} />
|
||||
</label>
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm"
|
||||
>Extra Query Args for Token request <Tooltip>Not needed in most cases</Tooltip></span
|
||||
>
|
||||
<OauthExtraParams bind:extra_params={connect_config.extra_params_callback} />
|
||||
</label>
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm"
|
||||
>Payload <Tooltip
|
||||
>Auth is passed in query most commonly. LinkedIn is an example of OAuth using
|
||||
x-www-form-urlencoded
|
||||
</Tooltip></span
|
||||
>
|
||||
<div>
|
||||
<Toggle
|
||||
options={{ left: 'in query args', right: 'in body x-www-form-urlencoded' }}
|
||||
bind:checked={connect_config.req_body_auth}
|
||||
/></div
|
||||
>
|
||||
</label>
|
||||
@@ -14,6 +14,9 @@
|
||||
import KeycloakSetting from './KeycloakSetting.svelte'
|
||||
import Alert from './common/alert/Alert.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { capitalize } from '$lib/utils'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import CustomOauth from './CustomOauth.svelte'
|
||||
|
||||
export const settings: Record<string, Setting[]> = {
|
||||
Core: [
|
||||
@@ -125,7 +128,7 @@
|
||||
}
|
||||
}
|
||||
initialOauths = (await SettingService.getGlobal({ key: 'oauths' })) ?? {}
|
||||
oauths = { ...initialOauths }
|
||||
oauths = JSON.parse(JSON.stringify(initialOauths))
|
||||
initialValues = Object.fromEntries(
|
||||
(
|
||||
await Promise.all(
|
||||
@@ -136,7 +139,7 @@
|
||||
)
|
||||
).flat()
|
||||
)
|
||||
values = { ...initialValues }
|
||||
values = JSON.parse(JSON.stringify(initialValues))
|
||||
if (values['retention_period_secs'] == undefined) {
|
||||
values['retention_period_secs'] = 60 * 60 * 24 * 60
|
||||
}
|
||||
@@ -158,7 +161,7 @@
|
||||
name: 'server',
|
||||
requestBody: newServerConfig
|
||||
})
|
||||
serverConfig = { ...newServerConfig }
|
||||
serverConfig = JSON.parse(JSON.stringify(newServerConfig))
|
||||
}
|
||||
await Promise.all(
|
||||
allSettings
|
||||
@@ -174,7 +177,7 @@
|
||||
await SettingService.setGlobal({ key: x.key, requestBody: { value: values?.[x.key] } })
|
||||
})
|
||||
)
|
||||
initialValues = { ...values }
|
||||
initialValues = JSON.parse(JSON.stringify(initialValues))
|
||||
|
||||
if (!deepEqual(initialOauths, oauths)) {
|
||||
await SettingService.setGlobal({
|
||||
@@ -183,7 +186,7 @@
|
||||
value: oauths
|
||||
}
|
||||
})
|
||||
initialOauths = { ...oauths }
|
||||
initialOauths = JSON.parse(JSON.stringify(oauths))
|
||||
}
|
||||
} else {
|
||||
console.error('Values not loaded')
|
||||
@@ -208,6 +211,23 @@
|
||||
}
|
||||
|
||||
let to: string = ''
|
||||
|
||||
const windmillBuiltins = [
|
||||
'github',
|
||||
'gitlab',
|
||||
'bitbucket',
|
||||
'slack',
|
||||
'gsheets',
|
||||
'gdrive',
|
||||
'gmail',
|
||||
'gcal',
|
||||
'gcloud',
|
||||
'gworkspace',
|
||||
'basecamp',
|
||||
'linkedin'
|
||||
]
|
||||
|
||||
let oauth_name = 'custom'
|
||||
</script>
|
||||
|
||||
<div class="pb-8">
|
||||
@@ -344,6 +364,11 @@
|
||||
<Alert type="warning" title="Limited to 50 SSO users">
|
||||
Without EE, the number of SSO users is limited to 50. SCIM/SAML is available on EE
|
||||
</Alert>
|
||||
<div class="py-1" />
|
||||
<Alert type="info" title="Test on a separate tab">
|
||||
The recommended workflow is to to save your oauth setting and test them directly on the
|
||||
login or resource page
|
||||
</Alert>
|
||||
<div class="flex flex-col gap-2 py-4">
|
||||
<OAuthSetting name="google" bind:value={oauths['google']} />
|
||||
<OAuthSetting name="microsoft" bind:value={oauths['microsoft']} />
|
||||
@@ -354,6 +379,11 @@
|
||||
<KeycloakSetting bind:value={oauths['keycloak']} />
|
||||
</div>
|
||||
<h4 class="py-4">OAuth</h4>
|
||||
<Alert type="info" title="Require a corresponding resource type">
|
||||
After setting an oauth client, make sure that there is a corresponding resource type
|
||||
with the same name with a "token" field in the admins workspace.
|
||||
</Alert>
|
||||
<div class="py-1" />
|
||||
<OAuthSetting login={false} name="slack" bind:value={oauths['slack']} />
|
||||
<div class="py-1" />
|
||||
|
||||
@@ -362,7 +392,7 @@
|
||||
{#if oauths[k]}
|
||||
<div class="flex flex-col gap-2 pb-4">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<label class="text-md font-medium text-gray-700">{k}</label>
|
||||
<label class="text-md font-medium text-primary">{k}</label>
|
||||
<CloseButton
|
||||
on:close={() => {
|
||||
delete oauths[k]
|
||||
@@ -383,6 +413,9 @@
|
||||
bind:value={oauths[k]['secret']}
|
||||
/>
|
||||
</label>
|
||||
{#if !windmillBuiltins.includes(k) && k != 'slack'}
|
||||
<CustomOauth bind:connect_config={oauths[k]['connect_config']} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -390,20 +423,28 @@
|
||||
{/each}
|
||||
|
||||
<div class="flex gap-2">
|
||||
<input type="text" placeholder="slack" bind:value={resourceName} />
|
||||
<select name="oauth_name" id="oauth_name" bind:value={oauth_name}>
|
||||
<option value="custom">Fully Custom (require ee)</option>
|
||||
{#each windmillBuiltins as name}
|
||||
<option value={name}>{capitalize(name)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<input type="text" placeholder="client_id" bind:value={resourceName} />
|
||||
<Button
|
||||
variant="border"
|
||||
color="blue"
|
||||
hover="yo"
|
||||
size="sm"
|
||||
endIcon={{ icon: faPlus }}
|
||||
disabled={resourceName == ''}
|
||||
disabled={(oauth_name == 'custom' && resourceName == '') ||
|
||||
(oauth_name == 'custom' && !$enterpriseLicense)}
|
||||
on:click={() => {
|
||||
oauths[resourceName] = { id: '', secret: '' }
|
||||
let name = oauth_name == 'custom' ? resourceName : oauth_name
|
||||
oauths[name] = { id: '', secret: '' }
|
||||
resourceName = ''
|
||||
}}
|
||||
>
|
||||
Add OAuth client
|
||||
Add OAuth client {oauth_name == 'custom' && !$enterpriseLicense ? '(require ee)' : ''}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium text-gray-700 flex gap-4 items-center"
|
||||
<label class="text-sm font-medium text-primary flex gap-4 items-center"
|
||||
><div class="w-[120px]"><IconedResourceType name={'keycloak'} after={true} /></div><Toggle
|
||||
checked={enabled}
|
||||
on:change={(e) => {
|
||||
|
||||
@@ -9,12 +9,11 @@
|
||||
|
||||
$: enabled = value != undefined
|
||||
|
||||
|
||||
let allowed_domains = value?.['allowed_domains'] ?? ''
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm flex gap-4 items-center font-medium text-gray-700"
|
||||
<label class="text-sm flex gap-4 items-center font-medium text-primary"
|
||||
><div class="w-[120px]"><IconedResourceType {name} after={true} /></div><Toggle
|
||||
checked={enabled}
|
||||
on:change={(e) => {
|
||||
@@ -87,4 +86,3 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { Button } from './common'
|
||||
|
||||
export let extra_params: Record<string, string> = {}
|
||||
|
||||
let extra_params_vec: [string, string][] = Object.entries(extra_params)
|
||||
|
||||
function sync() {
|
||||
extra_params = Object.fromEntries(extra_params_vec)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each extra_params_vec as o}
|
||||
<div class="flex flex-row max-w-md mb-2">
|
||||
<input type="text" on:keyup={sync} bind:value={o[0]} />
|
||||
<input type="text" on:keyup={sync} bind:value={o[1]} />
|
||||
<Button
|
||||
variant="border"
|
||||
color="red"
|
||||
size="xs"
|
||||
btnClasses="mx-6"
|
||||
on:click={() => {
|
||||
extra_params_vec = extra_params_vec.filter((e) => e[0] != o[0])
|
||||
sync()
|
||||
}}
|
||||
>
|
||||
<Icon data={faMinus} />
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="flex items-center mt-1">
|
||||
<Button
|
||||
variant="border"
|
||||
color="blue"
|
||||
hover="yo"
|
||||
size="sm"
|
||||
endIcon={{ icon: faPlus }}
|
||||
on:click={() => {
|
||||
extra_params_vec = extra_params_vec.concat([['key', 'value']])
|
||||
sync()
|
||||
}}
|
||||
>
|
||||
Add item
|
||||
</Button>
|
||||
<span class="ml-2 text-sm text-tertiary">
|
||||
({(extra_params_vec ?? []).length} item{(extra_params_vec ?? []).length > 1 ? 's' : ''})
|
||||
</span>
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { Button } from './common'
|
||||
|
||||
export let scopes: string[] = []
|
||||
</script>
|
||||
|
||||
{#each scopes as v}
|
||||
<div class="flex flex-row max-w-md mb-2">
|
||||
<input type="text" bind:value={v} />
|
||||
<Button
|
||||
variant="border"
|
||||
color="red"
|
||||
size="xs"
|
||||
btnClasses="mx-6"
|
||||
on:click={() => {
|
||||
scopes = scopes.filter((el) => el != v)
|
||||
}}
|
||||
>
|
||||
<Icon data={faMinus} />
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="flex items-center mt-1">
|
||||
<Button
|
||||
variant="border"
|
||||
color="blue"
|
||||
hover="yo"
|
||||
size="sm"
|
||||
endIcon={{ icon: faPlus }}
|
||||
on:click={() => {
|
||||
scopes = scopes.concat('')
|
||||
}}
|
||||
>
|
||||
Add item
|
||||
</Button>
|
||||
<span class="ml-2 text-sm text-tertiary">
|
||||
({(scopes ?? []).length} item{(scopes ?? []).length > 1 ? 's' : ''})
|
||||
</span>
|
||||
</div>
|
||||
@@ -33,7 +33,7 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium text-gray-700 flex gap-4 items-center"
|
||||
<label class="text-sm font-medium text-primary flex gap-4 items-center"
|
||||
><div class="w-[120px]"><IconedResourceType name="okta" after={true} /></div><Toggle
|
||||
checked={enabled}
|
||||
on:change={(e) => {
|
||||
|
||||
@@ -583,7 +583,6 @@ const config = {
|
||||
fontSize: '18px',
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
lineHeight: '1.3',
|
||||
color: theme('colors.gray.600'),
|
||||
[`@media (min-width: ${theme('screens.qhd')})`]: {
|
||||
fontSize: '20px'
|
||||
}
|
||||
@@ -592,7 +591,6 @@ const config = {
|
||||
fontSize: '16px',
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
lineHeight: '1.5',
|
||||
color: theme('colors.gray.600'),
|
||||
[`@media (min-width: ${theme('screens.qhd')})`]: {
|
||||
fontSize: '18px'
|
||||
}
|
||||
@@ -601,7 +599,6 @@ const config = {
|
||||
fontSize: '16px',
|
||||
fontWeight: theme('fontWeight.medium'),
|
||||
lineHeight: '1.5',
|
||||
color: theme('colors.gray.600'),
|
||||
[`@media (min-width: ${theme('screens.qhd')})`]: {
|
||||
fontSize: '18px'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user