refactor: implement strict role-based access control and scoped data visibility for finance and HR operations

This commit is contained in:
Yoga Pangestu 2026-06-14 15:49:51 +07:00
parent 36124c6535
commit 1de9d32136
8 changed files with 101 additions and 33 deletions

View File

@ -30,8 +30,8 @@ public function index(Request $request): Response
$user = $request->user(); $user = $request->user();
return Inertia::render('admin/finance/employee-advances/Index', [ return Inertia::render('admin/finance/employee-advances/Index', [
'employeeAdvances' => $this->employeeAdvanceService->paginateForIndex($tableQuery), 'employeeAdvances' => $this->employeeAdvanceService->paginateForIndex($tableQuery, $user),
'summary' => $this->employeeAdvanceService->outstandingSummary(), 'summary' => $this->employeeAdvanceService->outstandingSummary($user),
'authEmployeeId' => $user?->employee?->id, 'authEmployeeId' => $user?->employee?->id,
'canSubmit' => $user?->can(Permission::EMPLOYEE_ADVANCES_CREATE->value) 'canSubmit' => $user?->can(Permission::EMPLOYEE_ADVANCES_CREATE->value)
&& ! $user->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value) && ! $user->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value)
@ -71,7 +71,12 @@ public function destroy(EmployeeAdvance $employeeAdvance): RedirectResponse
public function approve(EmployeeAdvance $employeeAdvance): RedirectResponse public function approve(EmployeeAdvance $employeeAdvance): RedirectResponse
{ {
$this->employeeAdvanceService->approve($employeeAdvance, auth()->user()); $user = auth()->user();
if ($user && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
abort(403);
}
$this->employeeAdvanceService->approve($employeeAdvance, $user);
$this->flashSuccess('Kasbon berhasil disetujui dan dicairkan dari kas.'); $this->flashSuccess('Kasbon berhasil disetujui dan dicairkan dari kas.');
@ -80,10 +85,15 @@ public function approve(EmployeeAdvance $employeeAdvance): RedirectResponse
public function reject(RejectEmployeeAdvanceRequest $request, EmployeeAdvance $employeeAdvance): RedirectResponse public function reject(RejectEmployeeAdvanceRequest $request, EmployeeAdvance $employeeAdvance): RedirectResponse
{ {
$user = auth()->user();
if ($user && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
abort(403);
}
$this->employeeAdvanceService->reject( $this->employeeAdvanceService->reject(
$employeeAdvance, $employeeAdvance,
$request->validated('reason'), $request->validated('reason'),
auth()->user(), $user,
); );
$this->flashSuccess('Kasbon berhasil ditolak.'); $this->flashSuccess('Kasbon berhasil ditolak.');
@ -93,7 +103,12 @@ public function reject(RejectEmployeeAdvanceRequest $request, EmployeeAdvance $e
public function pay(EmployeeAdvance $employeeAdvance): RedirectResponse public function pay(EmployeeAdvance $employeeAdvance): RedirectResponse
{ {
$this->employeeAdvanceService->pay($employeeAdvance, auth()->user()); $user = auth()->user();
if ($user && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
abort(403);
}
$this->employeeAdvanceService->pay($employeeAdvance, $user);
$this->flashSuccess('Kasbon berhasil dilunasi.'); $this->flashSuccess('Kasbon berhasil dilunasi.');

View File

@ -20,10 +20,15 @@ public function __construct(
public function storeAdjustment(PayrollAdjustmentRequest $request, Payroll $payroll): RedirectResponse public function storeAdjustment(PayrollAdjustmentRequest $request, Payroll $payroll): RedirectResponse
{ {
$user = $request->user();
if ($user && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
abort(403, 'Anda tidak memiliki akses untuk menambah penyesuaian gaji.');
}
$this->payrollService->addAdjustment( $this->payrollService->addAdjustment(
$payroll, $payroll,
$request->validated(), $request->validated(),
$request->user(), $user,
); );
$this->flashSuccess('Penyesuaian gaji berhasil ditambahkan.'); $this->flashSuccess('Penyesuaian gaji berhasil ditambahkan.');
@ -35,10 +40,15 @@ public function storeAdjustment(PayrollAdjustmentRequest $request, Payroll $payr
public function updateAdjustment(PayrollAdjustmentRequest $request, PayrollAdjustment $payrollAdjustment): RedirectResponse public function updateAdjustment(PayrollAdjustmentRequest $request, PayrollAdjustment $payrollAdjustment): RedirectResponse
{ {
$user = $request->user();
if ($user && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
abort(403, 'Anda tidak memiliki akses untuk mengubah penyesuaian gaji.');
}
$this->payrollService->updateAdjustment( $this->payrollService->updateAdjustment(
$payrollAdjustment, $payrollAdjustment,
$request->validated(), $request->validated(),
$request->user(), $user,
); );
$this->flashSuccess('Penyesuaian gaji berhasil diperbarui.'); $this->flashSuccess('Penyesuaian gaji berhasil diperbarui.');
@ -50,6 +60,11 @@ public function updateAdjustment(PayrollAdjustmentRequest $request, PayrollAdjus
public function destroyAdjustment(PayrollAdjustment $payrollAdjustment): RedirectResponse public function destroyAdjustment(PayrollAdjustment $payrollAdjustment): RedirectResponse
{ {
$user = auth()->user();
if ($user && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
abort(403, 'Anda tidak memiliki akses untuk menghapus penyesuaian gaji.');
}
$payroll = $payrollAdjustment->payroll; $payroll = $payrollAdjustment->payroll;
$this->payrollService->deleteAdjustment($payrollAdjustment); $this->payrollService->deleteAdjustment($payrollAdjustment);

View File

@ -25,15 +25,16 @@ public function index(Request $request): Response
$tableQuery = $this->parseDataTableQuery($request); $tableQuery = $this->parseDataTableQuery($request);
$periodId = $request->integer('period_id') ?: null; $periodId = $request->integer('period_id') ?: null;
$period = $this->payrollService->resolvePeriod($periodId); $period = $this->payrollService->resolvePeriod($periodId);
$user = $request->user();
return Inertia::render('admin/finance/payroll/Index', [ return Inertia::render('admin/finance/payroll/Index', [
'periods' => $this->payrollService->listPeriods(), 'periods' => $this->payrollService->listPeriods(),
'currentPeriod' => $period, 'currentPeriod' => $period,
'payrolls' => $period 'payrolls' => $period
? $this->payrollService->paginateForPeriod($period, $tableQuery) ? $this->payrollService->paginateForPeriod($period, $tableQuery, $user)
: null, : null,
'summary' => $period 'summary' => $period
? $this->payrollService->periodSummary($period) ? $this->payrollService->periodSummary($period, $user)
: null, : null,
'adjustmentTypes' => PayrollAdjustmentType::selectOptions(), 'adjustmentTypes' => PayrollAdjustmentType::selectOptions(),
'filters' => array_merge( 'filters' => array_merge(

View File

@ -26,10 +26,10 @@ public function index(Request $request): Response
{ {
$user = $request->user(); $user = $request->user();
$employee = $user?->employee; $employee = $user?->employee;
$canManageAll = $user?->can(Permission::ATTENDANCES_MANAGE->value) ?? false; $isManager = $user?->hasAnyRole(['owner', 'developer', 'direktur']) ?? false;
$scopedEmployeeId = $canManageAll ? null : $employee?->id; $scopedEmployeeId = $isManager ? null : $employee?->id;
$hasScopedAccess = $canManageAll || $employee !== null; $hasScopedAccess = $isManager || $employee !== null;
$start = $request->date('start') ?? now()->startOfMonth(); $start = $request->date('start') ?? now()->startOfMonth();
$end = $request->date('end') ?? now()->endOfMonth(); $end = $request->date('end') ?? now()->endOfMonth();
@ -46,7 +46,7 @@ public function index(Request $request): Response
: null, : null,
'canCheckIn' => ($user?->can(Permission::ATTENDANCES_CREATE->value) ?? false) 'canCheckIn' => ($user?->can(Permission::ATTENDANCES_CREATE->value) ?? false)
&& $employee !== null, && $employee !== null,
'canManageAll' => $canManageAll, 'canManageAll' => $isManager,
'calendarRange' => [ 'calendarRange' => [
'start' => $start->toDateString(), 'start' => $start->toDateString(),
'end' => $end->toDateString(), 'end' => $end->toDateString(),
@ -74,6 +74,13 @@ public function checkOut(AttendanceCheckOutRequest $request): RedirectResponse
public function destroy(Attendance $attendance): RedirectResponse public function destroy(Attendance $attendance): RedirectResponse
{ {
$user = auth()->user();
if ($user && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
if ($attendance->employee_id !== $user->employee?->id) {
abort(403, 'Anda tidak memiliki akses untuk menghapus data presensi ini.');
}
}
$this->attendanceService->delete($attendance); $this->attendanceService->delete($attendance);
$this->flashDeleted('Data presensi'); $this->flashDeleted('Data presensi');

View File

@ -39,7 +39,7 @@ public function index(Request $request): Response
} }
return Inertia::render('admin/hr/leave-requests/Index', [ return Inertia::render('admin/hr/leave-requests/Index', [
'leaveRequests' => $this->leaveRequestService->paginateForIndex($tableQuery), 'leaveRequests' => $this->leaveRequestService->paginateForIndex($tableQuery, $user),
'authEmployeeId' => $user?->employee?->id, 'authEmployeeId' => $user?->employee?->id,
'canSubmit' => $user?->can(Permission::LEAVE_REQUESTS_CREATE->value) 'canSubmit' => $user?->can(Permission::LEAVE_REQUESTS_CREATE->value)
&& ! $user->can(Permission::LEAVE_REQUESTS_VERIFY->value) && ! $user->can(Permission::LEAVE_REQUESTS_VERIFY->value)
@ -80,7 +80,12 @@ public function destroy(LeaveRequest $leaveRequest): RedirectResponse
public function approve(LeaveRequest $leaveRequest): RedirectResponse public function approve(LeaveRequest $leaveRequest): RedirectResponse
{ {
$this->leaveRequestService->approve($leaveRequest, auth()->user()); $user = auth()->user();
if ($user && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
abort(403);
}
$this->leaveRequestService->approve($leaveRequest, $user);
$this->flashSuccess('Pengajuan cuti berhasil disetujui.'); $this->flashSuccess('Pengajuan cuti berhasil disetujui.');
@ -89,10 +94,15 @@ public function approve(LeaveRequest $leaveRequest): RedirectResponse
public function reject(RejectLeaveRequestRequest $request, LeaveRequest $leaveRequest): RedirectResponse public function reject(RejectLeaveRequestRequest $request, LeaveRequest $leaveRequest): RedirectResponse
{ {
$user = auth()->user();
if ($user && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
abort(403);
}
$this->leaveRequestService->reject( $this->leaveRequestService->reject(
$leaveRequest, $leaveRequest,
$request->validated('reason'), $request->validated('reason'),
auth()->user(), $user,
); );
$this->flashSuccess('Pengajuan cuti berhasil ditolak.'); $this->flashSuccess('Pengajuan cuti berhasil ditolak.');

View File

@ -26,15 +26,19 @@ public function __construct(
/** /**
* @return array{outstanding_amount: int, outstanding_amount_formatted: string, outstanding_count: int} * @return array{outstanding_amount: int, outstanding_amount_formatted: string, outstanding_count: int}
*/ */
public function outstandingSummary(): array public function outstandingSummary(?User $user = null): array
{ {
$outstandingAmount = (int) EmployeeAdvance::query() $query = EmployeeAdvance::query()
->where('status', EmployeeAdvanceStatus::APPROVED) ->where('status', EmployeeAdvanceStatus::APPROVED);
->sum('amount');
$outstandingCount = EmployeeAdvance::query() if ($user !== null && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
->where('status', EmployeeAdvanceStatus::APPROVED) $employeeId = $user->employee?->id ?? -1;
->count(); $query->where('employee_id', $employeeId);
}
$outstandingAmount = (int) $query->sum('amount');
$outstandingCount = $query->count();
return [ return [
'outstanding_amount' => $outstandingAmount, 'outstanding_amount' => $outstandingAmount,
@ -46,10 +50,14 @@ public function outstandingSummary(): array
/** /**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery * @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/ */
public function paginateForIndex(array $tableQuery): LengthAwarePaginator public function paginateForIndex(array $tableQuery, User $user): LengthAwarePaginator
{ {
$query = EmployeeAdvance::query() $query = EmployeeAdvance::query()
->with(['employee.user.profile', 'rejection']) ->with(['employee.user.profile', 'rejection'])
->when(! $user->hasAnyRole(['owner', 'developer', 'direktur']), function (Builder $query) use ($user): void {
$employeeId = $user->employee?->id ?? -1;
$query->where('employee_id', $employeeId);
})
->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void { ->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
$search = $tableQuery['search']; $search = $tableQuery['search'];
$query->where(function (Builder $query) use ($search): void { $query->where(function (Builder $query) use ($search): void {

View File

@ -59,15 +59,19 @@ public function resolvePeriod(?int $periodId): ?PayrollPeriod
/** /**
* @return array{total_amount: int, total_amount_formatted: string, total_count: int, status: string, status_label: string} * @return array{total_amount: int, total_amount_formatted: string, total_count: int, status: string, status_label: string}
*/ */
public function periodSummary(PayrollPeriod $period): array public function periodSummary(PayrollPeriod $period, User $user): array
{ {
$totalAmount = (int) Payroll::query() $query = Payroll::query()
->where('payroll_period_id', $period->id) ->where('payroll_period_id', $period->id);
->sum('total_amount');
$totalCount = Payroll::query() if (! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
->where('payroll_period_id', $period->id) $employeeId = $user->employee?->id ?? -1;
->count(); $query->where('employee_id', $employeeId);
}
$totalAmount = (int) $query->sum('total_amount');
$totalCount = $query->count();
return [ return [
'total_amount' => $totalAmount, 'total_amount' => $totalAmount,
@ -81,11 +85,15 @@ public function periodSummary(PayrollPeriod $period): array
/** /**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery * @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/ */
public function paginateForPeriod(PayrollPeriod $period, array $tableQuery): LengthAwarePaginator public function paginateForPeriod(PayrollPeriod $period, array $tableQuery, User $user): LengthAwarePaginator
{ {
$query = Payroll::query() $query = Payroll::query()
->with(['employee.user.profile', 'payrollPeriod', 'adjustments.createdBy.profile']) ->with(['employee.user.profile', 'payrollPeriod', 'adjustments.createdBy.profile'])
->where('payroll_period_id', $period->id) ->where('payroll_period_id', $period->id)
->when(! $user->hasAnyRole(['owner', 'developer', 'direktur']), function (Builder $query) use ($user): void {
$employeeId = $user->employee?->id ?? -1;
$query->where('employee_id', $employeeId);
})
->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void { ->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
$search = $tableQuery['search']; $search = $tableQuery['search'];
$query->where(function (Builder $query) use ($search): void { $query->where(function (Builder $query) use ($search): void {

View File

@ -24,10 +24,14 @@ public function __construct(
/** /**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery * @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/ */
public function paginateForIndex(array $tableQuery): LengthAwarePaginator public function paginateForIndex(array $tableQuery, User $user): LengthAwarePaginator
{ {
$query = LeaveRequest::query() $query = LeaveRequest::query()
->with(['employee.user.profile', 'rejection']) ->with(['employee.user.profile', 'rejection'])
->when(! $user->hasAnyRole(['owner', 'developer', 'direktur']), function (Builder $query) use ($user): void {
$employeeId = $user->employee?->id ?? -1;
$query->where('employee_id', $employeeId);
})
->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void { ->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
$search = $tableQuery['search']; $search = $tableQuery['search'];
$query->where(function (Builder $query) use ($search): void { $query->where(function (Builder $query) use ($search): void {