- 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.
93 lines
3.5 KiB
TypeScript
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>
|
|
);
|
|
}
|