Final Feature 9c #10

Merged
Lars merged 18 commits from develop into main 2026-03-21 12:41:41 +01:00
5 changed files with 336 additions and 2 deletions
Showing only changes of commit 86f7a513fe - Show all commits

View File

@ -8,6 +8,8 @@ import { Avatar } from './pages/ProfileSelect'
import SetupScreen from './pages/SetupScreen' import SetupScreen from './pages/SetupScreen'
import { ResetPassword } from './pages/PasswordRecovery' import { ResetPassword } from './pages/PasswordRecovery'
import LoginScreen from './pages/LoginScreen' import LoginScreen from './pages/LoginScreen'
import Register from './pages/Register'
import Verify from './pages/Verify'
import Dashboard from './pages/Dashboard' import Dashboard from './pages/Dashboard'
import CaptureHub from './pages/CaptureHub' import CaptureHub from './pages/CaptureHub'
import WeightScreen from './pages/WeightScreen' import WeightScreen from './pages/WeightScreen'
@ -59,9 +61,26 @@ function AppShell() {
} }
}, [session?.profile_id]) }, [session?.profile_id])
// Handle password reset link // Handle public pages (register, verify, reset-password)
const urlParams = new URLSearchParams(window.location.search) const urlParams = new URLSearchParams(window.location.search)
const resetToken = urlParams.get('reset-password') || (window.location.pathname === '/reset-password' ? urlParams.get('token') : null) const currentPath = window.location.pathname
// Register page
if (currentPath === '/register') return (
<div style={{minHeight:'100vh',background:'var(--bg)',padding:24}}>
<Register/>
</div>
)
// Verify email page
if (currentPath === '/verify') return (
<div style={{minHeight:'100vh',background:'var(--bg)',padding:24}}>
<Verify/>
</div>
)
// Password reset page
const resetToken = urlParams.get('reset-password') || (currentPath === '/reset-password' ? urlParams.get('token') : null)
if (resetToken) return ( if (resetToken) return (
<div style={{minHeight:'100vh',display:'flex',alignItems:'center',justifyContent:'center', <div style={{minHeight:'100vh',display:'flex',alignItems:'center',justifyContent:'center',
background:'var(--bg)',padding:24}}> background:'var(--bg)',padding:24}}>

View File

@ -105,6 +105,22 @@ export default function LoginScreen() {
textAlign:'center',padding:'4px 0',textDecoration:'underline'}}> textAlign:'center',padding:'4px 0',textDecoration:'underline'}}>
Passwort vergessen? Passwort vergessen?
</button> </button>
<div style={{
marginTop:24, paddingTop:20,
borderTop:'1px solid var(--border)',
textAlign:'center'
}}>
<span style={{color:'var(--text2)',fontSize:14}}>
Noch kein Account?{' '}
</span>
<a href="/register" style={{
color:'var(--accent)',fontWeight:600,fontSize:14,
textDecoration:'none'
}}>
Jetzt registrieren
</a>
</div>
</div> </div>
<div style={{textAlign:'center',marginTop:20,fontSize:11,color:'var(--text3)'}}> <div style={{textAlign:'center',marginTop:20,fontSize:11,color:'var(--text3)'}}>

View File

@ -0,0 +1,182 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { api } from '../utils/api'
export default function Register() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [passwordConfirm, setPasswordConfirm] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [success, setSuccess] = useState(false)
const handleSubmit = async (e) => {
e.preventDefault()
setError(null)
// Validation
if (!name || name.length < 2) {
setError('Name muss mindestens 2 Zeichen lang sein')
return
}
if (!email || !email.includes('@')) {
setError('Ungültige E-Mail-Adresse')
return
}
if (password.length < 8) {
setError('Passwort muss mindestens 8 Zeichen lang sein')
return
}
if (password !== passwordConfirm) {
setError('Passwörter stimmen nicht überein')
return
}
setLoading(true)
try {
await api.register(name, email, password)
setSuccess(true)
} catch (err) {
setError(err.message || 'Registrierung fehlgeschlagen')
} finally {
setLoading(false)
}
}
if (success) {
return (
<div style={{
maxWidth:420, margin:'0 auto', padding:'60px 20px',
textAlign:'center'
}}>
<div style={{
background:'var(--accent-light)',
border:'2px solid var(--accent)',
borderRadius:16, padding:'40px 24px', marginBottom:24
}}>
<div style={{fontSize:48, marginBottom:16}}></div>
<h2 style={{marginBottom:12, color:'var(--accent-dark)'}}>
Registrierung erfolgreich!
</h2>
<p style={{color:'var(--text2)', lineHeight:1.6}}>
Wir haben dir eine E-Mail mit einem Bestätigungslink gesendet.
Bitte prüfe dein Postfach und bestätige deine E-Mail-Adresse.
</p>
</div>
<Link to="/login" className="btn btn-primary btn-full">
Zum Login
</Link>
<p style={{fontSize:12, color:'var(--text3)', marginTop:20}}>
Keine E-Mail erhalten? Prüfe auch deinen Spam-Ordner.
</p>
</div>
)
}
return (
<div style={{maxWidth:420, margin:'0 auto', padding:'40px 20px'}}>
<h1 style={{textAlign:'center', marginBottom:8}}>Registrierung</h1>
<p style={{textAlign:'center', color:'var(--text2)', marginBottom:32}}>
Erstelle deinen Mitai Jinkendo Account
</p>
<form onSubmit={handleSubmit} className="card" style={{padding:24}}>
{error && (
<div style={{
padding:'12px 16px', background:'#FCEBEB',
border:'1px solid #D85A30', borderRadius:8,
color:'#D85A30', fontSize:14, marginBottom:20
}}>
{error}
</div>
)}
<div className="form-row">
<label className="form-label">Name *</label>
<input
type="text"
className="form-input"
value={name}
onChange={e => setName(e.target.value)}
placeholder="Dein Name"
disabled={loading}
required
/>
</div>
<div className="form-row">
<label className="form-label">E-Mail *</label>
<input
type="email"
className="form-input"
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="deine@email.de"
disabled={loading}
required
/>
</div>
<div className="form-row">
<label className="form-label">Passwort *</label>
<input
type="password"
className="form-input"
value={password}
onChange={e => setPassword(e.target.value)}
placeholder="Mindestens 8 Zeichen"
disabled={loading}
required
/>
</div>
<div className="form-row">
<label className="form-label">Passwort bestätigen *</label>
<input
type="password"
className="form-input"
value={passwordConfirm}
onChange={e => setPasswordConfirm(e.target.value)}
placeholder="Passwort wiederholen"
disabled={loading}
required
/>
</div>
<button
type="submit"
className="btn btn-primary btn-full"
disabled={loading}
style={{marginTop:8}}
>
{loading ? 'Registriere...' : 'Registrieren'}
</button>
<div style={{
textAlign:'center', marginTop:24,
paddingTop:20, borderTop:'1px solid var(--border)'
}}>
<span style={{color:'var(--text2)', fontSize:14}}>
Bereits registriert?{' '}
</span>
<Link to="/login" style={{
color:'var(--accent)', fontWeight:600, fontSize:14
}}>
Zum Login
</Link>
</div>
</form>
<p style={{
fontSize:11, color:'var(--text3)', textAlign:'center',
marginTop:20, lineHeight:1.6
}}>
Mit der Registrierung akzeptierst du unsere Nutzungsbedingungen
und Datenschutzerklärung.
</p>
</div>
)
}

View File

@ -0,0 +1,115 @@
import { useState, useEffect, useContext } from 'react'
import { useSearchParams, useNavigate } from 'react-router-dom'
import { AuthContext } from '../context/AuthContext'
import { api } from '../utils/api'
export default function Verify() {
const [searchParams] = useSearchParams()
const token = searchParams.get('token')
const navigate = useNavigate()
const { login } = useContext(AuthContext)
const [status, setStatus] = useState('loading') // loading | success | error
const [error, setError] = useState(null)
useEffect(() => {
const verify = async () => {
if (!token) {
setStatus('error')
setError('Kein Verifikations-Token gefunden')
return
}
try {
const result = await api.verifyEmail(token)
// Auto-login with returned token
if (result.token) {
login(result.token, result.profile)
setStatus('success')
// Redirect to dashboard after 2 seconds
setTimeout(() => {
navigate('/dashboard')
}, 2000)
} else {
setStatus('error')
setError('Verifizierung erfolgreich, aber Login fehlgeschlagen')
}
} catch (err) {
setStatus('error')
setError(err.message || 'Verifizierung fehlgeschlagen')
}
}
verify()
}, [token, login, navigate])
if (status === 'loading') {
return (
<div style={{
maxWidth:420, margin:'0 auto', padding:'60px 20px',
textAlign:'center'
}}>
<div className="spinner" style={{width:48, height:48, margin:'0 auto 24px'}}/>
<h2>E-Mail wird bestätigt...</h2>
<p style={{color:'var(--text2)'}}>Einen Moment bitte</p>
</div>
)
}
if (status === 'error') {
return (
<div style={{
maxWidth:420, margin:'0 auto', padding:'60px 20px',
textAlign:'center'
}}>
<div style={{
background:'#FCEBEB',
border:'2px solid #D85A30',
borderRadius:16, padding:'40px 24px', marginBottom:24
}}>
<div style={{fontSize:48, marginBottom:16}}></div>
<h2 style={{marginBottom:12, color:'#D85A30'}}>
Verifizierung fehlgeschlagen
</h2>
<p style={{color:'var(--text2)', lineHeight:1.6}}>
{error}
</p>
</div>
<button
onClick={() => navigate('/register')}
className="btn btn-primary btn-full"
>
Zur Registrierung
</button>
</div>
)
}
// Success
return (
<div style={{
maxWidth:420, margin:'0 auto', padding:'60px 20px',
textAlign:'center'
}}>
<div style={{
background:'var(--accent-light)',
border:'2px solid var(--accent)',
borderRadius:16, padding:'40px 24px', marginBottom:24
}}>
<div style={{fontSize:48, marginBottom:16}}></div>
<h2 style={{marginBottom:12, color:'var(--accent-dark)'}}>
E-Mail bestätigt!
</h2>
<p style={{color:'var(--text2)', lineHeight:1.6}}>
Dein Account wurde erfolgreich aktiviert.
Du wirst gleich zum Dashboard weitergeleitet...
</p>
</div>
<div className="spinner" style={{width:32, height:32, margin:'0 auto'}}/>
</div>
)
}

View File

@ -142,6 +142,8 @@ export const api = {
adminDeleteProfile: (id) => req(`/admin/profiles/${id}`,{method:'DELETE'}), adminDeleteProfile: (id) => req(`/admin/profiles/${id}`,{method:'DELETE'}),
adminSetPermissions: (id,d) => req(`/admin/profiles/${id}/permissions`,jput(d)), adminSetPermissions: (id,d) => req(`/admin/profiles/${id}/permissions`,jput(d)),
changePin: (pin) => req('/auth/pin',json({pin})), changePin: (pin) => req('/auth/pin',json({pin})),
register: (name,email,password) => req('/auth/register',json({name,email,password})),
verifyEmail: (token) => req(`/auth/verify/${token}`),
// v9c Subscription System // v9c Subscription System
// User-facing // User-facing