Compare commits
1 Commits
nico/3-sep
...
nico/4-9-2
| Author | SHA1 | Date | |
|---|---|---|---|
| 8817b937b1 |
@@ -57,6 +57,8 @@
|
||||
"form-data": "^4.0.2",
|
||||
"framer-motion": "^12.23.5",
|
||||
"get-port": "^7.1.0",
|
||||
"iron-session": "^8.0.4",
|
||||
"jose": "^6.1.0",
|
||||
"jotai": "^2.12.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"leaflet": "^1.9.4",
|
||||
@@ -64,7 +66,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"motion": "^12.4.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"next": "15.1.6",
|
||||
"next": "^15.5.2",
|
||||
"next-view-transitions": "^0.3.4",
|
||||
"node-fetch": "^3.3.2",
|
||||
"p-limit": "^6.2.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"id": "role_admin_desa",
|
||||
"name": "ADMIN_DESA",
|
||||
"id": "1",
|
||||
"name": "ADMIN DESA",
|
||||
"description": "Administrator Desa",
|
||||
"permissions": ["manage_users", "manage_content", "view_reports"],
|
||||
"isActive": true,
|
||||
@@ -9,8 +9,8 @@
|
||||
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "role_admin_kesehatan",
|
||||
"name": "ADMIN_KESEHATAN",
|
||||
"id": "2",
|
||||
"name": "ADMIN KESEHATAN",
|
||||
"description": "Administrator Bidang Kesehatan",
|
||||
"permissions": ["manage_health_data", "view_reports"],
|
||||
"isActive": true,
|
||||
@@ -18,8 +18,8 @@
|
||||
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "role_admin_sekolah",
|
||||
"name": "ADMIN_SEKOLAH",
|
||||
"id": "3",
|
||||
"name": "ADMIN SEKOLAH",
|
||||
"description": "Administrator Sekolah",
|
||||
"permissions": ["manage_school_data", "view_reports"],
|
||||
"isActive": true,
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
[
|
||||
{
|
||||
"id": "user_admin_desa",
|
||||
"id": "1",
|
||||
"nama": "Admin Desa",
|
||||
"email": "admin.desa@example.com",
|
||||
"password": "$2b$10$XFDWYOJFxQ7ZM5bA0N4Z0O8u0eKYv58wLsaR7h6XK9bqWJ1YQJQ9q",
|
||||
"roleId": "role_admin_desa",
|
||||
"nomor": "089647037426",
|
||||
"roleId": "1",
|
||||
"isActive": true,
|
||||
"lastLogin": "2025-08-31T10:00:00.000Z",
|
||||
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "user_admin_puskesmas",
|
||||
"id": "2",
|
||||
"nama": "Admin Kesehatan",
|
||||
"email": "admin.kesehatan@example.com",
|
||||
"password": "$2b$10$XFDWYOJFxQ7ZM5bA0N4Z0O8u0eKYv58wLsaR7h6XK9bqWJ1YQJQ9q",
|
||||
"roleId": "role_admin_kesehatan",
|
||||
"nomor": "082339004198",
|
||||
"roleId": "2",
|
||||
"isActive": true,
|
||||
"lastLogin": null,
|
||||
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "user_admin_sekolah",
|
||||
"id": "3",
|
||||
"nama": "Admin Sekolah",
|
||||
"email": "admin.sekolah@example.com",
|
||||
"password": "$2b$10$XFDWYOJFxQ7ZM5bA0N4Z0O8u0eKYv58wLsaR7h6XK9bqWJ1YQJQ9q",
|
||||
"roleId": "role_admin_sekolah",
|
||||
"nomor": "085237157222",
|
||||
"roleId": "3",
|
||||
"isActive": true,
|
||||
"lastLogin": null,
|
||||
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||
|
||||
@@ -2103,14 +2103,16 @@ model KategoriBuku {
|
||||
DataPerpustakaan DataPerpustakaan[]
|
||||
}
|
||||
|
||||
// ========================================= USER ========================================= //
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
nama String
|
||||
email String @unique
|
||||
password String?
|
||||
username String
|
||||
nomor String @unique
|
||||
role Role @relation(fields: [roleId], references: [id])
|
||||
roleId String
|
||||
instansi String? // Nama instansi (Puskesmas, Sekolah, dll)
|
||||
roleId String @default("1")
|
||||
instansi String?
|
||||
UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
|
||||
isActive Boolean @default(true)
|
||||
lastLogin DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
@@ -2132,6 +2134,15 @@ model Role {
|
||||
@@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
|
||||
model Permission {
|
||||
id String @id @default(cuid())
|
||||
@@ -2143,6 +2154,17 @@ model Permission {
|
||||
@@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 ========================================= //
|
||||
model DataPendidikan {
|
||||
id String @id @default(cuid())
|
||||
|
||||
@@ -57,47 +57,60 @@ import users from "./data/user/users.json";
|
||||
|
||||
(async () => {
|
||||
// =========== USER & ROLE ===========
|
||||
for (const r of roles) {
|
||||
await prisma.role.upsert({
|
||||
where: { id: r.id },
|
||||
update: {
|
||||
name: r.name,
|
||||
description: r.description,
|
||||
permissions: r.permissions,
|
||||
isActive: true,
|
||||
},
|
||||
create: {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
description: r.description,
|
||||
permissions: r.permissions,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
// In your seed.ts
|
||||
// =========== ROLES ===========
|
||||
console.log("🔄 Seeding roles...");
|
||||
for (const r of roles) {
|
||||
await prisma.role.upsert({
|
||||
where: { id: r.id },
|
||||
update: {
|
||||
name: r.name,
|
||||
description: r.description,
|
||||
permissions: r.permissions,
|
||||
isActive: r.isActive,
|
||||
},
|
||||
create: {
|
||||
id: r.id,
|
||||
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
|
||||
for (const u of users) {
|
||||
await prisma.user.upsert({
|
||||
where: { id: u.id },
|
||||
update: {
|
||||
nama: u.nama,
|
||||
email: u.email,
|
||||
password: u.password,
|
||||
roleId: u.roleId,
|
||||
isActive: true,
|
||||
},
|
||||
create: {
|
||||
id: u.id,
|
||||
nama: u.nama,
|
||||
email: u.email,
|
||||
password: u.password,
|
||||
roleId: u.roleId,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Users seeded");
|
||||
|
||||
await prisma.user.upsert({
|
||||
where: { id: u.id },
|
||||
update: {
|
||||
username: u.nama,
|
||||
nomor: u.nomor,
|
||||
roleId: u.roleId,
|
||||
isActive: u.isActive,
|
||||
},
|
||||
create: {
|
||||
id: u.id,
|
||||
username: u.nama,
|
||||
nomor: u.nomor,
|
||||
roleId: u.roleId,
|
||||
isActive: u.isActive,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Users seeded");
|
||||
// =========== LANDING PAGE ===========
|
||||
// =========== SUBMENU PROFILE ===========
|
||||
// =========== PROFILE PEJABAT DESA ===========
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { IconForms, IconUser } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
@@ -50,6 +51,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
User & Role
|
||||
</Title>
|
||||
<Tabs
|
||||
color={colors['blue-button']}
|
||||
variant="pills"
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
|
||||
@@ -88,12 +88,13 @@ function ListRole({ search }: { search: string }) {
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.name}</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} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color='red'
|
||||
disabled={listDataState.delete.loading}
|
||||
onClick={() => {
|
||||
|
||||
@@ -85,11 +85,11 @@ function ListUser({ search }: { search: string }) {
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<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 style={{ width: '20%', }}>
|
||||
<Text truncate fz="sm" c="dimmed">
|
||||
{item.email}
|
||||
{item.nomor}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '20%', }}>
|
||||
|
||||
15
src/app/admin/auth/_lib/api_fetch_auth.ts
Normal file
15
src/app/admin/auth/_lib/api_fetch_auth.ts
Normal 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);
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
|
||||
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 '../darmasaba/(pages)/desa/layanan/_com/BackButto';
|
||||
|
||||
|
||||
function Page() {
|
||||
// const router = useRouter()
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Checkbox, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import BackButton from '../darmasaba/(pages)/desa/layanan/_com/BackButto';
|
||||
|
||||
|
||||
|
||||
|
||||
50
src/app/api/[[...slugs]]/_lib/auth/_lib/decrypt.ts
Normal file
50
src/app/api/[[...slugs]]/_lib/auth/_lib/decrypt.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
26
src/app/api/[[...slugs]]/_lib/auth/_lib/encrypt.ts
Normal file
26
src/app/api/[[...slugs]]/_lib/auth/_lib/encrypt.ts
Normal 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
|
||||
13
src/app/api/[[...slugs]]/_lib/auth/_lib/get_KodeOtp_By_Id.ts
Normal file
13
src/app/api/[[...slugs]]/_lib/auth/_lib/get_KodeOtp_By_Id.ts
Normal 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;
|
||||
}
|
||||
4
src/app/api/[[...slugs]]/_lib/auth/_lib/randomOTP.ts
Normal file
4
src/app/api/[[...slugs]]/_lib/auth/_lib/randomOTP.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export function randomOTP() {
|
||||
const random = Math.floor(Math.random() * (9000 - 1000 )) + 1000
|
||||
return random;
|
||||
}
|
||||
36
src/app/api/[[...slugs]]/_lib/auth/_lib/session_create.ts
Normal file
36
src/app/api/[[...slugs]]/_lib/auth/_lib/session_create.ts
Normal 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
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
}
|
||||
63
src/app/api/[[...slugs]]/_lib/auth/login/route.ts
Normal file
63
src/app/api/[[...slugs]]/_lib/auth/login/route.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
38
src/app/api/[[...slugs]]/_lib/auth/logout/route.ts
Normal file
38
src/app/api/[[...slugs]]/_lib/auth/logout/route.ts
Normal 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 }
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
62
src/app/api/[[...slugs]]/_lib/auth/register/route.ts
Normal file
62
src/app/api/[[...slugs]]/_lib/auth/register/route.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
71
src/app/api/[[...slugs]]/_lib/auth/resend/route.ts
Normal file
71
src/app/api/[[...slugs]]/_lib/auth/resend/route.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
78
src/app/api/[[...slugs]]/_lib/auth/validasi/route.ts
Normal file
78
src/app/api/[[...slugs]]/_lib/auth/validasi/route.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,11 @@ import { NavbarSearch } from "./NavBarSearch"
|
||||
import { NavbarSubMenu } from "./NavbarSubMenu"
|
||||
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[] }) {
|
||||
const { item, isSearch } = useSnapshot(stateNav)
|
||||
const router = useTransitionRouter()
|
||||
@@ -40,9 +45,11 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
||||
/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{listNavbar.map((item, k) => (
|
||||
<MenuItemCom key={k} item={item} />
|
||||
))}
|
||||
|
||||
<Tooltip label="Search content" position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
@@ -56,20 +63,25 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
||||
<IconSearch size="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="My Profile" position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
next.push("/admin/landing-page/profile/program-inovasi")
|
||||
}}
|
||||
color={colors["blue-button"]}
|
||||
radius="xl"
|
||||
variant="light"
|
||||
>
|
||||
<IconUser size={22} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{/* hanya tampil kalau role = admin */}
|
||||
{stateAuth.role === "admin" && (
|
||||
<Tooltip label="My Profile" position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
next.push("/admin/landing-page/profile/program-inovasi")
|
||||
}}
|
||||
color={colors["blue-button"]}
|
||||
radius="xl"
|
||||
variant="light"
|
||||
>
|
||||
<IconUser size={22} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
</Container>
|
||||
|
||||
{item && <NavbarSubMenu item={item as MenuItem[]} />}
|
||||
{isSearch && <NavbarSearch />}
|
||||
</Stack>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
5
src/lib/router/router_home.ts
Normal file
5
src/lib/router/router_home.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const RouterHome = {
|
||||
main_home: "/admin",
|
||||
home_user_non_active: "/admin/admin-not-active",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user