tambah akaun dari tiap role
This commit is contained in:
+37
-6
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
@@ -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
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user