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:
2026-06-17 15:40:24 +05:30
parent 978f7effc0
commit 6aec093516
5 changed files with 143 additions and 9 deletions
+62 -7
View File
@@ -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[]
+50
View File
@@ -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>;
}
+10 -2
View File
@@ -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;
+19
View File
@@ -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}`;
}
+2
View File
@@ -3,3 +3,5 @@ export * from './message';
export * from './bot';
export * from './onboarding';
export * from './rule';
export * from './adapter';
export * from './hash';