mirror of
https://github.com/woodchen-ink/Q58Connect.git
synced 2025-07-18 05:51: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";
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user