dev1/src/components/InviteResponse.js
Josh c0a68eb81c
Some checks failed
ci/woodpecker/manual/woodpecker Pipeline failed
Big one - admin portal and DOB COPPA compliance
2025-10-30 10:28:38 +00:00

236 lines
9.9 KiB
JavaScript

import { useState, useEffect, useRef } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import axios from 'axios';
export default function InviteResponse() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [tokenData, setTokenData] = useState(null);
const [error, setError] = useState(null);
const [processing, setProcessing] = useState(false);
const shouldAutoLink = useRef(false);
const handleLinkAccount = async () => {
setProcessing(true);
setError(null);
try {
// Try to link the account directly - if not authenticated, backend will return 401
await axios.post('/api/link-account', {
token: tokenData.token
}, {
withCredentials: true
});
// Force a page reload to refresh user profile and trigger privacy settings check
window.location.href = '/signin-landing';
} catch (err) {
// If not authenticated, redirect to signin
if (err.response?.status === 401 || err.response?.status === 403) {
const returnUrl = encodeURIComponent(`/invite-response?token=${tokenData.token}&autolink=true`);
navigate(`/signin?redirect=${returnUrl}`);
return;
}
setError(err.response?.data?.error || 'Failed to link account. Please try again.');
setProcessing(false);
}
};
useEffect(() => {
validateToken();
}, []);
// Auto-link when tokenData is set and autolink flag is true
useEffect(() => {
if (tokenData && shouldAutoLink.current && !processing) {
shouldAutoLink.current = false; // Prevent double-trigger
handleLinkAccount();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tokenData]);
const validateToken = async () => {
const token = searchParams.get('token');
const autoLink = searchParams.get('autolink'); // Check if we should auto-link after signin
if (!token) {
setError('No invitation token provided');
setLoading(false);
return;
}
try {
// Decode token to check if it's for existing user
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const payload = JSON.parse(window.atob(base64));
if (payload.prp !== 'student_invite') {
setError('Invalid invitation token');
setLoading(false);
return;
}
if (payload.isNewUser !== false) {
// This is for a new user, redirect to signup
navigate(`/signup?invite=${token}`, { replace: true });
return;
}
// Token is valid for existing user
setTokenData({
token,
organizationId: payload.organizationId,
email: payload.email
});
setLoading(false);
// If autolink param is present, set flag to trigger auto-link
if (autoLink === 'true') {
shouldAutoLink.current = true;
}
} catch (err) {
setError('Invalid invitation token');
setLoading(false);
}
};
const handleCreateSeparateAccount = () => {
// User needs to contact admin for a new invitation with different email
navigate('/', { replace: true });
};
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-aptiva mx-auto"></div>
<p className="mt-4 text-gray-600">Validating invitation...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4">
<div className="max-w-md w-full bg-white shadow-lg rounded-lg p-8">
<div className="text-center">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
<svg className="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<h2 className="mt-4 text-xl font-semibold text-gray-900">Invalid Invitation</h2>
<p className="mt-2 text-gray-600">{error}</p>
<button
onClick={() => navigate('/')}
className="mt-6 w-full bg-aptiva text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors"
>
Go to Home
</button>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4 py-8">
<div className="max-w-2xl w-full bg-white shadow-lg rounded-lg p-8">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900">You're Invited!</h1>
<p className="mt-2 text-gray-600">Choose how you'd like to proceed</p>
</div>
<div className="bg-blue-50 border-l-4 border-aptiva p-4 mb-6">
<p className="text-sm text-blue-800">
<strong>We noticed you already have an AptivaAI account.</strong> You can either link your existing account or create a separate one for your organization.
</p>
</div>
{error && (
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-sm text-red-800">{error}</p>
</div>
)}
<div className="space-y-4">
{/* Option 1: Link Existing Account */}
<div className="border-2 border-gray-200 rounded-lg p-6 hover:border-aptiva transition-colors">
<h3 className="text-lg font-semibold text-gray-900 mb-2">Option 1: Link Your Existing Account</h3>
<ul className="space-y-2 mb-4 text-sm text-gray-600">
<li className="flex items-start">
<svg className="h-5 w-5 text-green-500 mr-2 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
Your existing data and progress will be preserved
</li>
<li className="flex items-start">
<svg className="h-5 w-5 text-green-500 mr-2 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
Your organization can view your activity based on your privacy settings
</li>
<li className="flex items-start">
<svg className="h-5 w-5 text-green-500 mr-2 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
You'll gain premium access through your organization
</li>
</ul>
<button
onClick={handleLinkAccount}
disabled={processing}
className="w-full bg-aptiva text-white py-3 px-4 rounded-lg hover:bg-blue-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
{processing ? 'Processing...' : 'Link My Existing Account'}
</button>
</div>
{/* Option 2: Create Separate Account */}
<div className="border-2 border-gray-200 rounded-lg p-6 hover:border-gray-400 transition-colors">
<h3 className="text-lg font-semibold text-gray-900 mb-2">Option 2: Create a Separate Account</h3>
<ul className="space-y-2 mb-4 text-sm text-gray-600">
<li className="flex items-start">
<svg className="h-5 w-5 text-blue-500 mr-2 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Keep your personal AptivaAI account completely separate
</li>
<li className="flex items-start">
<svg className="h-5 w-5 text-blue-500 mr-2 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Start fresh with a new profile for school/organization use
</li>
<li className="flex items-start">
<svg className="h-5 w-5 text-orange-500 mr-2 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span><strong>Requires a different email address</strong></span>
</li>
</ul>
<div className="bg-yellow-50 border border-yellow-200 rounded p-3 mb-4 text-xs text-yellow-800">
<strong>Note:</strong> To create a separate account, you'll need to contact your administrator and provide a different email address. They can then send you a new invitation.
</div>
<button
onClick={handleCreateSeparateAccount}
disabled={processing}
className="w-full bg-white border-2 border-gray-300 text-gray-700 py-3 px-4 rounded-lg hover:bg-gray-50 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
Contact Administrator
</button>
</div>
</div>
<p className="mt-6 text-center text-sm text-gray-500">
Questions? Contact your administrator for help.
</p>
</div>
</div>
);
}