fix: harden runtime and remove placeholder features
This commit is contained in:
@@ -26,27 +26,22 @@
|
||||
"@radix-ui/react-tooltip": "^1.2.9",
|
||||
"@tanstack/react-query": "5.59.16",
|
||||
"@tiptap/core": "^3.27.0",
|
||||
"@tiptap/extension-color": "^3.27.0",
|
||||
"@tiptap/extension-font-family": "^3.27.0",
|
||||
"@tiptap/extension-highlight": "^3.27.0",
|
||||
"@tiptap/extension-image": "^3.27.0",
|
||||
"@tiptap/extension-link": "^3.27.0",
|
||||
"@tiptap/extension-placeholder": "^3.27.0",
|
||||
"@tiptap/extension-text-align": "^3.27.0",
|
||||
"@tiptap/extension-text-style": "^3.27.0",
|
||||
"@tiptap/extension-underline": "^3.27.0",
|
||||
"@tiptap/pm": "^3.27.0",
|
||||
"@tiptap/react": "^3.27.0",
|
||||
"@tiptap/starter-kit": "^3.27.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "2.1.1",
|
||||
"dompurify": "3.4.10",
|
||||
"dompurify": "3.4.12",
|
||||
"lucide-react": "^0.468.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "6.30.4",
|
||||
"react-router-dom": "7.18.2",
|
||||
"tailwind-merge": "2.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -55,7 +50,7 @@
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@vitejs/plugin-react": "6.0.2",
|
||||
"autoprefixer": "10.4.20",
|
||||
"postcss": "8.5.15",
|
||||
"postcss": "8.5.25",
|
||||
"tailwindcss": "3.4.15",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "5.6.3",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as React from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { GripVertical } from "lucide-react"
|
||||
import * as ResizablePrimitive from "react-resizable-panels"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ResizablePanelGroup = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
||||
<ResizablePrimitive.PanelGroup
|
||||
className={cn(
|
||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const ResizablePanel = ResizablePrimitive.Panel
|
||||
|
||||
const ResizableHandle = ({
|
||||
withHandle,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||
withHandle?: boolean
|
||||
}) => (
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
className={cn(
|
||||
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
||||
<GripVertical className="h-2.5 w-2.5" />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.PanelResizeHandle>
|
||||
)
|
||||
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { User, AdminUser, AdminOverview, Domain, Mailbox, Alias, MailFolder, Attachment, MailLabel, MailMessage, MailTranslation, DNSRecord, DNSCheckResult, ListResponse, SendPayload, DraftPayload, ScheduleSendPayload, ScheduledSend, SendQueueItem, SendQueueAuditEvent, SendQueueStatus, Contact, MailSignature, MailRule, MailRuleCondition, MailRuleAction, BlockedSender, MailStats, ForwardingSettings, ExternalImapAccount, ExternalImapAccountPayload, ExternalImapFolder, ExternalImapOAuthProvider, ExternalImapOAuthStartPayload, ExternalImapSyncRun, MailboxApplyOptions, MailTemplate, MaildirSyncHealth, SystemSettings, SystemSettingsPayload, SystemVersion, SystemUpdateResult, PublicSettings, LoginPayload, LoginResponse, RegisterPayload, PermissionGroup, PermissionInfo, PermissionKey, PermissionLimits, APIToken } from "./api-types"
|
||||
import type { User, AdminUser, AdminOverview, Domain, Mailbox, Alias, MailFolder, MailLabel, MailMessage, MailTranslation, DNSRecord, DNSCheckResult, ListResponse, SendPayload, DraftPayload, ScheduleSendPayload, ScheduledSend, SendQueueItem, SendQueueAuditEvent, SendQueueStatus, Contact, MailSignature, MailRule, MailRuleCondition, MailRuleAction, BlockedSender, MailStats, ForwardingSettings, ExternalImapAccount, ExternalImapAccountPayload, ExternalImapFolder, ExternalImapOAuthProvider, ExternalImapOAuthStartPayload, ExternalImapSyncRun, MailboxApplyOptions, MailTemplate, MaildirSyncHealth, SystemSettings, SystemSettingsPayload, SystemVersion, SystemUpdateResult, PublicSettings, LoginPayload, LoginResponse, RegisterPayload, PermissionGroup, PermissionInfo, PermissionKey, PermissionLimits, APIToken } from "./api-types"
|
||||
export * from "./api-types"
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 15_000
|
||||
|
||||
+10
-7
@@ -6,14 +6,15 @@ import { Toaster } from "@/components/ui/toaster"
|
||||
import { LanguageDomSync } from "@/lib/language"
|
||||
import { ProtectedLayout } from "@/components/protected-layout"
|
||||
import { AdminOnly } from "@/components/admin-only"
|
||||
import { LoginPage } from "@/pages/login"
|
||||
import { RegisterPage } from "@/pages/register"
|
||||
import { MailPage } from "@/pages/mail"
|
||||
import { AdminPage } from "@/pages/admin"
|
||||
import { ProfilePage } from "@/pages/profile"
|
||||
import { NotFoundPage } from "@/pages/not-found"
|
||||
import "./index.css"
|
||||
|
||||
const LoginPage = React.lazy(() => import("@/pages/login").then((module) => ({ default: module.LoginPage })))
|
||||
const RegisterPage = React.lazy(() => import("@/pages/register").then((module) => ({ default: module.RegisterPage })))
|
||||
const MailPage = React.lazy(() => import("@/pages/mail").then((module) => ({ default: module.MailPage })))
|
||||
const AdminPage = React.lazy(() => import("@/pages/admin").then((module) => ({ default: module.AdminPage })))
|
||||
const ProfilePage = React.lazy(() => import("@/pages/profile").then((module) => ({ default: module.ProfilePage })))
|
||||
const NotFoundPage = React.lazy(() => import("@/pages/not-found").then((module) => ({ default: module.NotFoundPage })))
|
||||
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, staleTime: 10_000 } } })
|
||||
const router = createBrowserRouter([
|
||||
{ path: "/login", element: <LoginPage /> },
|
||||
@@ -31,7 +32,9 @@ const router = createBrowserRouter([
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
<React.Suspense fallback={<div className="grid h-svh place-items-center text-sm text-muted-foreground">加载中...</div>}>
|
||||
<RouterProvider router={router} />
|
||||
</React.Suspense>
|
||||
<Toaster />
|
||||
<LanguageDomSync />
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -422,13 +422,13 @@ function PermissionGroupDialog({ group, catalog, open, onOpenChange }: { group?:
|
||||
const defaultLimitsQuery = useQuery({ queryKey: ["admin", "permission-limits", "defaults"], queryFn: api.defaultPermissionLimits, enabled: dialogOpen })
|
||||
const defaultLimits = defaultLimitsQuery.data || defaultPermissionLimits
|
||||
const [permissions, setPermissions] = React.useState<PermissionKey[]>(group?.permissions || [])
|
||||
const [limits, setLimits] = React.useState<PermissionLimits>(group?.limits || defaultPermissionLimits)
|
||||
const [limits, setLimits] = React.useState<PermissionLimits>(group?.limits || defaultLimits)
|
||||
React.useEffect(() => {
|
||||
if (dialogOpen) {
|
||||
setPermissions(group?.permissions || [])
|
||||
setLimits(group?.limits || defaultPermissionLimits)
|
||||
setLimits(group?.limits || defaultLimits)
|
||||
}
|
||||
}, [dialogOpen, group])
|
||||
}, [defaultLimits, dialogOpen, group])
|
||||
const mutation = useMutation({
|
||||
mutationFn: (form: FormData) => {
|
||||
const payload = {
|
||||
@@ -1979,11 +1979,6 @@ function DNSPanel({ domain, embedded = false }: { domain?: Domain; embedded?: bo
|
||||
return <Card><CardHeader>{header}</CardHeader><CardContent>{content}</CardContent></Card>
|
||||
}
|
||||
|
||||
const dnsDescriptions: Record<string, string> = {
|
||||
MX: "指定收件服务器。把邮件投递到该地址指向的服务器。",
|
||||
TXT: "", // 具体含义根据内容区分
|
||||
}
|
||||
|
||||
function dnsDescription(record: DNSRecord): string {
|
||||
if (record.type === "TXT" && record.name.startsWith("_dmarc")) return "声明域名的 DMARC 策略(如何处理未通过 SPF/DKIM 验证的邮件)。"
|
||||
if (record.type === "TXT" && record.value.includes("DKIM1")) return "DKIM 公钥。收件服务器用此密钥验证邮件是否由你发出。"
|
||||
|
||||
@@ -11,8 +11,8 @@ import TextAlign from "@tiptap/extension-text-align"
|
||||
import Placeholder from "@tiptap/extension-placeholder"
|
||||
import { BackgroundColor, Color, FontFamily, FontSize, TextStyle } from "@tiptap/extension-text-style"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { AlignCenter, AlignLeft, AlignRight, Archive, ArrowLeft, Ban, Bold, Calendar, Check, ChevronDown, Clock3, Code2, Copy, Download, Ellipsis, Eraser, Eye, FileText, Folder, Forward, Highlighter, History, Image, Inbox, IndentDecrease, IndentIncrease, Italic, Link, List, ListOrdered, Mail, MailCheck, MailQuestion, Moon, PanelLeftOpen, Paperclip, PencilLine, Plus, Quote, Redo2, RefreshCcw, Reply, RotateCcw, Search, Send, Settings, ShieldCheck, Signature, SlidersHorizontal, Smile, Star, Strikethrough, Sun, Tag, Trash2, Type, Underline, Undo2, Upload, X } from "lucide-react"
|
||||
import { api, ExternalImapAccount, ExternalImapFolder, ListResponse, Mailbox, MailFolder, MailLabel, MailMessage, MailSearchParams, SendPayload, DraftPayload, ScheduledSend, SendQueueItem, SendQueueAuditEvent, SendQueueStatus, PermissionLimits } from "@/lib/api"
|
||||
import { AlignCenter, AlignLeft, AlignRight, Archive, ArrowLeft, Ban, Bold, Calendar, Check, ChevronDown, Clock3, Code2, Copy, Download, Ellipsis, Eraser, Eye, FileText, Folder, Forward, Highlighter, History, Image, Inbox, IndentDecrease, IndentIncrease, Italic, Link, List, ListOrdered, Mail, MailCheck, MailQuestion, Moon, PanelLeftOpen, Paperclip, PencilLine, Plus, Quote, Redo2, RefreshCcw, Reply, RotateCcw, Search, Send, Settings, ShieldCheck, Signature, SlidersHorizontal, Smile, Star, Strikethrough, Sun, Trash2, Type, Underline, Undo2, Upload, X } from "lucide-react"
|
||||
import { api, ExternalImapAccount, ListResponse, Mailbox, MailFolder, MailLabel, MailMessage, MailSearchParams, SendPayload, DraftPayload, ScheduledSend, SendQueueItem, SendQueueAuditEvent, SendQueueStatus, PermissionLimits } from "@/lib/api"
|
||||
import { cn, decodeMimeHeader, formatBytes, formatDate, formatDateTime, generateLabelColor } from "@/lib/utils"
|
||||
import { applyTheme, getInitialTheme } from "@/lib/theme"
|
||||
import { useDisplayMode } from "@/lib/display-mode"
|
||||
@@ -140,7 +140,7 @@ export function MailPage() {
|
||||
const [autoRefreshing, setAutoRefreshing] = React.useState(false)
|
||||
const [exportingMail, setExportingMail] = React.useState(false)
|
||||
const [importingMail, setImportingMail] = React.useState(false)
|
||||
const [lastAutoRefreshAt, setLastAutoRefreshAt] = React.useState<Date | null>(null)
|
||||
const [, setLastAutoRefreshAt] = React.useState<Date | null>(null)
|
||||
const [bulkPending, setBulkPending] = React.useState(false)
|
||||
const [pendingConfirm, setPendingConfirm] = React.useState<PendingConfirm | null>(null)
|
||||
const [cancelingScheduledId, setCancelingScheduledId] = React.useState("")
|
||||
@@ -4728,24 +4728,6 @@ function toDateTimeLocalValue(date: Date) {
|
||||
function normalizeSchedule(schedule: ScheduleDraft): ScheduleDraft {
|
||||
return { ...schedule, title: schedule.title.trim(), location: schedule.location.trim(), description: schedule.description.trim() }
|
||||
}
|
||||
function scheduleToHtml(schedule: ScheduleDraft) {
|
||||
const start = parseScheduleStart(schedule)
|
||||
const end = schedule.allDay ? new Date(start.getTime() + 24 * 60 * 60 * 1000) : new Date(start.getTime() + schedule.durationMinutes * 60 * 1000)
|
||||
const rows = [
|
||||
["时间", schedule.allDay ? formatDate(start.toISOString()) : `${formatDateTime(start.toISOString())} - ${formatTimeOnly(end)}`],
|
||||
["持续", schedule.allDay ? "全天" : durationLabel(schedule.durationMinutes)],
|
||||
["提醒", reminderLabel(schedule.reminderMinutes)],
|
||||
["重复", repeatLabel(schedule.repeat)],
|
||||
schedule.location ? ["位置", schedule.location] : undefined,
|
||||
schedule.description ? ["描述", schedule.description] : undefined,
|
||||
].filter(Boolean) as string[][]
|
||||
return DOMPurify.sanitize(`
|
||||
<div style="border:1px solid #d4d4d8;border-radius:8px;padding:14px 16px;margin:16px 0;background:#fafafa;">
|
||||
<div style="font-weight:600;font-size:16px;margin-bottom:10px;">${escapeHtml(schedule.title)}</div>
|
||||
${rows.map(([label, value]) => `<div style="margin:6px 0;"><span style="color:#71717a;">${label}:</span>${escapeHtml(value)}</div>`).join("")}
|
||||
</div>
|
||||
`)
|
||||
}
|
||||
function scheduleToFile(schedule: ScheduleDraft) {
|
||||
const ics = scheduleToIcs(schedule)
|
||||
const filename = `${safeFilename(schedule.title || "schedule")}.ics`
|
||||
|
||||
+68
-716
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,8 @@
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
|
||||
Reference in New Issue
Block a user