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> getDashboard( @RequestHeader("X-Tenant-Id") String tenantId) { @@ -33,12 +35,14 @@ public ResponseEntity> getDashboard( // ============================================================ // Multi-country payroll // ============================================================ + @RequiresRole({"admin", "hr", "manager"}) @GetMapping("/payrolls") public ResponseEntity> getPayrolls( @RequestHeader("X-Tenant-Id") String tenantId) { return ResponseEntity.ok(service.getAllPayrolls(tenantId)); } + @RequiresRole({"admin", "hr", "manager"}) @GetMapping("/payrolls/employee/{employeeId}") public ResponseEntity> getPayrollsByEmployee( @RequestHeader("X-Tenant-Id") String tenantId, @@ -46,6 +50,7 @@ public ResponseEntity> getPayrollsByEmployee( return ResponseEntity.ok(service.getPayrollsByEmployee(tenantId, employeeId)); } + @RequiresRole({"admin", "hr", "manager"}) @GetMapping("/payrolls/period/{period}") public ResponseEntity> getPayrollsByPeriod( @RequestHeader("X-Tenant-Id") String tenantId, @@ -53,6 +58,7 @@ public ResponseEntity> getPayrollsByPeriod( return ResponseEntity.ok(service.getPayrollsByPeriod(tenantId, period)); } + @RequiresRole({"admin"}) @PostMapping("/payrolls/run") public ResponseEntity runPayroll( @RequestHeader("X-Tenant-Id") String tenantId, @@ -76,24 +82,28 @@ public ResponseEntity runPayroll( // ============================================================ // Tax config // ============================================================ + @RequiresRole({"admin", "hr"}) @GetMapping("/tax-configs") public ResponseEntity> getTaxConfigs( @RequestHeader("X-Tenant-Id") String tenantId) { return ResponseEntity.ok(service.getTaxConfigs(tenantId)); } + @RequiresRole({"admin"}) @PostMapping("/tax-configs") public ResponseEntity saveTaxConfig( @RequestBody CountryTaxConfig config) { return ResponseEntity.ok(service.saveTaxConfig(config)); } + @RequiresRole({"admin", "hr"}) @GetMapping("/tax-brackets") public ResponseEntity> getTaxBrackets( @RequestHeader("X-Tenant-Id") String tenantId) { return ResponseEntity.ok(service.getTaxBrackets(tenantId)); } + @RequiresRole({"admin"}) @PostMapping("/tax-brackets") public ResponseEntity saveTaxBracket( @RequestBody TaxBracket bracket) { @@ -103,6 +113,7 @@ public ResponseEntity saveTaxBracket( // ============================================================ // Tax simulations // ============================================================ + @RequiresRole({"admin", "hr"}) @PostMapping("/tax/simulate") public ResponseEntity> simulateTax( @RequestHeader("X-Tenant-Id") String tenantId, @@ -112,6 +123,7 @@ public ResponseEntity> simulateTax( return ResponseEntity.ok(service.simulateTax(tenantId, country, grossSalary)); } + @RequiresRole({"admin", "hr"}) @PostMapping("/tax/compare") public ResponseEntity>> compareTax( @RequestHeader("X-Tenant-Id") String tenantId, @@ -125,6 +137,7 @@ public ResponseEntity>> compareTax( // ============================================================ // Payroll forecasting // ============================================================ + @RequiresRole({"admin", "hr"}) @PostMapping("/forecasts/generate") public ResponseEntity generateForecast( @RequestHeader("X-Tenant-Id") String tenantId, @@ -133,6 +146,7 @@ public ResponseEntity generateForecast( return ResponseEntity.ok(service.generateForecast(tenantId, period)); } + @RequiresRole({"admin", "hr"}) @GetMapping("/forecasts") public ResponseEntity> getForecasts( @RequestHeader("X-Tenant-Id") String tenantId) { @@ -142,6 +156,7 @@ public ResponseEntity> getForecasts( // ============================================================ // Payroll auditing // ============================================================ + @RequiresRole({"admin", "hr"}) @GetMapping("/audit-logs") public ResponseEntity> getAuditLogs( @RequestHeader("X-Tenant-Id") String tenantId) { @@ -151,12 +166,14 @@ public ResponseEntity> getAuditLogs( // ============================================================ // Payslip generation // ============================================================ + @RequiresRole({"admin", "hr", "manager", "employee"}) @GetMapping("/payslips") public ResponseEntity> getPayslips( @RequestHeader("X-Tenant-Id") String tenantId) { return ResponseEntity.ok(service.getPayslips(tenantId)); } + @RequiresRole({"admin", "hr", "manager", "employee"}) @GetMapping("/payslips/employee/{employeeId}") public ResponseEntity> getEmployeePayslips( @RequestHeader("X-Tenant-Id") String tenantId, @@ -167,6 +184,7 @@ public ResponseEntity> getEmployeePayslips( // ============================================================ // Bank integration // ============================================================ + @RequiresRole({"admin"}) @PostMapping("/bank/transactions") public ResponseEntity createBankTransaction( @RequestHeader("X-Tenant-Id") String tenantId, @@ -182,12 +200,14 @@ public ResponseEntity createBankTransaction( return ResponseEntity.ok(tx); } + @RequiresRole({"admin", "hr", "manager"}) @GetMapping("/bank/transactions") public ResponseEntity> getBankTransactions( @RequestHeader("X-Tenant-Id") String tenantId) { return ResponseEntity.ok(service.getBankTransactions(tenantId)); } + @RequiresRole({"admin"}) @PostMapping("/bank/transactions/{id}/process") public ResponseEntity processBankTransaction(@PathVariable Long id) { return ResponseEntity.ok(service.processBankTransaction(id)); @@ -196,12 +216,14 @@ public ResponseEntity processBankTransaction(@PathVariable Long // ============================================================ // Expense reimbursements // ============================================================ + @RequiresRole({"admin", "hr", "manager"}) @GetMapping("/expenses") public ResponseEntity> getExpenses( @RequestHeader("X-Tenant-Id") String tenantId) { return ResponseEntity.ok(service.getExpenses(tenantId)); } + @RequiresRole({"admin", "hr", "manager"}) @GetMapping("/expenses/employee/{employeeId}") public ResponseEntity> getExpensesByEmployee( @RequestHeader("X-Tenant-Id") String tenantId, @@ -209,6 +231,7 @@ public ResponseEntity> getExpensesByEmployee( return ResponseEntity.ok(service.getExpensesByEmployee(tenantId, employeeId)); } + @RequiresRole({"admin", "hr", "manager", "employee"}) @PostMapping("/expenses") public ResponseEntity submitExpense( @RequestHeader("X-Tenant-Id") String tenantId, @@ -223,6 +246,7 @@ public ResponseEntity submitExpense( return ResponseEntity.ok(expense); } + @RequiresRole({"admin", "hr"}) @PostMapping("/expenses/{id}/approve") public ResponseEntity approveExpense( @PathVariable Long id, @@ -230,6 +254,7 @@ public ResponseEntity approveExpense( return ResponseEntity.ok(service.approveExpense(id, req.getOrDefault("approvedBy", "admin"))); } + @RequiresRole({"admin", "hr"}) @PostMapping("/expenses/{id}/reject") public ResponseEntity rejectExpense( @PathVariable Long id, @@ -240,12 +265,14 @@ public ResponseEntity rejectExpense( // ============================================================ // Benefits administration // ============================================================ + @RequiresRole({"admin", "hr", "manager", "employee"}) @GetMapping("/benefit-plans") public ResponseEntity> getBenefitPlans( @RequestHeader("X-Tenant-Id") String tenantId) { return ResponseEntity.ok(service.getBenefitPlans(tenantId)); } + @RequiresRole({"admin", "hr"}) @PostMapping("/benefit-plans") public ResponseEntity createBenefitPlan( @RequestHeader("X-Tenant-Id") String tenantId, @@ -261,12 +288,14 @@ public ResponseEntity createBenefitPlan( return ResponseEntity.ok(plan); } + @RequiresRole({"admin", "hr", "manager", "employee"}) @GetMapping("/benefit-enrollments") public ResponseEntity> getBenefitEnrollments( @RequestHeader("X-Tenant-Id") String tenantId) { return ResponseEntity.ok(service.getBenefitEnrollments(tenantId)); } + @RequiresRole({"admin", "hr", "employee"}) @PostMapping("/benefit-enrollments") public ResponseEntity enrollBenefit( @RequestHeader("X-Tenant-Id") String tenantId, @@ -280,12 +309,14 @@ public ResponseEntity enrollBenefit( // ============================================================ // Compensation planning // ============================================================ + @RequiresRole({"admin", "hr", "manager"}) @GetMapping("/compensation-plans") public ResponseEntity> getCompensationPlans( @RequestHeader("X-Tenant-Id") String tenantId) { return ResponseEntity.ok(service.getCompensationPlans(tenantId)); } + @RequiresRole({"admin", "hr"}) @PostMapping("/compensation-plans") public ResponseEntity createCompensationPlan( @RequestHeader("X-Tenant-Id") String tenantId, @@ -304,12 +335,14 @@ public ResponseEntity createCompensationPlan( // ============================================================ // Bonus management // ============================================================ + @RequiresRole({"admin", "hr", "manager"}) @GetMapping("/bonuses") public ResponseEntity> getBonuses( @RequestHeader("X-Tenant-Id") String tenantId) { return ResponseEntity.ok(service.getBonuses(tenantId)); } + @RequiresRole({"admin", "hr"}) @PostMapping("/bonuses") public ResponseEntity createBonus( @RequestHeader("X-Tenant-Id") String tenantId, @@ -323,6 +356,7 @@ public ResponseEntity createBonus( return ResponseEntity.ok(bonus); } + @RequiresRole({"admin"}) @PostMapping("/bonuses/{id}/approve") public ResponseEntity approveBonus( @PathVariable Long id, @@ -333,12 +367,14 @@ public ResponseEntity approveBonus( // ============================================================ // Equity management // ============================================================ + @RequiresRole({"admin", "hr", "manager"}) @GetMapping("/equity") public ResponseEntity> getEquity( @RequestHeader("X-Tenant-Id") String tenantId) { return ResponseEntity.ok(service.getEquityGrants(tenantId)); } + @RequiresRole({"admin"}) @PostMapping("/equity") public ResponseEntity createEquityGrant( @RequestHeader("X-Tenant-Id") String tenantId, @@ -354,6 +390,7 @@ public ResponseEntity createEquityGrant( return ResponseEntity.ok(grant); } + @RequiresRole({"admin", "hr", "employee"}) @GetMapping("/equity/employee/{employeeId}") public ResponseEntity> getEmployeeEquity( @RequestHeader("X-Tenant-Id") String tenantId, @@ -364,12 +401,14 @@ public ResponseEntity> getEmployeeEquity( // ============================================================ // Salary benchmarking // ============================================================ + @RequiresRole({"admin", "hr", "manager"}) @GetMapping("/benchmarks") public ResponseEntity> getBenchmarks( @RequestHeader("X-Tenant-Id") String tenantId) { return ResponseEntity.ok(service.getBenchmarks(tenantId)); } + @RequiresRole({"admin", "hr"}) @PostMapping("/benchmarks") public ResponseEntity addBenchmark( @RequestHeader("X-Tenant-Id") String tenantId, @@ -389,6 +428,7 @@ public ResponseEntity addBenchmark( return ResponseEntity.ok(benchmark); } + @RequiresRole({"admin", "hr", "manager"}) @PostMapping("/benchmarks/compare") public ResponseEntity> compareBenchmark( @RequestHeader("X-Tenant-Id") String tenantId, @@ -404,12 +444,14 @@ public ResponseEntity> compareBenchmark( // ============================================================ // Compliance reports // ============================================================ + @RequiresRole({"admin", "hr", "manager"}) @GetMapping("/compliance-reports") public ResponseEntity> getComplianceReports( @RequestHeader("X-Tenant-Id") String tenantId) { return ResponseEntity.ok(service.getComplianceReports(tenantId)); } + @RequiresRole({"admin"}) @PostMapping("/compliance-reports/generate") public ResponseEntity generateComplianceReport( @RequestHeader("X-Tenant-Id") String tenantId, @@ -424,12 +466,14 @@ public ResponseEntity generateComplianceReport( // ============================================================ // Anomaly detection // ============================================================ + @RequiresRole({"admin", "hr"}) @GetMapping("/anomalies") public ResponseEntity> getAnomalies( @RequestHeader("X-Tenant-Id") String tenantId) { return ResponseEntity.ok(service.getAnomalies(tenantId)); } + @RequiresRole({"admin"}) @PostMapping("/anomalies/{id}/resolve") public ResponseEntity resolveAnomaly( @PathVariable Long id, diff --git a/services/payroll-java-service/src/main/java/com/ems/payroll/security/RequiresRole.java b/services/payroll-java-service/src/main/java/com/ems/payroll/security/RequiresRole.java new file mode 100644 index 0000000..0decfb4 --- /dev/null +++ b/services/payroll-java-service/src/main/java/com/ems/payroll/security/RequiresRole.java @@ -0,0 +1,12 @@ +package com.ems.payroll.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/payroll-java-service/src/main/java/com/ems/payroll/security/RoleAspect.java b/services/payroll-java-service/src/main/java/com/ems/payroll/security/RoleAspect.java new file mode 100644 index 0000000..6b466fd --- /dev/null +++ b/services/payroll-java-service/src/main/java/com/ems/payroll/security/RoleAspect.java @@ -0,0 +1,45 @@ +package com.ems.payroll.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/performance-service/pom.xml b/services/performance-service/pom.xml index bf1c9a0..f200805 100644 --- a/services/performance-service/pom.xml +++ b/services/performance-service/pom.xml @@ -29,6 +29,10 @@ org.springframework.boot spring-boot-starter-validation + + 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))); + } +}