feat(db): enforce runtime RLS boundaries

This commit is contained in:
2026-03-22 22:49:47 +04:00
parent 7665af0268
commit 97b5edcc0a
24 changed files with 2054 additions and 545 deletions

View File

@@ -4,7 +4,12 @@ LOG_LEVEL=info
PORT=3000 PORT=3000
# Database # Database
# Owner connection for migrations, seed, and schema checks only
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:54322/postgres DATABASE_URL=postgres://postgres:postgres@127.0.0.1:54322/postgres
# Runtime connection used by mini app and authenticated user-triggered API flows
APP_DATABASE_URL=postgres://housebot_app:housebot_app@127.0.0.1:54322/postgres
# Runtime connection used by bot ingestion, reminders, schedulers, and other worker flows
WORKER_DATABASE_URL=postgres://housebot_worker:housebot_worker@127.0.0.1:54322/postgres
DB_SCHEMA=public DB_SCHEMA=public
# Telegram # Telegram

View File

@@ -4,6 +4,8 @@ export interface BotRuntimeConfig {
telegramBotToken: string telegramBotToken: string
telegramWebhookSecret: string telegramWebhookSecret: string
telegramWebhookPath: string telegramWebhookPath: string
appDatabaseUrl?: string
workerDatabaseUrl?: string
databaseUrl?: string databaseUrl?: string
purchaseTopicIngestionEnabled: boolean purchaseTopicIngestionEnabled: boolean
financeCommandsEnabled: boolean financeCommandsEnabled: boolean
@@ -101,20 +103,22 @@ function parsePositiveInteger(raw: string | undefined, fallback: number, key: st
export function getBotRuntimeConfig(env: NodeJS.ProcessEnv = process.env): BotRuntimeConfig { export function getBotRuntimeConfig(env: NodeJS.ProcessEnv = process.env): BotRuntimeConfig {
const databaseUrl = parseOptionalValue(env.DATABASE_URL) const databaseUrl = parseOptionalValue(env.DATABASE_URL)
const appDatabaseUrl = parseOptionalValue(env.APP_DATABASE_URL)
const workerDatabaseUrl = parseOptionalValue(env.WORKER_DATABASE_URL)
const schedulerSharedSecret = parseOptionalValue(env.SCHEDULER_SHARED_SECRET) const schedulerSharedSecret = parseOptionalValue(env.SCHEDULER_SHARED_SECRET)
const schedulerOidcAllowedEmails = parseOptionalCsv(env.SCHEDULER_OIDC_ALLOWED_EMAILS) const schedulerOidcAllowedEmails = parseOptionalCsv(env.SCHEDULER_OIDC_ALLOWED_EMAILS)
const miniAppAllowedOrigins = parseOptionalCsv(env.MINI_APP_ALLOWED_ORIGINS) const miniAppAllowedOrigins = parseOptionalCsv(env.MINI_APP_ALLOWED_ORIGINS)
const miniAppUrl = parseOptionalValue(env.MINI_APP_URL) const miniAppUrl = parseOptionalValue(env.MINI_APP_URL)
const purchaseTopicIngestionEnabled = databaseUrl !== undefined const purchaseTopicIngestionEnabled = workerDatabaseUrl !== undefined
const financeCommandsEnabled = workerDatabaseUrl !== undefined
const financeCommandsEnabled = databaseUrl !== undefined const anonymousFeedbackEnabled = workerDatabaseUrl !== undefined
const anonymousFeedbackEnabled = databaseUrl !== undefined const assistantEnabled = workerDatabaseUrl !== undefined
const assistantEnabled = databaseUrl !== undefined const miniAppAuthEnabled = appDatabaseUrl !== undefined
const miniAppAuthEnabled = databaseUrl !== undefined
const hasSchedulerOidcConfig = schedulerOidcAllowedEmails.length > 0 const hasSchedulerOidcConfig = schedulerOidcAllowedEmails.length > 0
const reminderJobsEnabled = const reminderJobsEnabled =
databaseUrl !== undefined && (schedulerSharedSecret !== undefined || hasSchedulerOidcConfig) workerDatabaseUrl !== undefined &&
(schedulerSharedSecret !== undefined || hasSchedulerOidcConfig)
const runtime: BotRuntimeConfig = { const runtime: BotRuntimeConfig = {
port: parsePort(env.PORT), port: parsePort(env.PORT),
@@ -173,6 +177,12 @@ export function getBotRuntimeConfig(env: NodeJS.ProcessEnv = process.env): BotRu
if (databaseUrl !== undefined) { if (databaseUrl !== undefined) {
runtime.databaseUrl = databaseUrl runtime.databaseUrl = databaseUrl
} }
if (appDatabaseUrl !== undefined) {
runtime.appDatabaseUrl = appDatabaseUrl
}
if (workerDatabaseUrl !== undefined) {
runtime.workerDatabaseUrl = workerDatabaseUrl
}
if (schedulerSharedSecret !== undefined) { if (schedulerSharedSecret !== undefined) {
runtime.schedulerSharedSecret = schedulerSharedSecret runtime.schedulerSharedSecret = schedulerSharedSecret
} }

View File

@@ -47,7 +47,11 @@ import { createReminderJobsHandler } from './reminder-jobs'
import { registerReminderTopicUtilities } from './reminder-topic-utilities' import { registerReminderTopicUtilities } from './reminder-topic-utilities'
import { createSchedulerRequestAuthorizer } from './scheduler-auth' import { createSchedulerRequestAuthorizer } from './scheduler-auth'
import { createBotWebhookServer } from './server' import { createBotWebhookServer } from './server'
import { createMiniAppAuthHandler, createMiniAppJoinHandler } from './miniapp-auth' import {
createMiniAppAuthHandler,
createMiniAppJoinHandler,
type MiniAppAuthorizedSession
} from './miniapp-auth'
import { createMiniAppDashboardHandler } from './miniapp-dashboard' import { createMiniAppDashboardHandler } from './miniapp-dashboard'
import { import {
createMiniAppApproveMemberHandler, createMiniAppApproveMemberHandler,
@@ -90,13 +94,13 @@ configureLogger({
const logger = getLogger('runtime') const logger = getLogger('runtime')
const shutdownTasks: Array<() => Promise<void>> = [] const shutdownTasks: Array<() => Promise<void>> = []
const householdConfigurationRepositoryClient = runtime.databaseUrl const workerHouseholdConfigurationRepositoryClient = runtime.workerDatabaseUrl
? createDbHouseholdConfigurationRepository(runtime.databaseUrl) ? createDbHouseholdConfigurationRepository(runtime.workerDatabaseUrl)
: null : null
const bot = createTelegramBot( const bot = createTelegramBot(
runtime.telegramBotToken, runtime.telegramBotToken,
getLogger('telegram'), getLogger('telegram'),
householdConfigurationRepositoryClient?.repository workerHouseholdConfigurationRepositoryClient?.repository
) )
bot.botInfo = await bot.api.getMe() bot.botInfo = await bot.api.getMe()
const webhookHandler = webhookCallback(bot, 'std/http', { const webhookHandler = webhookCallback(bot, 'std/http', {
@@ -111,29 +115,23 @@ const paymentConfirmationServices = new Map<
const exchangeRateProvider = createNbgExchangeRateProvider({ const exchangeRateProvider = createNbgExchangeRateProvider({
logger: getLogger('fx') logger: getLogger('fx')
}) })
const householdOnboardingService = householdConfigurationRepositoryClient const householdOnboardingService = workerHouseholdConfigurationRepositoryClient
? createHouseholdOnboardingService({ ? createHouseholdOnboardingService({
repository: householdConfigurationRepositoryClient.repository repository: workerHouseholdConfigurationRepositoryClient.repository
}) })
: null : null
const miniAppAdminService = householdConfigurationRepositoryClient const telegramPendingActionRepositoryClient = runtime.workerDatabaseUrl
? createMiniAppAdminService(householdConfigurationRepositoryClient.repository) ? createDbTelegramPendingActionRepository(runtime.workerDatabaseUrl!)
: null
const localePreferenceService = householdConfigurationRepositoryClient
? createLocalePreferenceService(householdConfigurationRepositoryClient.repository)
: null
const telegramPendingActionRepositoryClient = runtime.databaseUrl
? createDbTelegramPendingActionRepository(runtime.databaseUrl!)
: null : null
const processedBotMessageRepositoryClient = const processedBotMessageRepositoryClient =
runtime.databaseUrl && runtime.assistantEnabled runtime.workerDatabaseUrl && runtime.assistantEnabled
? createDbProcessedBotMessageRepository(runtime.databaseUrl!) ? createDbProcessedBotMessageRepository(runtime.workerDatabaseUrl!)
: null : null
const purchaseRepositoryClient = runtime.databaseUrl const purchaseRepositoryClient = runtime.workerDatabaseUrl
? createPurchaseMessageRepository(runtime.databaseUrl!) ? createPurchaseMessageRepository(runtime.workerDatabaseUrl!)
: null : null
const topicMessageHistoryRepositoryClient = runtime.databaseUrl const topicMessageHistoryRepositoryClient = runtime.workerDatabaseUrl
? createDbTopicMessageHistoryRepository(runtime.databaseUrl!) ? createDbTopicMessageHistoryRepository(runtime.workerDatabaseUrl!)
: null : null
const purchaseInterpreter = createOpenAiPurchaseInterpreter( const purchaseInterpreter = createOpenAiPurchaseInterpreter(
runtime.openaiApiKey, runtime.openaiApiKey,
@@ -169,6 +167,167 @@ const anonymousFeedbackServices = new Map<
string, string,
ReturnType<typeof createAnonymousFeedbackService> ReturnType<typeof createAnonymousFeedbackService>
>() >()
const appHouseholdConfigurationRepositoryClients = new Map<
string,
ReturnType<typeof createDbHouseholdConfigurationRepository>
>()
const appOnboardingServices = new Map<string, ReturnType<typeof createHouseholdOnboardingService>>()
const appFinanceRepositoryClients = new Map<string, ReturnType<typeof createDbFinanceRepository>>()
const appFinanceServices = new Map<string, ReturnType<typeof createFinanceCommandService>>()
const appMiniAppAdminServices = new Map<string, ReturnType<typeof createMiniAppAdminService>>()
const appLocalePreferenceServices = new Map<
string,
ReturnType<typeof createLocalePreferenceService>
>()
function miniAppSessionKey(session: MiniAppAuthorizedSession): string {
return [
session.telegramUserId,
session.member.householdId,
session.member.id,
session.member.isAdmin ? 'admin' : 'member'
].join(':')
}
function appHouseholdConfigurationRepositoryKey(input: {
telegramUserId: string
householdId?: string
memberId?: string
isAdmin?: boolean
}): string {
return [
input.telegramUserId,
input.householdId ?? 'none',
input.memberId ?? 'none',
input.isAdmin === true ? 'admin' : 'member'
].join(':')
}
function appHouseholdConfigurationRepositoryForContext(input: {
telegramUserId: string
householdId?: string
memberId?: string
isAdmin?: boolean
}) {
const key = appHouseholdConfigurationRepositoryKey(input)
const existing = appHouseholdConfigurationRepositoryClients.get(key)
if (existing) {
return existing
}
const repositoryClient = createDbHouseholdConfigurationRepository(runtime.appDatabaseUrl!, {
sessionContext: {
telegramUserId: input.telegramUserId,
...(input.householdId
? {
householdId: input.householdId
}
: {}),
...(input.memberId
? {
memberId: input.memberId
}
: {}),
...(input.isAdmin !== undefined
? {
isAdmin: input.isAdmin
}
: {})
}
})
appHouseholdConfigurationRepositoryClients.set(key, repositoryClient)
shutdownTasks.push(repositoryClient.close)
return repositoryClient
}
function appOnboardingServiceForTelegramUserId(telegramUserId: string) {
const existing = appOnboardingServices.get(telegramUserId)
if (existing) {
return existing
}
const service = createHouseholdOnboardingService({
repository: appHouseholdConfigurationRepositoryForContext({
telegramUserId
}).repository
})
appOnboardingServices.set(telegramUserId, service)
return service
}
function appHouseholdConfigurationRepositoryForSession(session: MiniAppAuthorizedSession) {
return appHouseholdConfigurationRepositoryForContext({
telegramUserId: session.telegramUserId,
householdId: session.member.householdId,
memberId: session.member.id,
isAdmin: session.member.isAdmin
})
}
function appFinanceServiceForSession(session: MiniAppAuthorizedSession) {
const key = miniAppSessionKey(session)
const existing = appFinanceServices.get(key)
if (existing) {
return existing
}
const repositoryClient = createDbFinanceRepository(
runtime.appDatabaseUrl!,
session.member.householdId,
{
sessionContext: {
telegramUserId: session.telegramUserId,
householdId: session.member.householdId,
memberId: session.member.id,
...(session.member.isAdmin !== undefined
? {
isAdmin: session.member.isAdmin
}
: {})
}
}
)
appFinanceRepositoryClients.set(key, repositoryClient)
shutdownTasks.push(repositoryClient.close)
const service = createFinanceCommandService({
householdId: session.member.householdId,
repository: repositoryClient.repository,
householdConfigurationRepository:
appHouseholdConfigurationRepositoryForSession(session).repository,
exchangeRateProvider
})
appFinanceServices.set(key, service)
return service
}
function appMiniAppAdminServiceForSession(session: MiniAppAuthorizedSession) {
const key = miniAppSessionKey(session)
const existing = appMiniAppAdminServices.get(key)
if (existing) {
return existing
}
const service = createMiniAppAdminService(
appHouseholdConfigurationRepositoryForSession(session).repository
)
appMiniAppAdminServices.set(key, service)
return service
}
function appLocalePreferenceServiceForSession(session: MiniAppAuthorizedSession) {
const key = miniAppSessionKey(session)
const existing = appLocalePreferenceServices.get(key)
if (existing) {
return existing
}
const service = createLocalePreferenceService(
appHouseholdConfigurationRepositoryForSession(session).repository
)
appLocalePreferenceServices.set(key, service)
return service
}
function financeServiceForHousehold(householdId: string) { function financeServiceForHousehold(householdId: string) {
const existing = financeServices.get(householdId) const existing = financeServices.get(householdId)
@@ -180,7 +339,7 @@ function financeServiceForHousehold(householdId: string) {
const service = createFinanceCommandService({ const service = createFinanceCommandService({
householdId, householdId,
repository: repositoryClient.repository, repository: repositoryClient.repository,
householdConfigurationRepository: householdConfigurationRepositoryClient!.repository, householdConfigurationRepository: workerHouseholdConfigurationRepositoryClient!.repository,
exchangeRateProvider exchangeRateProvider
}) })
financeServices.set(householdId, service) financeServices.set(householdId, service)
@@ -193,7 +352,7 @@ function financeRepositoryForHousehold(householdId: string) {
return existing return existing
} }
const repositoryClient = createDbFinanceRepository(runtime.databaseUrl!, householdId) const repositoryClient = createDbFinanceRepository(runtime.workerDatabaseUrl!, householdId)
financeRepositoryClients.set(householdId, repositoryClient) financeRepositoryClients.set(householdId, repositoryClient)
shutdownTasks.push(repositoryClient.close) shutdownTasks.push(repositoryClient.close)
return repositoryClient return repositoryClient
@@ -209,7 +368,7 @@ function paymentConfirmationServiceForHousehold(householdId: string) {
householdId, householdId,
financeService: financeServiceForHousehold(householdId), financeService: financeServiceForHousehold(householdId),
repository: financeRepositoryForHousehold(householdId).repository, repository: financeRepositoryForHousehold(householdId).repository,
householdConfigurationRepository: householdConfigurationRepositoryClient!.repository, householdConfigurationRepository: workerHouseholdConfigurationRepositoryClient!.repository,
exchangeRateProvider exchangeRateProvider
}) })
paymentConfirmationServices.set(householdId, service) paymentConfirmationServices.set(householdId, service)
@@ -222,7 +381,10 @@ function anonymousFeedbackServiceForHousehold(householdId: string) {
return existing return existing
} }
const repositoryClient = createDbAnonymousFeedbackRepository(runtime.databaseUrl!, householdId) const repositoryClient = createDbAnonymousFeedbackRepository(
runtime.workerDatabaseUrl!,
householdId
)
anonymousFeedbackRepositoryClients.set(householdId, repositoryClient) anonymousFeedbackRepositoryClients.set(householdId, repositoryClient)
shutdownTasks.push(repositoryClient.close) shutdownTasks.push(repositoryClient.close)
@@ -231,8 +393,8 @@ function anonymousFeedbackServiceForHousehold(householdId: string) {
return service return service
} }
if (householdConfigurationRepositoryClient) { if (workerHouseholdConfigurationRepositoryClient) {
shutdownTasks.push(householdConfigurationRepositoryClient.close) shutdownTasks.push(workerHouseholdConfigurationRepositoryClient.close)
} }
if (telegramPendingActionRepositoryClient) { if (telegramPendingActionRepositoryClient) {
@@ -251,10 +413,10 @@ if (topicMessageHistoryRepositoryClient) {
shutdownTasks.push(topicMessageHistoryRepositoryClient.close) shutdownTasks.push(topicMessageHistoryRepositoryClient.close)
} }
if (purchaseRepositoryClient && householdConfigurationRepositoryClient) { if (purchaseRepositoryClient && workerHouseholdConfigurationRepositoryClient) {
registerConfiguredPurchaseTopicIngestion( registerConfiguredPurchaseTopicIngestion(
bot, bot,
householdConfigurationRepositoryClient.repository, workerHouseholdConfigurationRepositoryClient.repository,
purchaseRepositoryClient.repository, purchaseRepositoryClient.repository,
{ {
...(topicProcessor ...(topicProcessor
@@ -280,7 +442,7 @@ if (purchaseRepositoryClient && householdConfigurationRepositoryClient) {
registerConfiguredPaymentTopicIngestion( registerConfiguredPaymentTopicIngestion(
bot, bot,
householdConfigurationRepositoryClient.repository, workerHouseholdConfigurationRepositoryClient.repository,
telegramPendingActionRepositoryClient!.repository, telegramPendingActionRepositoryClient!.repository,
financeServiceForHousehold, financeServiceForHousehold,
paymentConfirmationServiceForHousehold, paymentConfirmationServiceForHousehold,
@@ -306,13 +468,13 @@ if (purchaseRepositoryClient && householdConfigurationRepositoryClient) {
event: 'runtime.feature_disabled', event: 'runtime.feature_disabled',
feature: 'purchase-topic-ingestion' feature: 'purchase-topic-ingestion'
}, },
'Purchase topic ingestion is disabled. Set DATABASE_URL to enable Telegram topic lookups.' 'Purchase topic ingestion is disabled. Set WORKER_DATABASE_URL to enable Telegram topic lookups.'
) )
} }
if (runtime.financeCommandsEnabled) { if (runtime.financeCommandsEnabled) {
const financeCommands = createFinanceCommandsService({ const financeCommands = createFinanceCommandsService({
householdConfigurationRepository: householdConfigurationRepositoryClient!.repository, householdConfigurationRepository: workerHouseholdConfigurationRepositoryClient!.repository,
financeServiceForHousehold, financeServiceForHousehold,
...(runtime.miniAppUrl ...(runtime.miniAppUrl
? { ? {
@@ -329,21 +491,21 @@ if (runtime.financeCommandsEnabled) {
event: 'runtime.feature_disabled', event: 'runtime.feature_disabled',
feature: 'finance-commands' feature: 'finance-commands'
}, },
'Finance commands are disabled. Set DATABASE_URL to enable household lookups.' 'Finance commands are disabled. Set WORKER_DATABASE_URL to enable household lookups.'
) )
} }
if (householdConfigurationRepositoryClient) { if (workerHouseholdConfigurationRepositoryClient) {
registerHouseholdSetupCommands({ registerHouseholdSetupCommands({
bot, bot,
householdSetupService: createHouseholdSetupService( householdSetupService: createHouseholdSetupService(
householdConfigurationRepositoryClient.repository workerHouseholdConfigurationRepositoryClient.repository
), ),
householdAdminService: createHouseholdAdminService( householdAdminService: createHouseholdAdminService(
householdConfigurationRepositoryClient.repository workerHouseholdConfigurationRepositoryClient.repository
), ),
householdOnboardingService: householdOnboardingService!, householdOnboardingService: householdOnboardingService!,
householdConfigurationRepository: householdConfigurationRepositoryClient.repository, householdConfigurationRepository: workerHouseholdConfigurationRepositoryClient.repository,
...(telegramPendingActionRepositoryClient ...(telegramPendingActionRepositoryClient
? { ? {
promptRepository: telegramPendingActionRepositoryClient.repository promptRepository: telegramPendingActionRepositoryClient.repository
@@ -362,20 +524,22 @@ if (householdConfigurationRepositoryClient) {
event: 'runtime.feature_disabled', event: 'runtime.feature_disabled',
feature: 'household-setup' feature: 'household-setup'
}, },
'Household setup commands are disabled. Set DATABASE_URL to enable.' 'Household setup commands are disabled. Set WORKER_DATABASE_URL to enable.'
) )
} }
const reminderJobs = runtime.reminderJobsEnabled const reminderJobs = runtime.reminderJobsEnabled
? (() => { ? (() => {
const reminderRepositoryClient = createDbReminderDispatchRepository(runtime.databaseUrl!) const reminderRepositoryClient = createDbReminderDispatchRepository(
runtime.workerDatabaseUrl!
)
const reminderService = createReminderJobService(reminderRepositoryClient.repository) const reminderService = createReminderJobService(reminderRepositoryClient.repository)
shutdownTasks.push(reminderRepositoryClient.close) shutdownTasks.push(reminderRepositoryClient.close)
return createReminderJobsHandler({ return createReminderJobsHandler({
listReminderTargets: () => listReminderTargets: () =>
householdConfigurationRepositoryClient!.repository.listReminderTargets(), workerHouseholdConfigurationRepositoryClient!.repository.listReminderTargets(),
ensureBillingCycle: async ({ householdId, at }) => { ensureBillingCycle: async ({ householdId, at }) => {
await financeServiceForHousehold(householdId).ensureExpectedCycle(at) await financeServiceForHousehold(householdId).ensureExpectedCycle(at)
}, },
@@ -426,19 +590,19 @@ if (!runtime.reminderJobsEnabled) {
event: 'runtime.feature_disabled', event: 'runtime.feature_disabled',
feature: 'reminder-jobs' feature: 'reminder-jobs'
}, },
'Reminder jobs are disabled. Set DATABASE_URL and either SCHEDULER_SHARED_SECRET or SCHEDULER_OIDC_ALLOWED_EMAILS to enable.' 'Reminder jobs are disabled. Set WORKER_DATABASE_URL and either SCHEDULER_SHARED_SECRET or SCHEDULER_OIDC_ALLOWED_EMAILS to enable.'
) )
} }
if ( if (
runtime.anonymousFeedbackEnabled && runtime.anonymousFeedbackEnabled &&
householdConfigurationRepositoryClient && workerHouseholdConfigurationRepositoryClient &&
telegramPendingActionRepositoryClient telegramPendingActionRepositoryClient
) { ) {
registerAnonymousFeedback({ registerAnonymousFeedback({
bot, bot,
anonymousFeedbackServiceForHousehold, anonymousFeedbackServiceForHousehold,
householdConfigurationRepository: householdConfigurationRepositoryClient!.repository, householdConfigurationRepository: workerHouseholdConfigurationRepositoryClient!.repository,
promptRepository: telegramPendingActionRepositoryClient!.repository, promptRepository: telegramPendingActionRepositoryClient!.repository,
logger: getLogger('anonymous-feedback') logger: getLogger('anonymous-feedback')
}) })
@@ -448,19 +612,19 @@ if (
event: 'runtime.feature_disabled', event: 'runtime.feature_disabled',
feature: 'anonymous-feedback' feature: 'anonymous-feedback'
}, },
'Anonymous feedback is disabled. Set DATABASE_URL to enable household and topic lookups.' 'Anonymous feedback is disabled. Set WORKER_DATABASE_URL to enable household and topic lookups.'
) )
} }
if ( if (
runtime.assistantEnabled && runtime.assistantEnabled &&
householdConfigurationRepositoryClient && workerHouseholdConfigurationRepositoryClient &&
telegramPendingActionRepositoryClient telegramPendingActionRepositoryClient
) { ) {
if (processedBotMessageRepositoryClient) { if (processedBotMessageRepositoryClient) {
registerDmAssistant({ registerDmAssistant({
bot, bot,
householdConfigurationRepository: householdConfigurationRepositoryClient.repository, householdConfigurationRepository: workerHouseholdConfigurationRepositoryClient.repository,
messageProcessingRepository: processedBotMessageRepositoryClient.repository, messageProcessingRepository: processedBotMessageRepositoryClient.repository,
promptRepository: telegramPendingActionRepositoryClient.repository, promptRepository: telegramPendingActionRepositoryClient.repository,
financeServiceForHousehold, financeServiceForHousehold,
@@ -492,7 +656,7 @@ if (
} else { } else {
registerDmAssistant({ registerDmAssistant({
bot, bot,
householdConfigurationRepository: householdConfigurationRepositoryClient.repository, householdConfigurationRepository: workerHouseholdConfigurationRepositoryClient.repository,
promptRepository: telegramPendingActionRepositoryClient.repository, promptRepository: telegramPendingActionRepositoryClient.repository,
financeServiceForHousehold, financeServiceForHousehold,
memoryStore: assistantMemoryStore, memoryStore: assistantMemoryStore,
@@ -523,10 +687,10 @@ if (
} }
} }
if (householdConfigurationRepositoryClient && telegramPendingActionRepositoryClient) { if (workerHouseholdConfigurationRepositoryClient && telegramPendingActionRepositoryClient) {
registerReminderTopicUtilities({ registerReminderTopicUtilities({
bot, bot,
householdConfigurationRepository: householdConfigurationRepositoryClient.repository, householdConfigurationRepository: workerHouseholdConfigurationRepositoryClient.repository,
promptRepository: telegramPendingActionRepositoryClient.repository, promptRepository: telegramPendingActionRepositoryClient.repository,
financeServiceForHousehold, financeServiceForHousehold,
logger: getLogger('reminder-utilities') logger: getLogger('reminder-utilities')
@@ -537,272 +701,272 @@ const server = createBotWebhookServer({
webhookPath: runtime.telegramWebhookPath, webhookPath: runtime.telegramWebhookPath,
webhookSecret: runtime.telegramWebhookSecret, webhookSecret: runtime.telegramWebhookSecret,
webhookHandler, webhookHandler,
miniAppAuth: householdOnboardingService miniAppAuth: runtime.miniAppAuthEnabled
? createMiniAppAuthHandler({ ? createMiniAppAuthHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
logger: getLogger('miniapp-auth') logger: getLogger('miniapp-auth')
}) })
: undefined, : undefined,
miniAppJoin: householdOnboardingService miniAppJoin: runtime.miniAppAuthEnabled
? createMiniAppJoinHandler({ ? createMiniAppJoinHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
logger: getLogger('miniapp-auth') logger: getLogger('miniapp-auth')
}) })
: undefined, : undefined,
miniAppDashboard: householdOnboardingService miniAppDashboard: runtime.miniAppAuthEnabled
? createMiniAppDashboardHandler({ ? createMiniAppDashboardHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
financeServiceForHousehold, financeServiceForSession: appFinanceServiceForSession,
onboardingService: householdOnboardingService!, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
logger: getLogger('miniapp-dashboard') logger: getLogger('miniapp-dashboard')
}) })
: undefined, : undefined,
miniAppPendingMembers: householdOnboardingService miniAppPendingMembers: runtime.miniAppAuthEnabled
? createMiniAppPendingMembersHandler({ ? createMiniAppPendingMembersHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminService: miniAppAdminService!, miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin') logger: getLogger('miniapp-admin')
}) })
: undefined, : undefined,
miniAppApproveMember: householdOnboardingService miniAppApproveMember: runtime.miniAppAuthEnabled
? createMiniAppApproveMemberHandler({ ? createMiniAppApproveMemberHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminService: miniAppAdminService!, miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin') logger: getLogger('miniapp-admin')
}) })
: undefined, : undefined,
miniAppRejectMember: householdOnboardingService miniAppRejectMember: runtime.miniAppAuthEnabled
? createMiniAppRejectMemberHandler({ ? createMiniAppRejectMemberHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminService: miniAppAdminService!, miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin') logger: getLogger('miniapp-admin')
}) })
: undefined, : undefined,
miniAppSettings: householdOnboardingService miniAppSettings: runtime.miniAppAuthEnabled
? createMiniAppSettingsHandler({ ? createMiniAppSettingsHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminService: miniAppAdminService!, miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
assistantUsageTracker, assistantUsageTracker,
logger: getLogger('miniapp-admin') logger: getLogger('miniapp-admin')
}) })
: undefined, : undefined,
miniAppUpdateSettings: householdOnboardingService miniAppUpdateSettings: runtime.miniAppAuthEnabled
? createMiniAppUpdateSettingsHandler({ ? createMiniAppUpdateSettingsHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminService: miniAppAdminService!, miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin') logger: getLogger('miniapp-admin')
}) })
: undefined, : undefined,
miniAppUpsertUtilityCategory: householdOnboardingService miniAppUpsertUtilityCategory: runtime.miniAppAuthEnabled
? createMiniAppUpsertUtilityCategoryHandler({ ? createMiniAppUpsertUtilityCategoryHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminService: miniAppAdminService!, miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin') logger: getLogger('miniapp-admin')
}) })
: undefined, : undefined,
miniAppPromoteMember: householdOnboardingService miniAppPromoteMember: runtime.miniAppAuthEnabled
? createMiniAppPromoteMemberHandler({ ? createMiniAppPromoteMemberHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminService: miniAppAdminService!, miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin') logger: getLogger('miniapp-admin')
}) })
: undefined, : undefined,
miniAppUpdateOwnDisplayName: householdOnboardingService miniAppUpdateOwnDisplayName: runtime.miniAppAuthEnabled
? createMiniAppUpdateOwnDisplayNameHandler({ ? createMiniAppUpdateOwnDisplayNameHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminService: miniAppAdminService!, miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin') logger: getLogger('miniapp-admin')
}) })
: undefined, : undefined,
miniAppUpdateMemberDisplayName: householdOnboardingService miniAppUpdateMemberDisplayName: runtime.miniAppAuthEnabled
? createMiniAppUpdateMemberDisplayNameHandler({ ? createMiniAppUpdateMemberDisplayNameHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminService: miniAppAdminService!, miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin') logger: getLogger('miniapp-admin')
}) })
: undefined, : undefined,
miniAppUpdateMemberRentWeight: householdOnboardingService miniAppUpdateMemberRentWeight: runtime.miniAppAuthEnabled
? createMiniAppUpdateMemberRentWeightHandler({ ? createMiniAppUpdateMemberRentWeightHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminService: miniAppAdminService!, miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin') logger: getLogger('miniapp-admin')
}) })
: undefined, : undefined,
miniAppUpdateMemberStatus: householdOnboardingService miniAppUpdateMemberStatus: runtime.miniAppAuthEnabled
? createMiniAppUpdateMemberStatusHandler({ ? createMiniAppUpdateMemberStatusHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminService: miniAppAdminService!, miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin') logger: getLogger('miniapp-admin')
}) })
: undefined, : undefined,
miniAppUpdateMemberAbsencePolicy: householdOnboardingService miniAppUpdateMemberAbsencePolicy: runtime.miniAppAuthEnabled
? createMiniAppUpdateMemberAbsencePolicyHandler({ ? createMiniAppUpdateMemberAbsencePolicyHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminService: miniAppAdminService!, miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin') logger: getLogger('miniapp-admin')
}) })
: undefined, : undefined,
miniAppBillingCycle: householdOnboardingService miniAppBillingCycle: runtime.miniAppAuthEnabled
? createMiniAppBillingCycleHandler({ ? createMiniAppBillingCycleHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForHousehold, financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing') logger: getLogger('miniapp-billing')
}) })
: undefined, : undefined,
miniAppOpenCycle: householdOnboardingService miniAppOpenCycle: runtime.miniAppAuthEnabled
? createMiniAppOpenCycleHandler({ ? createMiniAppOpenCycleHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForHousehold, financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing') logger: getLogger('miniapp-billing')
}) })
: undefined, : undefined,
miniAppCloseCycle: householdOnboardingService miniAppCloseCycle: runtime.miniAppAuthEnabled
? createMiniAppCloseCycleHandler({ ? createMiniAppCloseCycleHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForHousehold, financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing') logger: getLogger('miniapp-billing')
}) })
: undefined, : undefined,
miniAppRentUpdate: householdOnboardingService miniAppRentUpdate: runtime.miniAppAuthEnabled
? createMiniAppRentUpdateHandler({ ? createMiniAppRentUpdateHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForHousehold, financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing') logger: getLogger('miniapp-billing')
}) })
: undefined, : undefined,
miniAppAddUtilityBill: householdOnboardingService miniAppAddUtilityBill: runtime.miniAppAuthEnabled
? createMiniAppAddUtilityBillHandler({ ? createMiniAppAddUtilityBillHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForHousehold, financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing') logger: getLogger('miniapp-billing')
}) })
: undefined, : undefined,
miniAppSubmitUtilityBill: householdOnboardingService miniAppSubmitUtilityBill: runtime.miniAppAuthEnabled
? createMiniAppSubmitUtilityBillHandler({ ? createMiniAppSubmitUtilityBillHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForHousehold, financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing') logger: getLogger('miniapp-billing')
}) })
: undefined, : undefined,
miniAppUpdateUtilityBill: householdOnboardingService miniAppUpdateUtilityBill: runtime.miniAppAuthEnabled
? createMiniAppUpdateUtilityBillHandler({ ? createMiniAppUpdateUtilityBillHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForHousehold, financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing') logger: getLogger('miniapp-billing')
}) })
: undefined, : undefined,
miniAppDeleteUtilityBill: householdOnboardingService miniAppDeleteUtilityBill: runtime.miniAppAuthEnabled
? createMiniAppDeleteUtilityBillHandler({ ? createMiniAppDeleteUtilityBillHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForHousehold, financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing') logger: getLogger('miniapp-billing')
}) })
: undefined, : undefined,
miniAppAddPurchase: householdOnboardingService miniAppAddPurchase: runtime.miniAppAuthEnabled
? createMiniAppAddPurchaseHandler({ ? createMiniAppAddPurchaseHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForHousehold, financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing') logger: getLogger('miniapp-billing')
}) })
: undefined, : undefined,
miniAppUpdatePurchase: householdOnboardingService miniAppUpdatePurchase: runtime.miniAppAuthEnabled
? createMiniAppUpdatePurchaseHandler({ ? createMiniAppUpdatePurchaseHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForHousehold, financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing') logger: getLogger('miniapp-billing')
}) })
: undefined, : undefined,
miniAppDeletePurchase: householdOnboardingService miniAppDeletePurchase: runtime.miniAppAuthEnabled
? createMiniAppDeletePurchaseHandler({ ? createMiniAppDeletePurchaseHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForHousehold, financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing') logger: getLogger('miniapp-billing')
}) })
: undefined, : undefined,
miniAppAddPayment: householdOnboardingService miniAppAddPayment: runtime.miniAppAuthEnabled
? createMiniAppAddPaymentHandler({ ? createMiniAppAddPaymentHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForHousehold, financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing') logger: getLogger('miniapp-billing')
}) })
: undefined, : undefined,
miniAppUpdatePayment: householdOnboardingService miniAppUpdatePayment: runtime.miniAppAuthEnabled
? createMiniAppUpdatePaymentHandler({ ? createMiniAppUpdatePaymentHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForHousehold, financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing') logger: getLogger('miniapp-billing')
}) })
: undefined, : undefined,
miniAppDeletePayment: householdOnboardingService miniAppDeletePayment: runtime.miniAppAuthEnabled
? createMiniAppDeletePaymentHandler({ ? createMiniAppDeletePaymentHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForHousehold, financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing') logger: getLogger('miniapp-billing')
}) })
: undefined, : undefined,
miniAppLocalePreference: householdOnboardingService miniAppLocalePreference: runtime.miniAppAuthEnabled
? createMiniAppLocalePreferenceHandler({ ? createMiniAppLocalePreferenceHandler({
allowedOrigins: runtime.miniAppAllowedOrigins, allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken, botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService, onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
localePreferenceService: localePreferenceService!, localePreferenceServiceForSession: appLocalePreferenceServiceForSession,
logger: getLogger('miniapp-admin') logger: getLogger('miniapp-admin')
}) })
: undefined, : undefined,

View File

@@ -12,12 +12,59 @@ import type { AssistantUsageTracker } from './dm-assistant'
import { import {
allowedMiniAppOrigin, allowedMiniAppOrigin,
type MiniAppAuthorizedSession,
createMiniAppSessionService, createMiniAppSessionService,
miniAppErrorResponse, miniAppErrorResponse,
miniAppJsonResponse, miniAppJsonResponse,
readMiniAppRequestPayload readMiniAppRequestPayload
} from './miniapp-auth' } from './miniapp-auth'
interface MiniAppAdminHandlerBaseOptions {
allowedOrigins: readonly string[]
botToken: string
onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
miniAppAdminServiceForSession?: (session: MiniAppAuthorizedSession) => MiniAppAdminService
miniAppAdminService?: MiniAppAdminService
logger?: Logger
}
function createConfiguredMiniAppSessionService(options: {
botToken: string
onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
}) {
return createMiniAppSessionService({
botToken: options.botToken,
...(options.onboardingServiceForTelegramUserId
? {
onboardingServiceForTelegramUserId: options.onboardingServiceForTelegramUserId
}
: {}),
...(options.onboardingService
? {
onboardingService: options.onboardingService
}
: {})
})
}
function resolveMiniAppAdminService(
options: Pick<
MiniAppAdminHandlerBaseOptions,
'miniAppAdminServiceForSession' | 'miniAppAdminService'
>,
session: MiniAppAuthorizedSession
): MiniAppAdminService {
const service = options.miniAppAdminServiceForSession?.(session) ?? options.miniAppAdminService
if (!service) {
throw new Error('Mini app admin service is not configured')
}
return service
}
async function readApprovalPayload(request: Request): Promise<{ async function readApprovalPayload(request: Request): Promise<{
initData: string initData: string
pendingTelegramUserId: string pendingTelegramUserId: string
@@ -401,6 +448,7 @@ async function authenticateAdminSession(
| Response | Response
| { | {
member: NonNullable<MiniAppSessionResult['member']> member: NonNullable<MiniAppSessionResult['member']>
telegramUserId: string
} }
> { > {
const payload = await readMiniAppRequestPayload(request) const payload = await readMiniAppRequestPayload(request)
@@ -413,7 +461,7 @@ async function authenticateAdminSession(
return miniAppJsonResponse({ ok: false, error: 'Invalid Telegram init data' }, 401, origin) return miniAppJsonResponse({ ok: false, error: 'Invalid Telegram init data' }, 401, origin)
} }
if (!session.authorized || !session.member) { if (!session.authorized || !session.member || !session.telegramUser) {
return miniAppJsonResponse( return miniAppJsonResponse(
{ ok: false, error: 'Admin access required for active household members' }, { ok: false, error: 'Admin access required for active household members' },
403, 403,
@@ -430,23 +478,15 @@ async function authenticateAdminSession(
} }
return { return {
member: session.member member: session.member,
telegramUserId: session.telegramUser.id
} }
} }
export function createMiniAppPendingMembersHandler(options: { export function createMiniAppPendingMembersHandler(options: MiniAppAdminHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -478,6 +518,7 @@ export function createMiniAppPendingMembersHandler(options: {
if ( if (
!session.authorized || !session.authorized ||
!session.member || !session.member ||
!session.telegramUser ||
session.member.status !== 'active' || session.member.status !== 'active' ||
!session.member.isAdmin !session.member.isAdmin
) { ) {
@@ -488,7 +529,10 @@ export function createMiniAppPendingMembersHandler(options: {
) )
} }
const result = await options.miniAppAdminService.listPendingMembers({ const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).listPendingMembers({
householdId: session.member.householdId, householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin actorIsAdmin: session.member.isAdmin
}) })
@@ -513,20 +557,14 @@ export function createMiniAppPendingMembersHandler(options: {
} }
} }
export function createMiniAppSettingsHandler(options: { export function createMiniAppSettingsHandler(
allowedOrigins: readonly string[] options: MiniAppAdminHandlerBaseOptions & {
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
assistantUsageTracker?: AssistantUsageTracker assistantUsageTracker?: AssistantUsageTracker
logger?: Logger }
}): { ): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -545,9 +583,12 @@ export function createMiniAppSettingsHandler(options: {
if (auth instanceof Response) { if (auth instanceof Response) {
return auth return auth
} }
const { member } = auth const { member, telegramUserId } = auth
const result = await options.miniAppAdminService.getSettings({ const result = await resolveMiniAppAdminService(options, {
member,
telegramUserId
}).getSettings({
householdId: member.householdId, householdId: member.householdId,
actorIsAdmin: member.isAdmin actorIsAdmin: member.isAdmin
}) })
@@ -580,19 +621,10 @@ export function createMiniAppSettingsHandler(options: {
} }
} }
export function createMiniAppUpdateSettingsHandler(options: { export function createMiniAppUpdateSettingsHandler(options: MiniAppAdminHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -623,6 +655,7 @@ export function createMiniAppUpdateSettingsHandler(options: {
if ( if (
!session.authorized || !session.authorized ||
!session.member || !session.member ||
!session.telegramUser ||
session.member.status !== 'active' || session.member.status !== 'active' ||
!session.member.isAdmin !session.member.isAdmin
) { ) {
@@ -633,7 +666,10 @@ export function createMiniAppUpdateSettingsHandler(options: {
) )
} }
const result = await options.miniAppAdminService.updateSettings({ const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).updateSettings({
householdId: session.member.householdId, householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin, actorIsAdmin: session.member.isAdmin,
...(payload.householdName !== undefined ...(payload.householdName !== undefined
@@ -715,19 +751,12 @@ export function createMiniAppUpdateSettingsHandler(options: {
} }
} }
export function createMiniAppUpsertUtilityCategoryHandler(options: { export function createMiniAppUpsertUtilityCategoryHandler(
allowedOrigins: readonly string[] options: MiniAppAdminHandlerBaseOptions
botToken: string ): {
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -758,6 +787,7 @@ export function createMiniAppUpsertUtilityCategoryHandler(options: {
if ( if (
!session.authorized || !session.authorized ||
!session.member || !session.member ||
!session.telegramUser ||
session.member.status !== 'active' || session.member.status !== 'active' ||
!session.member.isAdmin !session.member.isAdmin
) { ) {
@@ -768,7 +798,10 @@ export function createMiniAppUpsertUtilityCategoryHandler(options: {
) )
} }
const result = await options.miniAppAdminService.upsertUtilityCategory({ const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).upsertUtilityCategory({
householdId: session.member.householdId, householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin, actorIsAdmin: session.member.isAdmin,
...(payload.slug ...(payload.slug
@@ -811,19 +844,10 @@ export function createMiniAppUpsertUtilityCategoryHandler(options: {
} }
} }
export function createMiniAppPromoteMemberHandler(options: { export function createMiniAppPromoteMemberHandler(options: MiniAppAdminHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -854,6 +878,7 @@ export function createMiniAppPromoteMemberHandler(options: {
if ( if (
!session.authorized || !session.authorized ||
!session.member || !session.member ||
!session.telegramUser ||
session.member.status !== 'active' || session.member.status !== 'active' ||
!session.member.isAdmin !session.member.isAdmin
) { ) {
@@ -864,7 +889,10 @@ export function createMiniAppPromoteMemberHandler(options: {
) )
} }
const result = await options.miniAppAdminService.promoteMemberToAdmin({ const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).promoteMemberToAdmin({
householdId: session.member.householdId, householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin, actorIsAdmin: session.member.isAdmin,
memberId: payload.memberId memberId: payload.memberId
@@ -898,19 +926,10 @@ export function createMiniAppPromoteMemberHandler(options: {
} }
} }
export function createMiniAppUpdateOwnDisplayNameHandler(options: { export function createMiniAppUpdateOwnDisplayNameHandler(options: MiniAppAdminHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -930,7 +949,7 @@ export function createMiniAppUpdateOwnDisplayNameHandler(options: {
initData: payload.initData initData: payload.initData
}) })
if (!session || !session.authorized || !session.member) { if (!session || !session.authorized || !session.member || !session.telegramUser) {
return miniAppJsonResponse( return miniAppJsonResponse(
{ ok: false, error: 'Active household membership required' }, { ok: false, error: 'Active household membership required' },
session ? 403 : 401, session ? 403 : 401,
@@ -938,7 +957,10 @@ export function createMiniAppUpdateOwnDisplayNameHandler(options: {
) )
} }
const result = await options.miniAppAdminService.updateOwnDisplayName({ const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).updateOwnDisplayName({
householdId: session.member.householdId, householdId: session.member.householdId,
actorMemberId: session.member.id, actorMemberId: session.member.id,
displayName: payload.displayName displayName: payload.displayName
@@ -974,19 +996,12 @@ export function createMiniAppUpdateOwnDisplayNameHandler(options: {
} }
} }
export function createMiniAppUpdateMemberRentWeightHandler(options: { export function createMiniAppUpdateMemberRentWeightHandler(
allowedOrigins: readonly string[] options: MiniAppAdminHandlerBaseOptions
botToken: string ): {
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -1017,6 +1032,7 @@ export function createMiniAppUpdateMemberRentWeightHandler(options: {
if ( if (
!session.authorized || !session.authorized ||
!session.member || !session.member ||
!session.telegramUser ||
session.member.status !== 'active' || session.member.status !== 'active' ||
!session.member.isAdmin !session.member.isAdmin
) { ) {
@@ -1027,7 +1043,10 @@ export function createMiniAppUpdateMemberRentWeightHandler(options: {
) )
} }
const result = await options.miniAppAdminService.updateMemberRentShareWeight({ const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).updateMemberRentShareWeight({
householdId: session.member.householdId, householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin, actorIsAdmin: session.member.isAdmin,
memberId: payload.memberId, memberId: payload.memberId,
@@ -1070,19 +1089,12 @@ export function createMiniAppUpdateMemberRentWeightHandler(options: {
} }
} }
export function createMiniAppUpdateMemberDisplayNameHandler(options: { export function createMiniAppUpdateMemberDisplayNameHandler(
allowedOrigins: readonly string[] options: MiniAppAdminHandlerBaseOptions
botToken: string ): {
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -1106,6 +1118,7 @@ export function createMiniAppUpdateMemberDisplayNameHandler(options: {
!session || !session ||
!session.authorized || !session.authorized ||
!session.member || !session.member ||
!session.telegramUser ||
session.member.status !== 'active' || session.member.status !== 'active' ||
!session.member.isAdmin !session.member.isAdmin
) { ) {
@@ -1120,7 +1133,10 @@ export function createMiniAppUpdateMemberDisplayNameHandler(options: {
return miniAppJsonResponse({ ok: false, error: 'Missing memberId' }, 400, origin) return miniAppJsonResponse({ ok: false, error: 'Missing memberId' }, 400, origin)
} }
const result = await options.miniAppAdminService.updateMemberDisplayName({ const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).updateMemberDisplayName({
householdId: session.member.householdId, householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin, actorIsAdmin: session.member.isAdmin,
memberId: payload.memberId, memberId: payload.memberId,
@@ -1163,19 +1179,10 @@ export function createMiniAppUpdateMemberDisplayNameHandler(options: {
} }
} }
export function createMiniAppUpdateMemberStatusHandler(options: { export function createMiniAppUpdateMemberStatusHandler(options: MiniAppAdminHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -1199,6 +1206,7 @@ export function createMiniAppUpdateMemberStatusHandler(options: {
!session || !session ||
!session.authorized || !session.authorized ||
!session.member || !session.member ||
!session.telegramUser ||
session.member.status !== 'active' || session.member.status !== 'active' ||
!session.member.isAdmin !session.member.isAdmin
) { ) {
@@ -1209,7 +1217,10 @@ export function createMiniAppUpdateMemberStatusHandler(options: {
) )
} }
const result = await options.miniAppAdminService.updateMemberStatus({ const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).updateMemberStatus({
householdId: session.member.householdId, householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin, actorIsAdmin: session.member.isAdmin,
memberId: payload.memberId, memberId: payload.memberId,
@@ -1244,19 +1255,12 @@ export function createMiniAppUpdateMemberStatusHandler(options: {
} }
} }
export function createMiniAppUpdateMemberAbsencePolicyHandler(options: { export function createMiniAppUpdateMemberAbsencePolicyHandler(
allowedOrigins: readonly string[] options: MiniAppAdminHandlerBaseOptions
botToken: string ): {
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -1280,6 +1284,7 @@ export function createMiniAppUpdateMemberAbsencePolicyHandler(options: {
!session || !session ||
!session.authorized || !session.authorized ||
!session.member || !session.member ||
!session.telegramUser ||
session.member.status !== 'active' || session.member.status !== 'active' ||
!session.member.isAdmin !session.member.isAdmin
) { ) {
@@ -1290,7 +1295,10 @@ export function createMiniAppUpdateMemberAbsencePolicyHandler(options: {
) )
} }
const result = await options.miniAppAdminService.updateMemberAbsencePolicy({ const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).updateMemberAbsencePolicy({
householdId: session.member.householdId, householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin, actorIsAdmin: session.member.isAdmin,
memberId: payload.memberId, memberId: payload.memberId,
@@ -1325,19 +1333,10 @@ export function createMiniAppUpdateMemberAbsencePolicyHandler(options: {
} }
} }
export function createMiniAppApproveMemberHandler(options: { export function createMiniAppApproveMemberHandler(options: MiniAppAdminHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -1368,6 +1367,7 @@ export function createMiniAppApproveMemberHandler(options: {
if ( if (
!session.authorized || !session.authorized ||
!session.member || !session.member ||
!session.telegramUser ||
session.member.status !== 'active' || session.member.status !== 'active' ||
!session.member.isAdmin !session.member.isAdmin
) { ) {
@@ -1378,7 +1378,10 @@ export function createMiniAppApproveMemberHandler(options: {
) )
} }
const result = await options.miniAppAdminService.approvePendingMember({ const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).approvePendingMember({
householdId: session.member.householdId, householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin, actorIsAdmin: session.member.isAdmin,
pendingTelegramUserId: payload.pendingTelegramUserId pendingTelegramUserId: payload.pendingTelegramUserId
@@ -1410,19 +1413,10 @@ export function createMiniAppApproveMemberHandler(options: {
} }
} }
export function createMiniAppRejectMemberHandler(options: { export function createMiniAppRejectMemberHandler(options: MiniAppAdminHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -1453,6 +1447,7 @@ export function createMiniAppRejectMemberHandler(options: {
if ( if (
!session.authorized || !session.authorized ||
!session.member || !session.member ||
!session.telegramUser ||
session.member.status !== 'active' || session.member.status !== 'active' ||
!session.member.isAdmin !session.member.isAdmin
) { ) {
@@ -1463,7 +1458,10 @@ export function createMiniAppRejectMemberHandler(options: {
) )
} }
const result = await options.miniAppAdminService.rejectPendingMember({ const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).rejectPendingMember({
householdId: session.member.householdId, householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin, actorIsAdmin: session.member.isAdmin,
pendingTelegramUserId: payload.pendingTelegramUserId pendingTelegramUserId: payload.pendingTelegramUserId

View File

@@ -115,12 +115,28 @@ export interface MiniAppSessionResult {
} }
} }
export interface MiniAppAuthorizedSession {
member: NonNullable<MiniAppSessionResult['member']>
telegramUserId: string
}
export function createMiniAppSessionService(options: { export function createMiniAppSessionService(options: {
botToken: string botToken: string
onboardingService: HouseholdOnboardingService onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
}): { }): {
authenticate: (payload: MiniAppRequestPayload) => Promise<MiniAppSessionResult | null> authenticate: (payload: MiniAppRequestPayload) => Promise<MiniAppSessionResult | null>
} { } {
const resolveOnboardingService =
options.onboardingServiceForTelegramUserId ??
(() => {
if (!options.onboardingService) {
throw new Error('Mini app onboarding service is not configured')
}
return options.onboardingService
})
return { return {
authenticate: async (payload) => { authenticate: async (payload) => {
if (!payload.initData) { if (!payload.initData) {
@@ -132,7 +148,7 @@ export function createMiniAppSessionService(options: {
return null return null
} }
const access = await options.onboardingService.getMiniAppAccess({ const access = await resolveOnboardingService(telegramUser.id).getMiniAppAccess({
identity: { identity: {
telegramUserId: telegramUser.id, telegramUserId: telegramUser.id,
displayName: displayName:
@@ -190,14 +206,24 @@ export function createMiniAppSessionService(options: {
export function createMiniAppAuthHandler(options: { export function createMiniAppAuthHandler(options: {
allowedOrigins: readonly string[] allowedOrigins: readonly string[]
botToken: string botToken: string
onboardingService: HouseholdOnboardingService onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
logger?: Logger logger?: Logger
}): { }): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createMiniAppSessionService({
botToken: options.botToken, botToken: options.botToken,
...(options.onboardingServiceForTelegramUserId
? {
onboardingServiceForTelegramUserId: options.onboardingServiceForTelegramUserId
}
: {}),
...(options.onboardingService
? {
onboardingService: options.onboardingService onboardingService: options.onboardingService
}
: {})
}) })
return { return {
@@ -264,7 +290,8 @@ export function createMiniAppAuthHandler(options: {
export function createMiniAppJoinHandler(options: { export function createMiniAppJoinHandler(options: {
allowedOrigins: readonly string[] allowedOrigins: readonly string[]
botToken: string botToken: string
onboardingService: HouseholdOnboardingService onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
logger?: Logger logger?: Logger
}): { }): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
@@ -304,7 +331,13 @@ export function createMiniAppJoinHandler(options: {
) )
} }
const result = await options.onboardingService.joinHousehold({ const onboardingService =
options.onboardingServiceForTelegramUserId?.(telegramUser.id) ?? options.onboardingService
if (!onboardingService) {
throw new Error('Mini app onboarding service is not configured')
}
const result = await onboardingService.joinHousehold({
identity: { identity: {
telegramUserId: telegramUser.id, telegramUserId: telegramUser.id,
displayName: displayName:

View File

@@ -2,7 +2,7 @@ import type { FinanceCommandService, HouseholdOnboardingService } from '@househo
import { BillingPeriod } from '@household/domain' import { BillingPeriod } from '@household/domain'
import type { Logger } from '@household/observability' import type { Logger } from '@household/observability'
import type { HouseholdConfigurationRepository } from '@household/ports' import type { HouseholdConfigurationRepository } from '@household/ports'
import type { MiniAppSessionResult } from './miniapp-auth' import type { MiniAppAuthorizedSession, MiniAppSessionResult } from './miniapp-auth'
import { import {
allowedMiniAppOrigin, allowedMiniAppOrigin,
@@ -12,6 +12,54 @@ import {
readMiniAppRequestPayload readMiniAppRequestPayload
} from './miniapp-auth' } from './miniapp-auth'
interface MiniAppBillingHandlerBaseOptions {
allowedOrigins: readonly string[]
botToken: string
financeServiceForSession?: (session: MiniAppAuthorizedSession) => FinanceCommandService
financeServiceForHousehold?: (householdId: string) => FinanceCommandService
onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
logger?: Logger
}
function createConfiguredMiniAppBillingSessionService(options: {
botToken: string
onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
}) {
return createMiniAppSessionService({
botToken: options.botToken,
...(options.onboardingServiceForTelegramUserId
? {
onboardingServiceForTelegramUserId: options.onboardingServiceForTelegramUserId
}
: {}),
...(options.onboardingService
? {
onboardingService: options.onboardingService
}
: {})
})
}
function resolveFinanceService(
options: Pick<
MiniAppBillingHandlerBaseOptions,
'financeServiceForSession' | 'financeServiceForHousehold'
>,
session: MiniAppAuthorizedSession
): FinanceCommandService {
const service =
options.financeServiceForSession?.(session) ??
options.financeServiceForHousehold?.(session.member.householdId)
if (!service) {
throw new Error('Mini app finance service is not configured')
}
return service
}
function serializeCycleState( function serializeCycleState(
state: Awaited<ReturnType<FinanceCommandService['getAdminCycleState']>> state: Awaited<ReturnType<FinanceCommandService['getAdminCycleState']>>
) { ) {
@@ -42,6 +90,7 @@ async function authenticateAdminSession(
| Response | Response
| { | {
member: NonNullable<MiniAppSessionResult['member']> member: NonNullable<MiniAppSessionResult['member']>
telegramUserId: string
} }
> { > {
const payload = await readMiniAppRequestPayload(request) const payload = await readMiniAppRequestPayload(request)
@@ -54,7 +103,7 @@ async function authenticateAdminSession(
return miniAppJsonResponse({ ok: false, error: 'Invalid Telegram init data' }, 401, origin) return miniAppJsonResponse({ ok: false, error: 'Invalid Telegram init data' }, 401, origin)
} }
if (!session.authorized || !session.member) { if (!session.authorized || !session.member || !session.telegramUser) {
return miniAppJsonResponse( return miniAppJsonResponse(
{ ok: false, error: 'Access limited to active household members' }, { ok: false, error: 'Access limited to active household members' },
403, 403,
@@ -67,7 +116,8 @@ async function authenticateAdminSession(
} }
return { return {
member: session.member member: session.member,
telegramUserId: session.telegramUser.id
} }
} }
@@ -79,6 +129,7 @@ async function authenticateMemberSession(
| Response | Response
| { | {
member: NonNullable<MiniAppSessionResult['member']> member: NonNullable<MiniAppSessionResult['member']>
telegramUserId: string
} }
> { > {
const payload = await readMiniAppRequestPayload(request) const payload = await readMiniAppRequestPayload(request)
@@ -91,7 +142,12 @@ async function authenticateMemberSession(
return miniAppJsonResponse({ ok: false, error: 'Invalid Telegram init data' }, 401, origin) return miniAppJsonResponse({ ok: false, error: 'Invalid Telegram init data' }, 401, origin)
} }
if (!session.authorized || !session.member || session.member.status !== 'active') { if (
!session.authorized ||
!session.member ||
!session.telegramUser ||
session.member.status !== 'active'
) {
return miniAppJsonResponse( return miniAppJsonResponse(
{ ok: false, error: 'Access limited to active household members' }, { ok: false, error: 'Access limited to active household members' },
403, 403,
@@ -100,7 +156,8 @@ async function authenticateMemberSession(
} }
return { return {
member: session.member member: session.member,
telegramUserId: session.telegramUser.id
} }
} }
@@ -530,19 +587,10 @@ async function readPaymentMutationPayload(request: Request): Promise<{
} }
} }
export function createMiniAppBillingCycleHandler(options: { export function createMiniAppBillingCycleHandler(options: MiniAppBillingHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppBillingSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -567,9 +615,9 @@ export function createMiniAppBillingCycleHandler(options: {
} }
const payload = await readCycleQueryPayload(request) const payload = await readCycleQueryPayload(request)
const cycleState = await options const cycleState = await resolveFinanceService(options, auth).getAdminCycleState(
.financeServiceForHousehold(auth.member.householdId) payload.period
.getAdminCycleState(payload.period) )
return miniAppJsonResponse( return miniAppJsonResponse(
{ {
@@ -587,19 +635,10 @@ export function createMiniAppBillingCycleHandler(options: {
} }
} }
export function createMiniAppOpenCycleHandler(options: { export function createMiniAppOpenCycleHandler(options: MiniAppBillingHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppBillingSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -624,7 +663,7 @@ export function createMiniAppOpenCycleHandler(options: {
} }
const payload = await readOpenCyclePayload(request) const payload = await readOpenCyclePayload(request)
const service = options.financeServiceForHousehold(auth.member.householdId) const service = resolveFinanceService(options, auth)
await service.openCycle(payload.period, payload.currency) await service.openCycle(payload.period, payload.currency)
const cycleState = await service.getAdminCycleState(payload.period) const cycleState = await service.getAdminCycleState(payload.period)
@@ -644,19 +683,10 @@ export function createMiniAppOpenCycleHandler(options: {
} }
} }
export function createMiniAppCloseCycleHandler(options: { export function createMiniAppCloseCycleHandler(options: MiniAppBillingHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppBillingSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -681,7 +711,7 @@ export function createMiniAppCloseCycleHandler(options: {
} }
const payload = await readCycleQueryPayload(request) const payload = await readCycleQueryPayload(request)
const service = options.financeServiceForHousehold(auth.member.householdId) const service = resolveFinanceService(options, auth)
await service.closeCycle(payload.period) await service.closeCycle(payload.period)
const cycleState = await service.getAdminCycleState() const cycleState = await service.getAdminCycleState()
@@ -701,19 +731,10 @@ export function createMiniAppCloseCycleHandler(options: {
} }
} }
export function createMiniAppRentUpdateHandler(options: { export function createMiniAppRentUpdateHandler(options: MiniAppBillingHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppBillingSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -738,7 +759,7 @@ export function createMiniAppRentUpdateHandler(options: {
} }
const payload = await readRentUpdatePayload(request) const payload = await readRentUpdatePayload(request)
const service = options.financeServiceForHousehold(auth.member.householdId) const service = resolveFinanceService(options, auth)
const result = await service.setRent(payload.amountMajor, payload.currency, payload.period) const result = await service.setRent(payload.amountMajor, payload.currency, payload.period)
if (!result) { if (!result) {
return miniAppJsonResponse( return miniAppJsonResponse(
@@ -766,19 +787,10 @@ export function createMiniAppRentUpdateHandler(options: {
} }
} }
export function createMiniAppAddUtilityBillHandler(options: { export function createMiniAppAddUtilityBillHandler(options: MiniAppBillingHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppBillingSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -803,7 +815,7 @@ export function createMiniAppAddUtilityBillHandler(options: {
} }
const payload = await readUtilityBillPayload(request) const payload = await readUtilityBillPayload(request)
const service = options.financeServiceForHousehold(auth.member.householdId) const service = resolveFinanceService(options, auth)
const result = await service.addUtilityBill( const result = await service.addUtilityBill(
payload.billName, payload.billName,
payload.amountMajor, payload.amountMajor,
@@ -837,19 +849,10 @@ export function createMiniAppAddUtilityBillHandler(options: {
} }
} }
export function createMiniAppSubmitUtilityBillHandler(options: { export function createMiniAppSubmitUtilityBillHandler(options: MiniAppBillingHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppBillingSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -874,7 +877,7 @@ export function createMiniAppSubmitUtilityBillHandler(options: {
} }
const payload = await readUtilityBillPayload(request) const payload = await readUtilityBillPayload(request)
const service = options.financeServiceForHousehold(auth.member.householdId) const service = resolveFinanceService(options, auth)
const result = await service.addUtilityBill( const result = await service.addUtilityBill(
payload.billName, payload.billName,
payload.amountMajor, payload.amountMajor,
@@ -905,35 +908,33 @@ export function createMiniAppSubmitUtilityBillHandler(options: {
} }
} }
export function createMiniAppSubmitPaymentHandler(options: { export function createMiniAppSubmitPaymentHandler(
allowedOrigins: readonly string[] options: MiniAppBillingHandlerBaseOptions & {
botToken: string householdConfigurationRepositoryForSession: (
financeServiceForHousehold: (householdId: string) => FinanceCommandService session: MiniAppAuthorizedSession
onboardingService: HouseholdOnboardingService ) => HouseholdConfigurationRepository
householdConfigurationRepository: HouseholdConfigurationRepository }
logger?: Logger ): {
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppBillingSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
async function notifyPaymentRecorded(input: { async function notifyPaymentRecorded(
session: MiniAppAuthorizedSession,
input: {
householdId: string householdId: string
memberName: string memberName: string
kind: 'rent' | 'utilities' kind: 'rent' | 'utilities'
amountMajor: string amountMajor: string
currency: string currency: string
period: string period: string
}) { }
) {
const householdConfigurationRepository =
options.householdConfigurationRepositoryForSession(session)
const [chat, topic] = await Promise.all([ const [chat, topic] = await Promise.all([
options.householdConfigurationRepository.getHouseholdChatByHouseholdId(input.householdId), householdConfigurationRepository.getHouseholdChatByHouseholdId(input.householdId),
options.householdConfigurationRepository.getHouseholdTopicBinding( householdConfigurationRepository.getHouseholdTopicBinding(input.householdId, 'reminders')
input.householdId,
'reminders'
)
]) ])
if (!chat || !topic) { if (!chat || !topic) {
@@ -996,7 +997,7 @@ export function createMiniAppSubmitPaymentHandler(options: {
return miniAppJsonResponse({ ok: false, error: 'Missing payment fields' }, 400, origin) return miniAppJsonResponse({ ok: false, error: 'Missing payment fields' }, 400, origin)
} }
const service = options.financeServiceForHousehold(auth.member.householdId) const service = resolveFinanceService(options, auth)
const payment = await service.addPayment( const payment = await service.addPayment(
auth.member.id, auth.member.id,
payload.kind, payload.kind,
@@ -1008,7 +1009,7 @@ export function createMiniAppSubmitPaymentHandler(options: {
return miniAppJsonResponse({ ok: false, error: 'Failed to record payment' }, 500, origin) return miniAppJsonResponse({ ok: false, error: 'Failed to record payment' }, 500, origin)
} }
await notifyPaymentRecorded({ await notifyPaymentRecorded(auth, {
householdId: auth.member.householdId, householdId: auth.member.householdId,
memberName: auth.member.displayName, memberName: auth.member.displayName,
kind: payload.kind, kind: payload.kind,
@@ -1032,19 +1033,10 @@ export function createMiniAppSubmitPaymentHandler(options: {
} }
} }
export function createMiniAppUpdateUtilityBillHandler(options: { export function createMiniAppUpdateUtilityBillHandler(options: MiniAppBillingHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppBillingSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -1069,7 +1061,7 @@ export function createMiniAppUpdateUtilityBillHandler(options: {
} }
const payload = await readUtilityBillUpdatePayload(request) const payload = await readUtilityBillUpdatePayload(request)
const service = options.financeServiceForHousehold(auth.member.householdId) const service = resolveFinanceService(options, auth)
const result = await service.updateUtilityBill( const result = await service.updateUtilityBill(
payload.billId, payload.billId,
payload.billName, payload.billName,
@@ -1099,19 +1091,10 @@ export function createMiniAppUpdateUtilityBillHandler(options: {
} }
} }
export function createMiniAppDeleteUtilityBillHandler(options: { export function createMiniAppDeleteUtilityBillHandler(options: MiniAppBillingHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppBillingSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -1136,7 +1119,7 @@ export function createMiniAppDeleteUtilityBillHandler(options: {
} }
const payload = await readUtilityBillDeletePayload(request) const payload = await readUtilityBillDeletePayload(request)
const service = options.financeServiceForHousehold(auth.member.householdId) const service = resolveFinanceService(options, auth)
const deleted = await service.deleteUtilityBill(payload.billId) const deleted = await service.deleteUtilityBill(payload.billId)
if (!deleted) { if (!deleted) {
@@ -1161,19 +1144,10 @@ export function createMiniAppDeleteUtilityBillHandler(options: {
} }
} }
export function createMiniAppAddPurchaseHandler(options: { export function createMiniAppAddPurchaseHandler(options: MiniAppBillingHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppBillingSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -1200,7 +1174,7 @@ export function createMiniAppAddPurchaseHandler(options: {
return miniAppJsonResponse({ ok: false, error: 'Missing purchase fields' }, 400, origin) return miniAppJsonResponse({ ok: false, error: 'Missing purchase fields' }, 400, origin)
} }
const service = options.financeServiceForHousehold(auth.member.householdId) const service = resolveFinanceService(options, auth)
const payerMemberId = payload.payerMemberId ?? auth.member.id const payerMemberId = payload.payerMemberId ?? auth.member.id
await service.addPurchase( await service.addPurchase(
payload.description, payload.description,
@@ -1218,19 +1192,10 @@ export function createMiniAppAddPurchaseHandler(options: {
} }
} }
export function createMiniAppUpdatePurchaseHandler(options: { export function createMiniAppUpdatePurchaseHandler(options: MiniAppBillingHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppBillingSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -1257,7 +1222,7 @@ export function createMiniAppUpdatePurchaseHandler(options: {
return miniAppJsonResponse({ ok: false, error: 'Missing purchase fields' }, 400, origin) return miniAppJsonResponse({ ok: false, error: 'Missing purchase fields' }, 400, origin)
} }
const service = options.financeServiceForHousehold(auth.member.householdId) const service = resolveFinanceService(options, auth)
const payerMemberId = payload.payerMemberId const payerMemberId = payload.payerMemberId
const updated = await service.updatePurchase( const updated = await service.updatePurchase(
payload.purchaseId, payload.purchaseId,
@@ -1280,19 +1245,10 @@ export function createMiniAppUpdatePurchaseHandler(options: {
} }
} }
export function createMiniAppDeletePurchaseHandler(options: { export function createMiniAppDeletePurchaseHandler(options: MiniAppBillingHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppBillingSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -1315,7 +1271,7 @@ export function createMiniAppDeletePurchaseHandler(options: {
} }
const payload = await readPurchaseMutationPayload(request) const payload = await readPurchaseMutationPayload(request)
const service = options.financeServiceForHousehold(auth.member.householdId) const service = resolveFinanceService(options, auth)
const deleted = await service.deletePurchase(payload.purchaseId) const deleted = await service.deletePurchase(payload.purchaseId)
if (!deleted) { if (!deleted) {
@@ -1330,19 +1286,10 @@ export function createMiniAppDeletePurchaseHandler(options: {
} }
} }
export function createMiniAppAddPaymentHandler(options: { export function createMiniAppAddPaymentHandler(options: MiniAppBillingHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppBillingSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -1369,7 +1316,7 @@ export function createMiniAppAddPaymentHandler(options: {
return miniAppJsonResponse({ ok: false, error: 'Missing payment fields' }, 400, origin) return miniAppJsonResponse({ ok: false, error: 'Missing payment fields' }, 400, origin)
} }
const service = options.financeServiceForHousehold(auth.member.householdId) const service = resolveFinanceService(options, auth)
const payment = await service.addPayment( const payment = await service.addPayment(
payload.memberId, payload.memberId,
payload.kind, payload.kind,
@@ -1389,19 +1336,10 @@ export function createMiniAppAddPaymentHandler(options: {
} }
} }
export function createMiniAppUpdatePaymentHandler(options: { export function createMiniAppUpdatePaymentHandler(options: MiniAppBillingHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppBillingSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -1428,7 +1366,7 @@ export function createMiniAppUpdatePaymentHandler(options: {
return miniAppJsonResponse({ ok: false, error: 'Missing payment fields' }, 400, origin) return miniAppJsonResponse({ ok: false, error: 'Missing payment fields' }, 400, origin)
} }
const service = options.financeServiceForHousehold(auth.member.householdId) const service = resolveFinanceService(options, auth)
const payment = await service.updatePayment( const payment = await service.updatePayment(
payload.paymentId, payload.paymentId,
payload.memberId, payload.memberId,
@@ -1449,19 +1387,10 @@ export function createMiniAppUpdatePaymentHandler(options: {
} }
} }
export function createMiniAppDeletePaymentHandler(options: { export function createMiniAppDeletePaymentHandler(options: MiniAppBillingHandlerBaseOptions): {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createConfiguredMiniAppBillingSessionService(options)
botToken: options.botToken,
onboardingService: options.onboardingService
})
return { return {
handler: async (request) => { handler: async (request) => {
@@ -1488,7 +1417,7 @@ export function createMiniAppDeletePaymentHandler(options: {
return miniAppJsonResponse({ ok: false, error: 'Missing payment id' }, 400, origin) return miniAppJsonResponse({ ok: false, error: 'Missing payment id' }, 400, origin)
} }
const service = options.financeServiceForHousehold(auth.member.householdId) const service = resolveFinanceService(options, auth)
const deleted = await service.deletePayment(payload.paymentId) const deleted = await service.deletePayment(payload.paymentId)
if (!deleted) { if (!deleted) {

View File

@@ -3,6 +3,7 @@ import type { Logger } from '@household/observability'
import { import {
allowedMiniAppOrigin, allowedMiniAppOrigin,
type MiniAppAuthorizedSession,
createMiniAppSessionService, createMiniAppSessionService,
miniAppErrorResponse, miniAppErrorResponse,
miniAppJsonResponse, miniAppJsonResponse,
@@ -12,15 +13,26 @@ import {
export function createMiniAppDashboardHandler(options: { export function createMiniAppDashboardHandler(options: {
allowedOrigins: readonly string[] allowedOrigins: readonly string[]
botToken: string botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService financeServiceForSession?: (session: MiniAppAuthorizedSession) => FinanceCommandService
onboardingService: HouseholdOnboardingService financeServiceForHousehold?: (householdId: string) => FinanceCommandService
onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
logger?: Logger logger?: Logger
}): { }): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createMiniAppSessionService({
botToken: options.botToken, botToken: options.botToken,
...(options.onboardingServiceForTelegramUserId
? {
onboardingServiceForTelegramUserId: options.onboardingServiceForTelegramUserId
}
: {}),
...(options.onboardingService
? {
onboardingService: options.onboardingService onboardingService: options.onboardingService
}
: {})
}) })
return { return {
@@ -62,7 +74,7 @@ export function createMiniAppDashboardHandler(options: {
) )
} }
if (!session.member) { if (!session.member || !session.telegramUser) {
return miniAppJsonResponse( return miniAppJsonResponse(
{ ok: false, error: 'Authenticated session is missing member context' }, { ok: false, error: 'Authenticated session is missing member context' },
500, 500,
@@ -70,9 +82,17 @@ export function createMiniAppDashboardHandler(options: {
) )
} }
const dashboard = await options const financeService =
.financeServiceForHousehold(session.member.householdId) options.financeServiceForSession?.({
.generateDashboard() member: session.member,
telegramUserId: session.telegramUser.id
}) ?? options.financeServiceForHousehold?.(session.member.householdId)
if (!financeService) {
throw new Error('Mini app finance service is not configured')
}
const dashboard = await financeService.generateDashboard()
if (!dashboard) { if (!dashboard) {
return miniAppJsonResponse( return miniAppJsonResponse(
{ ok: false, error: 'No billing cycle available' }, { ok: false, error: 'No billing cycle available' },

View File

@@ -4,6 +4,7 @@ import type { Logger } from '@household/observability'
import { import {
allowedMiniAppOrigin, allowedMiniAppOrigin,
type MiniAppAuthorizedSession,
createMiniAppSessionService, createMiniAppSessionService,
miniAppErrorResponse, miniAppErrorResponse,
miniAppJsonResponse miniAppJsonResponse
@@ -53,15 +54,26 @@ async function readLocalePreferenceRequest(request: Request): Promise<LocalePref
export function createMiniAppLocalePreferenceHandler(options: { export function createMiniAppLocalePreferenceHandler(options: {
allowedOrigins: readonly string[] allowedOrigins: readonly string[]
botToken: string botToken: string
onboardingService: HouseholdOnboardingService onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
localePreferenceService: LocalePreferenceService onboardingService?: HouseholdOnboardingService
localePreferenceServiceForSession?: (session: MiniAppAuthorizedSession) => LocalePreferenceService
localePreferenceService?: LocalePreferenceService
logger?: Logger logger?: Logger
}): { }): {
handler: (request: Request) => Promise<Response> handler: (request: Request) => Promise<Response>
} { } {
const sessionService = createMiniAppSessionService({ const sessionService = createMiniAppSessionService({
botToken: options.botToken, botToken: options.botToken,
...(options.onboardingServiceForTelegramUserId
? {
onboardingServiceForTelegramUserId: options.onboardingServiceForTelegramUserId
}
: {}),
...(options.onboardingService
? {
onboardingService: options.onboardingService onboardingService: options.onboardingService
}
: {})
}) })
return { return {
@@ -100,9 +112,18 @@ export function createMiniAppLocalePreferenceHandler(options: {
let memberPreferredLocale = session.member.preferredLocale let memberPreferredLocale = session.member.preferredLocale
let householdDefaultLocale = session.member.householdDefaultLocale let householdDefaultLocale = session.member.householdDefaultLocale
const localePreferenceService =
options.localePreferenceServiceForSession?.({
member: session.member,
telegramUserId: session.telegramUser.id
}) ?? options.localePreferenceService
if (!localePreferenceService) {
throw new Error('Mini app locale preference service is not configured')
}
if (payload.scope === 'member') { if (payload.scope === 'member') {
const result = await options.localePreferenceService.updateMemberLocale({ const result = await localePreferenceService.updateMemberLocale({
householdId: session.member.householdId, householdId: session.member.householdId,
telegramUserId: session.telegramUser.id, telegramUserId: session.telegramUser.id,
locale: payload.locale locale: payload.locale
@@ -115,7 +136,7 @@ export function createMiniAppLocalePreferenceHandler(options: {
memberPreferredLocale = result.member.preferredLocale memberPreferredLocale = result.member.preferredLocale
householdDefaultLocale = result.member.householdDefaultLocale householdDefaultLocale = result.member.householdDefaultLocale
} else { } else {
const result = await options.localePreferenceService.updateHouseholdLocale({ const result = await localePreferenceService.updateHouseholdLocale({
householdId: session.member.householdId, householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin, actorIsAdmin: session.member.isAdmin,
locale: payload.locale locale: payload.locale

View File

@@ -64,12 +64,13 @@ codex review --base origin/main
- Copy `.env.example` to `.env` before running app/database commands. - Copy `.env.example` to `.env` before running app/database commands.
- `bun run db:seed` refreshes the committed fixture household and is destructive for previously seeded fixture rows. - `bun run db:seed` refreshes the committed fixture household and is destructive for previously seeded fixture rows.
- Local bot feature flags come from env presence: - Local bot feature flags come from env presence:
- finance commands require `DATABASE_URL` plus household setup in Telegram via `/setup` - mini app auth and mini app API routes require `APP_DATABASE_URL`
- purchase ingestion requires `DATABASE_URL` plus a bound purchase topic via `/bind_purchase_topic` - finance commands require `WORKER_DATABASE_URL` plus household setup in Telegram via `/setup`
- anonymous feedback requires `DATABASE_URL` plus a bound feedback topic via `/bind_feedback_topic` - purchase ingestion requires `WORKER_DATABASE_URL` plus a bound purchase topic via `/bind_purchase_topic`
- reminders require `DATABASE_URL` plus `SCHEDULER_SHARED_SECRET` or `SCHEDULER_OIDC_ALLOWED_EMAILS` - anonymous feedback requires `WORKER_DATABASE_URL` plus a bound feedback topic via `/bind_feedback_topic`
- reminders require `WORKER_DATABASE_URL` plus `SCHEDULER_SHARED_SECRET` or `SCHEDULER_OIDC_ALLOWED_EMAILS`
and optionally use a dedicated reminders topic via `/bind_reminders_topic` and optionally use a dedicated reminders topic via `/bind_reminders_topic`
- mini app CORS can be constrained with `MINI_APP_ALLOWED_ORIGINS` - mini app CORS must be set explicitly with `MINI_APP_ALLOWED_ORIGINS`
- Migration workflow is documented in `docs/runbooks/migrations.md`. - Migration workflow is documented in `docs/runbooks/migrations.md`.
- Destructive dev reset guidance is documented in `docs/runbooks/dev-reset.md`. - Destructive dev reset guidance is documented in `docs/runbooks/dev-reset.md`.
- First deploy flow is documented in `docs/runbooks/first-deploy.md`. - First deploy flow is documented in `docs/runbooks/first-deploy.md`.

View File

@@ -12,7 +12,10 @@ Execute the first real deployment with a repeatable sequence that covers infrast
- GCP project - GCP project
- GitHub repo settings - GitHub repo settings
- Telegram bot token - Telegram bot token
- Supabase project and database URL - Supabase project and three database URLs:
- owner `DATABASE_URL` for migrations only
- `APP_DATABASE_URL` for authenticated request paths
- `WORKER_DATABASE_URL` for bot and scheduler workers
## Required Configuration Inventory ## Required Configuration Inventory
@@ -28,7 +31,8 @@ Required in your environment `*.tfvars`:
Recommended: Recommended:
- `database_url_secret_id = "database-url"` - `app_database_url_secret_id = "app-database-url"`
- `worker_database_url_secret_id = "worker-database-url"`
- `telegram_bot_token_secret_id = "telegram-bot-token"` - `telegram_bot_token_secret_id = "telegram-bot-token"`
- `openai_api_key_secret_id = "openai-api-key"` - `openai_api_key_secret_id = "openai-api-key"`
- `bot_mini_app_allowed_origins` - `bot_mini_app_allowed_origins`
@@ -46,10 +50,9 @@ Create the secret resources via Terraform, then add secret versions for:
- `telegram-bot-token` - `telegram-bot-token`
- `telegram-webhook-secret` - `telegram-webhook-secret`
- `scheduler-shared-secret` - `scheduler-shared-secret`
- `database-url` - `app-database-url`
- `worker-database-url`
- optional `openai-api-key` - optional `openai-api-key`
- optional `supabase-url`
- optional `supabase-publishable-key`
### GitHub Actions secrets ### GitHub Actions secrets
@@ -129,15 +132,19 @@ Use the real project ID from Terraform variables:
echo -n "<telegram-bot-token>" | gcloud secrets versions add telegram-bot-token --data-file=- --project <project_id> echo -n "<telegram-bot-token>" | gcloud secrets versions add telegram-bot-token --data-file=- --project <project_id>
echo -n "<telegram-webhook-secret>" | gcloud secrets versions add telegram-webhook-secret --data-file=- --project <project_id> echo -n "<telegram-webhook-secret>" | gcloud secrets versions add telegram-webhook-secret --data-file=- --project <project_id>
echo -n "<scheduler-shared-secret>" | gcloud secrets versions add scheduler-shared-secret --data-file=- --project <project_id> echo -n "<scheduler-shared-secret>" | gcloud secrets versions add scheduler-shared-secret --data-file=- --project <project_id>
echo -n "<database-url>" | gcloud secrets versions add database-url --data-file=- --project <project_id> echo -n "<app-database-url>" | gcloud secrets versions add app-database-url --data-file=- --project <project_id>
echo -n "<worker-database-url>" | gcloud secrets versions add worker-database-url --data-file=- --project <project_id>
``` ```
Add optional secret versions only if those integrations are enabled. Add optional secret versions only if those integrations are enabled.
For a functional household dev deployment, set `database_url_secret_id = "database-url"` in For a functional household deployment, set both `app_database_url_secret_id` and
`dev.tfvars` before the apply that creates the Cloud Run services. Otherwise the bot deploys `worker_database_url_secret_id` in `dev.tfvars` before the apply that creates the Cloud Run
without `DATABASE_URL`, and finance commands, reminders, mini app auth/dashboard, and anonymous services. Otherwise the bot deploys without `APP_DATABASE_URL` and `WORKER_DATABASE_URL`, and mini
feedback remain disabled. app auth, finance commands, reminders, purchase ingestion, and anonymous feedback remain disabled.
Keep `DATABASE_URL` out of normal runtime secrets. It is only required in GitHub Actions for the
migration step that runs before deploy.
Keep `telegram_bot_token_secret_id = "telegram-bot-token"` aligned with the actual bot token Keep `telegram_bot_token_secret_id = "telegram-bot-token"` aligned with the actual bot token
secret name. CD uses that secret to sync the Telegram command menu after deploy. secret name. CD uses that secret to sync the Telegram command menu after deploy.
@@ -218,6 +225,9 @@ The smoke script verifies:
- scheduler endpoint rejects unauthenticated requests - scheduler endpoint rejects unauthenticated requests
- Telegram webhook matches the expected URL when bot token is provided - Telegram webhook matches the expected URL when bot token is provided
Production deploys should also set `MINI_APP_ALLOWED_ORIGINS` explicitly. The browser path remains
bot API only; there is no supported direct browser access to Supabase.
## Phase 8: Scheduler Enablement ## Phase 8: Scheduler Enablement
First release: First release:

View File

@@ -47,9 +47,16 @@ bun run build
## CD behavior ## CD behavior
- CD deploy runs migrations before deploy and now requires the `DATABASE_URL` GitHub secret. - CD deploy runs migrations before deploy and requires the owner-only `DATABASE_URL` GitHub secret.
- If `DATABASE_URL` is missing, CD fails fast instead of deploying schema-dependent code without migrations. - If `DATABASE_URL` is missing, CD fails fast instead of deploying schema-dependent code without migrations.
## Runtime connection split
- `DATABASE_URL` is for migrations, schema checks, and other owner-only maintenance tasks.
- `APP_DATABASE_URL` is for authenticated request paths such as mini app routes.
- `WORKER_DATABASE_URL` is for Telegram ingestion, reminders, scheduler jobs, and other internal worker flows.
- Runtime services should not use `DATABASE_URL`.
## Safety rules ## Safety rules
- Prefer additive migrations first (new columns/tables) over destructive changes. - Prefer additive migrations first (new columns/tables) over destructive changes.

View File

@@ -29,8 +29,8 @@ Define a reproducible GCP infrastructure baseline for deployment of the bot API
- Bot runtime reads secret-backed env vars: - Bot runtime reads secret-backed env vars:
- `TELEGRAM_WEBHOOK_SECRET` - `TELEGRAM_WEBHOOK_SECRET`
- `SCHEDULER_SHARED_SECRET` - `SCHEDULER_SHARED_SECRET`
- `SUPABASE_URL` (optional) - `APP_DATABASE_URL` (optional)
- `SUPABASE_PUBLISHABLE_KEY` (optional) - `WORKER_DATABASE_URL` (optional)
## Domain Rules ## Domain Rules

View File

@@ -58,8 +58,9 @@ echo -n "<value>" | gcloud secrets versions add telegram-webhook-secret --data-f
echo -n "<value>" | gcloud secrets versions add scheduler-shared-secret --data-file=- --project <project_id> echo -n "<value>" | gcloud secrets versions add scheduler-shared-secret --data-file=- --project <project_id>
``` ```
If you configure optional secret IDs such as `database_url_secret_id` or If you configure optional secret IDs such as `app_database_url_secret_id`,
`openai_api_key_secret_id`, add versions for those secrets as well. `worker_database_url_secret_id`, or `openai_api_key_secret_id`, add versions for those secrets as
well.
If GitHub OIDC deploy access is enabled, keep `telegram_bot_token_secret_id` aligned with the If GitHub OIDC deploy access is enabled, keep `telegram_bot_token_secret_id` aligned with the
real bot token secret name so CD can read it and sync Telegram commands automatically. real bot token secret name so CD can read it and sync Telegram commands automatically.
@@ -84,6 +85,9 @@ Recommended approach:
`bot_assistant_rate_limit_rolling_window_ms` `bot_assistant_rate_limit_rolling_window_ms`
- optional `bot_mini_app_allowed_origins` - optional `bot_mini_app_allowed_origins`
- optional `alert_notification_emails` - optional `alert_notification_emails`
- runtime DB URLs should stay split:
`APP_DATABASE_URL` for authenticated request flows and `WORKER_DATABASE_URL` for background
workers
## Alerting baseline ## Alerting baseline
@@ -115,3 +119,4 @@ CI runs:
- Scheduler jobs default to `paused = true` and `dry_run = true` to prevent accidental sends before live reminder delivery is ready. - Scheduler jobs default to `paused = true` and `dry_run = true` to prevent accidental sends before live reminder delivery is ready.
- Bot API is public to accept Telegram webhooks; scheduler endpoint should still verify app-level auth. - Bot API is public to accept Telegram webhooks; scheduler endpoint should still verify app-level auth.
- `bot_mini_app_allowed_origins` cannot be auto-derived in Terraform because the bot and mini app Cloud Run services reference each other; set it explicitly once the mini app URL is known. - `bot_mini_app_allowed_origins` cannot be auto-derived in Terraform because the bot and mini app Cloud Run services reference each other; set it explicitly once the mini app URL is known.
- `DATABASE_URL` is migration-only and should not be injected into the bot runtime service.

View File

@@ -30,7 +30,8 @@ locals {
runtime_secret_ids = toset(compact([ runtime_secret_ids = toset(compact([
var.telegram_webhook_secret_id, var.telegram_webhook_secret_id,
var.scheduler_shared_secret_id, var.scheduler_shared_secret_id,
var.database_url_secret_id, var.app_database_url_secret_id,
var.worker_database_url_secret_id,
var.telegram_bot_token_secret_id, var.telegram_bot_token_secret_id,
var.openai_api_key_secret_id var.openai_api_key_secret_id
])) ]))

View File

@@ -179,8 +179,11 @@ module "bot_api_service" {
TELEGRAM_WEBHOOK_SECRET = var.telegram_webhook_secret_id TELEGRAM_WEBHOOK_SECRET = var.telegram_webhook_secret_id
SCHEDULER_SHARED_SECRET = var.scheduler_shared_secret_id SCHEDULER_SHARED_SECRET = var.scheduler_shared_secret_id
}, },
var.database_url_secret_id == null ? {} : { var.app_database_url_secret_id == null ? {} : {
DATABASE_URL = var.database_url_secret_id APP_DATABASE_URL = var.app_database_url_secret_id
},
var.worker_database_url_secret_id == null ? {} : {
WORKER_DATABASE_URL = var.worker_database_url_secret_id
}, },
var.telegram_bot_token_secret_id == null ? {} : { var.telegram_bot_token_secret_id == null ? {} : {
TELEGRAM_BOT_TOKEN = var.telegram_bot_token_secret_id TELEGRAM_BOT_TOKEN = var.telegram_bot_token_secret_id

View File

@@ -8,7 +8,8 @@ artifact_repository_id = "household-bot"
bot_api_image = "europe-west1-docker.pkg.dev/my-gcp-project/household-bot/bot:latest" bot_api_image = "europe-west1-docker.pkg.dev/my-gcp-project/household-bot/bot:latest"
mini_app_image = "europe-west1-docker.pkg.dev/my-gcp-project/household-bot/miniapp:latest" mini_app_image = "europe-west1-docker.pkg.dev/my-gcp-project/household-bot/miniapp:latest"
database_url_secret_id = "database-url" app_database_url_secret_id = "app-database-url"
worker_database_url_secret_id = "worker-database-url"
telegram_bot_token_secret_id = "telegram-bot-token" telegram_bot_token_secret_id = "telegram-bot-token"
openai_api_key_secret_id = "openai-api-key" openai_api_key_secret_id = "openai-api-key"
bot_purchase_parser_model = "gpt-4o-mini" bot_purchase_parser_model = "gpt-4o-mini"

View File

@@ -57,7 +57,21 @@ variable "scheduler_shared_secret_id" {
} }
variable "database_url_secret_id" { variable "database_url_secret_id" {
description = "Optional Secret Manager ID for DATABASE_URL" description = "Optional Secret Manager ID for owner-only DATABASE_URL used outside runtime deploys"
type = string
default = null
nullable = true
}
variable "app_database_url_secret_id" {
description = "Optional Secret Manager ID for APP_DATABASE_URL"
type = string
default = null
nullable = true
}
variable "worker_database_url_secret_id" {
description = "Optional Secret Manager ID for WORKER_DATABASE_URL"
type = string type = string
default = null default = null
nullable = true nullable = true

View File

@@ -1,6 +1,6 @@
import { and, desc, eq, gte, inArray, isNotNull, isNull, lt, lte, or, sql } from 'drizzle-orm' import { and, desc, eq, gte, inArray, isNotNull, isNull, lt, lte, or, sql } from 'drizzle-orm'
import { createDbClient, schema } from '@household/db' import { createDbClient, type DbSessionContext, schema } from '@household/db'
import type { FinanceRepository } from '@household/ports' import type { FinanceRepository } from '@household/ports'
import { import {
instantFromDatabaseValue, instantFromDatabaseValue,
@@ -22,14 +22,22 @@ function toCurrencyCode(raw: string): CurrencyCode {
export function createDbFinanceRepository( export function createDbFinanceRepository(
databaseUrl: string, databaseUrl: string,
householdId: string householdId: string,
options: {
sessionContext?: DbSessionContext
} = {}
): { ): {
repository: FinanceRepository repository: FinanceRepository
close: () => Promise<void> close: () => Promise<void>
} { } {
const { db, queryClient } = createDbClient(databaseUrl, { const { db, queryClient } = createDbClient(databaseUrl, {
max: 5, max: 5,
prepare: false prepare: false,
...(options.sessionContext
? {
sessionContext: options.sessionContext
}
: {})
}) })
async function loadPurchaseParticipants(purchaseIds: readonly string[]): Promise< async function loadPurchaseParticipants(purchaseIds: readonly string[]): Promise<

View File

@@ -1,6 +1,6 @@
import { and, asc, eq, sql } from 'drizzle-orm' import { and, asc, eq, sql } from 'drizzle-orm'
import { createDbClient, schema } from '@household/db' import { createDbClient, type DbSessionContext, schema } from '@household/db'
import { import {
instantToDate, instantToDate,
normalizeSupportedLocale, normalizeSupportedLocale,
@@ -334,13 +334,23 @@ function utilityCategorySlug(name: string): string {
.slice(0, 48) .slice(0, 48)
} }
export function createDbHouseholdConfigurationRepository(databaseUrl: string): { export function createDbHouseholdConfigurationRepository(
databaseUrl: string,
options: {
sessionContext?: DbSessionContext
} = {}
): {
repository: HouseholdConfigurationRepository repository: HouseholdConfigurationRepository
close: () => Promise<void> close: () => Promise<void>
} { } {
const { db, queryClient } = createDbClient(databaseUrl, { const { db, queryClient } = createDbClient(databaseUrl, {
max: 5, max: 5,
prepare: false prepare: false,
...(options.sessionContext
? {
sessionContext: options.sessionContext
}
: {})
}) })
const defaultUtilityCategories = [ const defaultUtilityCategories = [

View File

@@ -23,6 +23,7 @@
"0019_faithful_madame_masque.sql": "38711341799b04a7c47fcc64fd19faf5b26e6f183d6a4c01d492b9929cd63641", "0019_faithful_madame_masque.sql": "38711341799b04a7c47fcc64fd19faf5b26e6f183d6a4c01d492b9929cd63641",
"0020_natural_mauler.sql": "a80a4a0196a3b4931040850089346d1bc99b34a5afca77d6d62478ee4b8902c1", "0020_natural_mauler.sql": "a80a4a0196a3b4931040850089346d1bc99b34a5afca77d6d62478ee4b8902c1",
"0020_silver_payments.sql": "9686235c75453f1eaa016f2f4ab7fce8fe964c76a4e3515987a2b9f90bd7b1ad", "0020_silver_payments.sql": "9686235c75453f1eaa016f2f4ab7fce8fe964c76a4e3515987a2b9f90bd7b1ad",
"0021_sharp_payer.sql": "973596e154382984ba7769979ea58298b6d93c5139540854be01e8b283ddb4f1" "0021_sharp_payer.sql": "973596e154382984ba7769979ea58298b6d93c5139540854be01e8b283ddb4f1",
"0022_harden_rls.sql": "d2e24b3e5b7ec7ef9da7e90c0ddf0e408764f3578af3872f76b9b3198ffbd70e"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -155,6 +155,13 @@
"when": 1774200000000, "when": 1774200000000,
"tag": "0021_sharp_payer", "tag": "0021_sharp_payer",
"breakpoints": true "breakpoints": true
},
{
"idx": 22,
"version": "7",
"when": 1774204831000,
"tag": "0022_harden_rls",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,9 +1,34 @@
import postgres from 'postgres' import postgres from 'postgres'
import { drizzle } from 'drizzle-orm/postgres-js' import { drizzle } from 'drizzle-orm/postgres-js'
export interface DbSessionContext {
telegramUserId?: string
householdId?: string
memberId?: string
isAdmin?: boolean
isWorker?: boolean
}
export interface DbClientOptions { export interface DbClientOptions {
max?: number max?: number
prepare?: boolean prepare?: boolean
sessionContext?: DbSessionContext
}
function quoteRuntimeOptionValue(value: string): string {
return `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`
}
function appendRuntimeOption(
options: string[],
key: string,
value: string | boolean | undefined
): void {
if (value === undefined) {
return
}
options.push(`-c ${key}=${quoteRuntimeOptionValue(String(value))}`)
} }
export function createDbClient(databaseUrl: string, options: DbClientOptions = {}) { export function createDbClient(databaseUrl: string, options: DbClientOptions = {}) {
@@ -17,7 +42,17 @@ export function createDbClient(databaseUrl: string, options: DbClientOptions = {
url.searchParams.delete('options') url.searchParams.delete('options')
// Set search_path via options parameter (required for PgBouncer compatibility) // Set search_path via options parameter (required for PgBouncer compatibility)
url.searchParams.set('options', `-c search_path=${dbSchema}`) const runtimeOptions = [`-c search_path=${dbSchema}`]
appendRuntimeOption(
runtimeOptions,
'app.telegram_user_id',
options.sessionContext?.telegramUserId
)
appendRuntimeOption(runtimeOptions, 'app.household_id', options.sessionContext?.householdId)
appendRuntimeOption(runtimeOptions, 'app.member_id', options.sessionContext?.memberId)
appendRuntimeOption(runtimeOptions, 'app.is_admin', options.sessionContext?.isAdmin)
appendRuntimeOption(runtimeOptions, 'app.is_worker', options.sessionContext?.isWorker)
url.searchParams.set('options', runtimeOptions.join(' '))
const cleanUrl = url.toString() const cleanUrl = url.toString()

View File

@@ -1,2 +1,3 @@
export { createDbClient } from './client' export { createDbClient } from './client'
export type { DbSessionContext } from './client'
export * as schema from './schema' export * as schema from './schema'