Frontend: - Add @azure/msal-browser and @azure/msal-react packages - Create authConfig.ts with MSAL configuration for PKCE flow - Create authService.ts for token acquisition and user info - Wrap App with MsalProvider in index.tsx - Replace dummy login with real MSAL loginPopup() in Login.tsx - Update App.tsx to use useIsAuthenticated/useMsal hooks - Update Profile.tsx to display real user data from claims - Update geminiService.ts to include access_token in WebSocket messages - Update WIPReviewer.tsx to pass msalInstance for auth Backend: - Add python-jose and httpx dependencies for JWT verification - Create auth_service.py with Azure AD JWKS fetching and token verification - Create auth.py FastAPI dependency for protected REST endpoints - Update main.py to verify tokens on WebSocket and protect /info endpoint - Add AZURE_TENANT_ID, AZURE_CLIENT_ID, DISABLE_AUTH to config 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
118 lines
No EOL
5.4 KiB
TypeScript
118 lines
No EOL
5.4 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { IPublicClientApplication } from '@azure/msal-browser';
|
|
import { LogoutIcon } from './icons/LogoutIcon';
|
|
import { QuestionMarkIcon } from './icons/QuestionMarkIcon';
|
|
import { getUserInfo, UserInfo } from '../services/authService';
|
|
|
|
interface ProfileProps {
|
|
onLogout: () => void;
|
|
msalInstance: IPublicClientApplication;
|
|
}
|
|
|
|
export const Profile: React.FC<ProfileProps> = ({ onLogout, msalInstance }) => {
|
|
const [isQuestionFormVisible, setIsQuestionFormVisible] = useState(false);
|
|
const [question, setQuestion] = useState('');
|
|
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
|
|
|
useEffect(() => {
|
|
const info = getUserInfo(msalInstance);
|
|
setUserInfo(info);
|
|
}, [msalInstance]);
|
|
|
|
const userDetails = userInfo ? {
|
|
'Account Type': userInfo.accountType,
|
|
'First Name': userInfo.firstName || 'N/A',
|
|
'Last Name': userInfo.lastName || 'N/A',
|
|
'Email': userInfo.email,
|
|
} : {
|
|
'Account Type': 'Loading...',
|
|
'First Name': '',
|
|
'Last Name': '',
|
|
'Email': '',
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
onLogout();
|
|
};
|
|
|
|
const handleToggleQuestionForm = () => {
|
|
setIsQuestionFormVisible(prev => !prev);
|
|
};
|
|
|
|
const handleSubmitQuestion = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!question.trim()) {
|
|
alert('Please enter a question before submitting.');
|
|
return;
|
|
}
|
|
alert(`Your question has been submitted:\n\n"${question}"\n\nWe'll get back to you shortly.`);
|
|
setQuestion('');
|
|
setIsQuestionFormVisible(false);
|
|
};
|
|
|
|
return (
|
|
<div className="p-4 sm:p-6 lg:p-8 h-full bg-brand-gray">
|
|
<header className="mb-8">
|
|
<h1 className="text-3xl lg:text-4xl font-bold text-brand-dark-blue">Your Profile</h1>
|
|
<p className="text-base lg:text-lg text-gray-600 mt-1">View your account details and manage settings.</p>
|
|
</header>
|
|
|
|
<div className="max-w-3xl">
|
|
<section className="bg-white rounded-lg shadow-md p-6 sm:p-8 border border-gray-200">
|
|
<h2 className="text-2xl font-bold text-brand-dark-blue mb-6">Account Information</h2>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-5">
|
|
{Object.entries(userDetails).map(([key, value]) => (
|
|
<div key={key}>
|
|
<p className="text-sm font-semibold text-gray-500 tracking-wide uppercase">{key}</p>
|
|
<p className="text-lg text-gray-800 mt-1">{value}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="border-t border-gray-200 mt-8 pt-6 flex flex-col sm:flex-row gap-3">
|
|
<button
|
|
onClick={handleLogout}
|
|
className="flex items-center justify-center gap-2 bg-red-600 text-white font-semibold py-2 px-4 rounded-md hover:bg-red-700 transition-colors duration-300"
|
|
>
|
|
<LogoutIcon className="h-5 w-5" />
|
|
Logout
|
|
</button>
|
|
<button
|
|
onClick={handleToggleQuestionForm}
|
|
className="flex items-center justify-center gap-2 bg-gray-200 text-gray-800 font-semibold py-2 px-4 rounded-md hover:bg-gray-300 transition-colors duration-300"
|
|
>
|
|
<QuestionMarkIcon className="h-5 w-5" />
|
|
Got a question?
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
{isQuestionFormVisible && (
|
|
<section className="mt-8 bg-white rounded-lg shadow-md p-6 sm:p-8 border border-gray-200">
|
|
<h2 className="text-2xl font-bold text-brand-dark-blue mb-4">Ask a Question</h2>
|
|
<form onSubmit={handleSubmitQuestion}>
|
|
<p className="text-gray-600 mb-4">Your question will be sent to the OLIVER Agency support team.</p>
|
|
<textarea
|
|
value={question}
|
|
onChange={(e) => setQuestion(e.target.value)}
|
|
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-accent focus:border-brand-accent transition"
|
|
placeholder="Type your question here..."
|
|
rows={5}
|
|
required
|
|
/>
|
|
<div className="mt-4 flex justify-end">
|
|
<button
|
|
type="submit"
|
|
className="bg-brand-accent text-white font-semibold py-2 px-5 rounded-md hover:bg-brand-dark-blue transition-colors duration-300 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
|
disabled={!question.trim()}
|
|
>
|
|
Submit Question
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}; |