add useApi fetcher + handle login + route guards

This commit is contained in:
axolotle 2023-07-20 17:36:08 +02:00
parent 0491bd638d
commit 585109754c
6 changed files with 101 additions and 53 deletions

37
composables/api.ts Normal file
View file

@ -0,0 +1,37 @@
import type { AsyncData } from 'nuxt/app'
import type { FetchError } from 'ofetch'
export async function useApi<T>(
path: string,
{
method = 'GET',
body = undefined,
}: {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
body?: Record<string, any>
} = {},
): Promise<AsyncData<T, FetchError | null>> {
const host = window.location.hostname
const apiEndpoint =
'https://' +
(process.dev ? useRuntimeConfig().public.apiIp || host : host) +
'/yunohost/portalapi'
const result = await useFetch(apiEndpoint + path, {
headers: {
'Content-Type': 'application/json',
'X-Requested-With': '',
},
method,
credentials: 'include',
body,
})
const error = result.error.value
if (error && error.statusCode === 401) {
useIsLoggedIn().value = false
navigateTo('/login')
}
return result as AsyncData<T, FetchError | null>
}

View file

@ -1,2 +1 @@
export const useApiEndpoint = () => "https://" + window.location.hostname + '/yunohost/portalapi'
export const useIsLoggedIn = () => useState<boolean>('isLoggedIn', () => false) export const useIsLoggedIn = () => useState<boolean>('isLoggedIn', () => false)

12
middleware/auth.global.ts Normal file
View file

@ -0,0 +1,12 @@
import { useIsLoggedIn } from '@/composables/states'
export default defineNuxtRouteMiddleware((to, from) => {
const isLoggedIn = useIsLoggedIn()
if (to.name === 'login' && isLoggedIn.value) {
return navigateTo('/')
}
if (!isLoggedIn.value) {
navigateTo('/login')
}
})

View file

@ -12,4 +12,9 @@ export default defineNuxtConfig({
'Source+Sans+3': [500, 900], 'Source+Sans+3': [500, 900],
}, },
}, },
runtimeConfig: {
public: {
apiIp: '', // overridden by NUXT_PUBLIC_API_IP environment variable
},
},
}) })

View file

@ -1,20 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
const apiEndpoint = useApiEndpoint()
const isLoggedIn = useIsLoggedIn() const isLoggedIn = useIsLoggedIn()
let me = {}
const { data, error } = await useFetch(apiEndpoint + '/me', { const { data } = await useApi<{
credentials: 'include', username: string
}) fullname: string
mail: string
'mail-aliases': string[]
'mail-forward': string[]
groups: string[]
apps: Record<string, { label: string; url: string }>
}>('/me')
if (error.value && error.value.statusCode >= 400) { const me = computed(() => {
isLoggedIn.value = false // FIXME : not confident this actually mutates the state ...
// FIXME : we probably want different handlings between 401/403, 500, 502, ...
await navigateTo('/login')
} else {
me = data.value
Object.keys(me.apps).forEach((appId) => {
const appTileColors = [ const appTileColors = [
'red', 'red',
'orange', 'orange',
@ -27,30 +24,34 @@ if (error.value && error.value.statusCode >= 400) {
'purple', 'purple',
'rose', 'rose',
] ]
const randomColorNumber = if (!data.value) return
parseInt(me.apps[appId].label, 36) % appTileColors.length return {
me.apps[appId].color = appTileColors[randomColorNumber] ...data.value,
}) apps: Object.entries(data.value.apps).map(([id, app]) => {
} return {
...app,
id,
color: appTileColors[parseInt(app.label, 36) % appTileColors.length],
}
}),
}
})
async function logout() { async function logout() {
const { error } = await useFetch(apiEndpoint + '/logout', { const { error } = await useApi('/logout')
method: 'GET',
credentials: 'include',
})
if (error.value && error.value.statusCode !== 200) { if (!error.value) {
// FIXME : display an error or something
} else {
// FIXME : meh, turns out the cookie is still valid after successfully calling the route for some reason ... !? // FIXME : meh, turns out the cookie is still valid after successfully calling the route for some reason ... !?
isLoggedIn.value = false // FIXME : not confident this actually mutates the state ... isLoggedIn.value = false
await navigateTo('/login') await navigateTo('/login')
} else {
// FIXME : display an error or something
} }
} }
</script> </script>
<template> <template>
<div> <div v-if="me">
<div class="flex flex-row items-center min-w-full"> <div class="flex flex-row items-center min-w-full">
<span class="flex-none pr-5"> <span class="flex-none pr-5">
<Icon name="mdi:account-circle" size="5em" class="text-gray-500" /> <Icon name="mdi:account-circle" size="5em" class="text-gray-500" />
@ -73,7 +74,7 @@ async function logout() {
</div> </div>
<div id="apps" class="p-10"> <div id="apps" class="p-10">
<div v-if="Object.keys(me.apps).length == 0"> <div v-if="!me.apps.length">
<em class="text-gray-400" <em class="text-gray-400"
>There is no app to list here, either because no web app yet is >There is no app to list here, either because no web app yet is
installed on the server, or because you don't have access to any. installed on the server, or because you don't have access to any.
@ -81,7 +82,7 @@ async function logout() {
> >
</div> </div>
<ul class="flex space-x-4"> <ul v-else class="flex space-x-4">
<!-- NB : because of the usage of dynamic colors, gotta force tailwind to expose those, cf 'safelisting' --> <!-- NB : because of the usage of dynamic colors, gotta force tailwind to expose those, cf 'safelisting' -->
<li <li
v-for="app in me.apps" v-for="app in me.apps"

View file

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
const apiEndpoint = useApiEndpoint()
const isLoggedIn = useIsLoggedIn() const isLoggedIn = useIsLoggedIn()
const form = { const form = {
@ -8,21 +7,16 @@ const form = {
} }
async function login() { async function login() {
const { error } = await useFetch(apiEndpoint + '/login', { const { error } = await useApi('/login', {
headers: {
'Content-Type': 'application/json',
'X-Requested-With': '',
},
method: 'POST', method: 'POST',
credentials: 'include',
body: { credentials: form.username + ':' + form.password }, body: { credentials: form.username + ':' + form.password },
}) })
if (error.value && error.value.statusCode !== 200) { if (!error.value) {
// FIXME : display an error or something isLoggedIn.value = true
} else {
isLoggedIn.value = true // FIXME : not confident this actually mutates the state ...
await navigateTo('/') await navigateTo('/')
} else {
// FIXME : display an error or something
} }
} }
</script> </script>