mirror of
https://github.com/YunoHost/yunohost-portal.git
synced 2024-09-03 20:06:23 +02:00
add useApi fetcher + handle login + route guards
This commit is contained in:
parent
0491bd638d
commit
585109754c
6 changed files with 101 additions and 53 deletions
37
composables/api.ts
Normal file
37
composables/api.ts
Normal 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>
|
||||||
|
}
|
|
@ -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
12
middleware/auth.global.ts
Normal 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')
|
||||||
|
}
|
||||||
|
})
|
|
@ -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
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,56 +1,57 @@
|
||||||
<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')
|
||||||
|
|
||||||
|
const me = computed(() => {
|
||||||
|
const appTileColors = [
|
||||||
|
'red',
|
||||||
|
'orange',
|
||||||
|
'yellow',
|
||||||
|
'lime',
|
||||||
|
'green',
|
||||||
|
'teal',
|
||||||
|
'indigo',
|
||||||
|
'sky',
|
||||||
|
'purple',
|
||||||
|
'rose',
|
||||||
|
]
|
||||||
|
if (!data.value) return
|
||||||
|
return {
|
||||||
|
...data.value,
|
||||||
|
apps: Object.entries(data.value.apps).map(([id, app]) => {
|
||||||
|
return {
|
||||||
|
...app,
|
||||||
|
id,
|
||||||
|
color: appTileColors[parseInt(app.label, 36) % appTileColors.length],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (error.value && error.value.statusCode >= 400) {
|
|
||||||
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 = [
|
|
||||||
'red',
|
|
||||||
'orange',
|
|
||||||
'yellow',
|
|
||||||
'lime',
|
|
||||||
'green',
|
|
||||||
'teal',
|
|
||||||
'indigo',
|
|
||||||
'sky',
|
|
||||||
'purple',
|
|
||||||
'rose',
|
|
||||||
]
|
|
||||||
const randomColorNumber =
|
|
||||||
parseInt(me.apps[appId].label, 36) % appTileColors.length
|
|
||||||
me.apps[appId].color = appTileColors[randomColorNumber]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue