Implement Google OAuth linking functionality. Update backend to handle linking existing accounts with Google, including state token management. Enhance frontend to support linking process, including new UI components for user input and feedback. Update mobile app to handle OAuth callbacks and integrate linking features. Refactor related services and controllers for improved error handling and user experience.

This commit is contained in:
Torsten Schulz (local)
2026-05-15 08:27:36 +02:00
parent 95b611fd04
commit c16d2a6e4d
20 changed files with 768 additions and 127 deletions

View File

@@ -1,6 +1,24 @@
<template>
<div class="oauth-callback">
<div class="loading-container">
<div v-if="pendingToken" class="link-container">
<h2>Google-Konto verknüpfen</h2>
<p>
Für {{ googleEmail }} existiert bereits ein Account. Melden Sie sich mit dem bestehenden Account an,
um ihn mit Google zu verknüpfen.
</p>
<form @submit.prevent="linkExistingAccount" class="link-form">
<label>E-Mail-Adresse</label>
<input v-model="email" type="email" required>
<label>Passwort</label>
<input v-model="password" type="password" required>
<button class="btn btn-primary" :disabled="linking">
{{ linking ? 'Wird verknüpft...' : 'Bestehenden Account verknüpfen' }}
</button>
<button type="button" class="btn" @click="router.push('/login')">Abbrechen</button>
</form>
<p v-if="errorMessage" class="error">{{ errorMessage }}</p>
</div>
<div v-else class="loading-container">
<h2>{{ status }}</h2>
<div class="spinner"></div>
</div>
@@ -11,16 +29,60 @@
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '../stores/authStore'
import { API_BASE_URL } from '@/config/api'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const API_URL = API_BASE_URL
const status = ref('Authentifizierung läuft...')
const pendingToken = ref('')
const googleEmail = ref('')
const email = ref('')
const password = ref('')
const linking = ref(false)
const errorMessage = ref('')
async function finishLogin(token) {
authStore.saveToken(token)
await authStore.fetchCurrentUser()
status.value = 'Login erfolgreich! Sie werden weitergeleitet...'
setTimeout(() => {
router.push('/')
}, 1000)
}
async function linkExistingAccount() {
try {
linking.value = true
errorMessage.value = ''
const response = await fetch(`${API_URL}/auth/oauth/link-existing`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
pendingToken: pendingToken.value,
email: email.value,
password: password.value
})
})
const result = await response.json()
if (!response.ok || !result.success || !result.token) {
throw new Error(result.error || 'Verknüpfung fehlgeschlagen')
}
pendingToken.value = ''
await finishLogin(result.token)
} catch (error) {
errorMessage.value = error.message || 'Verknüpfung fehlgeschlagen'
} finally {
linking.value = false
}
}
onMounted(async () => {
const token = route.query.token
const error = route.query.error
const pending = route.query.pending
if (error) {
status.value = 'OAuth-Login fehlgeschlagen'
@@ -30,19 +92,16 @@ onMounted(async () => {
return
}
if (pending) {
pendingToken.value = pending
googleEmail.value = route.query.email || ''
email.value = route.query.email || ''
return
}
if (token) {
try {
// Token speichern
authStore.saveToken(token)
// Benutzer-Daten laden
await authStore.fetchCurrentUser()
status.value = 'Login erfolgreich! Sie werden weitergeleitet...'
setTimeout(() => {
router.push('/')
}, 1000)
await finishLogin(token)
} catch (err) {
status.value = 'Fehler beim Login'
setTimeout(() => {
@@ -71,6 +130,34 @@ onMounted(async () => {
text-align: center;
}
.link-container {
background: #fafafa;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 24px;
width: min(460px, calc(100vw - 32px));
}
.link-container h2 {
margin-top: 0;
}
.link-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.link-form input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
.error {
color: #d9534f;
}
.loading-container h2 {
color: #333;
font-weight: 500;
@@ -94,4 +181,3 @@ onMounted(async () => {
</style>

View File

@@ -87,6 +87,13 @@
</select>
</div>
<div class="form-group">
<label>Google-Anmeldung</label>
<button type="button" class="btn" @click="linkGoogle" :disabled="loading">
Mit Google-Konto verknüpfen
</button>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" :disabled="loading">
{{ loading ? 'Wird gespeichert...' : 'Einstellungen speichern' }}
@@ -216,6 +223,29 @@ async function saveProfile() {
}
}
async function linkGoogle() {
try {
loading.value = true
const response = await fetch(`${API_URL}/auth/google/link-url`, {
method: 'POST',
headers: {
...authStore.getAuthHeaders(),
'Content-Type': 'application/json'
},
body: JSON.stringify({ platform: 'web' })
})
const result = await response.json()
if (!response.ok || !result.url) {
throw new Error(result.error || 'Google-Verknüpfung konnte nicht gestartet werden')
}
window.location.href = result.url
} catch (error) {
await alert(`Fehler: ${error.message}`, 'Fehler')
} finally {
loading.value = false
}
}
// Initiales Laden
onMounted(async () => {
await Promise.all([
@@ -325,4 +355,3 @@ onMounted(async () => {
box-shadow: 0 4px 8px rgba(76, 175, 80, 0.4);
}
</style>