mirror of
https://github.com/woodchen-ink/Q58Connect.git
synced 2025-07-18 14:01:55 +08:00
refactor: Improve UI and UX for client management and authorization
- Updated client status toggle with Switch component - Refined admin pages for clients and users with layout improvements - Enhanced authorization action with more detailed error logging - Modified dashboard links to point to client list instead of new client page - Updated client edit form with clearer user input guidance
This commit is contained in:
parent
70e66294e3
commit
0dd6a3338f
@ -1,5 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
import { createAuthorization } from "@/lib/dto/authorization";
|
import { createAuthorization } from "@/lib/dto/authorization";
|
||||||
import { getAuthorizeUrl } from "@/lib/oauth/authorize-url";
|
import { getAuthorizeUrl } from "@/lib/oauth/authorize-url";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
@ -13,7 +15,7 @@ export async function handleAuthorizeAction(
|
|||||||
// 检查客户端是否限制了允许的用户
|
// 检查客户端是否限制了允许的用户
|
||||||
const client = await prisma.client.findUnique({
|
const client = await prisma.client.findUnique({
|
||||||
where: { id: clientId },
|
where: { id: clientId },
|
||||||
select: { allowedUsers: true },
|
select: { allowedUsers: true, name: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
@ -27,7 +29,15 @@ export async function handleAuthorizeAction(
|
|||||||
select: { username: true },
|
select: { username: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user || !client.allowedUsers.includes(user.username)) {
|
if (!user) {
|
||||||
|
console.error(`用户不存在: ${userId}`);
|
||||||
|
throw new Error("用户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client.allowedUsers.includes(user.username)) {
|
||||||
|
console.error(
|
||||||
|
`用户 ${user.username} 不在应用 ${client.name} 的允许列表中。允许列表: ${client.allowedUsers.join(", ")}`,
|
||||||
|
);
|
||||||
throw new Error("您没有权限使用此应用");
|
throw new Error("您没有权限使用此应用");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -44,5 +54,11 @@ export async function handleAuthorizeAction(
|
|||||||
lastUsedAt: new Date(), // 首次授权时间作为最后使用时间
|
lastUsedAt: new Date(), // 首次授权时间作为最后使用时间
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 刷新相关页面
|
||||||
|
revalidatePath("/dashboard");
|
||||||
|
revalidatePath(`/dashboard/clients/${clientId}`);
|
||||||
|
revalidatePath("/admin/clients");
|
||||||
|
revalidatePath(`/admin/clients/${clientId}`);
|
||||||
|
|
||||||
return redirectUrl;
|
return redirectUrl;
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,6 @@ export default async function ClientsPage({
|
|||||||
<TableHead>Client ID</TableHead>
|
<TableHead>Client ID</TableHead>
|
||||||
<TableHead>回调地址</TableHead>
|
<TableHead>回调地址</TableHead>
|
||||||
<TableHead>创建时间</TableHead>
|
<TableHead>创建时间</TableHead>
|
||||||
<TableHead>状态</TableHead>
|
|
||||||
<TableHead>操作</TableHead>
|
<TableHead>操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@ -130,16 +129,11 @@ export default async function ClientsPage({
|
|||||||
{new Date(client.createdAt).toLocaleString()}
|
{new Date(client.createdAt).toLocaleString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={client.enabled ? "default" : "destructive"}>
|
<div className="flex items-center gap-4">
|
||||||
{client.enabled ? "启用" : "禁用"}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
|
||||||
<ClientStatusToggle client={client} />
|
<ClientStatusToggle client={client} />
|
||||||
<Link href={`/admin/clients/${client.id}`}>
|
<Link href={`/admin/clients/${client.id}`}>
|
||||||
<Button variant="outline" size="sm">
|
<Button variant="outline" size="sm">
|
||||||
查看
|
详情
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,6 +66,7 @@ export default async function UsersPage({
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>ID</TableHead>
|
<TableHead>ID</TableHead>
|
||||||
<TableHead>用户名</TableHead>
|
<TableHead>用户名</TableHead>
|
||||||
|
<TableHead>显示名称</TableHead>
|
||||||
<TableHead>邮箱</TableHead>
|
<TableHead>邮箱</TableHead>
|
||||||
<TableHead>角色</TableHead>
|
<TableHead>角色</TableHead>
|
||||||
<TableHead>创建时间</TableHead>
|
<TableHead>创建时间</TableHead>
|
||||||
@ -76,6 +77,7 @@ export default async function UsersPage({
|
|||||||
{users.map((user) => (
|
{users.map((user) => (
|
||||||
<TableRow key={user.id}>
|
<TableRow key={user.id}>
|
||||||
<TableCell>{user.id}</TableCell>
|
<TableCell>{user.id}</TableCell>
|
||||||
|
<TableCell>{user.username}</TableCell>
|
||||||
<TableCell>{user.name}</TableCell>
|
<TableCell>{user.name}</TableCell>
|
||||||
<TableCell>{user.email}</TableCell>
|
<TableCell>{user.email}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
@ -72,7 +72,7 @@ export default async function DashboardPage() {
|
|||||||
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||||
<div className="mb-8 flex items-center justify-between">
|
<div className="mb-8 flex items-center justify-between">
|
||||||
<h2 className="text-lg font-medium">我的应用</h2>
|
<h2 className="text-lg font-medium">我的应用</h2>
|
||||||
<Link href="/dashboard/clients/new">
|
<Link href="/dashboard/clients">
|
||||||
<Button>创建新应用</Button>
|
<Button>创建新应用</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -123,7 +123,7 @@ export default async function DashboardPage() {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Link href="/dashboard/clients/new">
|
<Link href="/dashboard/clients">
|
||||||
<Button>创建新应用</Button>
|
<Button>创建新应用</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
@ -15,8 +15,8 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
||||||
interface ClientStatusToggleProps {
|
interface ClientStatusToggleProps {
|
||||||
client: Client | ExtendedClient;
|
client: Client | ExtendedClient;
|
||||||
@ -26,6 +26,7 @@ export function ClientStatusToggle({ client }: ClientStatusToggleProps) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
const [pendingState, setPendingState] = useState<boolean | null>(null);
|
||||||
|
|
||||||
const handleToggle = async () => {
|
const handleToggle = async () => {
|
||||||
try {
|
try {
|
||||||
@ -36,7 +37,7 @@ export function ClientStatusToggle({ client }: ClientStatusToggleProps) {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
enabled: !client.enabled,
|
enabled: pendingState,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -50,43 +51,53 @@ export function ClientStatusToggle({ client }: ClientStatusToggleProps) {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setShowDialog(false);
|
setShowDialog(false);
|
||||||
|
setPendingState(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex items-center gap-2">
|
||||||
<Badge
|
<Switch
|
||||||
variant={client.enabled ? "default" : "destructive"}
|
checked={client.enabled}
|
||||||
className="cursor-pointer"
|
disabled={isLoading}
|
||||||
onClick={() => setShowDialog(true)}
|
onCheckedChange={(checked) => {
|
||||||
>
|
setPendingState(checked);
|
||||||
{client.enabled ? "启用" : "禁用"}
|
setShowDialog(true);
|
||||||
</Badge>
|
}}
|
||||||
|
/>
|
||||||
|
<Label className="text-sm text-muted-foreground">
|
||||||
|
{client.enabled ? "已启用" : "已禁用"}
|
||||||
|
</Label>
|
||||||
|
|
||||||
<AlertDialog open={showDialog} onOpenChange={setShowDialog}>
|
<AlertDialog open={showDialog} onOpenChange={setShowDialog}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>
|
<AlertDialogTitle>
|
||||||
确定要{client.enabled ? "禁用" : "启用"}该应用吗?
|
确定要{pendingState ? "启用" : "禁用"}该应用吗?
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
{client.enabled
|
{pendingState
|
||||||
? "禁用后,该应用将无法使用 OAuth 服务,所有已授权的用户将无法访问。"
|
? "启用后,该应用将恢复使用 OAuth 服务的权限。"
|
||||||
: "启用后,该应用将恢复使用 OAuth 服务的权限。"}
|
: "禁用后,该应用将无法使用 OAuth 服务,所有已授权的用户将无法访问。"}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel disabled={isLoading}>取消</AlertDialogCancel>
|
<AlertDialogCancel
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={() => setPendingState(null)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className={client.enabled ? "bg-destructive" : undefined}
|
className={!pendingState ? "bg-destructive" : undefined}
|
||||||
>
|
>
|
||||||
确定
|
确定
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -160,11 +160,14 @@ export function EditClientForm({ client }: EditClientFormProps) {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>允许登录的用户</FormLabel>
|
<FormLabel>允许登录的用户</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="用户名列表,用逗号分隔" />
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="用户名列表,用逗号分隔(注意:请输入用户名,而不是显示名称)"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
留空表示允许所有 Q58
|
留空表示允许所有 Q58
|
||||||
论坛用户登录。如需限制,请输入用户名列表,用逗号分隔。
|
论坛用户登录。如需限制,请输入用户名列表(注意:必须是用户名,不是显示名称),用逗号分隔。
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
@ -11,7 +11,7 @@ const Switch = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SwitchPrimitives.Root
|
<SwitchPrimitives.Root
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@ -19,7 +19,7 @@ const Switch = React.forwardRef<
|
|||||||
>
|
>
|
||||||
<SwitchPrimitives.Thumb
|
<SwitchPrimitives.Thumb
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
|
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SwitchPrimitives.Root>
|
</SwitchPrimitives.Root>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user