diff --git a/frontend/src/app/api/auth/token/route.ts b/frontend/src/app/api/auth/token/route.ts
new file mode 100644
index 0000000..e38f177
--- /dev/null
+++ b/frontend/src/app/api/auth/token/route.ts
@@ -0,0 +1,8 @@
+import { cookies } from "next/headers";
+import { NextResponse } from "next/server";
+
+export async function GET() {
+ const cookieStore = await cookies();
+ const token = cookieStore.get("access_token")?.value;
+ return NextResponse.json({ token: token ?? null });
+}
diff --git a/frontend/src/components/auth/auth-guard.tsx b/frontend/src/components/auth/auth-guard.tsx
index 02b6ee4..ff4bcc7 100644
--- a/frontend/src/components/auth/auth-guard.tsx
+++ b/frontend/src/components/auth/auth-guard.tsx
@@ -1,8 +1,8 @@
"use client";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
-import { isAuthenticated } from "@/lib/auth";
+import { isAuthenticated, isInitialized, initializeAuth } from "@/lib/auth";
import { useAuthStore } from "@/stores/auth-store";
import { Skeleton } from "@/components/ui/skeleton";
@@ -10,10 +10,20 @@ export function AuthGuard({ children }: { children: React.ReactNode }) {
const router = useRouter();
const searchParams = useSearchParams();
const user = useAuthStore((s) => s.user);
- const authenticated = isAuthenticated() || !!user;
+ const [ready, setReady] = useState(isInitialized());
useEffect(() => {
- if (!authenticated) {
+ if (!isInitialized()) {
+ initializeAuth().then(() => setReady(true));
+ } else {
+ setReady(true);
+ }
+ }, []);
+
+ const authenticated = ready && (isAuthenticated() || !!user);
+
+ useEffect(() => {
+ if (ready && !authenticated) {
const redirect = searchParams?.get("redirect");
if (redirect) {
router.replace("/login?redirect=" + encodeURIComponent(redirect));
@@ -26,9 +36,9 @@ export function AuthGuard({ children }: { children: React.ReactNode }) {
}
}
}
- }, [authenticated, router, searchParams]);
+ }, [authenticated, ready, router, searchParams]);
- if (!authenticated) {
+ if (!ready || !authenticated) {
return (
diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts
index 2aff5c8..6dd9754 100644
--- a/frontend/src/lib/auth.ts
+++ b/frontend/src/lib/auth.ts
@@ -1,20 +1,44 @@
-const TOKEN_KEY = "atlas_access_token";
const USER_KEY = "atlas_user";
+let inMemoryToken: string | null = null;
+let _initialized = false;
+
export function getAccessToken(): string | null {
- if (typeof window === "undefined") return null;
- return localStorage.getItem(TOKEN_KEY);
+ return inMemoryToken;
}
export function setTokens(access: string): void {
- localStorage.setItem(TOKEN_KEY, access);
+ inMemoryToken = access;
}
export function clearAuth(): void {
- localStorage.removeItem(TOKEN_KEY);
+ inMemoryToken = null;
+ _initialized = false;
localStorage.removeItem(USER_KEY);
}
+export async function initializeAuth(): Promise
{
+ if (typeof window === "undefined") return false;
+ if (_initialized) return true;
+ try {
+ const res = await fetch("/api/auth/token");
+ const data = await res.json();
+ if (data.token) {
+ inMemoryToken = data.token;
+ _initialized = true;
+ return true;
+ }
+ } catch {
+ // network error — proceed as unauthenticated
+ }
+ _initialized = true;
+ return false;
+}
+
+export function isInitialized(): boolean {
+ return _initialized;
+}
+
export function setStoredUser(user: object): void {
localStorage.setItem(USER_KEY, JSON.stringify(user));
}
diff --git a/frontend/src/stores/auth-store.ts b/frontend/src/stores/auth-store.ts
index d8bdbce..452ff85 100644
--- a/frontend/src/stores/auth-store.ts
+++ b/frontend/src/stores/auth-store.ts
@@ -3,6 +3,7 @@ import { persist } from "zustand/middleware";
import type { User } from "@/types";
import {
clearAuth,
+ initializeAuth,
setStoredUser,
setTokens,
} from "@/lib/auth";
@@ -21,6 +22,7 @@ interface AuthState {
}) => Promise;
logout: () => void;
setUser: (user: User | null) => void;
+ initialize: () => Promise;
}
export const useAuthStore = create()(
@@ -29,6 +31,9 @@ export const useAuthStore = create()(
user: null,
isLoading: false,
setUser: (user) => set({ user }),
+ initialize: async () => {
+ await initializeAuth();
+ },
login: async (email, password) => {
set({ isLoading: true });
try {
@@ -76,6 +81,9 @@ export const useAuthStore = create()(
if (typeof window !== "undefined") window.location.href = "/login";
},
}),
- { name: "atlas-auth", partialize: (s) => ({ user: s.user }) }
+ {
+ name: "atlas-auth",
+ partialize: (s) => ({ user: s.user }),
+ }
)
);
diff --git a/services/leave-service/pom.xml b/services/leave-service/pom.xml
index 43193ab..c6443d9 100644
--- a/services/leave-service/pom.xml
+++ b/services/leave-service/pom.xml
@@ -31,6 +31,10 @@
org.springframework.boot
spring-boot-starter-amqp
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
org.flywaydb
flyway-core
diff --git a/services/leave-service/src/main/java/com/atlas/leave/LeaveController.java b/services/leave-service/src/main/java/com/atlas/leave/LeaveController.java
index 33335fa..9d87e9a 100644
--- a/services/leave-service/src/main/java/com/atlas/leave/LeaveController.java
+++ b/services/leave-service/src/main/java/com/atlas/leave/LeaveController.java
@@ -1,7 +1,11 @@
package com.atlas.leave;
+import com.atlas.leave.security.RequiresRole;
+import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.LocalDate;
import java.util.List;
@@ -18,12 +22,14 @@ public LeaveController(LeaveService service) {
this.service = service;
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@GetMapping
public ResponseEntity> getAllLeaveRequests(
@RequestHeader(value = "X-Tenant-Id", defaultValue = "default") String tenantId) {
return ResponseEntity.ok(service.getAllLeaveRequests(tenantId));
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@GetMapping("/employee/{employeeId}")
public ResponseEntity> getLeaveByEmployee(
@RequestHeader(value = "X-Tenant-Id", defaultValue = "default") String tenantId,
@@ -31,6 +37,7 @@ public ResponseEntity> getLeaveByEmployee(
return ResponseEntity.ok(service.getLeaveByEmployeeId(tenantId, employeeId));
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@PostMapping("/request")
public ResponseEntity> requestLeave(
@RequestBody Map request,
@@ -51,21 +58,17 @@ public ResponseEntity> requestLeave(
}
}
+ @RequiresRole({"admin", "hr", "manager"})
@PutMapping("/{id}/status")
public ResponseEntity> updateLeaveStatus(
@PathVariable Long id,
@RequestBody Map request,
- @RequestHeader(value = "X-User-Role", required = false) String userRole,
@RequestHeader(value = "X-Tenant-Id", defaultValue = "default") String tenantId) {
- // Ensure a role is provided; the service handles role-based authorization
- if (userRole == null || userRole.isBlank()) {
- return ResponseEntity.status(403)
- .body(Map.of("message", "Access denied: X-User-Role header is required"));
- }
-
try {
String status = request.get("status");
+ ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+ String userRole = attrs != null ? (String) attrs.getRequest().getAttribute("x-user-role") : "employee";
LeaveRecord record = service.updateLeaveStatus(tenantId, id, status, userRole);
return ResponseEntity.ok(record);
} catch (IllegalArgumentException e) {
diff --git a/services/leave-service/src/main/java/com/atlas/leave/security/RequiresRole.java b/services/leave-service/src/main/java/com/atlas/leave/security/RequiresRole.java
new file mode 100644
index 0000000..58d6c05
--- /dev/null
+++ b/services/leave-service/src/main/java/com/atlas/leave/security/RequiresRole.java
@@ -0,0 +1,12 @@
+package com.atlas.leave.security;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RequiresRole {
+ String[] value() default {};
+}
diff --git a/services/leave-service/src/main/java/com/atlas/leave/security/RoleAspect.java b/services/leave-service/src/main/java/com/atlas/leave/security/RoleAspect.java
new file mode 100644
index 0000000..a1de021
--- /dev/null
+++ b/services/leave-service/src/main/java/com/atlas/leave/security/RoleAspect.java
@@ -0,0 +1,45 @@
+package com.atlas.leave.security;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.util.Map;
+
+@Aspect
+@Component
+public class RoleAspect {
+
+ @Around("@annotation(requiresRole)")
+ public Object checkRole(ProceedingJoinPoint joinPoint, RequiresRole requiresRole) throws Throwable {
+ ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+ if (attrs == null) {
+ return ResponseEntity.status(500).body(Map.of("error", "No request context"));
+ }
+
+ HttpServletRequest request = attrs.getRequest();
+ String userRole = (String) request.getAttribute("x-user-role");
+
+ if (userRole == null || userRole.isBlank()) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access denied: no role assigned"));
+ }
+
+ String[] allowedRoles = requiresRole.value();
+ if (allowedRoles.length == 0) {
+ return joinPoint.proceed();
+ }
+
+ for (String role : allowedRoles) {
+ if (userRole.equalsIgnoreCase(role)) {
+ return joinPoint.proceed();
+ }
+ }
+
+ return ResponseEntity.status(403).body(Map.of("error", "Access denied: requires one of roles: " + String.join(", ", allowedRoles)));
+ }
+}
diff --git a/services/payroll-java-service/pom.xml b/services/payroll-java-service/pom.xml
index 9d9891a..4a8deab 100644
--- a/services/payroll-java-service/pom.xml
+++ b/services/payroll-java-service/pom.xml
@@ -38,6 +38,10 @@
org.springframework.boot
spring-boot-starter-validation
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
org.flywaydb
flyway-core
diff --git a/services/payroll-java-service/src/main/java/com/ems/payroll/PayrollController.java b/services/payroll-java-service/src/main/java/com/ems/payroll/PayrollController.java
index 292af7e..519f187 100644
--- a/services/payroll-java-service/src/main/java/com/ems/payroll/PayrollController.java
+++ b/services/payroll-java-service/src/main/java/com/ems/payroll/PayrollController.java
@@ -1,5 +1,6 @@
package com.ems.payroll;
+import com.ems.payroll.security.RequiresRole;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -19,11 +20,13 @@ public PayrollController(PayrollService service) {
this.service = service;
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@GetMapping
public ResponseEntity> getAllPayrolls(@RequestHeader(value = "X-Tenant-Id", defaultValue = "default") String tenantId) {
return ResponseEntity.ok(service.getAllPayrolls(tenantId));
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@GetMapping("/employee/{employeeId}")
public ResponseEntity> getPayrollsByEmployee(
@RequestHeader(value = "X-Tenant-Id", defaultValue = "default") String tenantId,
@@ -31,15 +34,11 @@ public ResponseEntity> getPayrollsByEmployee(
return ResponseEntity.ok(service.getPayrollsByEmployeeId(tenantId, employeeId));
}
+ @RequiresRole({"admin"})
@PostMapping("/run")
public ResponseEntity> runPayroll(
@RequestBody Map request,
- @RequestHeader(value = "X-User-Role", required = false) String userRole,
@RequestHeader(value = "X-Tenant-Id", defaultValue = "default") String tenantId) {
-
- if (userRole == null || !userRole.equalsIgnoreCase("admin")) {
- return ResponseEntity.status(403).body(Map.of("message", "Access denied: Requires administrator privileges"));
- }
try {
String employeeId = (String) request.get("employeeId");
diff --git a/services/payroll-java-service/src/main/java/com/ems/payroll/controller/PayrollEnterpriseController.java b/services/payroll-java-service/src/main/java/com/ems/payroll/controller/PayrollEnterpriseController.java
index d3e4de1..79c64ea 100644
--- a/services/payroll-java-service/src/main/java/com/ems/payroll/controller/PayrollEnterpriseController.java
+++ b/services/payroll-java-service/src/main/java/com/ems/payroll/controller/PayrollEnterpriseController.java
@@ -1,6 +1,7 @@
package com.ems.payroll.controller;
import com.ems.payroll.model.*;
+import com.ems.payroll.security.RequiresRole;
import com.ems.payroll.service.PayrollEnterpriseService;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
@@ -24,6 +25,7 @@ public PayrollEnterpriseController(PayrollEnterpriseService service) {
// ============================================================
// Dashboard
// ============================================================
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@GetMapping("/dashboard")
public ResponseEntity
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
org.postgresql
postgresql
diff --git a/services/performance-service/src/main/java/com/atlas/performance/controller/AnalyticsController.java b/services/performance-service/src/main/java/com/atlas/performance/controller/AnalyticsController.java
index 7ecc522..303e8ae 100644
--- a/services/performance-service/src/main/java/com/atlas/performance/controller/AnalyticsController.java
+++ b/services/performance-service/src/main/java/com/atlas/performance/controller/AnalyticsController.java
@@ -1,5 +1,6 @@
package com.atlas.performance.controller;
+import com.atlas.performance.security.RequiresRole;
import com.atlas.performance.service.PerformanceService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -17,24 +18,28 @@ public AnalyticsController(PerformanceService service) {
this.service = service;
}
+ @RequiresRole({"admin", "hr", "manager"})
@GetMapping("/overview")
public ResponseEntity> getOverview(
@RequestHeader(value = "X-Tenant-Id", defaultValue = "default") String tenantId) {
return ResponseEntity.ok(service.getAnalyticsOverview(tenantId));
}
+ @RequiresRole({"admin", "hr", "manager"})
@GetMapping("/department-ratings")
public ResponseEntity> getDepartmentRatings(
@RequestHeader(value = "X-Tenant-Id", defaultValue = "default") String tenantId) {
return ResponseEntity.ok(service.getDepartmentRatings(tenantId));
}
+ @RequiresRole({"admin", "hr", "manager"})
@GetMapping("/goal-completion")
public ResponseEntity> getGoalCompletion(
@RequestHeader(value = "X-Tenant-Id", defaultValue = "default") String tenantId) {
return ResponseEntity.ok(service.getGoalCompletionRates(tenantId));
}
+ @RequiresRole({"admin", "hr", "manager"})
@GetMapping("/succession-readiness")
public ResponseEntity> getSuccessionReadiness(
@RequestHeader(value = "X-Tenant-Id", defaultValue = "default") String tenantId) {
diff --git a/services/performance-service/src/main/java/com/atlas/performance/controller/FeedbackController.java b/services/performance-service/src/main/java/com/atlas/performance/controller/FeedbackController.java
index cc374ac..a6f1f31 100644
--- a/services/performance-service/src/main/java/com/atlas/performance/controller/FeedbackController.java
+++ b/services/performance-service/src/main/java/com/atlas/performance/controller/FeedbackController.java
@@ -1,6 +1,7 @@
package com.atlas.performance.controller;
import com.atlas.performance.model.Feedback360;
+import com.atlas.performance.security.RequiresRole;
import com.atlas.performance.service.PerformanceService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -19,6 +20,7 @@ public FeedbackController(PerformanceService service) {
this.service = service;
}
+ @RequiresRole({"admin", "hr", "manager"})
@GetMapping
public ResponseEntity> listFeedback(
@RequestHeader(value = "X-Tenant-Id", defaultValue = "default") String tenantId,
@@ -27,6 +29,7 @@ public ResponseEntity> listFeedback(
return ResponseEntity.ok(service.getFeedbackList(tenantId, employeeId, reviewerId));
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@PostMapping
public ResponseEntity> submitFeedback(
@RequestBody Feedback360 feedback,
@@ -38,6 +41,7 @@ public ResponseEntity> submitFeedback(
}
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@GetMapping("/{id}")
public ResponseEntity> getFeedback(
@PathVariable String id,
@@ -49,6 +53,7 @@ public ResponseEntity> getFeedback(
}
}
+ @RequiresRole({"admin", "hr", "manager"})
@GetMapping("/employee/{employeeId}/summary")
public ResponseEntity> getFeedbackSummary(
@PathVariable String employeeId,
diff --git a/services/performance-service/src/main/java/com/atlas/performance/controller/GoalController.java b/services/performance-service/src/main/java/com/atlas/performance/controller/GoalController.java
index 2f1b26a..fc81a0b 100644
--- a/services/performance-service/src/main/java/com/atlas/performance/controller/GoalController.java
+++ b/services/performance-service/src/main/java/com/atlas/performance/controller/GoalController.java
@@ -1,6 +1,7 @@
package com.atlas.performance.controller;
import com.atlas.performance.model.Goal;
+import com.atlas.performance.security.RequiresRole;
import com.atlas.performance.service.PerformanceService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -20,6 +21,7 @@ public GoalController(PerformanceService service) {
this.service = service;
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@GetMapping
public ResponseEntity> listGoals(
@RequestHeader(value = "X-Tenant-Id", defaultValue = "default") String tenantId,
@@ -29,6 +31,7 @@ public ResponseEntity> listGoals(
return ResponseEntity.ok(service.getGoals(tenantId, employeeId, status, category));
}
+ @RequiresRole({"admin", "hr", "manager"})
@PostMapping
public ResponseEntity> createGoal(
@RequestBody Goal goal,
@@ -40,6 +43,7 @@ public ResponseEntity> createGoal(
}
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@GetMapping("/{id}")
public ResponseEntity> getGoal(
@PathVariable String id,
@@ -51,6 +55,7 @@ public ResponseEntity> getGoal(
}
}
+ @RequiresRole({"admin", "hr", "manager"})
@PutMapping("/{id}")
public ResponseEntity> updateGoal(
@PathVariable String id,
@@ -63,6 +68,7 @@ public ResponseEntity> updateGoal(
}
}
+ @RequiresRole({"admin", "hr"})
@DeleteMapping("/{id}")
public ResponseEntity> deleteGoal(
@PathVariable String id,
@@ -75,6 +81,7 @@ public ResponseEntity> deleteGoal(
}
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@PutMapping("/{id}/progress")
public ResponseEntity> updateProgress(
@PathVariable String id,
@@ -90,6 +97,7 @@ public ResponseEntity> updateProgress(
}
}
+ @RequiresRole({"admin", "hr", "manager"})
@PutMapping("/{id}/status")
public ResponseEntity> updateStatus(
@PathVariable String id,
diff --git a/services/performance-service/src/main/java/com/atlas/performance/controller/PerformanceReviewController.java b/services/performance-service/src/main/java/com/atlas/performance/controller/PerformanceReviewController.java
index 5a5dd74..bd9b40d 100644
--- a/services/performance-service/src/main/java/com/atlas/performance/controller/PerformanceReviewController.java
+++ b/services/performance-service/src/main/java/com/atlas/performance/controller/PerformanceReviewController.java
@@ -1,6 +1,7 @@
package com.atlas.performance.controller;
import com.atlas.performance.model.PerformanceReview;
+import com.atlas.performance.security.RequiresRole;
import com.atlas.performance.service.PerformanceService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -19,6 +20,7 @@ public PerformanceReviewController(PerformanceService service) {
this.service = service;
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@GetMapping
public ResponseEntity> listReviews(
@RequestHeader(value = "X-Tenant-Id", defaultValue = "default") String tenantId,
@@ -29,6 +31,7 @@ public ResponseEntity> listReviews(
return ResponseEntity.ok(service.getReviews(tenantId, employeeId, reviewerId, status, cycle));
}
+ @RequiresRole({"admin", "hr", "manager"})
@PostMapping
public ResponseEntity> createReview(
@RequestBody PerformanceReview review,
@@ -40,6 +43,7 @@ public ResponseEntity> createReview(
}
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@GetMapping("/{id}")
public ResponseEntity> getReview(
@PathVariable String id,
@@ -51,6 +55,7 @@ public ResponseEntity> getReview(
}
}
+ @RequiresRole({"admin", "hr", "manager"})
@PutMapping("/{id}")
public ResponseEntity> updateReview(
@PathVariable String id,
@@ -63,6 +68,7 @@ public ResponseEntity> updateReview(
}
}
+ @RequiresRole({"admin", "hr", "manager"})
@PutMapping("/{id}/submit")
public ResponseEntity> submitReview(
@PathVariable String id,
@@ -74,6 +80,7 @@ public ResponseEntity> submitReview(
}
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@PutMapping("/{id}/acknowledge")
public ResponseEntity> acknowledgeReview(
@PathVariable String id,
@@ -85,6 +92,7 @@ public ResponseEntity> acknowledgeReview(
}
}
+ @RequiresRole({"admin", "hr", "manager"})
@GetMapping("/cycle/{cycle}")
public ResponseEntity> getReviewsByCycle(
@PathVariable String cycle,
@@ -92,6 +100,7 @@ public ResponseEntity> getReviewsByCycle(
return ResponseEntity.ok(service.getReviewsByCycle(tenantId, cycle));
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@GetMapping("/employee/{employeeId}")
public ResponseEntity> getReviewHistory(
@PathVariable String employeeId,
diff --git a/services/performance-service/src/main/java/com/atlas/performance/controller/RecognitionController.java b/services/performance-service/src/main/java/com/atlas/performance/controller/RecognitionController.java
index a5170ba..2f960c2 100644
--- a/services/performance-service/src/main/java/com/atlas/performance/controller/RecognitionController.java
+++ b/services/performance-service/src/main/java/com/atlas/performance/controller/RecognitionController.java
@@ -1,6 +1,7 @@
package com.atlas.performance.controller;
import com.atlas.performance.model.Recognition;
+import com.atlas.performance.security.RequiresRole;
import com.atlas.performance.service.PerformanceService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -19,6 +20,7 @@ public RecognitionController(PerformanceService service) {
this.service = service;
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@GetMapping
public ResponseEntity> listRecognitions(
@RequestHeader(value = "X-Tenant-Id", defaultValue = "default") String tenantId,
@@ -27,6 +29,7 @@ public ResponseEntity> listRecognitions(
return ResponseEntity.ok(service.getRecognitions(tenantId, employeeId, category));
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@PostMapping
public ResponseEntity> giveRecognition(
@RequestBody Recognition recognition,
@@ -38,6 +41,7 @@ public ResponseEntity> giveRecognition(
}
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@GetMapping("/wall")
public ResponseEntity> getRecognitionWall(
@RequestHeader(value = "X-Tenant-Id", defaultValue = "default") String tenantId) {
diff --git a/services/performance-service/src/main/java/com/atlas/performance/controller/SuccessionController.java b/services/performance-service/src/main/java/com/atlas/performance/controller/SuccessionController.java
index 4aa7838..a57541e 100644
--- a/services/performance-service/src/main/java/com/atlas/performance/controller/SuccessionController.java
+++ b/services/performance-service/src/main/java/com/atlas/performance/controller/SuccessionController.java
@@ -2,6 +2,7 @@
import com.atlas.performance.model.SuccessionCandidate;
import com.atlas.performance.model.SuccessionPlan;
+import com.atlas.performance.security.RequiresRole;
import com.atlas.performance.service.PerformanceService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -20,6 +21,7 @@ public SuccessionController(PerformanceService service) {
this.service = service;
}
+ @RequiresRole({"admin", "hr", "manager"})
@GetMapping("/plans")
public ResponseEntity> listPlans(
@RequestHeader(value = "X-Tenant-Id", defaultValue = "default") String tenantId,
@@ -28,6 +30,7 @@ public ResponseEntity> listPlans(
return ResponseEntity.ok(service.getSuccessionPlans(tenantId, department, status));
}
+ @RequiresRole({"admin", "hr"})
@PostMapping("/plans")
public ResponseEntity> createPlan(
@RequestBody SuccessionPlan plan,
@@ -39,6 +42,7 @@ public ResponseEntity> createPlan(
}
}
+ @RequiresRole({"admin", "hr", "manager"})
@GetMapping("/plans/{id}")
public ResponseEntity> getPlan(
@PathVariable String id,
@@ -50,6 +54,7 @@ public ResponseEntity> getPlan(
}
}
+ @RequiresRole({"admin", "hr"})
@PutMapping("/plans/{id}")
public ResponseEntity> updatePlan(
@PathVariable String id,
@@ -62,6 +67,7 @@ public ResponseEntity> updatePlan(
}
}
+ @RequiresRole({"admin", "hr"})
@DeleteMapping("/plans/{id}")
public ResponseEntity> deletePlan(
@PathVariable String id,
@@ -74,6 +80,7 @@ public ResponseEntity> deletePlan(
}
}
+ @RequiresRole({"admin", "hr"})
@PostMapping("/candidates")
public ResponseEntity> addCandidate(
@RequestBody SuccessionCandidate candidate,
@@ -85,6 +92,7 @@ public ResponseEntity> addCandidate(
}
}
+ @RequiresRole({"admin", "hr"})
@PutMapping("/candidates/{id}")
public ResponseEntity> updateCandidate(
@PathVariable String id,
@@ -97,6 +105,7 @@ public ResponseEntity> updateCandidate(
}
}
+ @RequiresRole({"admin", "hr"})
@DeleteMapping("/candidates/{id}")
public ResponseEntity> removeCandidate(
@PathVariable String id,
@@ -109,6 +118,7 @@ public ResponseEntity> removeCandidate(
}
}
+ @RequiresRole({"admin", "hr", "manager", "employee"})
@GetMapping("/readiness/{employeeId}")
public ResponseEntity> getEmployeeReadiness(
@PathVariable String employeeId,
diff --git a/services/performance-service/src/main/java/com/atlas/performance/security/RequiresRole.java b/services/performance-service/src/main/java/com/atlas/performance/security/RequiresRole.java
new file mode 100644
index 0000000..c6a7537
--- /dev/null
+++ b/services/performance-service/src/main/java/com/atlas/performance/security/RequiresRole.java
@@ -0,0 +1,12 @@
+package com.atlas.performance.security;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RequiresRole {
+ String[] value() default {};
+}
diff --git a/services/performance-service/src/main/java/com/atlas/performance/security/RoleAspect.java b/services/performance-service/src/main/java/com/atlas/performance/security/RoleAspect.java
new file mode 100644
index 0000000..e3f0b17
--- /dev/null
+++ b/services/performance-service/src/main/java/com/atlas/performance/security/RoleAspect.java
@@ -0,0 +1,45 @@
+package com.atlas.performance.security;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.util.Map;
+
+@Aspect
+@Component
+public class RoleAspect {
+
+ @Around("@annotation(requiresRole)")
+ public Object checkRole(ProceedingJoinPoint joinPoint, RequiresRole requiresRole) throws Throwable {
+ ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+ if (attrs == null) {
+ return ResponseEntity.status(500).body(Map.of("error", "No request context"));
+ }
+
+ HttpServletRequest request = attrs.getRequest();
+ String userRole = (String) request.getAttribute("x-user-role");
+
+ if (userRole == null || userRole.isBlank()) {
+ return ResponseEntity.status(403).body(Map.of("error", "Access denied: no role assigned"));
+ }
+
+ String[] allowedRoles = requiresRole.value();
+ if (allowedRoles.length == 0) {
+ return joinPoint.proceed();
+ }
+
+ for (String role : allowedRoles) {
+ if (userRole.equalsIgnoreCase(role)) {
+ return joinPoint.proceed();
+ }
+ }
+
+ return ResponseEntity.status(403).body(Map.of("error", "Access denied: requires one of roles: " + String.join(", ", allowedRoles)));
+ }
+}