From f47377cff15c00f074e500468086c9985d6ad367 Mon Sep 17 00:00:00 2001 From: vsxd Date: Thu, 12 Mar 2026 18:19:03 +0800 Subject: [PATCH] feat(cli): add Device Auth controllers --- .../controller/DeviceAuthController.java | 32 +++++++++ .../controller/DeviceAuthWebController.java | 35 +++++++++ .../controller/DeviceAuthControllerTest.java | 71 +++++++++++++++++++ .../skillhub/auth/config/SecurityConfig.java | 1 + 4 files changed, 139 insertions(+) create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/DeviceAuthController.java create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/DeviceAuthWebController.java create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/DeviceAuthControllerTest.java diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/DeviceAuthController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/DeviceAuthController.java new file mode 100644 index 00000000..81a6c536 --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/DeviceAuthController.java @@ -0,0 +1,32 @@ +package com.iflytek.skillhub.controller; + +import com.iflytek.skillhub.auth.device.DeviceAuthService; +import com.iflytek.skillhub.auth.device.DeviceCodeResponse; +import com.iflytek.skillhub.auth.device.DeviceTokenResponse; +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.RestController; + +@RestController +@RequestMapping("/api/v1/cli/auth/device") +public class DeviceAuthController { + + private final DeviceAuthService deviceAuthService; + + public DeviceAuthController(DeviceAuthService deviceAuthService) { + this.deviceAuthService = deviceAuthService; + } + + @PostMapping("/code") + public DeviceCodeResponse requestDeviceCode() { + return deviceAuthService.generateDeviceCode(); + } + + @PostMapping("/token") + public DeviceTokenResponse pollToken(@RequestBody TokenRequest request) { + return deviceAuthService.pollToken(request.deviceCode()); + } + + public record TokenRequest(String deviceCode) {} +} diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/DeviceAuthWebController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/DeviceAuthWebController.java new file mode 100644 index 00000000..704f1c7c --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/DeviceAuthWebController.java @@ -0,0 +1,35 @@ +package com.iflytek.skillhub.controller; + +import com.iflytek.skillhub.auth.device.DeviceAuthService; +import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; +import com.iflytek.skillhub.dto.ApiResponse; +import com.iflytek.skillhub.dto.ApiResponseFactory; +import com.iflytek.skillhub.dto.MessageResponse; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +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.RestController; + +@RestController +@RequestMapping("/api/v1/device") +public class DeviceAuthWebController extends BaseApiController { + + private final DeviceAuthService deviceAuthService; + + public DeviceAuthWebController(ApiResponseFactory responseFactory, DeviceAuthService deviceAuthService) { + super(responseFactory); + this.deviceAuthService = deviceAuthService; + } + + @PostMapping("/authorize") + public ApiResponse authorizeDevice( + @RequestBody AuthorizeRequest request, + @AuthenticationPrincipal PlatformPrincipal principal + ) { + deviceAuthService.authorizeDeviceCode(request.userCode(), principal.userId()); + return ok("response.success.update", new MessageResponse("Device authorized successfully")); + } + + public record AuthorizeRequest(String userCode) {} +} diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/DeviceAuthControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/DeviceAuthControllerTest.java new file mode 100644 index 00000000..66787eab --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/DeviceAuthControllerTest.java @@ -0,0 +1,71 @@ +package com.iflytek.skillhub.controller; + +import com.iflytek.skillhub.auth.device.DeviceAuthService; +import com.iflytek.skillhub.auth.device.DeviceCodeResponse; +import com.iflytek.skillhub.auth.device.DeviceTokenResponse; +import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class DeviceAuthControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private DeviceAuthService deviceAuthService; + + @MockBean + private NamespaceMemberRepository namespaceMemberRepository; + + @Test + void requestDeviceCode_returns_code() throws Exception { + DeviceCodeResponse response = new DeviceCodeResponse( + "device_abc123", + "ABCD-1234", + "https://skillhub.example.com/device", + 900, + 5 + ); + + given(deviceAuthService.generateDeviceCode()).willReturn(response); + + mockMvc.perform(post("/api/v1/cli/auth/device/code") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.deviceCode").value("device_abc123")) + .andExpect(jsonPath("$.userCode").value("ABCD-1234")) + .andExpect(jsonPath("$.verificationUri").value("https://skillhub.example.com/device")) + .andExpect(jsonPath("$.expiresIn").value(900)) + .andExpect(jsonPath("$.interval").value(5)); + } + + @Test + void pollToken_returns_pending() throws Exception { + DeviceTokenResponse response = DeviceTokenResponse.pending(); + + given(deviceAuthService.pollToken("device_abc123")).willReturn(response); + + mockMvc.perform(post("/api/v1/cli/auth/device/token") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"deviceCode\": \"device_abc123\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.error").value("authorization_pending")) + .andExpect(jsonPath("$.accessToken").isEmpty()) + .andExpect(jsonPath("$.tokenType").isEmpty()); + } +} diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java index 83da210e..4a418d36 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java @@ -66,6 +66,7 @@ public class SecurityConfig { "/api/v1/health", "/api/v1/auth/providers", "/api/v1/auth/me", + "/api/v1/cli/auth/device/**", "/actuator/health", "/v3/api-docs/**", "/swagger-ui/**",