security: K2 fix — registration token flow

registerUser() returns { registrationToken } (10-Min JWT)
createProfile() sends it as X-Registration-Token header
userId nie mehr aus Browser-Body — kommt aus signiertem Token

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 22:55:53 +02:00
parent bccec179a4
commit 520d0d139c
3 changed files with 11 additions and 8 deletions

View File

@@ -28,7 +28,7 @@ export async function getMe(userToken) {
return data // bereits vom API-Server entpackt return data // bereits vom API-Server entpackt
} }
// Registriert den Directus-User; gibt { userId } zurück // Registriert den Directus-User; gibt { registrationToken } zurück (10 Min gültig)
export async function registerUser(email, password) { export async function registerUser(email, password) {
const res = await fetch(`${BASE}/languparent/auth/register`, { const res = await fetch(`${BASE}/languparent/auth/register`, {
method: 'POST', headers: json, method: 'POST', headers: json,
@@ -36,7 +36,7 @@ export async function registerUser(email, password) {
}) })
const data = await res.json() const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Registrierung fehlgeschlagen.') if (!res.ok) throw new Error(data.error || 'Registrierung fehlgeschlagen.')
return data // { userId } return data // { registrationToken }
} }
export async function checkUsername(username /*, _userToken — nicht mehr nötig */) { export async function checkUsername(username /*, _userToken — nicht mehr nötig */) {
@@ -49,11 +49,13 @@ export async function checkUsername(username /*, _userToken — nicht mehr nöti
} }
// Erstellt Profil über API-Server (Admin-Token bleibt server-seitig) // Erstellt Profil über API-Server (Admin-Token bleibt server-seitig)
// userToken ist das kurzlebige Registration-Token aus registerUser()
// Gibt { token, expiresIn } zurück — muss via saveToken gespeichert werden // Gibt { token, expiresIn } zurück — muss via saveToken gespeichert werden
export async function createProfile({ userId, username, nativeLang, targetLang /*, userToken — ignoriert */ }) { export async function createProfile({ username, nativeLang, targetLang, userToken }) {
const res = await fetch(`${BASE}/languparent/auth/profile`, { const res = await fetch(`${BASE}/languparent/auth/profile`, {
method: 'POST', headers: json, method: 'POST',
body: JSON.stringify({ userId, username, nativeLang, targetLang }), headers: { ...json, 'X-Registration-Token': userToken },
body: JSON.stringify({ username, nativeLang, targetLang }),
}) })
const data = await res.json() const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Profilerstellung fehlgeschlagen.') if (!res.ok) throw new Error(data.error || 'Profilerstellung fehlgeschlagen.')

View File

@@ -14,8 +14,8 @@ export default function RegisterStep1({ onSuccess }) {
if (pw.length < 8) { setError('Passwort muss mindestens 8 Zeichen haben.'); return } if (pw.length < 8) { setError('Passwort muss mindestens 8 Zeichen haben.'); return }
setError(''); setLoading(true) setError(''); setLoading(true)
try { try {
const { userId } = await registerUser(email, pw) const { registrationToken } = await registerUser(email, pw)
onSuccess(userId, null) onSuccess(null, registrationToken) // Token statt userId — AuthScreen speichert es als pendingToken
} catch (err) { } catch (err) {
setError(err.message) setError(err.message)
} finally { } finally {

View File

@@ -35,7 +35,8 @@ export default function RegisterStep2({ userId, userToken, onSuccess }) {
if (!available) { if (!available) {
setError('Dieser Username ist bereits vergeben.'); setLoading(false); return setError('Dieser Username ist bereits vergeben.'); setLoading(false); return
} }
const { token } = await createProfile({ userId, username, nativeLang, targetLang }) // userToken ist das kurzlebige Registration-Token aus Schritt 1
const { token } = await createProfile({ username, nativeLang, targetLang, userToken })
saveToken(token) saveToken(token)
setUser({ id: userId, username, language_native: nativeLang, language_target: targetLang }) setUser({ id: userId, username, language_native: nativeLang, language_target: targetLang })
onSuccess(username) onSuccess(username)