Spaces:
Sleeping
Sleeping
| import { useState } from 'react'; | |
| import { req, saveToken, ENDPOINTS } from '../api'; | |
| import { Card, CardHeader, Callout, FormGroup, ResponseBox, StepBar, Tag } from '../components/ui'; | |
| import { RiskVizCard } from '../components/RiskViz'; | |
| const DEMO = ENDPOINTS.DEMO; | |
| const STEPS = [ | |
| { label: 'Setup', sub: 'Create demo user' }, | |
| { label: 'Normal Login', sub: 'Known context' }, | |
| { label: 'Suspicious Login', sub: 'Unknown context' }, | |
| { label: 'Verify Challenge', sub: 'Step-up auth' }, | |
| ]; | |
| export default function Scenario1Tab({ onTokenSave }) { | |
| const [step, setStep] = useState(-1); | |
| const [setupResp, setSetupResp] = useState(null); | |
| const [normalResp, setNormalResp] = useState(null); | |
| const [suspResp, setSuspResp] = useState(null); | |
| const [challengeResp, setChallengeResp] = useState(null); | |
| const [riskData, setRiskData] = useState(null); | |
| const [challengeId, setChallengeId] = useState(''); | |
| const [challengeCode, setChallengeCode] = useState(''); | |
| const [showChallenge, setShowChallenge] = useState(false); | |
| const [loading, setLoading] = useState({}); | |
| const setLoad = (k, v) => setLoading(prev => ({ ...prev, [k]: v })); | |
| const setupDemo = async (reset) => { | |
| setLoad('setup', true); | |
| const r = await req(`${DEMO}/setup?reset=${reset}`, 'POST', null, false); | |
| setSetupResp(r); | |
| if (r.ok) setStep(0); | |
| setLoad('setup', false); | |
| }; | |
| const checkState = async () => { | |
| setLoad('setup', true); | |
| const r = await req(`${DEMO}/state`, 'GET', null, false); | |
| setSetupResp(r); | |
| setLoad('setup', false); | |
| }; | |
| const doNormalLogin = async () => { | |
| setLoad('normal', true); | |
| const r = await req(`${DEMO}/scenario1/normal-login`, 'POST', null, false); | |
| setNormalResp(r); | |
| if (r.ok && r.data) { | |
| const fd = r.data.framework_decision || {}; | |
| const t = fd.access_token || r.data.access_token; | |
| if (t) { saveToken(t); onTokenSave?.(); } | |
| setRiskData({ decision: fd, notes: r.data.what_the_framework_checked }); | |
| setStep(s => Math.max(s, 1)); | |
| setShowChallenge(false); | |
| } | |
| setLoad('normal', false); | |
| }; | |
| const doSuspiciousLogin = async () => { | |
| setLoad('susp', true); | |
| const r = await req(`${DEMO}/scenario1/suspicious-login`, 'POST', null, false); | |
| setSuspResp(r); | |
| if (r.ok && r.data) { | |
| const fd = r.data.framework_decision || {}; | |
| setRiskData({ decision: fd, notes: r.data.anomalies_triggered }); | |
| setStep(s => Math.max(s, 2)); | |
| if (fd.status === 'challenge_required' && fd.challenge_id) { | |
| setChallengeId(fd.challenge_id); | |
| setShowChallenge(true); | |
| } else { | |
| setShowChallenge(false); | |
| } | |
| } | |
| setLoad('susp', false); | |
| }; | |
| const doCompleteChallenge = async () => { | |
| if (!challengeId || !challengeCode) { alert('Enter challenge ID and code.'); return; } | |
| setLoad('challenge', true); | |
| const url = `${DEMO}/scenario1/complete-challenge?challenge_id=${encodeURIComponent(challengeId)}&code=${encodeURIComponent(challengeCode)}`; | |
| const r = await req(url, 'POST', null, false); | |
| setChallengeResp(r); | |
| if (r.ok) { | |
| const t = r.data?.result?.access_token; | |
| if (t) { saveToken(t); onTokenSave?.(); } | |
| setStep(3); | |
| } | |
| setLoad('challenge', false); | |
| }; | |
| return ( | |
| <div> | |
| <Callout type="info"> | |
| <strong>Scenario 1 — User Behaviour Anomaly Detection</strong><br /> | |
| The demo user has <strong>30 days of normal login history</strong> from New York on Windows Chrome | |
| (Mon–Fri, 8AM–5PM). We show how the framework reacts when the <em>same password</em> is used | |
| from a completely different context. | |
| </Callout> | |
| <StepBar steps={STEPS} current={step} /> | |
| {/* Step 0 — Setup */} | |
| <Card> | |
| <CardHeader icon="⚙️">Step 0 – Setup Demo Environment</CardHeader> | |
| <p className="text-muted text-sm mb-3"> | |
| Creates the demo user with a realistic 30-day behavioral profile (15 logins from a | |
| trusted IP, device, and time window). | |
| </p> | |
| <div className="flex gap-2 flex-wrap"> | |
| <button className="btn btn-primary" onClick={() => setupDemo(false)} disabled={loading.setup}> | |
| 🔧 Setup Demo | |
| </button> | |
| <button className="btn btn-warn" onClick={() => setupDemo(true)} disabled={loading.setup}> | |
| 🔄 Reset & Re-setup | |
| </button> | |
| <button className="btn btn-ghost" onClick={checkState} disabled={loading.setup}> | |
| 📊 Check State | |
| </button> | |
| </div> | |
| <ResponseBox result={setupResp} /> | |
| </Card> | |
| {/* Step 1 & 2 — Compare */} | |
| <div className="compare-grid"> | |
| {/* Normal */} | |
| <div className="compare-side success"> | |
| <div className="compare-title success">✅ Normal Context</div> | |
| <div className="context-list"> | |
| <div><Tag>IP</Tag> 203.0.113.10 (known)</div> | |
| <div><Tag>Location</Tag> New York, US</div> | |
| <div><Tag>Device</Tag> Windows Chrome</div> | |
| <div><Tag>Time</Tag> Business hours</div> | |
| <div><Tag>History</Tag> 15 logins seen</div> | |
| </div> | |
| <button | |
| className="btn btn-success btn-full mt-3" | |
| onClick={doNormalLogin} | |
| disabled={loading.normal} | |
| > | |
| {loading.normal ? 'Logging in…' : '▶ Run Normal Login'} | |
| </button> | |
| <ResponseBox result={normalResp} /> | |
| </div> | |
| {/* Suspicious */} | |
| <div className="compare-side danger"> | |
| <div className="compare-title danger">🚩 Suspicious Context</div> | |
| <div className="context-list"> | |
| <div><Tag>IP</Tag> 198.51.100.55 (new!)</div> | |
| <div><Tag>Location</Tag> Moscow, Russia</div> | |
| <div><Tag>Device</Tag> iPhone Safari (new!)</div> | |
| <div><Tag>Time</Tag> Same password</div> | |
| <div><Tag>History</Tag> 0 logins from here</div> | |
| </div> | |
| <button | |
| className="btn btn-danger btn-full mt-3" | |
| onClick={doSuspiciousLogin} | |
| disabled={loading.susp} | |
| > | |
| {loading.susp ? 'Logging in…' : '▶ Run Suspicious Login'} | |
| </button> | |
| <ResponseBox result={suspResp} /> | |
| </div> | |
| </div> | |
| {/* Risk Visualization */} | |
| {riskData && ( | |
| <RiskVizCard decision={riskData.decision} notes={riskData.notes} /> | |
| )} | |
| {/* Step 3 — Challenge */} | |
| {showChallenge && ( | |
| <Card> | |
| <CardHeader icon="🔐">Step 3 – Complete Step-up Challenge</CardHeader> | |
| <Callout type="warn"> | |
| The framework triggered a challenge because of the suspicious context. In a live deployment | |
| this sends a real email. In the demo, use code <strong>000000</strong>. | |
| </Callout> | |
| <div className="grid-2" style={{ alignItems: 'start' }}> | |
| <div> | |
| <FormGroup label="Challenge ID"> | |
| <input value={challengeId} readOnly placeholder="Auto-filled from step 2" /> | |
| </FormGroup> | |
| <FormGroup label="Verification Code"> | |
| <input | |
| value={challengeCode} | |
| onChange={e => setChallengeCode(e.target.value)} | |
| placeholder="Enter code (000000 for demo)" | |
| /> | |
| </FormGroup> | |
| <button | |
| className="btn btn-primary btn-full" | |
| onClick={doCompleteChallenge} | |
| disabled={loading.challenge} | |
| > | |
| {loading.challenge ? 'Verifying…' : '✅ Verify & Complete Login'} | |
| </button> | |
| </div> | |
| <div className="text-sm text-2"> | |
| <p className="font-600">Why was this required?</p> | |
| <ul className="mt-2 ml-4" style={{ lineHeight: 2 }}> | |
| <li>Unknown IP address</li> | |
| <li>New device fingerprint</li> | |
| <li>Geographic location changed</li> | |
| <li>Security Level ≥ 2 → challenge required</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <ResponseBox result={challengeResp} /> | |
| </Card> | |
| )} | |
| </div> | |
| ); | |
| } | |