From a904a27934b3a52218529dd4f79cff2fa97dfb87 Mon Sep 17 00:00:00 2001
From: Mhanif26
Date: Sun, 17 May 2026 00:53:43 +0700
Subject: [PATCH] tambah akaun dari tiap role
---
api/auth.ts | 59 +++++--
scratch/check_users.ts | 16 ++
scratch/force_seed.ts | 10 ++
src/App.tsx | 16 +-
src/components/AppLayout.tsx | 19 +-
src/pages/PlacesOfWorshipPage.tsx | 176 ++++++++++---------
src/pages/RecipientsPage.tsx | 277 ++++++++++++++++--------------
7 files changed, 330 insertions(+), 243 deletions(-)
create mode 100644 scratch/check_users.ts
create mode 100644 scratch/force_seed.ts
diff --git a/api/auth.ts b/api/auth.ts
index dba9aff..864ae59 100644
--- a/api/auth.ts
+++ b/api/auth.ts
@@ -113,30 +113,61 @@ export async function seedLocalUserIfMissing() {
// Check existence without selecting `password_hash` to avoid failures on older schemas
try {
const [rows] = await db.execute(sql`SELECT id FROM users WHERE unionId = ${LocalAuth.defaultAdminEmail} LIMIT 1`);
- if (Array.isArray(rows) && rows.length > 0) return;
} catch (err) {
console.warn('[seed] existence check failed, will attempt to insert anyway', err);
}
- // Try to insert using the typed Drizzle insert (preferred). If that fails because the
- // `password_hash` column is missing, fall back to a raw INSERT that omits the column.
+ // 1. Ensure admin account
try {
- await db.insert(users).values({
- unionId: LocalAuth.defaultAdminEmail,
- name: "Admin Lokal",
- email: LocalAuth.defaultAdminEmail,
- passwordHash: hashPassword(LocalAuth.defaultAdminPassword),
- role: "admin",
- lastSignInAt: new Date(),
- });
+ const [existingAdmin] = await db.execute(sql`SELECT id FROM users WHERE unionId = ${LocalAuth.defaultAdminEmail} LIMIT 1`);
+ if (!existingAdmin || (Array.isArray(existingAdmin) && existingAdmin.length === 0)) {
+ await db.insert(users).values({
+ unionId: LocalAuth.defaultAdminEmail,
+ name: "Admin Lokal",
+ email: LocalAuth.defaultAdminEmail,
+ passwordHash: hashPassword(LocalAuth.defaultAdminPassword),
+ role: "admin",
+ lastSignInAt: new Date(),
+ });
+ console.log(`[seed] Created admin account: ${LocalAuth.defaultAdminEmail}`);
+ }
} catch (err) {
- console.warn('[seed] typed insert failed, attempting fallback insert without password_hash', err);
+ console.warn('[seed] Admin seeding failed, attempting fallback', err);
try {
- await db.execute(sql`INSERT INTO users (unionId, name, email, role, lastSignInAt) VALUES (
+ await db.execute(sql`INSERT IGNORE INTO users (unionId, name, email, role, lastSignInAt) VALUES (
${LocalAuth.defaultAdminEmail}, ${'Admin Lokal'}, ${LocalAuth.defaultAdminEmail}, ${'admin'}, ${new Date()}
)`);
} catch (err2) {
- console.error('[seed] fallback insert also failed', err2);
+ console.error('[seed] Admin fallback failed', err2);
+ }
+ }
+
+ // 2. Seed default roles for testing
+ const defaultAccounts = [
+ { email: "officer@example.com", name: "Petugas Lapangan", role: "officer" as const },
+ { email: "manager@example.com", name: "Pengelola Wilayah", role: "manager" as const },
+ ];
+
+ for (const acc of defaultAccounts) {
+ try {
+ const res = await db.execute(sql`SELECT id FROM users WHERE unionId = ${acc.email} LIMIT 1`);
+ const [existing] = res as any;
+
+ if (existing && (!Array.isArray(existing) || existing.length > 0)) {
+ continue;
+ }
+
+ await db.insert(users).values({
+ unionId: acc.email,
+ name: acc.name,
+ email: acc.email,
+ passwordHash: hashPassword("password123"),
+ role: acc.role,
+ lastSignInAt: new Date(),
+ });
+ console.log(`[seed] Created ${acc.role} account: ${acc.email}`);
+ } catch (err) {
+ console.warn(`[seed] Failed to create ${acc.role} (${acc.email})`, err);
}
}
}
\ No newline at end of file
diff --git a/scratch/check_users.ts b/scratch/check_users.ts
new file mode 100644
index 0000000..c7b2ccf
--- /dev/null
+++ b/scratch/check_users.ts
@@ -0,0 +1,16 @@
+import { sql } from "drizzle-orm";
+import { getDb } from "../api/queries/connection";
+import { users } from "../db/schema";
+
+async function check() {
+ const db = getDb();
+ const res = await db.execute(sql`SELECT id FROM users WHERE unionId = 'officer@example.com' LIMIT 1`);
+ console.log("Raw query result for officer:", res);
+
+ const allUsers = await db.select().from(users);
+ console.log("Current Users in DB:");
+ console.table(allUsers.map(u => ({ id: u.id, email: u.email, role: u.role })));
+ process.exit(0);
+}
+
+check().catch(console.error);
diff --git a/scratch/force_seed.ts b/scratch/force_seed.ts
new file mode 100644
index 0000000..f608604
--- /dev/null
+++ b/scratch/force_seed.ts
@@ -0,0 +1,10 @@
+import { seedLocalUserIfMissing } from "../api/auth";
+
+async function forceSeed() {
+ console.log("Starting force seed...");
+ await seedLocalUserIfMissing();
+ console.log("Force seed completed.");
+ process.exit(0);
+}
+
+forceSeed().catch(console.error);
diff --git a/src/App.tsx b/src/App.tsx
index d58724f..528a0ca 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -11,7 +11,13 @@ import VerificationPage from "./pages/VerificationPage";
import UsersPage from "./pages/UsersPage";
import SettingsPage from "./pages/SettingsPage";
-function ProtectedRoute({ children, adminOnly }: { children: React.ReactNode; adminOnly?: boolean }) {
+function ProtectedRoute({
+ children,
+ allowedRoles,
+}: {
+ children: React.ReactNode;
+ allowedRoles?: string[];
+}) {
const { user, isLoading } = useAuth();
if (isLoading) {
@@ -26,7 +32,7 @@ function ProtectedRoute({ children, adminOnly }: { children: React.ReactNode; ad
return ;
}
- if (adminOnly && user.role !== "admin") {
+ if (allowedRoles && !allowedRoles.includes(user.role)) {
return ;
}
@@ -72,7 +78,7 @@ export default function App() {
+
}
@@ -80,7 +86,7 @@ export default function App() {
+
}
@@ -88,7 +94,7 @@ export default function App() {
+
}
diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx
index 9fca0ee..9f460ca 100644
--- a/src/components/AppLayout.tsx
+++ b/src/components/AppLayout.tsx
@@ -33,9 +33,14 @@ const menuItems = [
{ icon: Map, label: "Peta GIS", path: "/map" },
{ icon: Users, label: "Penerima Bantuan", path: "/recipients" },
{ icon: MapPinHouse, label: "Rumah Ibadah", path: "/places" },
- { icon: ClipboardCheck, label: "Verifikasi", path: "/verification" },
- { icon: Shield, label: "Pengguna", path: "/users", adminOnly: true },
- { icon: Settings, label: "Pengaturan", path: "/settings" },
+ {
+ icon: ClipboardCheck,
+ label: "Verifikasi",
+ path: "/verification",
+ roles: ["admin", "officer"],
+ },
+ { icon: Shield, label: "Pengguna", path: "/users", roles: ["admin"] },
+ { icon: Settings, label: "Pengaturan", path: "/settings", roles: ["admin"] },
];
export default function AppLayout({ children }: { children: ReactNode }) {
@@ -46,11 +51,10 @@ export default function AppLayout({ children }: { children: ReactNode }) {
const [collapsed, setCollapsed] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
- const isAdmin = user?.role === "admin";
const activeItem = menuItems.find((item) => item.path === location.pathname);
const filteredItems = menuItems.filter(
- (item) => !item.adminOnly || isAdmin
+ (item) => !item.roles || (user?.role && item.roles.includes(user.role))
);
const sidebarWidth = collapsed ? 72 : 260;
@@ -95,11 +99,6 @@ export default function AppLayout({ children }: { children: ReactNode }) {
>
{!collapsed && {item.label}}
- {!collapsed && item.adminOnly && (
-
- Admin
-
- )}
);
})}
diff --git a/src/pages/PlacesOfWorshipPage.tsx b/src/pages/PlacesOfWorshipPage.tsx
index d7601a7..73f8ec2 100644
--- a/src/pages/PlacesOfWorshipPage.tsx
+++ b/src/pages/PlacesOfWorshipPage.tsx
@@ -59,6 +59,12 @@ function parseRupiahInput(input: string) {
}
export default function PlacesOfWorshipPage() {
+ const { user } = useAuth();
+ const isAdmin = user?.role === "admin";
+ const isManager = user?.role === "manager";
+ const canModify = isAdmin; // Only Admin can modify Places of Worship
+ const canDistribute = isAdmin || isManager; // Admin and Manager can distribute aid
+
const [search, setSearch] = useState("");
const [typeFilter, setTypeFilter] = useState("all");
const [isAddOpen, setIsAddOpen] = useState(false);
@@ -243,79 +249,81 @@ export default function PlacesOfWorshipPage() {
Rumah Ibadah
Kelola data rumah ibadah dan cakupan wilayahnya
-
+
+
+ Tambah Rumah Ibadah
+
+
+
+ )}
{/* Filters */}
@@ -385,15 +393,21 @@ export default function PlacesOfWorshipPage() {
-
-
-
+ {canModify && (
+
+ )}
+ {canDistribute && (
+
+ )}
+ {canModify && (
+
+ )}
diff --git a/src/pages/RecipientsPage.tsx b/src/pages/RecipientsPage.tsx
index 06ddc44..e72810a 100644
--- a/src/pages/RecipientsPage.tsx
+++ b/src/pages/RecipientsPage.tsx
@@ -78,6 +78,11 @@ function parseRupiahInput(input: string) {
}
export default function RecipientsPage() {
+ const { user } = useAuth();
+ const isAdmin = user?.role === "admin";
+ const isManager = user?.role === "manager";
+ const canModify = isAdmin || isManager;
+
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [isAddOpen, setIsAddOpen] = useState(false);
@@ -308,120 +313,122 @@ export default function RecipientsPage() {
Kelola data warga penerima bantuan sosial
-
+
+
+
+ Tambah Penerima Baru
+
+
+
+
+ )}
{/* Filters */}
@@ -512,26 +519,30 @@ export default function RecipientsPage() {
>
-
-
+ {canModify && (
+ <>
+
+
+ >
+ )}