mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat: public apps can require login (#3825)
This commit is contained in:
@@ -425,6 +425,8 @@ async fn get_app_by_id(
|
||||
}
|
||||
|
||||
async fn get_public_app_by_secret(
|
||||
OptAuthed(opt_authed): OptAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, secret)): Path<(String, String)>,
|
||||
) -> JsonResult<AppWithLastVersion> {
|
||||
@@ -454,10 +456,32 @@ async fn get_public_app_by_secret(
|
||||
|
||||
let policy = serde_json::from_str::<Policy>(app.policy.0.get()).map_err(to_anyhow)?;
|
||||
|
||||
if !matches!(policy.execution_mode, ExecutionMode::Anonymous) {
|
||||
return Err(Error::NotAuthorized(
|
||||
"App visibility does not allow public access".to_string(),
|
||||
));
|
||||
if matches!(policy.execution_mode, ExecutionMode::Anonymous) {
|
||||
return Ok(Json(app));
|
||||
}
|
||||
|
||||
if opt_authed.is_none() {
|
||||
{
|
||||
return Err(Error::NotAuthorized(
|
||||
"App visibility does not allow public access and you are not logged in".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
let authed = opt_authed.unwrap();
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let is_visible = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM app WHERE id = $1 AND workspace_id = $2)",
|
||||
id,
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
if !is_visible.unwrap_or(false) {
|
||||
return Err(Error::NotAuthorized(
|
||||
"App visibility does not allow public access and you are logged in but you have no read-access to that app".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(app))
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { ResourceService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import Select from './apps/svelte-select/lib/index'
|
||||
import { SELECT_INPUT_DEFAULT_STYLE } from '../defaults'
|
||||
|
||||
import DarkModeObserver from './DarkModeObserver.svelte'
|
||||
import { Button, Drawer, DrawerContent } from './common'
|
||||
import { Plus } from 'lucide-svelte'
|
||||
import type { AppViewerContext } from './apps/types'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -16,6 +17,8 @@
|
||||
export let resourceType: string | undefined = undefined
|
||||
export let disablePortal = false
|
||||
|
||||
const appViewerContext = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let valueSelect =
|
||||
initialValue || value
|
||||
? {
|
||||
@@ -77,7 +80,8 @@
|
||||
<iframe
|
||||
title="App connection"
|
||||
class="w-full h-full"
|
||||
src="/embed_connect?resource_type={resourceType}"
|
||||
src="/embed_connect?resource_type={resourceType}&workspace={appViewerContext?.workspace ??
|
||||
$workspaceStore}"
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import Github from '$lib/components/icons/brands/Github.svelte'
|
||||
import Gitlab from '$lib/components/icons/brands/Gitlab.svelte'
|
||||
import Google from '$lib/components/icons/brands/Google.svelte'
|
||||
import Microsoft from '$lib/components/icons/brands/Microsoft.svelte'
|
||||
import Okta from '$lib/components/icons/brands/Okta.svelte'
|
||||
|
||||
import { OauthService, UserService, WorkspaceService } from '$lib/gen'
|
||||
import { usersWorkspaceStore, workspaceStore, userStore } from '$lib/stores'
|
||||
import { classNames, emptyString, parseQueryParams } from '$lib/utils'
|
||||
import { getUserExt } from '$lib/user'
|
||||
import { Button, Skeleton } from '$lib/components/common'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { refreshSuperadmin } from '$lib/refreshUser'
|
||||
|
||||
export let rd: string | undefined = undefined
|
||||
export let email: string | undefined = undefined
|
||||
export let password: string | undefined = undefined
|
||||
export let error: string | undefined = undefined
|
||||
|
||||
const providers = [
|
||||
{
|
||||
type: 'github',
|
||||
name: 'GitHub',
|
||||
icon: Github
|
||||
},
|
||||
{
|
||||
type: 'gitlab',
|
||||
name: 'GitLab',
|
||||
icon: Gitlab
|
||||
},
|
||||
{
|
||||
type: 'google',
|
||||
name: 'Google',
|
||||
icon: Google
|
||||
},
|
||||
{
|
||||
type: 'microsoft',
|
||||
name: 'Microsoft',
|
||||
icon: Microsoft
|
||||
},
|
||||
{
|
||||
type: 'okta',
|
||||
name: 'Okta',
|
||||
icon: Okta
|
||||
}
|
||||
] as const
|
||||
|
||||
const providersType = providers.map((p) => p.type as string)
|
||||
|
||||
let showPassword = false
|
||||
let logins: string[] | undefined = undefined
|
||||
let saml: string | undefined = undefined
|
||||
|
||||
async function login(): Promise<void> {
|
||||
if (!email || !password) {
|
||||
sendUserToast('Please fill in both email and password', true)
|
||||
return
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
email,
|
||||
password
|
||||
}
|
||||
|
||||
try {
|
||||
await UserService.login({ requestBody })
|
||||
} catch (err) {
|
||||
sendUserToast('Invalid credentials', true)
|
||||
return
|
||||
}
|
||||
|
||||
// Once logged in, we can fetch the workspaces
|
||||
$usersWorkspaceStore = await WorkspaceService.listUserWorkspaces()
|
||||
// trigger a reload of the user
|
||||
if ($workspaceStore) {
|
||||
$userStore = await getUserExt($workspaceStore)
|
||||
}
|
||||
|
||||
// Finally, we check whether the user is a superadmin
|
||||
refreshSuperadmin()
|
||||
redirectUser()
|
||||
}
|
||||
|
||||
async function redirectUser() {
|
||||
const firstTimeCookie =
|
||||
document.cookie.match('(^|;)\\s*first_time\\s*=\\s*([^;]+)')?.pop() || '0'
|
||||
if (Number(firstTimeCookie) > 0 && email === 'admin@windmill.dev') {
|
||||
goto('/user/first-time')
|
||||
return
|
||||
}
|
||||
|
||||
if (rd?.startsWith('http')) {
|
||||
window.location.href = rd
|
||||
return
|
||||
}
|
||||
if ($workspaceStore) {
|
||||
goto(rd ?? '/')
|
||||
} else {
|
||||
let workspaceTarget = parseQueryParams(rd ?? undefined)['workspace']
|
||||
if (rd && workspaceTarget) {
|
||||
$workspaceStore = workspaceTarget
|
||||
goto(rd)
|
||||
return
|
||||
}
|
||||
|
||||
if (!$usersWorkspaceStore) {
|
||||
try {
|
||||
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const allWorkspaces = $usersWorkspaceStore?.workspaces.filter((x) => x.id != 'admins')
|
||||
|
||||
if (allWorkspaces?.length == 1) {
|
||||
workspaceStore.set(allWorkspaces[0].id)
|
||||
$userStore = await getUserExt($workspaceStore!)
|
||||
|
||||
if (!$userStore?.is_super_admin && $userStore?.operator) {
|
||||
let defaultApp = await WorkspaceService.getWorkspaceDefaultApp({
|
||||
workspace: $workspaceStore!
|
||||
})
|
||||
if (!emptyString(defaultApp.default_app_path)) {
|
||||
goto(`/apps/get/${defaultApp.default_app_path}`)
|
||||
} else {
|
||||
goto(rd ?? '/')
|
||||
}
|
||||
} else {
|
||||
goto(rd ?? '/')
|
||||
}
|
||||
} else if (rd?.startsWith('/user/workspaces')) {
|
||||
goto(rd)
|
||||
} else if (rd == '/#user-settings') {
|
||||
goto(`/user/workspaces#user-settings`)
|
||||
} else {
|
||||
goto(`/user/workspaces${rd ? `?rd=${encodeURIComponent(rd)}` : ''}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLogins() {
|
||||
const allLogins = await OauthService.listOauthLogins()
|
||||
logins = allLogins.oauth
|
||||
saml = allLogins.saml
|
||||
|
||||
showPassword = (logins.length == 0 && !saml) || (email != undefined && email.length > 0)
|
||||
}
|
||||
|
||||
loadLogins()
|
||||
|
||||
function handleKeyUp(event: KeyboardEvent) {
|
||||
const key = event.key
|
||||
|
||||
if (key === 'Enter') {
|
||||
event.preventDefault()
|
||||
login()
|
||||
}
|
||||
}
|
||||
|
||||
function storeRedirect(provider: string) {
|
||||
if (rd) {
|
||||
try {
|
||||
localStorage.setItem('rd', rd)
|
||||
} catch (e) {
|
||||
console.error('Could not persist redirection to local storage', e)
|
||||
}
|
||||
}
|
||||
window.location.href = window.location.origin + '/api/oauth/login/' + provider
|
||||
}
|
||||
|
||||
$: error && sendUserToast(error, true)
|
||||
</script>
|
||||
|
||||
<div class="bg-surface px-4 py-8 shadow md:border sm:rounded-lg sm:px-10">
|
||||
<div class="grid {logins && logins.length > 2 ? 'grid-cols-2' : ''} gap-4">
|
||||
{#if !logins}
|
||||
{#each Array(4) as _}
|
||||
<Skeleton layout={[0.5, [2.375]]} />
|
||||
{/each}
|
||||
{:else}
|
||||
{#each providers as { type, icon, name }}
|
||||
{#if logins?.includes(type)}
|
||||
<Button
|
||||
color="light"
|
||||
variant="border"
|
||||
startIcon={{ icon, classes: 'h-4' }}
|
||||
on:click={() => storeRedirect(type)}
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
{/if}
|
||||
{/each}
|
||||
{#each logins.filter((x) => !providersType?.includes(x)) as login}
|
||||
<Button
|
||||
color="dark"
|
||||
variant="border"
|
||||
btnClasses="mt-2 w-full !border-gray-300"
|
||||
on:click={() => storeRedirect(login)}
|
||||
>
|
||||
{login}
|
||||
</Button>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if saml}
|
||||
<Button
|
||||
color="dark"
|
||||
variant="border"
|
||||
btnClasses="mt-2 w-full !border-gray-300"
|
||||
on:click={() => {
|
||||
if (saml) {
|
||||
window.location.href = saml
|
||||
} else {
|
||||
sendUserToast('No SAML login available', true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
SSO
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if saml || (logins && logins.length > 0)}
|
||||
<div class={classNames('center-center', logins && logins.length > 0 ? 'mt-6' : '')}>
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
variant="border"
|
||||
btnClasses="!border-none"
|
||||
on:click={() => {
|
||||
showPassword = !showPassword
|
||||
}}
|
||||
>
|
||||
Log in without third-party
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showPassword}
|
||||
<div>
|
||||
<div class="space-y-6">
|
||||
{#if isCloudHosted()}
|
||||
<p class="text-xs text-tertiary italic pb-6">
|
||||
To get credentials without the OAuth providers above, send an email at
|
||||
contact@windmill.dev
|
||||
</p>
|
||||
{/if}
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium leading-6 text-primary">
|
||||
Email
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
type="email"
|
||||
bind:value={email}
|
||||
id="email"
|
||||
autocomplete="email"
|
||||
class="block w-full rounded-md border-0 py-1.5 text-primary shadow-sm ring-1 ring-inset placeholder:text-secondary focus:ring-2 focus:ring-inset focus:ring-frost-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium leading-6 text-primary">
|
||||
Password
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
on:keyup={handleKeyUp}
|
||||
bind:value={password}
|
||||
id="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="block w-full rounded-md border-0 py-1.5 text-shadow shadow-sm ring-1 ring-inset placeholder:text-secondary focus:ring-2 focus:ring-inset focus:ring-frost-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-2">
|
||||
<button
|
||||
on:click={login}
|
||||
disabled={!email || !password}
|
||||
class="flex w-full justify-center rounded-md bg-frost-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-frost-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-frost-600"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isCloudHosted()}
|
||||
<p class="text-2xs text-tertiary italic mt-10 text-center">
|
||||
By logging in, you agree to our
|
||||
<a href="https://windmill.dev/terms_of_service" target="_blank" rel="noreferrer">
|
||||
Terms of Service
|
||||
</a>
|
||||
and
|
||||
<a href="https://windmill.dev/privacy_policy" target="_blank" rel="noreferrer">
|
||||
Privacy Policy
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -9,7 +9,7 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import { AlignWrapper } from '../helpers'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
|
||||
export let id: string
|
||||
export let configuration: RichConfigurations
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import { RunnableWrapper } from '../helpers'
|
||||
import type { CustomComponentConfig } from '../../editor/component'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
|
||||
|
||||
export let id: string
|
||||
export let render: boolean
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
import { initCss, transformBareBase64IfNecessary } from '../../utils'
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import { AlignWrapper } from '../helpers'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { loadIcon } from '../icon'
|
||||
import ComponentErrorHandler from '../helpers/ComponentErrorHandler.svelte'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
|
||||
export let id: string
|
||||
export let configuration: RichConfigurations
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import { initConfig, initOutput } from '../../editor/appUtils'
|
||||
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
|
||||
import { initCss } from '../../utils'
|
||||
import { AlignWrapper } from '../helpers'
|
||||
import { loadIcon } from '../icon'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import { components } from '../../editor/component'
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
|
||||
export let id: string
|
||||
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = 'left'
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
import { initCss } from '../../utils'
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import { AlignWrapper } from '../helpers'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { loadIcon } from '../icon'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import Menu from '$lib/components/common/menu/MenuV2.svelte'
|
||||
import { AppButton } from '../buttons'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
|
||||
export let id: string
|
||||
export let configuration: RichConfigurations
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
import DbExplorerCount from './DbExplorerCount.svelte'
|
||||
import AppAggridExplorerTable from '../table/AppAggridExplorerTable.svelte'
|
||||
import type { IDatasource } from 'ag-grid-community'
|
||||
import { RunnableWrapper } from '../../helpers'
|
||||
import type RunnableComponent from '../../helpers/RunnableComponent.svelte'
|
||||
import InsertRowRunnable from './InsertRowRunnable.svelte'
|
||||
import DeleteRow from './DeleteRow.svelte'
|
||||
@@ -40,6 +39,7 @@
|
||||
import DebouncedInput from '../../helpers/DebouncedInput.svelte'
|
||||
import { CancelablePromise } from '$lib/gen'
|
||||
import RefreshButton from '$lib/components/apps/components/helpers/RefreshButton.svelte'
|
||||
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
|
||||
|
||||
export let id: string
|
||||
export let configuration: RichConfigurations
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
export { default as AppTable } from './table/AppTable.svelte'
|
||||
export { default as AppAggridTable } from './table/AppAggridTable.svelte'
|
||||
export { default as AppBarChart } from './AppBarChart.svelte'
|
||||
export { default as AppDisplayComponent } from './AppDisplayComponent.svelte'
|
||||
export { default as AppHtml } from './AppHtml.svelte'
|
||||
export { default as AppIcon } from './AppIcon.svelte'
|
||||
export { default as AppImage } from './AppImage.svelte'
|
||||
export { default as AppMap } from './AppMap.svelte'
|
||||
export { default as AppPdf } from './AppPdf.svelte'
|
||||
export { default as AppPieChart } from './AppPieChart.svelte'
|
||||
export { default as AppScatterChart } from './AppScatterChart.svelte'
|
||||
export { default as AppText } from './AppText.svelte'
|
||||
export { default as AppTimeseries } from './AppTimeseries.svelte'
|
||||
export { default as PlotlyHtml } from './PlotlyHtml.svelte'
|
||||
export { default as VegaLiteHtml } from './VegaLiteHtml.svelte'
|
||||
export { default as AppMarkdown } from './AppMarkdown.svelte'
|
||||
export { default as PlotlyHtmlV2 } from './PlotlyHtmlV2.svelte'
|
||||
+2
-1
@@ -16,9 +16,10 @@
|
||||
import { initCss } from '$lib/components/apps/utils'
|
||||
import ResolveStyle from '../../helpers/ResolveStyle.svelte'
|
||||
import AppAggridExplorerTable from './AppAggridExplorerTable.svelte'
|
||||
import { DebouncedInput, type RunnableComponent } from '../..'
|
||||
import { getPrimaryKeys } from '../dbtable/utils'
|
||||
import InitializeComponent from '../../helpers/InitializeComponent.svelte'
|
||||
import DebouncedInput from '../../helpers/DebouncedInput.svelte'
|
||||
import RunnableComponent from '../../helpers/RunnableComponent.svelte'
|
||||
|
||||
export let id: string
|
||||
export let componentInput: AppInput | undefined
|
||||
|
||||
+3
-1
@@ -7,7 +7,9 @@
|
||||
import 'ag-grid-community/styles/ag-theme-alpine.css'
|
||||
|
||||
import AppButton from '../../buttons/AppButton.svelte'
|
||||
import { AppCheckbox, AppSelect } from '../..'
|
||||
import AppCheckbox from '../../inputs/AppCheckbox.svelte'
|
||||
import AppSelect from '../../inputs/AppSelect.svelte'
|
||||
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { Popup } from '$lib/components/common'
|
||||
import { Plug2 } from 'lucide-svelte'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { InputValue } from '.'
|
||||
import InputValue from './InputValue.svelte'
|
||||
import type { RichConfiguration } from '../../types'
|
||||
|
||||
export let id: string
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
export { default as AlignWrapper } from './AlignWrapper.svelte'
|
||||
export { default as DebouncedInput } from './DebouncedInput.svelte'
|
||||
export { default as HiddenComponent } from './HiddenComponent.svelte'
|
||||
export { default as InputDefaultValue } from './InputDefaultValue.svelte'
|
||||
export { default as InputValue } from './InputValue.svelte'
|
||||
export { default as MissingConnectionWarning } from './MissingConnectionWarning.svelte'
|
||||
export { default as NonRunnableComponent } from './NonRunnableComponent.svelte'
|
||||
export { default as RefreshButton } from './RefreshButton.svelte'
|
||||
export { default as RunnableComponent } from './RunnableComponent.svelte'
|
||||
export { default as RunnableWrapper } from './RunnableWrapper.svelte'
|
||||
@@ -1,6 +0,0 @@
|
||||
export * from './buttons'
|
||||
export * from './display'
|
||||
export * from './inputs'
|
||||
export * from './layout'
|
||||
|
||||
export * from './helpers'
|
||||
@@ -5,9 +5,9 @@
|
||||
import type { AppViewerContext, ComponentCustomCSS, RichConfiguration } from '../../types'
|
||||
import { initCss } from '../../utils'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import { InputValue } from '../helpers'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
|
||||
export let id: string
|
||||
export let componentContainerHeight: number
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
// import type { EvalV2AppInput, StaticAppInput } from '../../inputType'
|
||||
import { writable } from 'svelte/store'
|
||||
import { InputValue } from '../helpers'
|
||||
import GroupWrapper from '../GroupWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
|
||||
export let id: string
|
||||
export let componentContainerHeight: number
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { initCss } from '../../utils'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import { InputValue } from '../helpers'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import type { DecisionTreeNode } from '../../editor/component'
|
||||
@@ -13,6 +12,7 @@
|
||||
import { initOutput } from '../../editor/appUtils'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import { getFirstNode, isDebugging } from '../../editor/settingsPanel/decisionTree/utils'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
|
||||
export let id: string
|
||||
export let componentContainerHeight: number
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
import { initCss } from '../../utils'
|
||||
import { Button, Drawer, DrawerContent } from '$lib/components/common'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { AlignWrapper } from '../helpers'
|
||||
import { initConfig, initOutput } from '../../editor/appUtils'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import { components } from '../../editor/component'
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
|
||||
export let customCss: ComponentCustomCSS<'drawercomponent'> | undefined = undefined
|
||||
export let id: string
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import { initCss } from '../../utils'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { AlignWrapper } from '../helpers'
|
||||
import { initConfig, initOutput } from '../../editor/appUtils'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import Portal from 'svelte-portal'
|
||||
@@ -15,6 +14,7 @@
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import Disposable from '$lib/components/common/drawer/Disposable.svelte'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
|
||||
export let customCss: ComponentCustomCSS<'modalcomponent'> | undefined = undefined
|
||||
export let id: string
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
import { initCss } from '../../utils'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { RunnableComponent, RunnableWrapper } from '../helpers'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import { ArrowLeftIcon, ArrowRightIcon, Loader2 } from 'lucide-svelte'
|
||||
import Stepper from '$lib/components/common/stepper/Stepper.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import RunnableComponent from '../helpers/RunnableComponent.svelte'
|
||||
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
|
||||
|
||||
export let id: string
|
||||
export let componentContainerHeight: number
|
||||
|
||||
@@ -177,7 +177,8 @@
|
||||
movingcomponents: writable(undefined),
|
||||
selectedComponentInEditor: writable(undefined),
|
||||
jobsDrawerOpen: writable(false),
|
||||
scale
|
||||
scale,
|
||||
stylePanel: () => StylePanel
|
||||
})
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
@@ -277,7 +277,6 @@
|
||||
const ntriggerables: Record<string, TriggerableV2> = Object.fromEntries(
|
||||
allTriggers.filter(Boolean) as [string, TriggerableV2][]
|
||||
)
|
||||
console.log(ntriggerables)
|
||||
policy.triggerables_v2 = ntriggerables
|
||||
}
|
||||
|
||||
@@ -389,7 +388,7 @@
|
||||
|
||||
let secretUrl: string | undefined = undefined
|
||||
|
||||
$: secretUrl == undefined && policy.execution_mode == 'anonymous' && getSecretUrl()
|
||||
$: appPath != '' && secretUrl == undefined && getSecretUrl()
|
||||
|
||||
async function getSecretUrl() {
|
||||
secretUrl = await AppService.getPublicSecretOfApp({
|
||||
@@ -399,15 +398,16 @@
|
||||
}
|
||||
|
||||
async function setPublishState() {
|
||||
await computeTriggerables()
|
||||
await AppService.updateApp({
|
||||
workspace: $workspaceStore!,
|
||||
path: appPath,
|
||||
requestBody: { policy }
|
||||
})
|
||||
if (policy.execution_mode == 'anonymous') {
|
||||
sendUserToast('App made visible publicly at the secret URL.')
|
||||
sendUserToast('App require no login to be accessed')
|
||||
} else {
|
||||
sendUserToast('App made unaccessible publicly')
|
||||
sendUserToast('App require login and read-access')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -923,22 +923,26 @@
|
||||
</Alert>
|
||||
{:else}
|
||||
<Alert title="App executed on behalf of you">
|
||||
A viewer of the app will execute the runnables of the app on behalf of the publisher (you).
|
||||
A viewer of the app will execute the runnables of the app on behalf of the publisher (you)
|
||||
<Tooltip>
|
||||
This is to ensure that all resources/runnable available at time of creating the app would
|
||||
prevent the good execution of the app. To guarantee tight security, a policy is computed
|
||||
at time of deployment of the app which only allow the scripts/flows referred to in the app
|
||||
to be called on behalf of, and the resources are passed by reference so that their actual
|
||||
value is . Furthermore, static parameters are not overridable. Hence, users will only be
|
||||
able to use the app as intended by the publisher without risk for leaking resources not
|
||||
used in the app.
|
||||
It ensures that all required resources/runnable visible for publisher but not for viewer
|
||||
at time of creating the app would prevent the execution of the app. To guarantee tight
|
||||
security, a policy is computed at time of deployment of the app which only allow the
|
||||
scripts/flows referred to in the app to be called on behalf of. Furthermore, static
|
||||
parameters are not overridable. Hence, users will only be able to use the app as intended
|
||||
by the publisher without risk for leaking resources not used in the app.
|
||||
</Tooltip>
|
||||
</Alert>
|
||||
|
||||
<div class="mt-10" />
|
||||
|
||||
<h2>Secret public URL</h2>
|
||||
<div class="mt-4" />
|
||||
|
||||
<Toggle
|
||||
options={{
|
||||
left: `Require read-access`,
|
||||
right: `Publish publicly for anyone knowing the secret url`
|
||||
left: `Require login and read-access`,
|
||||
right: `No login required`
|
||||
}}
|
||||
checked={policy.execution_mode == 'anonymous'}
|
||||
on:change={(e) => {
|
||||
@@ -947,11 +951,11 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if policy.execution_mode == 'anonymous' && secretUrl}
|
||||
{@const url = `${$page.url.hostname}/public/${$workspaceStore}/${secretUrl}`}
|
||||
{@const href = $page.url.protocol + '//' + url}
|
||||
<div class="my-6 box">
|
||||
Public url:
|
||||
<div class="my-6 box">
|
||||
Secret public url:
|
||||
{#if secretUrl}
|
||||
{@const url = `${$page.url.hostname}/public/${$workspaceStore}/${secretUrl}`}
|
||||
{@const href = $page.url.protocol + '//' + url}
|
||||
<a
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
@@ -965,12 +969,16 @@
|
||||
<Clipboard />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{:else}<Loader2 class="animate-spin" />
|
||||
{/if}
|
||||
<div class="text-xs text-secondary"
|
||||
>You may share this url directly or embed it using an iframe</div
|
||||
>
|
||||
</div>
|
||||
|
||||
<Alert type="info" title="Only latest saved app is publicly available">
|
||||
Once made public, you will still need to deploy the app to make visible the latest changes
|
||||
</Alert>
|
||||
{/if}
|
||||
<Alert type="info" title="Only latest deployed app is publicly available">
|
||||
You will still need to deploy the app to make visible the latest changes
|
||||
</Alert>
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
import Component from './component/Component.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { columnConfiguration } from '../gridUtils'
|
||||
import { HiddenComponent } from '../components'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { dfs, maxHeight } from './appUtils'
|
||||
import { BG_PREFIX, migrateApp } from '../utils'
|
||||
import { workspaceStore, enterpriseLicense } from '$lib/stores'
|
||||
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
|
||||
import { getTheme } from './componentsPanel/themeUtils'
|
||||
import HiddenComponent from '../components/helpers/HiddenComponent.svelte'
|
||||
|
||||
export let app: App
|
||||
export let appPath: string = ''
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import type { AppEditorContext, AppViewerContext } from '../types'
|
||||
import DeleteComponent from './settingsPanel/DeleteComponent.svelte'
|
||||
import { secondaryMenuLeft } from './settingsPanel/secondaryMenu'
|
||||
import StylePanel from './settingsPanel/StylePanel.svelte'
|
||||
import { clickOutside } from '$lib/utils'
|
||||
import Portal from 'svelte-portal'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -60,7 +59,7 @@
|
||||
let componentCallbacks: ComponentCallbacks | undefined = undefined
|
||||
|
||||
const { selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { movingcomponents } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { movingcomponents, stylePanel } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
let deleteComponent: DeleteComponent | undefined = undefined
|
||||
|
||||
@@ -85,7 +84,7 @@
|
||||
{
|
||||
label: 'Show style panel',
|
||||
onClick: () => {
|
||||
secondaryMenuLeft?.toggle(StylePanel, { type: 'style' })
|
||||
secondaryMenuLeft?.toggle(stylePanel(), { type: 'style' })
|
||||
},
|
||||
icon: Paintbrush2,
|
||||
disabled: $secondaryMenuLeft.isOpen
|
||||
|
||||
@@ -8,42 +8,7 @@
|
||||
import type { AppEditorContext, AppViewerContext } from '../../types'
|
||||
import ComponentHeader from '../ComponentHeader.svelte'
|
||||
import type { AppComponent } from './components'
|
||||
import {
|
||||
AppBarChart,
|
||||
AppDisplayComponent,
|
||||
AppTable,
|
||||
AppText,
|
||||
AppButton,
|
||||
AppPieChart,
|
||||
AppSelect,
|
||||
AppCheckbox,
|
||||
AppTextInput,
|
||||
AppNumberInput,
|
||||
AppDateInput,
|
||||
AppForm,
|
||||
AppScatterChart,
|
||||
AppTimeseries,
|
||||
AppHtml,
|
||||
AppMarkdown,
|
||||
AppSliderInputs,
|
||||
AppFormButton,
|
||||
VegaLiteHtml,
|
||||
PlotlyHtml,
|
||||
PlotlyHtmlV2,
|
||||
AppRangeInput,
|
||||
AppTabs,
|
||||
AppIcon,
|
||||
AppCurrencyInput,
|
||||
AppDivider,
|
||||
AppFileInput,
|
||||
AppImage,
|
||||
AppContainer,
|
||||
AppAggridTable,
|
||||
AppDrawer,
|
||||
AppMap,
|
||||
AppSplitpanes,
|
||||
AppPdf
|
||||
} from '../../components'
|
||||
|
||||
import AppMultiSelect from '../../components/inputs/AppMultiSelect.svelte'
|
||||
import AppMultiSelectV2 from '../../components/inputs/AppMultiSelectV2.svelte'
|
||||
import AppModal from '../../components/layout/AppModal.svelte'
|
||||
@@ -76,6 +41,40 @@
|
||||
import AppDateTimeInput from '../../components/inputs/AppDateTimeInput.svelte'
|
||||
import AppAggridInfiniteTable from '../../components/display/table/AppAggridInfiniteTable.svelte'
|
||||
import AppAggridInfiniteTableEe from '../../components/display/table/AppAggridInfiniteTableEe.svelte'
|
||||
import AppDisplayComponent from '../../components/display/AppDisplayComponent.svelte'
|
||||
import AppTimeseries from '../../components/display/AppTimeseries.svelte'
|
||||
import AppHtml from '../../components/display/AppHtml.svelte'
|
||||
import AppMarkdown from '../../components/display/AppMarkdown.svelte'
|
||||
import VegaLiteHtml from '../../components/display/VegaLiteHtml.svelte'
|
||||
import PlotlyHtml from '../../components/display/PlotlyHtml.svelte'
|
||||
import PlotlyHtmlV2 from '../../components/display/PlotlyHtmlV2.svelte'
|
||||
import AppScatterChart from '../../components/display/AppScatterChart.svelte'
|
||||
import AppPieChart from '../../components/display/AppPieChart.svelte'
|
||||
import AppTable from '../../components/display/table/AppTable.svelte'
|
||||
import AppAggridTable from '../../components/display/table/AppAggridTable.svelte'
|
||||
import AppText from '../../components/display/AppText.svelte'
|
||||
import AppButton from '../../components/buttons/AppButton.svelte'
|
||||
import AppForm from '../../components/buttons/AppForm.svelte'
|
||||
import AppFormButton from '../../components/buttons/AppFormButton.svelte'
|
||||
import AppCheckbox from '../../components/inputs/AppCheckbox.svelte'
|
||||
import AppTextInput from '../../components/inputs/AppTextInput.svelte'
|
||||
import AppDateInput from '../../components/inputs/AppDateInput.svelte'
|
||||
import AppSelect from '../../components/inputs/AppSelect.svelte'
|
||||
import AppBarChart from '../../components/display/AppBarChart.svelte'
|
||||
import AppDivider from '../../components/layout/AppDivider.svelte'
|
||||
import AppRangeInput from '../../components/inputs/AppRangeInput.svelte'
|
||||
import AppTabs from '../../components/layout/AppTabs.svelte'
|
||||
import AppContainer from '../../components/layout/AppContainer.svelte'
|
||||
import AppSplitpanes from '../../components/layout/AppSplitpanes.svelte'
|
||||
import AppIcon from '../../components/display/AppIcon.svelte'
|
||||
import AppFileInput from '../../components/inputs/AppFileInput.svelte'
|
||||
import AppImage from '../../components/display/AppImage.svelte'
|
||||
import AppDrawer from '../../components/layout/AppDrawer.svelte'
|
||||
import AppMap from '../../components/display/AppMap.svelte'
|
||||
import AppPdf from '../../components/display/AppPdf.svelte'
|
||||
import AppCurrencyInput from '../../components/inputs/currency/AppCurrencyInput.svelte'
|
||||
import AppSliderInputs from '../../components/inputs/AppSliderInputs.svelte'
|
||||
import AppNumberInput from '../../components/inputs/AppNumberInput.svelte'
|
||||
|
||||
export let component: AppComponent
|
||||
export let selected: boolean
|
||||
|
||||
@@ -192,16 +192,20 @@
|
||||
<span class="text-2xs italic text-tertiary">Field's value is set by the user</span>
|
||||
{/if}
|
||||
{#if (componentInput?.type === 'evalv2' || componentInput?.type === 'connected' || componentInput?.type === 'user') && fieldType == 'object' && format?.startsWith('resource-')}
|
||||
<div class="flex flex-row">
|
||||
<div class="flex flex-row items-center">
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={componentInput.allowUserResources}
|
||||
options={{ right: 'Allow resources from users' }}
|
||||
options={{
|
||||
left: 'static resource select only',
|
||||
right: 'resources from users allowed'
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
>Apps are executed on behalf of publishers and by default cannot access viewer's
|
||||
resources. If you use a non-static resource picker and connect it here, you need to toggle
|
||||
this.</Tooltip
|
||||
resources. If the resource passed here as a reference does not come from a static
|
||||
'Resource Select' component (which will be whitelisted by the auto-generated policy), you
|
||||
need to toggle this.</Tooltip
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -288,6 +288,7 @@ export type AppEditorContext = {
|
||||
movingcomponents: Writable<string[] | undefined>
|
||||
jobsDrawerOpen: Writable<boolean>
|
||||
scale: Writable<number>
|
||||
stylePanel: () => any
|
||||
}
|
||||
|
||||
export type FocusedGrid = { parentComponentId: string; subGridIndex: number }
|
||||
|
||||
@@ -1,88 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import Github from '$lib/components/icons/brands/Github.svelte'
|
||||
import Gitlab from '$lib/components/icons/brands/Gitlab.svelte'
|
||||
import Google from '$lib/components/icons/brands/Google.svelte'
|
||||
import Microsoft from '$lib/components/icons/brands/Microsoft.svelte'
|
||||
import Okta from '$lib/components/icons/brands/Okta.svelte'
|
||||
|
||||
import { OauthService, UserService, WorkspaceService } from '$lib/gen'
|
||||
import { UserService, WorkspaceService } from '$lib/gen'
|
||||
import { usersWorkspaceStore, workspaceStore, userStore, enterpriseLicense } from '$lib/stores'
|
||||
import { classNames, emptyString, parseQueryParams } from '$lib/utils'
|
||||
import { getUserExt } from '$lib/user'
|
||||
import { Button, Skeleton } from '$lib/components/common'
|
||||
import { WindmillIcon } from '$lib/components/icons'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { refreshSuperadmin } from '$lib/refreshUser'
|
||||
import LoginPageHeader from '$lib/components/LoginPageHeader.svelte'
|
||||
import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte'
|
||||
import { clearStores } from '$lib/storeUtils'
|
||||
import { setLicense } from '$lib/enterpriseUtils'
|
||||
import Login from '$lib/components/Login.svelte'
|
||||
|
||||
let email = $page.url.searchParams.get('email') ?? ''
|
||||
let password = $page.url.searchParams.get('password') ?? ''
|
||||
const email = $page.url.searchParams.get('email') ?? ''
|
||||
const password = $page.url.searchParams.get('password') ?? ''
|
||||
const error = $page.url.searchParams.get('error') ?? undefined
|
||||
const rd = $page.url.searchParams.get('rd')
|
||||
const providers = [
|
||||
{
|
||||
type: 'github',
|
||||
name: 'GitHub',
|
||||
icon: Github
|
||||
},
|
||||
{
|
||||
type: 'gitlab',
|
||||
name: 'GitLab',
|
||||
icon: Gitlab
|
||||
},
|
||||
{
|
||||
type: 'google',
|
||||
name: 'Google',
|
||||
icon: Google
|
||||
},
|
||||
{
|
||||
type: 'microsoft',
|
||||
name: 'Microsoft',
|
||||
icon: Microsoft
|
||||
},
|
||||
{
|
||||
type: 'okta',
|
||||
name: 'Okta',
|
||||
icon: Okta
|
||||
}
|
||||
] as const
|
||||
|
||||
const providersType = providers.map((p) => p.type as string)
|
||||
const rd = $page.url.searchParams.get('rd') ?? undefined
|
||||
|
||||
let showPassword = false
|
||||
let logins: string[] | undefined = undefined
|
||||
let saml: string | undefined = undefined
|
||||
|
||||
async function login(): Promise<void> {
|
||||
const requestBody = {
|
||||
email,
|
||||
password
|
||||
}
|
||||
|
||||
try {
|
||||
await UserService.login({ requestBody })
|
||||
} catch (err) {
|
||||
sendUserToast('Invalid credentials', true)
|
||||
return
|
||||
}
|
||||
|
||||
// Once logged in, we can fetch the workspaces
|
||||
$usersWorkspaceStore = await WorkspaceService.listUserWorkspaces()
|
||||
// trigger a reload of the user
|
||||
if ($workspaceStore) {
|
||||
$userStore = await getUserExt($workspaceStore)
|
||||
}
|
||||
|
||||
// Finally, we check whether the user is a superadmin
|
||||
refreshSuperadmin()
|
||||
redirectUser()
|
||||
}
|
||||
|
||||
async function redirectUser() {
|
||||
const firstTimeCookie =
|
||||
@@ -140,14 +76,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLogins() {
|
||||
const allLogins = await OauthService.listOauthLogins()
|
||||
logins = allLogins.oauth
|
||||
saml = allLogins.saml
|
||||
|
||||
showPassword = (logins.length == 0 && !saml) || (email != undefined && email.length > 0)
|
||||
}
|
||||
|
||||
async function redirectIfNecessary() {
|
||||
await UserService.getCurrentEmail()
|
||||
redirectUser()
|
||||
@@ -155,35 +83,10 @@
|
||||
|
||||
try {
|
||||
setLicense()
|
||||
loadLogins()
|
||||
redirectIfNecessary()
|
||||
} catch {
|
||||
clearStores()
|
||||
}
|
||||
|
||||
function handleKeyUp(event: KeyboardEvent) {
|
||||
const key = event.key
|
||||
|
||||
if (key === 'Enter') {
|
||||
event.preventDefault()
|
||||
login()
|
||||
}
|
||||
}
|
||||
|
||||
function storeRedirect(provider: string) {
|
||||
if (rd) {
|
||||
try {
|
||||
localStorage.setItem('rd', rd)
|
||||
} catch (e) {
|
||||
console.error('Could not persist redirection to local storage', e)
|
||||
}
|
||||
}
|
||||
goto('/api/oauth/login/' + provider)
|
||||
}
|
||||
|
||||
$: if (error) {
|
||||
sendUserToast(error, true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -210,134 +113,6 @@
|
||||
<div class="flex justify-end">
|
||||
<DarkModeToggle forcedDarkMode={false} />
|
||||
</div>
|
||||
<div class="bg-surface px-4 py-8 shadow md:border sm:rounded-lg sm:px-10">
|
||||
<div class="grid {logins && logins.length > 2 ? 'grid-cols-2' : ''} gap-4">
|
||||
{#if !logins}
|
||||
{#each Array(4) as _}
|
||||
<Skeleton layout={[0.5, [2.375]]} />
|
||||
{/each}
|
||||
{:else}
|
||||
{#each providers as { type, icon, name }}
|
||||
{#if logins?.includes(type)}
|
||||
<Button
|
||||
color="light"
|
||||
variant="border"
|
||||
startIcon={{ icon, classes: 'h-4' }}
|
||||
on:click={() => storeRedirect(type)}
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
{/if}
|
||||
{/each}
|
||||
{#each logins.filter((x) => !providersType?.includes(x)) as login}
|
||||
<Button
|
||||
color="dark"
|
||||
variant="border"
|
||||
btnClasses="mt-2 w-full !border-gray-300"
|
||||
on:click={() => storeRedirect(login)}
|
||||
>
|
||||
{login}
|
||||
</Button>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if saml}
|
||||
<Button
|
||||
color="dark"
|
||||
variant="border"
|
||||
btnClasses="mt-2 w-full !border-gray-300"
|
||||
on:click={() => {
|
||||
if (saml) {
|
||||
window.location.href = saml
|
||||
} else {
|
||||
sendUserToast('No SAML login available', true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
SSO
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if saml || (logins && logins.length > 0)}
|
||||
<div class={classNames('center-center', logins && logins.length > 0 ? 'mt-6' : '')}>
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
variant="border"
|
||||
btnClasses="!border-none"
|
||||
on:click={() => {
|
||||
showPassword = !showPassword
|
||||
}}
|
||||
>
|
||||
Log in without third-party
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showPassword}
|
||||
<div>
|
||||
<div class="space-y-6">
|
||||
{#if isCloudHosted()}
|
||||
<p class="text-xs text-tertiary italic pb-6">
|
||||
To get credentials without the OAuth providers above, send an email at
|
||||
contact@windmill.dev
|
||||
</p>
|
||||
{/if}
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium leading-6 text-primary">
|
||||
Email
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
type="email"
|
||||
bind:value={email}
|
||||
id="email"
|
||||
autocomplete="email"
|
||||
class="block w-full rounded-md border-0 py-1.5 text-primary shadow-sm ring-1 ring-inset placeholder:text-secondary focus:ring-2 focus:ring-inset focus:ring-frost-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium leading-6 text-primary">
|
||||
Password
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
on:keyup={handleKeyUp}
|
||||
bind:value={password}
|
||||
id="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="block w-full rounded-md border-0 py-1.5 text-shadow shadow-sm ring-1 ring-inset placeholder:text-secondary focus:ring-2 focus:ring-inset focus:ring-frost-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-2">
|
||||
<button
|
||||
on:click={login}
|
||||
disabled={!email || !password}
|
||||
class="flex w-full justify-center rounded-md bg-frost-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-frost-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-frost-600"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isCloudHosted()}
|
||||
<p class="text-2xs text-tertiary italic mt-10 text-center">
|
||||
By logging in, you agree to our
|
||||
<a href="https://windmill.dev/terms_of_service" target="_blank" rel="noreferrer">
|
||||
Terms of Service
|
||||
</a>
|
||||
and
|
||||
<a href="https://windmill.dev/privacy_policy" target="_blank" rel="noreferrer">
|
||||
Privacy Policy
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Login {rd} {error} {password} {email} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import AppConnectInner from '$lib/components/AppConnectInner.svelte'
|
||||
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
let resourceType = $page.url.searchParams.get('resource_type') ?? undefined
|
||||
@@ -15,6 +16,11 @@
|
||||
let appConnect: AppConnectInner | undefined = undefined
|
||||
|
||||
let darkMode: boolean = false
|
||||
const workspace = $page.url.searchParams.get('workspace')
|
||||
|
||||
if (workspace) {
|
||||
$workspaceStore = workspace
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (resourceType) {
|
||||
|
||||
@@ -15,10 +15,14 @@
|
||||
import { writable } from 'svelte/store'
|
||||
import { setLicense } from '$lib/enterpriseUtils'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import Login from '$lib/components/Login.svelte'
|
||||
import { getUserExt } from '$lib/user'
|
||||
import { User, UserRoundX } from 'lucide-svelte'
|
||||
import ChartHighlightTheme from '$lib/components/ChartHighlightTheme.svelte'
|
||||
|
||||
let app: (AppWithLastVersion & { value: any }) | undefined = undefined
|
||||
let notExists = false
|
||||
|
||||
let noPermission = false
|
||||
setContext(IS_APP_PUBLIC_CONTEXT_KEY, true)
|
||||
|
||||
async function loadApp() {
|
||||
@@ -28,22 +32,47 @@
|
||||
path: $page.params.secret
|
||||
})
|
||||
} catch (e) {
|
||||
notExists = true
|
||||
if (e.status == 401) {
|
||||
noPermission = true
|
||||
} else {
|
||||
notExists = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (BROWSER) {
|
||||
setLicense()
|
||||
loadApp()
|
||||
loadUser()
|
||||
}
|
||||
|
||||
async function loadUser() {
|
||||
try {
|
||||
userStore.set(await getUserExt($page.params.workspace))
|
||||
} catch (e) {
|
||||
console.warn('Anonymous user')
|
||||
}
|
||||
}
|
||||
|
||||
const breakpoint = writable<EditorBreakpoint>('lg')
|
||||
|
||||
const darkMode =
|
||||
window.localStorage.getItem('dark-mode') ??
|
||||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
||||
|
||||
if (darkMode === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{@html github}
|
||||
</svelte:head>
|
||||
|
||||
<ChartHighlightTheme />
|
||||
|
||||
<div
|
||||
class="z-50 text-xs fixed bottom-1 right-2 {$enterpriseLicense && !isCloudHosted()
|
||||
? 'transition-opacity delay-1000 duration-1000 opacity-20 hover:delay-0 hover:opacity-100'
|
||||
@@ -54,13 +83,26 @@
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="z-50 text-2xs text-tertiary absolute top-3 left-2"
|
||||
>{#if $userStore}
|
||||
<div class="flex gap-1 items-center"><User size={14} />{$userStore.username}</div>
|
||||
{:else}<UserRoundX size={14} />{/if}
|
||||
</div>
|
||||
|
||||
{#if notExists}
|
||||
<div class="px-4 mt-20"
|
||||
><Alert type="error" title="Not found"
|
||||
>There was an error loading the app. Either it does not exist at this url or its visibility
|
||||
has changed to not be public anymore. <a href="/">Go to app</a>
|
||||
>There was an error loading the app, is the url correct? <a href="/">Go to Windmill</a>
|
||||
</Alert></div
|
||||
>
|
||||
{:else if noPermission}
|
||||
<div class="px-4 mt-20 w-full text-center font-bold text-xl"
|
||||
>{#if $userStore}You are logged in but have no read access for this app{:else}You must be logged
|
||||
in and have read access for this app{/if}</div
|
||||
>
|
||||
<div class="px-2 mx-auto mt-20 max-w-xl w-full">
|
||||
<Login rd={$page.url.toString()} />
|
||||
</div>
|
||||
{:else if app}
|
||||
{#key app}
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user