Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CD - Build Artifact

on:
push:
branches: [ main ]
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Set up JDK 25
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '25'
cache: 'maven'
check-latest: true

- name: Build JAR
run: mvn package -DskipTests

- name: Upload JAR artifact
uses: actions/upload-artifact@v7
with:
name: team6-backend
path: target/*.jar
retention-days: 7
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI - Build and Test

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Set up JDK 25
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '25'
cache: 'maven'
check-latest: true

- name: Check code formatting with Spotless
run: mvn spotless:check

- name: Run tests with Maven
run: mvn test

- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-reports
path: target/surefire-reports/
78 changes: 78 additions & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: PR - Quality Checks

on:
pull_request:
branches: [ main ]
types: [ opened, synchronize, reopened, ready_for_review ]

jobs:
quality-checks:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Set up JDK 25
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '25'
cache: 'maven'
check-latest: true

- name: Check code formatting with Spotless
run: mvn spotless:check

- name: Run static code analysis with PMD
run: mvn pmd:check
continue-on-error: true

# - name: Check for vulnerabilities
# run: mvn dependency-check:check
# continue-on-error: true

- name: Run all tests with coverage
run: mvn test jacoco:report

- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-reports
path: target/surefire-reports/

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v7
with:
name: coverage-report
path: target/site/jacoco/

- name: Comment PR with coverage summary
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
try {
const coverage = fs.readFileSync('target/site/jacoco/index.html', 'utf8');
const match = coverage.match(/Total<\/td><td[^>]*>(\d+)%<\/td>/);
if (match) {
const coveragePercent = match[1];
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: `## Test Coverage Report\n\n**Coverage: ${coveragePercent}%**\n\nQuality checks completed.\n\nFull coverage report available in workflow artifacts.`
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: `## Test Coverage Report\n\nCoverage report generated but could not parse percentage. Check artifacts for details.`
});
}
} catch(e) {
console.log('Could not parse coverage report:', e.message);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@SpringBootApplication
public class Team6BackendApplication {

public static void main(String[] args) {
SpringApplication.run(Team6BackendApplication.class, args);
}
}
public static void main(String[] args) {
SpringApplication.run(Team6BackendApplication.class, args);
}
}
220 changes: 101 additions & 119 deletions src/main/java/org/example/team6backend/admin/AdminController.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,122 +29,104 @@
@Slf4j
public class AdminController {

private final UserService userService;
private final UserMapper userMapper;

@GetMapping("/users")
public ResponseEntity<Page<UserResponse>> getUsers(
@RequestParam(required = false) String email,
@RequestParam(required = false) String name,
@RequestParam(required = false) UserRole role,
@RequestParam(required = false) Boolean active,
@RequestParam(required = false) String search,
@PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {

Page<AppUser> users = resolveUsers(email, name, role, active, search, pageable);
return ResponseEntity.ok(userMapper.toResponsePage(users));
}

@GetMapping("/users/pending")
public ResponseEntity<Page<UserResponse>> getPendingUsers(
@PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
return ResponseEntity.ok(
userMapper.toResponsePage(userService.getUsersByRolePaginated(UserRole.PENDING, pageable))
);
}

@GetMapping("/users/{userId}")
public ResponseEntity<UserResponse> getUser(@PathVariable String userId) {
return ResponseEntity.ok(userMapper.toResponse(userService.getUserById(userId)));
}

@PostMapping("/users/{userId}/approve")
public ResponseEntity<UserResponse> approveUser(@PathVariable String userId) {
AppUser approvedUser = userService.approvePendingUser(userId);
return ResponseEntity.ok(userMapper.toResponse(approvedUser));
}

@PatchMapping("/users/{userId}/role")
public ResponseEntity<UserResponse> updateUserRole(
@PathVariable String userId,
@Valid @RequestBody UpdateUserRoleRequest request,
@AuthenticationPrincipal CustomUserDetails currentUser) {

if (currentUser.getUser().getId().equals(userId)) {
throw new IllegalStateException("You cannot change your own role");
}

if (request.role() != UserRole.ADMIN) {
AppUser targetUser = userService.getUserById(userId);
if (targetUser.getRole() == UserRole.ADMIN) {
long adminCount = userService.getAllUsers().stream()
.filter(u -> u.getRole() == UserRole.ADMIN)
.count();
if (adminCount <= 1) {
throw new IllegalStateException("Cannot remove the last admin user");
}
}
}

AppUser updatedUser = userService.updateUserRole(userId, request.role());
return ResponseEntity.ok(userMapper.toResponse(updatedUser));
}

@PatchMapping("/users/{userId}/status")
public ResponseEntity<UserResponse> updateUserStatus(
@PathVariable String userId,
@Valid @RequestBody UpdateUserStatusRequest request,
@AuthenticationPrincipal CustomUserDetails currentUser) {

if (currentUser.getUser().getId().equals(userId) && !request.active()) {
throw new IllegalStateException("You cannot deactivate your own account");
}

AppUser updatedUser = userService.updateUserActiveStatus(userId, request.active());
return ResponseEntity.ok(userMapper.toResponse(updatedUser));
}

@DeleteMapping("/users/{userId}")
public ResponseEntity<Void> deleteUser(
@PathVariable String userId,
@AuthenticationPrincipal CustomUserDetails currentUser) {

if (currentUser.getUser().getId().equals(userId)) {
throw new IllegalStateException("You cannot delete your own account");
}

userService.deleteUser(userId);
return ResponseEntity.noContent().build();
}

@GetMapping("/stats")
public ResponseEntity<Map<String, Long>> getStats() {
Map<String, Long> stats = Map.of(
"totalUsers", (long) userService.getAllUsers().size(),
"pendingUsers", (long) userService.getUsersByRole(UserRole.PENDING).size(),
"residents", (long) userService.getUsersByRole(UserRole.RESIDENT).size(),
"handlers", (long) userService.getUsersByRole(UserRole.HANDLER).size(),
"admins", (long) userService.getUsersByRole(UserRole.ADMIN).size()
);
return ResponseEntity.ok(stats);
}

private Page<AppUser> resolveUsers(
String email,
String name,
UserRole role,
Boolean active,
String search,
Pageable pageable
) {
if (search != null && !search.trim().isEmpty()) {
return userService.searchUsers(search, pageable);
}

if (email != null || name != null || role != null || active != null) {
return userService.getUsersWithFilters(email, name, role, active, pageable);
}

return userService.getAllUsersPaginated(pageable);
}
}
private final UserService userService;
private final UserMapper userMapper;

@GetMapping("/users")
public ResponseEntity<Page<UserResponse>> getUsers(@RequestParam(required = false) String email,
@RequestParam(required = false) String name, @RequestParam(required = false) UserRole role,
@RequestParam(required = false) Boolean active, @RequestParam(required = false) String search,
@PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {

Page<AppUser> users = resolveUsers(email, name, role, active, search, pageable);
return ResponseEntity.ok(userMapper.toResponsePage(users));
}

@GetMapping("/users/pending")
public ResponseEntity<Page<UserResponse>> getPendingUsers(
@PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
return ResponseEntity
.ok(userMapper.toResponsePage(userService.getUsersByRolePaginated(UserRole.PENDING, pageable)));
}

@GetMapping("/users/{userId}")
public ResponseEntity<UserResponse> getUser(@PathVariable String userId) {
return ResponseEntity.ok(userMapper.toResponse(userService.getUserById(userId)));
}

@PostMapping("/users/{userId}/approve")
public ResponseEntity<UserResponse> approveUser(@PathVariable String userId) {
AppUser approvedUser = userService.approvePendingUser(userId);
return ResponseEntity.ok(userMapper.toResponse(approvedUser));
}

@PatchMapping("/users/{userId}/role")
public ResponseEntity<UserResponse> updateUserRole(@PathVariable String userId,
@Valid @RequestBody UpdateUserRoleRequest request, @AuthenticationPrincipal CustomUserDetails currentUser) {

if (currentUser.getUser().getId().equals(userId)) {
throw new IllegalStateException("You cannot change your own role");
}

if (request.role() != UserRole.ADMIN) {
AppUser targetUser = userService.getUserById(userId);
if (targetUser.getRole() == UserRole.ADMIN) {
long adminCount = userService.getAllUsers().stream().filter(u -> u.getRole() == UserRole.ADMIN).count();
if (adminCount <= 1) {
throw new IllegalStateException("Cannot remove the last admin user");
}
}
}

AppUser updatedUser = userService.updateUserRole(userId, request.role());
return ResponseEntity.ok(userMapper.toResponse(updatedUser));
}

@PatchMapping("/users/{userId}/status")
public ResponseEntity<UserResponse> updateUserStatus(@PathVariable String userId,
@Valid @RequestBody UpdateUserStatusRequest request,
@AuthenticationPrincipal CustomUserDetails currentUser) {

if (currentUser.getUser().getId().equals(userId) && !request.active()) {
throw new IllegalStateException("You cannot deactivate your own account");
}

AppUser updatedUser = userService.updateUserActiveStatus(userId, request.active());
return ResponseEntity.ok(userMapper.toResponse(updatedUser));
}

@DeleteMapping("/users/{userId}")
public ResponseEntity<Void> deleteUser(@PathVariable String userId,
@AuthenticationPrincipal CustomUserDetails currentUser) {

if (currentUser.getUser().getId().equals(userId)) {
throw new IllegalStateException("You cannot delete your own account");
}

userService.deleteUser(userId);
return ResponseEntity.noContent().build();
}

@GetMapping("/stats")
public ResponseEntity<Map<String, Long>> getStats() {
Map<String, Long> stats = Map.of("totalUsers", (long) userService.getAllUsers().size(), "pendingUsers",
(long) userService.getUsersByRole(UserRole.PENDING).size(), "residents",
(long) userService.getUsersByRole(UserRole.RESIDENT).size(), "handlers",
(long) userService.getUsersByRole(UserRole.HANDLER).size(), "admins",
(long) userService.getUsersByRole(UserRole.ADMIN).size());
return ResponseEntity.ok(stats);
}

private Page<AppUser> resolveUsers(String email, String name, UserRole role, Boolean active, String search,
Pageable pageable) {
if (search != null && !search.trim().isEmpty()) {
return userService.searchUsers(search, pageable);
}

if (email != null || name != null || role != null || active != null) {
return userService.getUsersWithFilters(email, name, role, active, pageable);
}

return userService.getAllUsersPaginated(pageable);
}
}
Loading
Loading