Compare commits
3 Commits
nico/fix-p
...
fix/admin/
| Author | SHA1 | Date | |
|---|---|---|---|
| b1d28a8322 | |||
| b86a3a85c3 | |||
| fd63bb0fd4 |
@@ -236,7 +236,7 @@ model PrestasiDesa {
|
||||
imageId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ model KategoriPrestasiDesa {
|
||||
name String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
PrestasiDesa PrestasiDesa[]
|
||||
}
|
||||
@@ -263,7 +263,7 @@ model Responden {
|
||||
kelompokUmurId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ model JenisKelaminResponden {
|
||||
name String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
Responden Responden[]
|
||||
}
|
||||
@@ -282,7 +282,7 @@ model PilihanRatingResponden {
|
||||
name String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
Responden Responden[]
|
||||
}
|
||||
@@ -292,7 +292,7 @@ model UmurResponden {
|
||||
name String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
Responden Responden[]
|
||||
}
|
||||
@@ -326,6 +326,7 @@ model PosisiOrganisasiPPID {
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
||||
children PosisiOrganisasiPPID[] @relation("Parent")
|
||||
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
||||
@@ -345,6 +346,7 @@ model PegawaiPPID {
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
|
||||
strukturOrganisasi StrukturPPID[] // Relasi balik
|
||||
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
||||
@@ -370,7 +372,7 @@ model VisiMisiPPID {
|
||||
misi String @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
@@ -381,7 +383,7 @@ model DasarHukumPPID {
|
||||
content String @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
@@ -398,7 +400,7 @@ model ProfilePPID {
|
||||
imageId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
@@ -410,7 +412,7 @@ model DaftarInformasiPublik {
|
||||
tanggal DateTime @db.Date
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
@@ -431,7 +433,7 @@ model PermohonanInformasiPublik {
|
||||
caraMemperolehSalinanInformasiId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
@@ -440,7 +442,7 @@ model JenisInformasiDiminta {
|
||||
name String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
||||
}
|
||||
@@ -450,7 +452,7 @@ model CaraMemperolehInformasi {
|
||||
name String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
||||
}
|
||||
@@ -460,7 +462,7 @@ model CaraMemperolehSalinanInformasi {
|
||||
name String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
||||
}
|
||||
@@ -474,7 +476,7 @@ model FormulirPermohonanKeberatan {
|
||||
alasan String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
@@ -531,7 +533,7 @@ model SejarahDesa {
|
||||
deskripsi String @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
@@ -541,7 +543,7 @@ model VisiMisiDesa {
|
||||
misi String @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
@@ -551,7 +553,7 @@ model LambangDesa {
|
||||
deskripsi String @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
@@ -562,7 +564,7 @@ model MaskotDesa {
|
||||
images ProfileDesaImage[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
|
||||
@@ -69,8 +69,8 @@ import { seedProfilPpd } from "./_seeder_list/ppid/profil-ppid/seed_profil_ppd";
|
||||
|
||||
(async () => {
|
||||
// Always run seedAssets to handle new images without duplication
|
||||
// console.log("📂 Checking for new assets to seed...");
|
||||
// await seedAssets();
|
||||
console.log("📂 Checking for new assets to seed...");
|
||||
await seedAssets();
|
||||
|
||||
// // =========== FILE STORAGE ===========
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ function Page() {
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
lh={{ base: 1.4, md: 1.4 }}
|
||||
>
|
||||
I.B. Surya Prabhawa Manuaba, S.H., M.H.
|
||||
{perbekel.nama || "I.B. Surya Prabhawa Manuaba, S.H., M.H."}
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
|
||||
@@ -354,7 +354,8 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
borderLeft: `2px solid ${tokens.colors.primary}`,
|
||||
}),
|
||||
...(mounted && isChildActive && !isDark && {
|
||||
backgroundColor: tokens.colors.bg.hover,
|
||||
backgroundColor: 'rgba(25, 113, 194, 0.1)',
|
||||
borderLeft: `2px solid ${tokens.colors.primary}`,
|
||||
}),
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { requireAuth } from "@/lib/api-auth";
|
||||
|
||||
export default async function sejarahDesaFindFirst(request: Request) {
|
||||
// ✅ Authentication check
|
||||
const headers = new Headers(request.url);
|
||||
const authResult = await requireAuth({ headers });
|
||||
if (!authResult.authenticated) {
|
||||
return authResult.response;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the first active record
|
||||
const data = await prisma.sejarahDesa.findFirst({
|
||||
where: {
|
||||
isActive: true,
|
||||
deletedAt: null
|
||||
},
|
||||
orderBy: { createdAt: 'asc' } // Get the oldest one first
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "Data tidak ditemukan",
|
||||
}, {status: 404})
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
data,
|
||||
}, {status: 200})
|
||||
} catch (error) {
|
||||
console.error("Gagal mengambil data sejarah desa:", error)
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "Terjadi kesalahan saat mengambil data",
|
||||
}, {status: 500})
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import sejarahDesaFindById from "./find-by-id";
|
||||
import sejarahDesaUpdate from "./update";
|
||||
import sejarahDesaFindFirst from "./find-first";
|
||||
|
||||
const SejarahDesa = new Elysia({
|
||||
prefix: "/sejarah",
|
||||
tags: ["Desa/Profile"],
|
||||
})
|
||||
.get("/first", async (context) => {
|
||||
const response = await sejarahDesaFindFirst(new Request(context.request));
|
||||
return response;
|
||||
})
|
||||
.get("/:id", async (context) => {
|
||||
const response = await sejarahDesaFindById(new Request(context.request));
|
||||
return response;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { requireAuth } from "@/lib/api-auth";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function sejarahDesaUpdate(context: Context) {
|
||||
// ✅ Authentication check
|
||||
const authResult = await requireAuth(context);
|
||||
if (!authResult.authenticated) {
|
||||
return authResult.response;
|
||||
}
|
||||
|
||||
try {
|
||||
const id = context.params?.id as string;
|
||||
const body = await context.body as {
|
||||
|
||||
@@ -10,8 +10,7 @@ import {
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
useMantineColorScheme
|
||||
Text
|
||||
} from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import { Prisma } from "@prisma/client";
|
||||
@@ -24,8 +23,6 @@ type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: tr
|
||||
|
||||
function ModuleItem({ data }: { data: ProgramInovasiItem }) {
|
||||
const router = useTransitionRouter();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<motion.div whileHover={{ scale: 1.03 }}>
|
||||
@@ -37,7 +34,7 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="cursor-pointer transition-all"
|
||||
bg={isDark ? "dark.6" : "white"}
|
||||
bg="white"
|
||||
>
|
||||
<Center h={160}>
|
||||
{data.image?.link ? (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import colors from "@/con/colors";
|
||||
import { Box, Space, Stack } from "@mantine/core";
|
||||
|
||||
@@ -5,21 +7,20 @@ import { Navbar } from "@/app/darmasaba/_com/Navbar";
|
||||
import Footer from "./_com/Footer";
|
||||
|
||||
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Stack gap={0} bg={colors.grey[1]}>
|
||||
<Navbar />
|
||||
<Space h={{
|
||||
base: "3.9rem",
|
||||
md: "2.5rem"
|
||||
}} />
|
||||
<Box style={{
|
||||
overflow: "scroll"
|
||||
}}>
|
||||
{children}
|
||||
</Box>
|
||||
<Footer />
|
||||
</Stack>
|
||||
<Stack gap={0} bg={colors.grey[1]}>
|
||||
<Navbar />
|
||||
<Space h={{
|
||||
base: "3.9rem",
|
||||
md: "2.5rem"
|
||||
}} />
|
||||
<Box style={{
|
||||
overflow: "scroll"
|
||||
}}>
|
||||
{children}
|
||||
</Box>
|
||||
<Footer />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -98,16 +98,16 @@ export default function RootLayout({
|
||||
<html lang="id" {...mantineHtmlProps}>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<ColorSchemeScript />
|
||||
<ColorSchemeScript defaultColorScheme="light" />
|
||||
</head>
|
||||
<body>
|
||||
<MantineProvider theme={theme}>
|
||||
<MantineProvider theme={theme} defaultColorScheme="light">
|
||||
{children}
|
||||
<LoadDataFirstClient />
|
||||
<ToastContainer
|
||||
position="bottom-center"
|
||||
hideProgressBar
|
||||
style={{ zIndex: 9999 }}
|
||||
<ToastContainer
|
||||
position="bottom-center"
|
||||
hideProgressBar
|
||||
style={{ zIndex: 9999 }}
|
||||
/>
|
||||
</MantineProvider>
|
||||
</body>
|
||||
|
||||
84
src/lib/api-auth.ts
Normal file
84
src/lib/api-auth.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Authentication helper untuk API endpoints
|
||||
*
|
||||
* Usage:
|
||||
* import { requireAuth } from "@/lib/api-auth";
|
||||
*
|
||||
* export default async function myEndpoint(context: Context) {
|
||||
* const authResult = await requireAuth(context);
|
||||
* if (!authResult.authenticated) {
|
||||
* return authResult.response;
|
||||
* }
|
||||
* // Lanjut proses dengan authResult.user
|
||||
* }
|
||||
*/
|
||||
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
export type AuthResult =
|
||||
| { authenticated: true; user: any }
|
||||
| { authenticated: false; response: Response };
|
||||
|
||||
export async function requireAuth(context: any): Promise<AuthResult> {
|
||||
try {
|
||||
// Cek session dari cookies
|
||||
const session = await getSession();
|
||||
|
||||
if (!session || !session.user) {
|
||||
return {
|
||||
authenticated: false,
|
||||
response: new Response(JSON.stringify({
|
||||
success: false,
|
||||
message: "Unauthorized - Silakan login terlebih dahulu"
|
||||
}), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
// Check jika user masih aktif
|
||||
if (!session.user.isActive) {
|
||||
return {
|
||||
authenticated: false,
|
||||
response: new Response(JSON.stringify({
|
||||
success: false,
|
||||
message: "Akun Anda tidak aktif. Hubungi administrator."
|
||||
}), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
authenticated: true,
|
||||
user: session.user
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Auth error:", error);
|
||||
return {
|
||||
authenticated: false,
|
||||
response: new Response(JSON.stringify({
|
||||
success: false,
|
||||
message: "Authentication error"
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional auth - tidak error jika tidak authenticated
|
||||
* Berguna untuk endpoint yang bisa diakses public atau private
|
||||
*/
|
||||
export async function optionalAuth(context: any): Promise<any> {
|
||||
try {
|
||||
const session = await getSession();
|
||||
return session?.user || null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
68
src/lib/session.ts
Normal file
68
src/lib/session.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Session helper menggunakan iron-session
|
||||
*
|
||||
* Usage:
|
||||
* import { getSession } from "@/lib/session";
|
||||
*
|
||||
* const session = await getSession();
|
||||
* if (session?.user) {
|
||||
* // User authenticated
|
||||
* }
|
||||
*/
|
||||
|
||||
import { getIronSession } from 'iron-session';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export type SessionData = {
|
||||
user?: {
|
||||
id: string;
|
||||
name: string;
|
||||
roleId: number;
|
||||
menuIds?: string[] | null;
|
||||
isActive?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type Session = SessionData & {
|
||||
save: () => Promise<void>;
|
||||
destroy: () => Promise<void>;
|
||||
};
|
||||
|
||||
const SESSION_OPTIONS = {
|
||||
cookieName: 'desa-session',
|
||||
password: process.env.SESSION_PASSWORD || 'default-password-change-in-production',
|
||||
cookieOptions: {
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax' as const,
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||
},
|
||||
};
|
||||
|
||||
export async function getSession(): Promise<SessionData | null> {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const session = await getIronSession<SessionData>(
|
||||
cookieStore,
|
||||
SESSION_OPTIONS
|
||||
);
|
||||
|
||||
return session;
|
||||
} catch (error) {
|
||||
console.error('Session error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function destroySession(): Promise<void> {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const session = await getIronSession<SessionData>(
|
||||
cookieStore,
|
||||
SESSION_OPTIONS
|
||||
);
|
||||
await session.destroy();
|
||||
} catch (error) {
|
||||
console.error('Destroy session error:', error);
|
||||
}
|
||||
}
|
||||
@@ -21,17 +21,18 @@ import { proxy, useSnapshot } from 'valtio';
|
||||
|
||||
const STORAGE_KEY = 'darmasaba-admin-dark-mode';
|
||||
|
||||
// Initialize from localStorage or system preference
|
||||
// Initialize from localStorage or default to light mode
|
||||
const getInitialDarkMode = (): boolean => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
|
||||
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored !== null) {
|
||||
return stored === 'true';
|
||||
}
|
||||
|
||||
// Fallback to system preference
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
// Default to light mode for first-time users
|
||||
// System preference is NOT used as default to ensure consistent UX
|
||||
return false;
|
||||
};
|
||||
|
||||
class DarkModeStore {
|
||||
|
||||
Reference in New Issue
Block a user