diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 8475200..52b984c 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -7,19 +7,74 @@ datasource db { url = env("DATABASE_URL") } +// ============================================================================ +// Organization layer (above Tenancy) +// ============================================================================ + +enum OrgAdminRole { + ORG_OWNER + ORG_ADMIN +} + +model Organization { + id String @id @default(cuid()) + slug String @unique + name String + settings Json @default("{}") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tenants Tenant[] + orgAdmins OrgAdmin[] + orgRules OrgRule[] +} + +model OrgAdmin { + id String @id @default(cuid()) + organizationId String + organization Organization @relation(fields: [organizationId], references: [id]) + email String + passwordHash String + name String? + role OrgAdminRole @default(ORG_ADMIN) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([organizationId, email]) + @@index([organizationId]) +} + +model OrgRule { + id String @id @default(cuid()) + organizationId String + organization Organization @relation(fields: [organizationId], references: [id]) + matchType RuleMatchType + matchValue String + action RuleAction + priority Int @default(0) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([organizationId, matchType, matchValue]) + @@index([organizationId, isActive]) +} + // ============================================================================ // Tenancy // ============================================================================ model Tenant { - id String @id @default(cuid()) - slug String @unique + id String @id @default(cuid()) + slug String @unique name String - isActive Boolean @default(true) - isForwardingPaused Boolean @default(false) - settings Json @default("{}") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + isActive Boolean @default(true) + isForwardingPaused Boolean @default(false) + settings Json @default("{}") + organizationId String? + organization Organization? @relation(fields: [organizationId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt admins Admin[] tenantBots TenantBot[] diff --git a/packages/types/src/adapter.ts b/packages/types/src/adapter.ts new file mode 100644 index 0000000..1cc996a --- /dev/null +++ b/packages/types/src/adapter.ts @@ -0,0 +1,50 @@ +import { NormalizedReaction } from './message'; + +// Resolved inbound message — adapter has already mapped sourceGroupJid → sourceGroupId +export interface AdapterInboundMessage { + platformMsgId: string; + sourceGroupJid: string; // original WhatsApp/Telegram JID — kept for groupMetadata calls + sourceGroupId: string; // DB Group.id resolved by the adapter via its internal groupMap + senderJid: string; + senderName: string | null; + content: string; + accountId: string; + quotedPlatformMsgId?: string; // set when this message quotes/replies to another message +} + +export interface AdapterGroupMeta { + jid: string; + name: string; + participants: Array<{ + jid: string; + isAdmin: boolean; + }>; +} + +export interface AdapterOutboundCommand { + accountId: string; + toGroupJid: string; + text: string; +} + +// Handlers supplied by the orchestrator (main.ts) — contain business logic, no adapter specifics +export interface AdapterHandlers { + onMessage: (msg: AdapterInboundMessage) => Promise; + onReaction?: (reaction: NormalizedReaction) => Promise; +} + +export interface ChannelAdapter { + readonly platform: string; + + // Start receiving messages — sets up sessions, polling loops, etc. + start(handlers: AdapterHandlers): Promise; + + // Send a text message to a group + send(command: AdapterOutboundCommand): Promise; + + // Fetch live group metadata (members + admin flags). Returns null if unavailable. + getGroupMeta(groupJid: string, accountId: string): Promise; + + // Tear down all connections and polling timers + stop(): Promise; +} diff --git a/packages/types/src/auth.ts b/packages/types/src/auth.ts index 0959347..31e9455 100644 --- a/packages/types/src/auth.ts +++ b/packages/types/src/auth.ts @@ -1,6 +1,6 @@ export type AdminRole = 'OWNER' | 'ADMIN' | 'VIEWER'; -export type JwtKind = 'admin' | 'member' | 'superadmin'; +export type JwtKind = 'admin' | 'member' | 'superadmin' | 'orgadmin'; export interface AdminJwtPayload { kind: 'admin'; @@ -24,7 +24,15 @@ export interface SuperAdminJwtPayload { email: string; } -export type JwtPayload = AdminJwtPayload | MemberJwtPayload | SuperAdminJwtPayload; +export interface OrgAdminJwtPayload { + kind: 'orgadmin'; + sub: string; + organizationId: string; + email: string; + role: 'ORG_OWNER' | 'ORG_ADMIN'; +} + +export type JwtPayload = AdminJwtPayload | MemberJwtPayload | SuperAdminJwtPayload | OrgAdminJwtPayload; export interface LoginRequest { tenantSlug?: string; diff --git a/packages/types/src/hash.ts b/packages/types/src/hash.ts new file mode 100644 index 0000000..de79dc9 --- /dev/null +++ b/packages/types/src/hash.ts @@ -0,0 +1,19 @@ +import { createHash } from 'crypto'; + +/** + * Deterministic phone hash used for TowerUser identity. + * pepper = JWT_SECRET env var. Keep consistent across API and worker. + */ +export function hashPhone(phone: string, pepper: string): string { + const normalized = phone.replace(/[^\d+]/g, ''); + return createHash('sha256').update(`${pepper}:${normalized}`).digest('hex'); +} + +/** + * Converts a Baileys/WhatsApp JID to an E.164-style phone string. + * '919876543210@s.whatsapp.net' → '+919876543210' + */ +export function jidToPhone(jid: string): string { + const digits = jid.split('@')[0]; + return `+${digits}`; +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 317f895..26178a2 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -3,3 +3,5 @@ export * from './message'; export * from './bot'; export * from './onboarding'; export * from './rule'; +export * from './adapter'; +export * from './hash';