feat(auth): enforce strict branch scoping for branch-assigned HR and staff accounts

- Introduce authBranchId() helper to resolve assigned branch while allowing company admin/superadmin cross-branch oversight
- Restrict branch, department, and employee listings and mutations to assigned branch for branch-scoped accounts
- Scope attendance records, biometric logs, leave applications/balances, payslips, shifts, and announcements by assigned branch
- Add comprehensive automated feature tests in BranchScopedAccessTest to prevent branch cross-visibility regression
This commit is contained in:
2026-09-10 11:14:24 +08:00
parent 4b350c4628
commit 7f467600e9
18 changed files with 937 additions and 144 deletions

View File

@@ -12,8 +12,14 @@ class ShiftController extends Controller
public function index(Request $request)
{
if (Auth::user()->can('manage-shifts')) {
$query = Shift::with(['creator'])->where(function ($q) {
if (Auth::user()->can('manage-any-shifts')) {
$scopedBranchId = authBranchId();
$query = Shift::with(['creator'])->where(function ($q) use ($scopedBranchId) {
if ($scopedBranchId) {
$q->where(function($q2) use ($scopedBranchId) {
$q2->where('branch_id', $scopedBranchId)
->orWhereNull('branch_id'); // company wide shifts
})->whereIn('created_by', getCompanyAndUsersId());
} elseif (Auth::user()->can('manage-any-shifts')) {
$q->whereIn('created_by', getCompanyAndUsersId());
} else {
$branchId = Auth::user()->branch_id ?? Auth::user()->employee->branch_id ?? null;
@@ -67,8 +73,13 @@ class ShiftController extends Controller
$shifts = $query->paginate($request->per_page ?? 9);
// Stats always calculated from ALL records — never affected by filters or pagination
$allShifts = Shift::where(function ($q) {
if (Auth::user()->can('manage-any-shifts')) {
$allShifts = Shift::where(function ($q) use ($scopedBranchId) {
if ($scopedBranchId) {
$q->where(function($q2) use ($scopedBranchId) {
$q2->where('branch_id', $scopedBranchId)
->orWhereNull('branch_id'); // company wide shifts
})->whereIn('created_by', getCompanyAndUsersId());
} elseif (Auth::user()->can('manage-any-shifts')) {
$q->whereIn('created_by', getCompanyAndUsersId());
} else {
$branchId = Auth::user()->branch_id ?? Auth::user()->employee->branch_id ?? null;
@@ -90,7 +101,11 @@ class ShiftController extends Controller
'day' => (clone $allShifts)->where('is_night_shift', false)->count(),
];
$branches = \App\Models\Branch::whereIn('created_by', getCompanyAndUsersId())->get(['id', 'name']);
$branchesQuery = \App\Models\Branch::whereIn('created_by', getCompanyAndUsersId());
if ($scopedBranchId) {
$branchesQuery->where('id', $scopedBranchId);
}
$branches = $branchesQuery->get(['id', 'name']);
return Inertia::render('hr/shifts/index', [
'shifts' => $shifts,
@@ -122,7 +137,10 @@ class ShiftController extends Controller
$validated['status'] = $validated['status'] ?? 'active';
$validated['is_night_shift'] = $validated['is_night_shift'] ?? false;
if (!Auth::user()->can('manage-any-shifts')) {
$scopedBranchId = authBranchId();
if ($scopedBranchId) {
$validated['branch_id'] = $scopedBranchId;
} elseif (!Auth::user()->can('manage-any-shifts')) {
$validated['branch_id'] = Auth::user()->branch_id ?? Auth::user()->employee->branch_id ?? null;
} else {
$validated['branch_id'] = ($request->input('branch_id') === 'none' || empty($request->input('branch_id'))) ? null : $request->input('branch_id');
@@ -175,7 +193,10 @@ class ShiftController extends Controller
'branch_id' => 'nullable',
]);
if (!Auth::user()->can('manage-any-shifts')) {
$scopedBranchId = authBranchId();
if ($scopedBranchId) {
$validated['branch_id'] = $scopedBranchId;
} elseif (!Auth::user()->can('manage-any-shifts')) {
$validated['branch_id'] = Auth::user()->branch_id ?? Auth::user()->employee->branch_id ?? null;
} else {
$validated['branch_id'] = ($request->input('branch_id') === 'none' || empty($request->input('branch_id'))) ? null : $request->input('branch_id');
@@ -277,7 +298,12 @@ class ShiftController extends Controller
$q->where('employee_status', 'active');
});
if (!Auth::user()->can('manage-any-shifts')) {
$scopedBranchId = authBranchId();
if ($scopedBranchId) {
$query->whereHas('employee', function($q) use ($scopedBranchId) {
$q->where('branch_id', $scopedBranchId);
});
} elseif (!Auth::user()->can('manage-any-shifts')) {
$branchId = Auth::user()->branch_id ?? Auth::user()->employee->branch_id ?? null;
if ($branchId) {
$query->whereHas('employee', function($q) use ($branchId) {
@@ -399,8 +425,20 @@ class ShiftController extends Controller
];
});
$departments = \App\Models\Department::select('id', 'name')->get();
$allShifts = \App\Models\Shift::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->get();
$departmentsQuery = \App\Models\Department::whereIn('created_by', getCompanyAndUsersId())
->where('status', 'active');
if ($scopedBranchId) {
$departmentsQuery->where('branch_id', $scopedBranchId);
}
$departments = $departmentsQuery->select('id', 'name')->get();
$allShiftsQuery = \App\Models\Shift::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active');
if ($scopedBranchId) {
$allShiftsQuery->where(function($sq) use ($scopedBranchId) {
$sq->where('branch_id', $scopedBranchId)->orWhereNull('branch_id');
});
}
$allShifts = $allShiftsQuery->get();
return Inertia::render('hr/shifts/calendar', [
'users' => $users,
@@ -421,6 +459,14 @@ class ShiftController extends Controller
public function assignShift(Request $request)
{
if (Auth::user()->can('manage-shifts')) {
$scopedBranchId = authBranchId();
if ($scopedBranchId) {
$targetEmp = Employee::where('user_id', $request->employee_id)->first();
if ($targetEmp && (int)$targetEmp->branch_id !== (int)$scopedBranchId) {
return redirect()->back()->with('error', __('Permission Denied. You cannot assign shifts to employees outside your branch.'));
}
}
$validated = $request->validate([
'employee_id' => 'required|exists:users,id',
'start_date' => 'required|date',