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);