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:
wood chen 2025-02-20 02:02:04 +08:00
parent 70e66294e3
commit 0dd6a3338f
7 changed files with 60 additions and 34 deletions

View File

@ -1,5 +1,7 @@
"use server";
import { revalidatePath } from "next/cache";
import { createAuthorization } from "@/lib/dto/authorization";
import { getAuthorizeUrl } from "@/lib/oauth/authorize-url";
import { prisma } from "@/lib/prisma";
@ -13,7 +15,7 @@ export async function handleAuthorizeAction(
// 检查客户端是否限制了允许的用户
const client = await prisma.client.findUnique({
where: { id: clientId },
select: { allowedUsers: true },
select: { allowedUsers: true, name: true },
});
if (!client) {
@ -27,7 +29,15 @@ export async function handleAuthorizeAction(
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("您没有权限使用此应用");
}
}
@ -44,5 +54,11 @@ export async function handleAuthorizeAction(
lastUsedAt: new Date(), // 首次授权时间作为最后使用时间
});
// 刷新相关页面
revalidatePath("/dashboard");
revalidatePath(`/dashboard/clients/${clientId}`);
revalidatePath("/admin/clients");
revalidatePath(`/admin/clients/${clientId}`);
return redirectUrl;
}

View File

@ -99,7 +99,6 @@ export default async function ClientsPage({
<TableHead>Client ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
@ -130,16 +129,11 @@ export default async function ClientsPage({
{new Date(client.createdAt).toLocaleString()}
</TableCell>
<TableCell>
<Badge variant={client.enabled ? "default" : "destructive"}>
{client.enabled ? "启用" : "禁用"}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center justify-end gap-2">
<div className="flex items-center gap-4">
<ClientStatusToggle client={client} />
<Link href={`/admin/clients/${client.id}`}>
<Button variant="outline" size="sm">
</Button>
</Link>
</div>

View File

@ -66,6 +66,7 @@ export default async function UsersPage({
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
@ -76,6 +77,7 @@ export default async function UsersPage({
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.id}</TableCell>
<TableCell>{user.username}</TableCell>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>

View File

@ -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="mb-8 flex items-center justify-between">
<h2 className="text-lg font-medium"></h2>
<Link href="/dashboard/clients/new">
<Link href="/dashboard/clients">
<Button></Button>
</Link>
</div>
@ -123,7 +123,7 @@ export default async function DashboardPage() {
</CardDescription>
</CardHeader>
<CardContent>
<Link href="/dashboard/clients/new">
<Link href="/dashboard/clients">
<Button></Button>
</Link>
</CardContent>

View File

@ -15,8 +15,8 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
interface ClientStatusToggleProps {
client: Client | ExtendedClient;
@ -26,6 +26,7 @@ export function ClientStatusToggle({ client }: ClientStatusToggleProps) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [showDialog, setShowDialog] = useState(false);
const [pendingState, setPendingState] = useState<boolean | null>(null);
const handleToggle = async () => {
try {
@ -36,7 +37,7 @@ export function ClientStatusToggle({ client }: ClientStatusToggleProps) {
"Content-Type": "application/json",
},
body: JSON.stringify({
enabled: !client.enabled,
enabled: pendingState,
}),
});
@ -50,43 +51,53 @@ export function ClientStatusToggle({ client }: ClientStatusToggleProps) {
} finally {
setIsLoading(false);
setShowDialog(false);
setPendingState(null);
}
};
return (
<>
<Badge
variant={client.enabled ? "default" : "destructive"}
className="cursor-pointer"
onClick={() => setShowDialog(true)}
>
{client.enabled ? "启用" : "禁用"}
</Badge>
<div className="flex items-center gap-2">
<Switch
checked={client.enabled}
disabled={isLoading}
onCheckedChange={(checked) => {
setPendingState(checked);
setShowDialog(true);
}}
/>
<Label className="text-sm text-muted-foreground">
{client.enabled ? "已启用" : "已禁用"}
</Label>
<AlertDialog open={showDialog} onOpenChange={setShowDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{client.enabled ? "禁用" : "启用"}
{pendingState ? "启用" : "禁用"}
</AlertDialogTitle>
<AlertDialogDescription>
{client.enabled
? "禁用后,该应用将无法使用 OAuth 服务,所有已授权的用户将无法访问。"
: "启用后,该应用将恢复使用 OAuth 服务的权限。"}
{pendingState
? "启用后,该应用将恢复使用 OAuth 服务的权限。"
: "禁用后,该应用将无法使用 OAuth 服务,所有已授权的用户将无法访问。"}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isLoading}></AlertDialogCancel>
<AlertDialogCancel
disabled={isLoading}
onClick={() => setPendingState(null)}
>
</AlertDialogCancel>
<AlertDialogAction
onClick={handleToggle}
disabled={isLoading}
className={client.enabled ? "bg-destructive" : undefined}
className={!pendingState ? "bg-destructive" : undefined}
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
</div>
);
}

View File

@ -160,11 +160,14 @@ export function EditClientForm({ client }: EditClientFormProps) {
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input {...field} placeholder="用户名列表,用逗号分隔" />
<Input
{...field}
placeholder="用户名列表,用逗号分隔(注意:请输入用户名,而不是显示名称)"
/>
</FormControl>
<FormDescription>
Q58
</FormDescription>
<FormMessage />
</FormItem>

View File

@ -11,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
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,
)}
{...props}
@ -19,7 +19,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
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>