feat(cli): add Device Auth controllers

This commit is contained in:
vsxd 2026-03-12 18:19:03 +08:00
parent 9a4d64b11b
commit f47377cff1
4 changed files with 139 additions and 0 deletions

View file

@ -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) {}
}

View file

@ -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<MessageResponse> 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) {}
}

View file

@ -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());
}
}

View file

@ -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/**",