mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 16:03:47 +00:00
offer known Google scopes as checkboxes in the oauth connect dialog (#10945)
* feat: offer known Google scopes as checkboxes in the oauth connect dialog Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kv72vdCDggCZEjJSmNCwnX * fix: keep custom oauth scope rows apart from checked options while typing Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kv72vdCDggCZEjJSmNCwnX * fix: drop the rust scope_options field and render checkboxes from the ticked set Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kv72vdCDggCZEjJSmNCwnX * fix: keep ticked oauth scope options independent of free-text rows Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kv72vdCDggCZEjJSmNCwnX * fix: toggle oauth scope checkboxes from component state, not the reverted input Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kv72vdCDggCZEjJSmNCwnX * fix: keep the legacy gforms default scope so pre-migration accounts still refresh Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kv72vdCDggCZEjJSmNCwnX --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
fda7b3f086
commit
d232d57f0d
@@ -29,6 +29,10 @@
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"scopes": ["https://www.googleapis.com/auth/spreadsheets"],
|
||||
"scope_options": [
|
||||
"https://www.googleapis.com/auth/spreadsheets",
|
||||
"https://www.googleapis.com/auth/spreadsheets.readonly"
|
||||
],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"prompt": "consent"
|
||||
@@ -38,6 +42,11 @@
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"scopes": ["https://www.googleapis.com/auth/drive"],
|
||||
"scope_options": [
|
||||
"https://www.googleapis.com/auth/drive.file",
|
||||
"https://www.googleapis.com/auth/drive.readonly",
|
||||
"https://www.googleapis.com/auth/drive"
|
||||
],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"prompt": "consent"
|
||||
@@ -47,6 +56,13 @@
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"scopes": ["https://www.googleapis.com/auth/gmail.send"],
|
||||
"scope_options": [
|
||||
"https://www.googleapis.com/auth/gmail.send",
|
||||
"https://www.googleapis.com/auth/gmail.readonly",
|
||||
"https://www.googleapis.com/auth/gmail.compose",
|
||||
"https://www.googleapis.com/auth/gmail.modify",
|
||||
"https://www.googleapis.com/auth/gmail.labels"
|
||||
],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"prompt": "consent"
|
||||
@@ -56,6 +72,12 @@
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"scopes": ["https://www.googleapis.com/auth/calendar.events"],
|
||||
"scope_options": [
|
||||
"https://www.googleapis.com/auth/calendar.events",
|
||||
"https://www.googleapis.com/auth/calendar.events.readonly",
|
||||
"https://www.googleapis.com/auth/calendar.readonly",
|
||||
"https://www.googleapis.com/auth/calendar"
|
||||
],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"prompt": "consent"
|
||||
@@ -65,6 +87,12 @@
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"scopes": ["https://www.googleapis.com/auth/forms"],
|
||||
"scope_options": [
|
||||
"https://www.googleapis.com/auth/forms",
|
||||
"https://www.googleapis.com/auth/forms.body",
|
||||
"https://www.googleapis.com/auth/forms.body.readonly",
|
||||
"https://www.googleapis.com/auth/forms.responses.readonly"
|
||||
],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"prompt": "consent"
|
||||
@@ -74,6 +102,10 @@
|
||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"scopes": ["https://www.googleapis.com/auth/cloud-platform"],
|
||||
"scope_options": [
|
||||
"https://www.googleapis.com/auth/cloud-platform",
|
||||
"https://www.googleapis.com/auth/cloud-platform.read-only"
|
||||
],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"prompt": "consent"
|
||||
@@ -88,6 +120,15 @@
|
||||
"https://www.googleapis.com/auth/admin.directory.user.security",
|
||||
"https://www.googleapis.com/auth/admin.directory.orgunit"
|
||||
],
|
||||
"scope_options": [
|
||||
"https://www.googleapis.com/auth/admin.directory.user",
|
||||
"https://www.googleapis.com/auth/admin.directory.user.readonly",
|
||||
"https://www.googleapis.com/auth/admin.directory.group",
|
||||
"https://www.googleapis.com/auth/admin.directory.group.readonly",
|
||||
"https://www.googleapis.com/auth/admin.directory.orgunit",
|
||||
"https://www.googleapis.com/auth/admin.directory.orgunit.readonly",
|
||||
"https://www.googleapis.com/auth/admin.directory.user.security"
|
||||
],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
"prompt": "consent"
|
||||
|
||||
@@ -88,6 +88,8 @@ pub struct OAuthConfig {
|
||||
#[serde(default = "empty_string")]
|
||||
pub token_url: String,
|
||||
pub userinfo_url: Option<String>,
|
||||
/// The registry JSON may also carry `scope_options`, a frontend-only pick
|
||||
/// list for the connect dialog; it is deliberately not modelled here.
|
||||
pub scopes: Option<Vec<String>>,
|
||||
/// Default scopes for the client-credentials (2-legged) flow. These differ
|
||||
/// from the authorization-code `scopes` for most providers (member/consent
|
||||
|
||||
@@ -1476,7 +1476,7 @@
|
||||
>
|
||||
|
||||
{#if editScopes}
|
||||
<OauthScopes bind:scopes />
|
||||
<OauthScopes bind:scopes options={registryEntry()?.scope_options} />
|
||||
{:else}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each scopes as scope}
|
||||
|
||||
@@ -1,51 +1,114 @@
|
||||
<script lang="ts">
|
||||
import { Button } from './common'
|
||||
import TextInput from './text_input/TextInput.svelte'
|
||||
import Checkbox from './common/checkbox/Checkbox.svelte'
|
||||
import { Minus, Plus } from 'lucide-svelte'
|
||||
|
||||
interface Props {
|
||||
scopes?: string[]
|
||||
/** Scopes the provider is known to accept, offered as checkboxes. Anything
|
||||
* not in this list stays editable as free text below them. */
|
||||
options?: string[]
|
||||
}
|
||||
|
||||
let { scopes = $bindable() }: Props = $props()
|
||||
let { scopes = $bindable(), options = [] }: Props = $props()
|
||||
|
||||
// Ticked options and free-text rows are kept apart from `scopes` (the only
|
||||
// value the parent binds) so a row can pass through an option's exact value
|
||||
// while typing (`…/calendar` on the way to `…/calendar.acls`) without ticking
|
||||
// or unticking anything. `lastWritten` tells a parent-side reset apart from
|
||||
// the echo of our own write.
|
||||
let ticked: string[] = $state([])
|
||||
let custom: string[] = $state([])
|
||||
let lastWritten: string | undefined = undefined
|
||||
|
||||
$effect.pre(() => {
|
||||
if (!scopes) {
|
||||
scopes = []
|
||||
}
|
||||
const json = JSON.stringify([scopes, options])
|
||||
if (json != lastWritten) {
|
||||
lastWritten = json
|
||||
ticked = scopes.filter((v) => options.includes(v))
|
||||
custom = scopes.filter((v) => !options.includes(v))
|
||||
}
|
||||
})
|
||||
|
||||
function write(nextTicked: string[], rows: string[]) {
|
||||
ticked = nextTicked
|
||||
custom = rows
|
||||
scopes = [...nextTicked.filter((o) => !rows.includes(o)), ...rows]
|
||||
lastWritten = JSON.stringify([scopes, options])
|
||||
}
|
||||
|
||||
// Ticking an option absorbs a free-text row holding the same value. The
|
||||
// target state comes from `ticked`, not the DOM: `Checkbox` re-asserts its
|
||||
// `checked` prop on every click, so the input already reads the old value
|
||||
// again by the time `change` fires.
|
||||
function toggle(option: string, on: boolean) {
|
||||
const rest = ticked.filter((o) => o != option)
|
||||
write(on ? [...rest, option] : rest, on ? custom.filter((r) => r != option) : custom)
|
||||
}
|
||||
|
||||
function setRow(i: number, value: string) {
|
||||
const rows = [...custom]
|
||||
rows[i] = value
|
||||
write(ticked, rows)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if scopes && Array.isArray(scopes)}
|
||||
{#each scopes as v, i}
|
||||
<div class="flex flex-row max-w-md mb-2">
|
||||
<input type="text" bind:value={scopes[i]} />
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
btnClasses="mx-6"
|
||||
on:click={() => {
|
||||
scopes = scopes?.filter((el) => el != v)
|
||||
}}
|
||||
startIcon={{ icon: Minus }}
|
||||
iconOnly
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{#if options.length > 0}
|
||||
<div class="flex flex-col gap-1 mb-2">
|
||||
{#each options as option (option)}
|
||||
<label class="flex items-center gap-2 text-xs">
|
||||
<Checkbox
|
||||
checked={ticked.includes(option)}
|
||||
onChange={() => toggle(option, !ticked.includes(option))}
|
||||
/>
|
||||
<span class="font-mono break-all">{option}</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
<span class="text-xs text-secondary">Custom scopes</span>
|
||||
{/if}
|
||||
|
||||
{#each custom as v, i (i)}
|
||||
<div class="flex flex-row max-w-md mb-2">
|
||||
<TextInput
|
||||
value={v}
|
||||
size="sm"
|
||||
inputProps={{ oninput: (e) => setRow(i, e.currentTarget.value) }}
|
||||
/>
|
||||
<Button
|
||||
variant="default"
|
||||
unifiedSize="sm"
|
||||
btnClasses="mx-6"
|
||||
onclick={() => {
|
||||
write(
|
||||
ticked,
|
||||
custom.filter((_, j) => j != i)
|
||||
)
|
||||
}}
|
||||
startIcon={{ icon: Minus }}
|
||||
iconOnly
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="flex items-center mt-1">
|
||||
<Button
|
||||
variant="default"
|
||||
hover="yo"
|
||||
size="xs"
|
||||
unifiedSize="sm"
|
||||
startIcon={{ icon: Plus }}
|
||||
on:click={() => {
|
||||
scopes = (scopes ?? []).concat('')
|
||||
onclick={() => {
|
||||
write(ticked, [...custom, ''])
|
||||
}}
|
||||
>
|
||||
Add item
|
||||
</Button>
|
||||
<span class="ml-2 text-xs text-primary font-normal">
|
||||
({(scopes ?? []).length} item{(scopes ?? []).length > 1 ? 's' : ''})
|
||||
</span>
|
||||
{#if custom.length > 0}
|
||||
<span class="ml-2 text-xs text-primary font-normal">
|
||||
({custom.length} item{custom.length > 1 ? 's' : ''})
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user