feat: add server selection UI for desktop OAuth login
Add a server selection screen matching the mobile app UX with Vikunja Cloud, Try the Demo, and Custom Server URL options. Extract all desktop login logic into a dedicated DesktopLogin component. Use the existing ApiConfig component for custom server URL input. Skip loading server config on startup to avoid showing motd/demo popups on the login screen.
This commit is contained in:
parent
dd7532a57a
commit
a12002de6d
|
|
@ -25,7 +25,7 @@
|
|||
>
|
||||
{{ title }}
|
||||
</h2>
|
||||
<ApiConfig v-if="showApiConfig" />
|
||||
<ApiConfig v-if="shouldShowApiConfig" />
|
||||
<Message
|
||||
v-if="motd !== ''"
|
||||
class="is-hidden-tablet mbe-4"
|
||||
|
|
@ -52,8 +52,9 @@ import ApiConfig from '@/components/misc/ApiConfig.vue'
|
|||
|
||||
import { useTitle } from '@/composables/useTitle'
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
import { isDesktopApp } from '@/helpers/desktopAuth'
|
||||
|
||||
withDefaults(
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
showApiConfig?: boolean;
|
||||
}>(),
|
||||
|
|
@ -61,6 +62,11 @@ withDefaults(
|
|||
showApiConfig: false,
|
||||
},
|
||||
)
|
||||
|
||||
const isDesktop = isDesktopApp()
|
||||
const hasStoredApiUrl = isDesktop && localStorage.getItem('API_URL') !== null
|
||||
const shouldShowApiConfig = computed(() => props.showApiConfig && (!isDesktop || hasStoredApiUrl))
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const motd = computed(() => configStore.motd)
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,12 @@
|
|||
"openIdStateError": "State does not match, refusing to continue!",
|
||||
"openIdGeneralError": "An error occurred while authenticating against the third party.",
|
||||
"oauthMissingParams": "Missing required OAuth parameters: {params}",
|
||||
"oauthRedirectedToApp": "You have been redirected to the app. You can close this tab now.",
|
||||
"desktopTryDemo": "Try the Demo",
|
||||
"desktopCustomServer": "Custom Server URL",
|
||||
"desktopCustomServerDescription": "Enter the URL of your Vikunja server to get started.",
|
||||
"desktopWaitingForAuth": "Waiting for authentication…",
|
||||
"desktopOAuthError": "Authentication failed: {error}",
|
||||
"logout": "Logout",
|
||||
"emailInvalid": "Please enter a valid email address.",
|
||||
"usernameRequired": "Please provide a username.",
|
||||
|
|
@ -156,8 +162,7 @@
|
|||
"tokenCreated": "Here is your new token: {token}",
|
||||
"wontSeeItAgain": "Write it down or save it securely — you will not be able to see it again.",
|
||||
"mustUseToken": "You need to create a CalDAV token to use CalDAV with any third-party client. Enter the token in the password field of your client.",
|
||||
"usernameIs": "Your username for CalDAV is: {0}",
|
||||
"apiTokenHint": "You can also use an API token with CalDAV permission. Create one in {link}."
|
||||
"usernameIs": "Your username for CalDAV is: {0}"
|
||||
},
|
||||
"avatar": {
|
||||
"title": "Avatar",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
|||
import ProjectModel from '@/models/project'
|
||||
import ProjectService from '@/services/project'
|
||||
import {checkAndSetApiUrl, ERROR_NO_API_URL, InvalidApiUrlProvidedError, NoApiUrlProvidedError} from '@/helpers/checkAndSetApiUrl'
|
||||
import {isDesktopApp} from '@/helpers/desktopAuth'
|
||||
|
||||
import {useMenuActive} from '@/composables/useMenuActive'
|
||||
|
||||
|
|
@ -146,6 +147,19 @@ export const useBaseStore = defineStore('base', () => {
|
|||
|
||||
async function loadApp() {
|
||||
try {
|
||||
if (isDesktopApp()) {
|
||||
// On desktop, ignore the default window.API_URL (set by index.html)
|
||||
// and only use a previously stored API URL from localStorage.
|
||||
const storedApiUrl = localStorage.getItem('API_URL')
|
||||
if (storedApiUrl) {
|
||||
window.API_URL = storedApiUrl
|
||||
await authStore.checkAuth()
|
||||
}
|
||||
await router.isReady()
|
||||
ready.value = true
|
||||
return
|
||||
}
|
||||
|
||||
await checkAndSetApiUrl(window.API_URL)
|
||||
await authStore.checkAuth()
|
||||
await router.isReady()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
<template>
|
||||
<div>
|
||||
<Message
|
||||
v-if="errorMessage"
|
||||
variant="danger"
|
||||
class="mbe-4"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</Message>
|
||||
<Message
|
||||
v-if="waitingForAuth"
|
||||
class="mbe-4"
|
||||
>
|
||||
{{ $t('user.auth.desktopWaitingForAuth') }}
|
||||
</Message>
|
||||
|
||||
<template v-if="hasStoredServer">
|
||||
<XButton
|
||||
:loading="waitingForAuth"
|
||||
class="is-fullwidth"
|
||||
@click="loginWithServer(window.API_URL)"
|
||||
>
|
||||
{{ $t('user.auth.login') }}
|
||||
</XButton>
|
||||
</template>
|
||||
<template v-else-if="showCustomServerInput">
|
||||
<p class="mbe-4">
|
||||
{{ $t('user.auth.desktopCustomServerDescription') }}
|
||||
</p>
|
||||
<ApiConfig
|
||||
:configure-open="true"
|
||||
@foundApi="loginWithServer"
|
||||
/>
|
||||
<div class="has-text-centered mbs-2">
|
||||
<a
|
||||
role="button"
|
||||
@click="showCustomServerInput = false"
|
||||
>
|
||||
{{ $t('misc.cancel') }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<XButton
|
||||
:loading="waitingForAuth"
|
||||
class="is-fullwidth mbe-2"
|
||||
@click="loginWithServer('https://app.vikunja.cloud')"
|
||||
>
|
||||
Vikunja Cloud
|
||||
</XButton>
|
||||
<XButton
|
||||
:loading="waitingForAuth"
|
||||
variant="secondary"
|
||||
class="is-fullwidth mbe-2"
|
||||
@click="loginWithServer('https://try.vikunja.io')"
|
||||
>
|
||||
{{ $t('user.auth.desktopTryDemo') }}
|
||||
</XButton>
|
||||
<XButton
|
||||
variant="secondary"
|
||||
class="is-fullwidth"
|
||||
@click="showCustomServerInput = true"
|
||||
>
|
||||
{{ $t('user.auth.desktopCustomServer') }}
|
||||
</XButton>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import Message from '@/components/misc/Message.vue'
|
||||
import ApiConfig from '@/components/misc/ApiConfig.vue'
|
||||
|
||||
import {getErrorText} from '@/message'
|
||||
import {startDesktopOAuthLogin, listenForDesktopOAuthTokens, listenForDesktopOAuthError} from '@/helpers/desktopAuth'
|
||||
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
||||
import {useRedirectToLastVisited} from '@/composables/useRedirectToLastVisited'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const authStore = useAuthStore()
|
||||
const {redirectIfSaved} = useRedirectToLastVisited()
|
||||
|
||||
const waitingForAuth = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const hasStoredServer = localStorage.getItem('API_URL') !== null
|
||||
const showCustomServerInput = ref(false)
|
||||
|
||||
listenForDesktopOAuthTokens(async (tokens) => {
|
||||
waitingForAuth.value = false
|
||||
try {
|
||||
await authStore.handleDesktopOAuthTokens(tokens)
|
||||
redirectIfSaved()
|
||||
} catch (e) {
|
||||
errorMessage.value = getErrorText(e)
|
||||
}
|
||||
})
|
||||
|
||||
listenForDesktopOAuthError((error) => {
|
||||
waitingForAuth.value = false
|
||||
errorMessage.value = t('user.auth.desktopOAuthError', {error})
|
||||
})
|
||||
|
||||
async function loginWithServer(serverUrl: string) {
|
||||
errorMessage.value = ''
|
||||
waitingForAuth.value = true
|
||||
try {
|
||||
await checkAndSetApiUrl(serverUrl)
|
||||
await startDesktopOAuthLogin(window.API_URL)
|
||||
} catch (e) {
|
||||
waitingForAuth.value = false
|
||||
errorMessage.value = getErrorText(e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -15,8 +15,11 @@
|
|||
>
|
||||
{{ errorMessage }}
|
||||
</Message>
|
||||
|
||||
<DesktopLogin v-if="isDesktop" />
|
||||
|
||||
<form
|
||||
v-if="localAuthEnabled || ldapAuthEnabled"
|
||||
v-if="!isDesktop && (localAuthEnabled || ldapAuthEnabled)"
|
||||
id="loginform"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
|
|
@ -106,7 +109,7 @@
|
|||
</form>
|
||||
|
||||
<div
|
||||
v-if="hasOpenIdProviders"
|
||||
v-if="!isDesktop && hasOpenIdProviders"
|
||||
class="mbs-4"
|
||||
>
|
||||
<XButton
|
||||
|
|
@ -131,10 +134,12 @@ import {useDebounceFn} from '@vueuse/core'
|
|||
import Message from '@/components/misc/Message.vue'
|
||||
import Password from '@/components/input/Password.vue'
|
||||
import FormField from '@/components/input/FormField.vue'
|
||||
import DesktopLogin from '@/views/user/DesktopLogin.vue'
|
||||
|
||||
import {getErrorText} from '@/message'
|
||||
import {redirectToProvider} from '@/helpers/redirectToProvider'
|
||||
import {useRedirectToLastVisited} from '@/composables/useRedirectToLastVisited'
|
||||
import {isDesktopApp} from '@/helpers/desktopAuth'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
|
@ -157,6 +162,7 @@ const openidConnect = computed(() => configStore.auth.openidConnect)
|
|||
const hasOpenIdProviders = computed(() => openidConnect.value.enabled && openidConnect.value.providers?.length > 0)
|
||||
|
||||
const isLoading = computed(() => authStore.isLoading)
|
||||
const isDesktop = isDesktopApp()
|
||||
|
||||
const confirmedEmailSuccess = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
|
@ -189,6 +195,7 @@ const validateUsernameField = useDebounceFn(() => {
|
|||
usernameValid.value = usernameRef.value?.value !== ''
|
||||
}, 100)
|
||||
|
||||
|
||||
const needsTotpPasscode = computed(() => authStore.needsTotpPasscode)
|
||||
const totpPasscode = ref<HTMLInputElement | null>(null)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue