mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 07:36:37 +08:00
user
This commit is contained in:
@@ -46,12 +46,12 @@ export function LoginForm() {
|
||||
|
||||
const publicConfigQuery = useQuery({
|
||||
queryKey: ["public-config"],
|
||||
queryFn: services.config.getPublicConfig,
|
||||
queryFn: () => services.config.getPublicConfig(),
|
||||
})
|
||||
|
||||
const authSourcesQuery = useQuery({
|
||||
queryKey: ["auth-sources"],
|
||||
queryFn: services.auth.getAuthSources,
|
||||
queryFn: () => services.auth.getAuthSources(),
|
||||
enabled: publicConfigQuery.data?.oidc_login_enabled ?? true,
|
||||
})
|
||||
|
||||
@@ -61,7 +61,7 @@ export function LoginForm() {
|
||||
)
|
||||
|
||||
const loginMutation = useMutation({
|
||||
mutationFn: services.auth.login,
|
||||
mutationFn: (req: any) => services.auth.login(req),
|
||||
onSuccess: (user) => {
|
||||
setUser(user)
|
||||
router.replace(redirectTarget)
|
||||
@@ -72,7 +72,7 @@ export function LoginForm() {
|
||||
})
|
||||
|
||||
const registerMutation = useMutation({
|
||||
mutationFn: services.auth.register,
|
||||
mutationFn: (req: any) => services.auth.register(req),
|
||||
onSuccess: (user) => {
|
||||
setUser(user)
|
||||
router.replace(redirectTarget)
|
||||
@@ -125,9 +125,6 @@ export function LoginForm() {
|
||||
<h2 className="text-xl font-semibold tracking-tight text-foreground">
|
||||
账号登录
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
使用账号密码或第三方 OIDC 认证源登录
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs value={mode} onValueChange={(value) => setMode(value as "login" | "register")}>
|
||||
@@ -165,6 +162,7 @@ export function LoginForm() {
|
||||
<Button
|
||||
type="button"
|
||||
className="w-full"
|
||||
variant={"secondary"}
|
||||
onClick={handlePasswordLogin}
|
||||
disabled={!passwordLoginEnabled || loginMutation.isPending}
|
||||
>
|
||||
@@ -176,7 +174,7 @@ export function LoginForm() {
|
||||
) : (
|
||||
<>
|
||||
<KeyRound className="mr-2 size-4" />
|
||||
账号密码登录
|
||||
登录
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -216,6 +214,7 @@ export function LoginForm() {
|
||||
<Button
|
||||
type="button"
|
||||
className="w-full"
|
||||
variant="secondary"
|
||||
onClick={handleRegister}
|
||||
disabled={!registrationEnabled || registerMutation.isPending}
|
||||
>
|
||||
@@ -258,7 +257,7 @@ export function LoginForm() {
|
||||
</div>
|
||||
))
|
||||
) : null}
|
||||
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -142,14 +142,12 @@ export function LoginPage() {
|
||||
}}
|
||||
className="relative z-10 w-full max-w-sm px-4"
|
||||
>
|
||||
<div className="text-center mb-8 space-y-2">
|
||||
<h1 className="text-3xl font-bold tracking-tight text-foreground">
|
||||
LINUX DO <span className="font-serif italic text-primary">Credit</span>
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground font-light">
|
||||
简单、安全,专为社区设计
|
||||
</p>
|
||||
</div>
|
||||
{/*Title Do Not Remove*/}
|
||||
{/*<div className="text-center mb-8 space-y-2">*/}
|
||||
{/* <h1 className="text-3xl font-bold tracking-tight text-foreground">*/}
|
||||
{/* Refresh*/}
|
||||
{/* </h1>*/}
|
||||
{/*</div>*/}
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{isProcessingCallback || isCheckingSession ? (
|
||||
@@ -209,10 +207,6 @@ export function LoginPage() {
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="mt-8 text-center text-xs text-muted-foreground">
|
||||
© {new Date().getFullYear()} LINUX DO Credit. 版权所有
|
||||
</div>
|
||||
</motion.div>
|
||||
</AuroraBackground>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
"use client"
|
||||
|
||||
import {useEffect, useState} from "react"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} from "@/components/ui/dialog"
|
||||
import {Input} from "@/components/ui/input"
|
||||
import {Label} from "@/components/ui/label"
|
||||
import {Switch} from "@/components/ui/switch"
|
||||
import {useAdminUsers} from "@/contexts/admin-users-context"
|
||||
import type {CreateUserRequest} from "@/lib/services/admin"
|
||||
|
||||
const emptyForm: CreateUserRequest = {
|
||||
username: "",
|
||||
password: "",
|
||||
nickname: "",
|
||||
is_active: true,
|
||||
is_admin: false,
|
||||
}
|
||||
|
||||
export function CreateUserModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}) {
|
||||
const { createUser } = useAdminUsers()
|
||||
const [form, setForm] = useState<CreateUserRequest>(emptyForm)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setForm(emptyForm)
|
||||
setErrors({})
|
||||
} else {
|
||||
setSaving(false)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const validate = () => {
|
||||
const newErrors: Record<string, string> = {}
|
||||
if (!form.username.trim()) {
|
||||
newErrors.username = "用户名不能为空"
|
||||
} else if (form.username.trim().length < 3) {
|
||||
newErrors.username = "用户名长度不能少于 3 位"
|
||||
}
|
||||
|
||||
if (!form.password) {
|
||||
newErrors.password = "密码不能为空"
|
||||
} else if (form.password.length < 8) {
|
||||
newErrors.password = "密码长度不能少于 8 位"
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!validate()) return
|
||||
|
||||
setSaving(true)
|
||||
try {
|
||||
await createUser({
|
||||
...form,
|
||||
username: form.username.trim(),
|
||||
nickname: form.nickname?.trim() || undefined,
|
||||
})
|
||||
onClose()
|
||||
} catch {
|
||||
// Errors are already handled by context toast notifications
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>新增用户</DialogTitle>
|
||||
<DialogDescription>
|
||||
直接创建一个本地密码登录的新账户。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSave} className="space-y-4 pt-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="username">用户名</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={form.username}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, username: e.target.value }))}
|
||||
placeholder="请输入用户名 (至少 3 位)"
|
||||
/>
|
||||
{errors.username && (
|
||||
<p className="text-xs text-destructive">{errors.username}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="nickname">昵称 (选填)</Label>
|
||||
<Input
|
||||
id="nickname"
|
||||
value={form.nickname}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, nickname: e.target.value }))}
|
||||
placeholder="请输入昵称"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="password">密码</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}
|
||||
placeholder="请输入登录密码 (至少 8 位)"
|
||||
/>
|
||||
{errors.password && (
|
||||
<p className="text-xs text-destructive">{errors.password}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border border-dashed p-3 bg-muted/10">
|
||||
<div>
|
||||
<div className="font-medium text-sm">启用账户</div>
|
||||
<div className="text-xs text-muted-foreground">禁用后此账号将无法登录系统。</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={form.is_active}
|
||||
onCheckedChange={(checked) => setForm((prev) => ({ ...prev, is_active: checked }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border border-dashed p-3 bg-muted/10">
|
||||
<div>
|
||||
<div className="font-medium text-sm">管理员权限</div>
|
||||
<div className="text-xs text-muted-foreground">开启后此账号将拥有后台管理权限。</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={form.is_admin}
|
||||
onCheckedChange={(checked) => setForm((prev) => ({ ...prev, is_admin: checked }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2 border-t mt-2">
|
||||
<Button variant="outline" type="button" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit" disabled={saving} variant="secondary">
|
||||
{saving ? "创建中..." : "创建"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -147,7 +147,7 @@ export function FilesMain() {
|
||||
文件管理
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
管理您上传的所有文件,支持下载和批量操作
|
||||
管理您上传的所有文件,支持下载 and 批量操作
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,25 +1,38 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Sheet, SheetTitle, SheetContent } from "@/components/ui/sheet"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Layers, Search, UserX, UserCheck, Eye, ShieldCheck, Filter, X, ChevronDown, ChevronLeft, ChevronRight, Loader2 } from "lucide-react"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import {useEffect, useState} from "react"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Switch} from "@/components/ui/switch"
|
||||
import {Separator} from "@/components/ui/separator"
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table"
|
||||
import {Sheet, SheetContent, SheetTitle} from "@/components/ui/sheet"
|
||||
import {Badge} from "@/components/ui/badge"
|
||||
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar"
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
Filter,
|
||||
Layers,
|
||||
Loader2,
|
||||
Plus,
|
||||
Search,
|
||||
ShieldCheck,
|
||||
UserCheck,
|
||||
UserX,
|
||||
X
|
||||
} from "lucide-react"
|
||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"
|
||||
|
||||
import { AdminUser, AdminService } from "@/lib/services"
|
||||
import { toast } from "sonner"
|
||||
import { formatDateTime } from "@/lib/utils"
|
||||
import { EmptyStateWithBorder } from "@/components/layout/empty"
|
||||
import { LoadingStateWithBorder } from "@/components/layout/loading"
|
||||
import { ErrorInline } from "@/components/layout/error"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useAdminUsers } from "@/contexts/admin-users-context"
|
||||
import {AdminUser} from "@/lib/services"
|
||||
import {cn, formatDateTime} from "@/lib/utils"
|
||||
import {EmptyStateWithBorder} from "@/components/layout/empty"
|
||||
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
||||
import {ErrorInline} from "@/components/layout/error"
|
||||
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu"
|
||||
import {useAdminUsers} from "@/contexts/admin-users-context"
|
||||
import {CreateUserModal} from "./create-user-modal"
|
||||
|
||||
export function UsersManager() {
|
||||
const {
|
||||
@@ -43,6 +56,7 @@ export function UsersManager() {
|
||||
|
||||
const [selectedUser, setSelectedUser] = useState<AdminUser | null>(null)
|
||||
const [detailOpen, setDetailOpen] = useState(false)
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers()
|
||||
@@ -277,6 +291,10 @@ export function UsersManager() {
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-2xl font-semibold">用户管理</div>
|
||||
</div>
|
||||
<Button variant="secondary" size="sm" className="h-7 text-xs" onClick={() => setCreateModalOpen(true)}>
|
||||
<Plus className="size-3.5 mr-1" />
|
||||
新增用户
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{renderFilterBar()}
|
||||
@@ -409,9 +427,6 @@ export function UsersManager() {
|
||||
<Badge variant="secondary" className="h-4.5 px-1.5 text-[9px] uppercase font-medium">
|
||||
UID: {selectedUser.id}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="h-4.5 px-1.5 text-[9px] uppercase font-medium">
|
||||
Lv.{selectedUser.trust_level}
|
||||
</Badge>
|
||||
{selectedUser.is_admin && (
|
||||
<Badge className="h-4.5 px-1.5 text-[9px] uppercase font-medium bg-primary text-primary-foreground">
|
||||
Admin
|
||||
@@ -482,6 +497,8 @@ export function UsersManager() {
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<CreateUserModal isOpen={createModalOpen} onClose={() => setCreateModalOpen(false)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,11 +24,7 @@ export const termsSections: PolicySection[] = [
|
||||
title: "2. 服务定义与性质界定",
|
||||
content: (
|
||||
<div className="space-y-4 text-sm leading-relaxed">
|
||||
<p><strong>2.1 纯技术研发脚手架:</strong>本平台是一个开源/闭源授权的技术二次开发底座。我们仅提供用户注册、API 调用、安全配置维护等<strong>纯软件技术服务</strong>。</p>
|
||||
<p><strong>2.2 非金融机构与无承兑申明:</strong></p>
|
||||
<ul className="list-disc pl-4 md:pl-5 space-y-2">
|
||||
<li><strong>非持牌金融或支付机构:</strong>本系统不是银行、商户收单或清算结算机构。</li>
|
||||
<li><strong>不提供资金管理:</strong>系统无真实法币、加密货币或商业代金券充值、存储与兑现功能。若在二开中加入了积分等属性,其最终性质也应限制于虚拟软件积分。</li>
|
||||
<li><strong>责任自担:</strong>关于用户对本系统进行二次开发并应用于其他生产环境产生的任何业务行为,由二开部署运营主体承担全部合规责任。</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {ArrowRight, ExternalLink, FileText, HelpCircle, Layers, Shield, ShieldCh
|
||||
import Link from "next/link"
|
||||
|
||||
export function HomeMain() {
|
||||
const { user, getTrustLevelLabel } = useUser()
|
||||
const { user } = useUser()
|
||||
|
||||
const quickLinks = [
|
||||
{
|
||||
|
||||
@@ -90,56 +90,56 @@ export function AuthSourceModal({
|
||||
<div className="grid gap-4 md:grid-cols-2 pt-2">
|
||||
<div className="space-y-2">
|
||||
<Label>标识符 (英文名称)</Label>
|
||||
<Input
|
||||
value={form.name}
|
||||
<Input
|
||||
value={form.name}
|
||||
disabled={!!source}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="例如: github"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>展示名称</Label>
|
||||
<Input
|
||||
value={form.display_name}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, display_name: e.target.value }))}
|
||||
<Input
|
||||
value={form.display_name}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, display_name: e.target.value }))}
|
||||
placeholder="例如: GitHub 登录"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Client ID</Label>
|
||||
<Input
|
||||
value={form.client_id}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, client_id: e.target.value }))}
|
||||
<Input
|
||||
value={form.client_id}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, client_id: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Client Secret</Label>
|
||||
<Input
|
||||
value={form.client_secret}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, client_secret: e.target.value }))}
|
||||
placeholder={source ? "留空则保留原值" : ""}
|
||||
<Input
|
||||
value={form.client_secret}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, client_secret: e.target.value }))}
|
||||
placeholder={source ? "留空则保留原值" : ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label>Discovery URL (OIDC 发行方 URL)</Label>
|
||||
<Input
|
||||
value={form.openid_discovery_url}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, openid_discovery_url: e.target.value }))}
|
||||
<Input
|
||||
value={form.openid_discovery_url}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, openid_discovery_url: e.target.value }))}
|
||||
placeholder="https://..."
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Scopes</Label>
|
||||
<Input
|
||||
value={form.scopes}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, scopes: e.target.value }))}
|
||||
<Input
|
||||
value={form.scopes}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, scopes: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>图标 URL (可选)</Label>
|
||||
<Input
|
||||
value={form.icon_url}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, icon_url: e.target.value }))}
|
||||
<Input
|
||||
value={form.icon_url}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, icon_url: e.target.value }))}
|
||||
placeholder="https://... 或留空"
|
||||
/>
|
||||
</div>
|
||||
@@ -154,7 +154,7 @@ export function AuthSourceModal({
|
||||
<Button variant="outline" type="button" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="button" onClick={saveSource} disabled={saving} className="bg-indigo-600 hover:bg-indigo-700 text-white shadow-md shadow-indigo-600/10">
|
||||
<Button type="button" onClick={saveSource} disabled={saving} variant="secondary">
|
||||
{saving ? "保存中..." : "保存"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -14,18 +14,53 @@ import {
|
||||
BreadcrumbSeparator
|
||||
} from "@/components/ui/breadcrumb"
|
||||
import {useUser} from "@/contexts/user-context"
|
||||
import {ArrowRight, Link2, Loader2, Shield, Unlink} from "lucide-react"
|
||||
import {ArrowRight, Info, Link2, Loader2, Lock, Shield, Unlink} from "lucide-react"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Input} from "@/components/ui/input"
|
||||
import {Separator} from "@/components/ui/separator"
|
||||
import {AuthService} from "@/lib/services"
|
||||
import {toast} from "sonner"
|
||||
|
||||
export function ProfileMain() {
|
||||
const { user, loading, getTrustLevelLabel } = useUser()
|
||||
const { user, loading, refetch } = useUser()
|
||||
const controls = useAnimation()
|
||||
const isAnimatingRef = React.useRef(false)
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const [oldPassword, setOldPassword] = React.useState("")
|
||||
const [newPassword, setNewPassword] = React.useState("")
|
||||
const [confirmPassword, setConfirmPassword] = React.useState("")
|
||||
|
||||
const changePasswordMutation = useMutation({
|
||||
mutationFn: (req: any) => AuthService.changePassword(req),
|
||||
onSuccess: () => {
|
||||
toast.success("密码修改成功")
|
||||
setOldPassword("")
|
||||
setNewPassword("")
|
||||
setConfirmPassword("")
|
||||
void refetch()
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "修改密码失败,请重试")
|
||||
},
|
||||
})
|
||||
|
||||
const handlePasswordChange = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (newPassword !== confirmPassword) {
|
||||
toast.error("两次输入的新密码不一致")
|
||||
return
|
||||
}
|
||||
if (newPassword.length < 8) {
|
||||
toast.error("新密码长度不能少于 8 位")
|
||||
return
|
||||
}
|
||||
changePasswordMutation.mutate({
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword,
|
||||
})
|
||||
}
|
||||
|
||||
const externalAccountBindingsQuery = useQuery({
|
||||
queryKey: ["auth", "external-accounts"],
|
||||
queryFn: () => AuthService.getExternalAccountBindings(),
|
||||
@@ -135,10 +170,7 @@ export function ProfileMain() {
|
||||
<div className="text-sm font-mono font-semibold">{user.id}</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">信任等级</div>
|
||||
<div className="text-sm font-semibold">{getTrustLevelLabel(user.trust_level)}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">管理员身份</div>
|
||||
@@ -158,6 +190,72 @@ export function ProfileMain() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 修改密码面板 */}
|
||||
<div className="space-y-6 bg-card border border-dashed rounded-lg p-6">
|
||||
<div className="border-b pb-4 flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-amber-500/10 text-amber-500">
|
||||
<Lock className="size-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold tracking-tight">修改密码</h2>
|
||||
<p className="text-xs text-muted-foreground">更改您的账号密码以确保安全。密码长度不能少于 8 位。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{user.need_change_password && (
|
||||
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 px-3.5 py-3 text-xs text-amber-500 flex items-start gap-2.5">
|
||||
<Info className="size-4 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-semibold">密码风险提示</p>
|
||||
<p className="mt-0.5 text-amber-500/80 leading-relaxed font-normal">
|
||||
为了账号安全,您必须修改初始密码。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handlePasswordChange} className="space-y-4 pt-2 max-w-md">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-muted-foreground">当前密码</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="请输入当前密码"
|
||||
value={oldPassword}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-muted-foreground">新密码</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="新密码(至少 8 位)"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-muted-foreground">确认新密码</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="确认新密码"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full sm:w-auto"
|
||||
disabled={changePasswordMutation.isPending}
|
||||
>
|
||||
{changePasswordMutation.isPending ? "提交中..." : "确认修改"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* 账号绑定面板 */}
|
||||
<div className="space-y-6 bg-card border border-dashed rounded-lg p-6">
|
||||
<div className="border-b pb-4 flex items-center gap-2">
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, memo } from "react"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Bell, Plus, Settings, Search, Moon, Sun, Maximize2, Minimize2 } from "lucide-react"
|
||||
import { useUser } from "@/contexts/user-context"
|
||||
import { useBellRing } from "@/contexts/bell-ring-context"
|
||||
import { useNotificationSettings } from "@/contexts/notification-settings-context"
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar"
|
||||
import { useTheme } from "next-themes"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { SearchDialog } from "@/components/layout/search-dialog"
|
||||
import { Kbd } from "@/components/ui/kbd"
|
||||
import {memo, useEffect, useState} from "react"
|
||||
import {AnimatePresence, motion} from "motion/react"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Bell, Info, Maximize2, Minimize2, Moon, Search, Settings, Sun} from "lucide-react"
|
||||
import {useUser} from "@/contexts/user-context"
|
||||
import {useBellRing} from "@/contexts/bell-ring-context"
|
||||
import {useNotificationSettings} from "@/contexts/notification-settings-context"
|
||||
import {SidebarTrigger} from "@/components/ui/sidebar"
|
||||
import {useTheme} from "next-themes"
|
||||
import {useRouter} from "next/navigation"
|
||||
import {SearchDialog} from "@/components/layout/search-dialog"
|
||||
import {Kbd} from "@/components/ui/kbd"
|
||||
|
||||
|
||||
/**
|
||||
@@ -75,6 +75,22 @@ export function SiteHeader({ isFullWidth = false, onToggleFullWidth }: { isFullW
|
||||
return (
|
||||
<div className="flex flex-col w-full">
|
||||
<AppBanner />
|
||||
{user?.need_change_password && (
|
||||
<div className="bg-amber-500/10 border-b border-amber-500/20 px-4 py-3 text-amber-500 text-xs sm:text-sm flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Info className="size-4 shrink-0 text-amber-500" />
|
||||
<span>为了您的账号安全,请立即修改密码!</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-amber-500 hover:text-amber-600 border-amber-500/30 hover:border-amber-500/50 bg-transparent shrink-0 h-8 px-3 text-xs"
|
||||
onClick={() => router.push("/settings/profile")}
|
||||
>
|
||||
去修改密码
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<header className="flex h-(--header-height) shrink-0 items-center bg-background px-4 md:px-0">
|
||||
<div className="flex w-full items-center justify-between md:hidden">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -65,13 +65,13 @@ import {useUser} from "@/contexts/user-context"
|
||||
const data = {
|
||||
navMain: [
|
||||
{ title: "首页", url: "/home", icon: Home },
|
||||
{ title: "文件管理", url: "/settings/files", icon: FolderOpen },
|
||||
],
|
||||
admin: [
|
||||
{ title: "系统设置", url: "/admin/settings", icon: Settings },
|
||||
{ title: "系统配置", url: "/admin/system", icon: ShieldCheck },
|
||||
{ title: "用户管理", url: "/admin/users", icon: UserRound },
|
||||
{ title: "任务管理", url: "/admin/tasks", icon: Layers },
|
||||
{ title: "文件管理", url: "/admin/files", icon: FolderOpen },
|
||||
],
|
||||
document: [
|
||||
{ title: "接口文档", url: "/docs/api", icon: CreditCard, external: true },
|
||||
@@ -92,7 +92,7 @@ const data = {
|
||||
*/
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const { toggleSidebar, state, isMobile, setOpenMobile } = useSidebar()
|
||||
const { user, getTrustLevelLabel, logout } = useUser()
|
||||
const { user, logout } = useUser()
|
||||
const [showLogoutDialog, setShowLogoutDialog] = React.useState(false)
|
||||
const [isLoggingOut, setIsLoggingOut] = React.useState(false)
|
||||
const pathname = usePathname()
|
||||
@@ -185,7 +185,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
{user?.nickname || user?.username || "Unknown User"}
|
||||
</span>
|
||||
<span className="text-[11px] font-medium text-muted-foreground/100 truncate w-full text-left ml-2">
|
||||
{user ? getTrustLevelLabel(user.trust_level) : "Trust Level Unknown"}
|
||||
{user?.is_admin ? "系统管理员" : "普通用户"}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown className="size-4 text-muted-foreground ml-auto group-data-[collapsible=icon]:hidden" />
|
||||
@@ -226,7 +226,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
{user?.nickname || user?.username || "Unknown User"}
|
||||
</span>
|
||||
<span className="text-xs font-base text-muted-foreground">
|
||||
{user ? getTrustLevelLabel(user.trust_level) : "Trust Level Unknown"}
|
||||
{user?.is_admin ? "系统管理员" : "普通用户"}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
|
||||
Reference in New Issue
Block a user