You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
digital-rmb-backend/docs/superpowers/plans/2026-08-04-current-user-con...

186 lines
7.9 KiB
Markdown

# Current User Context Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
**Goal:** Add no-argument current-user access for backend code and return the same complete profile from GET /api/v1/auth/me.
**Architecture:** PlatformTokenVerifier parses verified profile claims into VerifiedPlatformToken. LocalSsoAccountService saves the profile in platform_user_snapshot. CurrentUserService reads that snapshot from the Jwt in SecurityContext, and AuthController delegates to it.
**Tech Stack:** Java 8, Spring Boot 2.7, Spring Security, MyBatis-Plus, H2, JUnit 5, MockMvc.
## Global Constraints
- Keep Java 8 and ApiResponse.
- userId and raw roleid are long; school, college, major, class, and student identifiers are String.
- Do not put profile data in application JWTs or expose passwords or Tokens.
- Business requests only read the local snapshot.
---
### Task 1: Parse complete profile claims
**Files:**
- Modify: src/main/java/com/yau/digitalrmb/platformintegration/application/VerifiedPlatformToken.java
- Modify: src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifier.java
- Modify: src/test/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifierTest.java
**Interfaces:** VerifiedPlatformToken gains schoolId, schoolName, collegeId, collegeName, majorId, majorName, roleId, name, classId, className, studentId.
- [ ] **Step 1: Write the failing test**
~~~java
@Test
void parsesCompleteUserProfileFromVerifiedToken() throws Exception {
VerifiedPlatformToken token = verifier.verify(token(487L, "tzs001", "new-password", 2L,
now.plus(Duration.ofMinutes(5)), completeProfile()));
assertThat(token.getSchoolId()).isEqualTo("610000");
assertThat(token.getCollegeName()).isEqualTo("Computer College");
assertThat(token.getMajorName()).isEqualTo("Software Engineering");
assertThat(token.getRoleId()).isEqualTo(2L);
assertThat(token.getName()).isEqualTo("Test Student");
assertThat(token.getClassId()).isEqualTo("202401");
assertThat(token.getStudentId()).isEqualTo("20240001");
}
~~~
- [ ] **Step 2: Verify red**
Run: mvn -Dtest=PlatformTokenVerifierTest test -DforkCount=0 -B
Expected: FAIL because the new getters do not exist.
- [ ] **Step 3: Minimal implementation**
Add immutable fields and a full constructor to VerifiedPlatformToken while retaining the old constructor for existing callers. In PlatformTokenVerifier, preserve parsed roleid as roleId; parse name with username fallback and all remaining profile fields via:
~~~java
private String optionalText(JsonNode payload, String field) {
JsonNode node = payload.path(field);
return node.isMissingNode() || node.isNull() ? null : node.asText();
}
~~~
- [ ] **Step 4: Verify green**
Run: mvn -Dtest=PlatformTokenVerifierTest test -DforkCount=0 -B
Expected: PASS.
### Task 2: Persist the profile snapshot
**Files:**
- Modify: src/main/resources/schema.sql
- Modify: src/main/java/com/yau/digitalrmb/identity/infrastructure/persistence/entity/PlatformUserSnapshotEntity.java
- Modify: src/main/java/com/yau/digitalrmb/identity/application/LocalSsoAccountService.java
- Modify: src/test/java/com/yau/digitalrmb/identity/LocalSsoAccountServiceTest.java
**Interfaces:** synchronize(VerifiedPlatformToken) upserts every complete profile field in platform_user_snapshot.
- [ ] **Step 1: Write the failing test**
~~~java
@Test
void synchronizesCompleteProfileIntoLocalSnapshot() {
service.synchronize(completeToken(603L, "sso603", 2L));
Map<String, Object> row = new JdbcTemplate(dataSource).queryForMap(
"SELECT school_id, college_name, major_name, role_id, class_name, student_id "
+ "FROM platform_user_snapshot WHERE platform_user_id = 603");
assertThat(row).containsEntry("school_id", "610000")
.containsEntry("college_name", "Computer College")
.containsEntry("major_name", "Software Engineering")
.containsEntry("role_id", 2L)
.containsEntry("class_name", "Class 1")
.containsEntry("student_id", "20240001");
}
~~~
- [ ] **Step 2: Verify red**
Run: mvn -Dtest=LocalSsoAccountServiceTest test -DforkCount=0 -B
Expected: FAIL because the columns do not exist.
- [ ] **Step 3: Minimal implementation**
Add nullable school_id, school_name, college_id, college_name, major_id, major_name, role_id, class_id, class_name, student_id columns to the table creation and idempotent ADD COLUMN IF NOT EXISTS statements afterward. Map them with TableField in the snapshot Entity and copy token fields in upsertSnapshot.
- [ ] **Step 4: Verify green**
Run: mvn -Dtest=LocalSsoAccountServiceTest test -DforkCount=0 -B
Expected: PASS.
### Task 3: Implement the common current-user service
**Files:**
- Create: src/main/java/com/yau/digitalrmb/security/application/CurrentUser.java
- Create: src/main/java/com/yau/digitalrmb/security/application/CurrentUserService.java
- Create: src/test/java/com/yau/digitalrmb/security/CurrentUserServiceTest.java
**Interfaces:** CurrentUserService.getCurrentUser() returns schoolId, schoolName, collegeId, collegeName, majorId, majorName, roleid, userId, username, name, classId, className, studentid.
- [ ] **Step 1: Write the failing test**
Synchronize a complete profile, put JwtAuthenticationToken for that user into SecurityContextHolder, call currentUserService.getCurrentUser(), and assert all fields. Also test no snapshot and assert BusinessException ErrorCode.UNAUTHORIZED. Clear SecurityContextHolder in AfterEach.
- [ ] **Step 2: Verify red**
Run: mvn -Dtest=CurrentUserServiceTest test -DforkCount=0 -B
Expected: FAIL because the service does not exist.
- [ ] **Step 3: Minimal implementation**
~~~java
public CurrentUser getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !(authentication.getPrincipal() instanceof Jwt)) {
throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户身份无效");
}
long userId = parseUserId((Jwt) authentication.getPrincipal());
PlatformUserSnapshotEntity snapshot = snapshotMapper.selectById(userId);
if (snapshot == null) {
throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户身份不存在");
}
return map(snapshot);
}
~~~
map converts the complete snapshot, and parseUserId maps malformed subjects to UNAUTHORIZED.
- [ ] **Step 4: Verify green**
Run: mvn -Dtest=CurrentUserServiceTest test -DforkCount=0 -B
Expected: PASS.
### Task 4: Extend /auth/me
**Files:**
- Modify: src/main/java/com/yau/digitalrmb/security/interfaces/CurrentUserResponse.java
- Modify: src/main/java/com/yau/digitalrmb/security/interfaces/AuthController.java
- Modify: src/test/java/com/yau/digitalrmb/security/CurrentUserAndLogoutTest.java
- Modify: src/test/java/com/yau/digitalrmb/security/AuthControllerTest.java
**Interfaces:** GET /api/v1/auth/me returns exactly the 13 CurrentUser fields in data.
- [ ] **Step 1: Write the failing endpoint test**
Synchronize complete Token data and assert data.userId, username, name, schoolId, schoolName, collegeId, collegeName, majorId, majorName, roleid, classId, className, studentid. Update local login assertion to username.
- [ ] **Step 2: Verify red**
Run: mvn -Dtest=CurrentUserAndLogoutTest,AuthControllerTest test -DforkCount=0 -B
Expected: FAIL because the response DTO lacks the fields.
- [ ] **Step 3: Minimal implementation**
Replace CurrentUserResponse with the 13 CurrentUser fields. Inject CurrentUserService and have /me map currentUserService.getCurrentUser() into the response; do not query the Mapper in that method.
- [ ] **Step 4: Verify green and build**
Run: mvn -Dtest=CurrentUserAndLogoutTest,AuthControllerTest test -DforkCount=0 -B
Expected: PASS.
Run: mvn test -DforkCount=0 -B
Expected: PASS.
Run: mvn package -DskipTests -B
Expected: BUILD SUCCESS.