This commit is contained in:
2025-09-04 11:46:08 +08:00
parent 2adf60f9eb
commit 8817b937b1
29 changed files with 593 additions and 374 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -57,6 +57,8 @@
"form-data": "^4.0.2", "form-data": "^4.0.2",
"framer-motion": "^12.23.5", "framer-motion": "^12.23.5",
"get-port": "^7.1.0", "get-port": "^7.1.0",
"iron-session": "^8.0.4",
"jose": "^6.1.0",
"jotai": "^2.12.3", "jotai": "^2.12.3",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
@@ -64,7 +66,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"motion": "^12.4.1", "motion": "^12.4.1",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"next": "15.1.6", "next": "^15.5.2",
"next-view-transitions": "^0.3.4", "next-view-transitions": "^0.3.4",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"p-limit": "^6.2.0", "p-limit": "^6.2.0",

View File

@@ -1,7 +1,7 @@
[ [
{ {
"id": "role_admin_desa", "id": "1",
"name": "ADMIN_DESA", "name": "ADMIN DESA",
"description": "Administrator Desa", "description": "Administrator Desa",
"permissions": ["manage_users", "manage_content", "view_reports"], "permissions": ["manage_users", "manage_content", "view_reports"],
"isActive": true, "isActive": true,
@@ -9,8 +9,8 @@
"updatedAt": "2025-09-01T00:00:00.000Z" "updatedAt": "2025-09-01T00:00:00.000Z"
}, },
{ {
"id": "role_admin_kesehatan", "id": "2",
"name": "ADMIN_KESEHATAN", "name": "ADMIN KESEHATAN",
"description": "Administrator Bidang Kesehatan", "description": "Administrator Bidang Kesehatan",
"permissions": ["manage_health_data", "view_reports"], "permissions": ["manage_health_data", "view_reports"],
"isActive": true, "isActive": true,
@@ -18,8 +18,8 @@
"updatedAt": "2025-09-01T00:00:00.000Z" "updatedAt": "2025-09-01T00:00:00.000Z"
}, },
{ {
"id": "role_admin_sekolah", "id": "3",
"name": "ADMIN_SEKOLAH", "name": "ADMIN SEKOLAH",
"description": "Administrator Sekolah", "description": "Administrator Sekolah",
"permissions": ["manage_school_data", "view_reports"], "permissions": ["manage_school_data", "view_reports"],
"isActive": true, "isActive": true,

View File

@@ -1,32 +1,29 @@
[ [
{ {
"id": "user_admin_desa", "id": "1",
"nama": "Admin Desa", "nama": "Admin Desa",
"email": "admin.desa@example.com", "nomor": "089647037426",
"password": "$2b$10$XFDWYOJFxQ7ZM5bA0N4Z0O8u0eKYv58wLsaR7h6XK9bqWJ1YQJQ9q", "roleId": "1",
"roleId": "role_admin_desa",
"isActive": true, "isActive": true,
"lastLogin": "2025-08-31T10:00:00.000Z", "lastLogin": "2025-08-31T10:00:00.000Z",
"createdAt": "2025-09-01T00:00:00.000Z", "createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z" "updatedAt": "2025-09-01T00:00:00.000Z"
}, },
{ {
"id": "user_admin_puskesmas", "id": "2",
"nama": "Admin Kesehatan", "nama": "Admin Kesehatan",
"email": "admin.kesehatan@example.com", "nomor": "082339004198",
"password": "$2b$10$XFDWYOJFxQ7ZM5bA0N4Z0O8u0eKYv58wLsaR7h6XK9bqWJ1YQJQ9q", "roleId": "2",
"roleId": "role_admin_kesehatan",
"isActive": true, "isActive": true,
"lastLogin": null, "lastLogin": null,
"createdAt": "2025-09-01T00:00:00.000Z", "createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z" "updatedAt": "2025-09-01T00:00:00.000Z"
}, },
{ {
"id": "user_admin_sekolah", "id": "3",
"nama": "Admin Sekolah", "nama": "Admin Sekolah",
"email": "admin.sekolah@example.com", "nomor": "085237157222",
"password": "$2b$10$XFDWYOJFxQ7ZM5bA0N4Z0O8u0eKYv58wLsaR7h6XK9bqWJ1YQJQ9q", "roleId": "3",
"roleId": "role_admin_sekolah",
"isActive": true, "isActive": true,
"lastLogin": null, "lastLogin": null,
"createdAt": "2025-09-01T00:00:00.000Z", "createdAt": "2025-09-01T00:00:00.000Z",

View File

@@ -2103,14 +2103,16 @@ model KategoriBuku {
DataPerpustakaan DataPerpustakaan[] DataPerpustakaan DataPerpustakaan[]
} }
// ========================================= USER ========================================= //
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
nama String username String
email String @unique nomor String @unique
password String?
role Role @relation(fields: [roleId], references: [id]) role Role @relation(fields: [roleId], references: [id])
roleId String roleId String @default("1")
instansi String? // Nama instansi (Puskesmas, Sekolah, dll) instansi String?
UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
isActive Boolean @default(true) isActive Boolean @default(true)
lastLogin DateTime? lastLogin DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -2132,6 +2134,15 @@ model Role {
@@map("roles") @@map("roles")
} }
model KodeOtp {
id String @id @default(cuid())
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
nomor String
otp Int
}
// Tabel untuk menyimpan permission // Tabel untuk menyimpan permission
model Permission { model Permission {
id String @id @default(cuid()) id String @id @default(cuid())
@@ -2143,6 +2154,17 @@ model Permission {
@@map("permissions") @@map("permissions")
} }
model UserSession {
id String @id @default(cuid())
token String
expires DateTime?
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
User User @relation(fields: [userId], references: [id])
userId String @unique
}
// ========================================= DATA PENDIDIKAN ========================================= // // ========================================= DATA PENDIDIKAN ========================================= //
model DataPendidikan { model DataPendidikan {
id String @id @default(cuid()) id String @id @default(cuid())

View File

@@ -57,47 +57,60 @@ import users from "./data/user/users.json";
(async () => { (async () => {
// =========== USER & ROLE =========== // =========== USER & ROLE ===========
for (const r of roles) { // In your seed.ts
await prisma.role.upsert({ // =========== ROLES ===========
where: { id: r.id }, console.log("🔄 Seeding roles...");
update: { for (const r of roles) {
name: r.name, await prisma.role.upsert({
description: r.description, where: { id: r.id },
permissions: r.permissions, update: {
isActive: true, name: r.name,
}, description: r.description,
create: { permissions: r.permissions,
id: r.id, isActive: r.isActive,
name: r.name, },
description: r.description, create: {
permissions: r.permissions, id: r.id,
isActive: true, name: r.name,
}, description: r.description,
}); permissions: r.permissions,
isActive: r.isActive,
},
});
}
console.log("✅ Roles seeded");
// =========== USERS ===========
console.log("🔄 Seeding users...");
for (const u of users) {
// First verify the role exists
const roleExists = await prisma.role.findUnique({
where: { id: u.roleId }
});
if (!roleExists) {
console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
continue;
} }
console.log("✅ Roles seeded");
//users await prisma.user.upsert({
for (const u of users) { where: { id: u.id },
await prisma.user.upsert({ update: {
where: { id: u.id }, username: u.nama,
update: { nomor: u.nomor,
nama: u.nama, roleId: u.roleId,
email: u.email, isActive: u.isActive,
password: u.password, },
roleId: u.roleId, create: {
isActive: true, id: u.id,
}, username: u.nama,
create: { nomor: u.nomor,
id: u.id, roleId: u.roleId,
nama: u.nama, isActive: u.isActive,
email: u.email, },
password: u.password, });
roleId: u.roleId, }
isActive: true, console.log("✅ Users seeded");
},
});
}
console.log("✅ Users seeded");
// =========== LANDING PAGE =========== // =========== LANDING PAGE ===========
// =========== SUBMENU PROFILE =========== // =========== SUBMENU PROFILE ===========
// =========== PROFILE PEJABAT DESA =========== // =========== PROFILE PEJABAT DESA ===========

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconForms, IconUser } from '@tabler/icons-react'; import { IconForms, IconUser } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
@@ -50,6 +51,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
User & Role User & Role
</Title> </Title>
<Tabs <Tabs
color={colors['blue-button']}
variant="pills" variant="pills"
value={activeTab} value={activeTab}
onChange={handleTabChange} onChange={handleTabChange}

View File

@@ -88,12 +88,13 @@ function ListRole({ search }: { search: string }) {
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.name}</TableTd> <TableTd>{item.name}</TableTd>
<TableTd> <TableTd>
<Button color='green' onClick={() => router.push(`/admin/user&role/role/${item.id}`)}> <Button variant="light" color='green' onClick={() => router.push(`/admin/user&role/role/${item.id}`)}>
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light"
color='red' color='red'
disabled={listDataState.delete.loading} disabled={listDataState.delete.loading}
onClick={() => { onClick={() => {

View File

@@ -85,11 +85,11 @@ function ListUser({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '25%', }}> <TableTd style={{ width: '25%', }}>
<Text fw={500} truncate="end" lineClamp={1}>{item.nama}</Text> <Text fw={500} truncate="end" lineClamp={1}>{item.username}</Text>
</TableTd> </TableTd>
<TableTd style={{ width: '20%', }}> <TableTd style={{ width: '20%', }}>
<Text truncate fz="sm" c="dimmed"> <Text truncate fz="sm" c="dimmed">
{item.email} {item.nomor}
</Text> </Text>
</TableTd> </TableTd>
<TableTd style={{ width: '20%', }}> <TableTd style={{ width: '20%', }}>

View File

@@ -0,0 +1,15 @@
export {
apiFetchLogin
};
const apiFetchLogin = async ({ nomor }: { nomor: string }) => {
const respone = await fetch("/api/auth/login", {
method: "POST",
body: JSON.stringify({ nomor: nomor }),
headers: {
"Content-Type": "application/json",
},
});
return await respone.json().catch(() => null);
};

View File

@@ -1,9 +1,10 @@
'use client' 'use client'
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Flex, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Center, Flex, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconUserFilled } from '@tabler/icons-react'; import { IconUserFilled } from '@tabler/icons-react';
import Link from 'next/link'; import Link from 'next/link';
import BackButton from '../darmasaba/(pages)/desa/layanan/_com/BackButto';
function Page() { function Page() {
// const router = useRouter() // const router = useRouter()

View File

@@ -1,7 +1,8 @@
'use client' 'use client'
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Checkbox, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Center, Checkbox, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import BackButton from '../darmasaba/(pages)/desa/layanan/_com/BackButto';

View File

@@ -0,0 +1,50 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { jwtVerify } from "jose";
export async function decrypt({
token,
encodedKey,
}: {
token: string;
encodedKey: string;
}): Promise<Record<string, any> | null> {
if (!token || !encodedKey) {
console.error("Missing required parameters:", {
hasToken: !!token,
hasEncodedKey: !!encodedKey,
});
return null;
}
try {
const enc = new TextEncoder().encode(encodedKey);
const { payload } = await jwtVerify(token, enc, {
algorithms: ["HS256"],
});
if (!payload || !payload.user) {
console.error("Invalid payload structure:", {
hasPayload: !!payload,
hasUser: payload ? !!payload.user : false,
});
return null;
}
// Logging untuk debug
// console.log("Decrypt successful:", {
// payloadExists: !!payload,
// userExists: !!payload.user,
// tokenPreview: token.substring(0, 10) + "...",
// });
return payload.user as Record<string, any>;
} catch (error) {
console.error("Token verification failed:", {
error,
tokenLength: token?.length,
errorName: error instanceof Error ? error.name : "Unknown error",
errorMessage: error instanceof Error ? error.message : String(error),
});
return null;
}
}

View File

@@ -0,0 +1,26 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SignJWT } from "jose";
export async function encrypt({
user,
exp = "7 year",
encodedKey,
}: {
user: Record<string, any>;
exp?: string;
encodedKey: string;
}): Promise<string | null> {
try {
const enc = new TextEncoder().encode(encodedKey);
return new SignJWT({ user })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime(exp)
.sign(enc);
} catch (error) {
console.error("Gagal mengenkripsi", error);
return null;
}
}
// wibu:0.2.82

View File

@@ -0,0 +1,13 @@
"use server";
import prisma from "@/lib/prisma";
export async function auth_getCodeOtpByNumber({kodeId}: {kodeId: string}) {
const data = await prisma.kodeOtp.findFirst({
where: {
id: kodeId,
},
});
return data;
}

View File

@@ -0,0 +1,4 @@
export function randomOTP() {
const random = Math.floor(Math.random() * (9000 - 1000 )) + 1000
return random;
}

View File

@@ -0,0 +1,36 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { cookies } from "next/headers";
import { encrypt } from "./encrypt";
export async function sessionCreate({
sessionKey,
exp = "7 year",
encodedKey,
user,
}: {
sessionKey: string;
exp?: string;
encodedKey: string;
user: Record<string, unknown>;
}) {
const token = await encrypt({
exp,
encodedKey,
user,
});
const cookie: any = {
key: sessionKey,
value: token,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
},
};
(await cookies()).set(cookie.key, cookie.value, { ...cookie.options });
return token;
}
// wibu:0.2.82

View File

@@ -1,81 +0,0 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
// ENV atau secret key untuk token
const JWT_SECRET = process.env.JWT_SECRET || "super-secret-key"; // ganti di env production
type LoginForm = {
email: string;
password: string;
};
export default async function userLogin(context: Context) {
const body = (await context.body) as LoginForm;
try {
// 1. Cari user berdasarkan email
const user = await prisma.user.findUnique({
where: { email: body.email },
include: { role: true }, // include role untuk otorisasi
});
// 2. Jika tidak ada user
if (!user) {
return {
success: false,
message: "Email tidak ditemukan",
};
}
// 3. Cek apakah user aktif
if (!user.isActive) {
return {
success: false,
message: "Akun tidak aktif",
};
}
// 4. Verifikasi password
const isMatch = await bcrypt.compare(body.password, user.password);
if (!isMatch) {
return {
success: false,
message: "Password salah",
};
}
// 5. Buat JWT token
const token = jwt.sign(
{
id: user.id,
email: user.email,
role: user.role.name,
},
JWT_SECRET,
{ expiresIn: "7d" } // expire 7 hari
);
// 6. Kirim response
return {
success: true,
message: "Login berhasil",
data: {
user: {
id: user.id,
nama: user.nama,
email: user.email,
role: user.role.name,
},
token,
},
};
} catch (error) {
console.error("Login error:", error);
return {
success: false,
message: "Terjadi kesalahan saat login",
};
}
}

View File

@@ -0,0 +1,63 @@
import prisma from "@/lib/prisma";
import { NextResponse } from "next/server";
import { randomOTP } from "../_lib/randomOTP";
export async function POST(req: Request) {
if (req.method !== "POST") {
return NextResponse.json(
{ success: false, message: "Method Not Allowed" },
{ status: 405 }
);
}
try {
const codeOtp = randomOTP();
const body = await req.json();
const { nomor } = body;
const res = await fetch(
`https://wa.wibudev.com/code?nom=${nomor}&text=Website Desa Darmasaba - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun Admin lainnya.
\n
>> Kode OTP anda: ${codeOtp}.
`
);
const sendWa = await res.json();
if (sendWa.status !== "success")
return NextResponse.json(
{ success: false, message: "Nomor Whatsapp Tidak Aktif" },
{ status: 400 }
);
const createOtpId = await prisma.kodeOtp.create({
data: {
nomor: nomor,
otp: codeOtp,
},
});
if (!createOtpId)
return NextResponse.json(
{ success: false, message: "Gagal mengirim kode OTP" },
{ status: 400 }
);
return NextResponse.json(
{
success: true,
message: "Kode verifikasi terkirim",
kodeId: createOtpId.id,
},
{ status: 200 }
);
} catch (error) {
console.log("Error Login", error);
return NextResponse.json(
{ success: false, message: "Terjadi masalah saat login" , reason: error as Error },
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -0,0 +1,38 @@
// import { cookies } from "next/headers";
// import { NextResponse } from "next/server";
// export const dynamic = "force-dynamic";
// export async function GET() {
// const sessionKey = process.env.NEXT_PUBLIC_BASE_SESSION_KEY!; // Gunakan environment variable yang tidak diekspos ke client-side
// if (!sessionKey) {
// return NextResponse.json(
// { success: false, message: "Session key tidak ditemukan" },
// { status: 500 }
// );
// }
// const cookieStore = cookies();
// const sessionCookie = cookieStore.get(sessionKey);
// if (!sessionCookie) {
// return NextResponse.json(
// { success: false, message: "Session tidak ditemukan" },
// { status: 400 }
// );
// }
// try {
// cookieStore.delete(sessionKey);
// return NextResponse.json(
// { success: true, message: "Logout berhasil" },
// { status: 200 }
// );
// } catch (error) {
// console.error("Gagal menghapus cookie:", error);
// return NextResponse.json(
// { success: false, message: "Gagal melakukan logout" },
// { status: 500 }
// );
// }
// }

View File

@@ -1,88 +0,0 @@
import prisma from "@/lib/prisma";
import bcrypt from "bcryptjs";
import { Context } from "elysia";
interface RegisterBody {
nama: string;
email: string;
password: string;
}
export default async function userRegister(context: Context) {
try {
const body = (await context.body) as RegisterBody;
// Validasi input
if (!body.nama || !body.email || !body.password) {
context.set.status = 400;
return {
success: false,
message: "Semua field harus diisi",
data: null
};
}
// Cek email sudah terdaftar
const existingUser = await prisma.user.findUnique({
where: { email: body.email },
});
if (existingUser) {
context.set.status = 400;
return {
success: false,
message: "Email sudah terdaftar",
data: null
};
}
// Dapatkan role warga
const role = await prisma.role.findFirst({
where: { name: "warga" }
});
if (!role) {
context.set.status = 500;
return {
success: false,
message: "Role warga tidak ditemukan",
data: null
};
}
// Hash password
const hashedPassword = await bcrypt.hash(body.password, 10);
// Buat user baru
const user = await prisma.user.create({
data: {
nama: body.nama,
email: body.email,
password: hashedPassword,
roleId: role.id,
},
select: {
id: true,
nama: true,
email: true,
roleId: true,
createdAt: true,
updatedAt: true
}
});
return {
success: true,
message: "Berhasil mendaftar",
data: user,
};
} catch (error) {
console.error("Registration error:", error);
context.set.status = 500;
return {
success: false,
message: "Terjadi kesalahan saat mendaftar",
data: null
};
}
}

View File

@@ -0,0 +1,62 @@
import prisma from "@/lib/prisma";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
if (req.method !== "POST") {
return NextResponse.json(
{ success: false, message: "Method Not Allowed" },
{ status: 405 }
);
}
try {
const { data } = await req.json();
const cekUsername = await prisma.user.findUnique({
where: {
username: data.username,
nomor: data.nomor,
},
});
if (cekUsername)
return NextResponse.json({
success: false,
message: "Username sudah digunakan",
});
const createUser = await prisma.user.create({
data: {
username: data.username,
nomor: data.nomor,
},
});
if (!createUser)
return NextResponse.json(
{ success: false, message: "Gagal Registrasi" },
{ status: 500 }
);
return NextResponse.json(
{
success: true,
message: "Registrasi Berhasil, Anda Sedang Login",
// data: createUser,
},
{ status: 201 }
);
} catch (error) {
console.error("Error registrasi:", error);
return NextResponse.json(
{
success: false,
message: "Maaf, Terjadi Keselahan",
reason: (error as Error).message,
},
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -0,0 +1,71 @@
import prisma from "@/lib/prisma";
import { randomOTP } from "../_lib/randomOTP";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
if (req.method !== "POST") {
return NextResponse.json(
{ success: false, message: "Method Not Allowed" },
{ status: 405 }
);
}
try {
const codeOtp = randomOTP();
const body = await req.json();
const { nomor } = body;
const res = await fetch(
`https://wa.wibudev.com/code?nom=${nomor}&text=HIPMI - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun pengurus HIPMI lainnya.
\n
>> Kode OTP anda: ${codeOtp}.
`
);
const sendWa = await res.json();
if (sendWa.status !== "success")
return NextResponse.json(
{
success: false,
message: "Nomor Whatsapp Tidak Aktif",
},
{ status: 400 }
);
const createOtpId = await prisma.kodeOtp.create({
data: {
nomor: nomor,
otp: codeOtp,
},
});
if (!createOtpId)
return NextResponse.json(
{
success: false,
message: "Gagal Membuat Kode OTP",
},
{ status: 400 }
);
return NextResponse.json(
{
success: true,
message: "Kode Verifikasi Dikirim",
kodeId: createOtpId.id,
},
{ status: 200 }
);
} catch (error) {
console.error(" Error Resend OTP", error);
return NextResponse.json(
{
success: false,
message: "Server Whatsapp Error !!",
},
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -0,0 +1,78 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { NextResponse } from "next/server";
import { sessionCreate } from "../_lib/session_create";
export async function POST(req: Request) {
if (req.method !== "POST") {
return NextResponse.json(
{ success: false, message: "Method Not Allowed" },
{ status: 405 }
);
}
try {
const { nomor } = await req.json();
const dataUser = await prisma.user.findUnique({
where: {
nomor: nomor,
},
select: {
id: true,
nomor: true,
username: true,
roleId: true,
},
});
if (dataUser == null)
return NextResponse.json(
{ success: false, message: "Nomor Belum Terdaftar" },
{ status: 200 }
);
const token = await sessionCreate({
sessionKey: process.env.NEXT_PUBLIC_BASE_SESSION_KEY!,
encodedKey: process.env.NEXT_PUBLIC_BASE_TOKEN_KEY!,
user: dataUser as any,
});
if (!token) {
return NextResponse.json(
{ success: false, message: "Gagal membuat session" },
{ status: 500 }
);
}
// Buat response dengan token dalam cookie
const response = NextResponse.json(
{
success: true,
message: "Berhasil Login",
roleId: dataUser.roleId,
},
{ status: 200 }
);
// Set cookie dengan token yang sudah dipastikan tidak null
response.cookies.set(process.env.NEXT_PUBLIC_BASE_SESSION_KEY!, token, {
path: "/",
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
maxAge: 30 * 24 * 60 * 60, // 30 hari dalam detik (1 bulan)
});
return response;
} catch (error) {
console.error("API Error or Server Error", error);
return NextResponse.json(
{
success: false,
message: "Maaf, Terjadi Keselahan",
reason: (error as Error).message,
},
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -12,6 +12,11 @@ import { NavbarSearch } from "./NavBarSearch"
import { NavbarSubMenu } from "./NavbarSubMenu" import { NavbarSubMenu } from "./NavbarSubMenu"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
// contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu)
const stateAuth = {
role: "admin", // coba ubah ke "user" buat test
}
export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) { export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
const { item, isSearch } = useSnapshot(stateNav) const { item, isSearch } = useSnapshot(stateNav)
const router = useTransitionRouter() const router = useTransitionRouter()
@@ -40,9 +45,11 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
/> />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
{listNavbar.map((item, k) => ( {listNavbar.map((item, k) => (
<MenuItemCom key={k} item={item} /> <MenuItemCom key={k} item={item} />
))} ))}
<Tooltip label="Search content" position="bottom" withArrow> <Tooltip label="Search content" position="bottom" withArrow>
<ActionIcon <ActionIcon
variant="transparent" variant="transparent"
@@ -56,20 +63,25 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
<IconSearch size="1.5rem" /> <IconSearch size="1.5rem" />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label="My Profile" position="bottom" withArrow>
<ActionIcon {/* hanya tampil kalau role = admin */}
onClick={() => { {stateAuth.role === "admin" && (
next.push("/admin/landing-page/profile/program-inovasi") <Tooltip label="My Profile" position="bottom" withArrow>
}} <ActionIcon
color={colors["blue-button"]} onClick={() => {
radius="xl" next.push("/admin/landing-page/profile/program-inovasi")
variant="light" }}
> color={colors["blue-button"]}
<IconUser size={22} /> radius="xl"
</ActionIcon> variant="light"
</Tooltip> >
<IconUser size={22} />
</ActionIcon>
</Tooltip>
)}
</Flex> </Flex>
</Container> </Container>
{item && <NavbarSubMenu item={item as MenuItem[]} />} {item && <NavbarSubMenu item={item as MenuItem[]} />}
{isSearch && <NavbarSearch />} {isSearch && <NavbarSearch />}
</Stack> </Stack>

View File

@@ -1,57 +0,0 @@
import colors from '@/con/colors';
import { Box, Button, Center, Flex, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconUserFilled } from '@tabler/icons-react';
import Link from 'next/link';
import BackButton from '../(pages)/desa/layanan/_com/BackButto';
function Page() {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Center>
<Image src={"/api/img/darmasaba-icon.png"} alt="" w={80} />
</Center>
<Box>
<Title ta={'center'} order={1} fw={'bold'} c={colors['blue-button']}>
E-Book Desa Darmasaba
</Title>
<Text ta={'center'} fz={'h4'} fw={'bold'} c={colors['blue-button']}>
Silahkan masukkan akun anda untuk menjelajahi berbagai macam buku di perpustakaan digital
</Text>
</Box>
</Box>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Group justify='center'>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Stack align='center'>
<Title order={2} fw={'bold'} c={colors['blue-button']}>
Login
</Title>
<IconUserFilled size={80} color={colors['blue-button']} />
<Box>
<Text c={colors['blue-button']} fw={'bold'}>Masuk Untuk Akses Lebih Banyak Buku</Text>
<TextInput placeholder='Email' />
<TextInput py={20} placeholder='Password' />
<Box pb={20} >
<Button component={Link} href={'/darmasaba/pendidikan/perpustakaan-digital'} fullWidth bg={colors['blue-button']} radius={'xl'}>Masuk</Button>
</Box>
<Flex justify={'center'} align={'center'}>
<Text>Belum punya akun? </Text>
<Button variant='transparent' component={Link} href={'/darmasaba/pendidikan/perpustakaan-digital/registrasi'}>
<Text c={colors['blue-button']} fw={'bold'}>Registrasi</Text>
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Group>
</Box>
</Stack>
);
}
export default Page;

View File

@@ -1,67 +0,0 @@
import colors from '@/con/colors';
import { Box, Button, Center, Checkbox, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import Link from 'next/link';
import BackButton from '../(pages)/desa/layanan/_com/BackButto';
function Page() {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Center>
<Image src={"/api/img/darmasaba-icon.png"} alt="" w={80} />
</Center>
<Box>
<Title ta={'center'} order={1} fw={'bold'} c={colors['blue-button']}>
E-Book Desa Darmasaba
</Title>
<Text ta={'center'} fz={'h4'} fw={'bold'} c={colors['blue-button']}>
Silahkan lengkapi data diri anda untuk mengakses e-book desa
</Text>
</Box>
</Box>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Group justify='center'>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Stack align='center'>
<Title order={2} fw={'bold'} c={colors['blue-button']}>
Registrasi
</Title>
<Box>
<TextInput placeholder='Nama Lengkap'
label='Nama Lengkap'
/>
<TextInput py={10} placeholder='Email'
label='Email'
/>
<TextInput placeholder='day / month / year'
label='Tanggal Lahir'
/>
<TextInput py={10} placeholder='08xx-xxxx-xxxx'
label='Nomor Telepon'
/>
<TextInput pb={10} placeholder='Password'
label='Password'
/>
<Box pb={10}>
<Checkbox
label="Saya menyetujui syarat dan ketentuan yang berlaku"
/>
</Box>
<Box pb={20} >
<Button component={Link} href={'/darmasaba/pendidikan/perpustakaan-digital'} fullWidth bg={colors['blue-button']} radius={'xl'}>Daftar</Button>
</Box>
</Box>
</Stack>
</Paper>
</Group>
</Box>
</Stack>
);
}
export default Page;

View File

@@ -0,0 +1,5 @@
export const RouterHome = {
main_home: "/admin",
home_user_non_active: "/admin/admin-not-active",
};