Merge pull request #474 from bipproduction/amalia/01-jul-25

Amalia/01 jul 25
This commit is contained in:
Amalia
2025-07-01 10:35:15 +08:00
committed by GitHub
17 changed files with 869 additions and 43 deletions

3
.env.test Normal file
View File

@@ -0,0 +1,3 @@
GOOGLE_PRIVATE_KEY_ID=764e1207d5acf4db2eac539256c8f1bf397c7d8f
GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwCU9PBpAbXsOl\ntb1syvWrmH3FSDRyI4oOVWZJRqYX+j44UTNfzTjYySpNy7x1lr91uOC2GGHJeFvT\nLg5er6uvzFvzwg42A8Rz4+aqxlUhvhNXYRyfaaP7tbui5X9GEmhKYzvYd6T/6z1u\njo7LE1tBaiB8eB69tSJidGcr90yXOsbvKFgaPkpvlrseNR/t0PYDUaXHsxdKvCHI\ntK13KxhJCJrU9+/W1Wwr+45WGfK9m+jLVuOEZT9dd3FUgDn/0CFzykZLA0iHRLjx\neczahlrlvLVCtUIJjHbmsjG8vLZyl6/puh1l2OkEZyADb6m7OOxFVTo5ADZvj4nD\nVCCirdMVAgMBAAECggEAMF0mbnJBpltnSkA/vkOWsmHPcCOx0QgFloGM/CXOXTkR\n3hwlDrWN4DWIi14ltXLIwFmeVzkkqJsKM19scEQ4WbC+NJ7Ek79+Ok7LYXDjE8Wq\nf6+9EukNtgqMdikySfilsYZI+2SHrw4czyKYhZ+YS0USjs/btkgtHbqYW+JyJvv4\nlXAGp3129kbOHTc6+DBq6tn4XiRMKUdBNtcRHe9k+zAIuwbeAdsl4bock1ADnMIv\n/Q4FfOua+nJl8MUpPCZDvz14az+3j/rUVkR/wgDqQirFNRfFfpEPNM2oXVSjp0oK\nTC8NEy5mN4aj0DYS8U2x8barsAFDr5N4L9JxTtdlgwKBgQDkXK9iieIe1/yJFDw6\ntHbQu/bl+t82DESapss62++6ckh2mo+IScvVg/rCwXIag7IRQO40BHWwYTrOwTbj\nD1VUamn6UaqJHpIjDj/SK+As3DumuOTcb+kbJq9TpjLGeR2hj0aKcFXAjL5+B+yr\nBt7fVsB2uhouS9aD68HV8azsxwKBgQDFV2yRKgSf11vNRsxtJekpZ7ruF4h8OZPA\nHcq1kMDPRJcuVD9XwG7RAEgxcErKKS6NrrT/2Iaq5r+P3owgxZ6yB5pabGGvsgcg\nqrvsVEjzETsrrDbp5IevwE/MTwplakr6vJBnfAyjqMbDQSGSZPp+6S8M5JtZhJDL\n9Pqy6yxNQwKBgEE9ZXGuWKZdKC11VXukAOnDOVcco9ZKDPNtwVPQb52BdshDgcv6\n4Tvfl606HMIMa7vYI/VCbOj17hoRQv/9anBScnJsEF9aF3/iW0NM+591T6li2ydK\n5Xq3Q5GPQqRHB7sXNpzoWOdIjkdtNiTqMpP1sch5hG9DhUZs/RSFFdUTAoGBALyV\nyD2NXu/1WVh5cQBZe1FDPMMtIBQ+3bB5h+8tDuTEEomGnyXX0s7OKy97tS0uX7us\nGnJo1IDblHMDZPwofnh5hYsmCdBiHCeeoYm+HhyS+e3JXIz2BKjy6g8/9ZpnEpI8\nwu7yAA4iSxfq1Q9Win/fjUQP71mDsvAGA9IZpbOLAoGBAK57RjNemVh3oNB5ZaQs\n45WzfmPPjKoDQdMYLtohHz9HhPxYFLuvlDc/9OcWFCz3tZHtyDrUtXvv+vX+rG4Y\nemxXkqdg3lYo7nayw772myJb2w6QIfGyuSRx/C1/phmPhp+UkHk7B+KdvWhpPmCC\nBufws2LSn5VZzivO6LrwSCfR\n-----END PRIVATE KEY-----\n"
GOOGLE_CLIENT_EMAIL=firebase-adminsdk-fbsvc@mobile-darmasaba.iam.gserviceaccount.com

5
.gitignore vendored
View File

@@ -44,4 +44,7 @@ next-env.d.ts
certificates
test.png
test2.png
test2.png
xxx.ts
key.json

View File

@@ -1,38 +1,83 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
# Sistem Desa Mandiri
## Getting Started
Sistem Desa Mandiri adalah aplikasi web yang dirancang untuk membantu pengelolaan administrasi dan informasi di tingkat desa. Dibangun dengan Next.js, aplikasi ini menyediakan berbagai fitur untuk mendukung kegiatan desa, mulai dari pengumuman, diskusi, manajemen proyek, hingga administrasi kependudukan.
First, run the development server:
## Fitur Utama
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
* **Manajemen Pengguna:** Mengelola data anggota dan hak akses.
* **Pengumuman:** Menyebarkan informasi penting kepada seluruh warga desa.
* **Diskusi:** Forum untuk berdiskusi antar warga atau perangkat desa.
* **Manajemen Proyek & Tugas:** Melacak kemajuan proyek dan tugas yang sedang berjalan di desa.
* **Dokumentasi:** Tempat terpusat untuk menyimpan dan mengelola dokumen-dokumen penting.
* **Notifikasi Push:** Mengirimkan notifikasi real-time ke perangkat pengguna.
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## Teknologi yang Digunakan
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
* **Framework:** [Next.js](https://nextjs.org/)
* **UI Framework:** [Mantine](https://mantine.dev/)
* **Database ORM:** [Prisma](https://www.prisma.io/)
* **Styling:** [Tailwind CSS](https://tailwindcss.com/)
* **State Management:** [Hookstate](https://hookstate.js.org/)
* **Push Notifications:** [Web Push](https://www.npmjs.com/package/web-push)
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Memulai
## Learn More
### Persyaratan
To learn more about Next.js, take a look at the following resources:
* [Node.js](https://nodejs.org/) (versi 20.x atau lebih tinggi)
* [Bun](https://bun.sh/) (direkomendasikan) atau package manager lain seperti npm/yarn/pnpm.
* Database (misalnya PostgreSQL, MySQL, atau SQLite).
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
### Instalasi
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
1. **Clone repositori ini:**
```bash
git clone https://github.com/username/sistem-desa-mandiri.git
cd sistem-desa-mandiri
```
## Deploy on Vercel
2. **Install dependensi:**
```bash
bun install
```
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
3. **Setup Variabel Lingkungan:**
Salin file `.env.test` menjadi `.env` dan sesuaikan nilainya, terutama untuk koneksi database.
```bash
cp .env.test .env
```
Buka file `.env` dan isi variabel yang diperlukan, seperti `DATABASE_URL`.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
4. **Migrasi Database:**
Jalankan migrasi Prisma untuk membuat skema database.
```bash
npx prisma migrate dev
```
v011
5. **Seed Database (Opsional):**
Jika Anda ingin mengisi database dengan data awal, jalankan perintah seed.
```bash
npx prisma db seed
```
6. **Jalankan Server Development:**
```bash
bun run dev
```
Aplikasi akan berjalan di [https://localhost:3000](https://localhost:3000).
## Skrip yang Tersedia
* `dev`: Menjalankan server development dengan HTTPS.
* `build`: Membuat build produksi dari aplikasi.
* `start`: Menjalankan server produksi.
* `lint`: Menjalankan linter untuk memeriksa kualitas kode.
* `prisma:seed`: Menjalankan skrip seed database.
## Kontribusi
Kontribusi dalam bentuk apapun sangat kami hargai. Jika Anda menemukan bug atau memiliki ide untuk fitur baru, silakan buat *issue* baru. Jika Anda ingin berkontribusi dalam kode, silakan buat *pull request*.
## Lisensi
Proyek ini dilisensikan di bawah [Lisensi ISC](LICENSE).

BIN
bun.lockb

Binary file not shown.

View File

@@ -41,11 +41,13 @@
"@tiptap/starter-kit": "^2.4.0",
"@types/bun": "^1.2.17",
"@types/busboy": "^1.5.4",
"@types/crypto-js": "^4.2.2",
"@types/lodash": "^4.17.6",
"@types/minimist": "^1.2.5",
"@types/multer": "^1.4.12",
"@types/web-push": "^3.6.3",
"busboy": "^1.6.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.11",
"echarts": "^5.5.1",
"echarts-for-react": "^3.0.2",

214
sendMultiple.ts Normal file
View File

@@ -0,0 +1,214 @@
import "dotenv/config";
import { initializeApp, cert, App, ServiceAccount } from "firebase-admin/app";
import { getMessaging, Message } from "firebase-admin/messaging";
import path from "path";
// --- KONFIGURASI ---
const CONFIG = {
/**
* Konten notifikasi default yang akan dikirim.
*/
notificationPayload: {
title: "Sistem Desa Mandiri",
body: "Ada informasi baru untuk Anda, silakan periksa aplikasi.",
},
/**
* Pengaturan untuk mekanisme coba lagi (retry) jika terjadi kegagalan jaringan.
*/
retry: {
maxRetries: 3, // Jumlah maksimal percobaan ulang
delay: 2000, // Waktu tunda antar percobaan dalam milidetik (ms)
},
};
// --- AKHIR KONFIGURASI ---
/**
* Membangun objek service account dari variabel lingkungan.
*/
function getServiceAccount(): ServiceAccount {
const privateKey = process.env.GOOGLE_PRIVATE_KEY?.replace(/\n/g, '\n');
if (!process.env.GOOGLE_PROJECT_ID || !process.env.GOOGLE_CLIENT_EMAIL || !privateKey) {
console.error("KRITIS: Variabel lingkungan Firebase (PROJECT_ID, CLIENT_EMAIL, PRIVATE_KEY) tidak lengkap.");
process.exit(1);
}
return {
projectId: process.env.GOOGLE_PROJECT_ID,
clientEmail: process.env.GOOGLE_CLIENT_EMAIL,
privateKey,
};
}
/**
* Inisialisasi Firebase Admin SDK.
* Hanya akan menginisialisasi satu kali.
*/
let firebaseApp: App | null = null;
function initializeFirebase(): App {
if (firebaseApp) {
return firebaseApp;
}
try {
const serviceAccount = getServiceAccount();
firebaseApp = initializeApp({
credential: cert(serviceAccount),
});
console.log("Firebase Admin SDK berhasil diinisialisasi.");
return firebaseApp;
} catch (error: any) {
console.error("KRITIS: Gagal inisialisasi Firebase. Pastikan variabel lingkungan sudah benar.");
console.error(`Detail Error: ${error.message}`);
process.exit(1); // Keluar dari proses jika Firebase gagal diinisialisasi
}
}
/**
* Mengambil daftar token perangkat dari database.
* TODO: Ganti fungsi ini dengan logika untuk mengambil token dari database Anda.
* @returns {Promise<string[]>} Daftar token FCM.
*/
async function getDeviceTokens(): Promise<string[]> {
console.log("Mengambil token perangkat dari sumber data...");
// CONTOH: Ini adalah data dummy. Seharusnya Anda mengambilnya dari database.
// Misalnya: const users = await prisma.user.findMany({ where: { fcmToken: { not: null } } });
// return users.map(user => user.fcmToken);
const exampleTokens: string[] = [
"c89yuexsS_uc1tOErVPu5a:APA91bEb6tEKXAfReZjFVJ2mMyOzoW_RXryLSnSJTpbIVV3G0L_DCNkLuRvJ02Ip-Erz88QCQBAt-C2SN8eCRxu3-v1sBzXzKPtDv-huXpkjXsyrkifqvUo", // Valid
"cRz96GHKTRaQaRJ35e8Hxa:APA91bEUSxE0VPbqKSzseQ_zGhbYsDofMexKykRw7o_3z2aPM9YFmZbeA2enrmb3qjdZ2g4-QQtiNHAyaZqAT1ITOrwo9jVJlShTeABmEFYP5GLEUZ3dlLc", // Valid
"token_tidak_valid_ini_pasti_gagal_12345", // Contoh token tidak valid
];
console.log(`Berhasil mendapatkan ${exampleTokens.length} token.`);
return exampleTokens;
}
/**
* Membuat array pesan yang akan dikirim ke setiap token.
* @param {string[]} tokens - Daftar token FCM.
* @returns {Message[]} Array objek pesan untuk `sendEach`.
*/
function constructMessages(tokens: string[]): Message[] {
return tokens.map((token) => ({
token,
notification: {
title: CONFIG.notificationPayload.title,
body: CONFIG.notificationPayload.body,
},
data: {
// Anda bisa menambahkan data tambahan di sini
// Contoh: click_action: "FLUTTER_NOTIFICATION_CLICK"
timestamp: String(Date.now()),
},
android: {
priority: "high",
notification: {
sound: "default",
channelId: "default_channel", // Pastikan channel ini ada di aplikasi Android Anda
},
},
apns: {
payload: {
aps: {
sound: "default",
},
},
},
}));
}
/**
* Menangani respons dari `sendEach` untuk mencatat keberhasilan dan kegagalan.
* @param response - Objek respons dari `sendEach`.
* @param originalTokens - Daftar token asli yang dikirimi pesan.
*/
function handleSendResponse(response: any, originalTokens: string[]) {
console.log("Total notifikasi berhasil dikirim:", response.successCount);
console.log("Total notifikasi gagal:", response.failureCount);
const tokensToRemove: string[] = [];
response.responses.forEach((resp: any, index: number) => {
const token = originalTokens[index];
if (resp.success) {
// console.log(`Pesan ke token ...${token.slice(-6)} berhasil:`, resp.messageId);
} else {
console.error(`Pesan ke token ...${token.slice(-6)} GAGAL:`, resp.error.code);
// Jika token tidak lagi terdaftar, tandai untuk dihapus
if (
resp.error.code === "messaging/registration-token-not-registered" ||
resp.error.code === "messaging/invalid-registration-token"
) {
tokensToRemove.push(token);
}
}
});
if (tokensToRemove.length > 0) {
console.warn("Token berikut tidak valid dan harus dihapus dari database:");
tokensToRemove.forEach(token => console.log(`- ${token}`));
// TODO: Implementasikan logika untuk menghapus token-token di atas dari database Anda.
// Misalnya: await prisma.user.updateMany({ where: { fcmToken: { in: tokensToRemove } }, data: { fcmToken: null } });
}
}
/**
* Fungsi utama untuk mengirim notifikasi ke banyak perangkat dengan mekanisme retry.
* @param {Message[]} messages - Array pesan yang akan dikirim.
* @param {string[]} tokens - Daftar token asli untuk logging.
*/
async function sendNotifications(messages: Message[], tokens: string[]) {
let lastError: any;
for (let attempt = 1; attempt <= CONFIG.retry.maxRetries; attempt++) {
try {
const response = await getMessaging().sendEach(messages);
handleSendResponse(response, tokens);
return; // Berhasil, keluar dari fungsi
} catch (error: any) {
lastError = error;
console.error(`Percobaan pengiriman ke-${attempt} gagal:`, error.message);
// Hanya coba lagi jika error berhubungan dengan jaringan
const isNetworkError = error.code === "app/network-error" || error.code?.includes("network");
if (isNetworkError && attempt < CONFIG.retry.maxRetries) {
console.log(`Menunggu ${CONFIG.retry.delay}ms sebelum mencoba lagi...`);
await new Promise((resolve) => setTimeout(resolve, CONFIG.retry.delay));
} else {
// Jika bukan error jaringan atau sudah mencapai batas retry, lempar error
throw new Error(`Gagal mengirim notifikasi setelah ${attempt} percobaan: ${error.message}`);
}
}
}
throw lastError;
}
/**
* Fungsi orchestrator untuk menjalankan seluruh proses.
*/
export async function sendMultiple() {
try {
initializeFirebase();
const tokens = await getDeviceTokens();
if (tokens.length === 0) {
console.log("Tidak ada token perangkat yang ditemukan. Proses dihentikan.");
return;
}
const messages = constructMessages(tokens);
console.log(`
Mempersiapkan pengiriman ${messages.length} notifikasi...`);
await sendNotifications(messages, tokens);
console.log("Proses pengiriman notifikasi selesai.");
} catch (error: any) {
console.error("Terjadi error fatal dalam proses pengiriman:", error.message);
process.exit(1);
}
}
sendMultiple();

View File

@@ -1,13 +0,0 @@
{
"type": "service_account",
"project_id": "mobile-darmasaba",
"private_key_id": "764e1207d5acf4db2eac539256c8f1bf397c7d8f",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwCU9PBpAbXsOl\ntb1syvWrmH3FSDRyI4oOVWZJRqYX+j44UTNfzTjYySpNy7x1lr91uOC2GGHJeFvT\nLg5er6uvzFvzwg42A8Rz4+aqxlUhvhNXYRyfaaP7tbui5X9GEmhKYzvYd6T/6z1u\njo7LE1tBaiB8eB69tSJidGcr90yXOsbvKFgaPkpvlrseNR/t0PYDUaXHsxdKvCHI\ntK13KxhJCJrU9+/W1Wwr+45WGfK9m+jLVuOEZT9dd3FUgDn/0CFzykZLA0iHRLjx\neczahlrlvLVCtUIJjHbmsjG8vLZyl6/puh1l2OkEZyADb6m7OOxFVTo5ADZvj4nD\nVCCirdMVAgMBAAECggEAMF0mbnJBpltnSkA/vkOWsmHPcCOx0QgFloGM/CXOXTkR\n3hwlDrWN4DWIi14ltXLIwFmeVzkkqJsKM19scEQ4WbC+NJ7Ek79+Ok7LYXDjE8Wq\nf6+9EukNtgqMdikySfilsYZI+2SHrw4czyKYhZ+YS0USjs/btkgtHbqYW+JyJvv4\nlXAGp3129kbOHTc6+DBq6tn4XiRMKUdBNtcRHe9k+zAIuwbeAdsl4bock1ADnMIv\n/Q4FfOua+nJl8MUpPCZDvz14az+3j/rUVkR/wgDqQirFNRfFfpEPNM2oXVSjp0oK\nTC8NEy5mN4aj0DYS8U2x8barsAFDr5N4L9JxTtdlgwKBgQDkXK9iieIe1/yJFDw6\ntHbQu/bl+t82DESapss62++6ckh2mo+IScvVg/rCwXIag7IRQO40BHWwYTrOwTbj\nD1VUamn6UaqJHpIjDj/SK+As3DumuOTcb+kbJq9TpjLGeR2hj0aKcFXAjL5+B+yr\nBt7fVsB2uhouS9aD68HV8azsxwKBgQDFV2yRKgSf11vNRsxtJekpZ7ruF4h8OZPA\nHcq1kMDPRJcuVD9XwG7RAEgxcErKKS6NrrT/2Iaq5r+P3owgxZ6yB5pabGGvsgcg\nqrvsVEjzETsrrDbp5IevwE/MTwplakr6vJBnfAyjqMbDQSGSZPp+6S8M5JtZhJDL\n9Pqy6yxNQwKBgEE9ZXGuWKZdKC11VXukAOnDOVcco9ZKDPNtwVPQb52BdshDgcv6\n4Tvfl606HMIMa7vYI/VCbOj17hoRQv/9anBScnJsEF9aF3/iW0NM+591T6li2ydK\n5Xq3Q5GPQqRHB7sXNpzoWOdIjkdtNiTqMpP1sch5hG9DhUZs/RSFFdUTAoGBALyV\nyD2NXu/1WVh5cQBZe1FDPMMtIBQ+3bB5h+8tDuTEEomGnyXX0s7OKy97tS0uX7us\nGnJo1IDblHMDZPwofnh5hYsmCdBiHCeeoYm+HhyS+e3JXIz2BKjy6g8/9ZpnEpI8\nwu7yAA4iSxfq1Q9Win/fjUQP71mDsvAGA9IZpbOLAoGBAK57RjNemVh3oNB5ZaQs\n45WzfmPPjKoDQdMYLtohHz9HhPxYFLuvlDc/9OcWFCz3tZHtyDrUtXvv+vX+rG4Y\nemxXkqdg3lYo7nayw772myJb2w6QIfGyuSRx/C1/phmPhp+UkHk7B+KdvWhpPmCC\nBufws2LSn5VZzivO6LrwSCfR\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-fbsvc@mobile-darmasaba.iam.gserviceaccount.com",
"client_id": "105653213329235865762",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40mobile-darmasaba.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View File

@@ -5,6 +5,7 @@ import _ from "lodash";
import moment from "moment";
import "moment/locale/id";
import { NextResponse } from "next/server";
import { sendFCMNotification } from "../../../../../xsend";
export const dynamic = 'force-dynamic'
@@ -99,6 +100,8 @@ export async function GET(request: Request) {
createdAt: moment(v.createdAt).format("ll")
}))
await sendFCMNotification('c89yuexsS_uc1tOErVPu5a:APA91bEb6tEKXAfReZjFVJ2mMyOzoW_RXryLSnSJTpbIVV3G0L_DCNkLuRvJ02Ip-Erz88QCQBAt-C2SN8eCRxu3-v1sBzXzKPtDv-huXpkjXsyrkifqvUo')
return NextResponse.json({ success: true, message: "Berhasil mendapatkan pengumuman", data: allData, }, { status: 200 });
} catch (error) {
console.error(error);

View File

@@ -1,19 +1,19 @@
import { sendFCM } from "@/lib/firebase/fcm";
import elysia from "elysia";
import { sendFCMNotification } from "../../../../../../xsend";
const ApiV2 = new elysia({
prefix: "/api/mobile/fcm"
})
.get("/", async () => {
const token = [
'c89yuexsS_uc1tOErVPu5a:APA91bEb6tEKXAfReZjFVJ2mMyOzoW_RXryLSnSJTpbIVV3G0L_DCNkLuRvJ02Ip-Erz88QCQBAt-C2SN8eCRxu3-v1sBzXzKPtDv-huXpkjXsyrkifqvUo',
'cRz96GHKTRaQaRJ35e8Hxa:APA91bEUSxE0VPbqKSzseQ_zGhbYsDofMexKykRw7o_3z2aPM9YFmZbeA2enrmb3qjdZ2g4-QQtiNHAyaZqAT1ITOrwo9jVJlShTeABmEFYP5GLEUZ3dlLc'
]
sendFCM(token, "Test dari Production STG;", "Ini hanya percobaan notifikasi dari script.");
await sendFCMNotification('c89yuexsS_uc1tOErVPu5a:APA91bEb6tEKXAfReZjFVJ2mMyOzoW_RXryLSnSJTpbIVV3G0L_DCNkLuRvJ02Ip-Erz88QCQBAt-C2SN8eCRxu3-v1sBzXzKPtDv-huXpkjXsyrkifqvUo')
return {
data: "success elysia api"
data: "success elysia"
};
})

View File

@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
export async function GET(request: Request) {
try {
return NextResponse.json({ success: true, version: "1.5.1", tahap: "beta", update: "-percobaan firebase admin fcm" }, { status: 200 });
return NextResponse.json({ success: true, version: "1.5.2", tahap: "beta", update: "-percobaan firebase admin fcm" }, { status: 200 });
} catch (error) {
console.error(error);
return NextResponse.json({ success: false, version: "Gagal mendapatkan version, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });

31
x.yml Normal file
View File

@@ -0,0 +1,31 @@
name: "darmasaba"
namespace: "darmasaba-staging"
branch: "staging"
repo: "sistem-desa-mandiri"
env: |
DATABASE_URL="postgresql://bip:Production_123@localhost:5433/darmasaba-stg?schema=public"
URL="http://localhost:3000"
WS_APIKEY="eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp7ImlkIjoiY20wdnQ4bzFrMDAwMDEyenE1eXl1emd5YiIsIm5hbWUiOiJhbWFsaWEiLCJlbWFpbCI6ImFtYWxpYUBiaXAuY29tIiwiQXBpS2V5IjpbeyJpZCI6ImNtMHZ0OG8xcjAwMDIxMnpxZDVzejd3eTgiLCJuYW1lIjoiZGVmYXVsdCJ9XX0sImlhdCI6MTcyNTkzNTE5MiwiZXhwIjo0ODgxNjk1MTkyfQ.7U-HUnNBDmeq_6XXohiFZjFnh2rSzUPMHDdrUKOd7G4"
NEXT_PUBLIC_VAPID_PUBLIC_KEY=BBC6ml3Ro9eBdhSq_DPx0zQ0hBH4NvOeJbFXdQy3cZ-UyJ2m6V1RyO1XD9B08kshTdVNoGZeqBDKBPzpWgwRBNY
VAPID_PRIVATE_KEY=p9GfSmCRJe1_dzwKqe29HF81mTE2JwlrW4cXINnkI7c
WIBU_REALTIME_KEY="padahariminggukuturutayahkekotanaikdelmanistimewakududukdimuka"
FCM_KEY=BAWSIlqadurVCx6wm50KiMVwd01IosHo3g7731yhPmweVqUDu1zx0l2ytKL6DSOmbEDVxuBryNJKYLEXCRiLCos
GOOGLE_PROJECT_ID=mobile-darmasaba
GOOGLE_PRIVATE_KEY_ID=764e1207d5acf4db2eac539256c8f1bf397c7d8f
GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwCU9PBpAbXsOl\ntb1syvWrmH3FSDRyI4oOVWZJRqYX+j44UTNfzTjYySpNy7x1lr91uOC2GGHJeFvT\nLg5er6uvzFvzwg42A8Rz4+aqxlUhvhNXYRyfaaP7tbui5X9GEmhKYzvYd6T/6z1u\njo7LE1tBaiB8eB69tSJidGcr90yXOsbvKFgaPkpvlrseNR/t0PYDUaXHsxdKvCHI\ntK13KxhJCJrU9+/W1Wwr+45WGfK9m+jLVuOEZT9dd3FUgDn/0CFzykZLA0iHRLjx\neczahlrlvLVCtUIJjHbmsjG8vLZyl6/puh1l2OkEZyADb6m7OOxFVTo5ADZvj4nD\nVCCirdMVAgMBAAECggEAMF0mbnJBpltnSkA/vkOWsmHPcCOx0QgFloGM/CXOXTkR\n3hwlDrWN4DWIi14ltXLIwFmeVzkkqJsKM19scEQ4WbC+NJ7Ek79+Ok7LYXDjE8Wq\nf6+9EukNtgqMdikySfilsYZI+2SHrw4czyKYhZ+YS0USjs/btkgtHbqYW+JyJvv4\nlXAGp3129kbOHTc6+DBq6tn4XiRMKUdBNtcRHe9k+zAIuwbeAdsl4bock1ADnMIv\n/Q4FfOua+nJl8MUpPCZDvz14az+3j/rUVkR/wgDqQirFNRfFfpEPNM2oXVSjp0oK\nTC8NEy5mN4aj0DYS8U2x8barsAFDr5N4L9JxTtdlgwKBgQDkXK9iieIe1/yJFDw6\ntHbQu/bl+t82DESapss62++6ckh2mo+IScvVg/rCwXIag7IRQO40BHWwYTrOwTbj\nD1VUamn6UaqJHpIjDj/SK+As3DumuOTcb+kbJq9TpjLGeR2hj0aKcFXAjL5+B+yr\nBt7fVsB2uhouS9aD68HV8azsxwKBgQDFV2yRKgSf11vNRsxtJekpZ7ruF4h8OZPA\nHcq1kMDPRJcuVD9XwG7RAEgxcErKKS6NrrT/2Iaq5r+P3owgxZ6yB5pabGGvsgcg\nqrvsVEjzETsrrDbp5IevwE/MTwplakr6vJBnfAyjqMbDQSGSZPp+6S8M5JtZhJDL\n9Pqy6yxNQwKBgEE9ZXGuWKZdKC11VXukAOnDOVcco9ZKDPNtwVPQb52BdshDgcv6\n4Tvfl606HMIMa7vYI/VCbOj17hoRQv/9anBScnJsEF9aF3/iW0NM+591T6li2ydK\n5Xq3Q5GPQqRHB7sXNpzoWOdIjkdtNiTqMpP1sch5hG9DhUZs/RSFFdUTAoGBALyV\nyD2NXu/1WVh5cQBZe1FDPMMtIBQ+3bB5h+8tDuTEEomGnyXX0s7OKy97tS0uX7us\nGnJo1IDblHMDZPwofnh5hYsmCdBiHCeeoYm+HhyS+e3JXIz2BKjy6g8/9ZpnEpI8\nwu7yAA4iSxfq1Q9Win/fjUQP71mDsvAGA9IZpbOLAoGBAK57RjNemVh3oNB5ZaQs\n45WzfmPPjKoDQdMYLtohHz9HhPxYFLuvlDc/9OcWFCz3tZHtyDrUtXvv+vX+rG4Y\nemxXkqdg3lYo7nayw772myJb2w6QIfGyuSRx/C1/phmPhp+UkHk7B+KdvWhpPmCC\nBufws2LSn5VZzivO6LrwSCfR\n-----END PRIVATE KEY-----\n"
GOOGLE_CLIENT_EMAIL=firebase-adminsdk-fbsvc@mobile-darmasaba.iam.gserviceaccount.com
GOOGLE_CLIENT_ID=105653213329235865762
GOOGLE_AUTH_URI=https://accounts.google.com/o/oauth2/auth
GOOGLE_TOKEN_URI=https://oauth2.googleapis.com/token
GOOGLE_AUTH_PROVIDER_CERT_URL=https://www.googleapis.com/oauth2/v1/certs
GOOGLE_CLIENT_CERT_URL=https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40mobile-darmasaba.iam.gserviceaccount.com
options:
dbPush: true
# dbSeed: true
build: true
# newConfig: true
# count: 1
# ports: null

1
xenc Normal file
View File

@@ -0,0 +1 @@
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

1
xencrypt.txt Normal file
View File

@@ -0,0 +1 @@
U2FsdGVkX1/zPGErdHO359FChVpjXGydZmqy9iPspWsrqDV/4ztGVen0J5kC+tp1awSJnmPAhqdUV11cPVgRgwopTMywhPmFJzZ03N3AohDPHhyQVHSaZHTiOROGDHzSwd3d7hT3ZyJjVB/d607xM5XbiUy56SMtCeWP7xLGqsrAZSlXouYn64Q/JfXWDJimSOVqoQWOBXc6hAcnNFHh5mCXLRMRfnBiMPe5uoS34Dshixke5LmbI7vi76USqElhUO1V/5vtlttealmBdPNEm9/ig4P8Sa6rvQR9faHXB0oRh9Jrt6r2PwGmXmHtX/VL/IqNDXsbN1WJw61r+c4+6ASeR7Ue8beg+H2wM+ZdnMPiFnpZhuMu3/Xiq4eVBQa4JCbjqNLzXQRGrOmoiJ2dhIBTuo1no9xDlDqxnnyhPYzPx72e7a1ROYqReSFJKmIrbim1WqJejg2zSziyHMBf0u+68h2A5Xm3Jx+qmXUNjXQupWlyqr752O6P8l4AOLq7xPNDuEaO5ECnf2NvhWHc5Hq/fPz7t6zGfxkhEcFDMpNA8i2hUzpQPD5olfFOlw9zhBOt3Kw16FrsPYofywQKksqtvRYQMwRIqnuS1fZ2dNqdwUuZ4LYRkbJkRkMWYQy5ScLDEcLZ8tyCwMf2p7Iftm3Y1iyMWDN+qIybDtYDbzDnn8gje5dI/B6NfCTdOqyUEnR1YTG1OQssXo1Bc4jLo2wntm6MWOY74U/e/XBK8rkFKzKj8zByIC4XW34QrJg8eKqt3lc3ICfagiI9THVcTS1eiE6RRDezTVmMlWn3jf2/XhpVni4ta9iP2UZljCeXOF4qAF5z3iay4pENJFg6Uqw4mH9kWoyWJllkHQXIPx+MpAuAj8psZwhfJZaE26rnIPodi91yXXVhfBHSWcBpAorCQ/xqWfzs22D6pEehPd2wrMwsmCnTVGN7K76Tbj36ls5Khz8iIRJPbdgTyH7lo00qsoMU1nA0cRA11iV2KuFBJCFXUch3mi1JEPpnJMLxiAxZkeTHYQ9MVHrlWCJMFt8Oi6fqRCUHrv3vIxTkqbV62Ly9KpPcAkd4/nNjLYXIOCpJDma9KBw8ii+qW74133H9piZCmG9Jy1A0fFLsGJxQtv0+qRD/DRPx454dQ4nlPtU3pNlhvUGohykw/RuIDX1ncUtAIu85td2zxQmBIeyroQerQcAaVTmRMjDeGJHSweYLt96jA4o9/et10+icV/DPiYPiTnuelU+lDACkavX0gQIOvqMphO/+a0deJ0yhDv98/PPBswiAc+j9ICLB6scy0YZi3L1LkRND2hccQTVKZwqfDXUt8F2zLP93XO0mvbf/DncTtAAqG2Tcu9MbGvgAItsx7+dJVZMXqH10nURqYLbXDdzVpHj5D24GBnzK272QJRdRVb6PrXhtexcXO3pDml3RUZ212u+b+JW0Q84g+9oszQ5BoITU7iMdkaSKHI4OCxIjzGcTatv6fTYGXqCQxSJiwnBJMcsVPJpRcoGKN0xB4P8xqY/Oucnj43UAQF8ZdiOlOPjv3z9mYfU6xt12QxCSHE9AkU0mBbRgmsQTZqeMLzoWlw3JDS5rE7PMrEojD1q+refpRLDKy+3pu/98MVz9l67FZvpzwbHlcixyFJyJUg043yl/ZGCQh6ydt6OcWavcrmEVc0WzqYNPaAYdMeq/eObiLnCO7lfEtaWpe9inTWXtMgVAA3xZxOGUCK+6n15vwrofqMPJ9JGVADIV5Xh8e1jvwpVVxda3V1Ant/Ku7iX4BN2lzsjiVh+vzaIAkP9dZbGeqOeko+Zxj3DbRFZ69LeWPlT0iy08dK2XeGWDnQmyvX5Elb2kCjl+KLvDsPhlSF72nETyCK9GN/F7yIkBeFtWTEDI7zQTXi2MvEc4IiyFwkMfr5lsrsBXTRbgfH1aV9VeWsKhdyy/R2F7Acooe2n9n/1AdJ3ArS2Y+QzEEsXFVh93ToznTaeW+hDeaqW/kNy0S9kz14BL4Ns72ThPHRfwsw6jgdmSYT1PxvzrYJnt5opTipYo9mrvcO9khvyIKX2RMmWuep/sAELyrCjD0rb0WJ3G8n51Q98nhRCM2N/zu9RBeI4eov3A6sy8uBeCcx8Jlli8+1KQTQi82qpPZGlZNOdoDcwsP3VPWJNR25t9i5JS8zzvUnjusAcPG57gMEMIHzxCFAI9cn9Xwba+vctXIKPtH54kwNK7CGk6VuG5FK23DqdM5ZL2fRBGlhBcuS4kJKqNJHc1Nox42UUG8u63xa4vnWbcZ6BeJk25cYcg+H0ueZ2T7vFh

58
xsend.ts Normal file
View File

@@ -0,0 +1,58 @@
// Impor Firebase Admin SDK
import CryptoJs from 'crypto-js';
import { cert, getApps, initializeApp } from "firebase-admin/app";
import { getMessaging } from "firebase-admin/messaging";
const enc = "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"
const decrypt = CryptoJs.AES.decrypt(enc, "amal").toString(CryptoJs.enc.Utf8)
// Fungsi untuk mengirim notifikasi FCM
export async function sendFCMNotification(token: string) {
// const serviceAccount = await fs.readFile(path.join(process.cwd(), "key.json"));
if(getApps().length === 0){
initializeApp({
credential: cert(JSON.parse(decrypt)),
});
}
try {
// Konfigurasi pesan
const message = {
notification: {
title: "Notifikasi Encrypted api",
body: "Ini adalah isi notifikasi key diencrypt dan api",
},
token,
data: {
key1: "value1",
key2: "value2",
},
// Opsional: konfigurasi Android
android: {
priority: "high",
notification: {
sound: "default",
channelId: "default_channel",
},
},
// Opsional: konfigurasi APNS (iOS)
apns: {
payload: {
aps: {
sound: "default",
},
},
},
};
// Kirim pesan
const response = await getMessaging().send(message as any);
console.log("Notifikasi berhasil dikirim:", response);
return response;
} catch (error) {
console.error("Error mengirim notifikasi:", error);
throw error;
}
}
// sendFCMNotification('c89yuexsS_uc1tOErVPu5a:APA91bEb6tEKXAfReZjFVJ2mMyOzoW_RXryLSnSJTpbIVV3G0L_DCNkLuRvJ02Ip-Erz88QCQBAt-C2SN8eCRxu3-v1sBzXzKPtDv-huXpkjXsyrkifqvUo')

116
xsendEach.ts Normal file
View File

@@ -0,0 +1,116 @@
import { initializeApp, cert } from "firebase-admin/app";
import { getMessaging } from "firebase-admin/messaging";
// Inisialisasi Firebase Admin
const serviceAccount = require("./key.json");
try {
initializeApp({
credential: cert(serviceAccount),
});
console.log("Firebase berhasil diinisialisasi");
} catch (error) {
console.error("Gagal inisialisasi Firebase:", error as Error);
process.exit(1);
}
// Fungsi untuk mengirim notifikasi ke banyak perangkat dengan retry
async function sendFCMEach(maxRetries = 3, retryDelay = 1000) {
try {
// Ganti dengan token perangkat yang valid
const tokens = [
'c89yuexsS_uc1tOErVPu5a:APA91bEb6tEKXAfReZjFVJ2mMyOzoW_RXryLSnSJTpbIVV3G0L_DCNkLuRvJ02Ip-Erz88QCQBAt-C2SN8eCRxu3-v1sBzXzKPtDv-huXpkjXsyrkifqvUo',
'cRz96GHKTRaQaRJ35e8Hxa:APA91bEUSxE0VPbqKSzseQ_zGhbYsDofMexKykRw7o_3z2aPM9YFmZbeA2enrmb3qjdZ2g4-QQtiNHAyaZqAT1ITOrwo9jVJlShTeABmEFYP5GLEUZ3dlLc'
];
// Validasi token sebelum mengirim
const validTokens: string[] = [];
for (const token of tokens) {
try {
await getMessaging().send({
token,
notification: { title: "Test", body: "Validasi token" },
});
validTokens.push(token);
console.log(`Token ${token.slice(-4)} valid`);
} catch (error) {
console.error(`Token ${token.slice(-4)} tidak valid:`, error as Error);
}
}
if (validTokens.length === 0) {
throw new Error("Tidak ada token valid untuk mengirim notifikasi");
}
// Buat pesan untuk setiap token
const messages = validTokens.map((token) => ({
notification: {
title: "Judul Notifikasi",
body: `Pesan untuk perangkat ${token.slice(-4)}`,
},
token,
data: {
key1: "value1",
key2: "value2",
},
android: {
priority: "high",
notification: {
sound: "default",
channelId: "default_channel",
},
},
apns: {
payload: {
aps: {
sound: "default",
},
},
},
}));
console.log("Mengirim notifikasi ke token:", validTokens.map((t) => t.slice(-4)));
// Kirim pesan dengan retry logic
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await getMessaging().sendEach(messages as any);
console.log("Notifikasi berhasil dikirim:", response.successCount);
console.log("Notifikasi gagal:", response.failureCount);
response.responses.forEach((resp, index) => {
if (resp.success) {
console.log(`Pesan ke ${validTokens[index].slice(-4)} berhasil:`, resp.messageId);
} else {
console.error(`Pesan ke ${validTokens[index].slice(-4)} gagal:`, resp.error?.code, resp.error?.message);
}
});
return response;
} catch (error) {
lastError = error;
console.error(`Percobaan ${attempt} gagal:`, error as Error);
if (attempt === maxRetries) {
throw new Error(`Gagal setelah ${maxRetries} percobaan: ${error as Error}`);
}
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
}
throw lastError;
} catch (error) {
console.error("Error mengirim notifikasi:", error as Error);
if ((error as any).code === "messaging/registration-token-not-registered") {
console.log("Token tidak valid. Hapus token dari database.");
}
throw error;
}
}
// Jalankan fungsi
sendFCMEach()
.then(() => console.log("Proses selesai"))
.catch((error) => {
console.error("Gagal menjalankan sendFCMEach:", error as Error);
process.exit(1);
});

145
xsendEachV2.ts Normal file
View File

@@ -0,0 +1,145 @@
import { initializeApp, cert } from "firebase-admin/app";
import { getMessaging } from "firebase-admin/messaging";
// Inisialisasi Firebase Admin
const serviceAccount = require("./key.json");
try {
initializeApp({
credential: cert(serviceAccount),
});
console.log("Firebase berhasil diinisialisasi");
} catch (error) {
console.error("Gagal inisialisasi Firebase:", (error as Error).message);
process.exit(1);
}
// Fungsi untuk menguji koneksi ke FCM server
async function testFCMConnection(): Promise<boolean> {
try {
const response = await fetch("https://fcm.googleapis.com", { method: "GET" });
console.log("Koneksi ke FCM server:", response.status, response.statusText);
return response.ok;
} catch (error) {
console.error("Gagal menghubungi FCM server:", (error as Error).message);
return false;
}
}
// Fungsi untuk mengirim notifikasi ke banyak perangkat dengan retry
async function sendFCMEach(maxRetries: number = 5, retryDelay: number = 2000): Promise<any> {
try {
// Token perangkat
const tokens: string[] = [
"c89yuexsS_uc1tOErVPu5a:APA91bEb6tEKXAfReZjFVJ2mMyOzoW_RXryLSnSJTpbIVV3G0L_DCNkLuRvJ02Ip-Erz88QCQBAt-C2SN8eCRxu3-v1sBzXzKPtDv-huXpkjXsyrkifqvUo",
"cRz96GHKTRaQaRJ35e8Hxa:APA91bEUSxE0VPbqKSzseQ_zGhbYsDofMexKykRw7o_3z2aPM9YFmZbeA2enrmb3qjdZ2g4-QQtiNHAyaZqAT1ITOrwo9jVJlShTeABmEFYP5GLEUZ3dlLc",
];
// Validasi token sebelum mengirim
const validTokens: string[] = [];
for (const token of tokens) {
try {
await getMessaging().send({
token,
notification: { title: "Test", body: "Validasi token" },
});
validTokens.push(token);
console.log(`Token ${token.slice(-4)} valid`);
} catch (error) {
console.error(`Token ${token.slice(-4)} tidak valid:`, (error as Error).message);
}
}
if (validTokens.length === 0) {
throw new Error("Tidak ada token valid untuk mengirim notifikasi");
}
// Buat pesan untuk setiap token
const messages = validTokens.map((token) => ({
notification: {
title: "Judul Notifikasi",
body: `Pesan untuk perangkat ${token.slice(-4)}`,
},
token,
data: {
key1: "value1",
key2: "value2",
},
android: {
priority: "high",
notification: {
sound: "default",
channelId: "default_channel",
},
},
apns: {
payload: {
aps: {
sound: "default",
},
},
},
}));
console.log("Mengirim notifikasi ke token:", validTokens.map((t) => t.slice(-4)));
// Cek koneksi ke FCM server
const isConnected = await testFCMConnection();
if (!isConnected) {
throw new Error("Gagal terhubung ke FCM server. Periksa jaringan atau firewall.");
}
// Kirim pesan dengan retry logic
let lastError: any;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await getMessaging().sendEach(messages as any);
console.log("Notifikasi berhasil dikirim:", response.successCount);
console.log("Notifikasi gagal:", response.failureCount);
response.responses.forEach((resp: any, index: number) => {
if (resp.success) {
console.log(`Pesan ke ${validTokens[index].slice(-4)} berhasil:`, resp.messageId);
} else {
console.error(
`Pesan ke ${validTokens[index].slice(-4)} gagal:`,
resp.error?.code,
resp.error?.message
);
}
});
return response;
} catch (error) {
lastError = error;
console.error(`Percobaan ${attempt} gagal:`, (error as Error).message);
if (error instanceof Error && (error as any).code === "app/network-error" && attempt < maxRetries) {
console.log(`Menunggu ${retryDelay}ms sebelum mencoba lagi...`);
await new Promise((resolve) => setTimeout(resolve, retryDelay));
} else {
throw new Error(`Gagal setelah ${maxRetries} percobaan: ${(error as Error).message}`);
}
}
}
throw lastError;
} catch (error) {
console.error("Error mengirim notifikasi:", (error as Error).message, (error as Error).stack);
if ((error as any).code === "messaging/registration-token-not-registered") {
console.log("Token tidak valid. Hapus token dari database.");
}
throw error;
}
}
// Jalankan fungsi
async function main() {
try {
await sendFCMEach();
console.log("Proses selesai");
} catch (error) {
console.error("Gagal menjalankan sendFCMEach:", (error as Error).message);
process.exit(1);
}
}
main();

217
xsendReady.ts Normal file
View File

@@ -0,0 +1,217 @@
import { initializeApp, cert, App } from "firebase-admin/app";
import { getMessaging, Message } from "firebase-admin/messaging";
import path from "path";
const key = {
"type": "service_account",
"project_id": "mobile-darmasaba",
"private_key_id": "764e1207d5acf4db2eac539256c8f1bf397c7d8f",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwCU9PBpAbXsOl\ntb1syvWrmH3FSDRyI4oOVWZJRqYX+j44UTNfzTjYySpNy7x1lr91uOC2GGHJeFvT\nLg5er6uvzFvzwg42A8Rz4+aqxlUhvhNXYRyfaaP7tbui5X9GEmhKYzvYd6T/6z1u\njo7LE1tBaiB8eB69tSJidGcr90yXOsbvKFgaPkpvlrseNR/t0PYDUaXHsxdKvCHI\ntK13KxhJCJrU9+/W1Wwr+45WGfK9m+jLVuOEZT9dd3FUgDn/0CFzykZLA0iHRLjx\neczahlrlvLVCtUIJjHbmsjG8vLZyl6/puh1l2OkEZyADb6m7OOxFVTo5ADZvj4nD\nVCCirdMVAgMBAAECggEAMF0mbnJBpltnSkA/vkOWsmHPcCOx0QgFloGM/CXOXTkR\n3hwlDrWN4DWIi14ltXLIwFmeVzkkqJsKM19scEQ4WbC+NJ7Ek79+Ok7LYXDjE8Wq\nf6+9EukNtgqMdikySfilsYZI+2SHrw4czyKYhZ+YS0USjs/btkgtHbqYW+JyJvv4\nlXAGp3129kbOHTc6+DBq6tn4XiRMKUdBNtcRHe9k+zAIuwbeAdsl4bock1ADnMIv\n/Q4FfOua+nJl8MUpPCZDvz14az+3j/rUVkR/wgDqQirFNRfFfpEPNM2oXVSjp0oK\nTC8NEy5mN4aj0DYS8U2x8barsAFDr5N4L9JxTtdlgwKBgQDkXK9iieIe1/yJFDw6\ntHbQu/bl+t82DESapss62++6ckh2mo+IScvVg/rCwXIag7IRQO40BHWwYTrOwTbj\nD1VUamn6UaqJHpIjDj/SK+As3DumuOTcb+kbJq9TpjLGeR2hj0aKcFXAjL5+B+yr\nBt7fVsB2uhouS9aD68HV8azsxwKBgQDFV2yRKgSf11vNRsxtJekpZ7ruF4h8OZPA\nHcq1kMDPRJcuVD9XwG7RAEgxcErKKS6NrrT/2Iaq5r+P3owgxZ6yB5pabGGvsgcg\nqrvsVEjzETsrrDbp5IevwE/MTwplakr6vJBnfAyjqMbDQSGSZPp+6S8M5JtZhJDL\n9Pqy6yxNQwKBgEE9ZXGuWKZdKC11VXukAOnDOVcco9ZKDPNtwVPQb52BdshDgcv6\n4Tvfl606HMIMa7vYI/VCbOj17hoRQv/9anBScnJsEF9aF3/iW0NM+591T6li2ydK\n5Xq3Q5GPQqRHB7sXNpzoWOdIjkdtNiTqMpP1sch5hG9DhUZs/RSFFdUTAoGBALyV\nyD2NXu/1WVh5cQBZe1FDPMMtIBQ+3bB5h+8tDuTEEomGnyXX0s7OKy97tS0uX7us\nGnJo1IDblHMDZPwofnh5hYsmCdBiHCeeoYm+HhyS+e3JXIz2BKjy6g8/9ZpnEpI8\nwu7yAA4iSxfq1Q9Win/fjUQP71mDsvAGA9IZpbOLAoGBAK57RjNemVh3oNB5ZaQs\n45WzfmPPjKoDQdMYLtohHz9HhPxYFLuvlDc/9OcWFCz3tZHtyDrUtXvv+vX+rG4Y\nemxXkqdg3lYo7nayw772myJb2w6QIfGyuSRx/C1/phmPhp+UkHk7B+KdvWhpPmCC\nBufws2LSn5VZzivO6LrwSCfR\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-fbsvc@mobile-darmasaba.iam.gserviceaccount.com",
"client_id": "105653213329235865762",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40mobile-darmasaba.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}
// --- KONFIGURASI ---
const CONFIG = {
/**
* Path ke file service account key JSON Anda.
* Pastikan file ini ada dan dapat diakses.
*/
firebaseServiceAccountPath: path.resolve(process.cwd(), "key.json"),
/**
* Konten notifikasi default yang akan dikirim.
*/
notificationPayload: {
title: "Sistem Desa Mandiri",
body: "Ada informasi baru untuk Anda, silakan periksa aplikasi.",
// Anda bisa menambahkan URL gambar ikon di sini
// imageUrl: "https://example.com/icon.png",
},
/**
* Pengaturan untuk mekanisme coba lagi (retry) jika terjadi kegagalan jaringan.
*/
retry: {
maxRetries: 3, // Jumlah maksimal percobaan ulang
delay: 2000, // Waktu tunda antar percobaan dalam milidetik (ms)
},
};
// --- AKHIR KONFIGURASI ---
/**
* Inisialisasi Firebase Admin SDK.
* Hanya akan menginisialisasi satu kali.
*/
let firebaseApp: App | null = null;
function initializeFirebase(): App {
if (firebaseApp) {
return firebaseApp;
}
try {
const serviceAccount = key;
firebaseApp = initializeApp({
credential: cert(serviceAccount as any),
});
console.log("Firebase Admin SDK berhasil diinisialisasi.");
return firebaseApp;
} catch (error: any) {
console.error("KRITIS: Gagal inisialisasi Firebase. Pastikan path service account key benar.");
console.error(`Detail Error: ${error.message}`);
process.exit(1); // Keluar dari proses jika Firebase gagal diinisialisasi
}
}
/**
* Mengambil daftar token perangkat dari database.
* TODO: Ganti fungsi ini dengan logika untuk mengambil token dari database Anda.
* @returns {Promise<string[]>} Daftar token FCM.
*/
async function getDeviceTokens(): Promise<string[]> {
console.log("Mengambil token perangkat dari sumber data...");
// CONTOH: Ini adalah data dummy. Seharusnya Anda mengambilnya dari database.
// Misalnya: const users = await prisma.user.findMany({ where: { fcmToken: { not: null } } });
// return users.map(user => user.fcmToken);
const exampleTokens: string[] = [
"c89yuexsS_uc1tOErVPu5a:APA91bEb6tEKXAfReZjFVJ2mMyOzoW_RXryLSnSJTpbIVV3G0L_DCNkLuRvJ02Ip-Erz88QCQBAt-C2SN8eCRxu3-v1sBzXzKPtDv-huXpkjXsyrkifqvUo", // Valid
"cRz96GHKTRaQaRJ35e8Hxa:APA91bEUSxE0VPbqKSzseQ_zGhbYsDofMexKykRw7o_3z2aPM9YFmZbeA2enrmb3qjdZ2g4-QQtiNHAyaZqAT1ITOrwo9jVJlShTeABmEFYP5GLEUZ3dlLc", // Valid
"token_tidak_valid_ini_pasti_gagal_12345", // Contoh token tidak valid
];
console.log(`Berhasil mendapatkan ${exampleTokens.length} token.`);
return exampleTokens;
}
/**
* Membuat array pesan yang akan dikirim ke setiap token.
* @param {string[]} tokens - Daftar token FCM.
* @returns {Message[]} Array objek pesan untuk `sendEach`.
*/
function constructMessages(tokens: string[]): Message[] {
return tokens.map((token) => ({
token,
notification: {
title: CONFIG.notificationPayload.title,
body: CONFIG.notificationPayload.body,
},
data: {
// Anda bisa menambahkan data tambahan di sini
// Contoh: click_action: "FLUTTER_NOTIFICATION_CLICK"
timestamp: String(Date.now()),
},
android: {
priority: "high",
notification: {
sound: "default",
channelId: "default_channel", // Pastikan channel ini ada di aplikasi Android Anda
},
},
apns: {
payload: {
aps: {
sound: "default",
},
},
},
}));
}
/**
* Menangani respons dari `sendEach` untuk mencatat keberhasilan dan kegagalan.
* @param response - Objek respons dari `sendEach`.
* @param originalTokens - Daftar token asli yang dikirimi pesan.
*/
function handleSendResponse(response: any, originalTokens: string[]) {
console.log("Total notifikasi berhasil dikirim:", response.successCount);
console.log("Total notifikasi gagal:", response.failureCount);
const tokensToRemove: string[] = [];
response.responses.forEach((resp: any, index: number) => {
const token = originalTokens[index];
if (resp.success) {
// console.log(`Pesan ke token ...${token.slice(-6)} berhasil:`, resp.messageId);
} else {
console.error(`Pesan ke token ...${token.slice(-6)} GAGAL:`, resp.error.code);
// Jika token tidak lagi terdaftar, tandai untuk dihapus
if (
resp.error.code === "messaging/registration-token-not-registered" ||
resp.error.code === "messaging/invalid-registration-token"
) {
tokensToRemove.push(token);
}
}
});
if (tokensToRemove.length > 0) {
console.warn("Token berikut tidak valid dan harus dihapus dari database:");
tokensToRemove.forEach(token => console.log(`- ${token}`));
// TODO: Implementasikan logika untuk menghapus token-token di atas dari database Anda.
// Misalnya: await prisma.user.updateMany({ where: { fcmToken: { in: tokensToRemove } }, data: { fcmToken: null } });
}
}
/**
* Fungsi utama untuk mengirim notifikasi ke banyak perangkat dengan mekanisme retry.
* @param {Message[]} messages - Array pesan yang akan dikirim.
* @param {string[]} tokens - Daftar token asli untuk logging.
*/
async function sendNotifications(messages: Message[], tokens: string[]) {
let lastError: any;
for (let attempt = 1; attempt <= CONFIG.retry.maxRetries; attempt++) {
try {
const response = await getMessaging().sendEach(messages);
handleSendResponse(response, tokens);
return; // Berhasil, keluar dari fungsi
} catch (error: any) {
lastError = error;
console.error(`Percobaan pengiriman ke-${attempt} gagal:`, error.message);
// Hanya coba lagi jika error berhubungan dengan jaringan
const isNetworkError = error.code === "app/network-error" || error.code?.includes("network");
if (isNetworkError && attempt < CONFIG.retry.maxRetries) {
console.log(`Menunggu ${CONFIG.retry.delay}ms sebelum mencoba lagi...`);
await new Promise((resolve) => setTimeout(resolve, CONFIG.retry.delay));
} else {
// Jika bukan error jaringan atau sudah mencapai batas retry, lempar error
throw new Error(`Gagal mengirim notifikasi setelah ${attempt} percobaan: ${error.message}`);
}
}
}
throw lastError;
}
/**
* Fungsi orchestrator untuk menjalankan seluruh proses.
*/
export async function sendMultiple() {
try {
initializeFirebase();
const tokens = await getDeviceTokens();
if (tokens.length === 0) {
console.log("Tidak ada token perangkat yang ditemukan. Proses dihentikan.");
return;
}
const messages = constructMessages(tokens);
console.log(`
Mempersiapkan pengiriman ${messages.length} notifikasi...`);
await sendNotifications(messages, tokens);
console.log("Proses pengiriman notifikasi selesai.");
} catch (error: any) {
console.error("Terjadi error fatal dalam proses pengiriman:", error.message);
process.exit(1);
}
}