enhanced invitation

This commit is contained in:
ValueOn AG 2026-02-10 10:56:28 +01:00
parent 7077e75fc7
commit 7798463e24
5 changed files with 34 additions and 16 deletions

View file

@ -77,7 +77,6 @@ function App() {
<AuthProvider> <AuthProvider>
<ToastProvider> <ToastProvider>
<WorkflowSelectionProvider> <WorkflowSelectionProvider>
<FileProvider>
<Router> <Router>
<Routes> <Routes>
{/* ================================================== */} {/* ================================================== */}
@ -94,7 +93,9 @@ function App() {
{/* ================================================== */} {/* ================================================== */}
<Route path="/" element={ <Route path="/" element={
<ProtectedRoute> <ProtectedRoute>
<FileProvider>
<MainLayout /> <MainLayout />
</FileProvider>
</ProtectedRoute> </ProtectedRoute>
}> }>
{/* Dashboard (Root) */} {/* Dashboard (Root) */}
@ -190,7 +191,6 @@ function App() {
} /> } />
</Routes> </Routes>
</Router> </Router>
</FileProvider>
</WorkflowSelectionProvider> </WorkflowSelectionProvider>
</ToastProvider> </ToastProvider>
</AuthProvider> </AuthProvider>

View file

@ -65,6 +65,7 @@ export interface InvitationValidation {
roleIds: string[]; roleIds: string[];
roleLabels?: string[]; roleLabels?: string[];
targetUsername?: string; targetUsername?: string;
email?: string;
} }

View file

@ -24,6 +24,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom'; import { useParams, useNavigate, Link } from 'react-router-dom';
import { useInvitations, type InvitationValidation } from '../hooks/useInvitations'; import { useInvitations, type InvitationValidation } from '../hooks/useInvitations';
import api from '../api';
import { FaCheckCircle, FaTimesCircle, FaSpinner, FaSignInAlt, FaUserPlus } from 'react-icons/fa'; import { FaCheckCircle, FaTimesCircle, FaSpinner, FaSignInAlt, FaUserPlus } from 'react-icons/fa';
import styles from './InvitePage.module.css'; import styles from './InvitePage.module.css';
@ -35,7 +36,7 @@ export const InvitePage: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { validateInvitation, acceptInvitation } = useInvitations(); const { validateInvitation, acceptInvitation } = useInvitations();
// Check if user has auth token (simplified check) // Check if user has an active session
const isAuthenticated = !!sessionStorage.getItem('auth_authority'); const isAuthenticated = !!sessionStorage.getItem('auth_authority');
// State // State
@ -68,12 +69,11 @@ export const InvitePage: React.FC = () => {
// Check if the target username already has an account // Check if the target username already has an account
if (result.targetUsername) { if (result.targetUsername) {
try { try {
const resp = await fetch(`/api/local/available?username=${encodeURIComponent(result.targetUsername)}`); const resp = await api.get(`/api/local/available`, {
if (resp.ok) { params: { username: result.targetUsername }
const data = await resp.json(); });
// available=true means username is free -> user does NOT exist // available=true means username is free -> user does NOT exist
setUserExists(!data.available); setUserExists(!resp.data.available);
}
} catch { } catch {
// On error, default to showing both options // On error, default to showing both options
setUserExists(null); setUserExists(null);
@ -104,6 +104,10 @@ export const InvitePage: React.FC = () => {
setTimeout(() => { setTimeout(() => {
navigate('/'); navigate('/');
}, 2000); }, 2000);
} else if (result.error?.includes('401') || result.error?.includes('Not authenticated')) {
// Session expired — clear auth and redirect to login
sessionStorage.removeItem('auth_authority');
handleLoginRedirect();
} else { } else {
setError(result.error || 'Fehler beim Annehmen der Einladung'); setError(result.error || 'Fehler beim Annehmen der Einladung');
} }
@ -111,20 +115,28 @@ export const InvitePage: React.FC = () => {
setAccepting(false); setAccepting(false);
}; };
// Handle redirect to login (stores token first) // Handle redirect to login (stores token first, passes invitation data for pre-fill)
const handleLoginRedirect = () => { const handleLoginRedirect = () => {
if (token) { if (token) {
localStorage.setItem(PENDING_INVITATION_KEY, token); localStorage.setItem(PENDING_INVITATION_KEY, token);
} }
navigate('/login', { state: { from: { pathname: `/invite/${token}` } } }); navigate('/login', { state: {
from: { pathname: `/invite/${token}` },
invitationUsername: validation?.targetUsername || '',
} });
}; };
// Handle redirect to register (stores token first) // Handle redirect to register (stores token first, passes invitation data for pre-fill)
const handleRegisterRedirect = () => { const handleRegisterRedirect = () => {
if (token) { if (token) {
localStorage.setItem(PENDING_INVITATION_KEY, token); localStorage.setItem(PENDING_INVITATION_KEY, token);
} }
navigate('/register', { state: { from: { pathname: `/invite/${token}` }, pendingInvitation: true } }); navigate('/register', { state: {
from: { pathname: `/invite/${token}` },
pendingInvitation: true,
invitationUsername: validation?.targetUsername || '',
invitationEmail: validation?.email || '',
} });
}; };
// Loading state // Loading state

View file

@ -12,7 +12,9 @@ import styles from './Login.module.css';
function Login() { function Login() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [username, setUsername] = useState(''); // Pre-fill username from invitation if provided via location.state
const invitationUsername = (location.state as any)?.invitationUsername || '';
const [username, setUsername] = useState(invitationUsername);
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [usernameFocused, setUsernameFocused] = useState(false); const [usernameFocused, setUsernameFocused] = useState(false);
const [passwordFocused, setPasswordFocused] = useState(false); const [passwordFocused, setPasswordFocused] = useState(false);

View file

@ -19,9 +19,12 @@ function Register() {
const { register, error: registerError, isLoading } = useRegister(); const { register, error: registerError, isLoading } = useRegister();
const { error: msalError } = useMsalRegister(); const { error: msalError } = useMsalRegister();
const { checkAvailability, isChecking, error: availabilityError } = useUsernameAvailability(); const { checkAvailability, isChecking, error: availabilityError } = useUsernameAvailability();
// Pre-fill from invitation if provided via location.state
const invitationUsername = (location.state as any)?.invitationUsername || '';
const invitationEmail = (location.state as any)?.invitationEmail || '';
const [formData, setFormData] = useState<RegisterFormData>({ const [formData, setFormData] = useState<RegisterFormData>({
username: '', username: invitationUsername,
email: '', email: invitationEmail,
fullName: '' fullName: ''
}); });
const [validationError, setValidationError] = useState<string | null>(null); const [validationError, setValidationError] = useState<string | null>(null);