feat(core): sync framework security hardening and accessibility improvements

- add util.Go with panic recovery for background goroutines
- add util.EscapeLike and explicit ESCAPE clause for SQL LIKE queries
- add DummyCheckPassword and subtle.ConstantTimeCompare against timing attacks
- enforce session ID rotation upon login/oauth callback to prevent session fixation
- add sliding window login failure rate limiting and oauth state rate limiting
- fix redis client capture race in pubsub listeners and wait on stop channel
- adjust global --primary to oklch(51.1% 0.262 276.966) for WCAG AA contrast
- fix semantic heading levels and missing aria-labels across UI components
- document security, concurrency, and a11y standards in AGENTS.md
This commit is contained in:
ryan
2026-08-27 23:01:28 +08:00
parent b66cf3ae9c
commit ae3b792e16
63 changed files with 570 additions and 176 deletions
@@ -189,9 +189,9 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
<div className='grid grid-cols-1 lg:grid-cols-5 gap-6'>
{/* 左边:状态区 (2/5 cols) */}
<div className='lg:col-span-2 space-y-4'>
<h4 className='text-xs font-semibold text-muted-foreground uppercase tracking-wider'>
<p className='text-xs font-semibold text-muted-foreground uppercase tracking-wider'>
{t('runtimeStatus')}
</h4>
</p>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
{/* 已占空间 */}
<div className='p-4 rounded-xl border border-border/40 bg-background/30 backdrop-blur-xs hover:border-primary/20 transition-all duration-300'>
@@ -240,9 +240,9 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
{/* 右边:配置区 (3/5 cols) */}
<div className='lg:col-span-3 border-t lg:border-t-0 lg:border-l border-border/40 pt-6 lg:pt-0 lg:pl-6 space-y-4'>
<h4 className='text-xs font-semibold text-muted-foreground uppercase tracking-wider'>
<p className='text-xs font-semibold text-muted-foreground uppercase tracking-wider'>
{t('policyConfig')}
</h4>
</p>
<form onSubmit={handleSaveConfig} className='space-y-4'>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
<div className='space-y-1.5'>
@@ -165,7 +165,10 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {
{t('quickTemplates')}
</span>
<Select onValueChange={handlePresetSQLChange}>
<SelectTrigger className='h-7 w-[180px] text-[11px] bg-background'>
<SelectTrigger
aria-label={t('selectPresetSQL')}
className='h-7 w-[180px] text-[11px] bg-background'
>
<SelectValue placeholder={t('selectPresetSQL')} />
</SelectTrigger>
<SelectContent>
@@ -140,7 +140,10 @@ export function TableBrowser({
<Skeleton className='h-8 w-48' />
) : (
<Select value={selectedTable} onValueChange={handleTableChange}>
<SelectTrigger className='h-8 w-[200px] text-xs bg-background border-border/40'>
<SelectTrigger
aria-label={t('selectTable')}
className='h-8 w-[200px] text-xs bg-background border-border/40'
>
<SelectValue placeholder={t('selectTable')} />
</SelectTrigger>
<SelectContent className='max-h-[300px]'>
@@ -227,6 +227,7 @@ export function AccessAnalytics() {
variant='ghost'
size='icon'
className='size-8'
aria-label={t('refresh')}
onClick={fetchAnalytics}
disabled={loading}
>
@@ -435,6 +435,7 @@ export function EventsTab() {
onCheckedChange={() =>
toggleEventMutation.mutate(event.id)
}
aria-label={t('colStatus')}
className='scale-75'
/>
</TableCell>
@@ -449,6 +450,7 @@ export function EventsTab() {
<Button
variant='ghost'
size='icon'
aria-label={t('configure')}
className='h-6 w-6 text-muted-foreground hover:text-foreground'
onClick={() => handleEditEventClick(event)}
>
@@ -467,6 +469,7 @@ export function EventsTab() {
<Button
variant='ghost'
size='icon'
aria-label={t('delete')}
className='h-6 w-6 text-muted-foreground hover:text-destructive hover:bg-destructive/10'
disabled={deleteEventMutation.isPending}
onClick={() => setDeleteTarget(event)}
+22 -15
View File
@@ -8,10 +8,9 @@ import {
ManageDetailPanel,
ManagePage,
} from '@/components/common/general/manage-pannel';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ShieldCheck } from 'lucide-react';
import { formatDateTime } from '@/lib/utils';
import { cn, formatDateTime } from '@/lib/utils';
import type { SystemConfig } from '@/lib/services/admin';
import { AdminProvider, useAdmin } from '@/contexts/admin-context';
@@ -255,20 +254,28 @@ export function SystemConfigs() {
emptyDescription={t('emptyDescription')}
loadingDescription={t('loadingDescription')}
headerExtra={
<Tabs
value={activeTab}
onValueChange={(val) => setActiveTab(val as 'system' | 'business')}
className='w-[180px]'
<div
role='group'
aria-label={t('configType')}
className='grid w-[180px] grid-cols-2 h-8 rounded-md border border-input bg-muted/40 p-0.5'
>
<TabsList className='grid w-full grid-cols-2 h-8'>
<TabsTrigger value='business' className='text-[11px] h-7'>
{t('businessConfig')}
</TabsTrigger>
<TabsTrigger value='system' className='text-[11px] h-7'>
{t('systemConfig')}
</TabsTrigger>
</TabsList>
</Tabs>
{(['business', 'system'] as const).map((tab) => (
<button
key={tab}
type='button'
onClick={() => setActiveTab(tab)}
aria-pressed={activeTab === tab}
className={cn(
'h-full rounded-sm text-[11px] font-medium transition-colors',
activeTab === tab
? 'bg-background shadow-sm text-foreground'
: 'text-muted-foreground hover:text-foreground',
)}
>
{tab === 'business' ? t('businessConfig') : t('systemConfig')}
</button>
))}
</div>
}
columns={[
{
@@ -372,9 +372,9 @@ export function TaskManager() {
<div className='space-y-2'>
<div className='flex items-start justify-between'>
<div className='space-y-1'>
<h3 className='font-semibold text-base tracking-tight'>
<p className='font-semibold text-base tracking-tight'>
{task.name}
</h3>
</p>
<p className='text-xs text-muted-foreground leading-relaxed line-clamp-2 min-h-[36px]'>
{task.description}
</p>
@@ -104,9 +104,9 @@ export function UserDetailSheet({
<div className='p-6 space-y-6'>
<div className='space-y-4'>
<h4 className='text-xs font-semibold text-muted-foreground uppercase tracking-wider px-1'>
<p className='text-xs font-semibold text-muted-foreground uppercase tracking-wider px-1'>
{t('personalInfo')}
</h4>
</p>
<div className='rounded-lg border divide-y bg-background/50'>
<div className='flex items-center justify-between gap-4 p-3.5 text-sm'>
<span className='flex items-center gap-2 text-[10px] text-muted-foreground'>
@@ -167,9 +167,9 @@ export function UserDetailSheet({
</div>
<div className='space-y-4'>
<h4 className='text-xs font-semibold text-muted-foreground uppercase tracking-wider px-1'>
<p className='text-xs font-semibold text-muted-foreground uppercase tracking-wider px-1'>
{t('systemRecords')}
</h4>
</p>
<div className='rounded-lg border divide-y bg-background/50'>
<div className='flex items-center justify-between p-3.5 text-sm'>
<span className='text-[10px]'>
@@ -26,6 +26,7 @@ import { cn } from '@/lib/utils';
export function UserFilterBar() {
const t = useTranslations('admin.users');
const tCommon = useTranslations('common');
const {
total,
loading,
@@ -223,6 +224,7 @@ export function UserFilterBar() {
<Button
variant='ghost'
size='icon'
aria-label={tCommon('previousPage')}
className='h-5.5 w-6 rounded-none rounded-l-md disabled:opacity-30'
onClick={() => setPage(Math.max(1, page - 1))}
disabled={page <= 1 || loading}
@@ -235,6 +237,7 @@ export function UserFilterBar() {
<Button
variant='ghost'
size='icon'
aria-label={tCommon('nextPage')}
className='h-5.5 w-6 rounded-none rounded-r-md disabled:opacity-30'
onClick={() => setPage(Math.min(totalPages, page + 1))}
disabled={page >= totalPages || loading}
+2 -1
View File
@@ -55,7 +55,8 @@
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(58.5% 51% 277.117);
/* indigo-600: 亮色下与 primary-foreground(#fafafa) 对比度 ~6.8,满足 WCAG AA;indigo-500 仅 4.27 */
--primary: oklch(51.1% 0.262 276.966);
--primary-foreground: oklch(98.5% 0% 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);