upd: upload gambar
Deskripsi: - fungsi upload gambar - verifikasi nomer hp - get list pengaduan by nomer hp NO Issues
This commit is contained in:
15
src/server/lib/normalizePhone.ts
Normal file
15
src/server/lib/normalizePhone.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export function normalizePhoneNumber({ phone }: { phone: string }) {
|
||||
// Hapus semua spasi, tanda hubung, atau karakter non-digit (+ tetap dipertahankan untuk dicek)
|
||||
let cleaned = phone.trim().replace(/[\s-]/g, "");
|
||||
|
||||
// Jika diawali dengan +62 → ganti jadi 62
|
||||
if (cleaned.startsWith("+62")) {
|
||||
cleaned = "62" + cleaned.slice(3);
|
||||
}
|
||||
// Jika diawali dengan 0 → ganti jadi 62
|
||||
else if (cleaned.startsWith("0")) {
|
||||
cleaned = "62" + cleaned.slice(1);
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
241
src/server/lib/seafile.ts
Normal file
241
src/server/lib/seafile.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env bun
|
||||
import { promises as fs } from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
// --- Constants ---
|
||||
const CONFIG_FILE = path.join(os.homedir(), '.note.conf');
|
||||
|
||||
// --- Types ---
|
||||
interface Config {
|
||||
TOKEN?: string;
|
||||
REPO?: string;
|
||||
URL?: string;
|
||||
}
|
||||
|
||||
export const defaultConfigSF: Config = {
|
||||
TOKEN: process.env.SF_TOKEN,
|
||||
REPO: process.env.SF_REPO,
|
||||
URL: process.env.SF_URL,
|
||||
}
|
||||
|
||||
// --- Config Management ---
|
||||
// async function createDefaultConfig(): Promise<void> {
|
||||
// const defaultConfig = `TOKEN=fa49bf1774cad2ec89d2882ae2c6ac1f5d7df445
|
||||
// REPO=repos/e23626dc-cc18-4bb8-8fbc-d103b7d33bc8
|
||||
// URL=https://cld-dkr-makuro-seafile.wibudev.com/api2
|
||||
// `;
|
||||
// await fs.writeFile(CONFIG_FILE, defaultConfig, 'utf8');
|
||||
// }
|
||||
|
||||
// async function editConfig(): Promise<void> {
|
||||
// if (!(await fs.stat(CONFIG_FILE)).isFile()) {
|
||||
// createDefaultConfig();
|
||||
// }
|
||||
// const editor = process.env.EDITOR || 'vim';
|
||||
// try {
|
||||
// execSync(`${editor} "${CONFIG_FILE}"`, { stdio: 'inherit' });
|
||||
// } catch {
|
||||
// console.error('❌ Failed to open editor');
|
||||
// process.exit(1);
|
||||
// }
|
||||
// }
|
||||
|
||||
export async function loadConfig(): Promise<Config> {
|
||||
if (!(await fs.stat(CONFIG_FILE)).isFile()) {
|
||||
console.error(`⚠️ Config file not found at ${CONFIG_FILE}`);
|
||||
console.error('Run: bun note.ts config to create/edit it.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const configContent = await fs.readFile(CONFIG_FILE, 'utf8');
|
||||
const config: Config = {};
|
||||
|
||||
configContent.split('\n').forEach((line) => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) return;
|
||||
|
||||
const [key, ...valueParts] = trimmed.split('=');
|
||||
if (key && valueParts.length > 0) {
|
||||
let value = valueParts.join('=').trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
config[key as keyof Config] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (!config.TOKEN || !config.REPO || !config.URL) {
|
||||
console.error(`❌ Config invalid. Please set TOKEN, REPO, and URL inside ${CONFIG_FILE}`);
|
||||
process.exit(1);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
// --- HTTP Helpers ---
|
||||
export async function fetchWithAuth(config: Config, url: string, options: RequestInit = {}): Promise<Response> {
|
||||
const headers = {
|
||||
Authorization: `Token ${config.TOKEN}`,
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
if (!response.ok) {
|
||||
console.error(`❌ Request failed: ${response.status} ${response.statusText}`);
|
||||
console.error(`🔍 URL: ${url}`);
|
||||
console.error(`🔍 Headers:`, headers);
|
||||
|
||||
try {
|
||||
const errorText = await response.text();
|
||||
console.error(`🔍 Response body: ${errorText}`);
|
||||
} catch {
|
||||
console.error('🔍 Could not read response body');
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// --- Commands ---
|
||||
export async function testConnection(config: Config): Promise<string> {
|
||||
try {
|
||||
const response = await fetchWithAuth(config, `${config.URL}/ping/`);
|
||||
return `✅ API connection successful: ${await response.text()}`
|
||||
} catch {
|
||||
// return '⚠️ API ping failed, trying repo access...'
|
||||
try {
|
||||
await fetchWithAuth(config, `${config.URL}/${config.REPO}/`);
|
||||
return `✅ Repo access successful`
|
||||
} catch {
|
||||
return '❌ Both API ping and repo access failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function listFiles(config: Config): Promise<{ name: string }[]> {
|
||||
const url = `${config.URL}/${config.REPO}/dir/?p=/`;
|
||||
const response = await fetchWithAuth(config, url);
|
||||
|
||||
try {
|
||||
const files = (await response.json()) as { name: string }[];
|
||||
return files
|
||||
} catch {
|
||||
console.error('❌ Failed to parse response');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export async function catFile(config: Config, fileName: string): Promise<string> {
|
||||
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`);
|
||||
const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, '');
|
||||
const content = await (await fetchWithAuth(config, downloadUrl)).text();
|
||||
return content
|
||||
}
|
||||
|
||||
export async function uploadFile(config: Config, file: File): Promise<string> {
|
||||
const remoteName = path.basename(file.name);
|
||||
|
||||
// 1. Dapatkan upload link (pakai Authorization)
|
||||
const uploadUrlResponse = await fetchWithAuth(
|
||||
config,
|
||||
`${config.URL}/${config.REPO}/upload-link/`
|
||||
);
|
||||
const uploadUrl = (await uploadUrlResponse.text()).replace(/"/g, "");
|
||||
|
||||
// 2. Siapkan form-data
|
||||
const formData = new FormData();
|
||||
formData.append("parent_dir", "/");
|
||||
formData.append("relative_path", "syarat-dokumen"); // tanpa slash di akhir
|
||||
formData.append("file", file, remoteName); // file langsung, jangan pakai Blob
|
||||
|
||||
// 3. Upload file TANPA Authorization header, token di query param
|
||||
const res = await fetch(`${uploadUrl}?token=${config.TOKEN}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const text = await res.text();
|
||||
console.log(text);
|
||||
|
||||
if (!res.ok) throw new Error(`Upload failed: ${text}`);
|
||||
return `✅ Uploaded ${file.name} successfully`;
|
||||
}
|
||||
|
||||
|
||||
export async function removeFile(config: Config, fileName: string): Promise<string> {
|
||||
await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`, { method: 'DELETE' });
|
||||
return `🗑️ Removed ${fileName}`
|
||||
}
|
||||
|
||||
export async function moveFile(config: Config, oldName: string, newName: string): Promise<string> {
|
||||
const url = `${config.URL}/${config.REPO}/file/?p=/${oldName}`;
|
||||
const formData = new FormData();
|
||||
formData.append('operation', 'rename');
|
||||
formData.append('newname', newName);
|
||||
|
||||
await fetchWithAuth(config, url, { method: 'POST', body: formData });
|
||||
return `✏️ Renamed ${oldName} → ${newName}`
|
||||
}
|
||||
|
||||
export async function downloadFile(config: Config, remoteFile: string, localFile?: string): Promise<string> {
|
||||
const localName = localFile || remoteFile;
|
||||
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${remoteFile}`);
|
||||
const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, '');
|
||||
|
||||
const buffer = Buffer.from(await (await fetchWithAuth(config, downloadUrl)).arrayBuffer());
|
||||
await fs.writeFile(localName, buffer);
|
||||
return `⬇️ Downloaded ${remoteFile} → ${localName}`
|
||||
}
|
||||
|
||||
export async function getFileLink(config: Config, fileName: string): Promise<string> {
|
||||
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`);
|
||||
return `🔗 Link for ${fileName}:\n${(await downloadUrlResponse.text()).replace(/"/g, '')}`
|
||||
}
|
||||
|
||||
// function showHelp(): void {
|
||||
// return `note - simple CLI for wibu not
|
||||
// Usage:
|
||||
// not3 ls List files
|
||||
// not3 cat <file> Show file content
|
||||
// not3 cp <local> [remote] Upload file
|
||||
// not3 rm <remote> Remove file
|
||||
// not3 mv <old> <new> Rename/move file
|
||||
// not3 get <remote> [local] Download file
|
||||
// not3 link <file> Get file link/URL
|
||||
// not3 test Test API connection
|
||||
// not3 config Edit config (~/.note.conf)
|
||||
|
||||
// Config (~/.note.conf):
|
||||
// TOKEN=your_seafile_token
|
||||
// REPO=repos/<repo-id>
|
||||
// URL=your_seafile_url/api2
|
||||
|
||||
// Version: ${version}`);
|
||||
// }
|
||||
|
||||
// --- Main ---
|
||||
// async function not3(): Promise<void> {
|
||||
// const [cmd, ...args] = process.argv.slice(2);
|
||||
// if (cmd === 'config') return editConfig();
|
||||
|
||||
// const config = await loadConfig();
|
||||
// switch (cmd) {
|
||||
// case 'test': return testConnection(config);
|
||||
// case 'ls': return listFiles(config);
|
||||
// case 'cat': return args[0] ? catFile(config, args[0]) : console.error('Usage: bun note.ts cat <file>');
|
||||
// case 'cp': return args[0] ? uploadFile(config, args[0], args[1]) : console.error('Usage: bun note.ts cp <local_file> [remote_file]');
|
||||
// case 'rm': return args[0] ? removeFile(config, args[0]) : console.error('Usage: bun note.ts rm <remote_file>');
|
||||
// case 'mv': return args[1] ? moveFile(config, args[0]!, args[1]) : console.error('Usage: bun note.ts mv <old_name> <new_name>');
|
||||
// case 'get': return args[0] ? downloadFile(config, args[0], args[1]) : console.error('Usage: bun note.ts get <remote_file> [local_file]');
|
||||
// case 'link': return args[0] ? getFileLink(config, args[0]) : console.error('Usage: bun note.ts link <file>');
|
||||
// default: return showHelp();
|
||||
// }
|
||||
// }
|
||||
|
||||
// not3().catch((error) => {
|
||||
// console.error('❌ Error:', error);
|
||||
// process.exit(1);
|
||||
// });
|
||||
@@ -1,7 +1,10 @@
|
||||
import { swagger } from "@elysiajs/swagger"
|
||||
import Elysia, { t } from "elysia"
|
||||
import type { StatusPengaduan } from "generated/prisma"
|
||||
import { generateNoPengaduan } from "../lib/no-pengaduan"
|
||||
import { prisma } from "../lib/prisma"
|
||||
import { defaultConfigSF, testConnection, uploadFile } from "../lib/seafile"
|
||||
import { normalizePhoneNumber } from "../lib/normalizePhone"
|
||||
|
||||
const PengaduanRoute = new Elysia({
|
||||
prefix: "pengaduan",
|
||||
@@ -136,9 +139,10 @@ const PengaduanRoute = new Elysia({
|
||||
})
|
||||
|
||||
if (!warga) {
|
||||
const cariWarga = await prisma.warga.findFirst({
|
||||
const nomorHP = normalizePhoneNumber({ phone })
|
||||
const cariWarga = await prisma.warga.findUnique({
|
||||
where: {
|
||||
phone,
|
||||
phone: nomorHP,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -146,7 +150,7 @@ const PengaduanRoute = new Elysia({
|
||||
const wargaCreate = await prisma.warga.create({
|
||||
data: {
|
||||
name: idWarga,
|
||||
phone,
|
||||
phone: nomorHP,
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
@@ -346,7 +350,7 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
.get("/", async ({ query }) => {
|
||||
const { take, page, search } = query
|
||||
const { take, page, search, phone } = query
|
||||
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
|
||||
|
||||
const data = await prisma.pengaduan.findMany({
|
||||
@@ -377,6 +381,11 @@ const PengaduanRoute = new Elysia({
|
||||
},
|
||||
}
|
||||
],
|
||||
AND: {
|
||||
Warga: {
|
||||
phone: phone
|
||||
}
|
||||
}
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -411,10 +420,57 @@ const PengaduanRoute = new Elysia({
|
||||
|
||||
return dataFix
|
||||
}, {
|
||||
query: t.Object({
|
||||
take: t.String({ optional: true }),
|
||||
page: t.String({ optional: true }),
|
||||
search: t.String({ optional: true }),
|
||||
phone: t.String({ minLength: 11, error: "phone harus diisi" }),
|
||||
}),
|
||||
detail: {
|
||||
summary: "List Pengaduan Warga",
|
||||
description: `tool untuk mendapatkan list pengaduan warga`,
|
||||
summary: "List Pengaduan Warga By Phone",
|
||||
description: `tool untuk mendapatkan list pengaduan warga by phone`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
.use(swagger())
|
||||
.post("/upload",
|
||||
async ({ body }) => {
|
||||
const { file } = body;
|
||||
|
||||
// Validasi file
|
||||
if (!file) {
|
||||
return { success: false, message: "File tidak ditemukan" };
|
||||
}
|
||||
|
||||
// Contoh: cek koneksi ke Seafile
|
||||
const coba = await testConnection(defaultConfigSF);
|
||||
console.log("Seafile Connection:", coba);
|
||||
|
||||
// Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer)
|
||||
// const buffer = await file.arrayBuffer();
|
||||
const result = await uploadFile(defaultConfigSF, file);
|
||||
|
||||
console.log("Upload result:", result);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Upload berhasil",
|
||||
filename: file.name,
|
||||
size: file.size,
|
||||
seafileResult: result
|
||||
};
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
file: t.File({ format: "binary" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "Upload File",
|
||||
description: "Tool untuk upload file ke Seafile",
|
||||
tags: ["mcp"],
|
||||
consumes: ["multipart/form-data"]
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default PengaduanRoute
|
||||
|
||||
Reference in New Issue
Block a user