mirror of
https://github.com/woodchen-ink/Q58Connect.git
synced 2025-07-18 05:51:55 +08:00
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:
parent
0dd6a3338f
commit
bd6b2b747d
@ -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: "创建应用失败" };
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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}
|
||||
>
|
||||
取消
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user