tambah akaun dari tiap role

This commit is contained in:
2026-05-17 00:53:43 +07:00
parent c205a89b88
commit a904a27934
7 changed files with 330 additions and 243 deletions
+37 -6
View File
@@ -113,14 +113,14 @@ export async function seedLocalUserIfMissing() {
// Check existence without selecting `password_hash` to avoid failures on older schemas // Check existence without selecting `password_hash` to avoid failures on older schemas
try { try {
const [rows] = await db.execute(sql`SELECT id FROM users WHERE unionId = ${LocalAuth.defaultAdminEmail} LIMIT 1`); 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) { } catch (err) {
console.warn('[seed] existence check failed, will attempt to insert anyway', 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 // 1. Ensure admin account
// `password_hash` column is missing, fall back to a raw INSERT that omits the column.
try { try {
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({ await db.insert(users).values({
unionId: LocalAuth.defaultAdminEmail, unionId: LocalAuth.defaultAdminEmail,
name: "Admin Lokal", name: "Admin Lokal",
@@ -129,14 +129,45 @@ export async function seedLocalUserIfMissing() {
role: "admin", role: "admin",
lastSignInAt: new Date(), lastSignInAt: new Date(),
}); });
console.log(`[seed] Created admin account: ${LocalAuth.defaultAdminEmail}`);
}
} catch (err) { } 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 { 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()} ${LocalAuth.defaultAdminEmail}, ${'Admin Lokal'}, ${LocalAuth.defaultAdminEmail}, ${'admin'}, ${new Date()}
)`); )`);
} catch (err2) { } 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);
} }
} }
} }
+16
View File
@@ -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);
+10
View File
@@ -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);
+11 -5
View File
@@ -11,7 +11,13 @@ import VerificationPage from "./pages/VerificationPage";
import UsersPage from "./pages/UsersPage"; import UsersPage from "./pages/UsersPage";
import SettingsPage from "./pages/SettingsPage"; 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(); const { user, isLoading } = useAuth();
if (isLoading) { if (isLoading) {
@@ -26,7 +32,7 @@ function ProtectedRoute({ children, adminOnly }: { children: React.ReactNode; ad
return <Navigate to="/login" replace />; return <Navigate to="/login" replace />;
} }
if (adminOnly && user.role !== "admin") { if (allowedRoles && !allowedRoles.includes(user.role)) {
return <Navigate to="/" replace />; return <Navigate to="/" replace />;
} }
@@ -72,7 +78,7 @@ export default function App() {
<Route <Route
path="/verification" path="/verification"
element={ element={
<ProtectedRoute> <ProtectedRoute allowedRoles={["admin", "officer"]}>
<VerificationPage /> <VerificationPage />
</ProtectedRoute> </ProtectedRoute>
} }
@@ -80,7 +86,7 @@ export default function App() {
<Route <Route
path="/users" path="/users"
element={ element={
<ProtectedRoute adminOnly> <ProtectedRoute allowedRoles={["admin"]}>
<UsersPage /> <UsersPage />
</ProtectedRoute> </ProtectedRoute>
} }
@@ -88,7 +94,7 @@ export default function App() {
<Route <Route
path="/settings" path="/settings"
element={ element={
<ProtectedRoute> <ProtectedRoute allowedRoles={["admin"]}>
<SettingsPage /> <SettingsPage />
</ProtectedRoute> </ProtectedRoute>
} }
+9 -10
View File
@@ -33,9 +33,14 @@ const menuItems = [
{ icon: Map, label: "Peta GIS", path: "/map" }, { icon: Map, label: "Peta GIS", path: "/map" },
{ icon: Users, label: "Penerima Bantuan", path: "/recipients" }, { icon: Users, label: "Penerima Bantuan", path: "/recipients" },
{ icon: MapPinHouse, label: "Rumah Ibadah", path: "/places" }, { icon: MapPinHouse, label: "Rumah Ibadah", path: "/places" },
{ icon: ClipboardCheck, label: "Verifikasi", path: "/verification" }, {
{ icon: Shield, label: "Pengguna", path: "/users", adminOnly: true }, icon: ClipboardCheck,
{ icon: Settings, label: "Pengaturan", path: "/settings" }, 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 }) { export default function AppLayout({ children }: { children: ReactNode }) {
@@ -46,11 +51,10 @@ export default function AppLayout({ children }: { children: ReactNode }) {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
const isAdmin = user?.role === "admin";
const activeItem = menuItems.find((item) => item.path === location.pathname); const activeItem = menuItems.find((item) => item.path === location.pathname);
const filteredItems = menuItems.filter( const filteredItems = menuItems.filter(
(item) => !item.adminOnly || isAdmin (item) => !item.roles || (user?.role && item.roles.includes(user.role))
); );
const sidebarWidth = collapsed ? 72 : 260; const sidebarWidth = collapsed ? 72 : 260;
@@ -95,11 +99,6 @@ export default function AppLayout({ children }: { children: ReactNode }) {
> >
<item.icon className="h-[18px] w-[18px] shrink-0" /> <item.icon className="h-[18px] w-[18px] shrink-0" />
{!collapsed && <span className="truncate">{item.label}</span>} {!collapsed && <span className="truncate">{item.label}</span>}
{!collapsed && item.adminOnly && (
<Badge variant="outline" className="ml-auto text-[9px] px-1 py-0 h-4">
Admin
</Badge>
)}
</button> </button>
); );
})} })}
+14
View File
@@ -59,6 +59,12 @@ function parseRupiahInput(input: string) {
} }
export default function PlacesOfWorshipPage() { 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 [search, setSearch] = useState("");
const [typeFilter, setTypeFilter] = useState("all"); const [typeFilter, setTypeFilter] = useState("all");
const [isAddOpen, setIsAddOpen] = useState(false); const [isAddOpen, setIsAddOpen] = useState(false);
@@ -243,6 +249,7 @@ export default function PlacesOfWorshipPage() {
<h2 className="text-2xl font-bold tracking-tight">Rumah Ibadah</h2> <h2 className="text-2xl font-bold tracking-tight">Rumah Ibadah</h2>
<p className="text-muted-foreground mt-1 text-sm">Kelola data rumah ibadah dan cakupan wilayahnya</p> <p className="text-muted-foreground mt-1 text-sm">Kelola data rumah ibadah dan cakupan wilayahnya</p>
</div> </div>
{canModify && (
<Dialog open={isAddOpen} onOpenChange={setIsAddOpen}> <Dialog open={isAddOpen} onOpenChange={setIsAddOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="rounded-xl" size="sm"> <Button className="rounded-xl" size="sm">
@@ -316,6 +323,7 @@ export default function PlacesOfWorshipPage() {
</form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)}
</div> </div>
{/* Filters */} {/* Filters */}
@@ -385,15 +393,21 @@ export default function PlacesOfWorshipPage() {
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setSelectedPlace(place.id); setIsViewOpen(true); }}> <Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setSelectedPlace(place.id); setIsViewOpen(true); }}>
<Eye className="h-3.5 w-3.5" /> <Eye className="h-3.5 w-3.5" />
</Button> </Button>
{canModify && (
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => handleEdit(place)}> <Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => handleEdit(place)}>
<Pencil className="h-3.5 w-3.5" /> <Pencil className="h-3.5 w-3.5" />
</Button> </Button>
)}
{canDistribute && (
<Button variant="secondary" size="sm" className="h-7 px-2 text-xs rounded-lg" onClick={() => { setSelectedPlace(place.id); setIsAidOpen(true); }}> <Button variant="secondary" size="sm" className="h-7 px-2 text-xs rounded-lg" onClick={() => { setSelectedPlace(place.id); setIsAidOpen(true); }}>
Bantuan Bantuan
</Button> </Button>
)}
{canModify && (
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => { if (confirm("Hapus rumah ibadah ini?")) deleteMutation.mutate({ id: place.id }); }}> <Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => { if (confirm("Hapus rumah ibadah ini?")) deleteMutation.mutate({ id: place.id }); }}>
<Trash2 className="h-3.5 w-3.5" /> <Trash2 className="h-3.5 w-3.5" />
</Button> </Button>
)}
</div> </div>
</div> </div>
</CardContent> </CardContent>
+11
View File
@@ -78,6 +78,11 @@ function parseRupiahInput(input: string) {
} }
export default function RecipientsPage() { 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 [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all");
const [isAddOpen, setIsAddOpen] = useState(false); const [isAddOpen, setIsAddOpen] = useState(false);
@@ -308,6 +313,7 @@ export default function RecipientsPage() {
Kelola data warga penerima bantuan sosial Kelola data warga penerima bantuan sosial
</p> </p>
</div> </div>
{canModify && (
<Dialog open={isAddOpen} onOpenChange={setIsAddOpen}> <Dialog open={isAddOpen} onOpenChange={setIsAddOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="rounded-xl" size="sm"> <Button className="rounded-xl" size="sm">
@@ -422,6 +428,7 @@ export default function RecipientsPage() {
</form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)}
</div> </div>
{/* Filters */} {/* Filters */}
@@ -512,6 +519,8 @@ export default function RecipientsPage() {
> >
<Eye className="h-3.5 w-3.5" /> <Eye className="h-3.5 w-3.5" />
</Button> </Button>
{canModify && (
<>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
@@ -532,6 +541,8 @@ export default function RecipientsPage() {
> >
<Trash2 className="h-3.5 w-3.5" /> <Trash2 className="h-3.5 w-3.5" />
</Button> </Button>
</>
)}
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>