diff --git a/src/api/teamsbotApi.ts b/src/api/teamsbotApi.ts index 7a47f33..87c0651 100644 --- a/src/api/teamsbotApi.ts +++ b/src/api/teamsbotApi.ts @@ -156,9 +156,24 @@ export interface AuthTestResults { }; } +// User Account (Mein Account) Types +export interface UserAccountStatus { + hasSavedCredentials: boolean; + email?: string; + displayName?: string; +} + +// MFA Types +export interface MfaChallengeEvent { + mfaType: 'numberMatch' | 'pushApproval' | 'smsCode' | 'totpCode' | 'timeout' | 'unknown'; + displayNumber?: string; + prompt: string; + timestamp?: string; +} + // SSE Event Types export interface TeamsbotSSEEvent { - type: 'transcript' | 'botResponse' | 'analysis' | 'suggestedResponse' | 'statusChange' | 'error' | 'ping' | 'sessionState' | 'ttsDeliveryStatus'; + type: 'transcript' | 'botResponse' | 'analysis' | 'suggestedResponse' | 'statusChange' | 'error' | 'ping' | 'sessionState' | 'ttsDeliveryStatus' | 'mfaChallenge' | 'mfaResolved'; data: any; timestamp?: string; } @@ -402,3 +417,48 @@ export function getScreenshotUrl(instanceId: string, filename: string): string { const baseUrl = api.defaults.baseURL || ''; return `${baseUrl}/api/teamsbot/${instanceId}/screenshots/${filename}`; } + +// ========================================================================= +// User Account (Mein Account) +// ========================================================================= + +export async function getUserAccount(instanceId: string): Promise { + const response = await api.get(`/api/teamsbot/${instanceId}/user-account`); + return response.data; +} + +export async function saveUserAccount( + instanceId: string, + email: string, + password: string, + displayName?: string, +): Promise<{ saved: boolean; email: string }> { + const response = await api.post(`/api/teamsbot/${instanceId}/user-account`, { + email, + password, + displayName, + }); + return response.data; +} + +export async function deleteUserAccount(instanceId: string): Promise<{ deleted: boolean }> { + const response = await api.delete(`/api/teamsbot/${instanceId}/user-account`); + return response.data; +} + +// ========================================================================= +// MFA +// ========================================================================= + +export async function submitMfaCode( + instanceId: string, + sessionId: string, + code: string, + action: 'code' | 'confirmed' = 'code', +): Promise<{ submitted: boolean }> { + const response = await api.post(`/api/teamsbot/${instanceId}/sessions/${sessionId}/mfa`, { + code, + action, + }); + return response.data; +} diff --git a/src/pages/views/teamsbot/Teamsbot.module.css b/src/pages/views/teamsbot/Teamsbot.module.css index e282c09..26afc09 100644 --- a/src/pages/views/teamsbot/Teamsbot.module.css +++ b/src/pages/views/teamsbot/Teamsbot.module.css @@ -163,6 +163,112 @@ border-left: 3px solid var(--success-color, #2D8E5C); } +/* User Account / MFA */ +.credentialsCard { + background: var(--surface-color, #f9f9f9); + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 8px; + padding: 1rem 1.2rem; + margin-top: 0.5rem; +} + +.credentialsInfo { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 0.9rem; + gap: 0.5rem; +} + +.credentialsEmail { + font-weight: 500; + color: var(--text-color, #333); +} + +.checkboxRow { + display: flex; + align-items: center; + gap: 0.5rem; + margin-top: 0.5rem; + font-size: 0.85rem; +} + +.mfaOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.mfaDialog { + background: var(--surface-color, #fff); + border-radius: 12px; + padding: 2rem; + max-width: 420px; + width: 90%; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + text-align: center; +} + +.mfaTitle { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 1rem; + color: var(--text-color, #333); +} + +.mfaNumber { + font-size: 2.5rem; + font-weight: 700; + color: var(--primary-color, #4A90D9); + margin: 1rem 0; + letter-spacing: 0.1em; +} + +.mfaPrompt { + font-size: 0.9rem; + color: var(--text-secondary, #666); + margin-bottom: 1.5rem; + line-height: 1.5; +} + +.mfaCodeInput { + width: 100%; + padding: 0.75rem; + font-size: 1.2rem; + text-align: center; + letter-spacing: 0.2em; + border: 2px solid var(--border-color, #e0e0e0); + border-radius: 8px; + margin-bottom: 1rem; +} + +.mfaCodeInput:focus { + border-color: var(--primary-color, #4A90D9); + outline: none; +} + +.mfaSpinner { + display: inline-block; + width: 24px; + height: 24px; + border: 3px solid var(--border-color, #e0e0e0); + border-top-color: var(--primary-color, #4A90D9); + border-radius: 50%; + animation: mfaSpin 0.8s linear infinite; + margin: 1rem auto; +} + +@keyframes mfaSpin { + to { transform: rotate(360deg); } +} + /* Section */ .sectionContainer { background: var(--surface-color, #fff); diff --git a/src/pages/views/teamsbot/TeamsbotDashboardView.tsx b/src/pages/views/teamsbot/TeamsbotDashboardView.tsx index 1869be3..7bf1b88 100644 --- a/src/pages/views/teamsbot/TeamsbotDashboardView.tsx +++ b/src/pages/views/teamsbot/TeamsbotDashboardView.tsx @@ -2,13 +2,14 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; import * as teamsbotApi from '../../../api/teamsbotApi'; -import type { TeamsbotSession, StartSessionRequest, TeamsbotJoinMode } from '../../../api/teamsbotApi'; +import type { TeamsbotSession, StartSessionRequest, TeamsbotJoinMode, UserAccountStatus, MfaChallengeEvent } from '../../../api/teamsbotApi'; import { getUserDataCache } from '../../../utils/userCache'; import styles from './Teamsbot.module.css'; /** * TeamsbotDashboardView - Overview of all Teams Bot sessions. * Allows starting new sessions and viewing active/past sessions. + * Supports "Mein Account" login with saved credentials and MFA relay. */ export const TeamsbotDashboardView: React.FC = () => { const { instance, mandateId, featureCode } = useCurrentInstance(); @@ -29,6 +30,21 @@ export const TeamsbotDashboardView: React.FC = () => { const [sessionContext, setSessionContext] = useState(''); const [isStarting, setIsStarting] = useState(false); + // User Account (Mein Account) state + const [userAccount, setUserAccount] = useState(null); + const [showCredentialForm, setShowCredentialForm] = useState(false); + const [credEmail, setCredEmail] = useState(''); + const [credPassword, setCredPassword] = useState(''); + const [saveCredentials, setSaveCredentials] = useState(true); + const [savingCredentials, setSavingCredentials] = useState(false); + + // MFA state + const [mfaChallenge, setMfaChallenge] = useState(null); + const [mfaCode, setMfaCode] = useState(''); + const [mfaSessionId, setMfaSessionId] = useState(null); + const [mfaWaitingPush, setMfaWaitingPush] = useState(false); + const sseRef = useRef(null); + const _loadSessions = useCallback(async () => { if (!instanceId) return; try { @@ -47,6 +63,13 @@ export const TeamsbotDashboardView: React.FC = () => { _loadSessions(); }, [_loadSessions]); + // Load user account status when joinMode changes to userAccount + useEffect(() => { + if (joinMode === 'userAccount' && instanceId) { + teamsbotApi.getUserAccount(instanceId).then(setUserAccount).catch(() => setUserAccount(null)); + } + }, [joinMode, instanceId]); + // Auto-refresh: poll every 10s when there are active sessions const pollRef = useRef | null>(null); useEffect(() => { @@ -59,12 +82,73 @@ export const TeamsbotDashboardView: React.FC = () => { return () => { if (pollRef.current) clearInterval(pollRef.current); }; }, [sessions, instanceId]); + // Cleanup SSE on unmount + useEffect(() => { + return () => { sseRef.current?.close(); }; + }, []); + + const _startMfaListener = useCallback((sessionId: string) => { + sseRef.current?.close(); + const es = teamsbotApi.createSessionStream(instanceId, sessionId); + sseRef.current = es; + setMfaSessionId(sessionId); + + es.onmessage = (event) => { + try { + const parsed = JSON.parse(event.data); + if (parsed.type === 'mfaChallenge') { + const data = parsed.data as MfaChallengeEvent; + if (data.mfaType === 'timeout') { + setMfaChallenge(null); + setMfaWaitingPush(false); + setError('MFA-Zeitlimit ueberschritten. Bitte erneut versuchen.'); + } else { + setMfaChallenge(data); + setMfaCode(''); + setMfaWaitingPush(data.mfaType === 'pushApproval' || data.mfaType === 'numberMatch'); + } + } else if (parsed.type === 'mfaResolved') { + setMfaChallenge(null); + setMfaWaitingPush(false); + setMfaSessionId(null); + es.close(); + sseRef.current = null; + _loadSessions(); + } + } catch { /* ignore parse errors */ } + }; + }, [instanceId, _loadSessions]); + const _handleStartSession = async () => { if (!meetingLink.trim()) return; - + + // For userAccount: need credentials (saved or entered now) + if (joinMode === 'userAccount' && !userAccount?.hasSavedCredentials && !credEmail) { + setShowCredentialForm(true); + return; + } + + // Save credentials if requested (first-time entry) + if (joinMode === 'userAccount' && showCredentialForm && credEmail && credPassword) { + if (saveCredentials) { + try { + setSavingCredentials(true); + await teamsbotApi.saveUserAccount(instanceId, credEmail, credPassword); + setUserAccount({ hasSavedCredentials: true, email: credEmail }); + } catch (err: any) { + setError(err.message || 'Fehler beim Speichern der Zugangsdaten'); + setSavingCredentials(false); + return; + } finally { + setSavingCredentials(false); + } + } + setShowCredentialForm(false); + } + setIsStarting(true); setError(null); - + try { const request: StartSessionRequest = { meetingLink: meetingLink.trim(), @@ -72,10 +156,17 @@ export const TeamsbotDashboardView: React.FC = () => { joinMode: joinMode, sessionContext: sessionContext.trim() || undefined, }; - - await teamsbotApi.startSession(instanceId, request); + + const result = await teamsbotApi.startSession(instanceId, request); + const newSessionId = result.session?.id; setMeetingLink(''); setBotName(''); + + // Start SSE listener for MFA events if userAccount mode + if (joinMode === 'userAccount' && newSessionId) { + _startMfaListener(newSessionId); + } + await _loadSessions(); } catch (err: any) { setError(err.message || 'Fehler beim Starten der Sitzung'); @@ -84,6 +175,35 @@ export const TeamsbotDashboardView: React.FC = () => { } }; + const _handleSubmitMfaCode = async () => { + if (!mfaSessionId || !instanceId) return; + const needsCode = mfaChallenge?.mfaType === 'totpCode' || mfaChallenge?.mfaType === 'smsCode'; + try { + await teamsbotApi.submitMfaCode( + instanceId, + mfaSessionId, + needsCode ? mfaCode : '', + needsCode ? 'code' : 'confirmed', + ); + if (!needsCode) { + setMfaWaitingPush(true); + } + } catch (err: any) { + setError(err.message || 'Fehler beim Senden des MFA-Codes'); + } + }; + + const _handleDeleteUserAccount = async () => { + try { + await teamsbotApi.deleteUserAccount(instanceId); + setUserAccount({ hasSavedCredentials: false }); + setCredEmail(''); + setCredPassword(''); + } catch (err: any) { + setError(err.message || 'Fehler beim Loeschen der Zugangsdaten'); + } + }; + const _handleStopSession = async (sessionId: string) => { try { await teamsbotApi.stopSession(instanceId, sessionId); @@ -129,8 +249,53 @@ export const TeamsbotDashboardView: React.FC = () => { const activeSessions = sessions.filter(s => ['pending', 'joining', 'active'].includes(s.status)); const pastSessions = sessions.filter(s => ['ended', 'error', 'leaving'].includes(s.status)); + const _needsCodeInput = mfaChallenge?.mfaType === 'totpCode' || mfaChallenge?.mfaType === 'smsCode'; + return (
+ {/* MFA Challenge Dialog */} + {mfaChallenge && ( +
+
+
Multi-Faktor-Authentifizierung
+ + {mfaChallenge.displayNumber && ( +
{mfaChallenge.displayNumber}
+ )} + +
{mfaChallenge.prompt}
+ + {_needsCodeInput ? ( + <> + setMfaCode(e.target.value)} + autoFocus + onKeyDown={(e) => e.key === 'Enter' && _handleSubmitMfaCode()} + /> + + + ) : mfaWaitingPush ? ( + <> +
+

+ Warte auf Bestaetigung in der Authenticator App... +

+ + ) : ( + + )} +
+
+ )} + {/* Start New Session Card */}

Neue Bot-Sitzung starten

@@ -164,6 +329,73 @@ export const TeamsbotDashboardView: React.FC = () => {
+ {/* User Account: saved credentials info or credential form */} + {joinMode === 'userAccount' && ( +
+ {userAccount?.hasSavedCredentials && !showCredentialForm ? ( +
+ + Gespeichert: {userAccount.email} + +
+ + +
+
+ ) : ( + <> +
+ + setCredEmail(e.target.value)} + disabled={savingCredentials} + /> +
+
+ + setCredPassword(e.target.value)} + disabled={savingCredentials} + /> +
+
+ setSaveCredentials(e.target.checked)} + /> + +
+ {userAccount?.hasSavedCredentials && ( + + )} + + )} +
+ )} +
{