From ab14aa096531da4cd737213a652ec3d2bb303b41 Mon Sep 17 00:00:00 2001 From: Liu Rui Date: Fri, 14 Aug 2026 17:33:50 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=E5=93=81=E7=89=8C=E7=99=BB=E5=BD=95=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../topics/identity-permission/WEB_API.md | 1 + .../spring/iam/web/LoginWebController.java | 19 +++++ .../iam/web/LoginWebControllerTest.java | 22 ++++++ .../spring/iam/tenant/TenantLoginContext.java | 10 +++ muyun-web/src/app/LoginView.vue | 79 ++++++++++++++++--- muyun-web/src/consumer/index.ts | 1 + muyun-web/src/web-contracts/index.ts | 6 ++ muyun-web/src/web-core/clients.ts | 4 + .../tests/app/LoginView.component.test.ts | 54 +++++++++++++ .../consumer/publicSurfaceContracts.test.ts | 1 + muyun-web/tests/web-core/webCore.test.ts | 24 ++++++ 11 files changed, 210 insertions(+), 11 deletions(-) create mode 100644 muyun-iam/src/main/java/net/ximatai/muyun/spring/iam/tenant/TenantLoginContext.java create mode 100644 muyun-web/tests/app/LoginView.component.test.ts diff --git a/docs/platform/topics/identity-permission/WEB_API.md b/docs/platform/topics/identity-permission/WEB_API.md index b4e78585..44c35995 100644 --- a/docs/platform/topics/identity-permission/WEB_API.md +++ b/docs/platform/topics/identity-permission/WEB_API.md @@ -9,6 +9,7 @@ | 方法 | URL | 功能 | | ------ | ------------------- | -------------------------------------------------------------------------------------------------- | | `POST` | `/iam.auth/login` | 用户登录。请求包含 `tenantId`、`username`、`password`;返回 Bearer token、当前登录 `sessionId`、签发时间和当前用户信息。 | +| `GET` | `/iam.auth/login-context?tenantId={tenantId}` | 匿名读取 URL 锁定租户的公开登录上下文。仅返回锁定租户标识及工作台品牌投影,供标准登录页展示 Logo、主标题和副标题;目标租户必须处于启用状态。 | | `POST` | `/iam.auth/logout` | 当前 Bearer token 登出。token 从 `Authorization: Bearer ...` 读取。 | | `GET` | `/iam.auth/context` | 返回当前请求解析出的用户上下文,用于前端会话恢复和启动态确认。 | | `GET` | `/iam.auth/tenant-branding` | 返回当前租户的工作台品牌投影(`lightLogo`、可选的 `darkLogo`);Logo 以受限 Base64 图片 data URL 保存。 | diff --git a/muyun-iam-web/src/main/java/net/ximatai/muyun/spring/iam/web/LoginWebController.java b/muyun-iam-web/src/main/java/net/ximatai/muyun/spring/iam/web/LoginWebController.java index 3c02c05f..29757c91 100644 --- a/muyun-iam-web/src/main/java/net/ximatai/muyun/spring/iam/web/LoginWebController.java +++ b/muyun-iam-web/src/main/java/net/ximatai/muyun/spring/iam/web/LoginWebController.java @@ -13,6 +13,7 @@ import net.ximatai.muyun.spring.iam.user.UpdateCurrentUserProfileRequest; import net.ximatai.muyun.spring.iam.user.UserSessionService; import net.ximatai.muyun.spring.iam.tenant.TenantBranding; +import net.ximatai.muyun.spring.iam.tenant.TenantLoginContext; import net.ximatai.muyun.spring.iam.tenant.TenantService; import net.ximatai.muyun.spring.platform.web.PlatformStaticActionDeclaration; import net.ximatai.muyun.spring.common.platform.ActionAccessMode; @@ -22,6 +23,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.beans.factory.annotation.Autowired; @@ -100,6 +102,16 @@ public TenantBranding tenantBranding() { : tenantService.branding(tenantId); } + /** + * Resolves branding for a tenant explicitly selected by the unauthenticated login entry. + */ + @GetMapping("/login-context") + public TenantLoginContext loginContext(@RequestParam String tenantId) { + TenantService service = requireTenantService(); + service.requireActiveTenant(tenantId); + return new TenantLoginContext(tenantId, service.branding(tenantId)); + } + private String bearerToken(HttpServletRequest request) { String header = request.getHeader("Authorization"); if (header == null || header.isBlank()) { @@ -124,6 +136,13 @@ private CurrentUserProfileService requireCurrentUserProfileService() { return currentUserProfileService; } + private TenantService requireTenantService() { + if (tenantService == null) { + throw new IllegalStateException("tenant service is not available"); + } + return tenantService; + } + private String clientIp(HttpServletRequest request) { String forwardedFor = request.getHeader("X-Forwarded-For"); if (forwardedFor != null && !forwardedFor.isBlank()) { diff --git a/muyun-iam-web/src/test/java/net/ximatai/muyun/spring/iam/web/LoginWebControllerTest.java b/muyun-iam-web/src/test/java/net/ximatai/muyun/spring/iam/web/LoginWebControllerTest.java index 1cc71b38..ad4a4e3c 100644 --- a/muyun-iam-web/src/test/java/net/ximatai/muyun/spring/iam/web/LoginWebControllerTest.java +++ b/muyun-iam-web/src/test/java/net/ximatai/muyun/spring/iam/web/LoginWebControllerTest.java @@ -11,6 +11,7 @@ import net.ximatai.muyun.spring.iam.user.CurrentUserProfile; import net.ximatai.muyun.spring.iam.user.CurrentUserProfileService; import net.ximatai.muyun.spring.iam.tenant.TenantBranding; +import net.ximatai.muyun.spring.iam.tenant.Tenant; import net.ximatai.muyun.spring.iam.tenant.TenantService; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -92,6 +93,27 @@ void shouldExposeOnlyTheCurrentTenantBrandingToWorkbenchStartup() throws Excepti verify(tenantService).branding("tenant-a"); } + @Test + void shouldExposeActiveLockedTenantBrandingBeforeAuthentication() throws Exception { + TenantService tenantService = mock(TenantService.class); + when(tenantService.requireActiveTenant("tenant-a")).thenReturn(mock(Tenant.class)); + when(tenantService.branding("tenant-a")) + .thenReturn(new TenantBranding("data:image/png;base64,bGlnaHQ=", null, + "logoWithTitle", "租户 A", "租户专属工作台")); + LoginWebController controller = new LoginWebController(mock(UserSessionService.class), tenantService); + MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + + mvc.perform(get("/iam.auth/login-context").param("tenantId", "tenant-a")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.tenantId").value("tenant-a")) + .andExpect(jsonPath("$.branding.lightLogo").value("data:image/png;base64,bGlnaHQ=")) + .andExpect(jsonPath("$.branding.title").value("租户 A")) + .andExpect(jsonPath("$.branding.subtitle").value("租户专属工作台")); + + verify(tenantService).requireActiveTenant("tenant-a"); + verify(tenantService).branding("tenant-a"); + } + @Test void shouldReturnUnauthorizedWhenLoginCredentialsAreInvalid() throws Exception { UserSessionService userSessionService = mock(UserSessionService.class); diff --git a/muyun-iam/src/main/java/net/ximatai/muyun/spring/iam/tenant/TenantLoginContext.java b/muyun-iam/src/main/java/net/ximatai/muyun/spring/iam/tenant/TenantLoginContext.java new file mode 100644 index 00000000..c4423f1d --- /dev/null +++ b/muyun-iam/src/main/java/net/ximatai/muyun/spring/iam/tenant/TenantLoginContext.java @@ -0,0 +1,10 @@ +package net.ximatai.muyun.spring.iam.tenant; + +/** + * Public, unauthenticated login-entry facts for a tenant selected by the login URL. + * + *

This projection deliberately contains only the locked tenant identity and its + * decorative branding. It is not a tenant-management or current-session projection.

+ */ +public record TenantLoginContext(String tenantId, TenantBranding branding) { +} diff --git a/muyun-web/src/app/LoginView.vue b/muyun-web/src/app/LoginView.vue index 5abf9e15..49a56849 100644 --- a/muyun-web/src/app/LoginView.vue +++ b/muyun-web/src/app/LoginView.vue @@ -1,7 +1,7 @@