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"; "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;
} }

View File

@ -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>

View File

@ -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>

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="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>

View File

@ -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>
); );
} }

View File

@ -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>

View File

@ -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>