diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
new file mode 100644
index 0000000..3271bbf
--- /dev/null
+++ b/.github/workflows/cd.yml
@@ -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
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..b998d4f
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -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/
\ No newline at end of file
diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml
new file mode 100644
index 0000000..b630d91
--- /dev/null
+++ b/.github/workflows/pr-checks.yml
@@ -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>
]*>(\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);
+ }
\ No newline at end of file
diff --git a/src/main/java/org/example/team6backend/Team6BackendApplication.java b/src/main/java/org/example/team6backend/Team6BackendApplication.java
index 953000c..84b2e4f 100644
--- a/src/main/java/org/example/team6backend/Team6BackendApplication.java
+++ b/src/main/java/org/example/team6backend/Team6BackendApplication.java
@@ -6,7 +6,7 @@
@SpringBootApplication
public class Team6BackendApplication {
- public static void main(String[] args) {
- SpringApplication.run(Team6BackendApplication.class, args);
- }
-}
\ No newline at end of file
+ public static void main(String[] args) {
+ SpringApplication.run(Team6BackendApplication.class, args);
+ }
+}
diff --git a/src/main/java/org/example/team6backend/admin/AdminController.java b/src/main/java/org/example/team6backend/admin/AdminController.java
index 3160cc7..e0b7979 100644
--- a/src/main/java/org/example/team6backend/admin/AdminController.java
+++ b/src/main/java/org/example/team6backend/admin/AdminController.java
@@ -29,122 +29,104 @@
@Slf4j
public class AdminController {
- private final UserService userService;
- private final UserMapper userMapper;
-
- @GetMapping("/users")
- public ResponseEntity> 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 users = resolveUsers(email, name, role, active, search, pageable);
- return ResponseEntity.ok(userMapper.toResponsePage(users));
- }
-
- @GetMapping("/users/pending")
- public ResponseEntity> 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 getUser(@PathVariable String userId) {
- return ResponseEntity.ok(userMapper.toResponse(userService.getUserById(userId)));
- }
-
- @PostMapping("/users/{userId}/approve")
- public ResponseEntity approveUser(@PathVariable String userId) {
- AppUser approvedUser = userService.approvePendingUser(userId);
- return ResponseEntity.ok(userMapper.toResponse(approvedUser));
- }
-
- @PatchMapping("/users/{userId}/role")
- public ResponseEntity 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 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 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 |