mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 08:03:50 +00:00
Authentication refactor (#65)
* Refactor login logic * Derive username from user + fix initial redirection if logged in * Simplify how login navigation works * Restore redirection * Redirect to login page when not logged in * Fix PR issues * Add missing refreshSuperadmin when reloading a page with a valid token * Explicitly clearing stores when logging out. Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
This commit is contained in:
co-authored by
Ruben Fiszel
parent
06eb50fbf2
commit
9e6ab11484
@@ -1 +1,104 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import { SvelteToast } from '@zerodevx/svelte-toast'
|
||||
import { onMount } from 'svelte'
|
||||
import { UserService, WorkspaceService } from '../gen'
|
||||
import {
|
||||
clearStores,
|
||||
superadmin,
|
||||
usernameStore,
|
||||
usersWorkspaceStore,
|
||||
workspaceStore
|
||||
} from '../stores'
|
||||
import { getUser, logout, logoutWithRedirect, refreshSuperadmin, sendUserToast } from '../utils'
|
||||
|
||||
// Default toast options
|
||||
const toastOptions = {
|
||||
duration: 4000, // duration of progress bar tween to the `next` value
|
||||
initial: 1, // initial progress bar value
|
||||
next: 0, // next progress value
|
||||
pausable: false, // pause progress bar tween on mouse hover
|
||||
dismissable: true, // allow dismiss with close button
|
||||
reversed: false, // insert new toast to bottom of stack
|
||||
intro: { x: 256 }, // toast intro fly animation settings
|
||||
theme: {} // css var overrides
|
||||
}
|
||||
|
||||
const monacoEditorUnhandledErrors = [
|
||||
'Model not found',
|
||||
'Connection is disposed.',
|
||||
'Connection got disposed.'
|
||||
]
|
||||
|
||||
async function redirectIfLoggedIn(): Promise<void> {
|
||||
try {
|
||||
await UserService.getCurrentEmail()
|
||||
goto('/')
|
||||
} catch {
|
||||
clearStores()
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
$usersWorkspaceStore = await WorkspaceService.listUserWorkspaces()
|
||||
await refreshSuperadmin()
|
||||
|
||||
if ($workspaceStore && $usernameStore) {
|
||||
await getUser($workspaceStore)
|
||||
} else if ($superadmin) {
|
||||
console.log('You are a superadmin, you can go wherever you please')
|
||||
} else {
|
||||
goto('/user/workspaces')
|
||||
}
|
||||
} catch {
|
||||
logoutWithRedirect($page.url.pathname)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRedirectionsAndLoadData() {
|
||||
if ($page.url.pathname === '/user/login') {
|
||||
redirectIfLoggedIn()
|
||||
} else {
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
handleRedirectionsAndLoadData()
|
||||
|
||||
window.onunhandledrejection = (event: PromiseRejectionEvent) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (event.reason?.message) {
|
||||
const { message, body, status } = event.reason
|
||||
|
||||
// Unhandled errors from Monaco Editor don't logout the user
|
||||
if (monacoEditorUnhandledErrors.includes(message)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (status == '401') {
|
||||
sendUserToast('Logged out after a request was unauthorized', true)
|
||||
logout($page.url.pathname)
|
||||
} else {
|
||||
sendUserToast(`${message}: ${body ?? ''}`, true)
|
||||
}
|
||||
} else {
|
||||
console.log('Caught unhandled promise rejection without message', event)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
<SvelteToast {toastOptions} />
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--toastBackground: #eff6ff;
|
||||
--toastBarBackground: #eff6ff;
|
||||
--toastColor: #123456;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,38 +1,27 @@
|
||||
<script lang="ts">
|
||||
import '../app.css'
|
||||
|
||||
import { OpenAPI, UserService, WorkspaceService } from '../gen'
|
||||
import { faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||
import {
|
||||
logout,
|
||||
clickOutside,
|
||||
sendUserToast,
|
||||
logoutWithRedirect,
|
||||
getUser,
|
||||
refreshSuperadmin
|
||||
} from '../utils'
|
||||
import { onMount } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import {
|
||||
faScroll,
|
||||
faPlay,
|
||||
faWallet,
|
||||
faEye,
|
||||
faChevronDown,
|
||||
faChevronRight,
|
||||
faChevronLeft,
|
||||
faBookOpen,
|
||||
faCubes,
|
||||
faCalendar,
|
||||
faRobot,
|
||||
faChevronDown,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faCog,
|
||||
faUser,
|
||||
faCrown,
|
||||
faCubes,
|
||||
faEye,
|
||||
faPlay,
|
||||
faRobot,
|
||||
faScroll,
|
||||
faUser,
|
||||
faUsersCog,
|
||||
faWallet,
|
||||
faWind
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { SvelteToast } from '@zerodevx/svelte-toast'
|
||||
import { faDiscord, faGithub, faPython } from '@fortawesome/free-brands-svg-icons'
|
||||
import { page } from '$app/stores'
|
||||
import { onMount } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import '../app.css'
|
||||
import { OpenAPI } from '../gen'
|
||||
import {
|
||||
superadmin,
|
||||
usernameStore,
|
||||
@@ -40,22 +29,10 @@
|
||||
usersWorkspaceStore,
|
||||
workspaceStore
|
||||
} from '../stores'
|
||||
import { goto } from '$app/navigation'
|
||||
import { clickOutside, logout } from '../utils'
|
||||
|
||||
OpenAPI.WITH_CREDENTIALS = true
|
||||
|
||||
// Default toast options
|
||||
const toastOptions = {
|
||||
duration: 4000, // duration of progress bar tween to the `next` value
|
||||
initial: 1, // initial progress bar value
|
||||
next: 0, // next progress value
|
||||
pausable: false, // pause progress bar tween on mouse hover
|
||||
dismissable: true, // allow dismiss with close button
|
||||
reversed: false, // insert new toast to bottom of stack
|
||||
intro: { x: 256 }, // toast intro fly animation settings
|
||||
theme: {} // css var overrides
|
||||
}
|
||||
|
||||
let menuOpen = false
|
||||
let workspacePickerOpen = false
|
||||
let isMobile = false
|
||||
@@ -79,63 +56,7 @@
|
||||
workspacePickerOpen = false
|
||||
}
|
||||
|
||||
async function loadUserInfo() {
|
||||
refreshSuperadmin()
|
||||
|
||||
if (!$usersWorkspaceStore) {
|
||||
try {
|
||||
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
|
||||
} catch {}
|
||||
}
|
||||
if ($usersWorkspaceStore) {
|
||||
if (!$workspaceStore) {
|
||||
workspaceStore.set(localStorage.getItem('workspace')?.toString())
|
||||
}
|
||||
if ($workspaceStore && $usernameStore) {
|
||||
await getUser($workspaceStore)
|
||||
} else if ($superadmin) {
|
||||
console.log('You are a superadmin, you can go wherever you please')
|
||||
} else {
|
||||
goto('/user/workspaces')
|
||||
}
|
||||
} else {
|
||||
logoutWithRedirect($page.url.pathname)
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($workspaceStore) {
|
||||
localStorage.setItem('workspace', $workspaceStore)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadUserInfo()
|
||||
window.onunhandledrejection = (e) => {
|
||||
if (e.reason && e.reason.message) {
|
||||
if (
|
||||
['Model not found', 'Connection is disposed.', 'Connection got disposed.'].includes(
|
||||
e.reason.message
|
||||
)
|
||||
) {
|
||||
// monaco editor promise cancelation
|
||||
console.log('caught expected error')
|
||||
} else {
|
||||
if (e.reason.status == '401') {
|
||||
sendUserToast('Logged out after a request was unauthorized', true)
|
||||
logout($page.url.pathname)
|
||||
} else {
|
||||
let message = `${e.reason?.message}: ${e.reason?.body ?? ''}`
|
||||
sendUserToast(message, true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('unexpected error ignored', e)
|
||||
}
|
||||
e.preventDefault()
|
||||
return false
|
||||
}
|
||||
|
||||
isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
|
||||
//Mobile
|
||||
isCollapsed = isMobile
|
||||
@@ -425,7 +346,6 @@
|
||||
: 'pl-44'} pr-8 flex h-full max-w-screen flex-col items-center"
|
||||
>
|
||||
<slot />
|
||||
<SvelteToast {toastOptions} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -436,10 +356,4 @@
|
||||
.menu-link {
|
||||
@apply flex flex-row h-10 transform hover:translate-x-1 transition-transform ease-in duration-200 text-gray-200 hover:text-white;
|
||||
}
|
||||
|
||||
:root {
|
||||
--toastBackground: #eff6ff;
|
||||
--toastBarBackground: #eff6ff;
|
||||
--toastColor: #123456;
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,4 @@
|
||||
<script lang="ts">
|
||||
import Fuse from 'fuse.js'
|
||||
import { ScriptService } from '../gen'
|
||||
import type { Script } from '../gen'
|
||||
|
||||
import { sendUserToast, groupBy, truncateHash, canWrite } from '../utils'
|
||||
import Icon from 'svelte-awesome'
|
||||
import {
|
||||
faArchive,
|
||||
faCalendarAlt,
|
||||
@@ -15,18 +9,22 @@
|
||||
faPlus,
|
||||
faShare
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import Dropdown from './components/Dropdown.svelte'
|
||||
import PageHeader from './components/PageHeader.svelte'
|
||||
import Modal from './components/Modal.svelte'
|
||||
import Tooltip from './components/Tooltip.svelte'
|
||||
import ShareModal from './components/ShareModal.svelte'
|
||||
import SharedBadge from './components/SharedBadge.svelte'
|
||||
import { superadmin, usernameStore, userStore, workspaceStore } from '../stores'
|
||||
import CenteredPage from './components/CenteredPage.svelte'
|
||||
import Tabs from './components/Tabs.svelte'
|
||||
import Fuse from 'fuse.js'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { slide } from 'svelte/transition'
|
||||
import type { Script } from '../gen'
|
||||
import { ScriptService } from '../gen'
|
||||
import { superadmin, usernameStore, userStore, workspaceStore } from '../stores'
|
||||
import { canWrite, groupBy, sendUserToast, truncateHash } from '../utils'
|
||||
import Badge from './components/Badge.svelte'
|
||||
import CenteredPage from './components/CenteredPage.svelte'
|
||||
import Dropdown from './components/Dropdown.svelte'
|
||||
import Modal from './components/Modal.svelte'
|
||||
import PageHeader from './components/PageHeader.svelte'
|
||||
import SharedBadge from './components/SharedBadge.svelte'
|
||||
import ShareModal from './components/ShareModal.svelte'
|
||||
import Tabs from './components/Tabs.svelte'
|
||||
import Tooltip from './components/Tooltip.svelte'
|
||||
|
||||
type Tab = 'all' | 'personal' | 'groups' | 'shared' | 'community'
|
||||
type Section = [string, ScriptW[]]
|
||||
|
||||
@@ -1,68 +1,3 @@
|
||||
<script lang="ts">
|
||||
import { SvelteToast } from '@zerodevx/svelte-toast'
|
||||
import { logout, refreshSuperadmin, sendUserToast } from '../../utils'
|
||||
import { onMount } from 'svelte'
|
||||
import { page } from '$app/stores'
|
||||
import { superadmin, userStore, workspaceStore } from '../../stores'
|
||||
|
||||
// Default options
|
||||
const toastOptions = {
|
||||
duration: 4000, // duration of progress bar tween to the `next` value
|
||||
initial: 1, // initial progress bar value
|
||||
next: 0, // next progress value
|
||||
pausable: false, // pause progress bar tween on mouse hover
|
||||
dismissable: true, // allow dismiss with close button
|
||||
reversed: false, // insert new toast to bottom of stack
|
||||
intro: { x: 256 }, // toast intro fly animation settings
|
||||
theme: {} // css var overrides
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($workspaceStore) {
|
||||
localStorage.setItem('workspace', $workspaceStore)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.onunhandledrejection = (e) => {
|
||||
e.preventDefault()
|
||||
if (e.reason && e.reason.message) {
|
||||
if (
|
||||
['Model not found', 'Connection is disposed.', 'Connection got disposed.'].includes(
|
||||
e.reason.message
|
||||
)
|
||||
) {
|
||||
// monaco editor promise cancelation
|
||||
console.log('caught expected error')
|
||||
}
|
||||
if (e.reason.status == '401') {
|
||||
sendUserToast('Logged out after a request was unauthorized', true)
|
||||
logout($page.url.pathname)
|
||||
} else {
|
||||
let message = `${e.reason?.message}: ${e.reason?.body ?? ''}`
|
||||
sendUserToast(message, true)
|
||||
}
|
||||
} else {
|
||||
console.log('unexpected error ignored', e)
|
||||
}
|
||||
e.preventDefault()
|
||||
return false
|
||||
}
|
||||
workspaceStore.set(undefined)
|
||||
userStore.set(undefined)
|
||||
refreshSuperadmin()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen antialiased text-gray-900">
|
||||
<slot />
|
||||
<SvelteToast {toastOptions} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--toastBackground: #eff6ff;
|
||||
--toastBarBackground: #eff6ff;
|
||||
--toastColor: #123456;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
|
||||
import { UserService } from '../../gen'
|
||||
import { refreshSuperadmin, sendUserToast } from '../../utils'
|
||||
import { page } from '$app/stores'
|
||||
import { userStore, usersWorkspaceStore, workspaceStore } from '../../stores'
|
||||
import CenteredModal from './CenteredModal.svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { onMount } from 'svelte'
|
||||
import { UserService, WorkspaceService } from '../../gen'
|
||||
import { userStore, usersWorkspaceStore, workspaceStore } from '../../stores'
|
||||
import { getUser, refreshSuperadmin, sendUserToast } from '../../utils'
|
||||
import CenteredModal from './CenteredModal.svelte'
|
||||
|
||||
let email = $page.url.searchParams.get('email') ?? ''
|
||||
let password = $page.url.searchParams.get('password') ?? ''
|
||||
@@ -20,13 +18,20 @@
|
||||
|
||||
async function login(): Promise<void> {
|
||||
try {
|
||||
await UserService.login({
|
||||
requestBody: {
|
||||
email: email,
|
||||
password
|
||||
}
|
||||
})
|
||||
const requestBody = {
|
||||
email,
|
||||
password
|
||||
}
|
||||
|
||||
await UserService.login({ requestBody })
|
||||
|
||||
// Once logged in, we can fetch the workspaces
|
||||
$usersWorkspaceStore = await WorkspaceService.listUserWorkspaces()
|
||||
// And the actual user
|
||||
$userStore = await getUser($workspaceStore!)
|
||||
// Finally, we check whether the user is a superadmin
|
||||
refreshSuperadmin()
|
||||
|
||||
if (rd) {
|
||||
goto(decodeURI(rd))
|
||||
} else {
|
||||
@@ -39,30 +44,20 @@
|
||||
|
||||
function handleKeyUp(event: KeyboardEvent) {
|
||||
const key = event.key || event.keyCode
|
||||
|
||||
if (key === 13 || key === 'Enter') {
|
||||
event.preventDefault()
|
||||
login()
|
||||
}
|
||||
}
|
||||
|
||||
async function redirectIfLoggedIn() {
|
||||
try {
|
||||
await UserService.getCurrentEmail()
|
||||
goto('/')
|
||||
} catch {
|
||||
usersWorkspaceStore.set(undefined)
|
||||
workspaceStore.set(undefined)
|
||||
userStore.set(undefined)
|
||||
}
|
||||
if (error) {
|
||||
sendUserToast(error, true)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(redirectIfLoggedIn, 1000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- Enable submit form on enter -->
|
||||
<CenteredModal subtitle={error}>
|
||||
<CenteredModal>
|
||||
<div class="justify-center text-center flex flex-col">
|
||||
<span class="text-xs text-gray-600">Currently only signup through Github is supported</span>
|
||||
<a rel="external" href="/api/oauth/login/github"
|
||||
|
||||
+21
-2
@@ -1,5 +1,6 @@
|
||||
import { writable, derived, readable } from 'svelte/store'
|
||||
import { browser } from '$app/env'
|
||||
import type { Readable } from 'svelte/store'
|
||||
import { derived, writable } from 'svelte/store'
|
||||
import type { UserWorkspaceList } from './gen'
|
||||
|
||||
export interface UserExt {
|
||||
@@ -10,9 +11,12 @@ export interface UserExt {
|
||||
groups: string[]
|
||||
pgroups: string[]
|
||||
}
|
||||
let persistedWorkspace = browser && localStorage.getItem('workspace')
|
||||
|
||||
export const userStore = writable<UserExt | undefined>(undefined)
|
||||
export const workspaceStore = writable<string | undefined>(undefined)
|
||||
export let workspaceStore = writable<string | undefined>(
|
||||
persistedWorkspace ? String(persistedWorkspace) : undefined
|
||||
)
|
||||
export const usersWorkspaceStore = writable<UserWorkspaceList | undefined>(undefined)
|
||||
export const usernameStore: Readable<string | undefined> = derived(
|
||||
[usersWorkspaceStore, workspaceStore],
|
||||
@@ -21,3 +25,18 @@ export const usernameStore: Readable<string | undefined> = derived(
|
||||
}
|
||||
)
|
||||
export const superadmin = writable<String | false | undefined>(undefined)
|
||||
|
||||
if (browser) {
|
||||
workspaceStore.subscribe((workspace) => {
|
||||
if (workspace) {
|
||||
localStorage.setItem('workspace', String(workspace))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function clearStores(): void {
|
||||
userStore.set(undefined)
|
||||
workspaceStore.set(undefined)
|
||||
usersWorkspaceStore.set(undefined)
|
||||
superadmin.set(undefined)
|
||||
}
|
||||
|
||||
+15
-18
@@ -1,10 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { toast } from '@zerodevx/svelte-toast'
|
||||
import { CancelablePromise, UserService } from './gen'
|
||||
import { superadmin, userStore, workspaceStore } from './stores'
|
||||
import type { UserExt } from './stores'
|
||||
import { get } from 'svelte/store'
|
||||
import { goto } from '$app/navigation'
|
||||
import { toast } from '@zerodevx/svelte-toast'
|
||||
import { get } from 'svelte/store'
|
||||
import { CancelablePromise, UserService, type User } from './gen'
|
||||
import { clearStores, superadmin, userStore, workspaceStore, type UserExt } from './stores'
|
||||
|
||||
export function isToday(someDate: Date): boolean {
|
||||
const today = new Date()
|
||||
@@ -76,14 +75,7 @@ export function truncateHash(hash: string): string {
|
||||
async function loadStore(workspace: string): Promise<UserExt | undefined> {
|
||||
try {
|
||||
const user = await UserService.whoami({ workspace })
|
||||
const nuser = {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
created_at: user.created_at,
|
||||
is_admin: user.is_admin,
|
||||
groups: user.groups!,
|
||||
pgroups: user.groups!.map((x) => `g/${x}`)
|
||||
}
|
||||
const nuser = mapUserToUserExt(user)
|
||||
userStore.set(nuser)
|
||||
return nuser
|
||||
} catch (error) {
|
||||
@@ -151,7 +143,7 @@ export async function refreshSuperadmin(): Promise<void> {
|
||||
|
||||
export async function logout(logoutMessage?: string): Promise<void> {
|
||||
try {
|
||||
superadmin.set(undefined)
|
||||
clearStores()
|
||||
goto(`/user/login${logoutMessage ? '?error=' + encodeURIComponent(logoutMessage) : ''}`)
|
||||
await UserService.logout()
|
||||
sendUserToast('you have been logged out')
|
||||
@@ -265,10 +257,7 @@ export function removeKeysWithEmptyValues(obj: any): any {
|
||||
}
|
||||
|
||||
export function allTrue(dict: { [id: string]: boolean }): boolean {
|
||||
for (let v of Object.values(dict)) {
|
||||
if (!v) return false
|
||||
}
|
||||
return true
|
||||
return Object.values(dict).every(Boolean)
|
||||
}
|
||||
|
||||
export function forLater(scheduledString: string): boolean {
|
||||
@@ -331,3 +320,11 @@ export function truncateRev(s: string, n: number, prefix: string = '...'): strin
|
||||
export function isString(value: any) {
|
||||
return typeof value === 'string' || value instanceof String
|
||||
}
|
||||
|
||||
export function mapUserToUserExt(user: User): UserExt {
|
||||
return {
|
||||
...user,
|
||||
groups: user.groups!,
|
||||
pgroups: user.groups!.map((x) => `g/${x}`)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user