Was ist das?
Bitte wähle die Art des Projekts, bevor es zugewiesen wird.
+ {kinds.length === 0 && (
+
Arten konnten nicht geladen werden – bitte neu laden.
+ )}
{kinds.map((k) => (
diff --git a/src/lib/returnPath.js b/src/lib/returnPath.js
new file mode 100644
index 0000000..e2b6e51
--- /dev/null
+++ b/src/lib/returnPath.js
@@ -0,0 +1,8 @@
+// Rueckkehr-Ziel nach dem Login: nur relative Pfade dieser App (kein offener Redirect).
+export function safeReturnPath(from, fallback = '/tickets') {
+ const path = typeof from === 'string' ? from : from?.pathname
+ if (typeof path !== 'string' || !path.startsWith('/') || path.startsWith('//') || path.includes('\\')) return fallback
+ if (path === '/login' || path.startsWith('/login/')) return fallback
+ const search = typeof from === 'object' && typeof from?.search === 'string' && from.search.startsWith('?') ? from.search : ''
+ return path + search
+}
diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx
index a945a18..0aaf3a5 100644
--- a/src/pages/LoginPage.jsx
+++ b/src/pages/LoginPage.jsx
@@ -1,7 +1,8 @@
import { useState } from 'react'
-import { useNavigate } from 'react-router-dom'
+import { useNavigate, useLocation } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import LetterGlitch from '../components/LetterGlitch'
+import { safeReturnPath } from '../lib/returnPath'
export default function LoginPage() {
const [email, setEmail] = useState('')
@@ -13,6 +14,8 @@ export default function LoginPage() {
const { login, mfaPending, sendCode, verifyCode, cancelMfa } = useAuth()
const navigate = useNavigate()
+ const location = useLocation()
+ const target = safeReturnPath(location.state?.from)
const handleSubmit = async (e) => {
e.preventDefault()
@@ -23,7 +26,7 @@ export default function LoginPage() {
const result = await login(email, password)
if (result.success) {
- navigate('/tickets')
+ navigate(target, { replace: true })
} else if (result.mfaRequired) {
if (result.error) setError(result.error)
} else {
@@ -52,7 +55,7 @@ export default function LoginPage() {
const result = await verifyCode(code)
setLoading(false)
if (result.success) {
- navigate('/tickets')
+ navigate(target, { replace: true })
} else {
setError(result.error)
setCode('')
diff --git a/tests/unit/returnPath.test.mjs b/tests/unit/returnPath.test.mjs
new file mode 100644
index 0000000..b1502e6
--- /dev/null
+++ b/tests/unit/returnPath.test.mjs
@@ -0,0 +1,21 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { loadModule } from './load.mjs'
+
+const { safeReturnPath } = await loadModule('src/lib/returnPath.js')
+
+test('Pfad und Suchteil bleiben erhalten', () => {
+ assert.equal(safeReturnPath({ pathname: '/projects/zuordnen/abc', search: '?x=1' }), '/projects/zuordnen/abc?x=1')
+ assert.equal(safeReturnPath({ pathname: '/projects/zuordnen/abc' }), '/projects/zuordnen/abc')
+ assert.equal(safeReturnPath('/customers/5'), '/customers/5')
+})
+
+test('Fallback bei fehlendem oder unsicherem Ziel', () => {
+ for (const bad of [undefined, null, {}, '', 'https://evil.example', '//evil.example', '/\\evil', 'javascript:alert(1)', { pathname: '//evil.example' }, { pathname: 'projects' }, { pathname: '/login' }, 42]) {
+ assert.equal(safeReturnPath(bad), '/tickets', JSON.stringify(bad))
+ }
+})
+
+test('Suchteil ohne ? wird verworfen', () => {
+ assert.equal(safeReturnPath({ pathname: '/a', search: 'evil' }), '/a')
+})