diff --git a/hub/package-lock.json b/hub/package-lock.json
index 8df708e..be0fff4 100644
--- a/hub/package-lock.json
+++ b/hub/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@paradigm/hub",
- "version": "0.0.12",
+ "version": "0.0.13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@paradigm/hub",
- "version": "0.0.12",
+ "version": "0.0.13",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.3.202",
"@fastify/cookie": "^11.0.2",
diff --git a/hub/package.json b/hub/package.json
index 759fe88..5c545b1 100644
--- a/hub/package.json
+++ b/hub/package.json
@@ -1,6 +1,6 @@
{
"name": "@paradigm/hub",
- "version": "0.0.12",
+ "version": "0.0.13",
"private": true,
"type": "module",
"engines": {
diff --git a/hub/src/admin/routes/authRoutes.ts b/hub/src/admin/routes/authRoutes.ts
index ca6266b..fe8b54e 100644
--- a/hub/src/admin/routes/authRoutes.ts
+++ b/hub/src/admin/routes/authRoutes.ts
@@ -273,6 +273,34 @@ export async function registerAuthRoutes(app: FastifyInstance, config: AuthRoute
return reply.status(204).send();
});
+ app.get("/auth/feishu/complete", async (request, reply) => {
+ const query = request.query as { org?: string };
+ const organizationName = typeof query.org === "string" && query.org.trim() !== ""
+ ? query.org.trim()
+ : "当前组织";
+ return reply.type("text/html").send(`
+
+
+
+
+ Educraft 登录成功
+
+
+
+
+ ✅
+ 登录并加入组织成功
+ 你已加入 ${escapeHtml(organizationName)}。
+ 现在可以关闭本页面,返回飞书群再次 @机器人继续使用。
+
+
+`);
+ });
+
app.get("/api/me", async (request, reply) => {
try {
const auth = await requireSession(request, reply, guardDeps);
@@ -401,10 +429,13 @@ async function resolvePostLoginRedirect(
revokedAt: null,
organization: { status: "ACTIVE" },
},
- select: { organization: { select: { slug: true } } },
+ select: { organization: { select: { slug: true, name: true } } },
});
if (intended === null) return "/admin?error=not_an_active_org_member";
const orgRoot = `/admin/org/${intended.organization.slug}`;
+ if (returnTo === "/admin") {
+ return `/auth/feishu/complete?org=${encodeURIComponent(intended.organization.name)}`;
+ }
return returnTo === orgRoot || returnTo.startsWith(`${orgRoot}/`) ? returnTo : orgRoot;
}
if (returnTo !== "/admin" && returnTo.startsWith("/admin")) {
diff --git a/hub/test/integration/admin-auth.test.ts b/hub/test/integration/admin-auth.test.ts
index c32e61d..1aa5fe9 100644
--- a/hub/test/integration/admin-auth.test.ts
+++ b/hub/test/integration/admin-auth.test.ts
@@ -336,6 +336,25 @@ describe("admin auth + org API guards", () => {
},
})).resolves.toBe(1);
+ const defaultStart = await app.inject({ method: "GET", url: "/auth/feishu/test-default" });
+ const defaultAuthorize = new URL(String(defaultStart.headers.location));
+ const defaultState = defaultAuthorize.searchParams.get("state");
+ expect(defaultState).not.toBeNull();
+ const defaultCallback = await app.inject({
+ method: "GET",
+ url: `/auth/feishu/callback?code=ok&state=${encodeURIComponent(defaultState!)}`,
+ headers: { cookie: cookiePair(defaultStart.headers["set-cookie"], OAUTH_STATE_COOKIE_NAME) },
+ });
+ expect(defaultCallback.statusCode).toBe(302);
+ expect(defaultCallback.headers.location).toBe(
+ "/auth/feishu/complete?org=Test%20Default%20Organization",
+ );
+ const complete = await app.inject({ method: "GET", url: defaultCallback.headers.location! });
+ expect(complete.statusCode).toBe(200);
+ expect(complete.headers["content-type"]).toContain("text/html");
+ expect(complete.body).toContain("登录并加入组织成功");
+ expect(complete.body).toContain("Test Default Organization");
+
await connections.disable({ organizationId: DEFAULT_ORG_ID, actorUserId: "scoped-owner" });
const revoked = await app.inject({ method: "GET", url: "/api/me", headers: { cookie: sessionCookie } });
expect(revoked.statusCode).toBe(401);