diff --git a/frontend/src/components/PanicListener.tsx b/frontend/src/components/PanicListener.tsx index 80564ea..c080394 100644 --- a/frontend/src/components/PanicListener.tsx +++ b/frontend/src/components/PanicListener.tsx @@ -143,8 +143,8 @@ const PanicListener: React.FC = ({ onPanic }) => { if (currentKeys.length !== shortcut.length) return; // Sort and compare - const normalizedPressed = currentKeys.map(k => k.toLowerCase()).sort(); - const normalizedShortcut = shortcut.map(k => k.toLowerCase()).sort(); + const normalizedPressed = (currentKeys ?? []).map(k => k.toLowerCase()).sort((a, b) => a - b); + const normalizedShortcut = shortcut.map(k => k.toLowerCase()).sort((a, b) => a - b); const matches = normalizedPressed.every((key, index) => key === normalizedShortcut[index]); diff --git a/frontend/src/features/docs/components/DocViewer.tsx b/frontend/src/features/docs/components/DocViewer.tsx index bc4cef4..a325ad7 100644 --- a/frontend/src/features/docs/components/DocViewer.tsx +++ b/frontend/src/features/docs/components/DocViewer.tsx @@ -315,262 +315,4 @@ let mermaidPromise: Promise | null = null; const loadMermaid = () => { if (!mermaidPromise) { mermaidPromise = import('mermaid').then(({ default: mermaid }) => { - mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'strict' }); - return mermaid; - }); - } - return mermaidPromise; -}; - -let mermaidIdCounter = 0; - -const MermaidDiagram = React.memo(({ chart }: { chart: string }) => { - const ref = useRef(null); - const idRef = useRef(`mermaid-diagram-${++mermaidIdCounter}`); - - useEffect(() => { - let cancelled = false; - - const render = async () => { - if (!ref.current) return; - - // Clean up orphaned mermaid SVG from a previous render with the same ID - const old = document.getElementById(idRef.current); - if (old) old.remove(); - - try { - const mermaid = await loadMermaid(); - if (cancelled || !ref.current) return; - const { svg, bindFunctions } = await mermaid.render(idRef.current, chart); - if (!cancelled && ref.current) { - ref.current.innerHTML = svg; - bindFunctions?.(ref.current); - } - } catch (err) { - console.error('Mermaid render error:', err); - if (!cancelled && ref.current) { - // XSS-safe error rendering - use textContent, never innerHTML with user input - ref.current.innerHTML = ''; - const pre = document.createElement('pre'); - pre.style.cssText = 'color:#f87171;font-size:12px;padding:8px;border:1px solid #f87171;border-radius:4px;white-space:pre-wrap;'; - pre.textContent = `Diagram parse error: ${err}`; - ref.current.appendChild(pre); - } - } - }; - render(); - - return () => { - cancelled = true; - }; - }, [chart]); - - return
; -}); - -MermaidDiagram.displayName = 'MermaidDiagram'; - -// ============================================================================ -// Main DocViewer Component -// ============================================================================ - -const DocViewer: React.FC = ({ filename, onContentLoad }) => { - const [content, setContent] = useState(''); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [notFound, setNotFound] = useState(false); - - const fetchDocument = useCallback(async () => { - setLoading(true); - setError(null); - setNotFound(false); - - try { - const response = await fetch(`/docs/${filename}`); - - if (!response.ok) { - if (response.status === 404) { - setNotFound(true); - return; - } - throw new Error(`Failed to fetch document (${response.status})`); - } - - const text = await response.text(); - setContent(text); - onContentLoad?.(text); - } catch (err) { - setError(err instanceof Error ? err.message : 'An unexpected error occurred'); - } finally { - setLoading(false); - } - }, [filename, onContentLoad]); - - useEffect(() => { - fetchDocument(); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }, [fetchDocument]); - - if (loading) return ; - if (notFound) return ; - if (error) return ; - - return ( -
- { - const id = String(children).toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-'); - return ( -

- {children} -

- ); - }, - h2: ({ children }) => { - const id = String(children).toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-'); - return ( -

- {children} -

- ); - }, - h3: ({ children }) => { - const id = String(children).toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-'); - return ( -

- {children} -

- ); - }, - h4: ({ children }) => ( -

- {children} -

- ), - - // Paragraphs - p: ({ children }) => ( -

- {children} -

- ), - - // Code blocks with syntax highlighting - code: ({ node, className, children, ...props }) => { - const match = /language-(\w+)/.exec(className || ''); - const language = match?.[1]; - const isInline = !match && !className; - - // Intercept mermaid code blocks and render as diagrams - if (language === 'mermaid') { - return ; - } - - if (isInline) { - return ( - - {children} - - ); - } - - return ( - - {String(children).replace(/\n$/, '')} - - ); - }, - - // Pre wrapper (handled by code block) - pre: ({ children }) => <>{children}, - - // Links - a: ({ href, children }) => {children}, - - // Lists - ul: ({ children }) => ( -
    - {children} -
- ), - ol: ({ children }) => ( -
    - {children} -
- ), - li: ({ children }) => ( -
  • {children}
  • - ), - - // Blockquotes (with callout detection) - blockquote: ({ children }) => { - // Check if it's a callout syntax - const text = String(children); - const calloutMatch = text.match(/\[!(NOTE|TIP|WARNING|DANGER|INFO)\]/i); - - if (calloutMatch) { - const type = calloutMatch[1].toLowerCase() as CalloutType; - const content = text.replace(/\[!(NOTE|TIP|WARNING|DANGER|INFO)\]/i, '').trim(); - return {content}; - } - - return ( -
    - {children} -
    - ); - }, - - // Tables - table: ({ children }) => ( -
    - - {children} -
    -
    - ), - thead: ({ children }) => ( - {children} - ), - th: ({ children }) => ( - - {children} - - ), - td: ({ children }) => ( - - {children} - - ), - - // Horizontal rule - hr: () =>
    , - - // Images - img: ({ src, alt }) => ( - {alt} - ), - - // Strong & Em - strong: ({ children }) => ( - {children} - ), - em: ({ children }) => ( - {children} - ), - }} - > - {content} -
    -
    - ); -}; - -export default DocViewer; + .catch(err => console.error(err)) \ No newline at end of file diff --git a/frontend/src/pages/OrganizationPage.tsx b/frontend/src/pages/OrganizationPage.tsx index 9f72d30..660710e 100644 --- a/frontend/src/pages/OrganizationPage.tsx +++ b/frontend/src/pages/OrganizationPage.tsx @@ -14,7 +14,7 @@ const OrganizationPage: React.FC = () => { // Create a placeholder organization object // ProfileManager will fetch actual data const organization: Organization = { - id: parseInt(id || '0'), + id: parseInt(id || '0', 10), category: categoryId, name: '', // Will be loaded by ProfileManager logo_url: null, diff --git a/frontend/src/pages/RegisterPage.tsx b/frontend/src/pages/RegisterPage.tsx index 36c4427..d5eea07 100644 --- a/frontend/src/pages/RegisterPage.tsx +++ b/frontend/src/pages/RegisterPage.tsx @@ -131,283 +131,4 @@ const RegisterPage: React.FC = () => { setIsCheckingUsername(true); const timerId = setTimeout(() => { checkUsername(username).then(data => { - setIsUsernameAvailable(!data.exists); - setIsCheckingUsername(false); - }); - }, 500); - return () => clearTimeout(timerId); - }, [username]); - - useEffect(() => { - const errors: string[] = []; - if (password.length < 8) errors.push("At least 8 characters"); - if (!/[a-z]/.test(password)) errors.push("At least one lowercase letter"); - if (!/[A-Z]/.test(password)) errors.push("At least one uppercase letter"); - if (!/[0-9]/.test(password)) errors.push("At least one number"); - if (!/[\W_]/.test(password)) errors.push("At least one special character"); - setPasswordErrors(errors); - }, [password]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(null); - setSuccess(null); - - if (!turnstileToken) { - setError('Please complete the verification challenge.'); - return; - } - - if (password !== password2) { - setError('Passwords do not match'); - return; - } - if (passwordErrors.length > 0) { - setError('Please fix the errors in your password.'); - return; - } - if (isUsernameAvailable === false) { - setError('This username is already taken.'); - return; - } - - setIsLoading(true); - try { - // Generate recovery key (for backup purposes) - const { recoveryKey: generatedRecoveryKey } = await initializeUserEncryption(); - - // Register account (generates salt internally and returns it) - const registerResponse = await register(username, email, password, password2, turnstileToken); - const salt = registerResponse.salt; - setSuccess('Account created successfully!'); - - // Store recovery key - storeKeyData(salt, generatedRecoveryKey); - - // Set auth token (already done by register function) - const authToken = registerResponse.key || registerResponse.token; - if (authToken) { - setToken(authToken); - - // Show recovery key modal - setRecoveryKey(generatedRecoveryKey); - setShowRecoveryModal(true); - } else { - setError('Registration succeeded but auto-login failed. Please log in manually.'); - } - } catch (err: unknown) { - const axiosError = err as { response?: { data?: { username?: string[]; email?: string[]; password?: string[]; error?: string } }; message?: string }; - if (axiosError.response?.data) { - const errors = axiosError.response.data; - const errorMessage = errors.username?.[0] || errors.email?.[0] || errors.password?.[0] || errors.error || 'Registration failed.'; - setError(errorMessage); - } else { - setError(axiosError.message || 'An unexpected error occurred.'); - } - } finally { - setIsLoading(false); - } - }; - - const handleRecoveryKeyConfirm = () => { - setShowRecoveryModal(false); - navigate('/', { replace: true }); - }; - - const renderUsernameFeedback = () => { - if (!username) return null; - if (isCheckingUsername) { - return ( -

    - - - - - Checking availability... -

    - ); - } - if (isUsernameAvailable === true) return

    Username is available

    ; - if (isUsernameAvailable === false) return

    Username is already taken

    ; - return null; - }; - - const renderPasswordFeedback = () => { - if (!password) return null; - const requirements = [ - { text: "8+ characters", valid: password.length >= 8 }, - { text: "Lowercase", valid: /[a-z]/.test(password) }, - { text: "Uppercase", valid: /[A-Z]/.test(password) }, - { text: "Number", valid: /[0-9]/.test(password) }, - { text: "Special char", valid: /[\W_]/.test(password) }, - ]; - - return ( -
    - {requirements.map(req => ( - - {req.valid ? : } - {req.text} - - ))} -
    - ); - }; - - return ( -
    -
    - {/* Logo and Title */} -
    -
    - -
    -

    Create an account

    -

    Start securing your credentials today

    -
    - - {/* Card */} -
    - {success && ( -
    - - {success} -
    - )} - - {error && ( -
    - - {error} -
    - )} - -
    -
    - -
    -
    - -
    - setUsername(e.target.value)} - className="as-input pl-10 h-11 md:h-12" - placeholder="Choose a username" - required - /> -
    - {renderUsernameFeedback()} -
    - -
    - -
    -
    - -
    - setEmail(e.target.value)} - className="as-input pl-10 h-11 md:h-12" - placeholder="Enter your email" - required - /> -
    -
    - -
    - - setPassword(e.target.value)} - className="h-11 md:h-12" - placeholder="Create a password" - autoComplete="new-password" - showLeftIcon - required - /> - {renderPasswordFeedback()} -
    - -
    - - setPassword2(e.target.value)} - className="h-11 md:h-12" - placeholder="Confirm your password" - autoComplete="new-password" - showLeftIcon - required - /> - {password2 && password !== password2 && ( -

    Passwords do not match

    - )} -
    - - {/* Cloudflare Turnstile */} -
    -
    -
    - - -
    - -
    -

    - Already have an account?{' '} - - Sign in - -

    -
    -
    - - {/* Footer */} -

    - Protected by end-to-end encryption -

    -
    - - {/* Recovery Key Modal */} - -
    - ); -}; - -export default RegisterPage; + .catch(err => console.error(err)) \ No newline at end of file diff --git a/frontend/src/services/securityService.ts b/frontend/src/services/securityService.ts index 3cd05a8..fd5b621 100644 --- a/frontend/src/services/securityService.ts +++ b/frontend/src/services/securityService.ts @@ -87,7 +87,7 @@ export const updatePasswordHash = async ( const msgBuffer = new TextEncoder().encode(password); const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + const hashHex = (hashArray ?? []).map(b => b.toString(16).padStart(2, '0')).join(''); await apiClient.post(`/security/profiles/${profileId}/hash/`, { password_hash: hashHex @@ -161,7 +161,7 @@ export const checkPasswordBreach = async ( if (hashSuffix.trim() === suffix) { return { isBreached: true, - breachCount: parseInt(count.trim(), 10) + breachCount: parseInt(count.trim(, 10), 10) }; } }