feat(auth): enforce strict branch scoping for overtime requests, attendance regularizations, and 13th month pay

- Restrict overtime applications query, submission, approval, and attendance details to assigned branch
- Restrict attendance regularization listing, statistics, and dropdowns to assigned branch
- Restrict 13th month pay batch generation, historical runs, entry updates, approvals, and CSV exports to assigned branch
- Add feature tests in BranchScopedAccessTest for overtime and 13th month isolation
This commit is contained in:
2026-09-10 11:22:15 +08:00
parent 7f467600e9
commit 33f776c6b2
4 changed files with 251 additions and 20 deletions

View File

@@ -24,6 +24,14 @@ class OvertimeApplicationController extends Controller
if (!in_array(Auth::user()->type, ['admin', 'hr', 'company'])) {
abort(403);
}
$scopedBranchId = authBranchId();
if ($scopedBranchId) {
$targetEmp = \App\Models\Employee::where('user_id', $userId)->first();
if ($targetEmp && (int)$targetEmp->branch_id !== (int)$scopedBranchId) {
abort(403, 'Unauthorized to view attendance details for an employee outside your assigned branch.');
}
}
}
$record = AttendanceRecord::with('employee.user', 'employee.shift')
@@ -85,19 +93,30 @@ class OvertimeApplicationController extends Controller
public function index()
{
$user = Auth::user();
$scopedBranchId = authBranchId();
$query = OvertimeApplication::with('user.employee');
// If not admin/hr, only show own requests
if (!in_array($user->type, ['admin', 'hr', 'company'])) {
$query->where('user_id', $user->id);
} elseif ($scopedBranchId) {
$query->whereHas('user.employee', function($q) use ($scopedBranchId) {
$q->where('branch_id', $scopedBranchId);
});
}
$applications = $query->orderBy('date', 'desc')->get();
$users = [];
if (in_array($user->type, ['admin', 'hr', 'company'])) {
$users = \App\Models\User::where('type', 'employee')->select('id', 'name')->get();
$usersQuery = \App\Models\User::where('type', 'employee');
if ($scopedBranchId) {
$usersQuery->whereHas('employee', function($q) use ($scopedBranchId) {
$q->where('branch_id', $scopedBranchId);
});
}
$users = $usersQuery->select('id', 'name')->get();
}
return Inertia::render('hr/overtime/index', [
@@ -117,10 +136,17 @@ class OvertimeApplicationController extends Controller
]);
$userId = Auth::id();
$scopedBranchId = authBranchId();
// If an admin/hr specifies a user_id, use it.
if ($request->has('user_id') && $request->user_id != Auth::id()) {
if (in_array(Auth::user()->type, ['admin', 'hr', 'company'])) {
if ($scopedBranchId) {
$targetEmp = \App\Models\Employee::where('user_id', $request->user_id)->first();
if ($targetEmp && (int)$targetEmp->branch_id !== (int)$scopedBranchId) {
abort(403, 'Unauthorized to apply overtime for an employee outside your assigned branch.');
}
}
$userId = $request->user_id;
} else {
abort(403, 'Unauthorized to apply overtime for another employee.');
@@ -160,6 +186,14 @@ class OvertimeApplicationController extends Controller
abort(403);
}
$scopedBranchId = authBranchId();
if ($scopedBranchId) {
$applicantEmp = $overtimeApplication->user?->employee;
if ($applicantEmp && (int)$applicantEmp->branch_id !== (int)$scopedBranchId) {
abort(403, 'Unauthorized to update overtime status for an employee outside your assigned branch.');
}
}
$request->validate([
'status' => 'required|in:approved,rejected',
'approved_hours' => 'nullable|required_if:status,approved|numeric|min:0',