feat: Implement User CRUD (COBOL COUSR00C-03C → Spring Boot)#175
feat: Implement User CRUD (COBOL COUSR00C-03C → Spring Boot)#175devin-ai-integration[bot] wants to merge 2 commits into
Conversation
Migrates COBOL programs COUSR00C (List Users), COUSR01C (Add User), COUSR02C (Update User), and COUSR03C (Delete User) from CICS/VSAM to a Spring Boot 3.x REST API. Components: - User entity mapping USRSEC record layout (CSUSR01Y.cpy) - UserRepository with JPA (replaces VSAM KSDS operations) - UserService with paginated list, add, get, update, delete - UserController with 5 REST endpoints under /api/admin/users - Spring Security with HTTP Basic + admin-only access (ROLE_ADMIN) - BCrypt password hashing (replaces plaintext SEC-USR-PWD) - Bean validation matching COBOL field constraints - H2 seed data with 3 users (ADMIN001, USER0001, USER0002) - Integration tests covering all CRUD ops + security (8 tests)
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| public UserResponse addUser(UserCreateRequest request) { | ||
| if (userRepository.existsByUserId(request.getUserId())) { | ||
| throw new UserAlreadyExistsException(request.getUserId()); | ||
| } | ||
| User user = new User(); | ||
| user.setUserId(request.getUserId()); | ||
| user.setFirstName(request.getFirstName()); | ||
| user.setLastName(request.getLastName()); | ||
| user.setPassword(passwordEncoder.encode(request.getPassword())); | ||
| user.setUserType(request.getUserType()); | ||
| userRepository.save(user); | ||
| return toResponse(user); |
There was a problem hiding this comment.
🔴 addUser can silently overwrite existing user due to JPA merge on manually-assigned @id
The User entity uses a manually-assigned @Id (String userId, no @GeneratedValue) and does not implement Persistable<String>. Spring Data JPA's SimpleJpaRepository.save() checks entityInformation.isNew(entity) which returns false when the ID is non-null, causing it to call entityManager.merge() instead of entityManager.persist(). In the addUser method, there is a TOCTOU race between the existsByUserId check (UserService.java:55) and the save() call (UserService.java:64): if a concurrent request creates the same userId and commits between these two calls, merge() will find the existing row and silently perform an UPDATE, overwriting the first user's password, name, and user type — without any error. This is a data integrity and security issue in a user management system.
Race condition timeline
- Thread A:
existsByUserId("X")→ false - Thread B:
existsByUserId("X")→ false - Thread A:
save()→ merge → SELECT → not found → INSERT → COMMIT - Thread B:
save()→ merge → SELECT → found → UPDATE (silently overwrites Thread A's user)
Both threads return 201 Created. Thread A's data is lost.
Prompt for agents
The root cause is that the User entity has a manually-assigned @Id (String userId) without @GeneratedValue, and does not implement Persistable<String>. This causes Spring Data JPA's save() to always call merge() instead of persist() for new entities, since isNew() checks if the ID is null.
Fix approach: Have the User entity implement org.springframework.data.domain.Persistable<String>. Add a transient boolean field (e.g., @Transient private boolean isNew = true) and override isNew() to return it. Override getId() to return userId. Set isNew to false in a @PostLoad / @PrePersist callback or after loading from the repository.
Alternatively, inject EntityManager into UserService and call entityManager.persist(user) directly instead of userRepository.save(user) in the addUser method. This ensures an INSERT is always attempted, and a duplicate key will throw a constraint violation rather than silently merging.
If using the persist() approach, also add a handler for DataIntegrityViolationException in GlobalExceptionHandler to return 409 Conflict as a safety net.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Migrates the COBOL User Management CRUD programs (COUSR00C, COUSR01C, COUSR02C, COUSR03C) from CICS/VSAM to a Spring Boot 3.x REST API under the new
carddemo-java/directory.COBOL → Java Mapping
GET /api/admin/users?page=0&size=10— Spring Data paginated queryPOST /api/admin/users—existsByUserId()check + JPA saveGET + PUT /api/admin/users/{userId}— JPA findById + saveDELETE /api/admin/users/{userId}— JPA deleteByIdComponents Added
Usermaps the USRSEC record layout (CSUSR01Y.cpy — 80-byte record with userId PK, firstName, lastName, password, userType)UserRepositoryextends JpaRepository withexistsByUserId()for duplicate checkingUserServicewith 5 methods replacing all 4 COBOL programsUserControllerwith 5 REST endpoints under/api/admin/usersUserDetailsService. Admin-only access enforced viaROLE_ADMIN(maps COBOLCDEMO-USRTYP-ADMINcheck)UserCreateRequest,UserUpdateRequest,UserResponse,PagedResponse<T>— password is never exposed in responsesTech Stack
Spring Boot 3.2.5, Java 17, Spring Data JPA, Spring Security, H2 database, Lombok, Bean Validation
Review & Testing Checklist for Human
ADMIN001/passwordvia HTTP Basic/api/admin/**endpointsTest Plan
Notes
data.sqlLink to Devin session: https://partner-workshops.devinenterprise.com/sessions/0a614e7c53b04a438d5a2c4af3920c2b
Requested by: @bsmitches