API Auth
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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 ===========
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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={() => {
|
||||||
|
|||||||
@@ -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%', }}>
|
||||||
|
|||||||
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'
|
'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()
|
||||||
@@ -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';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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 { 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>
|
||||||
|
|||||||
@@ -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