From a0537810e8dadb340eaa6747cb220f16515aa141 Mon Sep 17 00:00:00 2001 From: nico Date: Thu, 20 Nov 2025 02:42:39 +0800 Subject: [PATCH 01/11] Login, Register, Verifkasi Code Admin V1 --- .../migration.sql | 1127 +++++++++++++++++ prisma/schema.prisma | 66 +- .../(dashboard)/auth/login-admin/page.tsx | 118 +- .../auth/registrasi-admin/page.tsx | 160 +-- .../(dashboard)/auth/validasi-admin/page.tsx | 186 ++- src/app/admin/auth/_lib/api_fetch_auth.ts | 41 - .../api/[[...slugs]]/_lib/auth/login/route.ts | 63 +- .../api/[[...slugs]]/_lib/auth/me/route.ts | 30 + .../[[...slugs]]/_lib/auth/register/route.ts | 104 ++ src/app/api/auth/_lib/api_fetch_auth.ts | 152 +++ src/app/api/auth/_lib/decrypt.ts | 48 +- src/app/api/auth/_lib/encrypt.ts | 21 +- src/app/api/auth/_lib/session_create.ts | 40 +- .../api/auth/finalize-registration/route.ts | 40 + src/app/api/auth/login/route.ts | 80 +- src/app/api/auth/me/route.ts | 30 + src/app/api/auth/otp-data/route.ts | 41 + src/app/api/auth/register/route.ts | 154 ++- src/app/api/auth/resend/route.ts | 71 ++ src/app/api/auth/send-otp-register/route.ts | 51 + src/app/api/auth/validasi/route.ts | 78 ++ src/app/api/auth/verify-otp/route.ts | 139 ++ src/app/waiting-room/page.tsx | 92 ++ 23 files changed, 2536 insertions(+), 396 deletions(-) create mode 100644 prisma/migrations/20251119062255_add_unique_username/migration.sql delete mode 100644 src/app/admin/auth/_lib/api_fetch_auth.ts create mode 100644 src/app/api/[[...slugs]]/_lib/auth/me/route.ts create mode 100644 src/app/api/[[...slugs]]/_lib/auth/register/route.ts create mode 100644 src/app/api/auth/_lib/api_fetch_auth.ts create mode 100644 src/app/api/auth/finalize-registration/route.ts create mode 100644 src/app/api/auth/me/route.ts create mode 100644 src/app/api/auth/otp-data/route.ts create mode 100644 src/app/api/auth/resend/route.ts create mode 100644 src/app/api/auth/send-otp-register/route.ts create mode 100644 src/app/api/auth/validasi/route.ts create mode 100644 src/app/api/auth/verify-otp/route.ts create mode 100644 src/app/waiting-room/page.tsx diff --git a/prisma/migrations/20251119062255_add_unique_username/migration.sql b/prisma/migrations/20251119062255_add_unique_username/migration.sql new file mode 100644 index 00000000..d8d9e087 --- /dev/null +++ b/prisma/migrations/20251119062255_add_unique_username/migration.sql @@ -0,0 +1,1127 @@ +/* + Warnings: + + - You are about to drop the column `kelahiranKasar` on the `DataKematian_Kelahiran` table. All the data in the column will be lost. + - You are about to drop the column `kematianBayi` on the `DataKematian_Kelahiran` table. All the data in the column will be lost. + - You are about to drop the column `kematianKasar` on the `DataKematian_Kelahiran` table. All the data in the column will be lost. + - You are about to drop the column `tahun` on the `DataKematian_Kelahiran` table. All the data in the column will be lost. + - You are about to drop the column `jumlah` on the `GrafikKepuasan` table. All the data in the column will be lost. + - You are about to drop the column `label` on the `GrafikKepuasan` table. All the data in the column will be lost. + - You are about to drop the column `imageId` on the `KolaborasiInovasi` table. All the data in the column will be lost. + - You are about to drop the column `imageId` on the `KontakDaruratKeamanan` table. All the data in the column will be lost. + - You are about to drop the column `imageId` on the `KontakItem` table. All the data in the column will be lost. + - You are about to drop the column `kategoriId` on the `KontakItem` table. All the data in the column will be lost. + - You are about to drop the column `programKeamananId` on the `PencegahanKriminalitas` table. All the data in the column will be lost. + - You are about to drop the column `tipsKeamananId` on the `PencegahanKriminalitas` table. All the data in the column will be lost. + - You are about to drop the column `videoKeamananId` on the `PencegahanKriminalitas` table. All the data in the column will be lost. + - You are about to drop the column `kategori` on the `PotensiDesa` table. All the data in the column will be lost. + - You are about to drop the column `ikonUrl` on the `ProgramKemiskinan` table. All the data in the column will be lost. + - You are about to drop the `ProgramKeamanan` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `TipsKeamanan` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `VideoKeamanan` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `hubungan_organisasi` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `pegawai` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `posisi_organisasi` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `struktur_organisasi` table. If the table is not empty, all the data it contains will be lost. + - Added the required column `kelahiranId` to the `DataKematian_Kelahiran` table without a default value. This is not possible if the table is not empty. + - Added the required column `kematianId` to the `DataKematian_Kelahiran` table without a default value. This is not possible if the table is not empty. + - Added the required column `category` to the `FileStorage` table without a default value. This is not possible if the table is not empty. + - Added the required column `alamat` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty. + - Added the required column `jenisKelamin` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty. + - Added the required column `nama` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty. + - Added the required column `penyakit` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty. + - Added the required column `tanggal` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty. + - Added the required column `whatsapp` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty. + - Added the required column `icon` to the `KontakDaruratKeamanan` table without a default value. This is not possible if the table is not empty. + - Added the required column `kategoriId` to the `KontakDaruratKeamanan` table without a default value. This is not possible if the table is not empty. + - Added the required column `icon` to the `KontakItem` table without a default value. This is not possible if the table is not empty. + - Added the required column `notelp` to the `LowonganPekerjaan` table without a default value. This is not possible if the table is not empty. + - Added the required column `name` to the `MediaSosial` table without a default value. This is not possible if the table is not empty. + - Added the required column `kontak` to the `PasarDesa` table without a default value. This is not possible if the table is not empty. + - Added the required column `deskripsi` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty. + - Added the required column `deskripsiSingkat` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty. + - Added the required column `judul` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty. + - Added the required column `linkVideo` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty. + - Added the required column `jadwalPelayanan` to the `Posyandu` table without a default value. This is not possible if the table is not empty. + - Added the required column `icon` to the `ProgramKemiskinan` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "JenisKelamin" AS ENUM ('LAKI_LAKI', 'PEREMPUAN'); + +-- CreateEnum +CREATE TYPE "Agama" AS ENUM ('ISLAM', 'KRISTEN_PROTESTAN', 'KRISTEN_KATOLIK', 'HINDU', 'BUDDHA', 'KONGHUCU', 'LAINNYA'); + +-- CreateEnum +CREATE TYPE "StatusPernikahan" AS ENUM ('BELUM_MENIKAH', 'MENIKAH', 'JANDA_DUDA'); + +-- CreateEnum +CREATE TYPE "UkuranBaju" AS ENUM ('S', 'M', 'L', 'XL', 'XXL', 'LAINNYA'); + +-- CreateEnum +CREATE TYPE "StatusPeminjaman" AS ENUM ('Dipinjam', 'Dikembalikan', 'Terlambat', 'Dibatalkan'); + +-- DropForeignKey +ALTER TABLE "JadwalKegiatan" DROP CONSTRAINT "JadwalKegiatan_pendaftaranJadwalKegiatanId_fkey"; + +-- DropForeignKey +ALTER TABLE "KolaborasiInovasi" DROP CONSTRAINT "KolaborasiInovasi_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "KontakDaruratKeamanan" DROP CONSTRAINT "KontakDaruratKeamanan_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "KontakItem" DROP CONSTRAINT "KontakItem_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "KontakItem" DROP CONSTRAINT "KontakItem_kategoriId_fkey"; + +-- DropForeignKey +ALTER TABLE "MediaSosial" DROP CONSTRAINT "MediaSosial_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "PelayananSuratKeterangan" DROP CONSTRAINT "PelayananSuratKeterangan_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "PencegahanKriminalitas" DROP CONSTRAINT "PencegahanKriminalitas_programKeamananId_fkey"; + +-- DropForeignKey +ALTER TABLE "PencegahanKriminalitas" DROP CONSTRAINT "PencegahanKriminalitas_tipsKeamananId_fkey"; + +-- DropForeignKey +ALTER TABLE "PencegahanKriminalitas" DROP CONSTRAINT "PencegahanKriminalitas_videoKeamananId_fkey"; + +-- DropForeignKey +ALTER TABLE "Penghargaan" DROP CONSTRAINT "Penghargaan_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "PotensiDesa" DROP CONSTRAINT "PotensiDesa_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "hubungan_organisasi" DROP CONSTRAINT "hubungan_organisasi_atasanId_fkey"; + +-- DropForeignKey +ALTER TABLE "hubungan_organisasi" DROP CONSTRAINT "hubungan_organisasi_bawahanId_fkey"; + +-- DropForeignKey +ALTER TABLE "pegawai" DROP CONSTRAINT "pegawai_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "pegawai" DROP CONSTRAINT "pegawai_posisiId_fkey"; + +-- DropForeignKey +ALTER TABLE "struktur_organisasi" DROP CONSTRAINT "struktur_organisasi_hubunganOrganisasiId_fkey"; + +-- DropForeignKey +ALTER TABLE "struktur_organisasi" DROP CONSTRAINT "struktur_organisasi_pegawaiId_fkey"; + +-- DropForeignKey +ALTER TABLE "struktur_organisasi" DROP CONSTRAINT "struktur_organisasi_posisiOrganisasiId_fkey"; + +-- AlterTable +ALTER TABLE "ArtikelKesehatan" ADD COLUMN "imageId" TEXT; + +-- AlterTable +ALTER TABLE "DataKematian_Kelahiran" DROP COLUMN "kelahiranKasar", +DROP COLUMN "kematianBayi", +DROP COLUMN "kematianKasar", +DROP COLUMN "tahun", +ADD COLUMN "kelahiranId" TEXT NOT NULL, +ADD COLUMN "kematianId" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "FileStorage" ADD COLUMN "category" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "GrafikKepuasan" DROP COLUMN "jumlah", +DROP COLUMN "label", +ADD COLUMN "alamat" TEXT NOT NULL, +ADD COLUMN "jenisKelamin" TEXT NOT NULL, +ADD COLUMN "nama" TEXT NOT NULL, +ADD COLUMN "penyakit" TEXT NOT NULL, +ADD COLUMN "tanggal" TIMESTAMP(3) NOT NULL; + +-- AlterTable +ALTER TABLE "JadwalKegiatan" ALTER COLUMN "pendaftaranJadwalKegiatanId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "KolaborasiInovasi" DROP COLUMN "imageId"; + +-- AlterTable +ALTER TABLE "KontakDarurat" ADD COLUMN "whatsapp" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "KontakDaruratKeamanan" DROP COLUMN "imageId", +ADD COLUMN "deletedAt" TIMESTAMP(3), +ADD COLUMN "icon" TEXT NOT NULL, +ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true, +ADD COLUMN "kategoriId" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "KontakItem" DROP COLUMN "imageId", +DROP COLUMN "kategoriId", +ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "icon" TEXT NOT NULL, +ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true; + +-- AlterTable +ALTER TABLE "LaporanPublik" ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true, +ALTER COLUMN "status" SET DEFAULT 'Proses'; + +-- AlterTable +ALTER TABLE "LowonganPekerjaan" ADD COLUMN "notelp" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "MediaSosial" ADD COLUMN "name" TEXT NOT NULL, +ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "PasarDesa" ADD COLUMN "kontak" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "PelayananSuratKeterangan" ADD COLUMN "image2Id" TEXT, +ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "PencegahanKriminalitas" DROP COLUMN "programKeamananId", +DROP COLUMN "tipsKeamananId", +DROP COLUMN "videoKeamananId", +ADD COLUMN "deskripsi" TEXT NOT NULL, +ADD COLUMN "deskripsiSingkat" TEXT NOT NULL, +ADD COLUMN "judul" TEXT NOT NULL, +ADD COLUMN "linkVideo" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "Penghargaan" ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "Posyandu" ADD COLUMN "jadwalPelayanan" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "PotensiDesa" DROP COLUMN "kategori", +ADD COLUMN "kategoriId" TEXT, +ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "ProgramKemiskinan" DROP COLUMN "ikonUrl", +ADD COLUMN "icon" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "StrukturPPID" ADD COLUMN "pegawaiPPIDId" TEXT, +ADD COLUMN "posisiOrganisasiPPIDId" TEXT; + +-- DropTable +DROP TABLE "ProgramKeamanan"; + +-- DropTable +DROP TABLE "TipsKeamanan"; + +-- DropTable +DROP TABLE "VideoKeamanan"; + +-- DropTable +DROP TABLE "hubungan_organisasi"; + +-- DropTable +DROP TABLE "pegawai"; + +-- DropTable +DROP TABLE "posisi_organisasi"; + +-- DropTable +DROP TABLE "struktur_organisasi"; + +-- CreateTable +CREATE TABLE "DesaAntiKorupsi" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "kategoriId" TEXT NOT NULL, + "fileId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "DesaAntiKorupsi_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KategoriDesaAntiKorupsi" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "KategoriDesaAntiKorupsi_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SdgsDesa" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "jumlah" TEXT NOT NULL, + "imageId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "SdgsDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "APBDes" ( + "id" TEXT NOT NULL, + "tahun" INTEGER, + "name" TEXT, + "deskripsi" TEXT, + "jumlah" TEXT, + "imageId" TEXT, + "fileId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "APBDes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "APBDesItem" ( + "id" TEXT NOT NULL, + "kode" TEXT NOT NULL, + "uraian" TEXT NOT NULL, + "anggaran" DOUBLE PRECISION NOT NULL, + "realisasi" DOUBLE PRECISION NOT NULL, + "selisih" DOUBLE PRECISION NOT NULL, + "persentase" DOUBLE PRECISION NOT NULL, + "tipe" TEXT, + "level" INTEGER NOT NULL, + "parentId" TEXT, + "apbdesId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "APBDesItem_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PrestasiDesa" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "kategoriId" TEXT NOT NULL, + "imageId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "PrestasiDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KategoriPrestasiDesa" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "KategoriPrestasiDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Responden" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "tanggal" DATE NOT NULL, + "jenisKelaminId" TEXT NOT NULL, + "ratingId" TEXT NOT NULL, + "kelompokUmurId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Responden_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "JenisKelaminResponden" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "JenisKelaminResponden_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PilihanRatingResponden" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "PilihanRatingResponden_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UmurResponden" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "UmurResponden_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PosisiOrganisasiPPID" ( + "id" TEXT NOT NULL, + "nama" VARCHAR(100) NOT NULL, + "deskripsi" TEXT, + "hierarki" INTEGER NOT NULL, + "parentId" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PosisiOrganisasiPPID_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PegawaiPPID" ( + "id" TEXT NOT NULL, + "namaLengkap" VARCHAR(255) NOT NULL, + "gelarAkademik" VARCHAR(100), + "imageId" TEXT, + "tanggalMasuk" DATE, + "email" VARCHAR(255), + "telepon" VARCHAR(20), + "alamat" TEXT, + "posisiId" VARCHAR(50) NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PegawaiPPID_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "StrukturOrganisasiPPID" ( + "id" TEXT NOT NULL, + "posisiOrganisasiId" VARCHAR(50) NOT NULL, + "pegawaiId" TEXT NOT NULL, + "hubunganOrganisasiId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "StrukturOrganisasiPPID_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PerbekelDariMasaKeMasa" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "periode" TEXT NOT NULL, + "imageId" TEXT, + "daerah" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "PerbekelDariMasaKeMasa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KategoriPotensi" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "KategoriPotensi_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AjukanPermohonan" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "nik" TEXT NOT NULL, + "alamat" TEXT NOT NULL, + "nomorKk" TEXT NOT NULL, + "kategoriId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "AjukanPermohonan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Kelahiran" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "tanggal" TIMESTAMP(3) NOT NULL, + "jenisKelamin" TEXT NOT NULL, + "alamat" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Kelahiran_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Kematian" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "tanggal" TIMESTAMP(3) NOT NULL, + "jenisKelamin" TEXT NOT NULL, + "alamat" TEXT NOT NULL, + "penyebab" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Kematian_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KontakDaruratToItem" ( + "id" TEXT NOT NULL, + "kontakDaruratId" TEXT NOT NULL, + "kontakItemId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "KontakDaruratToItem_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "StrukturBumDes" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "imageId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "posisiOrganisasiBumDesId" TEXT, + "pegawaiBumDesId" TEXT, + + CONSTRAINT "StrukturBumDes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PosisiOrganisasiBumDes" ( + "id" TEXT NOT NULL, + "nama" VARCHAR(100) NOT NULL, + "deskripsi" TEXT, + "hierarki" INTEGER NOT NULL, + "parentId" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PosisiOrganisasiBumDes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PegawaiBumDes" ( + "id" TEXT NOT NULL, + "namaLengkap" VARCHAR(255) NOT NULL, + "gelarAkademik" VARCHAR(100), + "imageId" TEXT, + "tanggalMasuk" DATE, + "email" VARCHAR(255), + "telepon" VARCHAR(20), + "alamat" TEXT, + "posisiId" VARCHAR(50) NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PegawaiBumDes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "StrukturOrganisasiBumDes" ( + "id" TEXT NOT NULL, + "posisiOrganisasiId" VARCHAR(50) NOT NULL, + "pegawaiId" TEXT NOT NULL, + "hubunganOrganisasiId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "StrukturOrganisasiBumDes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MitraKolaborasi" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "imageId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "MitraKolaborasi_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "JenjangPendidikan" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "JenjangPendidikan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Lembaga" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "jenjangId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Lembaga_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Siswa" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "lembagaId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Siswa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Pengajar" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "lembagaId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Pengajar_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KeunggulanProgram" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "KeunggulanProgram_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "BeasiswaPendaftar" ( + "id" TEXT NOT NULL, + "namaLengkap" TEXT NOT NULL, + "nis" TEXT, + "kelas" TEXT, + "jenisKelamin" "JenisKelamin" NOT NULL, + "alamatDomisili" TEXT, + "tempatLahir" TEXT NOT NULL, + "tanggalLahir" TIMESTAMP(3) NOT NULL, + "namaOrtu" TEXT, + "nik" TEXT NOT NULL, + "pekerjaanOrtu" TEXT, + "penghasilan" TEXT, + "noHp" TEXT NOT NULL, + "kewarganegaraan" TEXT, + "agama" "Agama", + "alamatKTP" TEXT, + "email" TEXT, + "statusPernikahan" "StatusPernikahan", + "ukuranBaju" "UkuranBaju", + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "BeasiswaPendaftar_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TujuanProgram" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "TujuanProgram_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProgramUnggulan" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "ProgramUnggulan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TujuanBimbinganBelajarDesa" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "TujuanBimbinganBelajarDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "LokasiJadwalBimbinganBelajarDesa" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "LokasiJadwalBimbinganBelajarDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "FasilitasBimbinganBelajarDesa" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "FasilitasBimbinganBelajarDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TujuanPendidikanNonFormal" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "TujuanPendidikanNonFormal_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TempatKegiatan" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "TempatKegiatan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "JenisProgramYangDiselenggarakan" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "JenisProgramYangDiselenggarakan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DataPerpustakaan" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "kategoriId" TEXT NOT NULL, + "imageId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "DataPerpustakaan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KategoriBuku" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "KategoriBuku_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PeminjamanBuku" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "noTelp" TEXT NOT NULL, + "alamat" TEXT NOT NULL, + "bukuId" TEXT NOT NULL, + "tanggalPinjam" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "batasKembali" TIMESTAMP(3) NOT NULL, + "tanggalKembali" TIMESTAMP(3), + "status" "StatusPeminjaman" NOT NULL DEFAULT 'Dipinjam', + "catatan" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "PeminjamanBuku_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "username" TEXT NOT NULL, + "nomor" TEXT NOT NULL, + "roleId" TEXT NOT NULL DEFAULT '1', + "instansi" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "lastLogin" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "roles" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "permissions" JSONB NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + + CONSTRAINT "roles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KodeOtp" ( + "id" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "nomor" TEXT NOT NULL, + "otp" INTEGER NOT NULL, + + CONSTRAINT "KodeOtp_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "permissions" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "permissions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserSession" ( + "id" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3), + "active" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + + CONSTRAINT "UserSession_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DataPendidikan" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "jumlah" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "DataPendidikan_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "DesaAntiKorupsi_name_key" ON "DesaAntiKorupsi"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "KategoriDesaAntiKorupsi_name_key" ON "KategoriDesaAntiKorupsi"("name"); + +-- CreateIndex +CREATE INDEX "APBDesItem_kode_idx" ON "APBDesItem"("kode"); + +-- CreateIndex +CREATE INDEX "APBDesItem_level_idx" ON "APBDesItem"("level"); + +-- CreateIndex +CREATE INDEX "APBDesItem_apbdesId_idx" ON "APBDesItem"("apbdesId"); + +-- CreateIndex +CREATE UNIQUE INDEX "KategoriPrestasiDesa_name_key" ON "KategoriPrestasiDesa"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "Responden_name_key" ON "Responden"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "JenisKelaminResponden_name_key" ON "JenisKelaminResponden"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "PilihanRatingResponden_name_key" ON "PilihanRatingResponden"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "UmurResponden_name_key" ON "UmurResponden"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "PegawaiPPID_email_key" ON "PegawaiPPID"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "PegawaiBumDes_email_key" ON "PegawaiBumDes"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "BeasiswaPendaftar_nik_key" ON "BeasiswaPendaftar"("nik"); + +-- CreateIndex +CREATE UNIQUE INDEX "BeasiswaPendaftar_email_key" ON "BeasiswaPendaftar"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_nomor_key" ON "User"("nomor"); + +-- CreateIndex +CREATE UNIQUE INDEX "roles_name_key" ON "roles"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "permissions_name_key" ON "permissions"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserSession_userId_key" ON "UserSession"("userId"); + +-- AddForeignKey +ALTER TABLE "MediaSosial" ADD CONSTRAINT "MediaSosial_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DesaAntiKorupsi" ADD CONSTRAINT "DesaAntiKorupsi_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriDesaAntiKorupsi"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DesaAntiKorupsi" ADD CONSTRAINT "DesaAntiKorupsi_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SdgsDesa" ADD CONSTRAINT "SdgsDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APBDes" ADD CONSTRAINT "APBDes_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APBDes" ADD CONSTRAINT "APBDes_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APBDesItem" ADD CONSTRAINT "APBDesItem_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "APBDesItem"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APBDesItem" ADD CONSTRAINT "APBDesItem_apbdesId_fkey" FOREIGN KEY ("apbdesId") REFERENCES "APBDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PrestasiDesa" ADD CONSTRAINT "PrestasiDesa_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriPrestasiDesa"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PrestasiDesa" ADD CONSTRAINT "PrestasiDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Responden" ADD CONSTRAINT "Responden_jenisKelaminId_fkey" FOREIGN KEY ("jenisKelaminId") REFERENCES "JenisKelaminResponden"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Responden" ADD CONSTRAINT "Responden_ratingId_fkey" FOREIGN KEY ("ratingId") REFERENCES "PilihanRatingResponden"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Responden" ADD CONSTRAINT "Responden_kelompokUmurId_fkey" FOREIGN KEY ("kelompokUmurId") REFERENCES "UmurResponden"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturPPID" ADD CONSTRAINT "StrukturPPID_posisiOrganisasiPPIDId_fkey" FOREIGN KEY ("posisiOrganisasiPPIDId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturPPID" ADD CONSTRAINT "StrukturPPID_pegawaiPPIDId_fkey" FOREIGN KEY ("pegawaiPPIDId") REFERENCES "PegawaiPPID"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PosisiOrganisasiPPID" ADD CONSTRAINT "PosisiOrganisasiPPID_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PegawaiPPID" ADD CONSTRAINT "PegawaiPPID_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PegawaiPPID" ADD CONSTRAINT "PegawaiPPID_posisiId_fkey" FOREIGN KEY ("posisiId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturOrganisasiPPID" ADD CONSTRAINT "StrukturOrganisasiPPID_posisiOrganisasiId_fkey" FOREIGN KEY ("posisiOrganisasiId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturOrganisasiPPID" ADD CONSTRAINT "StrukturOrganisasiPPID_pegawaiId_fkey" FOREIGN KEY ("pegawaiId") REFERENCES "PegawaiPPID"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PerbekelDariMasaKeMasa" ADD CONSTRAINT "PerbekelDariMasaKeMasa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PotensiDesa" ADD CONSTRAINT "PotensiDesa_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriPotensi"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PotensiDesa" ADD CONSTRAINT "PotensiDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PelayananSuratKeterangan" ADD CONSTRAINT "PelayananSuratKeterangan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PelayananSuratKeterangan" ADD CONSTRAINT "PelayananSuratKeterangan_image2Id_fkey" FOREIGN KEY ("image2Id") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AjukanPermohonan" ADD CONSTRAINT "AjukanPermohonan_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "PelayananSuratKeterangan"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Penghargaan" ADD CONSTRAINT "Penghargaan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "JadwalKegiatan" ADD CONSTRAINT "JadwalKegiatan_pendaftaranJadwalKegiatanId_fkey" FOREIGN KEY ("pendaftaranJadwalKegiatanId") REFERENCES "PendaftaranJadwalKegiatan"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DataKematian_Kelahiran" ADD CONSTRAINT "DataKematian_Kelahiran_kematianId_fkey" FOREIGN KEY ("kematianId") REFERENCES "Kematian"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DataKematian_Kelahiran" ADD CONSTRAINT "DataKematian_Kelahiran_kelahiranId_fkey" FOREIGN KEY ("kelahiranId") REFERENCES "Kelahiran"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "KontakDaruratKeamanan" ADD CONSTRAINT "KontakDaruratKeamanan_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KontakItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "KontakDaruratToItem" ADD CONSTRAINT "KontakDaruratToItem_kontakDaruratId_fkey" FOREIGN KEY ("kontakDaruratId") REFERENCES "KontakDaruratKeamanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "KontakDaruratToItem" ADD CONSTRAINT "KontakDaruratToItem_kontakItemId_fkey" FOREIGN KEY ("kontakItemId") REFERENCES "KontakItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturBumDes" ADD CONSTRAINT "StrukturBumDes_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturBumDes" ADD CONSTRAINT "StrukturBumDes_posisiOrganisasiBumDesId_fkey" FOREIGN KEY ("posisiOrganisasiBumDesId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturBumDes" ADD CONSTRAINT "StrukturBumDes_pegawaiBumDesId_fkey" FOREIGN KEY ("pegawaiBumDesId") REFERENCES "PegawaiBumDes"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PosisiOrganisasiBumDes" ADD CONSTRAINT "PosisiOrganisasiBumDes_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PegawaiBumDes" ADD CONSTRAINT "PegawaiBumDes_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PegawaiBumDes" ADD CONSTRAINT "PegawaiBumDes_posisiId_fkey" FOREIGN KEY ("posisiId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturOrganisasiBumDes" ADD CONSTRAINT "StrukturOrganisasiBumDes_posisiOrganisasiId_fkey" FOREIGN KEY ("posisiOrganisasiId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturOrganisasiBumDes" ADD CONSTRAINT "StrukturOrganisasiBumDes_pegawaiId_fkey" FOREIGN KEY ("pegawaiId") REFERENCES "PegawaiBumDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MitraKolaborasi" ADD CONSTRAINT "MitraKolaborasi_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Lembaga" ADD CONSTRAINT "Lembaga_jenjangId_fkey" FOREIGN KEY ("jenjangId") REFERENCES "JenjangPendidikan"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Siswa" ADD CONSTRAINT "Siswa_lembagaId_fkey" FOREIGN KEY ("lembagaId") REFERENCES "Lembaga"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Pengajar" ADD CONSTRAINT "Pengajar_lembagaId_fkey" FOREIGN KEY ("lembagaId") REFERENCES "Lembaga"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DataPerpustakaan" ADD CONSTRAINT "DataPerpustakaan_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriBuku"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DataPerpustakaan" ADD CONSTRAINT "DataPerpustakaan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PeminjamanBuku" ADD CONSTRAINT "PeminjamanBuku_bukuId_fkey" FOREIGN KEY ("bukuId") REFERENCES "DataPerpustakaan"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserSession" ADD CONSTRAINT "UserSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7aa39b9a..099cb7f9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -183,41 +183,41 @@ model SdgsDesa { //========================================= APBDes ========================================= // model APBDes { - id String @id @default(cuid()) - tahun Int? - name String? // misalnya: "APBDes Tahun 2025" - deskripsi String? - jumlah String? // total keseluruhan (opsional, bisa juga dihitung dari items) - items APBDesItem[] - image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id]) - imageId String? - file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id]) - fileId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? // opsional, tidak perlu default now() - isActive Boolean @default(true) + id String @id @default(cuid()) + tahun Int? + name String? // misalnya: "APBDes Tahun 2025" + deskripsi String? + jumlah String? // total keseluruhan (opsional, bisa juga dihitung dari items) + items APBDesItem[] + image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id]) + imageId String? + file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id]) + fileId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? // opsional, tidak perlu default now() + isActive Boolean @default(true) } model APBDesItem { - id String @id @default(cuid()) - kode String // contoh: "4", "4.1", "4.1.2" - 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) - realisasi Float - selisih Float // realisasi - anggaran - persentase Float - tipe String? // (realisasi / anggaran) * 100 - level Int // 1 = kelompok utama, 2 = sub-kelompok, 3 = detail - parentId String? // untuk relasi hierarki - parent APBDesItem? @relation("APBDesItemParent", fields: [parentId], references: [id]) - children APBDesItem[] @relation("APBDesItemParent") - apbdesId String - apbdes APBDes @relation(fields: [apbdesId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - isActive Boolean @default(true) + id String @id @default(cuid()) + kode String // contoh: "4", "4.1", "4.1.2" + 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) + realisasi Float + selisih Float // realisasi - anggaran + persentase Float + tipe String? // (realisasi / anggaran) * 100 + level Int // 1 = kelompok utama, 2 = sub-kelompok, 3 = detail + parentId String? // untuk relasi hierarki + parent APBDesItem? @relation("APBDesItemParent", fields: [parentId], references: [id]) + children APBDesItem[] @relation("APBDesItemParent") + apbdesId String + apbdes APBDes @relation(fields: [apbdesId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + isActive Boolean @default(true) @@index([kode]) @@index([level]) @@ -2164,7 +2164,7 @@ enum StatusPeminjaman { model User { id String @id @default(cuid()) - username String + username String @unique nomor String @unique role Role @relation(fields: [roleId], references: [id]) roleId String @default("1") diff --git a/src/app/admin/(dashboard)/auth/login-admin/page.tsx b/src/app/admin/(dashboard)/auth/login-admin/page.tsx index ab591207..a44f35b0 100644 --- a/src/app/admin/(dashboard)/auth/login-admin/page.tsx +++ b/src/app/admin/(dashboard)/auth/login-admin/page.tsx @@ -1,104 +1,98 @@ -'use client' -import { apiFetchLogin } from '@/app/admin/auth/_lib/api_fetch_auth'; +'use client'; +import { apiFetchLogin } from '@/app/api/auth/_lib/api_fetch_auth'; import colors from '@/con/colors'; -import { Box, Button, Center, Flex, Image, Paper, Stack, Text, Title } from '@mantine/core'; -import Link from 'next/link'; +import { Box, Button, Center, Image, Paper, Stack, Title } from '@mantine/core'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; -import { PhoneInput } from "react-international-phone"; -import "react-international-phone/style.css"; +import { PhoneInput } from 'react-international-phone'; +import 'react-international-phone/style.css'; import { toast } from 'react-toastify'; - - function Login() { - const router = useRouter() - const [phone, setPhone] = useState("") - const [isError, setError] = useState(false) - const [loading, setLoading] = useState(false) + const router = useRouter(); + const [phone, setPhone] = useState(''); + const [loading, setLoading] = useState(false); + // Login.tsx async function onLogin() { - const nomor = phone.substring(1); - if (nomor.length <= 4) return setError(true) - + const cleanPhone = phone.replace(/\D/g, ''); + if (cleanPhone.length < 10) { + toast.error('Nomor telepon tidak valid'); + return; + } try { setLoading(true); - const response = await apiFetchLogin({ nomor: nomor }) - if (response && response.success) { - localStorage.setItem("hipmi_auth_code_id", response.kodeId); - toast.success(response.message); - router.push("/validasi", { scroll: false }); + const response = await apiFetchLogin({ nomor: cleanPhone }); + + if (!response.success) { + toast.error(response.message || 'Gagal memproses login'); + 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 { - setLoading(false); - toast.error(response?.message); + // ❌ User baru: langsung ke registrasi (tanpa kodeId) + router.push('/registrasi'); } } catch (error) { - setLoading(false) - console.log("Error Login", error) - toast.error("Terjadi kesalahan saat login") + console.error('Error Login:', error); + toast.error('Terjadi kesalahan saat login'); + } finally { + setLoading(false); } } return ( - + - - - + + + - + <Title ta="center" order={2} fw="bold" c={colors['blue-button']}> Login
- + Logo
- - {/* - Masuk Untuk Akses Admin - setUsername(e.target.value)} - required - /> - */} + { - setPhone(val); - }} + value={phone} + onChange={(val) => setPhone(val)} /> - {isError ? ( - toast.error("Masukan nomor telepon anda") - ) : ( - "" - )} - + - - Belum punya akun? - -
@@ -108,4 +102,4 @@ function Login() { ); } -export default Login; +export default Login; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/auth/registrasi-admin/page.tsx b/src/app/admin/(dashboard)/auth/registrasi-admin/page.tsx index 62d2554b..537ae814 100644 --- a/src/app/admin/(dashboard)/auth/registrasi-admin/page.tsx +++ b/src/app/admin/(dashboard)/auth/registrasi-admin/page.tsx @@ -1,113 +1,127 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions */ -'use client' -import { apiFetchRegister } from '@/app/admin/auth/_lib/api_fetch_auth'; +// app/registrasi/page.tsx +'use client'; + +import { apiFetchRegister } from '@/app/api/auth/_lib/api_fetch_auth'; import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto'; 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 { useState } from 'react'; -import { PhoneInput } from "react-international-phone"; -import "react-international-phone/style.css"; +import { useEffect, useState } from 'react'; +import { PhoneInput } from 'react-international-phone'; +import 'react-international-phone/style.css'; import { toast } from 'react-toastify'; -function Registrasi() { - const [phone, setPhone] = useState("") - const router = useRouter() - const [value, setValue] = useState("") - const [isValue, setIsValue] = useState(false); +export default function Registrasi() { + const router = useRouter(); + const [username, setUsername] = useState(''); const [loading, setLoading] = useState(false); + const [phone, setPhone] = useState(''); // ✅ tambahkan state untuk phone - async function onRegistarsi() { - if (value.length < 5) { - toast.error("Username minimal 5 karakter!"); + // Ambil data dari localStorage (dari login) + useEffect(() => { + const storedNomor = localStorage.getItem('auth_nomor'); + if (!storedNomor) { + toast.error('Akses tidak valid'); + router.push('/login'); return; } - - if (value.includes(" ")) { - toast.error("Username tidak boleh ada spasi!"); + setPhone(storedNomor); + }, [router]); + + const handleRegister = async () => { + if (!username || username.trim().length < 5) { + toast.error('Username minimal 5 karakter!'); return; } - - if (!phone) { - toast.error("Nomor telepon wajib diisi!"); + if (username.includes(' ')) { + toast.error('Username tidak boleh ada spasi!'); return; } - + + const cleanPhone = phone.replace(/\D/g, ''); + if (cleanPhone.length < 10) { + toast.error('Nomor tidak valid!'); + return; + } + try { 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) { - router.push("/login", { scroll: false }); - toast.success(respone.message); + if (response.success) { + // Simpan sementara + localStorage.setItem('auth_kodeId', response.kodeId); + localStorage.setItem('auth_username', username); // simpan username - } else { - setLoading(false); - toast.error(respone.message); + toast.success('Kode verifikasi dikirim!'); + router.push('/validasi'); // ✅ ke halaman validasi } } catch (error) { + console.error('Error Registrasi:', error); + toast.error('Gagal mengirim OTP'); + } finally { setLoading(false); - console.log("Error Registrasi", error); } - } + }; + return ( - + - - - - + <Stack justify="center" align="center" h="80vh"> + <Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '100%', sm: 400 }}> + <Stack align="center"> + <Title order={2} fw="bold" c={colors['blue-button']}> Registrasi
- +
- - + setUsername(e.currentTarget.value)} error={ - value.length > 0 && value.length < 5 - ? "Minimal 5 karakter !" - : value.includes(" ") - ? "Tidak boleh ada spasi" - : isValue - ? "Masukan username anda" - : "" + username.length > 0 && username.length < 5 + ? 'Minimal 5 karakter!' + : username.includes(' ') + ? 'Tidak boleh ada spasi' + : '' } - onChange={(val) => { - val.currentTarget.value.length > 0 ? setIsValue(false) : ""; - setValue(val.currentTarget.value); - }} required - /> - - Nomor Telepon + + + Nomor Telepon { - setPhone(val); - }} + value={phone} + disabled /> - - + + + - - + + +
@@ -116,6 +130,4 @@ function Registrasi() {
); -} - -export default Registrasi; +} \ No newline at end of file diff --git a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx index 862edb33..d5180665 100644 --- a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx +++ b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx @@ -1,31 +1,177 @@ -'use client' -import colors from '@/con/colors'; -import { Box, Button, Paper, PinInput, Stack, Text, Title } from '@mantine/core'; -import { useRouter } from 'next/navigation'; +// app/validasi/page.tsx +'use client'; + +import { apiFetchOtpData, apiFetchVerifyOtp } from '@/app/api/auth/_lib/api_fetch_auth'; +import colors from '@/con/colors'; +import { Box, Button, Loader, Paper, PinInput, Stack, Text, Title } from '@mantine/core'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; + +export default function Validasi() { + const router = useRouter(); + const [nomor, setNomor] = useState(null); + const [otp, setOtp] = useState(''); + const [loading, setLoading] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [kodeId, setKodeId] = useState(null); + + useEffect(() => { + const storedKodeId = localStorage.getItem('auth_kodeId'); + if (!storedKodeId) { + toast.error('Akses tidak valid'); + router.push('/login'); + return; + } + + setKodeId(storedKodeId); + + const fetchOtpData = async () => { + try { + const result = await apiFetchOtpData({ kodeId: storedKodeId }); + if (result.success && result.data?.nomor) { + setNomor(result.data.nomor); + } else { + throw new Error('OTP tidak valid'); + } + } catch (error) { + console.error('Gagal muat OTP:', error); + toast.error('Kode verifikasi tidak valid'); + router.push('/login'); + } finally { + setIsLoading(false); + } + }; + + fetchOtpData(); + }, [router]); + + const handleVerify = async () => { + if (!kodeId || !nomor || otp.length < 4) return; + + try { + setLoading(true); + const verifyResult = await apiFetchVerifyOtp({ nomor, otp, kodeId }); + + if (verifyResult.success) { + cleanupStorage(); + router.push('/admin/landing-page/profil/program-inovasi'); + return; // ✅ HENTIKAN eksekusi di sini + } + + // Hanya coba registrasi jika akun tidak ditemukan + if (verifyResult.status === 404 && verifyResult.message?.includes('Akun tidak ditemukan')) { + const username = localStorage.getItem('auth_username'); + if (!username) { + toast.error('Data registrasi hilang'); + return; + } + + const regRes = await fetch('/api/auth/finalize-registration', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nomor, username, otp, kodeId }), + }); + + const regData = await regRes.json(); + if (regData.success) { + cleanupStorage(); + router.push('/admin/landing-page/profil/program-inovasi'); + } else { + toast.error(regData.message || 'Registrasi gagal'); + } + } else { + // Hanya tampilkan error jika bukan kasus "akun tidak ditemukan" + toast.error(verifyResult.message || 'Verifikasi gagal'); + } + } catch (error) { + console.error('Verifikasi error:', error); + toast.error('Terjadi kesalahan'); + } finally { + setLoading(false); + } + }; + + 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-otp', { + 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'); + } + } catch { + toast.error('Gagal kirim ulang'); + } + }; + + if (isLoading) { + return ( + + + + ); + } + + if (!nomor) return null; -function Validasi() { - const router = useRouter() return ( - + - - - + + + - + <Title ta="center" order={2} fw="bold" c={colors['blue-button']}> Kode Verifikasi + + Kami telah mengirim kode ke nomor {nomor} + - - Masukkan Kode Verifikasi - + + + Masukkan Kode Verifikasi + + - - + + + Tidak menerima kode?{' '} + - + @@ -33,6 +179,4 @@ function Validasi() { ); -} - -export default Validasi; +} \ No newline at end of file diff --git a/src/app/admin/auth/_lib/api_fetch_auth.ts b/src/app/admin/auth/_lib/api_fetch_auth.ts deleted file mode 100644 index 55c1e63b..00000000 --- a/src/app/admin/auth/_lib/api_fetch_auth.ts +++ /dev/null @@ -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); -}; diff --git a/src/app/api/[[...slugs]]/_lib/auth/login/route.ts b/src/app/api/[[...slugs]]/_lib/auth/login/route.ts index dc002998..e7aca89a 100644 --- a/src/app/api/[[...slugs]]/_lib/auth/login/route.ts +++ b/src/app/api/[[...slugs]]/_lib/auth/login/route.ts @@ -1,5 +1,4 @@ import prisma from "@/lib/prisma"; - import { NextResponse } from "next/server"; import { randomOTP } from "../_lib/randomOTP"; @@ -12,52 +11,70 @@ export async function POST(req: Request) { } try { - const codeOtp = randomOTP(); const body = await req.json(); const { nomor } = body; - const res = await fetch( - `https://wa.wibudev.com/code?nom=${nomor}&text=Website Desa Darmasaba - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun Admin lainnya. - \n - >> Kode OTP anda: ${codeOtp}. - ` - ); - const sendWa = await res.json(); - - if (sendWa.status !== "success") + if (!nomor || typeof nomor !== "string") { return NextResponse.json( - { success: false, message: "Nomor Whatsapp Tidak Aktif" }, + { success: false, message: "Nomor tidak valid" }, { status: 400 } ); + } - const createOtpId = await prisma.kodeOtp.create({ - data: { - nomor: nomor, - otp: codeOtp, - }, + // Cek apakah user sudah terdaftar + const existingUser = await prisma.user.findUnique({ + where: { nomor }, + 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( - { success: false, message: "Gagal mengirim kode OTP" }, + { success: false, message: "Nomor WhatsApp tidak aktif" }, { 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( { success: true, message: "Kode verifikasi terkirim", - kodeId: createOtpId.id, + kodeId: otpRecord.id, + isRegistered, // 🔑 Ini kunci untuk frontend tahu harus ke register atau verifikasi }, { status: 200 } ); } catch (error) { - console.log("Error Login", error); + console.error("Error Login:", error); 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 } ); } finally { await prisma.$disconnect(); } -} +} \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/auth/me/route.ts b/src/app/api/[[...slugs]]/_lib/auth/me/route.ts new file mode 100644 index 00000000..4ea5d2fd --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/auth/me/route.ts @@ -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 }); +} \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/auth/register/route.ts b/src/app/api/[[...slugs]]/_lib/auth/register/route.ts new file mode 100644 index 00000000..cf2de7f0 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/auth/register/route.ts @@ -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(); +// } +// } \ No newline at end of file diff --git a/src/app/api/auth/_lib/api_fetch_auth.ts b/src/app/api/auth/_lib/api_fetch_auth.ts new file mode 100644 index 00000000..38d76ddb --- /dev/null +++ b/src/app/api/auth/_lib/api_fetch_auth.ts @@ -0,0 +1,152 @@ +/* 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/send-otp-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; +}; + +// export const apiFetchVerifyOtp = async ({ +// nomor, +// otp, +// kodeId +// }: { +// nomor: string; +// otp: string; +// kodeId: string; +// }) => { +// if (!nomor || !otp || !kodeId) { +// throw new Error('Data verifikasi tidak lengkap'); +// } + +// if (!/^\d{4,6}$/.test(otp)) { +// throw new Error('Kode OTP harus 4-6 digit angka'); +// } + +// const response = await fetch('/api/auth/verify-otp', { +// method: 'POST', +// headers: { 'Content-Type': 'application/json' }, +// body: JSON.stringify({ nomor, otp, kodeId }), +// }); + +// const data = await response.json(); + +// if (!response.ok) { +// throw new Error(data.message || 'Verifikasi OTP gagal'); +// } + +// return data; +// }; + +export const apiFetchVerifyOtp = async ({ + nomor, + otp, + kodeId +}: { + nomor: string; + otp: string; + kodeId: string; +}) => { + if (!nomor || !otp || !kodeId) { + throw new Error('Data verifikasi tidak lengkap'); + } + + if (!/^\d{4,6}$/.test(otp)) { + throw new Error('Kode OTP harus 4-6 digit angka'); + } + + const response = await fetch('/api/auth/verify-otp', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nomor, otp, kodeId }), + }); + + const data = await response.json(); + + // ✅ Jangan throw error untuk status 4xx — biarkan frontend handle + return { + success: response.ok, + ...data, + status: response.status, + }; +}; \ No newline at end of file diff --git a/src/app/api/auth/_lib/decrypt.ts b/src/app/api/auth/_lib/decrypt.ts index e36a5342..fc7893b2 100644 --- a/src/app/api/auth/_lib/decrypt.ts +++ b/src/app/api/auth/_lib/decrypt.ts @@ -1,50 +1,32 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { jwtVerify } from "jose"; export async function decrypt({ token, - encodedKey, + jwtSecret, }: { token: string; - encodedKey: string; -}): Promise | null> { - if (!token || !encodedKey) { - console.error("Missing required parameters:", { - hasToken: !!token, - hasEncodedKey: !!encodedKey, - }); - return null; - } + jwtSecret: string; +}): Promise | null> { + if (!token || !jwtSecret) return null; try { - const enc = new TextEncoder().encode(encodedKey); - const { payload } = await jwtVerify(token, enc, { + const secret = new TextEncoder().encode(jwtSecret); + const { payload } = await jwtVerify(token, secret, { algorithms: ["HS256"], }); - if (!payload || !payload.user) { - console.error("Invalid payload structure:", { - hasPayload: !!payload, - hasUser: payload ? !!payload.user : false, - }); + if ( + typeof payload !== "object" || + payload === null || + !("user" in payload) || + typeof payload.user !== "object" + ) { return null; } - // Logging untuk debug - // console.log("Decrypt successful:", { - // payloadExists: !!payload, - // userExists: !!payload.user, - // tokenPreview: token.substring(0, 10) + "...", - // }); - - return payload.user as Record; + return payload.user as Record; } catch (error) { - console.error("Token verification failed:", { - error, - tokenLength: token?.length, - errorName: error instanceof Error ? error.name : "Unknown error", - errorMessage: error instanceof Error ? error.message : String(error), - }); + console.error("JWT Decrypt failed:", error); return null; } -} +} \ No newline at end of file diff --git a/src/app/api/auth/_lib/encrypt.ts b/src/app/api/auth/_lib/encrypt.ts index cde1ebd9..c17970df 100644 --- a/src/app/api/auth/_lib/encrypt.ts +++ b/src/app/api/auth/_lib/encrypt.ts @@ -1,26 +1,23 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { SignJWT } from "jose"; export async function encrypt({ user, - exp = "7 year", - encodedKey, + exp = "7d", + jwtSecret, }: { - user: Record; - exp?: string; - encodedKey: string; + user: Record; + exp?: string | number; + jwtSecret: string; }): Promise { try { - const enc = new TextEncoder().encode(encodedKey); + const secret = new TextEncoder().encode(jwtSecret); return new SignJWT({ user }) .setProtectedHeader({ alg: "HS256" }) .setIssuedAt() .setExpirationTime(exp) - .sign(enc); + .sign(secret); } catch (error) { - console.error("Gagal mengenkripsi", error); + console.error("JWT Encrypt failed:", error); return null; } -} - -// wibu:0.2.82 +} \ No newline at end of file diff --git a/src/app/api/auth/_lib/session_create.ts b/src/app/api/auth/_lib/session_create.ts index fffd75cc..2d14b8af 100644 --- a/src/app/api/auth/_lib/session_create.ts +++ b/src/app/api/auth/_lib/session_create.ts @@ -1,36 +1,42 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { cookies } from "next/headers"; import { encrypt } from "./encrypt"; export async function sessionCreate({ sessionKey, exp = "7 year", - encodedKey, + jwtSecret, user, }: { sessionKey: string; exp?: string; - encodedKey: string; + jwtSecret: string; user: Record; }) { + // 🔒 Validasi kunci tidak kosong + if (!sessionKey || sessionKey.length === 0) { + throw new Error("sessionKey tidak boleh kosong"); + } + if (!jwtSecret || jwtSecret.length === 0) { + throw new Error("jwtSecret tidak boleh kosong"); + } + const token = await encrypt({ exp, - encodedKey, + jwtSecret, user, }); - const cookie: any = { - key: sessionKey, - value: token, - options: { - httpOnly: true, - sameSite: "lax", - path: "/", - }, - }; + if (token === null) { + throw new Error("Token generation failed"); + } + + const cookieStore = await cookies(); + cookieStore.set(sessionKey, token, { + httpOnly: true, + sameSite: "lax", + path: "/", + secure: process.env.NODE_ENV === "production", + }); - (await cookies()).set(cookie.key, cookie.value, { ...cookie.options }); return token; -} - -// wibu:0.2.82 +} \ No newline at end of file diff --git a/src/app/api/auth/finalize-registration/route.ts b/src/app/api/auth/finalize-registration/route.ts new file mode 100644 index 00000000..3736ab5a --- /dev/null +++ b/src/app/api/auth/finalize-registration/route.ts @@ -0,0 +1,40 @@ +// 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(); + + // Verifikasi OTP (sama seperti verify-otp) + const otpRecord = await prisma.kodeOtp.findUnique({ where: { id: kodeId } }); + if (!otpRecord?.isActive || otpRecord.nomor !== nomor) { + return NextResponse.json({ success: false, message: 'OTP tidak valid' }, { status: 400 }); + } + + // Buat user + const user = await prisma.user.create({ + data: { username, nomor, isActive: true } + }); + + // Nonaktifkan OTP + await prisma.kodeOtp.update({ where: { id: kodeId }, data: { isActive: false } }); + + // Buat session + const token = await sessionCreate({ + sessionKey: process.env.BASE_SESSION_KEY!, + jwtSecret: process.env.BASE_TOKEN_KEY!, + user: { id: user.id, nomor: user.nomor, username: user.username, roleId: user.roleId, isActive: true }, + }); + + const response = NextResponse.json({ success: true, roleId: user.roleId }); + response.cookies.set(process.env.BASE_SESSION_KEY!, token, { /* options */ }); + return response; + } catch (error) { + console.error('Finalize Registration Error:', error); + return NextResponse.json({ success: false, message: 'Registrasi gagal' }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} \ No newline at end of file diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index dc002998..96333148 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -1,5 +1,5 @@ +// app/api/auth/login/route.ts import prisma from "@/lib/prisma"; - import { NextResponse } from "next/server"; import { randomOTP } from "../_lib/randomOTP"; @@ -12,52 +12,66 @@ export async function POST(req: Request) { } try { - const codeOtp = randomOTP(); - const body = await req.json(); - const { nomor } = body; - const res = await fetch( - `https://wa.wibudev.com/code?nom=${nomor}&text=Website Desa Darmasaba - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun Admin lainnya. - \n - >> Kode OTP anda: ${codeOtp}. - ` - ); + const { nomor } = await req.json(); - const sendWa = await res.json(); - - if (sendWa.status !== "success") + if (!nomor || typeof nomor !== "string") { return NextResponse.json( - { success: false, message: "Nomor Whatsapp Tidak Aktif" }, + { success: false, message: "Nomor tidak valid" }, { status: 400 } ); + } - const createOtpId = await prisma.kodeOtp.create({ - data: { - nomor: nomor, - otp: codeOtp, - }, + const existingUser = await prisma.user.findUnique({ + where: { nomor }, + select: { id: true, isActive: true }, }); - if (!createOtpId) - return NextResponse.json( - { success: false, message: "Gagal mengirim kode OTP" }, - { status: 400 } - ); + const isRegistered = !!existingUser; - 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, - message: "Kode verifikasi terkirim", + message: "Kode verifikasi dikirim", kodeId: createOtpId.id, - }, - { status: 200 } - ); + isRegistered: true, + }); + } else { + // ❌ User belum terdaftar → JANGAN kirim OTP + return NextResponse.json({ + success: true, + message: "Nomor belum terdaftar", + isRegistered: false, + // Tidak ada kodeId + }); + } } catch (error) { - console.log("Error Login", error); + console.error("Error Login:", error); return NextResponse.json( - { success: false, message: "Terjadi masalah saat login" , reason: error as Error }, + { success: false, message: "Terjadi kesalahan saat login" }, { status: 500 } ); } finally { await prisma.$disconnect(); } -} +} \ No newline at end of file diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts new file mode 100644 index 00000000..e2795695 --- /dev/null +++ b/src/app/api/auth/me/route.ts @@ -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("desadarmasaba_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 }); +} \ No newline at end of file diff --git a/src/app/api/auth/otp-data/route.ts b/src/app/api/auth/otp-data/route.ts new file mode 100644 index 00000000..f1d6aef5 --- /dev/null +++ b/src/app/api/auth/otp-data/route.ts @@ -0,0 +1,41 @@ +// app/api/auth/otp-data/route.ts +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; + +export async function POST(req: Request) { + try { + const { kodeId } = await req.json(); + + if (!kodeId) { + return NextResponse.json( + { success: false, message: "Kode ID tidak diberikan" }, + { status: 400 } + ); + } + + const otpRecord = await prisma.kodeOtp.findUnique({ + where: { id: kodeId }, + select: { id: true, nomor: true, isActive: true, createdAt: 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: otpRecord, + }); + } catch (error) { + console.error("Error fetching OTP data:", error); + return NextResponse.json( + { success: false, message: "Gagal mengambil data OTP" }, + { status: 500 } + ); + } finally { + await prisma.$disconnect(); + } +} \ No newline at end of file diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts index eb7af548..be034c8f 100644 --- a/src/app/api/auth/register/route.ts +++ b/src/app/api/auth/register/route.ts @@ -1,62 +1,122 @@ -import prisma from "@/lib/prisma"; -import { NextResponse } from "next/server"; +// app/api/auth/register/route.ts +import { NextResponse } from 'next/server'; +import prisma from '@/lib/prisma'; export async function POST(req: Request) { - if (req.method !== "POST") { - return NextResponse.json( - { success: false, message: "Method Not Allowed" }, - { status: 405 } - ); - } - try { - const { data } = await req.json(); + // Terima langsung properti, bukan { data: { ... } } + const { username, nomor } = await req.json(); - const cekUsername = await prisma.user.findUnique({ - where: { - username: data.username, - nomor: data.nomor, - }, - }); - - if (cekUsername) - return NextResponse.json({ - success: false, - message: "Username sudah digunakan", - }); - - const createUser = await prisma.user.create({ - data: { - username: data.username, - nomor: data.nomor, - }, - }); - - if (!createUser) + // Validasi input + if (!username || !nomor) { return NextResponse.json( - { success: false, message: "Gagal Registrasi" }, - { status: 500 } + { success: false, message: 'Data tidak lengkap' }, + { status: 400 } ); + } - return NextResponse.json( - { - success: true, - message: "Registrasi Berhasil, Anda Sedang Login", - // data: createUser, + // // Validasi OTP: pastikan berisi digit saja + // const cleanOtp = otp.toString().trim(); + // if (!/^\d{4,6}$/.test(cleanOtp)) { + // return NextResponse.json( + // { success: false, message: 'Kode OTP tidak valid' }, + // { status: 400 } + // ); + // } + + // const receivedOtp = parseInt(cleanOtp, 10); + // if (isNaN(receivedOtp)) { + // return NextResponse.json( + // { success: false, message: 'Kode OTP tidak valid' }, + // { status: 400 } + // ); + // } + + // // Cari OTP record + // 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 kadaluarsa' }, + // { status: 400 } + // ); + // } + + // if (otpRecord.otp !== receivedOtp) { + // return NextResponse.json( + // { success: false, message: 'Kode OTP salah' }, + // { status: 400 } + // ); + // } + + // if (otpRecord.nomor !== nomor) { + // return NextResponse.json( + // { success: false, message: 'Nomor tidak sesuai' }, + // { status: 400 } + // ); + // } + + // Cek duplikat nomor + const existingUser = await prisma.user.findUnique({ + where: { nomor }, + }); + + if (existingUser) { + return NextResponse.json( + { success: false, message: 'Nomor sudah terdaftar' }, + { status: 409 } + ); + } + + // Cek username unik (pastikan ada @unique di schema!) + const existingByUsername = await prisma.user.findUnique({ + where: { username }, + }); + + if (existingByUsername) { + return NextResponse.json( + { success: false, message: 'Username sudah digunakan' }, + { status: 409 } + ); + } + + // Buat user + const newUser = await prisma.user.create({ + data: { + username: username.trim(), + nomor, + isActive: false, + // roleId default "1" }, - { status: 201 } - ); + }); + + // // Nonaktifkan OTP + // await prisma.kodeOtp.update({ + // where: { id: kodeId }, + // data: { isActive: false }, + // }); + + return NextResponse.json({ + success: true, + message: 'Pendaftaran berhasil. Menunggu persetujuan admin.', + userId: newUser.id, + }); } catch (error) { - console.error("Error registrasi:", error); + console.error('Registration error:', error); return NextResponse.json( - { - success: false, - message: "Maaf, Terjadi Keselahan", - reason: (error as Error).message, - }, + { success: false, message: 'Terjadi kesalahan saat pendaftaran' }, { status: 500 } ); } finally { await prisma.$disconnect(); } -} +} \ No newline at end of file diff --git a/src/app/api/auth/resend/route.ts b/src/app/api/auth/resend/route.ts new file mode 100644 index 00000000..06e147a3 --- /dev/null +++ b/src/app/api/auth/resend/route.ts @@ -0,0 +1,71 @@ +import prisma from "@/lib/prisma"; +import { randomOTP } from "../_lib/randomOTP"; +import { NextResponse } from "next/server"; + +export async function POST(req: Request) { + if (req.method !== "POST") { + return NextResponse.json( + { success: false, message: "Method Not Allowed" }, + { status: 405 } + ); + } + + try { + const codeOtp = randomOTP(); + const body = await req.json(); + const { nomor } = body; + + const res = await fetch( + `https://wa.wibudev.com/code?nom=${nomor}&text=HIPMI - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun pengurus HIPMI lainnya. + \n + >> Kode OTP anda: ${codeOtp}. + ` + ); + + const sendWa = await res.json(); + if (sendWa.status !== "success") + return NextResponse.json( + { + success: false, + message: "Nomor Whatsapp Tidak Aktif", + }, + { status: 400 } + ); + + const createOtpId = await prisma.kodeOtp.create({ + data: { + nomor: nomor, + otp: codeOtp, + }, + }); + + if (!createOtpId) + return NextResponse.json( + { + success: false, + message: "Gagal Membuat Kode OTP", + }, + { status: 400 } + ); + + return NextResponse.json( + { + success: true, + message: "Kode Verifikasi Dikirim", + kodeId: createOtpId.id, + }, + { status: 200 } + ); + } catch (error) { + console.error(" Error Resend OTP", error); + return NextResponse.json( + { + success: false, + message: "Server Whatsapp Error !!", + }, + { status: 500 } + ); + } finally { + await prisma.$disconnect(); + } +} diff --git a/src/app/api/auth/send-otp-register/route.ts b/src/app/api/auth/send-otp-register/route.ts new file mode 100644 index 00000000..8c52cbf9 --- /dev/null +++ b/src/app/api/auth/send-otp-register/route.ts @@ -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.findUnique({ 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(); + } +} \ No newline at end of file diff --git a/src/app/api/auth/validasi/route.ts b/src/app/api/auth/validasi/route.ts new file mode 100644 index 00000000..2e23908a --- /dev/null +++ b/src/app/api/auth/validasi/route.ts @@ -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(); + } +} diff --git a/src/app/api/auth/verify-otp/route.ts b/src/app/api/auth/verify-otp/route.ts new file mode 100644 index 00000000..16cc1402 --- /dev/null +++ b/src/app/api/auth/verify-otp/route.ts @@ -0,0 +1,139 @@ +// app/api/auth/verify-otp/route.ts +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, otp, kodeId } = await req.json(); + + // Validasi input + if (!nomor || !otp || !kodeId) { + return NextResponse.json( + { success: false, message: "Data tidak lengkap" }, + { status: 400 } + ); + } + + // Cari OTP record + 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" }, + { status: 400 } + ); + } + + // Pastikan tipe data cocok (OTP di DB = number) + const receivedOtp = Number(otp); + if (isNaN(receivedOtp) || otpRecord.otp !== receivedOtp) { + return NextResponse.json( + { success: false, message: "Kode OTP salah" }, + { status: 400 } + ); + } + + if (otpRecord.nomor !== nomor) { + return NextResponse.json( + { success: false, message: "Nomor tidak sesuai" }, + { status: 400 } + ); + } + + // Cek user berdasarkan nomor + const user = await prisma.user.findUnique({ + where: { nomor }, + select: { + id: true, + nomor: true, + username: true, + roleId: true, + isActive: true, + }, + }); + + if (!user) { + return NextResponse.json( + { success: false, message: "Akun tidak ditemukan" }, + { status: 404 } + ); + } + + if (!user.isActive) { + return NextResponse.json( + { success: false, message: "Akun belum disetujui oleh admin" }, + { status: 403 } + ); + } + + // Buat session + const token = await sessionCreate({ + sessionKey: process.env.BASE_SESSION_KEY!, + jwtSecret: process.env.BASE_TOKEN_KEY!, // ✅ + user: { + id: user.id, + nomor: user.nomor, + username: user.username, + roleId: user.roleId, + isActive: user.isActive, + }, + }); + if (!token) { + return NextResponse.json( + { success: false, message: "Gagal membuat session" }, + { status: 500 } + ); + } + + // Nonaktifkan OTP + await prisma.kodeOtp.update({ + where: { id: kodeId }, + data: { isActive: false }, + }); + + // Set cookie & respons + const response = NextResponse.json( + { + success: true, + message: "Berhasil login", + roleId: user.roleId, + }, + { status: 200 } + ); + + response.cookies.set(process.env.BASE_SESSION_KEY!, token, { + path: "/", + sameSite: "lax", + secure: process.env.NODE_ENV === "production", + httpOnly: true, // 🔒 lebih aman + maxAge: 30 * 24 * 60 * 60, + }); + + return response; + } catch (error) { + console.error("Verify OTP Error:", error); + return NextResponse.json( + { success: false, message: "Terjadi kesalahan saat verifikasi" }, + { status: 500 } + ); + } finally { + await prisma.$disconnect(); + } +} \ No newline at end of file diff --git a/src/app/waiting-room/page.tsx b/src/app/waiting-room/page.tsx new file mode 100644 index 00000000..91c07beb --- /dev/null +++ b/src/app/waiting-room/page.tsx @@ -0,0 +1,92 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +'use client'; + +import colors from '@/con/colors'; +import { Center, Loader, Paper, Stack, Text, Title } from '@mantine/core'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; + +// Ganti ini jika tidak pakai next-auth +async function fetchUser() { + const res = await fetch('/api/auth/me'); + if (!res.ok) throw new Error('Unauthorized'); + return res.json(); +} + +export default function WaitingRoom() { + const router = useRouter(); + const [user, setUser] = useState(null); + // const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let isMounted = true; + const interval = setInterval(async () => { + try { + const data = await fetchUser(); + if (!isMounted) return; + + setUser(data.user); + + // Jika sudah aktif, redirect ke dashboard admin + if (data.user.isActive) { + clearInterval(interval); + router.push('/admin'); // atau /dashboard + } + } catch (err: any) { + if (!isMounted) return; + setError(err.message || 'Gagal memuat status'); + clearInterval(interval); + // Redirect ke login jika unauthorized + if (err.message === 'Unauthorized') { + router.push('/login'); + } + } + }, 2000); // Cek setiap 2 detik + + // Cleanup + return () => { + isMounted = false; + clearInterval(interval); + }; + }, [router]); + + if (error) { + return ( +
+ + + Error + {error} + + +
+ ); + } + + return ( +
+ + + + Menunggu Persetujuan + + + + Akun Anda sedang dalam proses verifikasi oleh Superadmin. + + + + Nomor: {user?.nomor || '...'} + + + + + + Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui. + + + +
+ ); +} \ No newline at end of file -- 2.49.1 From 78b8aa74cdf35159f1cb0ef6939e10743f2acfc1 Mon Sep 17 00:00:00 2001 From: nico Date: Thu, 20 Nov 2025 14:07:26 +0800 Subject: [PATCH 02/11] Saat user baru registrasi maka akan diarahkan ke page waiting-room dan menunggu validasi admin --- .../(dashboard)/auth/validasi-admin/page.tsx | 2 +- src/app/admin/_com/list_PageAdmin.tsx | 47 ++++++- src/app/api/auth/_lib/api_fetch_auth.ts | 34 +---- .../api/auth/finalize-registration/route.ts | 22 +++- src/app/api/auth/register/route.ts | 123 ++++-------------- 5 files changed, 89 insertions(+), 139 deletions(-) diff --git a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx index d5180665..83c32f20 100644 --- a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx +++ b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx @@ -76,7 +76,7 @@ export default function Validasi() { const regData = await regRes.json(); if (regData.success) { cleanupStorage(); - router.push('/admin/landing-page/profil/program-inovasi'); + router.push('/waiting-room'); // ✅ } else { toast.error(regData.message || 'Registrasi gagal'); } diff --git a/src/app/admin/_com/list_PageAdmin.tsx b/src/app/admin/_com/list_PageAdmin.tsx index 17f5a45b..afb755ac 100644 --- a/src/app/admin/_com/list_PageAdmin.tsx +++ b/src/app/admin/_com/list_PageAdmin.tsx @@ -336,7 +336,8 @@ export const navBar = [ path: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana" } ] - }, { + }, + { id: "Pendidikan", name: "Pendidikan", path: "", @@ -377,5 +378,49 @@ export const navBar = [ path: "/admin/pendidikan/data-pendidikan" } ] + }, + { + id: "User & Role", + name: "User & Role", + path: "", + children: [ + { + id: "User", + name: "User", + path: "/admin/user/user" + }, + { + id: "Role", + name: "Role", + path: "/admin/role/role" + }, + { + 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" + } + ] } ] + + diff --git a/src/app/api/auth/_lib/api_fetch_auth.ts b/src/app/api/auth/_lib/api_fetch_auth.ts index 38d76ddb..fe073608 100644 --- a/src/app/api/auth/_lib/api_fetch_auth.ts +++ b/src/app/api/auth/_lib/api_fetch_auth.ts @@ -54,7 +54,7 @@ export const apiFetchRegister = async ({ const cleanPhone = nomor.replace(/\D/g, ''); if (cleanPhone.length < 10) throw new Error('Nomor tidak valid'); - const response = await fetch("/api/auth/send-otp-register", { + const response = await fetch("/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: username.trim(), nomor: cleanPhone }), @@ -86,38 +86,6 @@ export const apiFetchOtpData = async ({ kodeId }: { kodeId: string }) => { return data; }; -// export const apiFetchVerifyOtp = async ({ -// nomor, -// otp, -// kodeId -// }: { -// nomor: string; -// otp: string; -// kodeId: string; -// }) => { -// if (!nomor || !otp || !kodeId) { -// throw new Error('Data verifikasi tidak lengkap'); -// } - -// if (!/^\d{4,6}$/.test(otp)) { -// throw new Error('Kode OTP harus 4-6 digit angka'); -// } - -// const response = await fetch('/api/auth/verify-otp', { -// method: 'POST', -// headers: { 'Content-Type': 'application/json' }, -// body: JSON.stringify({ nomor, otp, kodeId }), -// }); - -// const data = await response.json(); - -// if (!response.ok) { -// throw new Error(data.message || 'Verifikasi OTP gagal'); -// } - -// return data; -// }; - export const apiFetchVerifyOtp = async ({ nomor, otp, diff --git a/src/app/api/auth/finalize-registration/route.ts b/src/app/api/auth/finalize-registration/route.ts index 3736ab5a..4fbe01cc 100644 --- a/src/app/api/auth/finalize-registration/route.ts +++ b/src/app/api/auth/finalize-registration/route.ts @@ -1,7 +1,8 @@ // app/api/auth/finalize-registration/route.ts import prisma from "@/lib/prisma"; +import { cookies } from "next/headers"; import { NextResponse } from "next/server"; -import { sessionCreate } from "../_lib/session_create"; +// import { sessionCreate } from "../_lib/session_create"; export async function POST(req: Request) { try { @@ -15,21 +16,28 @@ export async function POST(req: Request) { // Buat user const user = await prisma.user.create({ - data: { username, nomor, isActive: true } + data: { username, nomor, isActive: false } }); // Nonaktifkan OTP await prisma.kodeOtp.update({ where: { id: kodeId }, data: { isActive: false } }); // Buat session - const token = await sessionCreate({ - sessionKey: process.env.BASE_SESSION_KEY!, - jwtSecret: process.env.BASE_TOKEN_KEY!, - user: { id: user.id, nomor: user.nomor, username: user.username, roleId: user.roleId, isActive: true }, + // const token = await sessionCreate({ + // sessionKey: process.env.BASE_SESSION_KEY!, + // jwtSecret: process.env.BASE_TOKEN_KEY!, + // user: { id: user.id, nomor: user.nomor, username: user.username, roleId: user.roleId, isActive: true }, + // }); + + (await cookies()).set('desadarmasaba_user_id', user.id, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + path: '/', + maxAge: 30 * 24 * 60 * 60, // 30 hari }); const response = NextResponse.json({ success: true, roleId: user.roleId }); - response.cookies.set(process.env.BASE_SESSION_KEY!, token, { /* options */ }); + // response.cookies.set(process.env.BASE_SESSION_KEY!, token, { /* options */ }); return response; } catch (error) { console.error('Finalize Registration Error:', error); diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts index be034c8f..8dbe79c6 100644 --- a/src/app/api/auth/register/route.ts +++ b/src/app/api/auth/register/route.ts @@ -1,121 +1,50 @@ -// app/api/auth/register/route.ts import { NextResponse } from 'next/server'; import prisma from '@/lib/prisma'; +import { randomOTP } from '../_lib/randomOTP'; // pastikan ada export async function POST(req: Request) { try { - // Terima langsung properti, bukan { data: { ... } } const { username, nomor } = await req.json(); - // Validasi input if (!username || !nomor) { - return NextResponse.json( - { success: false, message: 'Data tidak lengkap' }, - { status: 400 } - ); + return NextResponse.json({ success: false, message: 'Data tidak lengkap' }, { status: 400 }); } - // // Validasi OTP: pastikan berisi digit saja - // const cleanOtp = otp.toString().trim(); - // if (!/^\d{4,6}$/.test(cleanOtp)) { - // return NextResponse.json( - // { success: false, message: 'Kode OTP tidak valid' }, - // { status: 400 } - // ); - // } - - // const receivedOtp = parseInt(cleanOtp, 10); - // if (isNaN(receivedOtp)) { - // return NextResponse.json( - // { success: false, message: 'Kode OTP tidak valid' }, - // { status: 400 } - // ); - // } - - // // Cari OTP record - // 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 kadaluarsa' }, - // { status: 400 } - // ); - // } - - // if (otpRecord.otp !== receivedOtp) { - // return NextResponse.json( - // { success: false, message: 'Kode OTP salah' }, - // { status: 400 } - // ); - // } - - // if (otpRecord.nomor !== nomor) { - // return NextResponse.json( - // { success: false, message: 'Nomor tidak sesuai' }, - // { status: 400 } - // ); - // } - - // Cek duplikat nomor - const existingUser = await prisma.user.findUnique({ - where: { nomor }, - }); - - if (existingUser) { - return NextResponse.json( - { success: false, message: 'Nomor sudah terdaftar' }, - { status: 409 } - ); + // Cek duplikat + if (await prisma.user.findUnique({ where: { nomor } })) { + return NextResponse.json({ success: false, message: 'Nomor sudah terdaftar' }, { status: 409 }); + } + if (await prisma.user.findUnique({ where: { username } })) { + return NextResponse.json({ success: false, message: 'Username sudah digunakan' }, { status: 409 }); } - // Cek username unik (pastikan ada @unique di schema!) - const existingByUsername = await prisma.user.findUnique({ - where: { username }, - }); + // ✅ Generate dan kirim OTP + const codeOtp = randomOTP(); + const otpNumber = Number(codeOtp); - if (existingByUsername) { - return NextResponse.json( - { success: false, message: 'Username sudah digunakan' }, - { status: 409 } - ); + 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 }); } - // Buat user - const newUser = await prisma.user.create({ - data: { - username: username.trim(), - nomor, - isActive: false, - // roleId default "1" - }, + // ✅ Simpan OTP ke database + const otpRecord = await prisma.kodeOtp.create({ + data: { nomor, otp: otpNumber, isActive: true } }); - // // Nonaktifkan OTP - // await prisma.kodeOtp.update({ - // where: { id: kodeId }, - // data: { isActive: false }, - // }); - + // ✅ Kembalikan kodeId (jangan buat user di sini!) return NextResponse.json({ success: true, - message: 'Pendaftaran berhasil. Menunggu persetujuan admin.', - userId: newUser.id, + message: 'Kode verifikasi dikirim', + kodeId: otpRecord.id, }); } catch (error) { - console.error('Registration error:', error); - return NextResponse.json( - { success: false, message: 'Terjadi kesalahan saat pendaftaran' }, - { status: 500 } - ); + console.error('Register OTP Error:', error); + return NextResponse.json({ success: false, message: 'Gagal mengirim OTP' }, { status: 500 }); } finally { await prisma.$disconnect(); } -- 2.49.1 From 0dff8f3254b475344e26b2a0dc407c44a2cba4bd Mon Sep 17 00:00:00 2001 From: nico Date: Thu, 20 Nov 2025 16:42:36 +0800 Subject: [PATCH 03/11] Nico 20 Nov 25 Dibagian layout admin sudah disesuaikan dengan rolenya : supadmin, admin desa, admin kesehatan, admin pendidikan Fix API User & Role Admin --- prisma/data/user/roles.json | 47 +-- prisma/data/user/users.json | 23 -- prisma/schema.prisma | 12 - prisma/seed.ts | 26 +- .../(dashboard)/_state/user/user-state.ts | 51 ++- .../(dashboard)/auth/validasi-admin/page.tsx | 12 +- .../(dashboard)/user&role/role/[id]/page.tsx | 65 +-- .../user&role/role/create/page.tsx | 30 +- .../admin/(dashboard)/user&role/user/page.tsx | 40 +- src/app/admin/_com/list_PageAdmin.tsx | 372 +++++++++++++++++- src/app/admin/_com/navigationByRole.ts | 9 + src/app/admin/layout.tsx | 278 ++++++++++++- src/app/api/[[...slugs]]/_lib/user/index.ts | 12 +- .../api/[[...slugs]]/_lib/user/role/create.ts | 2 - .../api/[[...slugs]]/_lib/user/role/index.ts | 2 - .../api/[[...slugs]]/_lib/user/role/updt.ts | 2 - src/app/api/[[...slugs]]/_lib/user/updt.ts | 37 +- src/app/api/auth/verify-otp/route.ts | 7 + src/store/authStore.ts | 17 + 19 files changed, 835 insertions(+), 209 deletions(-) delete mode 100644 prisma/data/user/users.json create mode 100644 src/app/admin/_com/navigationByRole.ts create mode 100644 src/store/authStore.ts diff --git a/prisma/data/user/roles.json b/prisma/data/user/roles.json index b79f3928..423965fc 100644 --- a/prisma/data/user/roles.json +++ b/prisma/data/user/roles.json @@ -1,23 +1,26 @@ [ - { - "id": "role-1", - "name": "ADMIN DESA", - "description": "Administrator Desa", - "permissions": ["manage_users", "manage_content", "view_reports"], - "isActive": true - }, - { - "id": "role-2", - "name": "ADMIN KESEHATAN", - "description": "Administrator Bidang Kesehatan", - "permissions": ["manage_health_data", "view_reports"], - "isActive": true - }, - { - "id": "role-3", - "name": "ADMIN SEKOLAH", - "description": "Administrator Sekolah", - "permissions": ["manage_school_data", "view_reports"], - "isActive": true - } - ] \ No newline at end of file + { + "id": "0", + "name": "SUPER ADMIN", + "description": "Administrator", + "isActive": true + }, + { + "id": "1", + "name": "ADMIN DESA", + "description": "Administrator Desa", + "isActive": true + }, + { + "id": "2", + "name": "ADMIN KESEHATAN", + "description": "Administrator Bidang Kesehatan", + "isActive": true + }, + { + "id": "3", + "name": "ADMIN PENDIDIKAN", + "description": "Administrator Bidang Pendidikan", + "isActive": true + } +] diff --git a/prisma/data/user/users.json b/prisma/data/user/users.json deleted file mode 100644 index eea2a98a..00000000 --- a/prisma/data/user/users.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "id": "user-1", - "nama": "Admin Desa", - "nomor": "089647037426", - "roleId": "role-1", - "isActive": true - }, - { - "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 - } -] diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 099cb7f9..40a68902 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -2181,7 +2181,6 @@ model Role { id String @id @default(cuid()) name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH description String? - permissions Json // Menyimpan permission dalam format JSON isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -2200,17 +2199,6 @@ model KodeOtp { 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 { id String @id @default(cuid()) token String diff --git a/prisma/seed.ts b/prisma/seed.ts index 52de4324..9ae7ef5d 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -54,14 +54,13 @@ import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-progr import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json"; import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json"; import roles from "./data/user/roles.json"; -import users from "./data/user/users.json"; import fileStorage from "./data/file-storage.json"; import jenjangPendidikan from "./data/pendidikan/info-sekolah/jenjang-pendidikan.json"; import seedAssets from "./seed_assets"; import { safeSeedUnique } from "./safeseedUnique"; (async () => { - // =========== USER & ROLE =========== + // =========== ROLE =========== // In your seed.ts // =========== ROLES =========== console.log("🔄 Seeding roles..."); @@ -69,35 +68,12 @@ import { safeSeedUnique } from "./safeseedUnique"; await safeSeedUnique("role", { id: r.id }, { name: r.name, description: r.description, - permissions: r.permissions, isActive: r.isActive, }); } console.log("✅ Roles seeded"); - // =========== USERS =========== - console.log("🔄 Seeding users..."); - for (const u of users) { - // First verify the role exists - const roleExists = await prisma.role.findUnique({ - where: { id: u.roleId }, - }); - - if (!roleExists) { - console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`); - continue; - } - - await safeSeedUnique("user", { id: u.id }, { - username: u.nama, - nomor: u.nomor, - roleId: u.roleId, - isActive: u.isActive, - }); - } - console.log("✅ Users seeded"); - // =========== FILE STORAGE =========== console.log("🔄 Seeding file storage..."); for (const f of fileStorage) { diff --git a/src/app/admin/(dashboard)/_state/user/user-state.ts b/src/app/admin/(dashboard)/_state/user/user-state.ts index 93594956..c686c8eb 100644 --- a/src/app/admin/(dashboard)/_state/user/user-state.ts +++ b/src/app/admin/(dashboard)/_state/user/user-state.ts @@ -90,27 +90,35 @@ const userState = proxy({ } }, }, - updateActive: { + update: { loading: false, - async submit(id: string, isActive: boolean) { + + 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({ id, isActive }), + body: JSON.stringify(payload), }); const data = await res.json(); + if (res.status === 200 && data.success) { toast.success(data.message); - userState.findMany.load(userState.findMany.page, 10, userState.findMany.search); + + // refresh list + userState.findMany.load( + userState.findMany.page, + 10, + userState.findMany.search + ); } else { - toast.error(data.message || "Gagal update status user"); + toast.error(data.message || "Gagal update user"); } } catch (e) { console.error(e); - toast.error("Gagal update status user"); + toast.error("Gagal update user"); } finally { this.loading = false; } @@ -120,12 +128,10 @@ const userState = proxy({ const templateRole = z.object({ name: z.string().min(1, "Nama harus diisi"), - permissions: z.array(z.string()).min(1, "Permission harus diisi"), }); const defaultRole = { name: "", - permissions: [] as string[], }; const roleState = proxy({ @@ -237,7 +243,7 @@ const roleState = proxy({ toast.warn("ID tidak valid"); return null; } - + try { const response = await fetch(`/api/role/${id}`, { method: "GET", @@ -245,31 +251,25 @@ const roleState = proxy({ "Content-Type": "application/json", }, }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - + const result = await response.json(); - + if (result?.success) { 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, - permissions: data.permissions, }; - return data; // Return the loaded data - } else { - throw new Error(result?.message || "Gagal memuat data"); + + return data; } } catch (error) { console.error("Error loading role:", error); - toast.error( - error instanceof Error ? error.message : "Gagal memuat data" - ); - return null; + toast.error("Gagal memuat data"); } - }, + }, async update() { const cek = templateRole.safeParse(roleState.update.form); if (!cek.success) { @@ -290,7 +290,6 @@ const roleState = proxy({ }, body: JSON.stringify({ name: this.form.name, - permissions: this.form.permissions, }), }); diff --git a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx index 83c32f20..0ea24413 100644 --- a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx +++ b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx @@ -7,6 +7,7 @@ import { Box, Button, Loader, Paper, PinInput, Stack, Text, Title } from '@manti 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(); @@ -53,10 +54,17 @@ export default function Validasi() { setLoading(true); const verifyResult = await apiFetchVerifyOtp({ nomor, otp, kodeId }); - if (verifyResult.success) { + if (verifyResult.success && verifyResult.user) { + // ✅ SET USER KE STORE + authStore.setUser({ + id: verifyResult.user.id, + name: verifyResult.user.name, + roleId: Number(verifyResult.user.roleId), + }); + cleanupStorage(); router.push('/admin/landing-page/profil/program-inovasi'); - return; // ✅ HENTIKAN eksekusi di sini + return; } // Hanya coba registrasi jika akun tidak ditemukan diff --git a/src/app/admin/(dashboard)/user&role/role/[id]/page.tsx b/src/app/admin/(dashboard)/user&role/role/[id]/page.tsx index f4e59441..9aacd720 100644 --- a/src/app/admin/(dashboard)/user&role/role/[id]/page.tsx +++ b/src/app/admin/(dashboard)/user&role/role/[id]/page.tsx @@ -1,7 +1,8 @@ +/* eslint-disable react-hooks/exhaustive-deps */ 'use client' 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 { useParams, useRouter } from 'next/navigation'; import { useCallback, useEffect, useState } from 'react'; @@ -9,6 +10,7 @@ import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import user from '../../../_state/user/user-state'; + function EditRole() { const stateRole = useProxy(user.roleState); const router = useRouter(); @@ -17,46 +19,37 @@ function EditRole() { // Controlled local state const [formData, setFormData] = useState({ name: '', - permissions: [] as string[], }); const [originalData, setOriginalData] = useState({ name: '', - permissions: [] as string[], }); const [isSubmitting, setIsSubmitting] = useState(false); // Load role data const loadRole = useCallback(async (id: string) => { - try { - const data = await stateRole.update.load(id); - if (data) { - setFormData({ - name: data.name || '', - permissions: data.permissions || [], - }); - setOriginalData({ - 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'); + const data = await stateRole.update.load(id); + + if (data) { + setFormData({ + name: data.name ?? '', + }); + + setOriginalData({ + name: data.name ?? '', + }); } - }, [stateRole.update]); + }, []); useEffect(() => { - stateRole.findMany.load(); // Load permissions/options - const id = params?.id as string; - if (id) loadRole(id); - }, [params?.id, loadRole, stateRole.findMany]); + stateRole.findMany.load(); // load permission + if (params?.id) loadRole(params.id as string); + }, [params?.id]); const handleResetForm = () => { setFormData({ name: originalData.name, - permissions: originalData.permissions, }); toast.info("Form dikembalikan ke data awal"); }; @@ -66,10 +59,6 @@ function EditRole() { toast.error('Nama role tidak boleh kosong'); return; } - if (!formData.permissions.length) { - toast.error('Pilih minimal satu permission'); - return; - } try { setIsSubmitting(true); @@ -77,7 +66,6 @@ function EditRole() { stateRole.update.form = { ...stateRole.update.form, name: formData.name, - permissions: formData.permissions, }; await stateRole.update.update(); toast.success('Role berhasil diperbarui!'); @@ -116,24 +104,7 @@ function EditRole() { label={Nama Role} placeholder="Masukkan nama role" /> - setFormData({ ...formData, permissions: val })} - label={Permission} - 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} - /> - - + {/* Tombol Batal */} + + )} +
+
+
+ ) +} + +export default MenuAccessPage \ No newline at end of file diff --git a/src/app/admin/(dashboard)/user&role/user/page.tsx b/src/app/admin/(dashboard)/user&role/user/page.tsx index 0b93d81e..6f1ecbc2 100644 --- a/src/app/admin/(dashboard)/user&role/user/page.tsx +++ b/src/app/admin/(dashboard)/user&role/user/page.tsx @@ -95,7 +95,24 @@ function ListUser({ search }: { search: string }) { }); if (success) { - // Reload data setelah berhasil update + // Cek apakah role berubah + const res = await fetch('/api/user/updt', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + id: userId, + roleId: newRoleId, + }), + }); + const data = await res.json(); + + if (data.roleChanged) { + // Tampilkan notifikasi + alert(`User ${username} akan logout otomatis!`); + } + stateUser.findMany.load(page, 10, search); } @@ -114,7 +131,9 @@ function ListUser({ search }: { search: string }) { } }; - const filteredData = data || []; + const filteredData = (data || []).filter( + (item) => item.roleId !== "0" // asumsikan id role SUPERADMIN = "0" + ); if (loading || !data) { return ( @@ -158,10 +177,12 @@ function ListUser({ search }: { search: string }) {