From 781962b44c6a0e1becf5ee1a48d5e1adff8e5823 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Thu, 30 Apr 2026 15:14:13 +0700 Subject: [PATCH] feat: restrict dashboard sensitive financial metrics for admin role --- app/Http/Controllers/DashboardController.php | 14 ++ resources/js/pages/dashboard.tsx | 181 +++++++++++++------ tests/Feature/Admin/DashboardTest.php | 25 +++ 3 files changed, 164 insertions(+), 56 deletions(-) diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index e4dbd0a..e3806fa 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -9,6 +9,7 @@ use App\Models\Order; use App\Models\OrderItem; use App\Models\Purchase; +use App\Models\User; use Carbon\Carbon; use Illuminate\Support\Facades\DB; use Inertia\Inertia; @@ -127,6 +128,19 @@ public function __invoke() ->limit(5) ->get(); + /** @var User $user */ + $user = auth()->user(); + if ($user->hasRole('Admin')) { + unset( + $stats['cogs'], + $stats['aov'], + $stats['gross_profit'], + $stats['net_profit'], + $stats['profit_margin'], + $stats['total_purchases'] + ); + } + return Inertia::render('dashboard', [ 'stats' => $stats, 'salesByHour' => $salesByHour, diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index 7f47544..665dee9 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -1,15 +1,33 @@ -import { Head, usePage } from '@inertiajs/react'; -import { useEffect, useState } from 'react'; import { StatCard } from '@/components/cards/stat-card'; import { WelcomeCard } from '@/components/cards/welcome-card'; import { dashboard } from '@/routes'; import type { DashboardPageProps } from '@/types'; +import { Head, usePage } from '@inertiajs/react'; +import { useEffect, useState } from 'react'; import { CustomBarChart } from '../components/charts/bar-chart'; import { CustomPieChart } from '../components/charts/pie-chart'; import { SalesByHourChart } from '../components/charts/sales-by-hour-chart'; export default function Dashboard() { - const { auth, stats, salesByHour, paymentMethods, orderStatuses, orderChannels, topProducts, topCustomers } = usePage().props; + const { + auth, + stats, + salesByHour, + paymentMethods, + orderStatuses, + orderChannels, + topProducts, + topCustomers, + } = usePage< + DashboardPageProps & { + salesByHour: any[]; + paymentMethods: any[]; + orderStatuses: any[]; + orderChannels: any[]; + topProducts: any[]; + topCustomers: any[]; + } + >().props; const [time, setTime] = useState(new Date()); useEffect(() => { @@ -22,43 +40,43 @@ export default function Dashboard() { const hour = time.getHours(); if (hour >= 5 && hour < 11) { -return { - greeting: 'Selamat Pagi', - class: 'bg-gradient-to-br from-amber-50 to-orange-100 dark:from-amber-950/40 dark:to-orange-900/40 border-amber-200 dark:border-amber-800/30 shadow-sm', - text: 'text-amber-950 dark:text-amber-50', - subtext: 'text-amber-900/70 dark:text-amber-200/70', - decoration: 'bg-amber-500/10 dark:bg-amber-500/20', - pageBackground: 'bg-amber-50/50 dark:bg-amber-950/10', - card: 'bg-white/80 dark:bg-amber-900/10 backdrop-blur-sm border-amber-200/50 dark:border-amber-800/20', - placeholder: 'stroke-amber-500/20 dark:stroke-amber-400/20' - }; -} + return { + greeting: 'Selamat Pagi', + class: 'bg-gradient-to-br from-amber-50 to-orange-100 dark:from-amber-950/40 dark:to-orange-900/40 border-amber-200 dark:border-amber-800/30 shadow-sm', + text: 'text-amber-950 dark:text-amber-50', + subtext: 'text-amber-900/70 dark:text-amber-200/70', + decoration: 'bg-amber-500/10 dark:bg-amber-500/20', + pageBackground: 'bg-amber-50/50 dark:bg-amber-950/10', + card: 'bg-white/80 dark:bg-amber-900/10 backdrop-blur-sm border-amber-200/50 dark:border-amber-800/20', + placeholder: 'stroke-amber-500/20 dark:stroke-amber-400/20', + }; + } if (hour >= 11 && hour < 15) { -return { - greeting: 'Selamat Siang', - class: 'bg-gradient-to-br from-sky-50 to-blue-100 dark:from-sky-950/40 dark:to-blue-900/40 border-sky-200 dark:border-sky-800/30 shadow-sm', - text: 'text-sky-950 dark:text-sky-50', - subtext: 'text-sky-900/70 dark:text-sky-200/70', - decoration: 'bg-sky-500/10 dark:bg-sky-500/20', - pageBackground: 'bg-sky-50/50 dark:bg-sky-950/10', - card: 'bg-white/80 dark:bg-sky-900/10 backdrop-blur-sm border-sky-200/50 dark:border-sky-800/20', - placeholder: 'stroke-sky-500/20 dark:stroke-sky-400/20' - }; -} + return { + greeting: 'Selamat Siang', + class: 'bg-gradient-to-br from-sky-50 to-blue-100 dark:from-sky-950/40 dark:to-blue-900/40 border-sky-200 dark:border-sky-800/30 shadow-sm', + text: 'text-sky-950 dark:text-sky-50', + subtext: 'text-sky-900/70 dark:text-sky-200/70', + decoration: 'bg-sky-500/10 dark:bg-sky-500/20', + pageBackground: 'bg-sky-50/50 dark:bg-sky-950/10', + card: 'bg-white/80 dark:bg-sky-900/10 backdrop-blur-sm border-sky-200/50 dark:border-sky-800/20', + placeholder: 'stroke-sky-500/20 dark:stroke-sky-400/20', + }; + } if (hour >= 15 && hour < 19) { -return { - greeting: 'Selamat Sore', - class: 'bg-gradient-to-br from-orange-50 to-purple-100 dark:from-orange-950/40 dark:to-purple-900/40 border-orange-200 dark:border-orange-800/30 shadow-sm', - text: 'text-orange-950 dark:text-orange-50', - subtext: 'text-orange-900/70 dark:text-orange-200/70', - decoration: 'bg-orange-500/10 dark:bg-orange-500/20', - pageBackground: 'bg-orange-50/50 dark:bg-orange-950/10', - card: 'bg-white/80 dark:bg-orange-900/10 backdrop-blur-sm border-orange-200/50 dark:border-orange-800/20', - placeholder: 'stroke-orange-500/20 dark:stroke-orange-400/20' - }; -} + return { + greeting: 'Selamat Sore', + class: 'bg-gradient-to-br from-orange-50 to-purple-100 dark:from-orange-950/40 dark:to-purple-900/40 border-orange-200 dark:border-orange-800/30 shadow-sm', + text: 'text-orange-950 dark:text-orange-50', + subtext: 'text-orange-900/70 dark:text-orange-200/70', + decoration: 'bg-orange-500/10 dark:bg-orange-500/20', + pageBackground: 'bg-orange-50/50 dark:bg-orange-950/10', + card: 'bg-white/80 dark:bg-orange-900/10 backdrop-blur-sm border-orange-200/50 dark:border-orange-800/20', + placeholder: 'stroke-orange-500/20 dark:stroke-orange-400/20', + }; + } return { greeting: 'Selamat Malam', @@ -68,52 +86,103 @@ return { decoration: 'bg-indigo-500/20', pageBackground: 'bg-slate-100 dark:bg-slate-950', card: 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800', - placeholder: 'stroke-slate-300 dark:stroke-slate-700' + placeholder: 'stroke-slate-300 dark:stroke-slate-700', }; }; const theme = getTimeTheme(); + const isAdmin = auth.roles.includes('Admin'); + + const statItems = [ + { + title: 'Total Penjualan', + stat: stats.total_sales, + isCurrency: false, + }, + { + title: 'Total Pendapatan', + stat: stats.total_revenue, + }, + { + title: 'Total Pengeluaran', + stat: stats.total_expenses, + }, + { + title: 'HPP (Modal)', + stat: stats.cogs, + sensitive: true, + }, + { + title: 'AOV (Avg Order Value)', + stat: stats.aov, + sensitive: true, + }, + { + title: 'Profit Margin', + stat: stats.profit_margin, + isPercentage: true, + sensitive: true, + }, + { + title: 'Total Diskon', + stat: stats.total_discount, + }, + { + title: 'Laba Kotor', + stat: stats.gross_profit, + sensitive: true, + }, + { + title: 'Laba Bersih', + stat: stats.net_profit, + sensitive: true, + }, + { + title: 'Total Pembelian', + stat: stats.total_purchases, + sensitive: true, + }, + { + title: 'Produk Terjual', + stat: stats.products_sold, + isCurrency: false, + }, + { + title: 'Total Pelanggan', + stat: stats.total_customers, + isCurrency: false, + }, + ].filter((item) => !isAdmin || !item.sensitive); return ( <> -
+
{/* Welcome Section */} {/* Stats Grid */}
- {[ - { title: "Total Penjualan", stat: stats.total_sales, isCurrency: false }, - { title: "Total Pendapatan", stat: stats.total_revenue }, - { title: "Total Pengeluaran", stat: stats.total_expenses }, - { title: "HPP (Modal)", stat: stats.cogs }, - { title: "AOV (Avg Order Value)", stat: stats.aov }, - { title: "Profit Margin", stat: stats.profit_margin, isPercentage: true }, - { title: "Total Diskon", stat: stats.total_discount }, - { title: "Laba Kotor", stat: stats.gross_profit }, - { title: "Laba Bersih", stat: stats.net_profit }, - { title: "Total Pembelian", stat: stats.total_purchases }, - { title: "Produk Terjual", stat: stats.products_sold, isCurrency: false }, - { title: "Total Pelanggan", stat: stats.total_customers, isCurrency: false }, - ].map((item, index) => ( + {statItems.map((item, index) => (
))}
-
diff --git a/tests/Feature/Admin/DashboardTest.php b/tests/Feature/Admin/DashboardTest.php index 974508d..45cb443 100644 --- a/tests/Feature/Admin/DashboardTest.php +++ b/tests/Feature/Admin/DashboardTest.php @@ -3,6 +3,7 @@ use App\Models\Expense; use App\Models\Order; use App\Models\Purchase; +use Spatie\Permission\Models\Role; use function Pest\Laravel\actingAs; use function Pest\Laravel\get; @@ -70,4 +71,28 @@ ->has('topCustomers') ); }); + + it('filters sensitive data for users with Admin role', function () { + Role::findOrCreate('Admin'); + $user = createAuthorizedUser(['View:Dashboard']); + $user->assignRole('Admin'); + + actingAs($user); + + get(route('dashboard')) + ->assertOk() + ->assertInertia(fn ($page) => $page + ->component('dashboard') + ->has('stats') + ->has('stats.total_sales') + ->has('stats.total_revenue') + ->missing('stats.cogs') + ->missing('stats.aov') + ->missing('stats.gross_profit') + ->missing('stats.net_profit') + ->missing('stats.profit_margin') + ->missing('stats.total_purchases') + ->has('topCustomers') + ); + }); });