Merge pull request 'nico/26-nov-25' (#16) from nico/26-nov-25 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/16
This commit is contained in:
@@ -54,6 +54,7 @@
|
|||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"elysia": "^1.3.5",
|
"elysia": "^1.3.5",
|
||||||
|
|||||||
@@ -1,23 +1,32 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "role-1",
|
"id": "0",
|
||||||
"name": "ADMIN DESA",
|
"name": "DEVELOPER",
|
||||||
"description": "Administrator Desa",
|
"description": "Developer",
|
||||||
"permissions": ["manage_users", "manage_content", "view_reports"],
|
"isActive": true
|
||||||
"isActive": true
|
},
|
||||||
},
|
{
|
||||||
{
|
"id": "1",
|
||||||
"id": "role-2",
|
"name": "SUPER ADMIN",
|
||||||
"name": "ADMIN KESEHATAN",
|
"description": "Administrator",
|
||||||
"description": "Administrator Bidang Kesehatan",
|
"isActive": true
|
||||||
"permissions": ["manage_health_data", "view_reports"],
|
},
|
||||||
"isActive": true
|
{
|
||||||
},
|
"id": "2",
|
||||||
{
|
"name": "ADMIN DESA",
|
||||||
"id": "role-3",
|
"description": "Administrator Desa",
|
||||||
"name": "ADMIN SEKOLAH",
|
"isActive": true
|
||||||
"description": "Administrator Sekolah",
|
},
|
||||||
"permissions": ["manage_school_data", "view_reports"],
|
{
|
||||||
"isActive": true
|
"id": "3",
|
||||||
}
|
"name": "ADMIN KESEHATAN",
|
||||||
]
|
"description": "Administrator Bidang Kesehatan",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"name": "ADMIN PENDIDIKAN",
|
||||||
|
"description": "Administrator Bidang Pendidikan",
|
||||||
|
"isActive": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,23 +1,10 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "user-1",
|
"id": "cmie1o0zh0002vn132vtzg7hh",
|
||||||
"nama": "Admin Desa",
|
"username": "SuperAdmin-Nico",
|
||||||
"nomor": "089647037426",
|
"nomor": "6289647037426",
|
||||||
"roleId": "role-1",
|
"roleId": 0,
|
||||||
"isActive": true
|
"isActive": true,
|
||||||
},
|
"sessionInvalid": false
|
||||||
{
|
|
||||||
"id": "user-2",
|
|
||||||
"nama": "Admin Kesehatan",
|
|
||||||
"nomor": "082339004198",
|
|
||||||
"roleId": "role-2",
|
|
||||||
"isActive": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "user-3",
|
|
||||||
"nama": "Admin Sekolah",
|
|
||||||
"nomor": "085237157222",
|
|
||||||
"roleId": "role-3",
|
|
||||||
"isActive": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
1127
prisma/migrations/20251119062255_add_unique_username/migration.sql
Normal file
1127
prisma/migrations/20251119062255_add_unique_username/migration.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -183,41 +183,41 @@ model SdgsDesa {
|
|||||||
|
|
||||||
//========================================= APBDes ========================================= //
|
//========================================= APBDes ========================================= //
|
||||||
model APBDes {
|
model APBDes {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
tahun Int?
|
tahun Int?
|
||||||
name String? // misalnya: "APBDes Tahun 2025"
|
name String? // misalnya: "APBDes Tahun 2025"
|
||||||
deskripsi String?
|
deskripsi String?
|
||||||
jumlah String? // total keseluruhan (opsional, bisa juga dihitung dari items)
|
jumlah String? // total keseluruhan (opsional, bisa juga dihitung dari items)
|
||||||
items APBDesItem[]
|
items APBDesItem[]
|
||||||
image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
|
image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id])
|
file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id])
|
||||||
fileId String?
|
fileId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime? // opsional, tidak perlu default now()
|
deletedAt DateTime? // opsional, tidak perlu default now()
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
model APBDesItem {
|
model APBDesItem {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
kode String // contoh: "4", "4.1", "4.1.2"
|
kode String // contoh: "4", "4.1", "4.1.2"
|
||||||
uraian String // nama item, contoh: "Pendapatan Asli Desa", "Hasil Usaha"
|
uraian String // nama item, contoh: "Pendapatan Asli Desa", "Hasil Usaha"
|
||||||
anggaran Float // dalam satuan Rupiah (bisa DECIMAL di DB, tapi Float umum di TS/JS)
|
anggaran Float // dalam satuan Rupiah (bisa DECIMAL di DB, tapi Float umum di TS/JS)
|
||||||
realisasi Float
|
realisasi Float
|
||||||
selisih Float // realisasi - anggaran
|
selisih Float // realisasi - anggaran
|
||||||
persentase Float
|
persentase Float
|
||||||
tipe String? // (realisasi / anggaran) * 100
|
tipe String? // (realisasi / anggaran) * 100
|
||||||
level Int // 1 = kelompok utama, 2 = sub-kelompok, 3 = detail
|
level Int // 1 = kelompok utama, 2 = sub-kelompok, 3 = detail
|
||||||
parentId String? // untuk relasi hierarki
|
parentId String? // untuk relasi hierarki
|
||||||
parent APBDesItem? @relation("APBDesItemParent", fields: [parentId], references: [id])
|
parent APBDesItem? @relation("APBDesItemParent", fields: [parentId], references: [id])
|
||||||
children APBDesItem[] @relation("APBDesItemParent")
|
children APBDesItem[] @relation("APBDesItemParent")
|
||||||
apbdesId String
|
apbdesId String
|
||||||
apbdes APBDes @relation(fields: [apbdesId], references: [id])
|
apbdes APBDes @relation(fields: [apbdesId], references: [id])
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
@@index([kode])
|
@@index([kode])
|
||||||
@@index([level])
|
@@index([level])
|
||||||
@@ -2163,25 +2163,27 @@ enum StatusPeminjaman {
|
|||||||
// ========================================= USER ========================================= //
|
// ========================================= USER ========================================= //
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
username String
|
username String
|
||||||
nomor String @unique
|
nomor String @unique
|
||||||
role Role @relation(fields: [roleId], references: [id])
|
roleId String @default("2")
|
||||||
roleId String @default("1")
|
isActive Boolean @default(false)
|
||||||
instansi String?
|
sessionInvalid Boolean @default(false)
|
||||||
UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
|
lastLogin DateTime?
|
||||||
isActive Boolean @default(true)
|
createdAt DateTime @default(now())
|
||||||
lastLogin DateTime?
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
sessions UserSession[] // ✅ Relasi one-to-many
|
||||||
deletedAt DateTime?
|
role Role @relation(fields: [roleId], references: [id])
|
||||||
|
menuAccesses UserMenuAccess[]
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Role {
|
model Role {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH
|
name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH
|
||||||
description String?
|
description String?
|
||||||
permissions Json // Menyimpan permission dalam format JSON
|
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -2200,26 +2202,32 @@ model KodeOtp {
|
|||||||
otp Int
|
otp Int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tabel untuk menyimpan permission
|
|
||||||
model Permission {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
name String @unique
|
|
||||||
description String?
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
@@map("permissions")
|
|
||||||
}
|
|
||||||
|
|
||||||
model UserSession {
|
model UserSession {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
token String
|
token String @db.Text // ✅ JWT bisa panjang
|
||||||
expires DateTime?
|
expiresAt DateTime // ✅ Ubah jadi expiresAt (konsisten)
|
||||||
active Boolean @default(true)
|
active Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
User User @relation(fields: [userId], references: [id])
|
|
||||||
userId String @unique
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String // ✅ HAPUS @unique - user bisa punya multiple sessions
|
||||||
|
|
||||||
|
@@index([userId]) // ✅ Index untuk query cepat
|
||||||
|
@@index([token]) // ✅ Index untuk verify cepat
|
||||||
|
@@map("user_sessions")
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserMenuAccess {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
menuId String // ID menu (misal: "Landing Page", "Kesehatan")
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
|
@@unique([userId, menuId]) // Satu user tidak bisa punya akses menu yang sama dua kali
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= DATA PENDIDIKAN ========================================= //
|
// ========================================= DATA PENDIDIKAN ========================================= //
|
||||||
|
|||||||
@@ -54,50 +54,48 @@ import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-progr
|
|||||||
import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json";
|
import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json";
|
||||||
import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
|
import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
|
||||||
import roles from "./data/user/roles.json";
|
import roles from "./data/user/roles.json";
|
||||||
import users from "./data/user/users.json";
|
|
||||||
import fileStorage from "./data/file-storage.json";
|
import fileStorage from "./data/file-storage.json";
|
||||||
import jenjangPendidikan from "./data/pendidikan/info-sekolah/jenjang-pendidikan.json";
|
import jenjangPendidikan from "./data/pendidikan/info-sekolah/jenjang-pendidikan.json";
|
||||||
import seedAssets from "./seed_assets";
|
import seedAssets from "./seed_assets";
|
||||||
|
import users from "./data/user/users.json";
|
||||||
import { safeSeedUnique } from "./safeseedUnique";
|
import { safeSeedUnique } from "./safeseedUnique";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
// =========== USER & ROLE ===========
|
// =========== USER ===========
|
||||||
|
console.log("🔄 Seeding user...");
|
||||||
|
for (const u of users) {
|
||||||
|
await safeSeedUnique(
|
||||||
|
"user",
|
||||||
|
{ id: u.id },
|
||||||
|
{
|
||||||
|
username: u.username,
|
||||||
|
nomor: u.nomor,
|
||||||
|
roleId: u.roleId.toString(),
|
||||||
|
isActive: u.isActive,
|
||||||
|
sessionInvalid: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ Roles seeded");
|
||||||
|
// =========== ROLE ===========
|
||||||
// In your seed.ts
|
// In your seed.ts
|
||||||
// =========== ROLES ===========
|
// =========== ROLES ===========
|
||||||
console.log("🔄 Seeding roles...");
|
console.log("🔄 Seeding roles...");
|
||||||
for (const r of roles) {
|
for (const r of roles) {
|
||||||
await safeSeedUnique("role", { id: r.id }, {
|
await safeSeedUnique(
|
||||||
name: r.name,
|
"role",
|
||||||
description: r.description,
|
{ id: r.id },
|
||||||
permissions: r.permissions,
|
{
|
||||||
isActive: r.isActive,
|
name: r.name,
|
||||||
});
|
description: r.description,
|
||||||
|
isActive: r.isActive,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("✅ Roles seeded");
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
await safeSeedUnique("user", { id: u.id }, {
|
|
||||||
username: u.nama,
|
|
||||||
nomor: u.nomor,
|
|
||||||
roleId: u.roleId,
|
|
||||||
isActive: u.isActive,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Users seeded");
|
|
||||||
|
|
||||||
// =========== FILE STORAGE ===========
|
// =========== FILE STORAGE ===========
|
||||||
console.log("🔄 Seeding file storage...");
|
console.log("🔄 Seeding file storage...");
|
||||||
for (const f of fileStorage) {
|
for (const f of fileStorage) {
|
||||||
@@ -811,7 +809,9 @@ import { safeSeedUnique } from "./safeseedUnique";
|
|||||||
const flattenedPosisiBumdes = posisiOrganisasi.flat();
|
const flattenedPosisiBumdes = posisiOrganisasi.flat();
|
||||||
|
|
||||||
// ✅ Urutkan berdasarkan hierarki
|
// ✅ Urutkan berdasarkan hierarki
|
||||||
const sortedPosisiBumdes = flattenedPosisiBumdes.sort((a, b) => a.hierarki - b.hierarki);
|
const sortedPosisiBumdes = flattenedPosisiBumdes.sort(
|
||||||
|
(a, b) => a.hierarki - b.hierarki
|
||||||
|
);
|
||||||
|
|
||||||
for (const p of sortedPosisiBumdes) {
|
for (const p of sortedPosisiBumdes) {
|
||||||
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
|
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
|
||||||
@@ -891,7 +891,7 @@ import { safeSeedUnique } from "./safeseedUnique";
|
|||||||
// Add IDs to the kategoriKegiatan data
|
// Add IDs to the kategoriKegiatan data
|
||||||
const kategoriKegiatan = kategoriKegiatanData.map((k, index) => ({
|
const kategoriKegiatan = kategoriKegiatanData.map((k, index) => ({
|
||||||
...k,
|
...k,
|
||||||
id: `kategori-${index + 1}`
|
id: `kategori-${index + 1}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
for (const k of kategoriKegiatan) {
|
for (const k of kategoriKegiatan) {
|
||||||
@@ -1183,7 +1183,6 @@ import { safeSeedUnique } from "./safeseedUnique";
|
|||||||
|
|
||||||
// seed assets
|
// seed assets
|
||||||
await seedAssets();
|
await seedAssets();
|
||||||
|
|
||||||
})()
|
})()
|
||||||
.then(() => prisma.$disconnect())
|
.then(() => prisma.$disconnect())
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -90,42 +90,96 @@ const userState = proxy({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updateActive: {
|
deleteUser: {
|
||||||
loading: false,
|
loading: false,
|
||||||
async submit(id: string, isActive: boolean) {
|
|
||||||
this.loading = true;
|
async delete(id: string) {
|
||||||
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/user/updt`, {
|
userState.deleteUser.loading = true;
|
||||||
method: "PUT",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
const response = await fetch(`/api/user/delUser/${id}`, {
|
||||||
body: JSON.stringify({ id, isActive }),
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const result = await response.json();
|
||||||
if (res.status === 200 && data.success) {
|
|
||||||
toast.success(data.message);
|
if (response.ok && result?.success) {
|
||||||
userState.findMany.load(userState.findMany.page, 10, userState.findMany.search);
|
toast.success(result.message || "User berhasil dihapus permanen");
|
||||||
|
await userState.findMany.load(); // refresh list user setelah delete
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.message || "Gagal update status user");
|
toast.error(result?.message || "Gagal menghapus user");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
console.error(e);
|
console.error("Gagal delete user:", error);
|
||||||
toast.error("Gagal update status user");
|
toast.error("Terjadi kesalahan saat menghapus user");
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
userState.deleteUser.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
// Di file userState.ts atau dimana state user berada
|
||||||
|
|
||||||
|
update: {
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
async submit(payload: { id: string; isActive?: boolean; roleId?: string }) {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/user/updt`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (res.status === 200 && data.success) {
|
||||||
|
// ✅ Tampilkan pesan yang berbeda jika role berubah
|
||||||
|
if (data.roleChanged) {
|
||||||
|
toast.success(
|
||||||
|
`${data.message}\n\nUser akan logout otomatis dalam beberapa detik.`,
|
||||||
|
{
|
||||||
|
autoClose: 5000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.success(data.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh list
|
||||||
|
await userState.findMany.load(
|
||||||
|
userState.findMany.page,
|
||||||
|
10,
|
||||||
|
userState.findMany.search
|
||||||
|
);
|
||||||
|
|
||||||
|
return true; // ✅ Return success untuk handling di component
|
||||||
|
} else {
|
||||||
|
toast.error(data.message || "Gagal update user");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("❌ Error update user:", e);
|
||||||
|
toast.error("Gagal update user");
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const templateRole = z.object({
|
const templateRole = z.object({
|
||||||
name: z.string().min(1, "Nama harus diisi"),
|
name: z.string().min(1, "Nama harus diisi"),
|
||||||
permissions: z.array(z.string()).min(1, "Permission harus diisi"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultRole = {
|
const defaultRole = {
|
||||||
name: "",
|
name: "",
|
||||||
permissions: [] as string[],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const roleState = proxy({
|
const roleState = proxy({
|
||||||
@@ -237,7 +291,7 @@ const roleState = proxy({
|
|||||||
toast.warn("ID tidak valid");
|
toast.warn("ID tidak valid");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/role/${id}`, {
|
const response = await fetch(`/api/role/${id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -245,31 +299,25 @@ const roleState = proxy({
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
const data = result.data;
|
const data = result.data;
|
||||||
this.id = data.id;
|
|
||||||
this.form = {
|
// langsung set melalui root state, bukan this
|
||||||
|
roleState.update.id = data.id;
|
||||||
|
roleState.update.form = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
permissions: data.permissions,
|
|
||||||
};
|
};
|
||||||
return data; // Return the loaded data
|
|
||||||
} else {
|
return data;
|
||||||
throw new Error(result?.message || "Gagal memuat data");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading role:", error);
|
console.error("Error loading role:", error);
|
||||||
toast.error(
|
toast.error("Gagal memuat data");
|
||||||
error instanceof Error ? error.message : "Gagal memuat data"
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async update() {
|
async update() {
|
||||||
const cek = templateRole.safeParse(roleState.update.form);
|
const cek = templateRole.safeParse(roleState.update.form);
|
||||||
if (!cek.success) {
|
if (!cek.success) {
|
||||||
@@ -290,7 +338,6 @@ const roleState = proxy({
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: this.form.name,
|
name: this.form.name,
|
||||||
permissions: this.form.permissions,
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +1,98 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import { apiFetchLogin } from '@/app/admin/auth/_lib/api_fetch_auth';
|
import { apiFetchLogin } from '@/app/api/auth/_lib/api_fetch_auth';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Flex, Image, Paper, Stack, Text, Title } from '@mantine/core';
|
import { Box, Button, Center, Image, Paper, Stack, Title } from '@mantine/core';
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { PhoneInput } from "react-international-phone";
|
import { PhoneInput } from 'react-international-phone';
|
||||||
import "react-international-phone/style.css";
|
import 'react-international-phone/style.css';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function Login() {
|
function Login() {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const [phone, setPhone] = useState("")
|
const [phone, setPhone] = useState('');
|
||||||
const [isError, setError] = useState(false)
|
const [loading, setLoading] = useState(false);
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
|
|
||||||
|
// Login.tsx
|
||||||
async function onLogin() {
|
async function onLogin() {
|
||||||
const nomor = phone.substring(1);
|
const cleanPhone = phone.replace(/\D/g, '');
|
||||||
if (nomor.length <= 4) return setError(true)
|
if (cleanPhone.length < 10) {
|
||||||
|
toast.error('Nomor telepon tidak valid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await apiFetchLogin({ nomor: nomor })
|
const response = await apiFetchLogin({ nomor: cleanPhone });
|
||||||
if (response && response.success) {
|
|
||||||
localStorage.setItem("hipmi_auth_code_id", response.kodeId);
|
if (!response.success) {
|
||||||
toast.success(response.message);
|
toast.error(response.message || 'Gagal memproses login');
|
||||||
router.push("/validasi", { scroll: false });
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan nomor untuk register
|
||||||
|
localStorage.setItem('auth_nomor', cleanPhone);
|
||||||
|
|
||||||
|
if (response.isRegistered) {
|
||||||
|
// ✅ User lama: simpan kodeId & ke validasi
|
||||||
|
localStorage.setItem('auth_kodeId', response.kodeId);
|
||||||
|
router.push('/validasi');
|
||||||
} else {
|
} else {
|
||||||
setLoading(false);
|
// ❌ User baru: langsung ke registrasi (tanpa kodeId)
|
||||||
toast.error(response?.message);
|
router.push('/registrasi');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoading(false)
|
console.error('Error Login:', error);
|
||||||
console.log("Error Login", error)
|
toast.error('Terjadi kesalahan saat login');
|
||||||
toast.error("Terjadi kesalahan saat login")
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg}>
|
<Stack pos="relative" bg={colors.Bg}>
|
||||||
<Box px={{ base: 'md', md: 100 }} pb={50}>
|
<Box px={{ base: 'md', md: 100 }} pb={50}>
|
||||||
<Stack align='center' justify='center' h={"100vh"}>
|
<Stack align="center" justify="center" h="100vh">
|
||||||
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
|
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '100%', sm: 400 }}>
|
||||||
<Stack align='center' gap={"lg"}>
|
<Stack align="center" gap="lg">
|
||||||
<Box>
|
<Box>
|
||||||
<Title ta={"center"} order={2} fw={'bold'} c={colors['blue-button']}>
|
<Title ta="center" order={2} fw="bold" c={colors['blue-button']}>
|
||||||
Login
|
Login
|
||||||
</Title>
|
</Title>
|
||||||
<Center>
|
<Center>
|
||||||
<Image loading="lazy" src={"/darmasaba-icon.png"} alt="" w={80} />
|
<Image
|
||||||
|
loading="lazy"
|
||||||
|
src="/darmasaba-icon.png"
|
||||||
|
alt="Logo"
|
||||||
|
w={80}
|
||||||
|
h={80}
|
||||||
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box w="100%">
|
||||||
{/* <Box mb={10}>
|
|
||||||
<Text c={colors['blue-button']} ta={"center"} fz={"sm"} fw={'bold'}>Masuk Untuk Akses Admin</Text>
|
|
||||||
<TextInput
|
|
||||||
label='Username'
|
|
||||||
placeholder='Username'
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Box> */}
|
|
||||||
<PhoneInput
|
<PhoneInput
|
||||||
countrySelectorStyleProps={{
|
countrySelectorStyleProps={{
|
||||||
buttonStyle: {
|
buttonStyle: {
|
||||||
backgroundColor: colors['blue-button'],
|
backgroundColor: colors['blue-button'],
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
inputStyle={{ width: "100%"}}
|
inputStyle={{ width: '100%' }}
|
||||||
defaultCountry="id"
|
defaultCountry="id"
|
||||||
onChange={(val) => {
|
value={phone}
|
||||||
setPhone(val);
|
onChange={(val) => setPhone(val)}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isError ? (
|
<Box py={20}>
|
||||||
toast.error("Masukan nomor telepon anda")
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
<Box py={20} >
|
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
bg={colors['blue-button']}
|
bg={colors['blue-button']}
|
||||||
radius={'xl'}
|
radius="xl"
|
||||||
onClick={onLogin}
|
onClick={onLogin}
|
||||||
loading={loading ? true : false}
|
loading={loading}
|
||||||
>Masuk
|
>
|
||||||
|
Masuk
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex justify={'center'} align={'center'}>
|
|
||||||
<Text>Belum punya akun? </Text>
|
|
||||||
<Button variant='transparent' component={Link} href={'/registrasi'}>
|
|
||||||
<Text c={colors['blue-button']} fw={'bold'}>Registrasi</Text>
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -108,4 +102,4 @@ function Login() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Login;
|
export default Login;
|
||||||
@@ -1,113 +1,127 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
// app/registrasi/page.tsx
|
||||||
'use client'
|
'use client';
|
||||||
import { apiFetchRegister } from '@/app/admin/auth/_lib/api_fetch_auth';
|
|
||||||
|
import { apiFetchRegister } from '@/app/api/auth/_lib/api_fetch_auth';
|
||||||
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
|
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, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box, Button, Center, Checkbox, Image, Paper, Stack, Text, TextInput, Title,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { PhoneInput } from "react-international-phone";
|
import { PhoneInput } from 'react-international-phone';
|
||||||
import "react-international-phone/style.css";
|
import 'react-international-phone/style.css';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
function Registrasi() {
|
export default function Registrasi() {
|
||||||
const [phone, setPhone] = useState("")
|
const router = useRouter();
|
||||||
const router = useRouter()
|
const [username, setUsername] = useState('');
|
||||||
const [value, setValue] = useState("")
|
|
||||||
const [isValue, setIsValue] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [phone, setPhone] = useState(''); // ✅ tambahkan state untuk phone
|
||||||
|
|
||||||
async function onRegistarsi() {
|
// Ambil data dari localStorage (dari login)
|
||||||
if (value.length < 5) {
|
useEffect(() => {
|
||||||
toast.error("Username minimal 5 karakter!");
|
const storedNomor = localStorage.getItem('auth_nomor');
|
||||||
|
if (!storedNomor) {
|
||||||
|
toast.error('Akses tidak valid');
|
||||||
|
router.push('/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setPhone(storedNomor);
|
||||||
if (value.includes(" ")) {
|
}, [router]);
|
||||||
toast.error("Username tidak boleh ada spasi!");
|
|
||||||
|
const handleRegister = async () => {
|
||||||
|
if (!username || username.trim().length < 5) {
|
||||||
|
toast.error('Username minimal 5 karakter!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (username.includes(' ')) {
|
||||||
if (!phone) {
|
toast.error('Username tidak boleh ada spasi!');
|
||||||
toast.error("Nomor telepon wajib diisi!");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cleanPhone = phone.replace(/\D/g, '');
|
||||||
|
if (cleanPhone.length < 10) {
|
||||||
|
toast.error('Nomor tidak valid!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const respone = await apiFetchRegister({ nomor: phone, username: value });
|
// ✅ Hanya kirim username & nomor → dapat kodeId
|
||||||
|
const response = await apiFetchRegister({ username, nomor: cleanPhone });
|
||||||
|
|
||||||
if (respone.success) {
|
if (response.success) {
|
||||||
router.push("/login", { scroll: false });
|
// Simpan sementara
|
||||||
toast.success(respone.message);
|
localStorage.setItem('auth_kodeId', response.kodeId);
|
||||||
|
localStorage.setItem('auth_username', username); // simpan username
|
||||||
|
|
||||||
} else {
|
toast.success('Kode verifikasi dikirim!');
|
||||||
setLoading(false);
|
router.push('/validasi'); // ✅ ke halaman validasi
|
||||||
toast.error(respone.message);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error Registrasi:', error);
|
||||||
|
toast.error('Gagal mengirim OTP');
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
console.log("Error Registrasi", error);
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg} gap={"22"} py={"xl"} h={"100vh"}>
|
<Stack pos="relative" bg={colors.Bg} gap="22" py="xl" h="100vh">
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<Stack justify='center' align='center' h={"80vh"}>
|
<Stack justify="center" align="center" h="80vh">
|
||||||
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
|
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '100%', sm: 400 }}>
|
||||||
<Stack align='center'>
|
<Stack align="center">
|
||||||
<Title order={2} fw={'bold'} c={colors['blue-button']}>
|
<Title order={2} fw="bold" c={colors['blue-button']}>
|
||||||
Registrasi
|
Registrasi
|
||||||
</Title>
|
</Title>
|
||||||
<Center>
|
<Center>
|
||||||
<Image loading="lazy" src={"/darmasaba-icon.png"} alt="" w={80} />
|
<Image loading="lazy" src="/darmasaba-icon.png" alt="" w={80} />
|
||||||
</Center>
|
</Center>
|
||||||
<Box>
|
<Box w="100%">
|
||||||
<TextInput placeholder='Username'
|
<TextInput
|
||||||
label='Username'
|
label="Username"
|
||||||
maxLength={50}
|
placeholder="Username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.currentTarget.value)}
|
||||||
error={
|
error={
|
||||||
value.length > 0 && value.length < 5
|
username.length > 0 && username.length < 5
|
||||||
? "Minimal 5 karakter !"
|
? 'Minimal 5 karakter!'
|
||||||
: value.includes(" ")
|
: username.includes(' ')
|
||||||
? "Tidak boleh ada spasi"
|
? 'Tidak boleh ada spasi'
|
||||||
: isValue
|
: ''
|
||||||
? "Masukan username anda"
|
|
||||||
: ""
|
|
||||||
}
|
}
|
||||||
onChange={(val) => {
|
|
||||||
val.currentTarget.value.length > 0 ? setIsValue(false) : "";
|
|
||||||
setValue(val.currentTarget.value);
|
|
||||||
}}
|
|
||||||
required
|
required
|
||||||
|
|
||||||
/>
|
/>
|
||||||
<Box py={10}>
|
|
||||||
<Text fz={"sm"} >Nomor Telepon</Text>
|
<Box pt="md">
|
||||||
|
<Text fz="sm">Nomor Telepon</Text>
|
||||||
<PhoneInput
|
<PhoneInput
|
||||||
countrySelectorStyleProps={{
|
|
||||||
buttonStyle: {
|
|
||||||
backgroundColor: colors['blue-button'],
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
inputStyle={{ width: "100%" }}
|
|
||||||
defaultCountry="id"
|
defaultCountry="id"
|
||||||
onChange={(val) => {
|
value={phone}
|
||||||
setPhone(val);
|
disabled
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box pb={10}>
|
|
||||||
<Checkbox
|
<Box pt="md">
|
||||||
label="Saya menyetujui syarat dan ketentuan yang berlaku"
|
<Checkbox label="Saya menyetujui syarat dan ketentuan" defaultChecked />
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box pb={20} >
|
|
||||||
<Button fullWidth bg={colors['blue-button']} radius={'xl'} onClick={onRegistarsi} loading={loading ? true : false}>Daftar</Button>
|
<Box pt="xl">
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
bg={colors['blue-button']}
|
||||||
|
radius="xl"
|
||||||
|
onClick={handleRegister}
|
||||||
|
loading={loading}
|
||||||
|
disabled={username.length < 5}
|
||||||
|
>
|
||||||
|
Kirim Kode Verifikasi
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -116,6 +130,4 @@ function Registrasi() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Registrasi;
|
|
||||||
@@ -1,31 +1,270 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import colors from '@/con/colors';
|
|
||||||
import { Box, Button, Paper, PinInput, Stack, Text, Title } from '@mantine/core';
|
import colors from '@/con/colors';
|
||||||
import { useRouter } from 'next/navigation';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Loader,
|
||||||
|
Paper,
|
||||||
|
PinInput,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { authStore } from '@/store/authStore';
|
||||||
|
|
||||||
|
export default function Validasi() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [nomor, setNomor] = useState<string | null>(null);
|
||||||
|
const [otp, setOtp] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [kodeId, setKodeId] = useState<string | null>(null);
|
||||||
|
const [isRegistrationFlow, setIsRegistrationFlow] = useState(false); // Tambahkan flag
|
||||||
|
|
||||||
|
// Cek apakah ini alur registrasi
|
||||||
|
useEffect(() => {
|
||||||
|
const storedUsername = localStorage.getItem('auth_username');
|
||||||
|
setIsRegistrationFlow(!!storedUsername);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedKodeId = localStorage.getItem('auth_kodeId');
|
||||||
|
if (!storedKodeId) {
|
||||||
|
toast.error('Akses tidak valid');
|
||||||
|
router.replace('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setKodeId(storedKodeId);
|
||||||
|
const loadOtpData = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/auth/otp-data?kodeId=${encodeURIComponent(storedKodeId)}`);
|
||||||
|
const result = await res.json();
|
||||||
|
|
||||||
|
if (res.ok && result.data?.nomor) {
|
||||||
|
setNomor(result.data.nomor);
|
||||||
|
} else {
|
||||||
|
throw new Error('Data OTP tidak valid');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gagal memuat data OTP:', error);
|
||||||
|
toast.error('Kode verifikasi tidak valid');
|
||||||
|
router.replace('/login');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadOtpData();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const handleVerify = async () => {
|
||||||
|
if (!kodeId || !nomor || otp.length < 4) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
if (isRegistrationFlow) {
|
||||||
|
// 🔑 Alur REGISTRASI
|
||||||
|
await handleRegistrationVerification();
|
||||||
|
} else {
|
||||||
|
// 🔑 Alur LOGIN
|
||||||
|
await handleLoginVerification();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saat verifikasi:', error);
|
||||||
|
toast.error('Terjadi kesalahan sistem');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegistrationVerification = async () => {
|
||||||
|
const username = localStorage.getItem('auth_username');
|
||||||
|
if (!username) {
|
||||||
|
toast.error('Data registrasi tidak ditemukan.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanNomor = nomor?.replace(/\D/g, '') ?? '';
|
||||||
|
if (cleanNomor.length < 10 || username.trim().length < 5) {
|
||||||
|
toast.error('Data tidak valid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifikasi OTP dulu
|
||||||
|
const verifyRes = await fetch('/api/auth/verify-otp-register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ nomor: cleanNomor, otp, kodeId }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const verifyData = await verifyRes.json();
|
||||||
|
if (!verifyRes.ok) {
|
||||||
|
toast.error(verifyData.message || 'Verifikasi OTP gagal');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Kirim ke finalize-registration → akan redirect ke /waiting-room
|
||||||
|
const finalizeRes = await fetch('/api/auth/finalize-registration', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ nomor, username, kodeId }),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (finalizeRes.redirected) {
|
||||||
|
// ✅ Redirect otomatis oleh server
|
||||||
|
window.location.href = finalizeRes.url;
|
||||||
|
} else {
|
||||||
|
const data = await finalizeRes.json();
|
||||||
|
toast.error(data.message || 'Registrasi gagal');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Verifikasi OTP untuk LOGIN
|
||||||
|
const handleLoginVerification = async () => {
|
||||||
|
const loginRes = await fetch('/api/auth/verify-otp-login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ nomor, otp, kodeId }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginData = await loginRes.json();
|
||||||
|
|
||||||
|
if (!loginRes.ok) {
|
||||||
|
toast.error(loginData.message || 'Verifikasi gagal');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, name, roleId, isActive } = loginData.user;
|
||||||
|
|
||||||
|
authStore.setUser({
|
||||||
|
id,
|
||||||
|
name: name || 'User',
|
||||||
|
roleId: Number(roleId),
|
||||||
|
});
|
||||||
|
|
||||||
|
cleanupStorage();
|
||||||
|
|
||||||
|
if (!isActive) {
|
||||||
|
window.location.href = '/waiting-room';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectPath = getRedirectPath(Number(roleId));
|
||||||
|
router.replace(redirectPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRedirectPath = (roleId: number): string => {
|
||||||
|
switch (roleId) {
|
||||||
|
case 0: // DEVELOPER
|
||||||
|
case 1: // SUPERADMIN
|
||||||
|
case 2: // ADMIN_DESA
|
||||||
|
return '/admin/landing-page/profil/program-inovasi';
|
||||||
|
case 3: // ADMIN_KESEHATAN
|
||||||
|
return '/admin/kesehatan/posyandu';
|
||||||
|
case 4: // ADMIN_PENDIDIKAN
|
||||||
|
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||||
|
default:
|
||||||
|
return '/admin';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanupStorage = () => {
|
||||||
|
localStorage.removeItem('auth_kodeId');
|
||||||
|
localStorage.removeItem('auth_nomor');
|
||||||
|
localStorage.removeItem('auth_username');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResend = async () => {
|
||||||
|
if (!nomor) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/resend', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ nomor }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
localStorage.setItem('auth_kodeId', data.kodeId);
|
||||||
|
toast.success('OTP baru dikirim');
|
||||||
|
} else {
|
||||||
|
toast.error(data.message || 'Gagal mengirim ulang OTP');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error('Gagal menghubungi server');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Stack pos="relative" bg={colors.Bg} align="center" justify="center" h="100vh">
|
||||||
|
<Loader size="md" color={colors['blue-button']} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nomor) return null;
|
||||||
|
|
||||||
function Validasi() {
|
|
||||||
const router = useRouter()
|
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg}>
|
<Stack pos="relative" bg={colors.Bg}>
|
||||||
<Box px={{ base: 'md', md: 100 }} pb={50}>
|
<Box px={{ base: 'md', md: 100 }} pb={50}>
|
||||||
<Stack align='center' justify='center' h={"100vh"}>
|
<Stack align="center" justify="center" h="100vh">
|
||||||
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
|
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '100%', sm: 400 }}>
|
||||||
<Stack align='center' gap={"lg"}>
|
<Stack align="center" gap="lg">
|
||||||
<Box>
|
<Box>
|
||||||
<Title ta={"center"} order={2} fw={'bold'} c={colors['blue-button']}>
|
<Title ta="center" order={2} fw="bold" c={colors['blue-button']}>
|
||||||
Kode Verifikasi
|
{isRegistrationFlow ? 'Verifikasi Registrasi' : 'Verifikasi Login'}
|
||||||
</Title>
|
</Title>
|
||||||
|
<Text ta="center" size="sm" c="dimmed" mt="xs">
|
||||||
|
Kami telah mengirim kode ke nomor <strong>{nomor}</strong>
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box w="100%">
|
||||||
<Box mb={10}>
|
<Box mb={20}>
|
||||||
<Text c={colors['blue-button']} ta={"center"} fz={"sm"} fw={'bold'}>Masukkan Kode Verifikasi</Text>
|
<Text c={colors['blue-button']} ta="center" fz="sm" fw="bold">
|
||||||
<PinInput type={/^[0-9]*$/} inputType="tel" inputMode="numeric" />
|
Masukkan Kode Verifikasi
|
||||||
|
</Text>
|
||||||
|
<Center>
|
||||||
|
<PinInput
|
||||||
|
length={4}
|
||||||
|
value={otp}
|
||||||
|
onChange={setOtp}
|
||||||
|
onComplete={handleVerify}
|
||||||
|
inputMode="numeric"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
<Box py={20} >
|
|
||||||
<Button onClick={() => router.push("/admin/landing-page/profile/program-inovasi")}>
|
<Button
|
||||||
Page
|
fullWidth
|
||||||
|
onClick={handleVerify}
|
||||||
|
loading={loading}
|
||||||
|
disabled={otp.length < 4}
|
||||||
|
bg={colors['blue-button']}
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
Verifikasi
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Text ta="center" size="sm" mt="md">
|
||||||
|
Tidak menerima kode?{' '}
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={handleResend}
|
||||||
|
size="xs"
|
||||||
|
p={0}
|
||||||
|
h="auto"
|
||||||
|
color={colors['blue-button']}
|
||||||
|
>
|
||||||
|
Kirim Ulang
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -33,6 +272,4 @@ function Validasi() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Validasi;
|
|
||||||
34
src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts
Normal file
34
src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// src/app/admin/(dashboard)/user&role/_com/dynamicNavbar.ts
|
||||||
|
import { devBar, navBar, role1, role2, role3 } from '@/app/admin/_com/list_PageAdmin';
|
||||||
|
|
||||||
|
// ✅ Helper: normalisasi ID menu agar konsisten
|
||||||
|
const normalizeMenuId = (id: string): string => {
|
||||||
|
return id.trim().toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getNavbar({
|
||||||
|
roleId,
|
||||||
|
menuIds,
|
||||||
|
}: {
|
||||||
|
roleId: number;
|
||||||
|
menuIds?: string[] | null;
|
||||||
|
}) {
|
||||||
|
// ✅ Jika menuIds tersedia, gunakan untuk filter — dengan normalisasi
|
||||||
|
if (menuIds && menuIds.length > 0) {
|
||||||
|
// Normalisasi semua menuIds dari DB/state
|
||||||
|
const normalizedMenuSet = new Set(menuIds.map(id => normalizeMenuId(id)));
|
||||||
|
|
||||||
|
return navBar.filter(section => {
|
||||||
|
const normalizedSectionId = normalizeMenuId(section.id);
|
||||||
|
return normalizedMenuSet.has(normalizedSectionId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔁 Fallback ke role-based navigation
|
||||||
|
if (roleId === 0) return devBar;
|
||||||
|
if (roleId === 1) return navBar;
|
||||||
|
if (roleId === 2) return role1;
|
||||||
|
if (roleId === 3) return role2;
|
||||||
|
if (roleId === 4) return role3;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||||
import { IconForms, IconUser } from '@tabler/icons-react';
|
import { IconBrush, IconForms, IconUser } from '@tabler/icons-react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -23,6 +23,12 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
href: "/admin/user&role/role",
|
href: "/admin/user&role/role",
|
||||||
icon: <IconForms size={18} stroke={1.8} />,
|
icon: <IconForms size={18} stroke={1.8} />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Menu Access",
|
||||||
|
value: "menu-access",
|
||||||
|
href: "/admin/user&role/menu-access",
|
||||||
|
icon: <IconBrush size={18} stroke={1.8} />,
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const currentTab = tabs.find(tab => tab.href === pathname);
|
const currentTab = tabs.find(tab => tab.href === pathname);
|
||||||
|
|||||||
129
src/app/admin/(dashboard)/user&role/menu-access/page.tsx
Normal file
129
src/app/admin/(dashboard)/user&role/menu-access/page.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
// src/app/admin/user&role/menu-access/page.tsx
|
||||||
|
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { navBar } from '@/app/admin/_com/list_PageAdmin'
|
||||||
|
import { Button, Checkbox, Group, Paper, Select, Stack, Text, Title } from '@mantine/core'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useProxy } from 'valtio/utils'
|
||||||
|
import user from '../../_state/user/user-state'
|
||||||
|
import { useShallowEffect } from '@mantine/hooks'
|
||||||
|
|
||||||
|
|
||||||
|
// ✅ Helper: ekstrak semua menu ID dari struktur navBar
|
||||||
|
const extractMenuIds = (navSections: typeof navBar) => {
|
||||||
|
return navSections.map(section => ({
|
||||||
|
value: section.id, // "Landing Page", "Kesehatan", dll
|
||||||
|
label: section.name // "Landing Page", "Kesehatan", dll
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
function MenuAccessPage() {
|
||||||
|
const stateUser = useProxy(user.userState)
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
|
||||||
|
const [userMenus, setUserMenus] = useState<string[]>([])
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
stateUser.findMany.load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// ✅ Gunakan helper untuk ekstrak menu
|
||||||
|
const availableMenus = extractMenuIds(navBar);
|
||||||
|
|
||||||
|
// Ambil data menu akses user
|
||||||
|
const loadUserMenuAccess = async () => {
|
||||||
|
if (!selectedUserId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ✅ Perbaiki URL: gunakan query string bukan dynamic route
|
||||||
|
const res = await fetch(`/api/admin/user-menu-access?userId=${selectedUserId}`)
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setUserMenus(data.menuIds || [])
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gagal memuat menu akses:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedUserId) {
|
||||||
|
loadUserMenuAccess()
|
||||||
|
}
|
||||||
|
}, [selectedUserId])
|
||||||
|
|
||||||
|
const handleToggleMenu = (menuId: string) => {
|
||||||
|
setUserMenus(prev =>
|
||||||
|
prev.includes(menuId)
|
||||||
|
? prev.filter(id => id !== menuId)
|
||||||
|
: [...prev, menuId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!selectedUserId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/user-menu-access', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ userId: selectedUserId, menuIds: userMenus }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.success) {
|
||||||
|
alert('Menu akses berhasil disimpan')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gagal menyimpan menu akses:', error)
|
||||||
|
alert('Terjadi kesalahan')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Title order={2}>Tampilan Menu</Title>
|
||||||
|
<Paper p="xl" shadow="md" radius="md">
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Group>
|
||||||
|
<Text fw={500}>Pilih User:</Text>
|
||||||
|
<Select
|
||||||
|
placeholder="Pilih user"
|
||||||
|
data={stateUser.findMany.data.map(u => ({
|
||||||
|
value: u.id,
|
||||||
|
label: `${u.username} (${u.nomor})`,
|
||||||
|
}))}
|
||||||
|
value={selectedUserId}
|
||||||
|
onChange={setSelectedUserId}
|
||||||
|
w={300}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{selectedUserId && (
|
||||||
|
<>
|
||||||
|
<Text fw={500}>Menu yang Bisa Diakses:</Text>
|
||||||
|
<Stack>
|
||||||
|
{availableMenus.map(menu => (
|
||||||
|
<Checkbox
|
||||||
|
key={menu.value}
|
||||||
|
label={menu.label}
|
||||||
|
checked={userMenus.includes(menu.value)}
|
||||||
|
onChange={() => handleToggleMenu(menu.value)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Button onClick={handleSave} mt="md">
|
||||||
|
Simpan Perubahan
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MenuAccessPage
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Loader, Group, MultiSelect, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
@@ -9,6 +10,7 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import user from '../../../_state/user/user-state';
|
import user from '../../../_state/user/user-state';
|
||||||
|
|
||||||
|
|
||||||
function EditRole() {
|
function EditRole() {
|
||||||
const stateRole = useProxy(user.roleState);
|
const stateRole = useProxy(user.roleState);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -17,46 +19,37 @@ function EditRole() {
|
|||||||
// Controlled local state
|
// Controlled local state
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
permissions: [] as string[],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [originalData, setOriginalData] = useState({
|
const [originalData, setOriginalData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
permissions: [] as string[],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
// Load role data
|
// Load role data
|
||||||
const loadRole = useCallback(async (id: string) => {
|
const loadRole = useCallback(async (id: string) => {
|
||||||
try {
|
const data = await stateRole.update.load(id);
|
||||||
const data = await stateRole.update.load(id);
|
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: data.name || '',
|
name: data.name ?? '',
|
||||||
permissions: data.permissions || [],
|
});
|
||||||
});
|
|
||||||
setOriginalData({
|
setOriginalData({
|
||||||
name: data.name || '',
|
name: data.name ?? '',
|
||||||
permissions: data.permissions || [],
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading role:', error);
|
|
||||||
toast.error(error instanceof Error ? error.message : 'Gagal mengambil data role');
|
|
||||||
}
|
}
|
||||||
}, [stateRole.update]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateRole.findMany.load(); // Load permissions/options
|
stateRole.findMany.load(); // load permission
|
||||||
const id = params?.id as string;
|
if (params?.id) loadRole(params.id as string);
|
||||||
if (id) loadRole(id);
|
}, [params?.id]);
|
||||||
}, [params?.id, loadRole, stateRole.findMany]);
|
|
||||||
|
|
||||||
const handleResetForm = () => {
|
const handleResetForm = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: originalData.name,
|
name: originalData.name,
|
||||||
permissions: originalData.permissions,
|
|
||||||
});
|
});
|
||||||
toast.info("Form dikembalikan ke data awal");
|
toast.info("Form dikembalikan ke data awal");
|
||||||
};
|
};
|
||||||
@@ -66,10 +59,6 @@ function EditRole() {
|
|||||||
toast.error('Nama role tidak boleh kosong');
|
toast.error('Nama role tidak boleh kosong');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!formData.permissions.length) {
|
|
||||||
toast.error('Pilih minimal satu permission');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
@@ -77,7 +66,6 @@ function EditRole() {
|
|||||||
stateRole.update.form = {
|
stateRole.update.form = {
|
||||||
...stateRole.update.form,
|
...stateRole.update.form,
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
permissions: formData.permissions,
|
|
||||||
};
|
};
|
||||||
await stateRole.update.update();
|
await stateRole.update.update();
|
||||||
toast.success('Role berhasil diperbarui!');
|
toast.success('Role berhasil diperbarui!');
|
||||||
@@ -116,24 +104,7 @@ function EditRole() {
|
|||||||
label={<Text fw="bold" fz="sm">Nama Role</Text>}
|
label={<Text fw="bold" fz="sm">Nama Role</Text>}
|
||||||
placeholder="Masukkan nama role"
|
placeholder="Masukkan nama role"
|
||||||
/>
|
/>
|
||||||
<MultiSelect
|
<Group justify="right">
|
||||||
value={formData.permissions}
|
|
||||||
onChange={(val) => setFormData({ ...formData, permissions: val })}
|
|
||||||
label={<Text fw="bold" fz="sm">Permission</Text>}
|
|
||||||
placeholder="Pilih permission"
|
|
||||||
data={
|
|
||||||
stateRole.findMany.data?.map((v) => ({
|
|
||||||
value: v.id,
|
|
||||||
label: v.name,
|
|
||||||
})) || []
|
|
||||||
}
|
|
||||||
clearable
|
|
||||||
searchable
|
|
||||||
required
|
|
||||||
error={!formData.permissions.length ? 'Pilih minimal satu permission' : undefined}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Group justify="right">
|
|
||||||
{/* Tombol Batal */}
|
{/* Tombol Batal */}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
MultiSelect,
|
|
||||||
Paper,
|
|
||||||
Loader,
|
Loader,
|
||||||
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title
|
Title
|
||||||
@@ -15,9 +14,9 @@ import {
|
|||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import user from '../../../_state/user/user-state';
|
import user from '../../../_state/user/user-state';
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
|
|
||||||
|
|
||||||
export default function CreateRole() {
|
export default function CreateRole() {
|
||||||
@@ -31,8 +30,7 @@ export default function CreateRole() {
|
|||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateRole.create.form = {
|
stateRole.create.form = {
|
||||||
name: '',
|
name: ''
|
||||||
permissions: [],
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,28 +78,6 @@ export default function CreateRole() {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
|
||||||
<MultiSelect
|
|
||||||
label="Permission"
|
|
||||||
placeholder="Pilih permission"
|
|
||||||
data={
|
|
||||||
Array.from(
|
|
||||||
new Set(
|
|
||||||
stateRole.findMany.data
|
|
||||||
?.map((item) => item.permissions)
|
|
||||||
.flat()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.filter((p): p is string => typeof p === 'string')
|
|
||||||
.map((p) => ({ label: p, value: p }))
|
|
||||||
}
|
|
||||||
value={stateRole.create.form.permissions}
|
|
||||||
onChange={(value) => (stateRole.create.form.permissions = value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
import { Box, Button, Center, Group, Pagination, Paper, Select, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconCheck, IconSearch, IconX } from '@tabler/icons-react';
|
import { IconCheck, IconSearch, IconTrash, IconX } from '@tabler/icons-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../_com/header';
|
import HeaderSearch from '../../_com/header';
|
||||||
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
|
||||||
import user from '../../_state/user/user-state';
|
import user from '../../_state/user/user-state';
|
||||||
|
import { authStore } from '@/store/authStore';
|
||||||
|
|
||||||
function User() {
|
function User() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -27,9 +27,10 @@ function User() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListUser({ search }: { search: string }) {
|
function ListUser({ search }: { search: string }) {
|
||||||
const stateUser = useProxy(user.userState)
|
const stateUser = useProxy(user.userState);
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const stateRole = useProxy(user.roleState);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -41,20 +42,103 @@ function ListUser({ search }: { search: string }) {
|
|||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
stateUser.delete.submit(selectedId)
|
stateUser.deleteUser.delete(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
|
stateUser.findMany.load();
|
||||||
stateUser.findMany.load()
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search)
|
stateRole.findMany.load();
|
||||||
}, [page, search])
|
load(page, 10, search);
|
||||||
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = data || []
|
// ✅ Helper function untuk nama role
|
||||||
|
const getRoleName = (roleId: string) => {
|
||||||
|
// Cari dari data role yang sudah diload
|
||||||
|
const role = stateRole.findMany.data.find((r) => r.id === roleId);
|
||||||
|
return role?.name || "Unknown Role";
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Handler untuk perubahan role dengan konfirmasi
|
||||||
|
const handleRoleChange = async (
|
||||||
|
userId: string,
|
||||||
|
username: string,
|
||||||
|
oldRoleId: string,
|
||||||
|
newRoleId: string
|
||||||
|
) => {
|
||||||
|
// Skip jika sama
|
||||||
|
if (oldRoleId === newRoleId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Konfirmasi perubahan role
|
||||||
|
const confirmed = window.confirm(
|
||||||
|
`⚠️ PERINGATAN\n\n` +
|
||||||
|
`Mengubah role untuk "${username}" akan:\n` +
|
||||||
|
`• Logout user otomatis dari semua device\n` +
|
||||||
|
`• Mengubah akses menu sesuai role baru\n\n` +
|
||||||
|
`Role: ${getRoleName(oldRoleId)} → ${getRoleName(newRoleId)}\n\n` +
|
||||||
|
`Lanjutkan?`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
// Reload data untuk reset dropdown ke nilai lama
|
||||||
|
stateUser.findMany.load(page, 10, search);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Submit update
|
||||||
|
const success = await stateUser.update.submit({
|
||||||
|
id: userId,
|
||||||
|
roleId: newRoleId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// ✅ Logout user jika sedang mengedit diri sendiri
|
||||||
|
const currentUserId = authStore.user?.id;
|
||||||
|
if (currentUserId === userId) {
|
||||||
|
authStore.setUser(null);
|
||||||
|
document.cookie = `${process.env.BASE_SESSION_KEY}=; Max-Age=0; path=/;`;
|
||||||
|
alert("Perubahan memerlukan login ulang");
|
||||||
|
window.location.href = "/login";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload data
|
||||||
|
stateUser.findMany.load(page, 10, search);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Handler untuk toggle isActive
|
||||||
|
const handleToggleActive = async (userId: string, currentStatus: boolean) => {
|
||||||
|
const success = await stateUser.update.submit({
|
||||||
|
id: userId,
|
||||||
|
isActive: !currentStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// ✅ Logout user jika sedang mengedit diri sendiri
|
||||||
|
const currentUserId = authStore.user?.id;
|
||||||
|
if (currentUserId === userId) {
|
||||||
|
authStore.setUser(null);
|
||||||
|
document.cookie = `${process.env.BASE_SESSION_KEY}=; Max-Age=0; path=/;`;
|
||||||
|
alert("Perubahan memerlukan login ulang");
|
||||||
|
window.location.href = "/login";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload data
|
||||||
|
stateUser.findMany.load(page, 10, search);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredData = (data || []).filter(
|
||||||
|
(item) => item.roleId !== "0" // asumsikan id role SUPERADMIN = "0"
|
||||||
|
);
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
@@ -78,25 +162,51 @@ function ListUser({ search }: { search: string }) {
|
|||||||
<TableTh style={{ width: '20%' }}>Nomor</TableTh>
|
<TableTh style={{ width: '20%' }}>Nomor</TableTh>
|
||||||
<TableTh style={{ width: '20%' }}>Role</TableTh>
|
<TableTh style={{ width: '20%' }}>Role</TableTh>
|
||||||
<TableTh style={{ width: '15%' }}>Aktif / Nonaktif</TableTh>
|
<TableTh style={{ width: '15%' }}>Aktif / Nonaktif</TableTh>
|
||||||
|
<TableTh style={{ width: '15%' }}>Hapus</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.length > 0 ? (
|
{filteredData.length > 0 ? (
|
||||||
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.username}</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.nomor}
|
{item.nomor}
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '20%', }}>
|
<TableTd style={{ width: '20%' }}>
|
||||||
<Text truncate fz="sm" c="dimmed">
|
<Select
|
||||||
{item.role.name}
|
placeholder="Pilih role"
|
||||||
</Text>
|
data={stateRole.findMany.data
|
||||||
|
.filter(r => r.id !== "0") // ❌ Sembunyikan SUPERADMIN
|
||||||
|
.map(r => ({
|
||||||
|
label: r.name,
|
||||||
|
value: r.id,
|
||||||
|
}))}
|
||||||
|
value={item.roleId}
|
||||||
|
onChange={(val) => {
|
||||||
|
if (!val) return;
|
||||||
|
|
||||||
|
// ✅ Panggil handleRoleChange dengan konfirmasi
|
||||||
|
handleRoleChange(
|
||||||
|
item.id,
|
||||||
|
item.username,
|
||||||
|
item.roleId,
|
||||||
|
val
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
searchable
|
||||||
|
clearable={false}
|
||||||
|
nothingFoundMessage="Role tidak ditemukan"
|
||||||
|
disabled={stateUser.update.loading}
|
||||||
|
/>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
|
|
||||||
<TableTd style={{ width: '15%' }}>
|
<TableTd style={{ width: '15%' }}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={item.isActive ? "Nonaktifkan user" : "Aktifkan user"}
|
label={item.isActive ? "Nonaktifkan user" : "Aktifkan user"}
|
||||||
@@ -105,22 +215,34 @@ function ListUser({ search }: { search: string }) {
|
|||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color={item.isActive ? "green" : "red"}
|
color={item.isActive ? "green" : "red"}
|
||||||
onClick={async () => {
|
onClick={() => handleToggleActive(item.id, item.isActive)}
|
||||||
await stateUser.updateActive.submit(item.id, !item.isActive)
|
disabled={stateUser.update.loading}
|
||||||
stateUser.findMany.load(page, 10, search)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{item.isActive ? <IconCheck size={20} /> : <IconX size={20} />}
|
{item.isActive ? <IconCheck size={20} /> : <IconX size={20} />}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
|
|
||||||
|
<TableTd style={{ width: '15%' }}>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color='red'
|
||||||
|
disabled={stateUser.deleteUser.loading}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(item.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={4}>
|
<TableTd colSpan={5}>
|
||||||
<Center py={20}>
|
<Center py={20}>
|
||||||
<Text color="dimmed">Tidak ada data user yang cocok</Text>
|
<Text c="dimmed">Tidak ada data user yang cocok</Text>
|
||||||
</Center>
|
</Center>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
@@ -129,6 +251,7 @@ function ListUser({ search }: { search: string }) {
|
|||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
@@ -143,6 +266,7 @@ function ListUser({ search }: { search: string }) {
|
|||||||
radius="md"
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
{/* Modal Konfirmasi Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
@@ -154,4 +278,4 @@ function ListUser({ search }: { search: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default User;
|
export default User;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export const navBar = [
|
export const devBar = [
|
||||||
{
|
{
|
||||||
id: "Landing Page",
|
id: "Landing Page",
|
||||||
name: "Landing Page",
|
name: "Landing Page",
|
||||||
@@ -84,7 +84,6 @@ export const navBar = [
|
|||||||
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: "Desa",
|
id: "Desa",
|
||||||
name: "Desa",
|
name: "Desa",
|
||||||
@@ -336,7 +335,824 @@ export const navBar = [
|
|||||||
path: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana"
|
path: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Pendidikan",
|
||||||
|
name: "Pendidikan",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "Pendidikan_1",
|
||||||
|
name: "Info Sekolah",
|
||||||
|
path: "/admin/pendidikan/info-sekolah/jenjang-pendidikan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Pendidikan_2",
|
||||||
|
name: "Beasiswa Desa",
|
||||||
|
path: "/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Pendidikan_3",
|
||||||
|
name: "Program Pendidikan Anak",
|
||||||
|
path: "/admin/pendidikan/program-pendidikan-anak/program-unggulan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Pendidikan_4",
|
||||||
|
name: "Bimbingan Belajar Desa",
|
||||||
|
path: "/admin/pendidikan/bimbingan-belajar-desa/tujuan-program"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Pendidikan_5",
|
||||||
|
name: "Pendidikan Non Formal",
|
||||||
|
path: "/admin/pendidikan/pendidikan-non-formal/tujuan-program"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Pendidikan_6",
|
||||||
|
name: "Perpustakaan Digital",
|
||||||
|
path: "/admin/pendidikan/perpustakaan-digital/data-perpustakaan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Pendidikan_7",
|
||||||
|
name: "Data Pendidikan",
|
||||||
|
path: "/admin/pendidikan/data-pendidikan"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "User & Role",
|
||||||
|
name: "User & Role",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "User",
|
||||||
|
name: "User",
|
||||||
|
path: "/admin/user&role/user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Role",
|
||||||
|
name: "Role",
|
||||||
|
path: "/admin/user&role/role"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Menu Access",
|
||||||
|
name: "Menu Access",
|
||||||
|
path: "/admin/user&role/menu-access"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const navBar = [
|
||||||
|
{
|
||||||
|
id: "Landing Page",
|
||||||
|
name: "Landing Page",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "Landing_Page_1",
|
||||||
|
name: "Profil",
|
||||||
|
path: "/admin/landing-page/profil/program-inovasi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Landing_Page_2",
|
||||||
|
name: "Desa Anti Korupsi",
|
||||||
|
path: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Landing_Page_3",
|
||||||
|
name: "Indeks Kepuasan Masyarakat",
|
||||||
|
path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Landing_Page_4",
|
||||||
|
name: "SDGs",
|
||||||
|
path: "/admin/landing-page/SDGs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Landing_Page_5",
|
||||||
|
name: "APBDes",
|
||||||
|
path: "/admin/landing-page/apbdes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Landing_Page_6",
|
||||||
|
name: "Prestasi Desa",
|
||||||
|
path: "/admin/landing-page/prestasi-desa/list-prestasi-desa"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PPID",
|
||||||
|
name: "PPID",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "PPID_1",
|
||||||
|
name: "Profil PPID",
|
||||||
|
path: "/admin/ppid/profil-ppid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PPID_2",
|
||||||
|
name: "Struktur PPID",
|
||||||
|
path: "/admin/ppid/struktur-ppid/pegawai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PPID_3",
|
||||||
|
name: "Visi Misi PPID",
|
||||||
|
path: "/admin/ppid/visi-misi-ppid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PPID_4",
|
||||||
|
name: "Dasar Hukum",
|
||||||
|
path: "/admin/ppid/dasar-hukum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PPID_5",
|
||||||
|
name: "Permohonan Informasi Publik",
|
||||||
|
path: "/admin/ppid/permohonan-informasi-publik"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PPID_6",
|
||||||
|
name: "Permohonan Keberatan Informasi Publik",
|
||||||
|
path: "/admin/ppid/permohonan-keberatan-informasi-publik"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PPID_7",
|
||||||
|
name: "Daftar Informasi Publik",
|
||||||
|
path: "/admin/ppid/daftar-informasi-publik"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PPID_8",
|
||||||
|
name: "Indeks Kepuasan Masyarakat",
|
||||||
|
path: "/admin/ppid/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Desa",
|
||||||
|
name: "Desa",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "Desa_1",
|
||||||
|
name: "Profile",
|
||||||
|
path: "/admin/desa/profile/profile-desa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Desa_2",
|
||||||
|
name: "Potensi",
|
||||||
|
path: "/admin/desa/potensi/list-potensi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Desa_3",
|
||||||
|
name: "Berita",
|
||||||
|
path: "/admin/desa/berita/list-berita"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Desa_4",
|
||||||
|
name: "Pengumuman",
|
||||||
|
path: "/admin/desa/pengumuman/list-pengumuman"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Desa_5",
|
||||||
|
name: "Gallery",
|
||||||
|
path: "/admin/desa/gallery/foto"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Desa_6",
|
||||||
|
name: "Layanan",
|
||||||
|
path: "/admin/desa/layanan/pelayanan_surat_keterangan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Desa_7",
|
||||||
|
name: "Penghargaan",
|
||||||
|
path: "/admin/desa/penghargaan"
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Kesehatan",
|
||||||
|
name: "Kesehatan",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "Kesehatan_1",
|
||||||
|
name: "Posyandu",
|
||||||
|
path: "/admin/kesehatan/posyandu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Kesehatan_2",
|
||||||
|
name: "Data Kesehatan Warga",
|
||||||
|
path: "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Kesehatan_3",
|
||||||
|
name: "Puskesmas",
|
||||||
|
path: "/admin/kesehatan/puskesmas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Kesehatan_4",
|
||||||
|
name: "Program Kesehatan",
|
||||||
|
path: "/admin/kesehatan/program-kesehatan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Kesehatan_5",
|
||||||
|
name: "Penanganan Darurat",
|
||||||
|
path: "/admin/kesehatan/penanganan-darurat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Kesehatan_6",
|
||||||
|
name: "Kontak Darurat",
|
||||||
|
path: "/admin/kesehatan/kontak-darurat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Kesehatan_7",
|
||||||
|
name: "Info Wabah/Penyakit",
|
||||||
|
path: "/admin/kesehatan/info-wabah-penyakit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Keamanan",
|
||||||
|
name: "Keamanan",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "Keamanan_1",
|
||||||
|
name: "Keamanan Lingkungan (Pecalang/Patwal)",
|
||||||
|
path: "/admin/keamanan/keamanan-lingkungan-pecalang-patwal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Keamanan_2",
|
||||||
|
name: "Polsek Terdekat",
|
||||||
|
path: "/admin/keamanan/polsek-terdekat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Keamanan_3",
|
||||||
|
name: "Kontak Darurat",
|
||||||
|
path: "/admin/keamanan/kontak-darurat/kontak-darurat-keamanan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Keamanan_4",
|
||||||
|
name: "Pencegahan Kriminalitas",
|
||||||
|
path: "/admin/keamanan/pencegahan-kriminalitas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Keamanan_5",
|
||||||
|
name: "Laporan Publik",
|
||||||
|
path: "/admin/keamanan/laporan-publik"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Keamanan_6",
|
||||||
|
name: "Tips Keamanan",
|
||||||
|
path: "/admin/keamanan/tips-keamanan"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi",
|
||||||
|
name: "Ekonomi",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "Ekonomi_1",
|
||||||
|
name: "Pasar Desa",
|
||||||
|
path: "/admin/ekonomi/pasar-desa/produk-pasar-desa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_2",
|
||||||
|
name: "Lowongan Kerja Lokal",
|
||||||
|
path: "/admin/ekonomi/lowongan-kerja-lokal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_3",
|
||||||
|
name: "Struktur Organisasi Dan Sk Pengurus Bumdesa",
|
||||||
|
path: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_4",
|
||||||
|
name: "PADesa (Pendapatan Asli Desa)",
|
||||||
|
path: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_5",
|
||||||
|
name: "Jumlah Pengangguran",
|
||||||
|
path: "/admin/ekonomi/jumlah-pengangguran"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_6",
|
||||||
|
name: "Jumlah penduduk usia kerja yang menganggur",
|
||||||
|
path: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_7",
|
||||||
|
name: "Jumlah Penduduk Miskin",
|
||||||
|
path: "/admin/ekonomi/jumlah-penduduk-miskin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_8",
|
||||||
|
name: "Program Kemiskinan",
|
||||||
|
path: "/admin/ekonomi/program-kemiskinan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_9",
|
||||||
|
name: "Sektor Unggulan Desa",
|
||||||
|
path: "/admin/ekonomi/sektor-unggulan-desa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_10",
|
||||||
|
name: "Demografi Pekerjaan",
|
||||||
|
path: "/admin/ekonomi/demografi-pekerjaan"
|
||||||
|
}
|
||||||
|
]
|
||||||
}, {
|
}, {
|
||||||
|
id: "Inovasi",
|
||||||
|
name: "Inovasi",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "Inovasi_1",
|
||||||
|
name: "Desa Digital/Smart Village",
|
||||||
|
path: "/admin/inovasi/desa-digital-smart-village"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Inovasi_2",
|
||||||
|
name: "Layanan Online Desa",
|
||||||
|
path: "/admin/inovasi/layanan-online-desa/administrasi-online"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Inovasi_3",
|
||||||
|
name: "Program Kreatif Desa",
|
||||||
|
path: "/admin/inovasi/program-kreatif-desa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Inovasi_4",
|
||||||
|
name: "Kolaborasi Inovasi",
|
||||||
|
path: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Inovasi_5",
|
||||||
|
name: "Info Teknologi Tepat Guna",
|
||||||
|
path: "/admin/inovasi/info-teknologi-tepat-guna"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Inovasi_6",
|
||||||
|
name: "Ajukan Ide Inovatif",
|
||||||
|
path: "/admin/inovasi/ajukan-ide-inovatif"
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
id: "Lingkungan",
|
||||||
|
name: "Lingkungan",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "Lingkungan_1",
|
||||||
|
name: "Pengelolaan Sampah (Bank Sampah)",
|
||||||
|
path: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Lingkungan_2",
|
||||||
|
name: "Program Penghijauan",
|
||||||
|
path: "/admin/lingkungan/program-penghijauan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Lingkungan_3",
|
||||||
|
name: "Data Lingkungan Desa",
|
||||||
|
path: "/admin/lingkungan/data-lingkungan-desa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Lingkungan_4",
|
||||||
|
name: "Gotong Royong",
|
||||||
|
path: "/admin/lingkungan/gotong-royong/kegiatan-desa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Lingkungan_5",
|
||||||
|
name: "Edukasi Lingkungan",
|
||||||
|
path: "/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Lingkungan_6",
|
||||||
|
name: "Konservasi Adat Bali",
|
||||||
|
path: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Pendidikan",
|
||||||
|
name: "Pendidikan",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "Pendidikan_1",
|
||||||
|
name: "Info Sekolah",
|
||||||
|
path: "/admin/pendidikan/info-sekolah/jenjang-pendidikan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Pendidikan_2",
|
||||||
|
name: "Beasiswa Desa",
|
||||||
|
path: "/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Pendidikan_3",
|
||||||
|
name: "Program Pendidikan Anak",
|
||||||
|
path: "/admin/pendidikan/program-pendidikan-anak/program-unggulan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Pendidikan_4",
|
||||||
|
name: "Bimbingan Belajar Desa",
|
||||||
|
path: "/admin/pendidikan/bimbingan-belajar-desa/tujuan-program"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Pendidikan_5",
|
||||||
|
name: "Pendidikan Non Formal",
|
||||||
|
path: "/admin/pendidikan/pendidikan-non-formal/tujuan-program"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Pendidikan_6",
|
||||||
|
name: "Perpustakaan Digital",
|
||||||
|
path: "/admin/pendidikan/perpustakaan-digital/data-perpustakaan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Pendidikan_7",
|
||||||
|
name: "Data Pendidikan",
|
||||||
|
path: "/admin/pendidikan/data-pendidikan"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "User & Role",
|
||||||
|
name: "User & Role",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "User",
|
||||||
|
name: "User",
|
||||||
|
path: "/admin/user&role/user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Role",
|
||||||
|
name: "Role",
|
||||||
|
path: "/admin/user&role/role"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Menu Access",
|
||||||
|
name: "Menu Access",
|
||||||
|
path: "/admin/user&role/menu-access"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const role1 = [
|
||||||
|
{
|
||||||
|
id: "Landing Page",
|
||||||
|
name: "Landing Page",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "Landing_Page_1",
|
||||||
|
name: "Profil",
|
||||||
|
path: "/admin/landing-page/profil/program-inovasi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Landing_Page_2",
|
||||||
|
name: "Desa Anti Korupsi",
|
||||||
|
path: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Landing_Page_3",
|
||||||
|
name: "Indeks Kepuasan Masyarakat",
|
||||||
|
path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Landing_Page_4",
|
||||||
|
name: "SDGs",
|
||||||
|
path: "/admin/landing-page/SDGs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Landing_Page_5",
|
||||||
|
name: "APBDes",
|
||||||
|
path: "/admin/landing-page/apbdes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Landing_Page_6",
|
||||||
|
name: "Prestasi Desa",
|
||||||
|
path: "/admin/landing-page/prestasi-desa/list-prestasi-desa"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PPID",
|
||||||
|
name: "PPID",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "PPID_1",
|
||||||
|
name: "Profil PPID",
|
||||||
|
path: "/admin/ppid/profil-ppid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PPID_2",
|
||||||
|
name: "Struktur PPID",
|
||||||
|
path: "/admin/ppid/struktur-ppid/pegawai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PPID_3",
|
||||||
|
name: "Visi Misi PPID",
|
||||||
|
path: "/admin/ppid/visi-misi-ppid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PPID_4",
|
||||||
|
name: "Dasar Hukum",
|
||||||
|
path: "/admin/ppid/dasar-hukum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PPID_5",
|
||||||
|
name: "Permohonan Informasi Publik",
|
||||||
|
path: "/admin/ppid/permohonan-informasi-publik"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PPID_6",
|
||||||
|
name: "Permohonan Keberatan Informasi Publik",
|
||||||
|
path: "/admin/ppid/permohonan-keberatan-informasi-publik"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PPID_7",
|
||||||
|
name: "Daftar Informasi Publik",
|
||||||
|
path: "/admin/ppid/daftar-informasi-publik"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PPID_8",
|
||||||
|
name: "Indeks Kepuasan Masyarakat",
|
||||||
|
path: "/admin/ppid/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Desa",
|
||||||
|
name: "Desa",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "Desa_1",
|
||||||
|
name: "Profile",
|
||||||
|
path: "/admin/desa/profile/profile-desa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Desa_2",
|
||||||
|
name: "Potensi",
|
||||||
|
path: "/admin/desa/potensi/list-potensi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Desa_3",
|
||||||
|
name: "Berita",
|
||||||
|
path: "/admin/desa/berita/list-berita"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Desa_4",
|
||||||
|
name: "Pengumuman",
|
||||||
|
path: "/admin/desa/pengumuman/list-pengumuman"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Desa_5",
|
||||||
|
name: "Gallery",
|
||||||
|
path: "/admin/desa/gallery/foto"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Desa_6",
|
||||||
|
name: "Layanan",
|
||||||
|
path: "/admin/desa/layanan/pelayanan_surat_keterangan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Desa_7",
|
||||||
|
name: "Penghargaan",
|
||||||
|
path: "/admin/desa/penghargaan"
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Keamanan",
|
||||||
|
name: "Keamanan",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "Keamanan_1",
|
||||||
|
name: "Keamanan Lingkungan (Pecalang/Patwal)",
|
||||||
|
path: "/admin/keamanan/keamanan-lingkungan-pecalang-patwal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Keamanan_2",
|
||||||
|
name: "Polsek Terdekat",
|
||||||
|
path: "/admin/keamanan/polsek-terdekat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Keamanan_3",
|
||||||
|
name: "Kontak Darurat",
|
||||||
|
path: "/admin/keamanan/kontak-darurat/kontak-darurat-keamanan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Keamanan_4",
|
||||||
|
name: "Pencegahan Kriminalitas",
|
||||||
|
path: "/admin/keamanan/pencegahan-kriminalitas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Keamanan_5",
|
||||||
|
name: "Laporan Publik",
|
||||||
|
path: "/admin/keamanan/laporan-publik"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Keamanan_6",
|
||||||
|
name: "Tips Keamanan",
|
||||||
|
path: "/admin/keamanan/tips-keamanan"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi",
|
||||||
|
name: "Ekonomi",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "Ekonomi_1",
|
||||||
|
name: "Pasar Desa",
|
||||||
|
path: "/admin/ekonomi/pasar-desa/produk-pasar-desa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_2",
|
||||||
|
name: "Lowongan Kerja Lokal",
|
||||||
|
path: "/admin/ekonomi/lowongan-kerja-lokal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_3",
|
||||||
|
name: "Struktur Organisasi Dan Sk Pengurus Bumdesa",
|
||||||
|
path: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_4",
|
||||||
|
name: "PADesa (Pendapatan Asli Desa)",
|
||||||
|
path: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_5",
|
||||||
|
name: "Jumlah Pengangguran",
|
||||||
|
path: "/admin/ekonomi/jumlah-pengangguran"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_6",
|
||||||
|
name: "Jumlah penduduk usia kerja yang menganggur",
|
||||||
|
path: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_7",
|
||||||
|
name: "Jumlah Penduduk Miskin",
|
||||||
|
path: "/admin/ekonomi/jumlah-penduduk-miskin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_8",
|
||||||
|
name: "Program Kemiskinan",
|
||||||
|
path: "/admin/ekonomi/program-kemiskinan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_9",
|
||||||
|
name: "Sektor Unggulan Desa",
|
||||||
|
path: "/admin/ekonomi/sektor-unggulan-desa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Ekonomi_10",
|
||||||
|
name: "Demografi Pekerjaan",
|
||||||
|
path: "/admin/ekonomi/demografi-pekerjaan"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Inovasi",
|
||||||
|
name: "Inovasi",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "Inovasi_1",
|
||||||
|
name: "Desa Digital/Smart Village",
|
||||||
|
path: "/admin/inovasi/desa-digital-smart-village"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Inovasi_2",
|
||||||
|
name: "Layanan Online Desa",
|
||||||
|
path: "/admin/inovasi/layanan-online-desa/administrasi-online"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Inovasi_3",
|
||||||
|
name: "Program Kreatif Desa",
|
||||||
|
path: "/admin/inovasi/program-kreatif-desa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Inovasi_4",
|
||||||
|
name: "Kolaborasi Inovasi",
|
||||||
|
path: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Inovasi_5",
|
||||||
|
name: "Info Teknologi Tepat Guna",
|
||||||
|
path: "/admin/inovasi/info-teknologi-tepat-guna"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Inovasi_6",
|
||||||
|
name: "Ajukan Ide Inovatif",
|
||||||
|
path: "/admin/inovasi/ajukan-ide-inovatif"
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Lingkungan",
|
||||||
|
name: "Lingkungan",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "Lingkungan_1",
|
||||||
|
name: "Pengelolaan Sampah (Bank Sampah)",
|
||||||
|
path: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Lingkungan_2",
|
||||||
|
name: "Program Penghijauan",
|
||||||
|
path: "/admin/lingkungan/program-penghijauan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Lingkungan_3",
|
||||||
|
name: "Data Lingkungan Desa",
|
||||||
|
path: "/admin/lingkungan/data-lingkungan-desa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Lingkungan_4",
|
||||||
|
name: "Gotong Royong",
|
||||||
|
path: "/admin/lingkungan/gotong-royong/kegiatan-desa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Lingkungan_5",
|
||||||
|
name: "Edukasi Lingkungan",
|
||||||
|
path: "/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Lingkungan_6",
|
||||||
|
name: "Konservasi Adat Bali",
|
||||||
|
path: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const role2 = [
|
||||||
|
{
|
||||||
|
id: "Kesehatan",
|
||||||
|
name: "Kesehatan",
|
||||||
|
path: "",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "Kesehatan_1",
|
||||||
|
name: "Posyandu",
|
||||||
|
path: "/admin/kesehatan/posyandu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Kesehatan_2",
|
||||||
|
name: "Data Kesehatan Warga",
|
||||||
|
path: "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Kesehatan_3",
|
||||||
|
name: "Puskesmas",
|
||||||
|
path: "/admin/kesehatan/puskesmas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Kesehatan_4",
|
||||||
|
name: "Program Kesehatan",
|
||||||
|
path: "/admin/kesehatan/program-kesehatan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Kesehatan_5",
|
||||||
|
name: "Penanganan Darurat",
|
||||||
|
path: "/admin/kesehatan/penanganan-darurat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Kesehatan_6",
|
||||||
|
name: "Kontak Darurat",
|
||||||
|
path: "/admin/kesehatan/kontak-darurat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Kesehatan_7",
|
||||||
|
name: "Info Wabah/Penyakit",
|
||||||
|
path: "/admin/kesehatan/info-wabah-penyakit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const role3 = [
|
||||||
|
{
|
||||||
id: "Pendidikan",
|
id: "Pendidikan",
|
||||||
name: "Pendidikan",
|
name: "Pendidikan",
|
||||||
path: "",
|
path: "",
|
||||||
@@ -378,4 +1194,4 @@ export const navBar = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
9
src/app/admin/_com/navigationByRole.ts
Normal file
9
src/app/admin/_com/navigationByRole.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// src/app/(admin)/_com/navigationByRole.ts
|
||||||
|
import { navBar, role1, role2, role3 } from './list_PageAdmin';
|
||||||
|
|
||||||
|
export const navigationByRole = {
|
||||||
|
0: navBar, // SUPERADMIN
|
||||||
|
1: role1, // ADMIN DESA
|
||||||
|
2: role2, // ADMIN KESEHATAN
|
||||||
|
3: role3, // ADMIN PENDIDIKAN
|
||||||
|
} as const;
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
export {
|
|
||||||
apiFetchLogin,
|
|
||||||
apiFetchRegister
|
|
||||||
};
|
|
||||||
|
|
||||||
const apiFetchLogin = async ({ nomor }: { nomor: string }) => {
|
|
||||||
const response = await fetch("/api/auth/login", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ nomor: nomor }),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return await response.json().catch(() => null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const apiFetchRegister = async ({
|
|
||||||
nomor,
|
|
||||||
username,
|
|
||||||
}: {
|
|
||||||
nomor: string;
|
|
||||||
username: string;
|
|
||||||
}) => {
|
|
||||||
const data = {
|
|
||||||
username: username,
|
|
||||||
nomor: nomor,
|
|
||||||
};
|
|
||||||
const respone = await fetch("/api/auth/register", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ data }),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await respone.json();
|
|
||||||
|
|
||||||
return result;
|
|
||||||
// return await respone.json().catch(() => null);
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
|
import { authStore } from "@/store/authStore";
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
AppShell,
|
AppShell,
|
||||||
@@ -8,9 +9,11 @@ import {
|
|||||||
AppShellMain,
|
AppShellMain,
|
||||||
AppShellNavbar,
|
AppShellNavbar,
|
||||||
Burger,
|
Burger,
|
||||||
|
Center,
|
||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
Image,
|
Image,
|
||||||
|
Loader,
|
||||||
NavLink,
|
NavLink,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
Text,
|
Text,
|
||||||
@@ -26,14 +29,120 @@ import {
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
||||||
import { navBar } from "./_com/list_PageAdmin";
|
import { useEffect, useState } from "react";
|
||||||
|
// import { useSnapshot } from "valtio";
|
||||||
|
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const [opened, { toggle }] = useDisclosure();
|
const [opened, { toggle }] = useDisclosure();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||||
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
||||||
|
|
||||||
|
// const { user } = useSnapshot(authStore);
|
||||||
|
|
||||||
|
// console.log("Current user in store:", user);
|
||||||
|
|
||||||
|
// ✅ FIX: Selalu fetch user data setiap kali komponen mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUser = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/me');
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.user) {
|
||||||
|
// Check if user is active
|
||||||
|
if (!data.user.isActive) {
|
||||||
|
authStore.setUser(null);
|
||||||
|
router.replace('/waiting-room');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ PENTING: Selalu fetch menuIds terbaru setiap login
|
||||||
|
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`);
|
||||||
|
const menuData = await menuRes.json();
|
||||||
|
|
||||||
|
const menuIds = menuData.success && Array.isArray(menuData.menuIds)
|
||||||
|
? [...menuData.menuIds]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// ✅ Set user dengan menuIds yang fresh dari database
|
||||||
|
authStore.setUser({
|
||||||
|
id: data.user.id,
|
||||||
|
name: data.user.name,
|
||||||
|
roleId: Number(data.user.roleId),
|
||||||
|
menuIds, // menuIds terbaru
|
||||||
|
isActive: data.user.isActive
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
authStore.setUser(null);
|
||||||
|
router.replace('/login');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gagal memuat data pengguna:', error);
|
||||||
|
authStore.setUser(null);
|
||||||
|
router.replace('/login');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUser();
|
||||||
|
}, [router]); // ✅ Hapus dependency pada authStore.user
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<AppShell>
|
||||||
|
<AppShellMain>
|
||||||
|
<Center h="100vh">
|
||||||
|
<Loader />
|
||||||
|
</Center>
|
||||||
|
</AppShellMain>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Ambil menu berdasarkan roleId dan menuIds
|
||||||
|
const currentNav = authStore.user
|
||||||
|
? getNavbar({ roleId: authStore.user.roleId, menuIds: authStore.user.menuIds })
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoggingOut(true);
|
||||||
|
|
||||||
|
// ✅ Panggil API logout untuk clear session di server
|
||||||
|
const response = await fetch('/api/auth/logout', { method: 'POST' });
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Clear user data dari store
|
||||||
|
authStore.setUser(null);
|
||||||
|
|
||||||
|
// Clear localStorage
|
||||||
|
localStorage.removeItem('auth_nomor');
|
||||||
|
localStorage.removeItem('auth_kodeId');
|
||||||
|
|
||||||
|
// Force reload untuk reset semua state
|
||||||
|
window.location.href = '/login';
|
||||||
|
} else {
|
||||||
|
console.error('Logout failed:', result.message);
|
||||||
|
// Tetap redirect meskipun gagal
|
||||||
|
authStore.setUser(null);
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during logout:', error);
|
||||||
|
// Tetap clear store dan redirect jika error
|
||||||
|
authStore.setUser(null);
|
||||||
|
window.location.href = '/login';
|
||||||
|
} finally {
|
||||||
|
setIsLoggingOut(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
@@ -115,13 +224,13 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
variant="gradient"
|
variant="gradient"
|
||||||
gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src="/assets/images/darmasaba-icon.png"
|
src="/assets/images/darmasaba-icon.png"
|
||||||
alt="Logo Darmasaba"
|
alt="Logo Darmasaba"
|
||||||
w={20}
|
w={20}
|
||||||
h={20}
|
h={20}
|
||||||
radius="md"
|
radius="md"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
style={{
|
style={{
|
||||||
minWidth: '20px',
|
minWidth: '20px',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
@@ -131,14 +240,14 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label="Keluar" position="bottom" withArrow>
|
<Tooltip label="Keluar" position="bottom" withArrow>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => {
|
onClick={handleLogout}
|
||||||
router.push("/darmasaba");
|
|
||||||
}}
|
|
||||||
color={colors["blue-button"]}
|
color={colors["blue-button"]}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="gradient"
|
variant="gradient"
|
||||||
gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
||||||
|
loading={isLoggingOut}
|
||||||
|
disabled={isLoggingOut}
|
||||||
>
|
>
|
||||||
<IconLogout2 size={22} />
|
<IconLogout2 size={22} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -156,7 +265,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
p={{ base: 'xs', sm: 'sm' }}
|
p={{ base: 'xs', sm: 'sm' }}
|
||||||
>
|
>
|
||||||
<AppShell.Section p="sm">
|
<AppShell.Section p="sm">
|
||||||
{navBar.map((v, k) => {
|
{currentNav.map((v, k) => {
|
||||||
const isParentActive = segments.includes(_.lowerCase(v.name));
|
const isParentActive = segments.includes(_.lowerCase(v.name));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -254,4 +363,4 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
</AppShellMain>
|
</AppShellMain>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { randomOTP } from "../_lib/randomOTP";
|
import { randomOTP } from "../_lib/randomOTP";
|
||||||
|
|
||||||
@@ -12,52 +11,70 @@ export async function POST(req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const codeOtp = randomOTP();
|
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const { nomor } = body;
|
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 (!nomor || typeof nomor !== "string") {
|
||||||
|
|
||||||
if (sendWa.status !== "success")
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Nomor Whatsapp Tidak Aktif" },
|
{ success: false, message: "Nomor tidak valid" },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const createOtpId = await prisma.kodeOtp.create({
|
// Cek apakah user sudah terdaftar
|
||||||
data: {
|
const existingUser = await prisma.user.findUnique({
|
||||||
nomor: nomor,
|
where: { nomor },
|
||||||
otp: codeOtp,
|
select: { id: true }, // cukup cek ada/tidak
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!createOtpId)
|
const isRegistered = !!existingUser;
|
||||||
|
|
||||||
|
// Generate OTP
|
||||||
|
const codeOtp = randomOTP(); // Pastikan ini menghasilkan number (sesuai tipe di KodeOtp.otp: Int)
|
||||||
|
|
||||||
|
// Kirim OTP via WA
|
||||||
|
const waRes = await fetch(
|
||||||
|
`https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=Website Desa Darmasaba - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun Admin lainnya.%0A%0A>> Kode OTP anda: ${codeOtp}.`
|
||||||
|
);
|
||||||
|
|
||||||
|
const sendWa = await waRes.json();
|
||||||
|
|
||||||
|
if (sendWa.status !== "success") {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Gagal mengirim kode OTP" },
|
{ success: false, message: "Nomor WhatsApp tidak aktif" },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan OTP ke database
|
||||||
|
const otpRecord = await prisma.kodeOtp.create({
|
||||||
|
data: {
|
||||||
|
nomor: nomor,
|
||||||
|
otp: codeOtp, // Pastikan tipe ini number (Int di Prisma = number di TS)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: true,
|
success: true,
|
||||||
message: "Kode verifikasi terkirim",
|
message: "Kode verifikasi terkirim",
|
||||||
kodeId: createOtpId.id,
|
kodeId: otpRecord.id,
|
||||||
|
isRegistered, // 🔑 Ini kunci untuk frontend tahu harus ke register atau verifikasi
|
||||||
},
|
},
|
||||||
{ status: 200 }
|
{ status: 200 }
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error Login", error);
|
console.error("Error Login:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Terjadi masalah saat login" , reason: error as Error },
|
{
|
||||||
|
success: false,
|
||||||
|
message: "Terjadi masalah saat login",
|
||||||
|
// Hindari mengirim error mentah ke client di production
|
||||||
|
// reason: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined,
|
||||||
|
},
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
30
src/app/api/[[...slugs]]/_lib/auth/me/route.ts
Normal file
30
src/app/api/[[...slugs]]/_lib/auth/me/route.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
// Jika pakai custom session (bukan next-auth), ganti dengan logic session-mu
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
// 🔸 GANTI DENGAN LOGIC SESSION-MU
|
||||||
|
// Contoh: jika kamu simpan user.id di cookie atau JWT
|
||||||
|
const userId = req.cookies.get("hipmi_user_id")?.value; // sesuaikan
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
nomor: true,
|
||||||
|
isActive: true,
|
||||||
|
role: { select: { name: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return Response.json({ error: "User not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ user });
|
||||||
|
}
|
||||||
104
src/app/api/[[...slugs]]/_lib/auth/register/route.ts
Normal file
104
src/app/api/[[...slugs]]/_lib/auth/register/route.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// 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 { username, nomor, otp, kodeId } = await req.json();
|
||||||
|
|
||||||
|
// // Validasi input
|
||||||
|
// if (!username || !nomor || !otp || !kodeId) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: "Data tidak lengkap" },
|
||||||
|
// { status: 400 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 1. Verifikasi OTP
|
||||||
|
// const otpRecord = await prisma.kodeOtp.findUnique({
|
||||||
|
// where: { id: kodeId },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (!otpRecord) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: "Kode verifikasi tidak valid" },
|
||||||
|
// { status: 400 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!otpRecord.isActive) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: "Kode verifikasi sudah digunakan atau kadaluarsa" },
|
||||||
|
// { status: 400 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (otpRecord.otp !== otp) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: "Kode OTP salah" },
|
||||||
|
// { status: 400 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (otpRecord.nomor !== nomor) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: "Nomor tidak sesuai dengan kode verifikasi" },
|
||||||
|
// { status: 400 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 3. Cek apakah nomor sudah terdaftar
|
||||||
|
// const existingUserByNomor = await prisma.user.findUnique({
|
||||||
|
// where: { nomor },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (existingUserByNomor) {
|
||||||
|
// return NextResponse.json(
|
||||||
|
// { success: false, message: "Nomor sudah terdaftar" },
|
||||||
|
// { status: 409 }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 4. Buat user
|
||||||
|
// const newUser = await prisma.user.create({
|
||||||
|
// data: {
|
||||||
|
// username,
|
||||||
|
// nomor,
|
||||||
|
// // roleId default "1" (sesuai model)
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // 5. Nonaktifkan OTP agar tidak bisa dipakai lagi
|
||||||
|
// await prisma.kodeOtp.update({
|
||||||
|
// where: { id: kodeId },
|
||||||
|
// data: { isActive: false },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return NextResponse.json(
|
||||||
|
// {
|
||||||
|
// success: true,
|
||||||
|
// message: "Registrasi berhasil",
|
||||||
|
// userId: newUser.id,
|
||||||
|
// },
|
||||||
|
// { status: 201 }
|
||||||
|
// );
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error("Error registrasi:", error);
|
||||||
|
// return NextResponse.json(
|
||||||
|
// {
|
||||||
|
// success: false,
|
||||||
|
// message: "Terjadi kesalahan saat registrasi",
|
||||||
|
// // reason: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined,
|
||||||
|
// },
|
||||||
|
// { status: 500 }
|
||||||
|
// );
|
||||||
|
// } finally {
|
||||||
|
// await prisma.$disconnect();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
File diff suppressed because it is too large
Load Diff
62
src/app/api/[[...slugs]]/_lib/user/delUser.ts
Normal file
62
src/app/api/[[...slugs]]/_lib/user/delUser.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// /api/user/delUser.ts
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
import { Context } from 'elysia';
|
||||||
|
|
||||||
|
export default async function userDeleteAccount(context: Context) {
|
||||||
|
const { id } = context.params as { id: string };
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Cek user dulu
|
||||||
|
const existingUser = await prisma.user.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingUser) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'User tidak ditemukan',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 2. Hapus SEMUA relasi dalam TRANSACTION
|
||||||
|
const result = await prisma.$transaction(async (tx) => {
|
||||||
|
// Hapus UserSession
|
||||||
|
const deletedSessions = await tx.userSession.deleteMany({
|
||||||
|
where: { userId: id },
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Hapus UserMenuAccess
|
||||||
|
const deletedMenuAccess = await tx.userMenuAccess.deleteMany({
|
||||||
|
where: { userId: id },
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Tambahkan relasi lain jika ada (contoh):
|
||||||
|
// await tx.userLog.deleteMany({ where: { userId: id } });
|
||||||
|
// await tx.userNotification.deleteMany({ where: { userId: id } });
|
||||||
|
// await tx.userToken.deleteMany({ where: { userId: id } });
|
||||||
|
|
||||||
|
// Hapus user
|
||||||
|
const deletedUser = await tx.user.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: deletedUser,
|
||||||
|
sessionsDeleted: deletedSessions.count,
|
||||||
|
menuAccessDeleted: deletedMenuAccess.count,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `User berhasil dihapus permanen (${result.sessionsDeleted} session, ${result.menuAccessDeleted} menu access)`,
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error delete user:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Terjadi kesalahan saat menghapus user',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import userFindMany from "./findMany";
|
|||||||
import userFindUnique from "./findUnique";
|
import userFindUnique from "./findUnique";
|
||||||
import userDelete from "./del"; // `delete` nggak boleh jadi nama file JS langsung, jadi biasanya `del.ts`
|
import userDelete from "./del"; // `delete` nggak boleh jadi nama file JS langsung, jadi biasanya `del.ts`
|
||||||
import userUpdate from "./updt";
|
import userUpdate from "./updt";
|
||||||
|
import userDeleteAccount from "./delUser";
|
||||||
|
|
||||||
const User = new Elysia({ prefix: "/api/user" })
|
const User = new Elysia({ prefix: "/api/user" })
|
||||||
.get("/findMany", userFindMany)
|
.get("/findMany", userFindMany)
|
||||||
@@ -14,6 +15,21 @@ const User = new Elysia({ prefix: "/api/user" })
|
|||||||
id: t.String(),
|
id: t.String(),
|
||||||
}),
|
}),
|
||||||
}) // pakai PUT untuk soft delete
|
}) // pakai PUT untuk soft delete
|
||||||
.put("/updt", userUpdate);
|
.put(
|
||||||
|
"/updt",
|
||||||
|
userUpdate,
|
||||||
|
{
|
||||||
|
body: t.Object({
|
||||||
|
id: t.String(),
|
||||||
|
isActive: t.Optional(t.Boolean()),
|
||||||
|
roleId: t.Optional(t.String()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.delete("/delUser/:id", userDeleteAccount, {
|
||||||
|
params: t.Object({
|
||||||
|
id: t.String(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
export default User;
|
export default User;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Context } from "elysia";
|
|||||||
|
|
||||||
type FormCreate = {
|
type FormCreate = {
|
||||||
name: string;
|
name: string;
|
||||||
permissions: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function roleCreate(context: Context) {
|
export default async function roleCreate(context: Context) {
|
||||||
@@ -13,7 +12,6 @@ export default async function roleCreate(context: Context) {
|
|||||||
const result = await prisma.role.create({
|
const result = await prisma.role.create({
|
||||||
data: {
|
data: {
|
||||||
name: body.name,
|
name: body.name,
|
||||||
permissions: body.permissions,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const Role = new Elysia({
|
|||||||
.post("/create", roleCreate, {
|
.post("/create", roleCreate, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
name: t.String(),
|
name: t.String(),
|
||||||
permissions: t.Array(t.String()),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -27,7 +26,6 @@ const Role = new Elysia({
|
|||||||
.put("/:id", roleUpdate, {
|
.put("/:id", roleUpdate, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
name: t.String(),
|
name: t.String(),
|
||||||
permissions: t.Array(t.String()),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.delete("/del/:id", roleDelete);
|
.delete("/del/:id", roleDelete);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Context } from "elysia";
|
|||||||
|
|
||||||
type FormUpdate = {
|
type FormUpdate = {
|
||||||
name: string;
|
name: string;
|
||||||
permissions: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function roleUpdate(context: Context) {
|
export default async function roleUpdate(context: Context) {
|
||||||
@@ -15,7 +14,6 @@ export default async function roleUpdate(context: Context) {
|
|||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
name: body.name,
|
name: body.name,
|
||||||
permissions: body.permissions,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,39 +2,77 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
// API update user
|
||||||
export default async function userUpdate(context: Context) {
|
export default async function userUpdate(context: Context) {
|
||||||
try {
|
try {
|
||||||
const { id, isActive } = await context.body as { id: string, isActive: boolean };
|
const { id, isActive, roleId } = (await context.body) as {
|
||||||
|
id: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
roleId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return {
|
return { success: false, message: "ID user wajib ada" };
|
||||||
success: false,
|
|
||||||
message: "ID user wajib ada",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validasi role
|
||||||
|
if (roleId) {
|
||||||
|
const role = await prisma.role.findUnique({ where: { id: roleId } });
|
||||||
|
if (!role) return { success: false, message: "Role tidak ditemukan" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUser = await prisma.user.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { roleId: true, isActive: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!currentUser) {
|
||||||
|
return { success: false, message: "User tidak ditemukan" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRoleChanged = roleId && currentUser.roleId !== roleId;
|
||||||
|
const isActiveChanged = isActive !== undefined && currentUser.isActive !== isActive;
|
||||||
|
|
||||||
|
// Update user
|
||||||
const updatedUser = await prisma.user.update({
|
const updatedUser = await prisma.user.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { isActive },
|
data: {
|
||||||
|
...(isActive !== undefined && { isActive }),
|
||||||
|
...(roleId && { roleId }),
|
||||||
|
// Force logout: invalidate semua sesi
|
||||||
|
...(isRoleChanged ? { sessionInvalid: true } : {}),
|
||||||
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
nomor: true,
|
nomor: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
updatedAt: true,
|
roleId: true,
|
||||||
|
role: { select: { name: true } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ✅ HAPUS SEMUA SESI USER DI DATABASE
|
||||||
|
if (isRoleChanged) {
|
||||||
|
await prisma.userSession.deleteMany({ where: { userId: id } });
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `User berhasil ${isActive ? "diaktifkan" : "dinonaktifkan"}`,
|
roleChanged: isRoleChanged,
|
||||||
|
isActiveChanged,
|
||||||
data: updatedUser,
|
data: updatedUser,
|
||||||
|
message: isRoleChanged
|
||||||
|
? `Role ${updatedUser.username} diubah. User akan logout otomatis.`
|
||||||
|
: isActiveChanged
|
||||||
|
? `${updatedUser.username} ${isActive ? 'diaktifkan' : 'dinonaktifkan'}.`
|
||||||
|
: "User berhasil diupdate"
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error update user:", e);
|
console.error("❌ Error update user:", e);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "Gagal mengupdate status user",
|
message: "Gagal mengupdate user: " + (e.message || "Unknown error"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
65
src/app/api/admin/user-menu-access/route.ts
Normal file
65
src/app/api/admin/user-menu-access/route.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// src/app/api/admin/user-menu-access/route.ts
|
||||||
|
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
|
||||||
|
// ❌ HAPUS { params } karena tidak dipakai
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const userId = searchParams.get('userId')
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'User ID diperlukan' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuAccess = await prisma.userMenuAccess.findMany({
|
||||||
|
where: { userId },
|
||||||
|
select: { menuId: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
menuIds: menuAccess.map(m => m.menuId),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('GET User Menu Access Error:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'Gagal memuat menu akses' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST tetap sama (tanpa perubahan)
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const { userId, menuIds } = await request.json()
|
||||||
|
|
||||||
|
if (!userId || !Array.isArray(menuIds)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'Data tidak valid' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.userMenuAccess.deleteMany({ where: { userId } })
|
||||||
|
|
||||||
|
if (menuIds.length > 0) {
|
||||||
|
await prisma.userMenuAccess.createMany({
|
||||||
|
data: menuIds.map((menuId: string) => ({ userId, menuId })),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('POST User Menu Access Error:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'Gagal menyimpan menu akses' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
142
src/app/api/auth/_lib/api_fetch_auth.ts
Normal file
142
src/app/api/auth/_lib/api_fetch_auth.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
// app/api/auth/_lib/api_fetch_auth.ts
|
||||||
|
|
||||||
|
// app/api/auth/_lib/api_fetch_auth.ts
|
||||||
|
|
||||||
|
export const apiFetchLogin = async ({ nomor }: { nomor: string }) => {
|
||||||
|
if (!nomor || nomor.replace(/\D/g, '').length < 10) {
|
||||||
|
throw new Error('Nomor tidak valid');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanPhone = nomor.replace(/\D/g, '');
|
||||||
|
|
||||||
|
const response = await fetch("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ nomor: cleanPhone }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pastikan respons bisa di-parse sebagai JSON
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Non-JSON response from /api/auth/login:", await response.text());
|
||||||
|
throw new Error('Respons server tidak valid');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || 'Gagal memproses login');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validasi minimal respons
|
||||||
|
if (typeof data.success !== 'boolean' || typeof data.isRegistered !== 'boolean') {
|
||||||
|
throw new Error('Respons tidak sesuai format');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
if (data.isRegistered && !data.kodeId) {
|
||||||
|
throw new Error('Kode verifikasi tidak ditemukan untuk user terdaftar');
|
||||||
|
}
|
||||||
|
return data; // { success, isRegistered, kodeId? }
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || 'Login gagal');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const apiFetchRegister = async ({
|
||||||
|
username,
|
||||||
|
nomor,
|
||||||
|
}: {
|
||||||
|
username: string;
|
||||||
|
nomor: string;
|
||||||
|
}) => {
|
||||||
|
const cleanPhone = nomor.replace(/\D/g, '');
|
||||||
|
if (cleanPhone.length < 10) throw new Error('Nomor tidak valid');
|
||||||
|
|
||||||
|
const response = await fetch("/api/auth/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ username: username.trim(), nomor: cleanPhone }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok) throw new Error(data.message || 'Gagal mengirim OTP');
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const apiFetchOtpData = async ({ kodeId }: { kodeId: string }) => {
|
||||||
|
if (!kodeId) {
|
||||||
|
throw new Error('Kode ID tidak valid');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch("/api/auth/otp-data", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ kodeId }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || 'Gagal memuat data OTP');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ganti endpoint ke verify-otp-login
|
||||||
|
export const apiFetchVerifyOtp = async ({ nomor, otp, kodeId }: { nomor: string; otp: string; kodeId: string }) => {
|
||||||
|
const response = await fetch('/api/auth/verify-otp-login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ nomor, otp, kodeId }),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
success: response.ok,
|
||||||
|
...data,
|
||||||
|
status: response.status,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Di dalam api_fetch_auth.ts
|
||||||
|
|
||||||
|
export async function apiFetchUserMenuAccess(userId: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
menuIds?: string[];
|
||||||
|
message?: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/admin/user-menu-access/${userId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Fetch User Menu Access Error:', error);
|
||||||
|
return { success: false, message: 'Gagal memuat menu akses' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiUpdateUserMenuAccess(
|
||||||
|
userId: string,
|
||||||
|
menuIds: string[]
|
||||||
|
): Promise<{ success: boolean; message?: string }> {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/user-menu-access', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ userId, menuIds }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Update User Menu Access Error:', error);
|
||||||
|
return { success: false, message: 'Gagal menyimpan menu akses' };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,50 +1,32 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { jwtVerify } from "jose";
|
import { jwtVerify } from "jose";
|
||||||
|
|
||||||
export async function decrypt({
|
export async function decrypt({
|
||||||
token,
|
token,
|
||||||
encodedKey,
|
jwtSecret,
|
||||||
}: {
|
}: {
|
||||||
token: string;
|
token: string;
|
||||||
encodedKey: string;
|
jwtSecret: string;
|
||||||
}): Promise<Record<string, any> | null> {
|
}): Promise<Record<string, unknown> | null> {
|
||||||
if (!token || !encodedKey) {
|
if (!token || !jwtSecret) return null;
|
||||||
console.error("Missing required parameters:", {
|
|
||||||
hasToken: !!token,
|
|
||||||
hasEncodedKey: !!encodedKey,
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const enc = new TextEncoder().encode(encodedKey);
|
const secret = new TextEncoder().encode(jwtSecret);
|
||||||
const { payload } = await jwtVerify(token, enc, {
|
const { payload } = await jwtVerify(token, secret, {
|
||||||
algorithms: ["HS256"],
|
algorithms: ["HS256"],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!payload || !payload.user) {
|
if (
|
||||||
console.error("Invalid payload structure:", {
|
typeof payload !== "object" ||
|
||||||
hasPayload: !!payload,
|
payload === null ||
|
||||||
hasUser: payload ? !!payload.user : false,
|
!("user" in payload) ||
|
||||||
});
|
typeof payload.user !== "object"
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logging untuk debug
|
return payload.user as Record<string, unknown>;
|
||||||
// console.log("Decrypt successful:", {
|
|
||||||
// payloadExists: !!payload,
|
|
||||||
// userExists: !!payload.user,
|
|
||||||
// tokenPreview: token.substring(0, 10) + "...",
|
|
||||||
// });
|
|
||||||
|
|
||||||
return payload.user as Record<string, any>;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Token verification failed:", {
|
console.error("JWT Decrypt failed:", error);
|
||||||
error,
|
|
||||||
tokenLength: token?.length,
|
|
||||||
errorName: error instanceof Error ? error.name : "Unknown error",
|
|
||||||
errorMessage: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,23 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { SignJWT } from "jose";
|
import { SignJWT } from "jose";
|
||||||
|
|
||||||
export async function encrypt({
|
export async function encrypt({
|
||||||
user,
|
user,
|
||||||
exp = "7 year",
|
exp = "7d",
|
||||||
encodedKey,
|
jwtSecret,
|
||||||
}: {
|
}: {
|
||||||
user: Record<string, any>;
|
user: Record<string, unknown>;
|
||||||
exp?: string;
|
exp?: string | number;
|
||||||
encodedKey: string;
|
jwtSecret: string;
|
||||||
}): Promise<string | null> {
|
}): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const enc = new TextEncoder().encode(encodedKey);
|
const secret = new TextEncoder().encode(jwtSecret);
|
||||||
return new SignJWT({ user })
|
return new SignJWT({ user })
|
||||||
.setProtectedHeader({ alg: "HS256" })
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
.setIssuedAt()
|
.setIssuedAt()
|
||||||
.setExpirationTime(exp)
|
.setExpirationTime(exp)
|
||||||
.sign(enc);
|
.sign(secret);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Gagal mengenkripsi", error);
|
console.error("JWT Encrypt failed:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// wibu:0.2.82
|
|
||||||
@@ -1,36 +1,65 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
// src/app/api/auth/_lib/sessionCreate.ts
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { encrypt } from "./encrypt";
|
import { encrypt } from "./encrypt";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { add } from "date-fns";
|
||||||
|
|
||||||
export async function sessionCreate({
|
export async function sessionCreate({
|
||||||
sessionKey,
|
sessionKey,
|
||||||
exp = "7 year",
|
exp = "30 day",
|
||||||
encodedKey,
|
jwtSecret,
|
||||||
user,
|
user,
|
||||||
|
invalidatePrevious = true, // 🔑 kontrol apakah sesi lama di-nonaktifkan
|
||||||
}: {
|
}: {
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
exp?: string;
|
exp?: string;
|
||||||
encodedKey: string;
|
jwtSecret: string;
|
||||||
user: Record<string, unknown>;
|
user: Record<string, unknown> & { id: string };
|
||||||
|
invalidatePrevious?: boolean; // default true untuk login, false untuk registrasi
|
||||||
}) {
|
}) {
|
||||||
const token = await encrypt({
|
// ✅ Validasi env vars
|
||||||
exp,
|
if (!sessionKey || sessionKey.length === 0) {
|
||||||
encodedKey,
|
throw new Error("sessionKey tidak boleh kosong");
|
||||||
user,
|
}
|
||||||
|
if (!jwtSecret || jwtSecret.length < 32) {
|
||||||
|
throw new Error("jwtSecret minimal 32 karakter");
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await encrypt({ exp, jwtSecret, user });
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("Token generation failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Hitung expiresAt
|
||||||
|
let expiresAt = add(new Date(), { days: 30 });
|
||||||
|
if (exp === "7 day") expiresAt = add(new Date(), { days: 7 });
|
||||||
|
|
||||||
|
// 🔐 Hanya nonaktifkan sesi aktif sebelumnya jika diminta (misal: saat login ulang)
|
||||||
|
if (invalidatePrevious) {
|
||||||
|
await prisma.userSession.updateMany({
|
||||||
|
where: { userId: user.id, active: true },
|
||||||
|
data: { active: false },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Simpan sesi baru
|
||||||
|
await prisma.userSession.create({
|
||||||
|
data: {
|
||||||
|
token,
|
||||||
|
userId: user.id,
|
||||||
|
active: true,
|
||||||
|
expiresAt,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const cookie: any = {
|
// ✅ Set cookie
|
||||||
key: sessionKey,
|
(await cookies()).set(sessionKey, token, {
|
||||||
value: token,
|
httpOnly: true,
|
||||||
options: {
|
sameSite: "lax",
|
||||||
httpOnly: true,
|
path: "/",
|
||||||
sameSite: "lax",
|
secure: process.env.NODE_ENV === "production",
|
||||||
path: "/",
|
maxAge: 30 * 24 * 60 * 60, // 30 hari dalam detik
|
||||||
},
|
});
|
||||||
};
|
|
||||||
|
|
||||||
(await cookies()).set(cookie.key, cookie.value, { ...cookie.options });
|
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// wibu:0.2.82
|
|
||||||
42
src/app/api/auth/_lib/session_delete.ts
Normal file
42
src/app/api/auth/_lib/session_delete.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// app/api/auth/_lib/session_delete.ts
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hapus session dari database dan cookie
|
||||||
|
*/
|
||||||
|
export async function sessionDelete({
|
||||||
|
sessionKey,
|
||||||
|
userId,
|
||||||
|
}: {
|
||||||
|
sessionKey: string;
|
||||||
|
userId?: string;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get(sessionKey)?.value;
|
||||||
|
|
||||||
|
// Hapus dari database
|
||||||
|
if (token) {
|
||||||
|
const deleted = await prisma.userSession.deleteMany({
|
||||||
|
where: { token },
|
||||||
|
});
|
||||||
|
console.log(`🗑️ Deleted ${deleted.count} session(s) by token`);
|
||||||
|
} else if (userId) {
|
||||||
|
// Fallback: hapus berdasarkan userId
|
||||||
|
const deleted = await prisma.userSession.deleteMany({
|
||||||
|
where: { userId },
|
||||||
|
});
|
||||||
|
console.log(`🗑️ Deleted ${deleted.count} session(s) for user ${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hapus cookie
|
||||||
|
cookieStore.delete(sessionKey);
|
||||||
|
console.log('✅ Session deleted successfully');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error deleting session:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/app/api/auth/_lib/session_verify.ts
Normal file
39
src/app/api/auth/_lib/session_verify.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// app/api/auth/_lib/session_verify.ts
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { decrypt } from "./decrypt";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export async function verifySession() {
|
||||||
|
try {
|
||||||
|
const sessionKey = process.env.BASE_SESSION_KEY;
|
||||||
|
const jwtSecret = process.env.BASE_TOKEN_KEY;
|
||||||
|
if (!sessionKey || !jwtSecret) throw new Error('Env tidak lengkap');
|
||||||
|
|
||||||
|
const token = (await cookies()).get(sessionKey)?.value;
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
const jwtUser = await decrypt({ token, jwtSecret });
|
||||||
|
if (!jwtUser?.id) return null;
|
||||||
|
|
||||||
|
// Cari session di DB berdasarkan token
|
||||||
|
const dbSession = await prisma.userSession.findFirst({
|
||||||
|
where: {
|
||||||
|
token,
|
||||||
|
active: true,
|
||||||
|
expiresAt: { gte: new Date() }
|
||||||
|
},
|
||||||
|
include: { user: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dbSession) {
|
||||||
|
console.log('⚠️ Session tidak ditemukan di DB');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't check isActive here, let the frontend handle it
|
||||||
|
return dbSession.user;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Session verification failed:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/app/api/auth/finalize-registration/route.ts
Normal file
92
src/app/api/auth/finalize-registration/route.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
// src/app/api/auth/finalize-registration/route.ts
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { sessionCreate } from "../_lib/session_create";
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const { nomor, username, kodeId } = await req.json();
|
||||||
|
const cleanNomor = nomor.replace(/\D/g, "");
|
||||||
|
|
||||||
|
if (!cleanNomor || !username || !kodeId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Data tidak lengkap" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const otpRecord = await prisma.kodeOtp.findUnique({ where: { id: kodeId } });
|
||||||
|
if (!otpRecord?.isActive || otpRecord.nomor !== cleanNomor) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "OTP tidak valid" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await prisma.user.findFirst({ where: { username } })) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Username sudah digunakan" },
|
||||||
|
{ status: 409 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultRole = await prisma.role.findFirst({
|
||||||
|
where: { name: "ADMIN DESA" },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!defaultRole) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Role default tidak ditemukan" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
username,
|
||||||
|
nomor,
|
||||||
|
roleId: defaultRole.id,
|
||||||
|
isActive: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.kodeOtp.update({
|
||||||
|
where: { id: kodeId },
|
||||||
|
data: { isActive: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = await sessionCreate({
|
||||||
|
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||||
|
jwtSecret: process.env.BASE_TOKEN_KEY!,
|
||||||
|
exp: "30 day",
|
||||||
|
user: {
|
||||||
|
id: newUser.id,
|
||||||
|
nomor: newUser.nomor,
|
||||||
|
username: newUser.username,
|
||||||
|
roleId: newUser.roleId,
|
||||||
|
isActive: false,
|
||||||
|
},
|
||||||
|
invalidatePrevious: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ REDIRECT DARI SERVER — cookie pasti tersedia
|
||||||
|
const response = NextResponse.redirect(new URL('/waiting-room', req.url));
|
||||||
|
response.cookies.set(process.env.BASE_SESSION_KEY!, token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
path: "/",
|
||||||
|
maxAge: 30 * 24 * 60 * 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Finalize Registration Error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Registrasi gagal" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
// app/api/auth/login/route.ts
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { randomOTP } from "../_lib/randomOTP";
|
import { randomOTP } from "../_lib/randomOTP";
|
||||||
|
|
||||||
@@ -12,52 +12,66 @@ export async function POST(req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const codeOtp = randomOTP();
|
const { nomor } = await req.json();
|
||||||
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 (!nomor || typeof nomor !== "string") {
|
||||||
|
|
||||||
if (sendWa.status !== "success")
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Nomor Whatsapp Tidak Aktif" },
|
{ success: false, message: "Nomor tidak valid" },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const createOtpId = await prisma.kodeOtp.create({
|
const existingUser = await prisma.user.findUnique({
|
||||||
data: {
|
where: { nomor },
|
||||||
nomor: nomor,
|
select: { id: true, isActive: true },
|
||||||
otp: codeOtp,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!createOtpId)
|
const isRegistered = !!existingUser;
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, message: "Gagal mengirim kode OTP" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
|
|
||||||
return NextResponse.json(
|
if (isRegistered) {
|
||||||
{
|
// ✅ User terdaftar → kirim OTP
|
||||||
|
const codeOtp = randomOTP();
|
||||||
|
const otpNumber = Number(codeOtp);
|
||||||
|
|
||||||
|
const waMessage = `Website Desa Darmasaba - Kode verifikasi Anda: ${codeOtp}`;
|
||||||
|
const waUrl = `https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=${encodeURIComponent(waMessage)}`;
|
||||||
|
|
||||||
|
const res = await fetch(waUrl);
|
||||||
|
const sendWa = await res.json();
|
||||||
|
|
||||||
|
if (sendWa.status !== "success") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Gagal mengirim OTP via WhatsApp" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createOtpId = await prisma.kodeOtp.create({
|
||||||
|
data: { nomor, otp: otpNumber, isActive: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Kode verifikasi terkirim",
|
message: "Kode verifikasi dikirim",
|
||||||
kodeId: createOtpId.id,
|
kodeId: createOtpId.id,
|
||||||
},
|
isRegistered: true,
|
||||||
{ status: 200 }
|
});
|
||||||
);
|
} else {
|
||||||
|
// ❌ User belum terdaftar → JANGAN kirim OTP
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "Nomor belum terdaftar",
|
||||||
|
isRegistered: false,
|
||||||
|
// Tidak ada kodeId
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error Login", error);
|
console.error("Error Login:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Terjadi masalah saat login" , reason: error as Error },
|
{ success: false, message: "Terjadi kesalahan saat login" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
35
src/app/api/auth/logout/route.ts
Normal file
35
src/app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// app/api/auth/logout/route.ts
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { sessionDelete } from "../_lib/session_delete";
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
try {
|
||||||
|
const deleted = await sessionDelete({
|
||||||
|
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deleted) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "Logout berhasil",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: "Gagal logout",
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Logout Error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: "Terjadi kesalahan saat logout",
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/app/api/auth/me/route.ts
Normal file
59
src/app/api/auth/me/route.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// src/app/api/auth/me/route.ts
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { verifySession } from '../_lib/session_verify';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const sessionUser = await verifySession();
|
||||||
|
if (!sessionUser) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Unauthorized", user: null },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [dbUser, menuAccess] = await Promise.all([
|
||||||
|
prisma.user.findUnique({
|
||||||
|
where: { id: sessionUser.id },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
nomor: true,
|
||||||
|
roleId: true, // STRING!
|
||||||
|
isActive: true, // BOOLEAN!
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.userMenuAccess.findMany({
|
||||||
|
where: { userId: sessionUser.id },
|
||||||
|
select: { menuId: true },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!dbUser) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "User not found", user: null },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
user: {
|
||||||
|
id: dbUser.id,
|
||||||
|
name: dbUser.username,
|
||||||
|
username: dbUser.username,
|
||||||
|
nomor: dbUser.nomor,
|
||||||
|
roleId: dbUser.roleId, // STRING!
|
||||||
|
isActive: dbUser.isActive, // BOOLEAN!
|
||||||
|
menuIds: menuAccess.map(m => m.menuId),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error in /api/auth/me:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Internal server error", user: null },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/app/api/auth/otp-data/route.ts
Normal file
42
src/app/api/auth/otp-data/route.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// app/api/auth/otp-data/route.ts
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const kodeId = searchParams.get("kodeId");
|
||||||
|
|
||||||
|
if (!kodeId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Kode ID tidak diberikan" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const otpRecord = await prisma.kodeOtp.findUnique({
|
||||||
|
where: { id: kodeId },
|
||||||
|
select: { nomor: true, isActive: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!otpRecord || !otpRecord.isActive) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Kode verifikasi tidak valid atau sudah digunakan" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: { nomor: otpRecord.nomor },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Gagal mengambil data OTP:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Terjadi kesalahan internal" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/app/api/auth/refresh-session/route.ts
Normal file
55
src/app/api/auth/refresh-session/route.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { verifySession } from '../_lib/session_verify';
|
||||||
|
import { sessionCreate } from '../_lib/session_create';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
try {
|
||||||
|
const sessionUser = await verifySession();
|
||||||
|
if (!sessionUser) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Unauthorized" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get fresh user data
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: sessionUser.id },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
roleId: true,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "User not found" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new session with updated data
|
||||||
|
await sessionCreate({
|
||||||
|
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||||
|
jwtSecret: process.env.BASE_TOKEN_KEY!,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
roleId: user.roleId,
|
||||||
|
isActive: user.isActive,
|
||||||
|
},
|
||||||
|
invalidatePrevious: false, // Keep existing sessions
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing session:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,62 +1,51 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import { NextResponse } from 'next/server';
|
||||||
import { NextResponse } from "next/server";
|
import prisma from '@/lib/prisma';
|
||||||
|
import { randomOTP } from '../_lib/randomOTP'; // pastikan ada
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
if (req.method !== "POST") {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, message: "Method Not Allowed" },
|
|
||||||
{ status: 405 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await req.json();
|
const { username, nomor } = await req.json();
|
||||||
|
|
||||||
const cekUsername = await prisma.user.findUnique({
|
if (!username || !nomor) {
|
||||||
where: {
|
return NextResponse.json({ success: false, message: 'Data tidak lengkap' }, { status: 400 });
|
||||||
username: data.username,
|
}
|
||||||
nomor: data.nomor,
|
|
||||||
},
|
// Cek duplikat
|
||||||
|
if (await prisma.user.findUnique({ where: { nomor } })) {
|
||||||
|
return NextResponse.json({ success: false, message: 'Nomor sudah terdaftar' }, { status: 409 });
|
||||||
|
}
|
||||||
|
if (await prisma.user.findFirst({ where: { username } })) {
|
||||||
|
return NextResponse.json({ success: false, message: 'Username sudah digunakan' }, { status: 409 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Generate dan kirim OTP
|
||||||
|
const codeOtp = randomOTP();
|
||||||
|
const otpNumber = Number(codeOtp);
|
||||||
|
|
||||||
|
const waMessage = `Website Desa Darmasaba - Kode verifikasi Anda: ${codeOtp}`;
|
||||||
|
const waUrl = `https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=${encodeURIComponent(waMessage)}`;
|
||||||
|
const waRes = await fetch(waUrl);
|
||||||
|
const waData = await waRes.json();
|
||||||
|
|
||||||
|
if (waData.status !== "success") {
|
||||||
|
return NextResponse.json({ success: false, message: 'Gagal mengirim OTP via WhatsApp' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Simpan OTP ke database
|
||||||
|
const otpRecord = await prisma.kodeOtp.create({
|
||||||
|
data: { nomor, otp: otpNumber, isActive: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (cekUsername)
|
// ✅ Kembalikan kodeId (jangan buat user di sini!)
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: false,
|
success: true,
|
||||||
message: "Username sudah digunakan",
|
message: 'Kode verifikasi dikirim',
|
||||||
});
|
kodeId: otpRecord.id,
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error("Error registrasi:", error);
|
console.error('Register OTP Error:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json({ success: false, message: 'Gagal mengirim OTP' }, { status: 500 });
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
message: "Maaf, Terjadi Keselahan",
|
|
||||||
reason: (error as Error).message,
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
58
src/app/api/auth/resend/route.ts
Normal file
58
src/app/api/auth/resend/route.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// src/app/api/auth/resend-otp/route.ts
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { randomOTP } from "../_lib/randomOTP";
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const { nomor } = await req.json();
|
||||||
|
|
||||||
|
if (!nomor || typeof nomor !== 'string') {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Nomor tidak valid" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeOtp = randomOTP();
|
||||||
|
const otpNumber = Number(codeOtp);
|
||||||
|
|
||||||
|
// Kirim OTP via WhatsApp
|
||||||
|
const waMessage = `Kode verifikasi Anda: ${codeOtp}`;
|
||||||
|
const waUrl = `https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=${encodeURIComponent(waMessage)}`;
|
||||||
|
const waRes = await fetch(waUrl);
|
||||||
|
const waData = await waRes.json();
|
||||||
|
|
||||||
|
if (waData.status !== "success") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Gagal mengirim OTP via WhatsApp" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan OTP ke database
|
||||||
|
const otpRecord = await prisma.kodeOtp.create({
|
||||||
|
data: {
|
||||||
|
nomor,
|
||||||
|
otp: otpNumber,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "OTP baru dikirim",
|
||||||
|
kodeId: otpRecord.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error Resend OTP:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Gagal mengirim ulang OTP" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/app/api/auth/send-otp-register/route.ts
Normal file
51
src/app/api/auth/send-otp-register/route.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { randomOTP } from "../_lib/randomOTP";
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const { username, nomor } = await req.json();
|
||||||
|
|
||||||
|
if (!username || !nomor) {
|
||||||
|
return NextResponse.json({ success: false, message: 'Data tidak lengkap' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek duplikat
|
||||||
|
if (await prisma.user.findUnique({ where: { nomor } })) {
|
||||||
|
return NextResponse.json({ success: false, message: 'Nomor sudah terdaftar' }, { status: 409 });
|
||||||
|
}
|
||||||
|
if (await prisma.user.findFirst({ where: { username } })) {
|
||||||
|
return NextResponse.json({ success: false, message: 'Username sudah digunakan' }, { status: 409 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate OTP
|
||||||
|
const codeOtp = randomOTP();
|
||||||
|
const otpNumber = Number(codeOtp);
|
||||||
|
|
||||||
|
// Kirim WA
|
||||||
|
const waUrl = `https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=Website Desa Darmasaba - Kode verifikasi Anda: ${codeOtp}`;
|
||||||
|
const res = await fetch(waUrl);
|
||||||
|
const sendWa = await res.json();
|
||||||
|
|
||||||
|
if (sendWa.status !== "success") {
|
||||||
|
return NextResponse.json({ success: false, message: 'Gagal mengirim OTP' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan OTP
|
||||||
|
const otpRecord = await prisma.kodeOtp.create({
|
||||||
|
data: { nomor, otp: otpNumber, isActive: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Kode verifikasi dikirim',
|
||||||
|
kodeId: otpRecord.id,
|
||||||
|
nomor,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Send OTP for Register Error:', error);
|
||||||
|
return NextResponse.json({ success: false, message: 'Gagal mengirim OTP' }, { status: 500 });
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/app/api/auth/validasi/route.ts
Normal file
78
src/app/api/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.BASE_SESSION_KEY!,
|
||||||
|
jwtSecret: process.env.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/app/api/auth/verify-otp-login/route.ts
Normal file
101
src/app/api/auth/verify-otp-login/route.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
// src/app/api/auth/verify-otp-login/route.ts
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { sessionCreate } from "../_lib/session_create";
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const { nomor, otp, kodeId } = await req.json();
|
||||||
|
|
||||||
|
if (!nomor || !otp || !kodeId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Data tidak lengkap" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const otpRecord = await prisma.kodeOtp.findUnique({ where: { id: kodeId } });
|
||||||
|
if (!otpRecord || !otpRecord.isActive || otpRecord.nomor !== nomor) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Kode verifikasi tidak valid" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const receivedOtp = Number(otp);
|
||||||
|
if (isNaN(receivedOtp) || otpRecord.otp !== receivedOtp) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Kode OTP salah" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔍 CARI USER — JANGAN BUAT BARU!
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { nomor },
|
||||||
|
select: { id: true, nomor: true, username: true, roleId: true, isActive: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
// ❌ Nomor belum terdaftar → suruh registrasi
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Akun tidak ditemukan. Silakan registrasi terlebih dahulu." },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Buat sesi
|
||||||
|
const token = await sessionCreate({
|
||||||
|
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||||
|
jwtSecret: process.env.BASE_TOKEN_KEY!,
|
||||||
|
exp: "30 day",
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
nomor: user.nomor,
|
||||||
|
username: user.username,
|
||||||
|
roleId: user.roleId,
|
||||||
|
isActive: user.isActive,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.kodeOtp.update({ where: { id: kodeId }, data: { isActive: false } }),
|
||||||
|
prisma.user.update({ where: { id: user.id }, data: { lastLogin: new Date() } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const response = NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: user.isActive ? "Berhasil login" : "Menunggu persetujuan",
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
name: user.username,
|
||||||
|
roleId: user.roleId,
|
||||||
|
isActive: user.isActive,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
response.cookies.set(process.env.BASE_SESSION_KEY!, token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
path: "/",
|
||||||
|
maxAge: 30 * 24 * 60 * 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("❌ Verify OTP Login Error:", error);
|
||||||
|
if (error.message.includes("sessionKey") || error.message.includes("jwtSecret")) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Konfigurasi server tidak lengkap" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Terjadi kesalahan saat login" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/app/api/auth/verify-otp-register/route.ts
Normal file
61
src/app/api/auth/verify-otp-register/route.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// src/app/api/auth/verify-otp-register/route.ts
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const { nomor, otp, kodeId } = await req.json();
|
||||||
|
|
||||||
|
if (!nomor || !otp || !kodeId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Data tidak lengkap" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const otpRecord = await prisma.kodeOtp.findUnique({
|
||||||
|
where: { id: kodeId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!otpRecord || !otpRecord.isActive) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Kode verifikasi tidak valid atau sudah kadaluarsa" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otpRecord.nomor !== nomor) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Nomor tidak sesuai dengan kode verifikasi" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const receivedOtp = Number(otp);
|
||||||
|
if (isNaN(receivedOtp) || otpRecord.otp !== receivedOtp) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Kode OTP salah" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Hanya validasi — jangan update isActive!
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "OTP valid. Lanjutkan ke finalisasi registrasi.",
|
||||||
|
data: {
|
||||||
|
nomor,
|
||||||
|
kodeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Verify OTP Register Error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: "Terjadi kesalahan saat verifikasi OTP" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ const state = useProxy(indeksKepuasanState.responden);
|
|||||||
indeksKepuasanState.jenisKelaminResponden.findMany.load()
|
indeksKepuasanState.jenisKelaminResponden.findMany.load()
|
||||||
indeksKepuasanState.pilihanRatingResponden.findMany.load()
|
indeksKepuasanState.pilihanRatingResponden.findMany.load()
|
||||||
indeksKepuasanState.kelompokUmurResponden.findMany.load()
|
indeksKepuasanState.kelompokUmurResponden.findMany.load()
|
||||||
})
|
},[])
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function Kepuasan() {
|
|||||||
indeksKepuasanState.jenisKelaminResponden.findMany.load()
|
indeksKepuasanState.jenisKelaminResponden.findMany.load()
|
||||||
indeksKepuasanState.pilihanRatingResponden.findMany.load()
|
indeksKepuasanState.pilihanRatingResponden.findMany.load()
|
||||||
indeksKepuasanState.kelompokUmurResponden.findMany.load()
|
indeksKepuasanState.kelompokUmurResponden.findMany.load()
|
||||||
})
|
},[])
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -59,6 +59,35 @@ const getWorkStatus = (day: string, currentTime: string): { status: string; mess
|
|||||||
: { status: "Tutup", message: "08:00 - 17:00" };
|
: { status: "Tutup", message: "08:00 - 17:00" };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Skeleton component untuk Social Media
|
||||||
|
const SosmedSkeleton = () => (
|
||||||
|
<Flex gap="md" justify="center" wrap="wrap">
|
||||||
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<Skeleton key={i} height={56} width={56} circle />
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Skeleton component untuk Profile
|
||||||
|
const ProfileSkeleton = () => (
|
||||||
|
<Card
|
||||||
|
radius="xl"
|
||||||
|
bg={colors.grey[1]}
|
||||||
|
p="lg"
|
||||||
|
shadow="xl"
|
||||||
|
w={{ base: "100%", md: "35%" }}
|
||||||
|
style={{ height: "fit-content" }}
|
||||||
|
>
|
||||||
|
<Stack gap="lg" align="center">
|
||||||
|
<Skeleton height={300} width="100%" radius="lg" />
|
||||||
|
<Stack gap="xs" w="100%" align="center">
|
||||||
|
<Skeleton height={20} width="60%" />
|
||||||
|
<Skeleton height={32} width="80%" />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
function LandingPage() {
|
function LandingPage() {
|
||||||
const [socialMedia, setSocialMedia] = useState<
|
const [socialMedia, setSocialMedia] = useState<
|
||||||
Prisma.MediaSosialGetPayload<{ include: { image: true } }>[]
|
Prisma.MediaSosialGetPayload<{ include: { image: true } }>[]
|
||||||
@@ -66,9 +95,8 @@ function LandingPage() {
|
|||||||
const [profile, setProfile] = useState<
|
const [profile, setProfile] = useState<
|
||||||
Prisma.PejabatDesaGetPayload<{ include: { image: true } }> | null
|
Prisma.PejabatDesaGetPayload<{ include: { image: true } }> | null
|
||||||
>(null);
|
>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoadingSosmed, setIsLoadingSosmed] = useState(true);
|
||||||
|
const [isLoadingProfile, setIsLoadingProfile] = useState(true);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSocialMedia = async () => {
|
const fetchSocialMedia = async () => {
|
||||||
@@ -86,7 +114,7 @@ function LandingPage() {
|
|||||||
} catch {
|
} catch {
|
||||||
setSocialMedia([]);
|
setSocialMedia([]);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoadingSosmed(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,6 +126,8 @@ function LandingPage() {
|
|||||||
setProfile(result.data || null);
|
setProfile(result.data || null);
|
||||||
} catch {
|
} catch {
|
||||||
setProfile(null);
|
setProfile(null);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingProfile(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -189,8 +219,8 @@ function LandingPage() {
|
|||||||
|
|
||||||
<ModuleView />
|
<ModuleView />
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoadingSosmed ? (
|
||||||
<Skeleton height={32} width="100%" />
|
<SosmedSkeleton />
|
||||||
) : socialMedia.length > 0 ? (
|
) : socialMedia.length > 0 ? (
|
||||||
<SosmedView data={socialMedia} />
|
<SosmedView data={socialMedia} />
|
||||||
) : (
|
) : (
|
||||||
@@ -207,19 +237,27 @@ function LandingPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoadingProfile ? (
|
||||||
<Skeleton height={300} width="100%" radius="lg" />
|
<ProfileSkeleton />
|
||||||
) : profile ? (
|
) : profile ? (
|
||||||
<ProfileView data={profile} />
|
<ProfileView data={profile} />
|
||||||
) : (
|
) : (
|
||||||
<Center w="100%">
|
<Card
|
||||||
<Text c="dimmed">Informasi profil belum tersedia</Text>
|
radius="xl"
|
||||||
</Center>
|
bg={colors.grey[1]}
|
||||||
|
p="lg"
|
||||||
|
shadow="xl"
|
||||||
|
w={{ base: "100%", md: "35%" }}
|
||||||
|
style={{ height: "fit-content" }}
|
||||||
|
>
|
||||||
|
<Center h={300}>
|
||||||
|
<Text c="dimmed">Informasi profil belum tersedia</Text>
|
||||||
|
</Center>
|
||||||
|
</Card>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LandingPage;
|
export default LandingPage;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import DesaAntiKorupsi from "@/app/darmasaba/_com/main-page/desaantikorupsi";
|
import DesaAntiKorupsi from "@/app/darmasaba/_com/main-page/desaantikorupsi";
|
||||||
import Kepuasan from "@/app/darmasaba/_com/main-page/kepuasan";
|
import Kepuasan from "@/app/darmasaba/_com/main-page/kepuasan";
|
||||||
@@ -13,32 +14,34 @@ import Apbdes from "./_com/main-page/apbdes";
|
|||||||
import Prestasi from "./_com/main-page/prestasi";
|
import Prestasi from "./_com/main-page/prestasi";
|
||||||
import ScrollToTopButton from "./_com/scrollToTopButton";
|
import ScrollToTopButton from "./_com/scrollToTopButton";
|
||||||
|
|
||||||
import NewsReaderLanding from "./_com/NewsReaderalanding";
|
import { useEffect, useMemo } from "react";
|
||||||
import ModernNewsNotification from "./_com/ModernNeewsNotification";
|
import { useSnapshot } from "valtio";
|
||||||
import { useMemo } from "react";
|
|
||||||
import { useProxy } from "valtio/utils";
|
|
||||||
import stateDashboardBerita from "../admin/(dashboard)/_state/desa/berita";
|
import stateDashboardBerita from "../admin/(dashboard)/_state/desa/berita";
|
||||||
import stateDesaPengumuman from "../admin/(dashboard)/_state/desa/pengumuman";
|
import stateDesaPengumuman from "../admin/(dashboard)/_state/desa/pengumuman";
|
||||||
import { useEffect } from "react";
|
import ModernNewsNotification from "./_com/ModernNeewsNotification";
|
||||||
|
import NewsReaderLanding from "./_com/NewsReaderalanding";
|
||||||
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const featured = useProxy(stateDashboardBerita.berita.findFirst);
|
const snap1 = useSnapshot(stateDashboardBerita.berita.findFirst);
|
||||||
|
const snap2 = useSnapshot(stateDesaPengumuman.pengumuman.findFirst);
|
||||||
|
|
||||||
|
const featured = snap1;
|
||||||
|
const pengumuman = snap2;
|
||||||
const loadingFeatured = featured.loading;
|
const loadingFeatured = featured.loading;
|
||||||
const pengumuman = useProxy(stateDesaPengumuman.pengumuman.findFirst);
|
|
||||||
const loadingPengumuman = pengumuman.loading;
|
const loadingPengumuman = pengumuman.loading;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!featured.data && !loadingFeatured) {
|
if (!featured.data && !loadingFeatured) {
|
||||||
stateDashboardBerita.berita.findFirst.load();
|
stateDashboardBerita.berita.findFirst.load();
|
||||||
}
|
}
|
||||||
}, [featured.data, loadingFeatured]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pengumuman.data && !loadingPengumuman) {
|
if (!pengumuman.data && !loadingPengumuman) {
|
||||||
stateDesaPengumuman.pengumuman.findFirst.load();
|
stateDesaPengumuman.pengumuman.findFirst.load();
|
||||||
}
|
}
|
||||||
}, [pengumuman.data, loadingPengumuman]);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const newsData = useMemo(() => {
|
const newsData = useMemo(() => {
|
||||||
|
|||||||
193
src/app/waiting-room/page.tsx
Normal file
193
src/app/waiting-room/page.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Loader,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { authStore } from '@/store/authStore'; // ✅ integrasi authStore
|
||||||
|
|
||||||
|
async function fetchUser() {
|
||||||
|
const res = await fetch('/api/auth/me');
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(`HTTP ${res.status}: ${text}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WaitingRoom() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [user, setUser] = useState<any>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||||
|
const [retryCount, setRetryCount] = useState(0);
|
||||||
|
const MAX_RETRIES = 2;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
let interval: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
|
if (isRedirecting || !isMounted) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetchUser();
|
||||||
|
if (!isMounted) return;
|
||||||
|
|
||||||
|
const currentUser = data.user;
|
||||||
|
setUser(currentUser);
|
||||||
|
|
||||||
|
// ✅ Update authStore
|
||||||
|
if (currentUser) {
|
||||||
|
authStore.setUser({
|
||||||
|
id: currentUser.id,
|
||||||
|
name: currentUser.name,
|
||||||
|
roleId: Number(currentUser.roleId),
|
||||||
|
menuIds: currentUser.menuIds || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the poll function
|
||||||
|
if (currentUser?.isActive === true) {
|
||||||
|
setIsRedirecting(true);
|
||||||
|
clearInterval(interval);
|
||||||
|
|
||||||
|
// Update authStore with the current user data
|
||||||
|
authStore.setUser({
|
||||||
|
id: currentUser.id,
|
||||||
|
name: currentUser.name || 'User',
|
||||||
|
roleId: Number(currentUser.roleId),
|
||||||
|
menuIds: currentUser.menuIds || null,
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up storage
|
||||||
|
localStorage.removeItem('auth_kodeId');
|
||||||
|
localStorage.removeItem('auth_nomor');
|
||||||
|
localStorage.removeItem('auth_username');
|
||||||
|
|
||||||
|
// Force a session refresh
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/refresh-session', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
// Redirect based on role
|
||||||
|
let redirectPath = '/admin';
|
||||||
|
switch (String(currentUser.roleId)) {
|
||||||
|
case "0": case "1": case "2":
|
||||||
|
redirectPath = '/admin/landing-page/profil/program-inovasi';
|
||||||
|
break;
|
||||||
|
case "3":
|
||||||
|
redirectPath = '/admin/kesehatan/posyandu';
|
||||||
|
break;
|
||||||
|
case "4":
|
||||||
|
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
window.location.href = redirectPath; // Use window.location to force full page reload
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing session:', error);
|
||||||
|
router.refresh(); // Fallback to client-side refresh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
if (!isMounted) return;
|
||||||
|
|
||||||
|
if (err.message.includes('401')) {
|
||||||
|
if (retryCount < MAX_RETRIES) {
|
||||||
|
setRetryCount((prev) => prev + 1);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isMounted) interval = setInterval(poll, 3000);
|
||||||
|
}, 800);
|
||||||
|
} else {
|
||||||
|
setError('Sesi tidak valid. Silakan login ulang.');
|
||||||
|
clearInterval(interval);
|
||||||
|
authStore.setUser(null); // ✅ clear sesi
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Error polling:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interval = setInterval(poll, 3000);
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
if (interval) clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [router, isRedirecting, retryCount]);
|
||||||
|
|
||||||
|
// ✅ UI Error
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Center h="100vh">
|
||||||
|
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={400}>
|
||||||
|
<Stack align="center" gap="md">
|
||||||
|
<Title order={3} c="red">
|
||||||
|
Sesi Tidak Valid
|
||||||
|
</Title>
|
||||||
|
<Text>{error}</Text>
|
||||||
|
<Button onClick={() => router.push('/login')}>
|
||||||
|
Login Ulang
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ UI Redirecting
|
||||||
|
if (isRedirecting) {
|
||||||
|
return (
|
||||||
|
<Center h="100vh" bg={colors.Bg}>
|
||||||
|
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}>
|
||||||
|
<Stack align="center" gap="lg">
|
||||||
|
<Title order={2} c={colors['blue-button']} ta="center">
|
||||||
|
Akun Disetujui! ✅
|
||||||
|
</Title>
|
||||||
|
<Text ta="center" c="green">
|
||||||
|
Mengalihkan ke dashboard...
|
||||||
|
</Text>
|
||||||
|
<Loader size="sm" color="green" />
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ UI Default (MENUNGGU) — INI YANG KAMU HILANGKAN!
|
||||||
|
return (
|
||||||
|
<Center h="100vh" bg={colors.Bg}>
|
||||||
|
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}>
|
||||||
|
<Stack align="center" gap="lg">
|
||||||
|
<Title order={2} c={colors['blue-button']} ta="center">
|
||||||
|
Menunggu Persetujuan
|
||||||
|
</Title>
|
||||||
|
<Text ta="center" c="dimmed">
|
||||||
|
Akun Anda sedang dalam proses verifikasi oleh Superadmin.
|
||||||
|
</Text>
|
||||||
|
<Text ta="center" size="sm" c="dimmed">
|
||||||
|
Nomor: {user?.nomor || '...'}
|
||||||
|
</Text>
|
||||||
|
<Loader size="sm" color={colors['blue-button']} />
|
||||||
|
<Text ta="center" size="xs" c="dimmed">
|
||||||
|
Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,46 +1,98 @@
|
|||||||
// app/middleware.js
|
// // app/middleware.js
|
||||||
import { NextResponse, NextRequest } from 'next/server';
|
// import { NextResponse, NextRequest } from 'next/server';
|
||||||
|
|
||||||
// Daftar route yang diizinkan tanpa login (public routes)
|
// // Daftar route yang diizinkan tanpa login (public routes)
|
||||||
const publicRoutes = [
|
// const publicRoutes = [
|
||||||
'/*', // Home page
|
// '/*', // Home page
|
||||||
'/about', // About page
|
// '/about', // About page
|
||||||
'/public/*', // Wildcard untuk semua route di bawah /public
|
// '/public/*', // Wildcard untuk semua route di bawah /public
|
||||||
'/login', // Halaman login
|
// '/login', // Halaman login
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// // Fungsi untuk memeriksa apakah route saat ini adalah route publik
|
||||||
|
// function isPublicRoute(pathname: string) {
|
||||||
|
// return publicRoutes.some((route) => {
|
||||||
|
// // Jika route mengandung wildcard (*), gunakan regex untuk mencocokkan
|
||||||
|
// if (route.endsWith('*')) {
|
||||||
|
// const baseRoute = route.replace('*', ''); // Hapus wildcard
|
||||||
|
// return pathname.startsWith(baseRoute); // Cocokkan dengan pathname
|
||||||
|
// }
|
||||||
|
// return pathname === route; // Cocokkan exact path
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export function middleware(request: NextRequest) {
|
||||||
|
// const { pathname } = request.nextUrl;
|
||||||
|
|
||||||
|
// // Jika route adalah public, izinkan akses
|
||||||
|
// if (isPublicRoute(pathname)) {
|
||||||
|
// return NextResponse.next();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Jika bukan public route, periksa apakah pengguna sudah login
|
||||||
|
// const isLoggedIn = request.cookies.get('darmasaba-auth-token'); // Contoh: cek cookie auth-token
|
||||||
|
// if (!isLoggedIn) {
|
||||||
|
// // Redirect ke halaman login jika belum login
|
||||||
|
// return NextResponse.redirect(new URL('/login', request.url));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Jika sudah login, izinkan akses
|
||||||
|
// return NextResponse.next();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Konfigurasi untuk menentukan path mana yang akan dijalankan middleware
|
||||||
|
// export const config = {
|
||||||
|
// matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], // Jalankan middleware untuk semua route kecuali file statis
|
||||||
|
// };
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
// src/app/admin/middleware.ts
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
import { jwtVerify } from 'jose';
|
||||||
|
|
||||||
|
// Route publik di dalam /admin (boleh diakses tanpa login penuh)
|
||||||
|
const PUBLIC_ADMIN_ROUTES = [
|
||||||
|
'/admin/login',
|
||||||
|
'/admin/registrasi',
|
||||||
|
'/admin/validasi',
|
||||||
|
'/admin/waiting-room',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Fungsi untuk memeriksa apakah route saat ini adalah route publik
|
export async function middleware(request: NextRequest) {
|
||||||
function isPublicRoute(pathname: string) {
|
const path = request.nextUrl.pathname;
|
||||||
return publicRoutes.some((route) => {
|
|
||||||
// Jika route mengandung wildcard (*), gunakan regex untuk mencocokkan
|
|
||||||
if (route.endsWith('*')) {
|
|
||||||
const baseRoute = route.replace('*', ''); // Hapus wildcard
|
|
||||||
return pathname.startsWith(baseRoute); // Cocokkan dengan pathname
|
|
||||||
}
|
|
||||||
return pathname === route; // Cocokkan exact path
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function middleware(request: NextRequest) {
|
// Izinkan akses ke route publik di /admin
|
||||||
const { pathname } = request.nextUrl;
|
if (PUBLIC_ADMIN_ROUTES.some(route => path.startsWith(route))) {
|
||||||
|
|
||||||
// Jika route adalah public, izinkan akses
|
|
||||||
if (isPublicRoute(pathname)) {
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jika bukan public route, periksa apakah pengguna sudah login
|
|
||||||
const isLoggedIn = request.cookies.get('darmasaba-auth-token'); // Contoh: cek cookie auth-token
|
|
||||||
if (!isLoggedIn) {
|
|
||||||
// Redirect ke halaman login jika belum login
|
|
||||||
return NextResponse.redirect(new URL('/login', request.url));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jika sudah login, izinkan akses
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ambil token dari cookie
|
||||||
|
const token = request.cookies.get(process.env.BASE_SESSION_KEY!)?.value;
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.redirect(new URL('/login', request.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verifikasi JWT
|
||||||
|
const secret = new TextEncoder().encode(process.env.BASE_TOKEN_KEY!);
|
||||||
|
const { payload } = await jwtVerify(token, secret);
|
||||||
|
const user = (payload as any).user;
|
||||||
|
|
||||||
|
// Cek apakah user aktif
|
||||||
|
if (!user || !user.isActive) {
|
||||||
|
return NextResponse.redirect(new URL('/login', request.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ User valid → izinkan akses
|
||||||
|
return NextResponse.next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Middleware auth error:', error);
|
||||||
|
return NextResponse.redirect(new URL('/login', request.url));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Konfigurasi untuk menentukan path mana yang akan dijalankan middleware
|
// Hanya berlaku untuk /admin/*
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], // Jalankan middleware untuk semua route kecuali file statis
|
matcher: ['/admin/:path*'],
|
||||||
};
|
};
|
||||||
20
src/store/authStore.ts
Normal file
20
src/store/authStore.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// src/store/authStore.ts
|
||||||
|
import { proxy } from 'valtio';
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
roleId: number;
|
||||||
|
menuIds?: string[] | null; // ✅ Pastikan pakai `string[]`
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const authStore = proxy<{
|
||||||
|
user: User | null;
|
||||||
|
setUser: (user: User | null) => void;
|
||||||
|
}>({
|
||||||
|
user: null,
|
||||||
|
setUser(user) {
|
||||||
|
authStore.user = user;
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user