mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 23:26:38 +08:00
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:
@@ -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)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -219,7 +219,7 @@ export function AccessTokenMain() {
|
||||
</div>
|
||||
|
||||
{/* 安全警告提示 */}
|
||||
<div className='rounded-xl border border-amber-500/20 bg-amber-500/5 p-4 flex gap-3 text-amber-600 text-xs leading-relaxed'>
|
||||
<div className='rounded-xl border border-amber-500/20 bg-amber-500/5 p-4 flex gap-3 text-amber-700 text-xs leading-relaxed'>
|
||||
<AlertTriangle className='size-4 shrink-0 mt-0.5' />
|
||||
<div className='space-y-1'>
|
||||
<span className='font-bold'>{ta('securityTitle')}</span>
|
||||
|
||||
@@ -23,6 +23,7 @@ export function NotificationsMain() {
|
||||
return (
|
||||
<div className='py-6 space-y-6'>
|
||||
<div className='font-semibold'>
|
||||
<h1 className='sr-only'>{tn('breadcrumb')}</h1>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
|
||||
@@ -238,7 +238,7 @@ export function UserFileManager() {
|
||||
<Upload className='size-10' />
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
<h3 className='font-semibold text-sm'>{t('noFiles')}</h3>
|
||||
<p className='font-semibold text-sm'>{t('noFiles')}</p>
|
||||
<p className='text-xs text-muted-foreground max-w-xs'>
|
||||
{debouncedKeyword ? t('noMatchingFiles') : t('uploadFirstFile')}
|
||||
</p>
|
||||
|
||||
@@ -81,7 +81,7 @@ export function EmptyState({
|
||||
</div>
|
||||
|
||||
{displayTitle && (
|
||||
<h3 className='text-base font-medium mb-1'>{displayTitle}</h3>
|
||||
<p className='text-base font-medium mb-1'>{displayTitle}</p>
|
||||
)}
|
||||
|
||||
{displayDescription && (
|
||||
|
||||
@@ -74,7 +74,7 @@ export function ErrorDisplay({
|
||||
<Icon className='size-6 text-red-600 dark:text-red-400' />
|
||||
</div>
|
||||
|
||||
<h3 className='text-lg font-semibold mb-2'>{displayTitle}</h3>
|
||||
<p className='text-lg font-semibold mb-2'>{displayTitle}</p>
|
||||
|
||||
<p className='text-sm text-muted-foreground max-w-md mb-4'>
|
||||
{errorMessage}
|
||||
|
||||
@@ -160,7 +160,7 @@ export function SiteHeader({
|
||||
<Search className='absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground' />
|
||||
<div className='flex h-8 items-center rounded-md border border-border/60 bg-muted/70 pl-10 pr-2.5 text-sm text-muted-foreground transition-colors hover:border-border hover:bg-muted'>
|
||||
<span>{t('search')}</span>
|
||||
<Kbd className='ml-auto gap-0.5 font-mono'>
|
||||
<Kbd className='ml-auto gap-0.5 font-mono text-foreground/70'>
|
||||
<span>{metaKey}</span>
|
||||
<span>K</span>
|
||||
</Kbd>
|
||||
|
||||
@@ -120,9 +120,7 @@ export function LoadingState({
|
||||
</div>
|
||||
|
||||
{displayTitle && (
|
||||
<h3 className='text-sm font-medium mb-1 animate-pulse'>
|
||||
{displayTitle}
|
||||
</h3>
|
||||
<p className='text-sm font-medium mb-1 animate-pulse'>{displayTitle}</p>
|
||||
)}
|
||||
|
||||
{displayDescription && (
|
||||
|
||||
@@ -217,12 +217,15 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<Sidebar
|
||||
collapsible='icon'
|
||||
{...props}
|
||||
role='navigation'
|
||||
aria-label={t('mainNavigation')}
|
||||
className='px-2 relative border-r border-border/40 group-data-[collapsible=icon]'
|
||||
>
|
||||
<Button
|
||||
onClick={toggleSidebar}
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
aria-label={t('toggleSidebar')}
|
||||
className='absolute top-1/2 -right-6 w-2 h-4 text-muted-foreground hover:bg-background hidden md:flex'
|
||||
>
|
||||
{state === 'expanded' ? (
|
||||
|
||||
@@ -4559,4 +4559,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -10,7 +10,9 @@
|
||||
"settings": "Settings",
|
||||
"search": "Search",
|
||||
"unknownError": "Something went wrong. Please try again.",
|
||||
"loadFailed": "Load failed"
|
||||
"loadFailed": "Load failed",
|
||||
"previousPage": "Previous page",
|
||||
"nextPage": "Next page"
|
||||
},
|
||||
"layout": {
|
||||
"nav": {
|
||||
@@ -94,7 +96,9 @@
|
||||
"empty": {
|
||||
"noData": "No data",
|
||||
"noContent": "No records found"
|
||||
}
|
||||
},
|
||||
"toggleSidebar": "Toggle sidebar",
|
||||
"mainNavigation": "Main navigation"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
@@ -1243,7 +1247,8 @@
|
||||
"noActiveUsersData": "No active user data",
|
||||
"unknown": "Unknown",
|
||||
"noNickname": "(no nickname)",
|
||||
"requests": "requests"
|
||||
"requests": "requests",
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
"appLogs": {
|
||||
"loading": "Loading...",
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
"settings": "设置",
|
||||
"search": "搜索",
|
||||
"unknownError": "发生错误,请重试",
|
||||
"loadFailed": "加载失败"
|
||||
"loadFailed": "加载失败",
|
||||
"previousPage": "上一页",
|
||||
"nextPage": "下一页"
|
||||
},
|
||||
"layout": {
|
||||
"nav": {
|
||||
@@ -94,7 +96,9 @@
|
||||
"empty": {
|
||||
"noData": "暂无数据",
|
||||
"noContent": "当前没有任何记录"
|
||||
}
|
||||
},
|
||||
"toggleSidebar": "切换侧边栏",
|
||||
"mainNavigation": "主导航"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
@@ -1243,7 +1247,8 @@
|
||||
"noActiveUsersData": "暂无活跃用户数据",
|
||||
"unknown": "未知",
|
||||
"noNickname": "(无昵称)",
|
||||
"requests": "次请求"
|
||||
"requests": "次请求",
|
||||
"refresh": "刷新"
|
||||
},
|
||||
"appLogs": {
|
||||
"loading": "加载中...",
|
||||
|
||||
Reference in New Issue
Block a user