feat: add org layer to schema and export OrgAdminJwtPayload, adapter, hash from types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,59 @@ datasource db {
|
|||||||
url = env("DATABASE_URL")
|
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
|
// Tenancy
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -18,6 +71,8 @@ model Tenant {
|
|||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
isForwardingPaused Boolean @default(false)
|
isForwardingPaused Boolean @default(false)
|
||||||
settings Json @default("{}")
|
settings Json @default("{}")
|
||||||
|
organizationId String?
|
||||||
|
organization Organization? @relation(fields: [organizationId], references: [id])
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
@@ -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<void>;
|
||||||
|
onReaction?: (reaction: NormalizedReaction) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelAdapter {
|
||||||
|
readonly platform: string;
|
||||||
|
|
||||||
|
// Start receiving messages — sets up sessions, polling loops, etc.
|
||||||
|
start(handlers: AdapterHandlers): Promise<void>;
|
||||||
|
|
||||||
|
// Send a text message to a group
|
||||||
|
send(command: AdapterOutboundCommand): Promise<void>;
|
||||||
|
|
||||||
|
// Fetch live group metadata (members + admin flags). Returns null if unavailable.
|
||||||
|
getGroupMeta(groupJid: string, accountId: string): Promise<AdapterGroupMeta | null>;
|
||||||
|
|
||||||
|
// Tear down all connections and polling timers
|
||||||
|
stop(): Promise<void>;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export type AdminRole = 'OWNER' | 'ADMIN' | 'VIEWER';
|
export type AdminRole = 'OWNER' | 'ADMIN' | 'VIEWER';
|
||||||
|
|
||||||
export type JwtKind = 'admin' | 'member' | 'superadmin';
|
export type JwtKind = 'admin' | 'member' | 'superadmin' | 'orgadmin';
|
||||||
|
|
||||||
export interface AdminJwtPayload {
|
export interface AdminJwtPayload {
|
||||||
kind: 'admin';
|
kind: 'admin';
|
||||||
@@ -24,7 +24,15 @@ export interface SuperAdminJwtPayload {
|
|||||||
email: string;
|
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 {
|
export interface LoginRequest {
|
||||||
tenantSlug?: string;
|
tenantSlug?: string;
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
}
|
||||||
@@ -3,3 +3,5 @@ export * from './message';
|
|||||||
export * from './bot';
|
export * from './bot';
|
||||||
export * from './onboarding';
|
export * from './onboarding';
|
||||||
export * from './rule';
|
export * from './rule';
|
||||||
|
export * from './adapter';
|
||||||
|
export * from './hash';
|
||||||
|
|||||||
Reference in New Issue
Block a user