feat(notifications): add ZaloOaLinkController + migration + schema — TEC-3065

Include files missed from previous commit:
- ZaloOaLinkController (GET /auth/zalo-oa/link, GET /auth/zalo-oa/callback, DELETE)
- prisma/schema.prisma — ZaloAccountLink model + User.zaloAccountLink relation
- prisma/migrations/20260421010000_add_zalo_account_links/migration.sql
- Updated ZaloOaService, webhook controller, notifications module, and specs

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-21 04:49:52 +07:00
parent 603ef7db86
commit a720825257
8 changed files with 1198 additions and 248 deletions

View File

@@ -0,0 +1,29 @@
-- [TEC-3065] Add zalo_account_links table for Zalo OA OAuth account linking.
-- Stores per-user OA access/refresh tokens (AES-256-GCM encrypted at app layer)
-- and the last interaction timestamp used for the 24-hour ZNS window check.
CREATE TABLE "zalo_account_links" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"zaloUserId" TEXT NOT NULL,
"accessToken" TEXT NOT NULL,
"refreshToken" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"lastInteractAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "zalo_account_links_pkey" PRIMARY KEY ("id")
);
-- One link per platform user
CREATE UNIQUE INDEX "zalo_account_links_userId_key" ON "zalo_account_links"("userId");
-- One link per Zalo OA UID
CREATE UNIQUE INDEX "zalo_account_links_zaloUserId_key" ON "zalo_account_links"("zaloUserId");
CREATE INDEX "zalo_account_links_zaloUserId_idx" ON "zalo_account_links"("zaloUserId");
CREATE INDEX "zalo_account_links_expiresAt_idx" ON "zalo_account_links"("expiresAt");
ALTER TABLE "zalo_account_links"
ADD CONSTRAINT "zalo_account_links_userId_fkey"
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -81,6 +81,7 @@ model User {
ownedProjects ProjectDevelopment[] @relation("ProjectOwner")
/// KCN do user này vận hành (role=PARK_OPERATOR).
ownedIndustrialParks IndustrialPark[] @relation("IndustrialParkOwner")
zaloAccountLink ZaloAccountLink?
@@index([role])
@@index([kycStatus])
@@ -145,6 +146,30 @@ model OAuthAccount {
@@index([userId])
}
/// Zalo OA account link — stores the OA-scoped access/refresh tokens for sending
/// template messages to a linked user via ZNS.
/// Token fields are AES-256-GCM encrypted at the application layer.
model ZaloAccountLink {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
/// Zalo user ID scoped to the Official Account (OA UID, not Social Graph UID)
zaloUserId String @unique
/// AES-256-GCM encrypted access token (base64url: iv.tag.ciphertext)
accessToken String
/// AES-256-GCM encrypted refresh token (base64url: iv.tag.ciphertext)
refreshToken String
expiresAt DateTime
/// Unix epoch (seconds) of the last user→OA interaction; used for 24-hour window check
lastInteractAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([zaloUserId])
@@index([expiresAt])
@@map("zalo_account_links")
}
model Agent {
id String @id @default(cuid())
userId String @unique