diff --git a/packages/api/src/router/statusPage.ts b/packages/api/src/router/statusPage.ts index 03988ac11..684ae756f 100644 --- a/packages/api/src/router/statusPage.ts +++ b/packages/api/src/router/statusPage.ts @@ -1541,10 +1541,11 @@ export const statusPageRouter = createTRPCRouter({ }); } + // Public procedure: never return the raw row (password, IP allow-list). return { email: opts.input.email, slug: opts.input.slug, - page: _page, + page: { title: _page.title }, }; }), diff --git a/packages/api/src/router/stripe/shared.ts b/packages/api/src/router/stripe/shared.ts index 0b1341463..cffade88e 100644 --- a/packages/api/src/router/stripe/shared.ts +++ b/packages/api/src/router/stripe/shared.ts @@ -159,24 +159,21 @@ export async function cancelSupersededSubscriptions( export async function cancelSubscription(customer?: string) { if (!customer) return; - try { - const { current } = await getCurrentSubscription(customer); - if (!current) return; - - if (current.status === "trialing") { - return await stripe.subscriptions.cancel(current.id); - } + // Throws on Stripe failure so the caller never deletes an account whose + // subscription is still billing. + const { current } = await getCurrentSubscription(customer); + if (!current) return; - return await stripe.subscriptions.update(current.id, { - cancel_at_period_end: true, - cancellation_details: { - comment: "Customer deleted their OpenStatus project.", - }, - }); - } catch (error) { - console.log("Error cancelling Stripe subscription", error); - return; + if (current.status === "trialing") { + return await stripe.subscriptions.cancel(current.id); } + + return await stripe.subscriptions.update(current.id, { + cancel_at_period_end: true, + cancellation_details: { + comment: "Customer deleted their OpenStatus project.", + }, + }); } export function customerIdOf(subscription: Stripe.Subscription) { diff --git a/packages/api/src/service/telegram-updates.ts b/packages/api/src/service/telegram-updates.ts index d48e402d6..235707715 100644 --- a/packages/api/src/service/telegram-updates.ts +++ b/packages/api/src/service/telegram-updates.ts @@ -91,6 +91,9 @@ export type ValidUpdate = user: { id: number; first_name: string; username?: string }; }; +// Matches the phase-1 token expiry in `createTelegramToken`. +const TELEGRAM_CHAT_BINDING_TTL_SECONDS = 1800; + // ---- Helpers ---------------------------------------------------------------- function extractPrivateChatStart( @@ -221,6 +224,8 @@ export async function processTelegramUpdates(args: { }) : updates; + const chatKey = `telegram:workspace_chat:${workspaceId}`; + // 2. Phase 1: private /start (no privateChatId filter) if (!privateChatId) { const tokenKey = `telegram:workspace_token:${workspaceId}`; @@ -232,6 +237,9 @@ export async function processTelegramUpdates(args: { if (result) { // Single-use token: delete and stop await redisClient.del(tokenKey); + await redisClient.set(chatKey, result.chatId, { + ex: TELEGRAM_CHAT_BINDING_TTL_SECONDS, + }); validUpdates.push({ chatType: "private", ...result }); break; } @@ -240,6 +248,14 @@ export async function processTelegramUpdates(args: { } // 3. Phase 2: group/supergroup additions else { + // Only the chat that completed phase 1 for this workspace may be used; + // otherwise any caller could read another user's group additions. + // String(): Upstash deserializes numeric strings into numbers. + const boundChatId = await redisClient.get(chatKey); + if (boundChatId == null || String(boundChatId) !== privateChatId) { + return validUpdates; + } + for (const update of recentUpdates) { const result = extractGroupBotAddition( update, diff --git a/packages/services/src/page/update.ts b/packages/services/src/page/update.ts index e0d90a615..3e097d5ff 100644 --- a/packages/services/src/page/update.ts +++ b/packages/services/src/page/update.ts @@ -155,7 +155,8 @@ export async function updatePagePasswordProtection(args: { // non-empty joins (e.g. `"a@b.com"` / `"10.0.0.0/24"`) pass // through unchanged. authEmailDomains: input.authEmailDomains?.join(",") || null, - password: input.password, + // Drop a retired password; `undefined` keeps the stored one. + password: input.accessType === "password" ? input.password : null, allowedIpRanges: input.allowedIpRanges?.join(",") || null, ...(input.allowIndex !== undefined && { allowIndex: input.allowIndex }), updatedAt: new Date(),