Compare commits

...

31 Commits

Author SHA1 Message Date
13f88efb35 upd: api
deskripsi:
- api

No Issues
2025-11-04 15:32:14 +08:00
25fc7e2d26 Merge pull request 'upd: api pelayanan surat' (#7) from amalia/03-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/7
2025-11-03 17:42:23 +08:00
26241fd36c upd: api pelayanan surat
Deskripsi:
- create
- update status
- list
- detail

No Issues
"
git statys
2025-11-03 17:40:38 +08:00
37e76d82c0 Merge pull request 'upd: pelayanan' (#6) from amalia/31-okt-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/6
2025-10-31 12:10:52 +08:00
73bf785d13 upd: pelayanan
Deskripsi :
- api category pelayanan list
- api category pelayanan create
- api category pelayanan update
- api category pelayanan delete

No Issues
2025-10-31 12:09:31 +08:00
cc7dcccd1b Merge pull request 'upd : pelayanan surat' (#5) from amalia/30-okt-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/5
2025-10-30 18:11:41 +08:00
a475db688b upd : pelayanan surat
Deskripsi:
- update database
- update seeder categori pelayanan surat

No Issues
2025-10-30 18:10:48 +08:00
f93b486bbb Merge pull request 'upd: api pengaduan' (#4) from amalia/29-okt-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/4
2025-10-29 17:42:38 +08:00
06feeae9a5 upd: api pengaduan
Deskripsi:
- update seeder kategori pengaduan
- list pengaduan warga

NO Issues
2025-10-29 14:41:40 +08:00
b102643675 Merge pull request 'upd: database' (#3) from amalia/28-okt-25-v2 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/3
2025-10-28 17:29:27 +08:00
bipproduction
b2f8dc3714 tambahan 2025-10-28 17:28:18 +08:00
578ad51726 upd: database 2025-10-28 17:25:13 +08:00
bipproduction
8a3eaa2193 tambahan 2025-10-28 16:47:39 +08:00
bipproduction
cae9ed7282 tambahan 2025-10-28 16:29:41 +08:00
bipproduction
2003364bff tambahan 2025-10-28 16:26:03 +08:00
bipproduction
5dc83dbd35 tambahan 2025-10-28 16:18:13 +08:00
bipproduction
9c96031574 tambahan 2025-10-28 16:11:43 +08:00
bipproduction
841fca55d1 tambahan 2025-10-28 16:09:27 +08:00
bipproduction
e009e27d47 tambahan 2025-10-28 16:09:04 +08:00
bipproduction
b52da1c4bd tambahan 2025-10-28 16:04:14 +08:00
bipproduction
3edcc52e74 tambahan 2025-10-28 16:03:00 +08:00
bipproduction
17bd04e389 tambahan 2025-10-28 16:00:48 +08:00
bipproduction
69377a3491 tambahan 2025-10-28 15:58:28 +08:00
bipproduction
3e2245da29 tambahan 2025-10-28 15:49:46 +08:00
bipproduction
65b24ab031 tambahan 2025-10-28 15:12:58 +08:00
78b1c0ee2d Merge pull request 'amalia/28-okt-25-v2' (#2) from amalia/28-okt-25-v2 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/2
2025-10-28 15:04:46 +08:00
7cc49655b4 Merge branch 'main' into amalia/28-okt-25-v2
oke
2025-10-28 15:01:09 +08:00
6a9ce54311 update 2025-10-28 15:00:57 +08:00
bf0083e678 update api pengaduan 2025-10-28 14:23:40 +08:00
bipproduction
fb5a859ebc tambahan 2025-10-28 14:17:48 +08:00
bipproduction
e0fdb88c32 tambahan 2025-10-28 14:05:53 +08:00
15 changed files with 1791 additions and 419 deletions

View File

@@ -4,6 +4,7 @@
"": {
"name": "bun-react-template",
"dependencies": {
"@elysiajs/bearer": "^1.4.1",
"@elysiajs/cors": "^1.4.0",
"@elysiajs/eden": "^1.4.4",
"@elysiajs/jwt": "^1.4.0",
@@ -48,6 +49,8 @@
"@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="],
"@elysiajs/bearer": ["@elysiajs/bearer@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-vLLSMEVsLKp/8p/eoAbXZdXKRs1jEQO4OkrfcKM2x8FkiK2aKNcFgLID45bH+6rYbCf8Ihg0NKw59zxMLl43OQ=="],
"@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="],
"@elysiajs/eden": ["@elysiajs/eden@1.4.4", "", { "peerDependencies": { "elysia": ">= 1.4.0-exp.0" } }, "sha512-/LVqflmgUcCiXb8rz1iRq9Rx3SWfIV/EkoNqDFGMx+TvOyo8QHAygFXAVQz7RHs+jk6n6mEgpI6KlKBANoErsQ=="],

View File

@@ -11,6 +11,7 @@
"lint": "bunx oxlint src"
},
"dependencies": {
"@elysiajs/bearer": "^1.4.1",
"@elysiajs/cors": "^1.4.0",
"@elysiajs/eden": "^1.4.4",
"@elysiajs/jwt": "^1.4.0",

View File

@@ -24,10 +24,12 @@ model User {
email String? @unique
password String?
phone String? @unique
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ApiKey ApiKey[]
HistoryPengaduan HistoryPengaduan[]
HistoryPelayanan HistoryPelayanan[]
}
model ApiKey {
@@ -82,7 +84,7 @@ model HistoryPengaduan {
id String @id @default(cuid())
Pengaduan Pengaduan @relation(fields: [idPengaduan], references: [id])
idPengaduan String
User User? @relation(fields: [idUser], references: [id])
User User? @relation(fields: [idUser], references: [id])
idUser String?
deskripsi String?
status StatusPengaduan @default(antrian)
@@ -91,12 +93,110 @@ model HistoryPengaduan {
}
model Warga {
id String @id @default(cuid())
name String?
phone String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Pengaduan Pengaduan[]
id String @id @default(cuid())
name String?
phone String? @unique
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Pengaduan Pengaduan[]
PelayananAjuan PelayananAjuan[]
SuratPelayanan SuratPelayanan[]
}
model CategoryPelayanan {
id String @id @default(cuid())
name String
syaratDokumen Json[]
dataText String[]
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
PelayananAjuan PelayananAjuan[]
SyaratDokumenPelayanan SyaratDokumenPelayanan[]
DataTextPelayanan DataTextPelayanan[]
SuratPelayanan SuratPelayanan[]
}
model PelayananAjuan {
id String @id @default(cuid())
Warga Warga @relation(fields: [idWarga], references: [id])
idWarga String
CategoryPelayanan CategoryPelayanan @relation(fields: [idCategory], references: [id])
idCategory String
noPengajuan String
status StatusPengaduan @default(antrian)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
HistoryPelayanan HistoryPelayanan[]
SyaratDokumenPelayanan SyaratDokumenPelayanan[]
DataTextPelayanan DataTextPelayanan[]
SuratPelayanan SuratPelayanan[]
}
model HistoryPelayanan {
id String @id @default(cuid())
PelayananAjuan PelayananAjuan @relation(fields: [idPengajuanLayanan], references: [id])
idPengajuanLayanan String
User User? @relation(fields: [idUser], references: [id])
idUser String?
deskripsi String?
keteranganAlasan String?
status StatusPengaduan @default(antrian)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model SyaratDokumenPelayanan {
id String @id @default(cuid())
PelayananAjuan PelayananAjuan @relation(fields: [idPengajuanLayanan], references: [id])
idPengajuanLayanan String
CategoryPelayanan CategoryPelayanan @relation(fields: [idCategory], references: [id])
idCategory String
jenis String
value String
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model DataTextPelayanan {
id String @id @default(cuid())
PelayananAjuan PelayananAjuan @relation(fields: [idPengajuanLayanan], references: [id])
idPengajuanLayanan String
CategoryPelayanan CategoryPelayanan @relation(fields: [idCategory], references: [id])
idCategory String
jenis String
value String
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model SuratPelayanan {
id String @id @default(cuid())
PelayananAjuan PelayananAjuan @relation(fields: [idPengajuanLayanan], references: [id])
idPengajuanLayanan String
CategoryPelayanan CategoryPelayanan @relation(fields: [idCategory], references: [id])
idCategory String
Warga Warga @relation(fields: [idWarga], references: [id])
idWarga String
noSurat String
dateExpired DateTime @db.Date
status Int
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Configuration {
id String @id @default(cuid())
category String
value String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum StatusPengaduan {

View File

@@ -1,5 +1,29 @@
import { categoryPelayananSurat } from "@/lib/categoryPelayananSurat";
import { prisma } from "@/server/lib/prisma";
const category = [
{
id: "lainnya",
name: "Lainnya"
},
{
id: "kebersihan",
name: "Kebersihan"
},
{
id: "keamanan",
name: "Keamanan"
},
{
id: "pelayanan",
name: "Pelayanan"
},
{
id: "infrastruktur",
name: "Infrastruktur"
},
]
const role = [
{
id: "developer",
@@ -17,6 +41,7 @@ const role = [
const user = [
{
id: "bip",
name: "Bip",
email: "bip@bip.com",
password: "bip",
@@ -25,16 +50,6 @@ const user = [
];
(async () => {
for (const u of user) {
await prisma.user.upsert({
where: { email: u.email },
create: u,
update: u
})
console.log(`✅ User ${u.email} seeded successfully`)
}
for (const r of role) {
console.log(`Seeding role ${r.name}`)
await prisma.role.upsert({
@@ -46,6 +61,38 @@ const user = [
console.log(`✅ Role ${r.name} seeded successfully`)
}
for (const u of user) {
await prisma.user.upsert({
where: { email: u.email },
create: u,
update: u
})
console.log(`✅ User ${u.email} seeded successfully`)
}
for (const c of category) {
await prisma.categoryPengaduan.upsert({
where: { id: c.id },
create: c,
update: c
})
console.log(`✅ Category ${c.name} seeded successfully`)
}
for (const cp of categoryPelayananSurat){
await prisma.categoryPelayanan.upsert({
where: { id: cp.id },
create: cp,
update: cp
})
console.log(`✅ Category Pelayanan ${cp.name} seeded successfully`)
}
})().catch((e) => {
console.error(e)

View File

@@ -1,18 +1,18 @@
import Swagger from "@elysiajs/swagger";
import Elysia from "elysia";
import html from "./index.html";
import apiAuth from "./server/middlewares/apiAuth";
import { convertOpenApiToMcp } from "./server/lib/mcp-converter";
import { apiAuth } from "./server/middlewares/apiAuth";
import AduanRoute from "./server/routes/aduan_route";
import ApiKeyRoute from "./server/routes/apikey_route";
import Auth from "./server/routes/auth_route";
import CredentialRoute from "./server/routes/credential_route";
import DarmasabaRoute from "./server/routes/darmasaba_route";
import { convertOpenApiToMcp } from "./server/lib/mcp-converter";
import UserRoute from "./server/routes/user_route";
import LayananRoute from "./server/routes/layanan_route";
import AduanRoute from "./server/routes/aduan_route";
import { cors } from "@elysiajs/cors";
import { MCPRoute } from "./server/routes/mcp_route";
import PelayananRoute from "./server/routes/pelayanan_surat_route";
import PengaduanRoute from "./server/routes/pengaduan_route";
import UserRoute from "./server/routes/user_route";
const Docs = new Elysia({
tags: ["docs"],
@@ -26,6 +26,8 @@ const Api = new Elysia({
prefix: "/api",
tags: ["api"],
})
.use(PengaduanRoute)
.use(PelayananRoute)
.use(apiAuth)
.use(ApiKeyRoute)
.use(DarmasabaRoute)

View File

@@ -0,0 +1,109 @@
export const categoryPelayananSurat = [
{
id: "skbedabiodata",
name: "Surat Keterangan Beda Biodata Diri",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas di Wilayah Masing-masing" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "dokumen yang beda", desc: "Fotokopi dokumen bersangkutan yang terdapat perbedaan biodata diri, misalnya: Sertifikat Tanah, Ijazah, Polis Asuransi, dan lainnya." }
],
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "pekerjaan", "dokumen", "tertulis pada dokumen a", "tertulis pada dokumen b"]
},
{
id: "skbelumkawin",
name: "Surat Keterangan Belum Kawin",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "akta cerai", desc: "Fotokopi Akta Cerai bagi yang berstatus janda/duda" }
],
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "status perkawinan"]
},
{
id: "skdomisiliorganisasi",
name: "Surat Keterangan Domisili Organisasi",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "skt organisasi", desc: "Fotokopi Surat Keterangan Terdaftar (SKT) Organisasi atau Pengukuhan Kelompok" },
{name: "susunan pengurus", desc: "Jika Pengajuan baru pembuatan SKT maka melengkapi Susunan Pengurus lengkap denganKop Organisasi"}
],
dataText: ["nama organisasi", "alamat organisasi", "nama pemohon", "jabatan pemohon", "kontak", "penanggung jawab", "tanggal berdiri"]
},
{
id: "skkelahiran",
name: "Surat Keterangan Kelahiran",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "surat lahir", desc: "Fotokopi Surat Keterangan Lahir dari Bidan/Dokter (jika ada)" }
],
dataText: ["nama ayah", "nama ibu", "nama anak", "tanggal lahir", "tempat lahir", "jenis kelamin", "nama pelapor"]
},
{
id: "skkelakuanbaik",
name: "Surat Keterangan Kelakuan Baik (Pengantar SKCK)",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" }
],
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "keperluan"]
},
{
id: "skkematian",
name: "Surat Keterangan Kematian",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "surat kematian", desc: "Surat Keterangan Kematian dari Rumah Sakit/Dokter (jika ada)" }
],
dataText: ["nama almarhum", "nik", "tempat tanggal lahir", "alamat", "tanggal kematian", "waktu kematian", "penyebab kematian"]
},
{
id: "skpenghasilan",
name: "Surat Keterangan Penghasilan",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp ortu/kk", desc: "Fotokopi KTP orang tua atau Kartu Keluarga" },
{ name: "surat pernyataan", desc: "Surat Pernyataan Penghasilan bermaterai" }
],
dataText: ["nama", "nik", "alamat", "pekerjaan", "jenis usaha", "penghasilan"]
},
{
id: "sktempatusaha",
name: "Surat Keterangan Tempat Usaha",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "foto lokasi", desc: "Foto lokasi usaha dicetak dalam selembar kertas, diparaf dan distempel oleh Kelian" },
{ name: "sppt/sertifikat/sewa", desc: "Fotokopi SPPT, Sertifikat Hak Milik, Surat Perjanjian Sewa, atau Kwitansi Pembayaran Sewa 3 bulan terakhir" }
],
dataText: ["nama usaha", "bidang usaha", "alamat usaha", "status tempat usaha", "luas tempat usaha", "jumlah karyawan", "tujuan pembuatan surat"]
},
{
id: "sktidakmampu",
name: "Surat Keterangan Tidak Mampu",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kia/kk", desc: "Fotokopi KTP, KIA, atau Kartu Keluarga" }
],
dataText: ["nik", "nama", "tempat tanggal lahir", "alamat", "alasan permohonan"]
},
{
id: "skusaha",
name: "Surat Keterangan Usaha",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "foto lokasi", desc: "Foto lokasi usaha dicetak dalam selembar kertas, diparaf dan distempel oleh Kelian" }
],
dataText: ["jenis usaha", "alamat usaha"]
},
{
id: "skyatimpiatu",
name: "Surat Keterangan Yatim / Piatu / Yatim Piatu",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kia/kk", desc: "Fotokopi KTP, KIA, atau Kartu Keluarga" }
],
dataText: ["nama anak", "nama ayah", "status ayah", "nama ibu", "status ibu"]
}
];

View File

@@ -0,0 +1,108 @@
import _ from "lodash";
import { v4 as uuidv4 } from "uuid";
interface McpTool {
name: string;
description: string;
inputSchema: any;
"x-props": {
method: string;
path: string;
operationId?: string;
tag?: string;
deprecated?: boolean;
summary?: string;
};
}
/**
* Convert OpenAPI 3.x JSON spec into MCP-compatible tool definitions (without run()).
* Hanya menyertakan endpoint yang memiliki tag berisi "mcp".
*/
export function convertOpenApiToMcpTools(openApiJson: any): McpTool[] {
const tools: McpTool[] = [];
const paths = openApiJson.paths || {};
for (const [path, methods] of Object.entries(paths)) {
// ✅ skip semua path internal MCP
if (path.startsWith("/mcp")) continue;
for (const [method, operation] of Object.entries<any>(methods as any)) {
const tags: string[] = Array.isArray(operation.tags) ? operation.tags : [];
// ✅ exclude semua yang tidak punya tag atau tag-nya tidak mengandung "mcp"
if (!tags.length || !tags.some(t => t.toLowerCase().includes("mcp"))) continue;
const rawName = _.snakeCase(operation.operationId || `${method}_${path}`) || "unnamed_tool";
const name = cleanToolName(rawName);
const description =
operation.description ||
operation.summary ||
`Execute ${method.toUpperCase()} ${path}`;
const schema =
operation.requestBody?.content?.["application/json"]?.schema || {
type: "object",
properties: {},
additionalProperties: true,
};
const tool: McpTool = {
name,
description,
"x-props": {
method: method.toUpperCase(),
path,
operationId: operation.operationId,
tag: tags[0],
deprecated: operation.deprecated || false,
summary: operation.summary,
},
inputSchema: {
...schema,
additionalProperties: true,
$schema: "http://json-schema.org/draft-07/schema#",
},
};
tools.push(tool);
}
}
return tools;
}
/**
* Bersihkan nama agar valid untuk digunakan sebagai tool name
* - hapus karakter spesial
* - ubah slash jadi underscore
* - hilangkan prefix umum (get_, post_, api_, dll)
* - rapikan underscore berganda
*/
function cleanToolName(name: string): string {
return name
.replace(/[{}]/g, "")
.replace(/[^a-zA-Z0-9_]/g, "_")
.replace(/_+/g, "_")
.replace(/^_|_$/g, "")
.replace(/^(get|post|put|delete|patch|api)_/i, "")
.replace(/^(get_|post_|put_|delete_|patch_|api_)+/gi, "")
.replace(/(^_|_$)/g, "");
}
/**
* Ambil OpenAPI JSON dari endpoint dan konversi ke tools MCP
*/
export async function getMcpTools() {
const data = await fetch(`${process.env.BUN_PUBLIC_BASE_URL}/docs/json`);
const openApiJson = await data.json();
const tools = convertOpenApiToMcpTools(openApiJson);
return tools;
}
// === CLI Mode ===
if (import.meta.main) {
const tools = await getMcpTools();
await Bun.write("./tools.json", JSON.stringify(tools, null, 2));
}

View File

@@ -0,0 +1,23 @@
import { prisma } from "./prisma"
export const generateNoPengajuanSurat = async () => {
const date = new Date()
const year = String(date.getFullYear()).slice(-2) // ambil 2 digit terakhir
const month = String(date.getMonth() + 1).padStart(2, "0")
const day = String(date.getDate()).padStart(2, "0")
const prefix = `PS-${day}${month}${year}`
const count = await prisma.pelayananAjuan.count({
where: {
noPengajuan: {
contains: prefix
}
}
})
// pastikan nomor urut selalu 3 digit
const number = String(count + 1).padStart(3, "0")
return `${prefix}-${number}`
}

View File

@@ -5,7 +5,11 @@ import { prisma } from '../lib/prisma'
const secret = process.env.JWT_SECRET
export default function apiAuth(app: Elysia) {
if (!secret) {
throw new Error('JWT_SECRET is not defined')
}
export function apiAuth(app: Elysia) {
if (!secret) {
throw new Error('JWT_SECRET is not defined')
}
@@ -16,37 +20,63 @@ export default function apiAuth(app: Elysia) {
secret,
})
)
.derive(async ({ cookie, headers, jwt }) => {
.derive(async ({ cookie, headers, jwt, request }) => {
let token: string | undefined
if (cookie?.token?.value) {
token = cookie.token.value as any
}
if (headers['x-token']?.startsWith('Bearer ')) {
token = (headers['x-token'] as string).slice(7)
}
if (headers['authorization']?.startsWith('Bearer ')) {
token = (headers['authorization'] as string).slice(7)
}
// 🔸 Ambil token dari Cookie
if (cookie?.token?.value) token = cookie.token.value as string
let user: null | Awaited<ReturnType<typeof prisma.user.findUnique>> = null
if (token) {
try {
const decoded = (await jwt.verify(token)) as JWTPayloadSpec
if (decoded.sub) {
user = await prisma.user.findUnique({
where: { id: decoded.sub as string },
})
}
} catch (err) {
console.warn('[SERVER][apiAuth] Invalid token', err)
// 🔸 Ambil token dari Header (case-insensitive)
const possibleHeaders = [
'authorization',
'Authorization',
'x-token',
'X-Token',
]
for (const key of possibleHeaders) {
const value = headers[key]
if (typeof value === 'string') {
token = value.startsWith('Bearer ') ? value.slice(7) : value
break
}
}
return { user }
// 🔸 Tidak ada token
if (!token) {
console.warn(`[AUTH] No token found for ${request.method} ${request.url}`)
return { user: null }
}
// 🔸 Verifikasi token
try {
const decoded = (await jwt.verify(token)) as JWTPayloadSpec
if (!decoded?.sub) {
console.warn('[AUTH] Token missing sub field:', decoded)
return { user: null }
}
const user = await prisma.user.findUnique({
where: { id: decoded.sub as string },
})
if (!user) {
console.warn('[AUTH] User not found for sub:', decoded.sub)
return { user: null }
}
return { user }
} catch (err) {
console.warn('[AUTH] Invalid JWT token:', err)
return { user: null }
}
})
.onBeforeHandle(({ user, set }) => {
.onBeforeHandle(({ user, set, request }) => {
if (!user) {
console.warn(
`[AUTH] Unauthorized access: ${request.method} ${request.url}`
)
set.status = 401
return { error: 'Unauthorized' }
}

View File

@@ -1,102 +1,8 @@
import { Elysia } from "elysia";
import { v4 as uuidv4 } from "uuid";
import { getMcpTools } from "../lib/mcp_tool_convert";
// import tools from "./../../../tools.json";
// const API_KEY = process.env.MCP_API_KEY ?? "super-secret-key";
// const PORT = Number(process.env.PORT ?? 3000);
// // =====================
// // Helper Functions
// // =====================
// function isAuthorized(headers: Headers) {
// const authHeader = headers.get("authorization");
// if (authHeader?.startsWith("Bearer ")) {
// const token = authHeader.substring(7);
// return token === API_KEY;
// }
// return headers.get("x-api-key") === API_KEY;
// }
// =====================
// Tools Definition
// =====================
type Tool = {
name: string;
description: string;
inputSchema: {
type: string;
properties: Record<string, any>;
required?: string[];
additionalProperties?: boolean;
$schema?: string;
};
run: (input?: any) => Promise<any>;
};
const tools: Tool[] = [
{
name: "perbekal_darmasaba",
description: "Mengembalikan nama perbekal darmasaba",
inputSchema: {
type: "object",
properties: {},
additionalProperties: true,
$schema: "http://json-schema.org/draft-07/schema#",
},
run: async () => ({ perbekal_darmasaba: "malik kurosaki" }),
},
{
name: "uuid",
description: "Menghasilkan UUID v4 unik.",
inputSchema: {
type: "object",
properties: {},
additionalProperties: true,
$schema: "http://json-schema.org/draft-07/schema#",
},
run: async () => ({ uuid: uuidv4() }),
},
{
name: "echo",
description: "Mengembalikan data yang dikirim.",
inputSchema: {
type: "object",
properties: {
input: {
type: "string",
description: "Message to echo back",
},
},
required: ["input"],
additionalProperties: true,
$schema: "http://json-schema.org/draft-07/schema#",
},
run: async (input) => ({ echo: input }),
},
{
name: "Calculator",
description: "Useful for getting the result of a math expression. The input to this tool should be a valid mathematical expression that could be executed by a simple calculator.",
inputSchema: {
type: "object",
properties: {
input: {
type: "string",
},
},
required: ["input"],
additionalProperties: true,
$schema: "http://json-schema.org/draft-07/schema#",
},
run: async (input) => {
try {
// Simple math evaluation (be careful in production!)
const result = Function(`"use strict"; return (${input.input})`)();
return { result: String(result) };
} catch (error: any) {
throw new Error(`Invalid expression: ${error.message}`);
}
},
},
];
var tools = [] as any[];
// =====================
// MCP Protocol Types
@@ -119,16 +25,50 @@ type JSONRPCResponse = {
};
};
type JSONRPCNotification = {
jsonrpc: "2.0";
method: string;
params?: any;
};
// =====================
// Tool Executor
// =====================
export async function executeTool(
tool: any,
args: Record<string, any> = {},
baseUrl: string
) {
const x = tool["x-props"] || {};
const method = (x.method || "GET").toUpperCase();
const path = x.path || `/${tool.name}`;
const url = `${baseUrl}${path}`;
const opts: RequestInit = {
method,
headers: { "Content-Type": "application/json" },
};
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
opts.body = JSON.stringify(args || {});
}
const res = await fetch(url, opts);
const contentType = res.headers.get("content-type") || "";
const data = contentType.includes("application/json")
? await res.json()
: await res.text();
return {
success: res.ok,
status: res.status,
method,
path,
data,
};
}
// =====================
// MCP Handler
// MCP Handler (Async)
// =====================
function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
async function handleMCPRequestAsync(
request: JSONRPCRequest
): Promise<JSONRPCResponse> {
const { id, method, params } = request;
switch (method) {
@@ -138,13 +78,8 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
id,
result: {
protocolVersion: "2024-11-05",
capabilities: {
tools: {},
},
serverInfo: {
name: "elysia-mcp-server",
version: "1.0.0",
},
capabilities: { tools: {} },
serverInfo: { name: "elysia-mcp-server", version: "1.0.0" },
},
};
@@ -153,15 +88,16 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
jsonrpc: "2.0",
id,
result: {
tools: tools.map(({ name, description, inputSchema }) => ({
tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({
name,
description,
inputSchema,
"x-props": x,
})),
},
};
case "tools/call":
case "tools/call": {
const toolName = params?.name;
const tool = tools.find((t) => t.name === toolName);
@@ -169,18 +105,14 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
return {
jsonrpc: "2.0",
id,
error: {
code: -32601,
message: `Tool '${toolName}' not found`,
},
error: { code: -32601, message: `Tool '${toolName}' not found` },
};
}
try {
// Note: This is synchronous for simplicity
// In real implementation, you'd need to handle async properly
let result: any;
tool.run(params?.arguments || {}).then((r) => (result = r));
const baseUrl =
process.env.BUN_PUBLIC_BASE_URL || "http://localhost:3000";
const result = await executeTool(tool, params?.arguments || {}, baseUrl);
return {
jsonrpc: "2.0",
@@ -189,7 +121,7 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
content: [
{
type: "text",
text: JSON.stringify(result || { pending: true }),
text: JSON.stringify(result, null, 2),
},
],
},
@@ -198,111 +130,48 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
return {
jsonrpc: "2.0",
id,
error: {
code: -32603,
message: error.message,
},
error: { code: -32603, message: error.message },
};
}
}
case "ping":
return {
jsonrpc: "2.0",
id,
result: {},
};
return { jsonrpc: "2.0", id, result: {} };
default:
return {
jsonrpc: "2.0",
id,
error: {
code: -32601,
message: `Method '${method}' not found`,
},
error: { code: -32601, message: `Method '${method}' not found` },
};
}
}
async function handleMCPRequestAsync(request: JSONRPCRequest): Promise<JSONRPCResponse> {
const { id, method, params } = request;
if (method === "tools/call") {
const toolName = params?.name;
const tool = tools.find((t) => t.name === toolName);
if (!tool) {
return {
jsonrpc: "2.0",
id,
error: {
code: -32601,
message: `Tool '${toolName}' not found`,
},
};
}
try {
const result = await tool.run(params?.arguments || {});
return {
jsonrpc: "2.0",
id,
result: {
content: [
{
type: "text",
text: JSON.stringify(result),
},
],
},
};
} catch (error: any) {
return {
jsonrpc: "2.0",
id,
error: {
code: -32603,
message: error.message,
},
};
}
}
// For other methods, use sync handler
return handleMCPRequest(request);
}
// =====================
// Server Initialization
// Elysia MCP Server
// =====================
export const MCPRoute = new Elysia()
// =====================
// MCP HTTP Streamable Endpoint
// =====================
.post("/mcp/:sessionId", async ({ params, request, set }) => {
export const MCPRoute = new Elysia({
tags: ["MCP Server"]
})
.post("/mcp", async ({ request, set }) => {
if (!tools.length) {
tools = await getMcpTools();
}
set.headers["Content-Type"] = "application/json";
set.headers["Access-Control-Allow-Origin"] = "*";
// Optional: Check authorization
// if (!isAuthorized(request.headers)) {
// set.status = 401;
// return { error: "Unauthorized" };
// }
try {
const body = await request.json();
// Handle single request
if (!Array.isArray(body)) {
const response = await handleMCPRequestAsync(body as JSONRPCRequest);
return response;
const res = await handleMCPRequestAsync(body);
return res;
}
// Handle batch requests
const responses = await Promise.all(
body.map((req) => handleMCPRequestAsync(req as JSONRPCRequest))
const results = await Promise.all(
body.map((req) => handleMCPRequestAsync(req))
);
return responses;
return results;
} catch (error: any) {
set.status = 400;
return {
@@ -317,60 +186,58 @@ export const MCPRoute = new Elysia()
}
})
// =====================
// Simple tools list endpoint (for debugging)
// =====================
.get("/mcp/:sessionId/tools", ({ set }) => {
// Tools list (debug)
.get("/mcp/tools", async ({ set }) => {
if (!tools.length) {
tools = await getMcpTools();
}
set.headers["Access-Control-Allow-Origin"] = "*";
return {
data: tools.map(({ name, description, inputSchema }) => ({
tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({
name,
value: name,
description,
inputSchema,
"x-props": x,
})),
};
})
// =====================
// Session Status
// =====================
.get("/mcp/:sessionId/status", ({ params, set }) => {
// MCP status
.get("/mcp/status", ({ set }) => {
set.headers["Access-Control-Allow-Origin"] = "*";
return {
sessionId: params.sessionId,
status: "active",
timestamp: Date.now(),
};
return { status: "active", timestamp: Date.now() };
})
// =====================
// Health Check
// =====================
// Health check
.get("/health", ({ set }) => {
set.headers["Access-Control-Allow-Origin"] = "*";
return { status: "ok", timestamp: Date.now(), tools: tools.length };
})
.get("/mcp/init", async ({ set }) => {
const _tools = await getMcpTools();
tools = _tools;
return {
status: "ok",
timestamp: Date.now(),
success: true,
message: "MCP initialized",
tools: tools.length,
};
})
// =====================
// CORS preflight
// =====================
.options("/mcp/:sessionId", ({ set }) => {
// CORS
.options("/mcp", ({ set }) => {
set.headers["Access-Control-Allow-Origin"] = "*";
set.headers["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS";
set.headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization,X-API-Key";
set.headers["Access-Control-Allow-Headers"] =
"Content-Type,Authorization,X-API-Key";
set.status = 204;
return "";
})
.options("/mcp/:sessionId/tools", ({ set }) => {
.options("/mcp/tools", ({ set }) => {
set.headers["Access-Control-Allow-Origin"] = "*";
set.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS";
set.headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization,X-API-Key";
set.headers["Access-Control-Allow-Headers"] =
"Content-Type,Authorization,X-API-Key";
set.status = 204;
return "";
});
});

View File

@@ -0,0 +1,319 @@
import Elysia, { StatusMap, t } from "elysia"
import { generateNoPengajuanSurat } from "../lib/no-pengajuan-surat"
import { prisma } from "../lib/prisma"
import type { StatusPengaduan } from "generated/prisma"
const PelayananRoute = new Elysia({
prefix: "pelayanan",
tags: ["pelayanan"],
})
// --- KATEGORI PELAYANAN ---
.get("/category", async () => {
const data = await prisma.categoryPelayanan.findMany({
where: {
isActive: true
},
orderBy:{
name: "asc"
}
})
return data
}, {
detail: {
summary: "List Kategori Pelayanan Surat",
description: `tool untuk mendapatkan list kategori pelayanan surat`,
tags: ["mcp"]
}
})
.post("/category/create", async ({ body }) => {
const { name, syaratDokumen, dataText } = body
await prisma.categoryPelayanan.create({
data: {
name,
syaratDokumen,
dataText,
}
})
return `
${JSON.stringify(body)}
kategori pelayanan surat sudah dibuat`
}, {
body: t.Object({
name: t.String({ minLength: 1, error: "name harus diisi" }),
syaratDokumen: t.Array(t.String({ minLength: 1, error: "syaratDokumen harus diisi" })),
dataText: t.Array(t.String({ minLength: 1, error: "dataText harus diisi" })),
}),
detail: {
summary: "buat kategori pelayanan surat",
description: `tool untuk membuat kategori pelayanan surat`
}
})
.post("/category/update", async ({ body }) => {
const { id, name, syaratDokumen, dataText } = body
await prisma.categoryPelayanan.update({
where: {
id,
},
data: {
name,
syaratDokumen,
dataText,
}
})
return `
${JSON.stringify(body)}
kategori pelayanan surat sudah diperbarui`
}, {
body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
name: t.String({ minLength: 1, error: "name harus diisi" }),
syaratDokumen: t.Array(t.String({ minLength: 1, error: "syaratDokumen harus diisi" })),
dataText: t.Array(t.String({ minLength: 1, error: "dataText harus diisi" })),
}),
detail: {
summary: "update kategori pelayanan surat",
description: `tool untuk update kategori pelayanan surat`
}
})
.post("/category/delete", async ({ body }) => {
const { id } = body
await prisma.categoryPelayanan.update({
where: {
id,
},
data: {
isActive: false
}
})
return `
${JSON.stringify(body)}
kategori pelayanan surat sudah dihapus`
}, {
body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
}),
detail: {
summary: "delete kategori pelayanan surat",
description: `tool untuk delete kategori pelayanan surat`
}
})
// --- PELAYANAN SURAT ---
.get("/", async () => {
const data = await prisma.pelayananAjuan.findMany({
where: {
isActive: true
}
})
return data
}, {
detail: {
summary: "List Ajuan Pelayanan Surat",
description: `tool untuk mendapatkan list ajuan pelayanan surat`,
tags: ["mcp"]
}
})
.get("/detail", async ({ query }) => {
const { id } = query
const data = await prisma.pelayananAjuan.findUnique({
where: {
id,
}
})
return data
}, {
query: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
}),
detail: {
summary: "Detail Ajuan Pelayanan Surat",
description: `tool untuk mendapatkan detail ajuan pelayanan surat`,
tags: ["mcp"]
}
})
.post("/create", async ({ body }) => {
const { idCategory, idWarga, phone, dataText, syaratDokumen } = body
const noPengajuan = await generateNoPengajuanSurat()
let idCategoryFix = idCategory
let idWargaFix = idWarga
const category = await prisma.categoryPelayanan.findUnique({
where: {
id: idCategory,
}
})
if (!category) {
const cariCategory = await prisma.categoryPelayanan.findFirst({
where: {
name: idCategory,
}
})
if (!cariCategory) {
throw new Error("kategori pelayanan surat tidak ditemukan")
} else {
idCategoryFix = cariCategory.id
}
}
const warga = await prisma.warga.findUnique({
where: {
id: idWarga,
}
})
if (!warga) {
const cariWarga = await prisma.warga.findFirst({
where: {
phone,
}
})
if (!cariWarga) {
const wargaCreate = await prisma.warga.create({
data: {
name: idWarga,
phone,
},
select: {
id: true
}
})
idWargaFix = wargaCreate.id
} else {
idWargaFix = cariWarga.id
}
}
const pengaduan = await prisma.pelayananAjuan.create({
data: {
noPengajuan,
idCategory: idCategoryFix,
idWarga: idWargaFix,
},
select: {
id: true,
}
})
if (!pengaduan.id) {
throw new Error("gagal membuat pengaduan")
}
let dataInsertSyaratDokumen = []
let dataInsertDataText = []
for (const item of syaratDokumen) {
dataInsertSyaratDokumen.push({
idPengajuanLayanan: pengaduan.id,
idCategory: idCategoryFix,
jenis: item.jenis,
value: item.value,
})
}
for (const item of dataText) {
dataInsertDataText.push({
idPengajuanLayanan: pengaduan.id,
idCategory: idCategoryFix,
jenis: item.jenis,
value: item.value,
})
}
await prisma.syaratDokumenPelayanan.createMany({
data: dataInsertSyaratDokumen,
})
await prisma.dataTextPelayanan.createMany({
data: dataInsertDataText,
})
await prisma.historyPelayanan.create({
data: {
idPengajuanLayanan: pengaduan.id,
deskripsi: "Pengajuan surat dibuat",
}
})
return `
${JSON.stringify(body)}
pengaduan sudah dibuat`
}, {
body: t.Object({
idCategory: t.String({ minLength: 1, error: "idCategory harus diisi" }),
idWarga: t.String({ minLength: 1, error: "idWarga harus diisi" }),
phone: t.String({ minLength: 1, error: "phone harus diisi" }),
dataText: t.Array(t.Object({
jenis: t.String({ minLength: 1, error: "jenis harus diisi" }),
value: t.String({ minLength: 1, error: "value harus diisi" }),
})),
syaratDokumen: t.Array(t.Object({
jenis: t.String({ minLength: 1, error: "jenis harus diisi" }),
value: t.String({ minLength: 1, error: "value harus diisi" }),
})),
}),
detail: {
summary: "Create Pengajuan Pelayanan Surat",
description: `tool untuk membuat pengajuan pelayanan surat`,
tags: ["mcp"]
}
})
.post("/update-status", async ({ body }) => {
const { id, status, keterangan, idUser } = body
const pengajuan = await prisma.pelayananAjuan.update({
where: {
id,
},
data: {
status: status as StatusPengaduan,
}
})
if (!pengajuan) {
throw new Error("gagal membuat pengajuan")
}
await prisma.historyPelayanan.create({
data: {
idPengajuanLayanan: pengajuan.id,
deskripsi: "Pengajuan surat diperbarui",
keteranganAlasan: keterangan,
}
})
return `
${JSON.stringify(body)}
pengajuan surat sudah diperbarui`
}, {
body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
status: t.String({ minLength: 1, error: "status harus diisi" }),
keterangan: t.String({ minLength: 1, error: "keterangan harus diisi" }),
idUser: t.String({ minLength: 1, error: "idUser harus diisi" }),
}),
detail: {
summary: "Update Status Pengajuan Pelayanan Surat",
description: `tool untuk update status pengajuan pelayanan surat`,
tags: ["mcp"]
}
})
export default PelayananRoute

View File

@@ -13,13 +13,17 @@ const PengaduanRoute = new Elysia({
const data = await prisma.categoryPengaduan.findMany({
where: {
isActive: true
},
orderBy: {
name: "asc"
}
})
return data
}, {
detail: {
summary: "get kategori pengaduan",
description: `tool untuk mendapatkan kategori pengaduan`
summary: "List Kategori Pengaduan",
description: `tool untuk mendapatkan list kategori pengaduan`,
tags: ["mcp"]
}
})
.post("/category/create", async ({ body }) => {
@@ -100,15 +104,67 @@ const PengaduanRoute = new Elysia({
// --- PENGADUAN ---
.post("/create", async ({ body }) => {
const { title, detail, location, image, idCategory, idWarga } = body
const { title, detail, location, image, idCategory, idWarga, phone } = body
const noPengaduan = await generateNoPengaduan()
let idCategoryFix = idCategory
let idWargaFix = idWarga
const category = await prisma.categoryPengaduan.findUnique({
where: {
id: idCategory,
}
})
if (!category) {
const cariCategory = await prisma.categoryPengaduan.findFirst({
where: {
name: idCategory,
}
})
if (!cariCategory) {
idCategoryFix = "lainnya"
} else {
idCategoryFix = cariCategory.id
}
}
const warga = await prisma.warga.findUnique({
where: {
id: idWarga,
}
})
if (!warga) {
const cariWarga = await prisma.warga.findFirst({
where: {
phone,
}
})
if (!cariWarga) {
const wargaCreate = await prisma.warga.create({
data: {
name: idWarga,
phone,
},
select: {
id: true
}
})
idWargaFix = wargaCreate.id
} else {
idWargaFix = cariWarga.id
}
}
const pengaduan = await prisma.pengaduan.create({
data: {
title,
detail,
idCategory,
idWarga,
idCategory: idCategoryFix,
idWarga: idWargaFix,
location,
image,
noPengaduan,
@@ -138,17 +194,19 @@ const PengaduanRoute = new Elysia({
title: t.String({ minLength: 1, error: "title harus diisi" }),
detail: t.String({ minLength: 1, error: "detail harus diisi" }),
location: t.String({ minLength: 1, error: "location harus diisi" }),
image: t.String({ minLength: 1, error: "image harus diisi" }),
image: t.Any(),
idCategory: t.String({ minLength: 1, error: "idCategory harus diisi" }),
idWarga: t.String({ minLength: 1, error: "idWarga harus diisi" }),
phone: t.String({ minLength: 1, error: "phone harus diisi" }),
}),
detail: {
summary: "buat pengaduan",
description: `tool untuk membuat pengaduan`
summary: "Create Pengaduan Warga",
description: `tool untuk membuat pengaduan warga`,
tags: ["mcp"]
}
})
.post("/update-status", async ({ body }) => {
const { id, status, keterangan } = body
const { id, status, keterangan, idUser } = body
let deskripsi = ""
const pengaduan = await prisma.pengaduan.update({
@@ -165,13 +223,13 @@ const PengaduanRoute = new Elysia({
throw new Error("gagal membuat pengaduan")
}
if(status === "diterima") {
if (status === "diterima") {
deskripsi = "Pengaduan diterima oleh admin"
} else if(status === "dikerjakan") {
} else if (status === "dikerjakan") {
deskripsi = "Pengaduan dikerjakan oleh petugas"
} else if(status === "ditolak") {
} else if (status === "ditolak") {
deskripsi = "Pengaduan ditolak dengan keterangan " + keterangan
} else if(status === "selesai") {
} else if (status === "selesai") {
deskripsi = "Pengaduan selesai"
}
@@ -180,7 +238,7 @@ const PengaduanRoute = new Elysia({
idPengaduan: pengaduan.id,
deskripsi,
status: status as StatusPengaduan,
idUser: ""
idUser,
}
})
@@ -192,12 +250,171 @@ const PengaduanRoute = new Elysia({
body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
status: t.String({ minLength: 1, error: "status harus diisi" }),
keterangan: t.Any()
keterangan: t.Any(),
idUser: t.String({ minLength: 1, error: "idUser harus diisi" }),
}),
detail: {
summary: "update status pengaduan",
summary: "Update status pengaduan",
description: `tool untuk update status pengaduan`
}
})
.get("/detail", async ({ query }) => {
const { id } = query
const data = await prisma.pengaduan.findUnique({
where: {
id,
},
select: {
id: true,
noPengaduan: true,
title: true,
detail: true,
location: true,
image: true,
idCategory: true,
idWarga: true,
status: true,
keterangan: true,
createdAt: true,
updatedAt: true,
CategoryPengaduan: {
select: {
name: true
}
},
Warga: {
select: {
name: true,
}
}
}
})
const dataHistory = await prisma.historyPengaduan.findMany({
where: {
idPengaduan: id,
},
select: {
id: true,
deskripsi: true,
status: true,
createdAt: true,
idUser: true,
User: {
select: {
name: true,
}
}
}
})
const dataHistoryFix = dataHistory.map((item) => {
return {
id: item.id,
deskripsi: item.deskripsi,
status: item.status,
createdAt: item.createdAt,
idUser: item.idUser,
nameUser: item.User?.name,
}
})
const datafix = {
id: data?.id,
noPengaduan: data?.noPengaduan,
title: data?.title,
detail: data?.detail,
location: data?.location,
image: data?.image,
CategoryPengaduan: data?.CategoryPengaduan.name,
idWarga: data?.idWarga,
nameWarga: data?.Warga?.name,
status: data?.status,
keterangan: data?.keterangan,
createdAt: data?.createdAt,
updatedAt: data?.updatedAt,
history: dataHistoryFix,
}
return datafix
}, {
detail: {
summary: "Detail Pengaduan Warga",
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan`,
tags: ["mcp"]
}
})
.get("/", async ({ query }) => {
const { take, page, search } = query
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
const data = await prisma.pengaduan.findMany({
skip,
take: !take ? 10 : Number(take),
orderBy: {
createdAt: "asc"
},
where: {
isActive: true,
OR: [
{
title: {
contains: search ?? "",
mode: "insensitive"
},
},
{
noPengaduan: {
contains: search ?? "",
mode: "insensitive"
},
},
{
detail: {
contains: search ?? "",
mode: "insensitive"
},
}
],
},
select: {
id: true,
noPengaduan: true,
title: true,
detail: true,
location: true,
status: true,
createdAt: true,
CategoryPengaduan: {
select: {
name: true
}
},
Warga: {
select: {
name: true,
}
}
}
})
const dataFix = data.map((item) => {
return {
noPengaduan: item.noPengaduan,
title: item.title,
detail: item.detail,
status: item.status,
createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
}
})
return dataFix
}, {
detail: {
summary: "List Pengaduan Warga",
description: `tool untuk mendapatkan list pengaduan warga`,
tags: ["mcp"]
}
})
export default PengaduanRoute

612
tools.json Normal file
View File

@@ -0,0 +1,612 @@
[
{
"name": "apikey_create",
"description": "create api key by user",
"x-props": {
"method": "POST",
"path": "/api/apikey/create",
"operationId": "postApiApikeyCreate",
"tag": "apikey",
"deprecated": false,
"summary": "create"
},
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"expiredAt": {
"format": "date-time",
"type": "string"
}
},
"required": [
"name",
"description"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "apikey_list",
"description": "get api key list by user",
"x-props": {
"method": "GET",
"path": "/api/apikey/list",
"operationId": "getApiApikeyList",
"tag": "apikey",
"deprecated": false,
"summary": "list"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "apikey_delete",
"description": "delete api key by id",
"x-props": {
"method": "DELETE",
"path": "/api/apikey/delete",
"operationId": "deleteApiApikeyDelete",
"tag": "apikey",
"deprecated": false,
"summary": "delete"
},
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "darmasaba_repos",
"description": "get list of repositories",
"x-props": {
"method": "GET",
"path": "/api/darmasaba/repos",
"operationId": "getApiDarmasabaRepos",
"tag": "darmasaba",
"deprecated": false,
"summary": "repos"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "darmasaba_ls",
"description": "get list of dir in darmasaba",
"x-props": {
"method": "GET",
"path": "/api/darmasaba/ls",
"operationId": "getApiDarmasabaLs",
"tag": "darmasaba",
"deprecated": false,
"summary": "ls"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "darmasaba_ls_by_dir",
"description": "get list of files in darmasaba/<dir>",
"x-props": {
"method": "GET",
"path": "/api/darmasaba/ls/{dir}",
"operationId": "getApiDarmasabaLsByDir",
"tag": "darmasaba",
"deprecated": false,
"summary": "ls"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "darmasaba_file_by_dir_by_file_name",
"description": "get content of file in darmasaba/<dir>/<file_name>",
"x-props": {
"method": "GET",
"path": "/api/darmasaba/file/{dir}/{file_name}",
"operationId": "getApiDarmasabaFileByDirByFile_name",
"tag": "darmasaba",
"deprecated": false,
"summary": "file"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "darmasaba_list_pengetahuan_umum",
"description": "get list of files in darmasaba/pengetahuan-umum",
"x-props": {
"method": "GET",
"path": "/api/darmasaba/list-pengetahuan-umum",
"operationId": "getApiDarmasabaList-pengetahuan-umum",
"tag": "darmasaba",
"deprecated": false,
"summary": "list-pengetahuan-umum"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "darmasaba_pengetahuan_umum_by_file_name",
"description": "get content of file in darmasaba/pengetahuan-umum/<file_name>",
"x-props": {
"method": "GET",
"path": "/api/darmasaba/pengetahuan-umum/{file_name}",
"operationId": "getApiDarmasabaPengetahuan-umumByFile_name",
"tag": "darmasaba",
"deprecated": false,
"summary": "pengetahuan-umum"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "darmasaba_buat_pengaduan",
"description": "tool untuk membuat pengaduan atau pelaporan warga kepada desa darmasaba",
"x-props": {
"method": "POST",
"path": "/api/darmasaba/buat-pengaduan",
"operationId": "postApiDarmasabaBuat-pengaduan",
"tag": "darmasaba",
"deprecated": false,
"summary": "buat-pengaduan atau pelaporan"
},
"inputSchema": {
"type": "object",
"properties": {
"jenis_laporan": {
"minLength": 1,
"error": "jenis laporan harus diisi",
"type": "string"
},
"name": {
"minLength": 1,
"error": "name harus diisi",
"type": "string"
},
"phone": {
"minLength": 1,
"error": "phone harus diisi",
"type": "string"
},
"detail": {
"minLength": 1,
"error": "detail harus diisi",
"type": "string"
}
},
"required": [
"jenis_laporan",
"name",
"phone",
"detail"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "darmasaba_status_pengaduan",
"description": "melikat status pengaduan dari user",
"x-props": {
"method": "POST",
"path": "/api/darmasaba/status-pengaduan",
"operationId": "postApiDarmasabaStatus-pengaduan",
"tag": "darmasaba",
"deprecated": false,
"summary": "lihat status pengaduan"
},
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"phone": {
"type": "string"
}
},
"required": [
"name",
"phone"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "credential_create",
"description": "create credential",
"x-props": {
"method": "POST",
"path": "/api/credential/create",
"operationId": "postApiCredentialCreate",
"tag": "credential",
"deprecated": false,
"summary": "create"
},
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"name",
"value"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "credential_list",
"description": "get credential list",
"x-props": {
"method": "GET",
"path": "/api/credential/list",
"operationId": "getApiCredentialList",
"tag": "credential",
"deprecated": false,
"summary": "list"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "credential_rm",
"description": "delete credential by id",
"x-props": {
"method": "DELETE",
"path": "/api/credential/rm",
"operationId": "deleteApiCredentialRm",
"tag": "credential",
"deprecated": false,
"summary": "rm"
},
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "user_find",
"description": "find user",
"x-props": {
"method": "GET",
"path": "/api/user/find",
"operationId": "getApiUserFind",
"tag": "user",
"deprecated": false,
"summary": "find"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "user_upsert",
"description": "upsert user",
"x-props": {
"method": "POST",
"path": "/api/user/upsert",
"operationId": "postApiUserUpsert",
"tag": "user",
"deprecated": false,
"summary": "upsert"
},
"inputSchema": {
"type": "object",
"properties": {
"name": {
"minLength": 1,
"error": "name is required",
"type": "string"
},
"phone": {
"minLength": 1,
"error": "phone is required",
"type": "string"
}
},
"required": [
"name",
"phone"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "layanan_list",
"description": "Returns the list of all available public services.",
"x-props": {
"method": "GET",
"path": "/api/layanan/list",
"operationId": "getApiLayananList",
"tag": "layanan",
"deprecated": false,
"summary": "List Layanan"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "layanan_create_ktp",
"description": "Create a new service request for KTP or KK.",
"x-props": {
"method": "POST",
"path": "/api/layanan/create-ktp",
"operationId": "postApiLayananCreate-ktp",
"tag": "layanan",
"deprecated": false,
"summary": "Create Layanan KTP/KK"
},
"inputSchema": {
"type": "object",
"properties": {
"jenis": {
"anyOf": [
{
"const": "ktp",
"type": "string"
},
{
"const": "kk",
"type": "string"
}
]
},
"nama": {
"minLength": 3,
"description": "Nama pemohon layanan",
"type": "string"
},
"deskripsi": {
"minLength": 5,
"description": "Deskripsi singkat permohonan layanan",
"type": "string"
}
},
"required": [
"jenis",
"nama",
"deskripsi"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "layanan_status_ktp",
"description": "Retrieve the current status of a KTP/KK request by unique ID.",
"x-props": {
"method": "POST",
"path": "/api/layanan/status-ktp",
"operationId": "postApiLayananStatus-ktp",
"tag": "layanan",
"deprecated": false,
"summary": "Cek Status KTP"
},
"inputSchema": {
"type": "object",
"properties": {
"uniqid": {
"description": "Unique ID layanan",
"type": "string"
}
},
"required": [
"uniqid"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "aduan_create",
"description": "create aduan",
"x-props": {
"method": "POST",
"path": "/api/aduan/create",
"operationId": "postApiAduanCreate",
"tag": "aduan",
"deprecated": false,
"summary": "create"
},
"inputSchema": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": [
"title",
"description"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "aduan_aduan_sampah",
"description": "tool untuk membuat aduan sampah liar",
"x-props": {
"method": "POST",
"path": "/api/aduan/aduan-sampah",
"operationId": "postApiAduanAduan-sampah",
"tag": "aduan",
"deprecated": false,
"summary": "aduan sampah"
},
"inputSchema": {
"type": "object",
"properties": {
"judul": {
"type": "string"
},
"deskripsi": {
"type": "string"
}
},
"required": [
"judul",
"deskripsi"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "aduan_list_aduan_sampah",
"description": "tool untuk melihat list aduan sampah liar",
"x-props": {
"method": "GET",
"path": "/api/aduan/list-aduan-sampah",
"operationId": "getApiAduanList-aduan-sampah",
"tag": "aduan",
"deprecated": false,
"summary": "list aduan sampah"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "auth_login",
"description": "Login with phone; auto-register if not found",
"x-props": {
"method": "POST",
"path": "/auth/login",
"operationId": "postAuthLogin",
"tag": "auth",
"deprecated": false,
"summary": "login"
},
"inputSchema": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"password": {
"type": "string"
}
},
"required": [
"email",
"password"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "auth_logout",
"description": "Logout (clear token cookie)",
"x-props": {
"method": "DELETE",
"path": "/auth/logout",
"operationId": "deleteAuthLogout",
"tag": "auth",
"deprecated": false,
"summary": "logout"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "health",
"description": "Execute GET /health",
"x-props": {
"method": "GET",
"path": "/health",
"operationId": "getHealth",
"deprecated": false
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
]

8
x.sh
View File

@@ -1,6 +1,2 @@
# curl -N -v -X GET "https://cld-dkr-prod-jenna-mcp.wibudev.com/mcp/test-session-id"
curl -X POST http://localhost:3000/mcp/test-room \
-H "Content-Type: application/json" \
-H "X-API-Key: super-secret-key" \
-d '{"event":"notice","data":{"msg":"hello world"}}'
TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJob3N0Iiwic3ViIjoiYmlwIiwicGF5bG9hZCI6IntcIm5hbWVcIjpcImplbm5hLW1jcFwiLFwiZGVzY3JpcHRpb25cIjpcInVudHVrIGplbm5hIG1jcFwiLFwiZXhwaXJlZEF0XCI6XCIyMDQ4LTA2LTI4XCJ9IiwiZXhwIjoyNDc2OTE1MjAwLCJpYXQiOjE3NjE2NDA1NDN9.EY4P246r3GBHo3yJgG0c5hvgG7p1z2x0KNL2fWBMpk8
curl http://localhost:3000/api/pengaduan/category

176
xx.ts
View File

@@ -1,127 +1,65 @@
import { readdirSync, statSync, writeFileSync } from "fs";
import _ from "lodash";
import { basename, extname, join, relative } from "path";
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Elysia } from 'elysia'
import jwt, { type JWTPayloadSpec } from '@elysiajs/jwt'
import bearer from '@elysiajs/bearer'
import { prisma } from '../lib/prisma'
const PAGES_DIR = join(process.cwd(), "src/pages");
const OUTPUT_FILE = join(process.cwd(), "src/AppRoutes.tsx");
// =========================================================
// JWT Secret Validation
// =========================================================
const secret = process.env.JWT_SECRET
if (!secret) throw new Error('JWT_SECRET environment variable is missing')
// 🧩 Ubah nama file ke nama komponen (PascalCase)
const toComponentName = (fileName: string) =>
fileName
.replace(/_/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase())
.replace(/\s/g, "");
// =========================================================
// Auth Middleware Plugin
// =========================================================
export default function apiAuth(app: Elysia) {
if (!secret) throw new Error('JWT_SECRET environment variable is missing')
return app
// Register Bearer and JWT plugins
.use(bearer()) // ✅ Extracts Bearer token automatically (case-insensitive)
.use(
jwt({
name: 'jwt',
secret,
})
)
// 🧩 Ubah nama file ke path route
function toRoutePath(name: string): string {
if (name.toLowerCase() === "home") return "/";
if (name.toLowerCase() === "login") return "/login";
if (name.toLowerCase() === "notfound") return "/*";
if (name.endsWith("_page")) return name.replace("_page", "").toLowerCase();
if (name.startsWith("form_")) return name.replace("form_", "").toLowerCase();
return name.toLowerCase();
}
// Derive user from JWT or cookie
.derive(async ({ bearer, cookie, jwt }) => {
// Normalize token type to string or undefined
const token =
(typeof bearer === 'string' ? bearer : undefined) ??
(typeof cookie?.token?.value === 'string' ? cookie.token.value : undefined)
// 🧭 Scan folder pages secara rekursif
function scan(dir: string): any[] {
const items = readdirSync(dir);
const routes: any[] = [];
let user: Awaited<ReturnType<typeof prisma.user.findUnique>> | null = null
for (const item of items) {
const full = join(dir, item);
const stat = statSync(full);
if (token) {
try {
const decoded = (await jwt.verify(token)) as JWTPayloadSpec
if (stat.isDirectory()) {
routes.push({
name: item,
path: item.toLowerCase(),
children: scan(full),
});
} else if (extname(item) === ".tsx") {
routes.push({
name: basename(item, ".tsx"),
filePath: relative(join(process.cwd(), "src"), full).replace(/\\/g, "/"),
});
if (decoded?.sub && typeof decoded.sub === 'string') {
user = await prisma.user.findUnique({
where: { id: decoded.sub },
})
}
} catch (err) {
console.warn('[SERVER][apiAuth] Invalid token:', (err as Error).message)
}
}
return routes;
}
return { user }
})
// Protect all routes by default
.onBeforeHandle(({ user, set, request }) => {
// Whitelist public routes if needed
const publicPaths = ['/auth/login', '/auth/register', '/public']
if (publicPaths.some((path) => request.url.includes(path))) return
if (!user) {
set.status = 401
return { error: 'Unauthorized' }
}
})
}
// 🏗️ Generate <Route> JSX dari struktur folder
function generateJSX(routes: any[], parentPath = ""): string {
let jsx = "";
for (const route of routes) {
if (route.children) {
// cari layout di folder
const layout = route.children.find((r: any) => r.name.endsWith("_layout"));
if (layout) {
const LayoutComponent = toComponentName(layout.name.replace("_layout", "Layout"));
const nested = route.children.filter((r: any) => r !== layout);
const nestedRoutes = generateJSX(nested, `${parentPath}/${route.path}`);
jsx += `
<Route path="${parentPath}/${route.path}" element={<${LayoutComponent} />}>
${nestedRoutes}
</Route>
`;
} else {
jsx += generateJSX(route.children, `${parentPath}/${route.path}`);
}
} else {
const Component = toComponentName(route.name);
const routePath = toRoutePath(route.name);
// Hapus duplikasi segmen
const fullPath =
routePath.startsWith("/")
? routePath
: `${parentPath}/${_.kebabCase(routePath)}`.replace(/\/+/g, "/");
jsx += `<Route path="${fullPath}" element={<${Component} />} />\n`;
}
}
return jsx;
}
// 🧾 Generate import otomatis
function generateImports(routes: any[]): string {
const imports = new Set<string>();
function collect(rs: any[]) {
for (const r of rs) {
if (r.children) collect(r.children);
else {
const Comp = toComponentName(r.name);
imports.add(`import ${Comp} from "./${r.filePath.replace(/\.tsx$/, "")}";`);
}
}
}
collect(routes);
return Array.from(imports).join("\n");
}
// 🧠 Main generator
const allRoutes = scan(PAGES_DIR);
const imports = generateImports(allRoutes);
const jsxRoutes = generateJSX(allRoutes);
const finalCode = `
// ⚡ Auto-generated by generateRoutes.ts — DO NOT EDIT MANUALLY
import { BrowserRouter, Routes, Route } from "react-router-dom";
import ProtectedRoute from "./components/ProtectedRoute";
${imports}
export default function AppRoutes() {
return (
<BrowserRouter>
<Routes>
${jsxRoutes}
</Routes>
</BrowserRouter>
);
}
`;
writeFileSync(OUTPUT_FILE, finalCode);
console.log("✅ Routes generated successfully → src/AppRoutes.generated.tsx");
Bun.spawnSync(["bunx", "prettier", "--write", "src/**/*.tsx"])