handle better public domain for apps (#7136)

* cookelogin

* cookelogin

* all

* all

* fix

* all

* all

* update back

* all

* all

* cookelogin

* cookelogin

* Update frontend/src/lib/components/apps/editor/PublicApp.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update frontend/src/lib/components/apps/editor/PublicApp.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* all

* all

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
Ruben Fiszel
2025-11-14 11:14:32 +01:00
committed by GitHub
parent 958e60e9ed
commit 45cd6e210d
15 changed files with 240 additions and 221 deletions

View File

@@ -1 +1 @@
4302ebbb8ded39e04ebcf7aaac71847c9bebe19c
5c9a2780bdc82f243d2539007045c34049d762a6

View File

@@ -75,6 +75,7 @@ fn is_public_route_whitelisted(path: &str) -> bool {
"/api/oauth/list_logins",
"/public/*",
"/a/*",
"/api/oauth/get_connect/*",
"/Inter-Variable.woff2",
];

View File

@@ -33,6 +33,7 @@
import TextInput from './text_input/TextInput.svelte'
import { usePromise } from '$lib/svelte5Utils.svelte'
import { pollJobResult } from './jobs/utils'
import { sameTopDomainOrigin } from '$lib/cookies'
interface Props {
step?: number
@@ -239,7 +240,7 @@
function popupListener(event) {
console.log('Received oauth popup message', event)
let data = event.data
if (event.origin == null || event.origin !== window.location.origin) {
if (!sameTopDomainOrigin(event.origin, window.location.origin)) {
console.log(
'Received oauth popup message from different origin',
event.origin,
@@ -397,6 +398,7 @@
}
window.addEventListener('message', popupListener)
window.addEventListener('storage', handleStorageEvent)
console.log('opening popup', url.toString())
window.open(url.toString(), '_blank', 'popup=true')
step += 1
}

View File

@@ -42,7 +42,7 @@
<Button variant="default" on:click={appConnect?.back ?? (() => {})}>Back</Button>
{/if}
<Button {disabled} on:click={appConnect?.next ?? (() => {})}>
<Button variant="accent" {disabled} on:click={appConnect?.next ?? (() => {})}>
{#if step == 2 && !manual}
Connect
{:else if step == 1}

View File

@@ -19,6 +19,7 @@
import { onDestroy, onMount } from 'svelte'
import Skeleton from './common/skeleton/Skeleton.svelte'
import Button from './common/button/Button.svelte'
import { sameTopDomainOrigin } from '$lib/cookies'
interface Props {
rd?: string | undefined
@@ -212,8 +213,9 @@
function popupListener(event) {
let data = event.data
console.log('popupListener', data, event.origin, window.location.origin)
if (event.origin !== window.location.origin) {
// console.log('popupListener', data, event.origin, window.location.origin)
if (!sameTopDomainOrigin(event.origin, window.location.origin)) {
console.log('popupListener from different origin', event.origin, window.location.origin)
return
}
@@ -261,7 +263,9 @@
console.error('Could not persist redirection to local storage', e)
}
}
let url = base + '/api/oauth/login/' + provider
let url = base + '/api/oauth/login/' + provider + (popup ? '?close=true' : '')
console.log('storeRedirect', popup, url)
if (popup) {
localStorage.setItem('closeUponLogin', 'true')
window.addEventListener('message', popupListener)

View File

@@ -0,0 +1,145 @@
<script lang="ts">
import { User, UserRoundX } from 'lucide-svelte'
import { enterpriseLicense, userStore } from '$lib/stores'
import { base } from '$app/paths'
import { page } from '$app/state'
import Login from '$lib/components/Login.svelte'
import { isCloudHosted } from '$lib/cloud'
import { Alert, Skeleton } from '$lib/components/common'
import { WindmillIcon } from '$lib/components/icons'
import { onMount, setContext } from 'svelte'
import { IS_APP_PUBLIC_CONTEXT_KEY, type EditorBreakpoint } from '../types'
import { UserService, type AppWithLastVersion, type GlobalWhoamiResponse } from '$lib/gen'
import { urlParamsToObject } from '$lib/utils'
import { goto } from '$app/navigation'
import AppPreview from './AppPreview.svelte'
import { twMerge } from 'tailwind-merge'
import { writable } from 'svelte/store'
let {
notExists,
noPermission,
jwtError,
onLoginSuccess,
app,
workspace
}: {
notExists: boolean
noPermission: boolean
jwtError: boolean
onLoginSuccess: () => void
app: (AppWithLastVersion & { value: any }) | undefined
workspace: string | undefined
} = $props()
setContext(IS_APP_PUBLIC_CONTEXT_KEY, true)
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')
}
let globalUser = $state<GlobalWhoamiResponse | undefined>(undefined)
async function loadGlobalUser() {
try {
globalUser = await UserService.globalWhoami()
} catch (error) {
console.error(error)
}
// const user = await fetch('/api/global/user')
// console.log(user)
}
onMount(() => {
// this is to avoid loading global user if the userStore is set at loading
setTimeout(() => {
if ($userStore) return
loadGlobalUser()
}, 2000)
})
</script>
<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'
: ''}"
>
<a href="https://windmill.dev" class="whitespace-nowrap text-primary inline-flex items-center"
>Powered by &nbsp;<WindmillIcon />&nbsp;Windmill</a
>
</div>
{#snippet userInfo(child)}
<div class="flex gap-1 items-center"><User size={14} />{child}</div>
{/snippet}
<div class="z-50 text-2xs text-primary absolute top-3 left-2"
>{#if $userStore}
{@render userInfo($userStore.username)}
{:else if globalUser}
{@render userInfo(globalUser.email)}
{: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, is the url correct? <a href={base}>Go to Windmill</a>
</Alert></div
>
{:else if noPermission}
<div class="px-4 mt-20 w-full text-center font-bold text-xl"> This app requires read access </div>
<div class="text-center mt-8 text-sm text-primary">
{#if $userStore}You are logged in but have no read access to this app{:else if globalUser && workspace}
You are logged in but are not a member of the workspace <span class="text-xl font-bold"
>{workspace}</span
> this app is part of
{:else}You must be logged in and have read access to this app{/if}</div
>
<div class="px-2 mx-auto mt-20 max-w-xl w-full">
{#if !jwtError}
<Login {onLoginSuccess} popup rd={page.url.toString()} />
{/if}
</div>
{:else if app}
{#key app}
<div
class={twMerge(
'min-h-screen h-full w-full flex',
app?.value?.['css']?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={app?.value?.['css']?.['app']?.['viewer']?.style}
>
<AppPreview
noBackend={false}
context={{
email: $userStore?.email,
name: $userStore?.name,
groups: $userStore?.groups,
username: $userStore?.username,
query: urlParamsToObject(page.url.searchParams),
hash: page.url.hash.substring(1)
}}
{workspace}
summary={app.summary}
app={app.value}
appPath={app.path}
{breakpoint}
policy={app.policy}
isEditor={false}
replaceStateFn={(path) => goto(path)}
gotoFn={(path, opt) => goto(path, opt)}
/>
</div>
{/key}
{:else}
<Skeleton layout={[[4], 0.5, [50]]} />
{/if}

View File

@@ -0,0 +1,29 @@
/**
* Reads the value of a cookie by name.
* @param {string} name - The name of the cookie to retrieve.
* @returns {string | undefined} The cookie value, or undefined if not found.
*/
export function getCookie(name: string): string | undefined {
const match = document.cookie.match(
new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()\[\]\\\/\+^])/g, '\\$1') + '=([^;]*)')
)
return match ? decodeURIComponent(match[1]) : undefined
}
// this only check the last 2 segments to work for popup on shared top-domain
export function sameTopDomainOrigin(origin: string | null, desktopOrigin: string): boolean {
if (origin == null) {
return false
}
const getLastTwoSegments = (url: string) => {
const parts = url.split('.');
return parts.length >= 2 ? parts.slice(-2).join('.') : url;
};
if (origin.includes('.') && desktopOrigin.includes('.')) {
return getLastTwoSegments(origin) === getLastTwoSegments(desktopOrigin);
} else {
return origin === desktopOrigin;
}
}

View File

@@ -1,33 +1,21 @@
<script lang="ts">
import { BROWSER } from 'esm-env'
import { base } from '$lib/base'
import AppPreview from '$lib/components/apps/editor/AppPreview.svelte'
import { IS_APP_PUBLIC_CONTEXT_KEY, type EditorBreakpoint } from '$lib/components/apps/types'
import { Alert, Skeleton } from '$lib/components/common'
import { WindmillIcon } from '$lib/components/icons'
import { AppService, OpenAPI, type AppWithLastVersion } from '$lib/gen'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { twMerge } from 'tailwind-merge'
import { userStore, workspaceStore } from '$lib/stores'
import { setContext } from 'svelte'
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 { goto } from '$app/navigation'
import { sendUserToast } from '$lib/toast'
import { page } from '$app/state'
import { urlParamsToObject } from '$lib/utils'
import PublicApp from '$lib/components/apps/editor/PublicApp.svelte'
let app: (AppWithLastVersion & { value: any }) | undefined = $state(undefined)
let notExists = $state(false)
let noPermission = $state(false)
let jwtError = $state(false)
setContext(IS_APP_PUBLIC_CONTEXT_KEY, true)
function isJwt(t: string) {
// simply check that the first part is a valid base64 encoded json
@@ -56,9 +44,10 @@
}
}
const parsedCustomPath = parseCustomPath(page.params.path ?? '')
let workspace: string | undefined = $state(undefined)
async function loadApp() {
const parsedCustomPath = parseCustomPath(page.params.path ?? '')
if (parsedCustomPath.jwt) {
const token = 'jwt_ext_' + parsedCustomPath.jwt
OpenAPI.TOKEN = token
@@ -69,6 +58,7 @@
app = await AppService.getPublicAppByCustomPath({
customPath: parsedCustomPath.path
})
workspace = app.workspace_id
workspaceStore.set(app.workspace_id)
noPermission = false
notExists = false
@@ -95,93 +85,15 @@
setLicense()
loadApp()
}
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>
<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'
: ''}"
>
<a href="https://windmill.dev" class="whitespace-nowrap text-primary inline-flex items-center"
>Powered by &nbsp;<WindmillIcon />&nbsp;Windmill</a
>
</div>
<div class="z-50 text-2xs text-primary 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, is the url correct? <a href={base}>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 to this app{:else}You must be logged
in and have read access to this app{/if}</div
>
<div class="px-2 mx-auto mt-20 max-w-xl w-full">
{#if !jwtError}
<Login
onLoginSuccess={() => {
// window.location.reload()
loadApp()
app = app
}}
popup
rd={page.url.toString()}
/>
{/if}
</div>
{:else if app}
{#key app}
<div
class={twMerge(
'min-h-screen h-full w-full flex',
app?.value?.['css']?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={app?.value?.['css']?.['app']?.['viewer']?.style}
>
<AppPreview
noBackend={false}
context={{
email: $userStore?.email,
name: $userStore?.name,
groups: $userStore?.groups,
username: $userStore?.username,
query: urlParamsToObject(page.url.searchParams),
hash: page.url.hash.substring(1)
}}
workspace={page.params.workspace}
summary={app.summary}
app={app.value}
appPath={app.path}
{breakpoint}
policy={app.policy}
isEditor={false}
replaceStateFn={(path) => goto(path)}
gotoFn={(path, opt) => goto(path, opt)}
/>
</div>
{/key}
{:else}
<Skeleton layout={[[4], 0.5, [50]]} />
{/if}
<PublicApp
{workspace}
{notExists}
{noPermission}
{jwtError}
{app}
onLoginSuccess={() => {
loadApp()
}}
></PublicApp>

View File

@@ -1,34 +1,22 @@
<script lang="ts">
import { BROWSER } from 'esm-env'
import { base } from '$lib/base'
import AppPreview from '$lib/components/apps/editor/AppPreview.svelte'
import { IS_APP_PUBLIC_CONTEXT_KEY, type EditorBreakpoint } from '$lib/components/apps/types'
import { WindmillIcon } from '$lib/components/icons'
import { AppService, OpenAPI, type AppWithLastVersion } from '$lib/gen'
import { enterpriseLicense, userStore } from '$lib/stores'
import { twMerge } from 'tailwind-merge'
import { userStore } from '$lib/stores'
import { setContext } from 'svelte'
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 { goto } from '$app/navigation'
import { sendUserToast } from '$lib/toast'
import Alert from '$lib/components/common/alert/Alert.svelte'
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
import { page } from '$app/state'
import { urlParamsToObject } from '$lib/utils'
import PublicApp from '$lib/components/apps/editor/PublicApp.svelte'
let app: (AppWithLastVersion & { value: any }) | undefined = $state(undefined)
let notExists = $state(false)
let noPermission = $state(false)
let jwtError = $state(false)
setContext(IS_APP_PUBLIC_CONTEXT_KEY, true)
function parseSecret(secret: string): { secret: string; jwt: string } {
const parts = secret.split('/')
@@ -59,6 +47,11 @@
if (BROWSER) {
setLicense()
loadAll()
}
function loadAll() {
console.log('loadAll')
loadUser().then(() => {
loadApp()
})
@@ -81,95 +74,15 @@
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>
<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'
: ''}"
>
<a href="https://windmill.dev" class="whitespace-nowrap text-primary inline-flex items-center"
>Powered by &nbsp;<WindmillIcon />&nbsp;Windmill</a
>
</div>
<div class="z-50 text-2xs text-primary 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, is the url correct? <a href={base}>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 to this app{:else}You must be logged
in and have read access to this app{/if}</div
>
<div class="px-2 mx-auto mt-20 max-w-xl w-full">
{#if !jwtError}
<Login
onLoginSuccess={() => {
console.log('login')
// window.location.reload()
loadUser().then(() => {
loadApp()
})
app = app
}}
popup
rd={page.url.toString()}
/>
{/if}
</div>
{:else if app}
{#key app}
<div
class={twMerge(
'min-h-screen h-full w-full flex',
app?.value?.['css']?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={app?.value?.['css']?.['app']?.['viewer']?.style}
>
<AppPreview
noBackend={false}
context={{
email: $userStore?.email,
name: $userStore?.name,
groups: $userStore?.groups,
username: $userStore?.username,
query: urlParamsToObject(page.url.searchParams),
hash: page.url.hash.substring(1)
}}
workspace={page.params.workspace}
summary={app.summary}
app={app.value}
appPath={app.path}
{breakpoint}
policy={app.policy}
isEditor={false}
replaceStateFn={(path) => goto(path)}
gotoFn={(path, opt) => goto(path, opt)}
/>
</div>
{/key}
{:else}
<Skeleton layout={[[4], 0.5, [50]]} />
{/if}
<PublicApp
{app}
workspace={page.params.workspace}
{notExists}
{noPermission}
{jwtError}
onLoginSuccess={() => {
loadAll()
}}
></PublicApp>

View File

@@ -12,6 +12,8 @@
import { parseQueryParams } from '$lib/utils'
import { page } from '$app/state'
import { isCloudHosted } from '$lib/cloud'
import { getCookie } from '$lib/cookies'
// import { getAndDeleteCookie } from '$lib/cookies'
let error = page.url.searchParams.get('error')
let clientName = page.params.client_name ?? ''
@@ -19,11 +21,14 @@
let state = page.url.searchParams.get('state') ?? undefined
onMount(async () => {
// const closeCookie = getAndDeleteCookie('close')
// console.log('closeCookie', closeCookie)
const rd = localStorage.getItem('rd')
if (rd) {
localStorage.removeItem('rd')
}
const closeUponLogin = localStorage.getItem('closeUponLogin') == 'true'
const cookieCloseUponLogin = getCookie('close') == 'true'
const closeUponLogin = cookieCloseUponLogin ?? localStorage.getItem('closeUponLogin') == 'true'
if (error) {
sendUserToast(`Error trying to login with ${clientName} ${error}`, true)
if (closeUponLogin) {

View File

@@ -11,7 +11,15 @@ const version = JSON.parse(json)
const config = {
server: {
https: process.env.HTTPS === 'true',
allowedHosts: ['localhost', '127.0.0.1', '0.0.0.0', 'rubendev.wimill.xyz', 'windmill.xyz'],
allowedHosts: [
'localhost',
'127.0.0.1',
'0.0.0.0',
'rubendev.wimill.xyz',
'windmill.xyz',
'app.windmill.xyz',
'public.windmill.xyz'
],
port: 3000,
proxy: {
'^/api/w/[^/]+/s3_proxy/.*': {
@@ -28,8 +36,8 @@ const config = {
},
'^/api/.*': {
target: process.env.REMOTE ?? 'https://app.windmill.dev/',
changeOrigin: true,
cookieDomainRewrite: 'localhost'
changeOrigin: true
// cookieDomainRewrite: 'localhost'
},
'^/ws/.*': {
target: process.env.REMOTE_LSP ?? 'https://app.windmill.dev',
@@ -64,7 +72,7 @@ const config = {
exclude: [
'@codingame/monaco-vscode-standalone-typescript-language-features',
'@codingame/monaco-vscode-standalone-languages'
],
]
},
worker: {
format: 'es'