Files
hamed 6876135a53 feat: add BlogBodySanitizer for HTML sanitization on article save
- Implemented BlogBodySanitizer to clean HTML content before saving articles, ensuring security against XSS attacks.
- Added tests for BlogBodySanitizer to verify that unsafe tags and attributes are stripped from the content.
- Introduced ApiLeastPrivilegeTest to ensure that unauthorized users cannot access sensitive API routes, maintaining strict access control.
2026-08-07 21:13:38 +03:30

93 lines
3.5 KiB
TypeScript

import { useNavigate } from 'react-router';
import { useState } from 'react';
import { useAuthStore, ContextItem } from '../stores/authStore';
const ROLE_LABELS: Record<string, string> = {
admin: 'مدیر کل',
clinic: 'مالک کلینیک',
doctor: 'پزشک',
secretary: 'منشی',
user: 'کاربر',
};
const TYPE_ICONS: Record<string, string> = {
doctor: '🏥',
clinic: '🏢',
};
export default function SelectContextPage() {
const { availableContexts, switchContext } = useAuthStore();
const navigate = useNavigate();
const [loading, setLoading] = useState<string | null>(null);
const handleSelect = async (ctx: ContextItem) => {
setLoading(ctx.db_uuid);
await switchContext(ctx.db_uuid);
navigate('/admin/dashboard', { replace: true });
};
return (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--surface-alt, #f4f6f9)',
padding: '2rem',
}}>
<div style={{ width: '100%', maxWidth: 480 }}>
<div className="card card-pad" style={{ textAlign: 'center' }}>
<h2 style={{ marginBottom: '.25rem', fontSize: '1.25rem', fontWeight: 600 }}>
انتخاب محیط کاری
</h2>
<p className="muted" style={{ marginBottom: '1.5rem', fontSize: '.875rem' }}>
لطفاً محیط کاری مورد نظر خود را انتخاب کنید
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '.75rem' }}>
{availableContexts.map((ctx) => (
<button
key={ctx.db_uuid}
onClick={() => handleSelect(ctx)}
disabled={loading !== null}
style={{
display: 'flex',
alignItems: 'center',
gap: '1rem',
padding: '.875rem 1rem',
border: '1.5px solid var(--border, #e2e8f0)',
borderRadius: '0.625rem',
background: loading === ctx.db_uuid ? 'var(--surface-alt, #f4f6f9)' : 'var(--surface)',
cursor: loading !== null ? 'wait' : 'pointer',
textAlign: 'right',
transition: 'border-color .15s, box-shadow .15s',
opacity: loading !== null && loading !== ctx.db_uuid ? 0.5 : 1,
}}
onMouseEnter={e => (e.currentTarget.style.borderColor = 'var(--primary, #6366f1)')}
onMouseLeave={e => (e.currentTarget.style.borderColor = 'var(--border, #e2e8f0)')}
>
<span style={{ fontSize: '1.5rem', lineHeight: 1 }}>
{TYPE_ICONS[ctx.type] ?? '👤'}
</span>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, fontSize: '.9375rem', marginBottom: '.125rem' }}>
{ctx.name}
</div>
<div style={{ fontSize: '.8125rem' }}>
<span className="badge blue" style={{ fontSize: '.75rem' }}>
{ROLE_LABELS[ctx.role] ?? ctx.role}
</span>
</div>
</div>
{loading === ctx.db_uuid && (
<span className="skeleton" style={{ width: 20, height: 20, borderRadius: '50%' }} />
)}
</button>
))}
</div>
</div>
</div>
</div>
);
}