feat: add frontend OAuth authorize route and component

Add /oauth/authorize frontend route with OAuthAuthorize.vue that
handles the OAuth authorization flow: validates required query params,
calls the API to generate an authorization code, and redirects to the
callback URI. Authentication is handled by the standard router guard.
This commit is contained in:
kolaente 2026-03-26 16:32:23 +01:00 committed by kolaente
parent e5987acf80
commit 0471f8a729
5 changed files with 86 additions and 1 deletions

View File

@ -54,6 +54,7 @@
"authenticating": "Authenticating…",
"openIdStateError": "State does not match, refusing to continue!",
"openIdGeneralError": "An error occurred while authenticating against the third party.",
"oauthMissingParams": "Missing required OAuth parameters: {params}",
"logout": "Logout",
"emailInvalid": "Please enter a valid email address.",
"usernameRequired": "Please provide a username.",

View File

@ -394,6 +394,11 @@ const router = createRouter({
name: 'openid.auth',
component: OpenIdAuth,
},
{
path: '/oauth/authorize',
name: 'oauth.authorize',
component: () => import('@/views/user/OAuthAuthorize.vue'),
},
{
path: '/about',
name: 'about',

View File

@ -217,6 +217,7 @@ async function submit() {
try {
await authStore.login(credentials)
authStore.setNeedsTotpPasscode(false)
redirectIfSaved()
} catch (e) {
if (e.response?.data.code === 1017 && !credentials.totpPasscode) {

View File

@ -0,0 +1,77 @@
<template>
<div>
<Message
v-if="errorMessage"
variant="danger"
>
{{ errorMessage }}
</Message>
<Message v-if="loading">
{{ $t('user.auth.authenticating') }}
</Message>
</div>
</template>
<script setup lang="ts">
import {ref, onMounted} from 'vue'
import {useRoute} from 'vue-router'
import {useI18n} from 'vue-i18n'
import {getErrorText} from '@/message'
import Message from '@/components/misc/Message.vue'
import {AuthenticatedHTTPFactory} from '@/helpers/fetcher'
defineOptions({name: 'OAuthAuthorize'})
const {t} = useI18n({useScope: 'global'})
const route = useRoute()
const loading = ref(true)
const errorMessage = ref('')
const requiredParams = [
'response_type',
'client_id',
'redirect_uri',
'code_challenge',
'code_challenge_method',
] as const
async function authorize() {
// Validate required query parameters
const missing = requiredParams.filter(p => !route.query[p])
if (missing.length > 0) {
errorMessage.value = t('user.auth.oauthMissingParams', {params: missing.join(', ')})
loading.value = false
return
}
try {
const HTTP = AuthenticatedHTTPFactory()
const response = await HTTP.post('oauth/authorize', {
response_type: route.query.response_type,
client_id: route.query.client_id,
redirect_uri: route.query.redirect_uri,
state: route.query.state,
code_challenge: route.query.code_challenge,
code_challenge_method: route.query.code_challenge_method,
})
const {code, redirect_uri, state} = response.data
const redirectUrl = new URL(redirect_uri)
redirectUrl.searchParams.set('code', code)
if (state) {
redirectUrl.searchParams.set('state', state)
}
window.location.href = redirectUrl.toString()
} catch (e) {
errorMessage.value = getErrorText(e)
} finally {
loading.value = false
}
}
onMounted(() => authorize())
</script>

View File

@ -80,8 +80,9 @@ async function authenticateWithCode() {
provider: route.params.provider,
code: route.query.code,
})
redirectIfSaved()
} catch(e) {
} catch (e) {
errorMessage.value = getErrorText(e)
} finally {
localStorage.removeItem('authenticating')