From 563d95b92835732e2d4b621b6b4b3373e9568bac Mon Sep 17 00:00:00 2001 From: bagasbanuna Date: Tue, 16 Dec 2025 17:50:03 +0800 Subject: [PATCH] Penerapan notifikasi mobil ke database Fix: - modified: prisma/schema.prisma Add: prisma/migrations/20251216041242_add_token_user_device_indexes/ src/app/api/mobile/auth/device-tokens/ ### No Issue --- .../migration.sql | 28 ++++++++ prisma/schema.prisma | 33 ++++++++-- .../api/mobile/auth/device-tokens/route.ts | 64 +++++++++++++++++++ 3 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 prisma/migrations/20251216041242_add_token_user_device_indexes/migration.sql create mode 100644 src/app/api/mobile/auth/device-tokens/route.ts diff --git a/prisma/migrations/20251216041242_add_token_user_device_indexes/migration.sql b/prisma/migrations/20251216041242_add_token_user_device_indexes/migration.sql new file mode 100644 index 00000000..9dcbe48a --- /dev/null +++ b/prisma/migrations/20251216041242_add_token_user_device_indexes/migration.sql @@ -0,0 +1,28 @@ +-- AlterTable +ALTER TABLE "Notifikasi" ADD COLUMN "deepLink" TEXT, +ADD COLUMN "readAt" TIMESTAMP(3); + +-- CreateTable +CREATE TABLE "TokenUserDevice" ( + "id" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "platform" TEXT, + "deviceId" TEXT, + "model" TEXT, + "appVersion" TEXT, + "token" TEXT NOT NULL, + "userId" TEXT, + + CONSTRAINT "TokenUserDevice_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "TokenUserDevice_userId_idx" ON "TokenUserDevice"("userId"); + +-- CreateIndex +CREATE INDEX "TokenUserDevice_token_idx" ON "TokenUserDevice"("token"); + +-- AddForeignKey +ALTER TABLE "TokenUserDevice" ADD CONSTRAINT "TokenUserDevice_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a0ad008b..06d1ff30 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -58,6 +58,7 @@ model User { acceptedTermsAt DateTime? acceptedForumTermsAt DateTime? + tokenUserDevices TokenUserDevice[] } model MasterUserRole { @@ -973,16 +974,19 @@ model NomorAdmin { } model Notifikasi { - id String @id @default(cuid()) - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - isRead Boolean @default(false) + id String @id @default(cuid()) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt appId String kategoriApp String pesan String title String? status String? + + isRead Boolean @default(false) + readAt DateTime? // kapan user membaca notifikasi ini + deepLink String? // misal: "announcement/123", "user/profile/cmha6wb9w0001cfndwl9fcse6" Role MasterUserRole? @relation(fields: [userRoleId], references: [id]) userRoleId String @@ -1099,3 +1103,22 @@ model BlockedUser { @@unique([blockerId, blockedId]) } + +model TokenUserDevice { + id String @id @default(uuid()) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + platform String? // "ios" | "android" + deviceId String? // UUID unik dari device (misal: dari expo-device) + model String? // "iPhone15,4", "Pixel 7", dll + appVersion String? // "1.5.15" — sangat berguna saat debug + + token String @db.Text + user User? @relation(fields: [userId], references: [id]) + userId String? + + @@index([userId]) + @@index([token]) // untuk pencarian cepat & deduplikasi +} diff --git a/src/app/api/mobile/auth/device-tokens/route.ts b/src/app/api/mobile/auth/device-tokens/route.ts new file mode 100644 index 00000000..010aef4b --- /dev/null +++ b/src/app/api/mobile/auth/device-tokens/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib"; + +export async function POST(request: NextRequest) { + const { data } = await request.json(); + try { + console.log("Data >>", JSON.stringify(data, null, 2)); + + const { userId, platform, deviceId, model, appVersion, fcmToken } = + data; + + if (!fcmToken) { + return NextResponse.json({ error: "Missing Token" }, { status: 400 }); + } + + const existing = await prisma.tokenUserDevice.findFirst({ + where: { + token: fcmToken, + userId: userId, + }, + select: { + id: true, + }, + }); + + let deviceToken; + + if (existing) { + deviceToken = await prisma.tokenUserDevice.update({ + where: { + id: existing?.id, + }, + data: { + platform, + deviceId, + model, + appVersion, + isActive: true, + updatedAt: new Date(), + }, + }); + } else { + // Buat baru jika belum ada + deviceToken = await prisma.tokenUserDevice.create({ + data: { + token: fcmToken, + userId: userId, + platform, + deviceId, + model, + appVersion, + isActive: true, + }, + }); + } + + return NextResponse.json({ success: true, data: deviceToken }); + } catch (error) { + return NextResponse.json( + { error: (error as Error).message }, + { status: 500 } + ); + } +}