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();
return Inertia::render('admin/finance/employee-advances/Index', [
'employeeAdvances' => $this->employeeAdvanceService->paginateForIndex($tableQuery),
'summary' => $this->employeeAdvanceService->outstandingSummary(),
'employeeAdvances' => $this->employeeAdvanceService->paginateForIndex($tableQuery, $user),
'summary' => $this->employeeAdvanceService->outstandingSummary($user),
'authEmployeeId' => $user?->employee?->id,
'canSubmit' => $user?->can(Permission::EMPLOYEE_ADVANCES_CREATE->value)
&& ! $user->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value)
@ -71,7 +71,12 @@ public function destroy(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.');
@ -80,10 +85,15 @@ public function approve(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(
$employeeAdvance,
$request->validated('reason'),
auth()->user(),
$user,
);
$this->flashSuccess('Kasbon berhasil ditolak.');
@ -93,7 +103,12 @@ public function reject(RejectEmployeeAdvanceRequest $request, EmployeeAdvance $e
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.');

View File

@ -20,10 +20,15 @@ public function __construct(
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(
$payroll,
$request->validated(),
$request->user(),
$user,
);
$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
{
$user = $request->user();
if ($user && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
abort(403, 'Anda tidak memiliki akses untuk mengubah penyesuaian gaji.');
}
$this->payrollService->updateAdjustment(
$payrollAdjustment,
$request->validated(),
$request->user(),
$user,
);
$this->flashSuccess('Penyesuaian gaji berhasil diperbarui.');
@ -50,6 +60,11 @@ public function updateAdjustment(PayrollAdjustmentRequest $request, PayrollAdjus
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;
$this->payrollService->deleteAdjustment($payrollAdjustment);

View File

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

View File

@ -26,10 +26,10 @@ public function index(Request $request): Response
{
$user = $request->user();
$employee = $user?->employee;
$canManageAll = $user?->can(Permission::ATTENDANCES_MANAGE->value) ?? false;
$isManager = $user?->hasAnyRole(['owner', 'developer', 'direktur']) ?? false;
$scopedEmployeeId = $canManageAll ? null : $employee?->id;
$hasScopedAccess = $canManageAll || $employee !== null;
$scopedEmployeeId = $isManager ? null : $employee?->id;
$hasScopedAccess = $isManager || $employee !== null;
$start = $request->date('start') ?? now()->startOfMonth();
$end = $request->date('end') ?? now()->endOfMonth();
@ -46,7 +46,7 @@ public function index(Request $request): Response
: null,
'canCheckIn' => ($user?->can(Permission::ATTENDANCES_CREATE->value) ?? false)
&& $employee !== null,
'canManageAll' => $canManageAll,
'canManageAll' => $isManager,
'calendarRange' => [
'start' => $start->toDateString(),
'end' => $end->toDateString(),
@ -74,6 +74,13 @@ public function checkOut(AttendanceCheckOutRequest $request): 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->flashDeleted('Data presensi');

View File

@ -39,7 +39,7 @@ public function index(Request $request): Response
}
return Inertia::render('admin/hr/leave-requests/Index', [
'leaveRequests' => $this->leaveRequestService->paginateForIndex($tableQuery),
'leaveRequests' => $this->leaveRequestService->paginateForIndex($tableQuery, $user),
'authEmployeeId' => $user?->employee?->id,
'canSubmit' => $user?->can(Permission::LEAVE_REQUESTS_CREATE->value)
&& ! $user->can(Permission::LEAVE_REQUESTS_VERIFY->value)
@ -80,7 +80,12 @@ public function destroy(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.');
@ -89,10 +94,15 @@ public function approve(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(
$leaveRequest,
$request->validated('reason'),
auth()->user(),
$user,
);
$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}
*/
public function outstandingSummary(): array
public function outstandingSummary(?User $user = null): array
{
$outstandingAmount = (int) EmployeeAdvance::query()
->where('status', EmployeeAdvanceStatus::APPROVED)
->sum('amount');
$query = EmployeeAdvance::query()
->where('status', EmployeeAdvanceStatus::APPROVED);
$outstandingCount = EmployeeAdvance::query()
->where('status', EmployeeAdvanceStatus::APPROVED)
->count();
if ($user !== null && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
$employeeId = $user->employee?->id ?? -1;
$query->where('employee_id', $employeeId);
}
$outstandingAmount = (int) $query->sum('amount');
$outstandingCount = $query->count();
return [
'outstanding_amount' => $outstandingAmount,
@ -46,10 +50,14 @@ public function outstandingSummary(): array
/**
* @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()
->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 {
$search = $tableQuery['search'];
$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}
*/
public function periodSummary(PayrollPeriod $period): array
public function periodSummary(PayrollPeriod $period, User $user): array
{
$totalAmount = (int) Payroll::query()
->where('payroll_period_id', $period->id)
->sum('total_amount');
$query = Payroll::query()
->where('payroll_period_id', $period->id);
$totalCount = Payroll::query()
->where('payroll_period_id', $period->id)
->count();
if (! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
$employeeId = $user->employee?->id ?? -1;
$query->where('employee_id', $employeeId);
}
$totalAmount = (int) $query->sum('total_amount');
$totalCount = $query->count();
return [
'total_amount' => $totalAmount,
@ -81,11 +85,15 @@ public function periodSummary(PayrollPeriod $period): array
/**
* @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()
->with(['employee.user.profile', 'payrollPeriod', 'adjustments.createdBy.profile'])
->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 {
$search = $tableQuery['search'];
$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
*/
public function paginateForIndex(array $tableQuery): LengthAwarePaginator
public function paginateForIndex(array $tableQuery, User $user): LengthAwarePaginator
{
$query = LeaveRequest::query()
->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 {
$search = $tableQuery['search'];
$query->where(function (Builder $query) use ($search): void {