feat: Add user allowlist and improve client creation validation

- Implement user allowlist feature for client applications
- Add input validation for client creation form
- Handle parsing of allowed users list
- Improve error handling and user feedback during client creation
- Update authorization process to check client enabled status and user permissions
This commit is contained in:
wood chen 2025-02-20 02:13:06 +08:00
parent 0dd6a3338f
commit bd6b2b747d
4 changed files with 137 additions and 48 deletions

View File

@ -10,8 +10,28 @@ export async function AddClientAction(formData: FormData) {
const logo = formData.get("logo") as string;
const redirectUri = formData.get("redirectUri") as string;
const description = formData.get("description") as string;
const allowedUsersStr = formData.get("allowedUsers") as string;
const user = await getCurrentUser();
if (!user?.id) {
return { success: false, error: "未登录" };
}
// 验证必填字段
if (!name || !home || !redirectUri) {
return { success: false, error: "请填写所有必填字段" };
}
// 解析允许的用户列表
let allowedUsers: string[] = [];
if (allowedUsersStr) {
try {
allowedUsers = JSON.parse(allowedUsersStr);
} catch (error) {
console.error("Error parsing allowedUsers:", error);
return { success: false, error: "允许用户列表格式错误" };
}
}
// Generate a unique client ID and secret
let clientId = generateRandomKey();
@ -29,13 +49,14 @@ export async function AddClientAction(formData: FormData) {
description,
clientId,
clientSecret,
userId: user?.id || "",
userId: user.id,
allowedUsers,
});
console.log("New client created:", newClient);
return { success: true, client: newClient };
} catch (error) {
console.error("Error creating client:", error);
return { success: false, error: "Failed to create client" };
return { success: false, error: "创建应用失败" };
}
}

View File

@ -12,53 +12,60 @@ export async function handleAuthorizeAction(
clientId: string,
scope: string,
) {
// 检查客户端是否限制了允许的用户
const client = await prisma.client.findUnique({
where: { id: clientId },
select: { allowedUsers: true, name: true },
});
if (!client) {
throw new Error("应用不存在");
}
// 如果设置了允许的用户列表,检查当前用户是否在列表中
if (client.allowedUsers.length > 0) {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { username: true },
try {
// 检查客户端是否限制了允许的用户
const client = await prisma.client.findUnique({
where: { id: clientId },
select: { allowedUsers: true, name: true, enabled: true },
});
if (!user) {
console.error(`用户不存在: ${userId}`);
throw new Error("用户不存在");
if (!client) {
return { error: "应用不存在" };
}
if (!client.allowedUsers.includes(user.username)) {
console.error(
`用户 ${user.username} 不在应用 ${client.name} 的允许列表中。允许列表: ${client.allowedUsers.join(", ")}`,
);
throw new Error("您没有权限使用此应用");
if (!client.enabled) {
return { error: "该应用已被禁用" };
}
// 如果设置了允许的用户列表,检查当前用户是否在列表中
if (client.allowedUsers.length > 0) {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { username: true },
});
if (!user) {
return { error: "用户不存在" };
}
if (!client.allowedUsers.includes(user.username)) {
return {
error: "您没有权限使用此应用",
};
}
}
const oauthParams = new URLSearchParams(atob(oauth));
const redirectUrl = getAuthorizeUrl(oauthParams);
// 保存授权
await createAuthorization({
userId,
clientId,
scope,
enabled: true, // 新授权默认启用
lastUsedAt: new Date(), // 首次授权时间作为最后使用时间
});
// 刷新相关页面
revalidatePath("/dashboard");
revalidatePath(`/dashboard/clients/${clientId}`);
revalidatePath("/admin/clients");
revalidatePath(`/admin/clients/${clientId}`);
return { redirectUrl };
} catch (error) {
console.error("授权处理失败:", error);
return { error: "授权处理失败,请稍后重试" };
}
const oauthParams = new URLSearchParams(atob(oauth));
const redirectUrl = getAuthorizeUrl(oauthParams);
// 保存授权
await createAuthorization({
userId,
clientId,
scope,
enabled: true, // 新授权默认启用
lastUsedAt: new Date(), // 首次授权时间作为最后使用时间
});
// 刷新相关页面
revalidatePath("/dashboard");
revalidatePath(`/dashboard/clients/${clientId}`);
revalidatePath("/admin/clients");
revalidatePath(`/admin/clients/${clientId}`);
return redirectUrl;
}

View File

@ -47,6 +47,7 @@ export function AuthorizationCard({
null,
);
const [isAuthorizing, setIsAuthorizing] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const togglePermission = (id: string) => {
@ -56,16 +57,43 @@ export function AuthorizationCard({
const authorizingHandler = async () => {
try {
setIsAuthorizing(true);
const url = await handleAuthorizeAction(
setError(null);
const result = await handleAuthorizeAction(
oauthParams,
client.userId,
client.id,
permissions[0].id,
);
router.push(url);
if (result.error) {
setError(result.error);
setIsAuthorizing(false);
return;
}
if (result.redirectUrl) {
const url = await Promise.resolve(result.redirectUrl);
router.push(url);
}
} catch (error) {
setError("授权处理失败,请稍后重试");
setIsAuthorizing(false);
// 这里可以添加错误提示
}
};
const handleCancel = () => {
// 从 URL 中获取 redirect_uri 参数
const params = new URLSearchParams(atob(oauthParams));
const redirectUri = params.get("redirect_uri");
if (redirectUri) {
// 添加错误参数返回
const redirectUrl = new URL(redirectUri);
redirectUrl.searchParams.set("error", "access_denied");
redirectUrl.searchParams.set("error_description", "用户取消了授权请求");
router.push(redirectUrl.toString());
} else {
// 如果没有 redirect_uri返回首页
router.push("/");
}
};
@ -102,6 +130,13 @@ export function AuthorizationCard({
</CardHeader>
<CardContent className="space-y-4">
{error && (
<div className="mb-4 rounded-lg border border-red-200 bg-red-50 p-4 text-red-600">
<p className="text-sm">
<strong></strong> {error}
</p>
</div>
)}
<div className="space-y-3 rounded-lg bg-gray-100 p-4 dark:bg-gray-800">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
@ -143,6 +178,7 @@ export function AuthorizationCard({
<Button
variant="outline"
className="min-w-[100px] transition-all duration-200 hover:bg-gray-100"
onClick={handleCancel}
disabled={isAuthorizing}
>

View File

@ -30,6 +30,18 @@ export function AddClientButton() {
setIsLoading(true);
const formData = new FormData(event.currentTarget);
const allowedUsers = formData.get("allowedUsers") as string;
if (allowedUsers) {
formData.set(
"allowedUsers",
JSON.stringify(
allowedUsers
.split(",")
.map((u) => u.trim())
.filter(Boolean),
),
);
}
const response = await AddClientAction(formData);
setIsLoading(false);
@ -112,6 +124,19 @@ export function AddClientButton() {
disabled={isLoading}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="allowedUsers"></Label>
<Input
id="allowedUsers"
name="allowedUsers"
placeholder="用户名列表,用逗号分隔"
disabled={isLoading}
/>
<p className="text-sm text-muted-foreground">
Q58
</p>
</div>
</div>
<DialogFooter>
<Button