Skip to content

Commit

Permalink
Add basic auth UI (#16411)
Browse files Browse the repository at this point in the history
Co-authored-by: Chris White <[email protected]>
  • Loading branch information
aaazzam and cicdw authored Dec 17, 2024
1 parent 3174ce0 commit 53a83eb
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 21 deletions.
3 changes: 1 addition & 2 deletions ui/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
<template>
<div class="app" data-teleport-target="app">
<suspense>
<AppRouterView />
<router-view />
</suspense>
</div>
</template>

<script lang="ts" setup>
import { useColorTheme } from '@prefecthq/prefect-design'
import AppRouterView from '@/pages/AppRouterView.vue'
useColorTheme()
</script>
34 changes: 26 additions & 8 deletions ui/src/pages/AppRouterView.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="app-router-view">
<template v-if="!media.lg">
<template v-if="!media.lg && !$route.meta.public">
<PGlobalSidebar class="app-router-view__mobile-menu">
<template #upper-links>
<router-link :to="appRoutes.root()">
Expand All @@ -12,8 +12,8 @@
</template>
</PGlobalSidebar>
</template>
<ContextSidebar v-if="showMenu" class="app-router-view__sidebar" @click="close" />
<router-view class="app-router-view__view">
<ContextSidebar v-if="showMenu && !$route.meta.public" class="app-router-view__sidebar" @click="close" />
<router-view :class="['app-router-view__view', { 'app-router-view__view--public': $route.meta.public }]">
<template #default="{ Component }">
<transition name="app-router-view-fade" mode="out-in">
<component :is="Component" />
Expand All @@ -32,7 +32,7 @@
import { useApiConfig } from '@/compositions/useApiConfig'
import { useCreateCan } from '@/compositions/useCreateCan'
import { useMobileMenuOpen } from '@/compositions/useMobileMenuOpen'
import { routes as appRoutes } from '@/router'
import router, { routes as appRoutes } from '@/router'
import { createPrefectApi, prefectApiKey } from '@/utilities/api'
import { canKey } from '@/utilities/permissions'
Expand All @@ -46,10 +46,21 @@
provide(prefectApiKey, api)
provide(workspaceApiKey, api)
provide(workspaceRoutesKey, routes)
api.health.isHealthy().then(healthy => {
if (!healthy) {
showToast(`Can't connect to Server API at ${config.baseUrl}. Check that it's accessible from your machine.`, 'error', { timeout: false })
api.admin.authCheck().then(authenticated => {
if (!authenticated) {
if (router.currentRoute.value.name !== 'login') {
showToast('Authentication failed.', 'error', { timeout: false })
}
router.push({
name: 'login',
query: { redirect: router.currentRoute.value.fullPath }
})
} else {
api.health.isHealthy().then(healthy => {
if (!healthy) {
showToast(`Can't connect to Server API at ${config.baseUrl}. Check that it's accessible from your machine.`, 'error', { timeout: false })
}
})
}
})
Expand Down Expand Up @@ -108,6 +119,13 @@
height: 100%;
}
.app-router-view__view--public { @apply
flex
items-center
justify-center;
grid-column: 1 / -1;
}
@screen lg {
.app-router-view {
--prefect-scroll-margin: theme('spacing.2');
Expand Down
62 changes: 62 additions & 0 deletions ui/src/pages/Unauthenticated.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<template>
<div class="flex items-center justify-center min-h-screen">
<div class="w-full max-w-[400px] p-8 m-4 bg-surface-raised rounded-lg shadow-lg">
<p-heading tag="h1" size="lg" class="mb-6 text-center text-default">
Login
</p-heading>
<form @submit.prevent="handleSubmit" class="flex flex-col gap-4">
<p-text-input
v-model="password"
type="password"
placeholder="Enter password"
:error="error"
autofocus
class="w-full"
/>
<p-button
type="submit"
:loading="loading"
class="w-full"
>
Login
</p-button>
</form>
</div>
</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { usePrefectApi } from '@/compositions/usePrefectApi'
const props = defineProps<{
redirect?: string
}>()
const password = ref('')
const loading = ref(false)
const error = ref('')
const router = useRouter()
const api = usePrefectApi()
const handleSubmit = async (): Promise<void> => {
if (loading.value) return
loading.value = true
error.value = ''
try {
localStorage.setItem('prefect-password', btoa(password.value))
router.push(props.redirect || '/')
} catch (e) {
localStorage.removeItem('prefect-password')
error.value = 'Invalid password'
} finally {
loading.value = false
}
}
</script>

<style>
</style>
34 changes: 25 additions & 9 deletions ui/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,24 +43,40 @@ const workspaceRoutes = createWorkspaceRouteRecords({
workPoolQueue: () => import('@/pages/WorkPoolQueue.vue'),
workPoolQueueCreate: () => import('@/pages/WorkPoolQueueCreate.vue'),
workPoolQueueEdit: () => import('@/pages/WorkPoolQueueEdit.vue'),

})

const routeRecords: AppRouteRecord[] = [
{
name: 'root',
path: '/',
redirect: routes.dashboard(),
children: workspaceRoutes,
component: (): RouteComponent => import('@/pages/AppRouterView.vue'),
children: [
{
name: 'root',
path: '',
redirect: routes.dashboard(),
children: [
...workspaceRoutes,
{
name: 'login',
path: '/login',
component: (): RouteComponent => import('@/pages/Unauthenticated.vue'),
meta: { public: true },
props: (route) => ({ redirect: route.query.redirect }),
},
]
},
{
name: 'settings',
path: 'settings',
component: (): RouteComponent => import('@/pages/Settings.vue'),
},
],
},
{
name: 'settings',
path: '/settings',
component: (): RouteComponent => import('@/pages/Settings.vue'),
},

{
path: '/:pathMatch(.*)*',
name: '404',
meta: { public: true },
component: (): RouteComponent => import('@/pages/404.vue'),
},
]
Expand Down
3 changes: 2 additions & 1 deletion ui/src/router/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { RouteLocationRaw, RouteRecordName, RouteRecordRaw } from 'vue-router'
export const routes = {
root: () => ({ name: 'root' }) as const,
404: () => ({ name: '404' }) as const,
settings: () => ({ name: 'settings' }) as const,
settings: () => ({ name: 'settings', }) as const,
login: () => ({ name: 'login', meta: { public: true } }) as const,
...createWorkspaceRoutes(),
}

Expand Down
11 changes: 10 additions & 1 deletion ui/src/services/adminApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,13 @@ export class AdminApi extends Api {
public async getVersion(): Promise<string> {
return await this.get<string>('/version').then(({ data }) => data)
}
}

public async authCheck(): Promise<boolean> {
try {
await this.getVersion()
return true
} catch {
return false
}
}
}
5 changes: 5 additions & 0 deletions ui/src/services/csrfTokenApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ export function setupCsrfInterceptor(csrfTokenApi: CreateActions<CsrfTokenApi>,
await csrfTokenApi.addCsrfHeaders(config)
}

const password = localStorage.getItem('prefect-password')
if (password) {
config.headers['Authorization'] = `Basic ${password}`
}

return config
})

Expand Down
7 changes: 7 additions & 0 deletions ui/src/utilities/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@ import { AdminApi } from '@/services/adminApi'
import { AutomationsApi } from '@/services/automationsApi'
import { CsrfTokenApi, setupCsrfInterceptor } from '@/services/csrfTokenApi'



// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createPrefectApi(config: PrefectConfig) {
const csrfTokenApi = createActions(new CsrfTokenApi(config))

function axiosInstanceSetupHook(axiosInstance: AxiosInstance): void {
setupCsrfInterceptor(csrfTokenApi, axiosInstance)

const password = localStorage.getItem('prefect-password')
if (password) {
axiosInstance.defaults.headers.common['Authorization'] = `Basic ${password}`
}
}

const workspaceApi = createApi(config, axiosInstanceSetupHook)
Expand Down

0 comments on commit 53a83eb

Please sign in to comment.