mirror of
https://github.com/woodchen-ink/Q58Connect.git
synced 2025-07-18 14:01: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 logo = formData.get("logo") as string;
|
||||||
const redirectUri = formData.get("redirectUri") as string;
|
const redirectUri = formData.get("redirectUri") as string;
|
||||||
const description = formData.get("description") as string;
|
const description = formData.get("description") as string;
|
||||||
|
const allowedUsersStr = formData.get("allowedUsers") as string;
|
||||||
|
|
||||||
const user = await getCurrentUser();
|
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
|
// Generate a unique client ID and secret
|
||||||
let clientId = generateRandomKey();
|
let clientId = generateRandomKey();
|
||||||
@ -29,13 +49,14 @@ export async function AddClientAction(formData: FormData) {
|
|||||||
description,
|
description,
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
userId: user?.id || "",
|
userId: user.id,
|
||||||
|
allowedUsers,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("New client created:", newClient);
|
console.log("New client created:", newClient);
|
||||||
return { success: true, client: newClient };
|
return { success: true, client: newClient };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating client:", 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,
|
clientId: string,
|
||||||
scope: string,
|
scope: string,
|
||||||
) {
|
) {
|
||||||
// 检查客户端是否限制了允许的用户
|
try {
|
||||||
const client = await prisma.client.findUnique({
|
// 检查客户端是否限制了允许的用户
|
||||||
where: { id: clientId },
|
const client = await prisma.client.findUnique({
|
||||||
select: { allowedUsers: true, name: true },
|
where: { id: clientId },
|
||||||
});
|
select: { allowedUsers: true, name: true, enabled: true },
|
||||||
|
|
||||||
if (!client) {
|
|
||||||
throw new Error("应用不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果设置了允许的用户列表,检查当前用户是否在列表中
|
|
||||||
if (client.allowedUsers.length > 0) {
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { id: userId },
|
|
||||||
select: { username: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!client) {
|
||||||
console.error(`用户不存在: ${userId}`);
|
return { error: "应用不存在" };
|
||||||
throw new Error("用户不存在");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!client.allowedUsers.includes(user.username)) {
|
if (!client.enabled) {
|
||||||
console.error(
|
return { error: "该应用已被禁用" };
|
||||||
`用户 ${user.username} 不在应用 ${client.name} 的允许列表中。允许列表: ${client.allowedUsers.join(", ")}`,
|
|
||||||
);
|
|
||||||
throw new 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,
|
null,
|
||||||
);
|
);
|
||||||
const [isAuthorizing, setIsAuthorizing] = useState(false);
|
const [isAuthorizing, setIsAuthorizing] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const togglePermission = (id: string) => {
|
const togglePermission = (id: string) => {
|
||||||
@ -56,16 +57,43 @@ export function AuthorizationCard({
|
|||||||
const authorizingHandler = async () => {
|
const authorizingHandler = async () => {
|
||||||
try {
|
try {
|
||||||
setIsAuthorizing(true);
|
setIsAuthorizing(true);
|
||||||
const url = await handleAuthorizeAction(
|
setError(null);
|
||||||
|
const result = await handleAuthorizeAction(
|
||||||
oauthParams,
|
oauthParams,
|
||||||
client.userId,
|
client.userId,
|
||||||
client.id,
|
client.id,
|
||||||
permissions[0].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) {
|
} catch (error) {
|
||||||
|
setError("授权处理失败,请稍后重试");
|
||||||
setIsAuthorizing(false);
|
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>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="space-y-4">
|
<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">
|
<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">
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||||
请求的权限
|
请求的权限
|
||||||
@ -143,6 +178,7 @@ export function AuthorizationCard({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="min-w-[100px] transition-all duration-200 hover:bg-gray-100"
|
className="min-w-[100px] transition-all duration-200 hover:bg-gray-100"
|
||||||
|
onClick={handleCancel}
|
||||||
disabled={isAuthorizing}
|
disabled={isAuthorizing}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
|
@ -30,6 +30,18 @@ export function AddClientButton() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const formData = new FormData(event.currentTarget);
|
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);
|
const response = await AddClientAction(formData);
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -112,6 +124,19 @@ export function AddClientButton() {
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
|
Loading…
x
Reference in New Issue
Block a user