mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 07:06:36 +08:00
文件管理权限控制
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import {useMutation, useQueryClient, type UseQueryResult} from "@tanstack/react-query"
|
||||
import {Globe, Search} from "lucide-react"
|
||||
import {useMemo} from "react"
|
||||
import {useMutation, useQuery, useQueryClient, type UseQueryResult} from "@tanstack/react-query"
|
||||
import {KeyRound, ShieldAlert, X} from "lucide-react"
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
|
||||
import {Switch} from "@/components/ui/switch"
|
||||
import {Badge} from "@/components/ui/badge"
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"
|
||||
import {AdminService} from "@/lib/services"
|
||||
import type {SystemConfig} from "@/lib/services/admin"
|
||||
import {TemplatesManager} from "./templates"
|
||||
@@ -17,62 +19,144 @@ interface OperationTabProps {
|
||||
export function OperationTab({ configs, systemConfigsQuery }: OperationTabProps) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const updateConfigMutation = useMutation({
|
||||
mutationFn: async ({ key, value }: { key: string; value: boolean }) => {
|
||||
const config = configs[key]
|
||||
const uploadTypesQuery = useQuery({
|
||||
queryKey: ["admin", "upload-types"],
|
||||
queryFn: () => AdminService.listUploadTypes(),
|
||||
})
|
||||
|
||||
const updateWhitelistMutation = useMutation({
|
||||
mutationFn: async (newValue: string) => {
|
||||
const config = configs["file_access_whitelist"]
|
||||
if (!config) {
|
||||
throw new Error(`缺少配置项: ${key}`)
|
||||
throw new Error("缺少配置项: file_access_whitelist")
|
||||
}
|
||||
await AdminService.updateSystemConfig(key, {
|
||||
value: value ? "true" : "false",
|
||||
await AdminService.updateSystemConfig("file_access_whitelist", {
|
||||
value: newValue,
|
||||
description: config.description,
|
||||
})
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["admin", "system-configs"] })
|
||||
await queryClient.invalidateQueries({ queryKey: ["public-config"] })
|
||||
toast.success("运营配置已更新")
|
||||
toast.success("文件访问白名单已更新")
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "更新配置失败")
|
||||
toast.error(error.message || "更新白名单失败")
|
||||
},
|
||||
})
|
||||
|
||||
const indexingEnabled = configs["search_engine_indexing_enabled"]?.value === "true"
|
||||
const whitelistConfig = configs["file_access_whitelist"]
|
||||
const currentWhitelist = useMemo<string[]>(() => {
|
||||
if (!whitelistConfig?.value) return ["avatar"]
|
||||
try {
|
||||
const parsed = JSON.parse(whitelistConfig.value)
|
||||
if (Array.isArray(parsed)) return parsed
|
||||
} catch {
|
||||
// 降级支持逗号分隔解析
|
||||
return whitelistConfig.value.split(",").map(s => s.trim()).filter(Boolean)
|
||||
}
|
||||
return ["avatar"]
|
||||
}, [whitelistConfig?.value])
|
||||
|
||||
const handleAddType = (type: string) => {
|
||||
if (!type || currentWhitelist.includes(type)) return
|
||||
const newWhitelist = [...currentWhitelist, type]
|
||||
updateWhitelistMutation.mutate(JSON.stringify(newWhitelist))
|
||||
}
|
||||
|
||||
const handleRemoveType = (typeToRemove: string) => {
|
||||
const newWhitelist = currentWhitelist.filter(t => t !== typeToRemove)
|
||||
updateWhitelistMutation.mutate(JSON.stringify(newWhitelist))
|
||||
}
|
||||
|
||||
const availableTypes = useMemo(() => {
|
||||
const types = uploadTypesQuery.data ?? []
|
||||
return types.map(t => {
|
||||
let label = t
|
||||
if (t === "avatar") label = "头像 (avatar)"
|
||||
else if (t === "attachment") label = "附件 (attachment)"
|
||||
else if (t === "doc") label = "文档 (doc)"
|
||||
else if (t === "generic") label = "通用 (generic)"
|
||||
return { value: t, label }
|
||||
})
|
||||
}, [uploadTypesQuery.data])
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 搜索引擎检索设置 */}
|
||||
|
||||
{/* 文件访问白名单设置 */}
|
||||
<Card className="border border-dashed shadow-sm">
|
||||
<CardHeader className="border-b border-dashed pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-indigo-500/10 text-indigo-500">
|
||||
<Globe className="size-4" />
|
||||
<KeyRound className="size-4" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base font-semibold">SEO 与搜索引擎检索</CardTitle>
|
||||
<CardDescription className="text-xs">配置站点是否允许被搜索引擎抓取和检索</CardDescription>
|
||||
<CardTitle className="text-base font-semibold">文件访问权限控制</CardTitle>
|
||||
<CardDescription className="text-xs">配置免登录直接访问的文件业务类型。不在白名单内的文件将要求登录鉴权。</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between gap-4 rounded-xl border border-dashed p-4 bg-card hover:bg-muted/10 hover:border-indigo-500/30 transition-all duration-300 shadow-sm">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Search className="size-4 text-indigo-500" />
|
||||
<span className="font-medium text-sm text-foreground">允许被搜索引擎检索</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed pr-2">
|
||||
默认关闭。关闭后系统将自动下发 noindex 指令并限制 robots.txt,禁止搜索引擎爬虫抓取本站页面。
|
||||
</p>
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-muted-foreground">添加免鉴权类型:</span>
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={handleAddType}
|
||||
disabled={updateWhitelistMutation.isPending || systemConfigsQuery.isPending || uploadTypesQuery.isPending}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]" size="sm">
|
||||
<SelectValue placeholder="选择业务类型..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableTypes
|
||||
.filter(t => !currentWhitelist.includes(t.value))
|
||||
.map(t => (
|
||||
<SelectItem key={t.value} value={t.value}>
|
||||
{t.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{availableTypes.filter(t => !currentWhitelist.includes(t.value)).length === 0 && (
|
||||
<div className="text-xs text-muted-foreground p-2 text-center">所有类型已添加</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 当前白名单列表 */}
|
||||
<div className="rounded-xl border border-dashed p-4 bg-card hover:bg-muted/10 hover:border-indigo-500/30 transition-all duration-300 shadow-sm space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldAlert className="size-4 text-indigo-500" />
|
||||
<span className="font-medium text-sm text-foreground">当前免鉴权列表</span>
|
||||
</div>
|
||||
|
||||
{currentWhitelist.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{currentWhitelist.map(type => (
|
||||
<Badge
|
||||
key={type}
|
||||
variant="secondary"
|
||||
className="px-2.5 py-1 text-xs gap-1.5 flex items-center bg-indigo-500/10 text-indigo-700 dark:text-indigo-300 dark:bg-indigo-500/20 border border-indigo-500/20"
|
||||
>
|
||||
{availableTypes.find(t => t.value === type)?.label || type}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveType(type)}
|
||||
disabled={updateWhitelistMutation.isPending || systemConfigsQuery.isPending}
|
||||
className="rounded-full outline-hidden hover:bg-indigo-500/20 p-0.5 text-indigo-600 dark:text-indigo-400 cursor-pointer disabled:cursor-not-allowed"
|
||||
>
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
白名单已空,所有类型文件的访问都将需要登录。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
checked={indexingEnabled}
|
||||
disabled={updateConfigMutation.isPending || systemConfigsQuery.isPending}
|
||||
onCheckedChange={(checked) =>
|
||||
updateConfigMutation.mutate({ key: "search_engine_indexing_enabled", value: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
|
||||
import {useEffect, useState} from "react"
|
||||
import {useMutation, useQueryClient, type UseQueryResult} from "@tanstack/react-query"
|
||||
import {Loader2, Mail, Server} from "lucide-react"
|
||||
import {Globe, Info, Loader2, Mail, Search, Server, Sparkles} from "lucide-react"
|
||||
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
|
||||
import {Input} from "@/components/ui/input"
|
||||
import {Label} from "@/components/ui/label"
|
||||
import {Switch} from "@/components/ui/switch"
|
||||
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from "@/components/ui/dialog"
|
||||
import {Badge} from "@/components/ui/badge"
|
||||
import {AdminService} from "@/lib/services"
|
||||
import type {SystemConfig} from "@/lib/services/admin"
|
||||
import {toast} from "sonner"
|
||||
@@ -41,6 +43,34 @@ export function SystemTab({ configs, systemConfigsQuery }: SystemTabProps) {
|
||||
}
|
||||
}, [systemConfigsQuery.data, configs])
|
||||
|
||||
const handleDetectAddress = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
setServerAddress(window.location.origin)
|
||||
toast.success("已自动获取当前域名并填充")
|
||||
}
|
||||
}
|
||||
|
||||
const updateConfigMutation = useMutation({
|
||||
mutationFn: async ({ key, value }: { key: string; value: boolean }) => {
|
||||
const config = configs[key]
|
||||
if (!config) {
|
||||
throw new Error(`缺少配置项: ${key}`)
|
||||
}
|
||||
await AdminService.updateSystemConfig(key, {
|
||||
value: value ? "true" : "false",
|
||||
description: config.description,
|
||||
})
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["admin", "system-configs"] })
|
||||
await queryClient.invalidateQueries({ queryKey: ["public-config"] })
|
||||
toast.success("配置已更新")
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "更新配置失败")
|
||||
},
|
||||
})
|
||||
|
||||
const saveSystemMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const currentCfg = configs["server_address"]
|
||||
@@ -143,54 +173,142 @@ export function SystemTab({ configs, systemConfigsQuery }: SystemTabProps) {
|
||||
testSmtpMutation.mutate()
|
||||
}
|
||||
|
||||
const indexingEnabled = configs["search_engine_indexing_enabled"]?.value === "true"
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-8">
|
||||
{/* 通用设置 */}
|
||||
<Card className="border border-dashed shadow-sm">
|
||||
<CardHeader className="border-b border-dashed pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-indigo-500/10 text-indigo-500">
|
||||
<Server className="size-4" />
|
||||
<Card className="border border-zinc-200 dark:border-zinc-800 shadow-md bg-gradient-to-b from-card to-zinc-50/30 dark:to-zinc-950/20 overflow-hidden">
|
||||
<CardHeader className="border-b border-zinc-100 dark:border-zinc-800 pb-5 bg-zinc-50/50 dark:bg-zinc-900/30">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2.5 rounded-xl bg-indigo-500/10 text-indigo-500 ring-4 ring-indigo-500/5">
|
||||
<Server className="size-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base font-semibold">通用设置</CardTitle>
|
||||
<CardDescription className="text-xs">配置系统的全局通用参数</CardDescription>
|
||||
<CardTitle className="text-lg font-bold tracking-tight">通用设置</CardTitle>
|
||||
<CardDescription className="text-xs text-muted-foreground">配置系统的全局网络通信访问限制与搜索引擎的公开检索收录参数</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<form onSubmit={handleSystemSave} className="space-y-6">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="server_address" className="text-xs font-semibold">服务器地址</Label>
|
||||
<Input
|
||||
id="server_address"
|
||||
type="text"
|
||||
value={serverAddress}
|
||||
onChange={(e) => setServerAddress(e.target.value)}
|
||||
placeholder="例如: https://example.com"
|
||||
className="bg-card border-dashed text-xs"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground leading-normal">
|
||||
这里可以编辑更改服务器地址。默认不设定,允许从任意源(*)访问 API,此时存在跨域安全风险;如果手动设置服务器地址,CORS 允许源将更新为该地址,消除跨域安全隐患。
|
||||
</p>
|
||||
<CardContent className="p-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
|
||||
{/* 跨域源与服务器地址 */}
|
||||
<form onSubmit={handleSystemSave} className="group relative flex flex-col justify-between space-y-5 rounded-2xl border border-zinc-200/80 dark:border-zinc-800/80 p-6 bg-card hover:shadow-lg hover:border-indigo-500/30 dark:hover:border-indigo-500/30 transition-all duration-300">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2.5 text-indigo-500">
|
||||
<Globe className="size-5" />
|
||||
<span className="font-semibold text-sm text-foreground">访问域名与跨域来源限制</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleDetectAddress}
|
||||
className="h-7 px-2.5 text-[10px] gap-1 font-medium hover:bg-indigo-500/10 hover:text-indigo-500 hover:border-indigo-500/30 transition-all duration-200 shadow-xs"
|
||||
>
|
||||
<Sparkles className="size-3 text-indigo-500" />
|
||||
使用当前域名
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="server_address" className="text-xs font-semibold text-muted-foreground">服务器访问地址 (Server Address)</Label>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="server_address"
|
||||
type="text"
|
||||
value={serverAddress}
|
||||
onChange={(e) => setServerAddress(e.target.value)}
|
||||
placeholder="例如: https://example.com"
|
||||
className="bg-zinc-50/50 dark:bg-zinc-900/50 border-zinc-200 dark:border-zinc-800 text-xs focus-visible:ring-1 focus-visible:ring-indigo-500 transition-all duration-200 h-9.5"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start gap-1.5 mt-2 bg-zinc-50 dark:bg-zinc-900/30 p-2.5 rounded-lg border border-zinc-100 dark:border-zinc-800/50">
|
||||
<Info className="size-3.5 text-muted-foreground shrink-0 mt-0.5" />
|
||||
<p className="text-[10px] text-muted-foreground leading-relaxed">
|
||||
配置 API 的对外服务访问域名。留空则允许任意源访问(CORS 将开放 `*`,有安全风险)。若配置具体域名,将激活同源及 CORS 白名单验证保护。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end pt-4 border-t border-zinc-100 dark:border-zinc-800/50">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
className="h-8.5 px-4 text-xs font-medium bg-indigo-600 hover:bg-indigo-500 text-white shadow-md hover:shadow-indigo-500/10 active:scale-[0.98] transition-all"
|
||||
disabled={saveSystemMutation.isPending}
|
||||
>
|
||||
{saveSystemMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-1.5 size-3.5 animate-spin" />
|
||||
保存中...
|
||||
</>
|
||||
) : (
|
||||
"保存访问配置"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* SEO 搜索引擎抓取 */}
|
||||
<div className={`group relative flex flex-col justify-between space-y-5 rounded-2xl border p-6 bg-card hover:shadow-lg transition-all duration-500 ${
|
||||
indexingEnabled
|
||||
? "border-emerald-500/20 dark:border-emerald-500/30 hover:border-emerald-500/40 bg-gradient-to-b from-card to-emerald-500/[0.02] dark:to-emerald-500/[0.04]"
|
||||
: "border-zinc-200 dark:border-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700 bg-gradient-to-b from-card to-zinc-500/[0.01]"
|
||||
}`}>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={`flex items-center gap-2.5 transition-colors duration-300 ${indexingEnabled ? "text-emerald-500" : "text-zinc-500"}`}>
|
||||
<Search className="size-5" />
|
||||
<span className="font-semibold text-sm text-foreground">搜索引擎抓取检索 (SEO)</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant={indexingEnabled ? "default" : "secondary"}
|
||||
className={`text-[10px] font-semibold tracking-wide px-2.5 py-0.5 shadow-xs transition-all duration-300 ${
|
||||
indexingEnabled
|
||||
? "bg-emerald-500/15 text-emerald-600 hover:bg-emerald-500/20 dark:bg-emerald-500/20 dark:text-emerald-400 border border-emerald-500/20"
|
||||
: "bg-zinc-100 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400 border border-zinc-200 dark:border-zinc-700"
|
||||
}`}
|
||||
>
|
||||
<span className={`inline-block size-1.5 rounded-full mr-1.5 shrink-0 ${indexingEnabled ? "bg-emerald-500 animate-pulse" : "bg-zinc-400"}`} />
|
||||
{indexingEnabled ? "已启用索引" : "已屏蔽检索"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs font-semibold text-muted-foreground block">站点检索可见性开关</span>
|
||||
<div className="flex items-start gap-1.5 bg-zinc-50 dark:bg-zinc-900/30 p-2.5 rounded-lg border border-zinc-100 dark:border-zinc-800/50">
|
||||
<Info className="size-3.5 text-muted-foreground shrink-0 mt-0.5" />
|
||||
<p className="text-[10px] text-muted-foreground leading-relaxed">
|
||||
控制本系统是否向主流搜索引擎(如 Google、Baidu、Bing)开放公开索引。关闭时,系统响应的 HTML 头部会自动注入 meta robots 标签,防止爬虫收录。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-4 border-t border-zinc-100 dark:border-zinc-800/50 h-12">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-foreground font-semibold">
|
||||
{indexingEnabled ? "允许爬虫访问与收录" : "全面屏蔽外部搜索"}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{indexingEnabled ? "爬虫可自由搜集并抓取网站内容" : "已拦截所有网络爬虫的收录请求"}
|
||||
</span>
|
||||
</div>
|
||||
<Switch
|
||||
checked={indexingEnabled}
|
||||
disabled={updateConfigMutation.isPending || systemConfigsQuery.isPending}
|
||||
onCheckedChange={(checked) =>
|
||||
updateConfigMutation.mutate({ key: "search_engine_indexing_enabled", value: checked })
|
||||
}
|
||||
className="data-[state=checked]:bg-emerald-500 dark:data-[state=checked]:bg-emerald-600 focus-visible:ring-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end pt-4 border-t border-dashed">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
disabled={saveSystemMutation.isPending}
|
||||
>
|
||||
{saveSystemMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-1.5 size-3.5 animate-spin" />
|
||||
保存中...
|
||||
</>
|
||||
) : (
|
||||
"保存配置"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -370,3 +488,4 @@ export function SystemTab({ configs, systemConfigsQuery }: SystemTabProps) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user