feat(framework): 回灌 OpenFlare 分层、安全与运行时改进

将平台域持久化收敛为 repository 唯一入口,model 去掉 IO。
邮件头写入前清除 CR/LF,防止 header 注入。
httppool 支持可配置 Transport;batchwriter 增加 MinBatchSize/Stats,flush 失败交回批次;任务 PermanentError 作为 SkipRetry 终态。
设置与推送页的确认改为 AlertDialog;axios 去尾斜杠并按 Gin 数组序列化查询参数。
升级共享 Go 依赖(Gin、Asynq、OTel、GORM、Redis 等)。
This commit is contained in:
ryan
2026-08-16 11:07:20 +08:00
parent b9b42e3174
commit 6a53619dd2
58 changed files with 2201 additions and 1175 deletions
@@ -37,6 +37,16 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import {
Breadcrumb,
BreadcrumbItem,
@@ -53,6 +63,8 @@ import { useLocale, useTranslations } from 'next-intl';
import { formatDateTime } from '@/i18n/format';
import type { AppLocale } from '@/i18n/config';
type ConfirmTarget = { id: number; name: string };
export function AccessTokenMain() {
const { user } = useAuth();
const queryClient = useQueryClient();
@@ -67,6 +79,12 @@ export function AccessTokenMain() {
const [copiedId, setCopiedId] = React.useState<number | null>(null);
const [newCreatedToken, setNewCreatedToken] =
React.useState<CreateTokenResponse | null>(null);
const [deleteTarget, setDeleteTarget] = React.useState<ConfirmTarget | null>(
null,
);
const [rotateTarget, setRotateTarget] = React.useState<ConfirmTarget | null>(
null,
);
// 获取 Token 列表
const accessTokensQuery = useQuery({
@@ -98,6 +116,7 @@ export function AccessTokenMain() {
const deleteTokenMutation = useMutation({
mutationFn: (id: number) => UserService.deleteAccessToken(id),
onSuccess: () => {
setDeleteTarget(null);
void queryClient.invalidateQueries({
queryKey: ['user', 'access-tokens'],
});
@@ -112,6 +131,7 @@ export function AccessTokenMain() {
const rotateTokenMutation = useMutation({
mutationFn: (id: number) => UserService.rotateAccessToken(id),
onSuccess: (data) => {
setRotateTarget(null);
setNewCreatedToken(data);
setViewDialogOpen(true);
void queryClient.invalidateQueries({
@@ -136,18 +156,6 @@ export function AccessTokenMain() {
});
};
const handleDeleteToken = (id: number, name: string) => {
if (window.confirm(ta('deleteConfirm', { name }))) {
deleteTokenMutation.mutate(id);
}
};
const handleRotateToken = (id: number, name: string) => {
if (window.confirm(ta('rotateConfirm', { name }))) {
rotateTokenMutation.mutate(id);
}
};
const handleCopyText = async (text: string, id: number) => {
try {
await navigator.clipboard.writeText(text);
@@ -297,7 +305,9 @@ export function AccessTokenMain() {
variant='outline'
size='sm'
className='text-xs border-dashed text-muted-foreground hover:text-primary hover:bg-primary/5 rounded-lg h-8 px-2.5'
onClick={() => handleRotateToken(token.id, token.name)}
onClick={() =>
setRotateTarget({ id: token.id, name: token.name })
}
disabled={rotateTokenMutation.isPending}
>
<RefreshCw
@@ -310,7 +320,9 @@ export function AccessTokenMain() {
variant='outline'
size='sm'
className='text-xs border-dashed text-muted-foreground hover:text-rose-500 hover:bg-rose-500/10 hover:border-rose-500/20 rounded-lg h-8 px-2.5'
onClick={() => handleDeleteToken(token.id, token.name)}
onClick={() =>
setDeleteTarget({ id: token.id, name: token.name })
}
disabled={deleteTokenMutation.isPending}
>
<Trash2 className='size-3.5 mr-1' />
@@ -482,6 +494,64 @@ export function AccessTokenMain() {
</DialogFooter>
</DialogContent>
</Dialog>
<AlertDialog
open={Boolean(deleteTarget)}
onOpenChange={(open) => !open && setDeleteTarget(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{ta('deleteConfirmTitle')}</AlertDialogTitle>
<AlertDialogDescription>
{ta('deleteConfirm', { name: deleteTarget?.name ?? '' })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deleteTokenMutation.isPending}>
{tCommon('cancel')}
</AlertDialogCancel>
<AlertDialogAction
disabled={deleteTokenMutation.isPending}
onClick={() =>
deleteTarget && deleteTokenMutation.mutate(deleteTarget.id)
}
>
{deleteTokenMutation.isPending
? ta('revoking')
: ta('confirmRevoke')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog
open={Boolean(rotateTarget)}
onOpenChange={(open) => !open && setRotateTarget(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{ta('rotateConfirmTitle')}</AlertDialogTitle>
<AlertDialogDescription>
{ta('rotateConfirm', { name: rotateTarget?.name ?? '' })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={rotateTokenMutation.isPending}>
{tCommon('cancel')}
</AlertDialogCancel>
<AlertDialogAction
disabled={rotateTokenMutation.isPending}
onClick={() =>
rotateTarget && rotateTokenMutation.mutate(rotateTarget.id)
}
>
{rotateTokenMutation.isPending
? ta('rotating')
: ta('confirmRotate')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</motion.div>
);
}
@@ -40,6 +40,16 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { AuthSourceModal } from '@/components/common/settings/auth-source-modal';
import services from '@/lib/services';
import type { AuthSource, SystemConfig } from '@/lib/services/admin';
@@ -93,8 +103,10 @@ interface SecurityTabProps {
export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
const queryClient = useQueryClient();
const t = useTranslations('settings.security');
const tCommon = useTranslations('common');
const [authSourceModalOpen, setAuthSourceModalOpen] = useState(false);
const [selectedSource, setSelectedSource] = useState<AuthSource | null>(null);
const [deleteTarget, setDeleteTarget] = useState<AuthSource | null>(null);
const [capCount, setCapCount] = useState('');
const [capDifficulty, setCapDifficulty] = useState('');
@@ -233,6 +245,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
await services.adminAuthSource.deleteAuthSource(sourceId);
},
onSuccess: async () => {
setDeleteTarget(null);
await queryClient.invalidateQueries({ queryKey: ['auth', 'sources'] });
await queryClient.invalidateQueries({
queryKey: ['auth', 'public-sources'],
@@ -490,17 +503,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
size='icon'
className='size-8 text-muted-foreground hover:text-rose-500 hover:bg-rose-500/10 rounded-lg transition-colors'
disabled={deleteSourceMutation.isPending}
onClick={() => {
if (
window.confirm(
t('deleteAuthSourceConfirm', {
name: source.display_name || source.name,
}),
)
) {
deleteSourceMutation.mutate(source.id);
}
}}
onClick={() => setDeleteTarget(source)}
>
<Trash2 className='size-4' />
</Button>
@@ -706,6 +709,37 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
await authSourcesQuery.refetch();
}}
/>
<AlertDialog
open={Boolean(deleteTarget)}
onOpenChange={(open) => !open && setDeleteTarget(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('deleteAuthSourceTitle')}</AlertDialogTitle>
<AlertDialogDescription>
{t('deleteAuthSourceConfirm', {
name: deleteTarget?.display_name || deleteTarget?.name || '',
})}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deleteSourceMutation.isPending}>
{tCommon('cancel')}
</AlertDialogCancel>
<AlertDialogAction
disabled={deleteSourceMutation.isPending}
onClick={() =>
deleteTarget && deleteSourceMutation.mutate(deleteTarget.id)
}
>
{deleteSourceMutation.isPending
? t('deleting')
: t('confirmDelete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
@@ -23,6 +23,16 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import services from '@/lib/services';
import type { Template } from '@/lib/services/admin/types';
import { toast } from 'sonner';
@@ -35,6 +45,7 @@ export function TemplatesManager() {
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
null,
);
const [deleteTarget, setDeleteTarget] = useState<Template | null>(null);
// Form states
const [key, setKey] = useState('');
@@ -95,6 +106,7 @@ export function TemplatesManager() {
await services.adminTemplate.deleteTemplate(key);
},
onSuccess: async () => {
setDeleteTarget(null);
await queryClient.invalidateQueries({ queryKey: ['admin', 'templates'] });
toast.success(t('templateDeleted'));
},
@@ -225,15 +237,7 @@ export function TemplatesManager() {
disabled={
tmpl.is_system || deleteTemplateMutation.isPending
}
onClick={() => {
if (
window.confirm(
t('deleteTemplateConfirm', { name: tmpl.name }),
)
) {
deleteTemplateMutation.mutate(tmpl.key);
}
}}
onClick={() => setDeleteTarget(tmpl)}
>
<Trash2 className='size-4' />
</Button>
@@ -401,6 +405,35 @@ export function TemplatesManager() {
</form>
</DialogContent>
</Dialog>
<AlertDialog
open={Boolean(deleteTarget)}
onOpenChange={(open) => !open && setDeleteTarget(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('deleteTemplateTitle')}</AlertDialogTitle>
<AlertDialogDescription>
{t('deleteTemplateConfirm', { name: deleteTarget?.name ?? '' })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deleteTemplateMutation.isPending}>
{t('cancel')}
</AlertDialogCancel>
<AlertDialogAction
disabled={deleteTemplateMutation.isPending}
onClick={() =>
deleteTarget && deleteTemplateMutation.mutate(deleteTarget.key)
}
>
{deleteTemplateMutation.isPending
? t('deleting')
: t('confirmDelete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}