Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/PROJECT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ Local `main` in the main clone is at `5693f7b`, an older Production commit that
| #32 M4.3.2 Financial Integrity Remediation | `feature/m4-3-1-review-remediation` ← `feature/m4-3-2-financial-integrity` | `62cb0a3` | `lint, unit, e2e` ✅ · Vercel ✅ | Fail-Closed Merchant Identity & Secure Manual Refund Reconciliation |
| #33 M4.3.3 Final Provider Verification Integrity | `feature/m4-3-2-financial-integrity` ← `feature/m4-3-3-provider-verification` | `4c54711` | `lint, unit, e2e` ✅ · Vercel ✅ | Removal of Currency Defaults, Elimination of Merchant Fallback, Authentic S2S Polling & Attestation |
| #34 M4.3.4 Provider Contract Decision & M4 Closure | `feature/m4-3-3-provider-verification` ← `feature/m4-3-4-contract-decision` | `1e5bd19` | `lint, unit, e2e` ✅ | PhonePe Contract Determination (Outcome B), Full Regression Suite, Audit & Milestone M4 Closure |
| #35 M5 Telegram & Operational Workflows | `feature/m4-3-4-contract-decision` ← `feature/m5-telegram-operational-workflows` | `262f4f0` | `lint, unit, e2e` ✅ | Telegram Identity & Account Linking, Event Private Group Isolation (No Shared Group), Operational Notifications with Quiet Hours, /report Workflow |

**Owner gate:** PR #17 must not be merged without the owner's separate approval. `main` has no GitHub Actions runs on record. PR #34, PR #33, PR #32, PR #31, PR #30, and PR #17 must not be merged without separate owner approval. Keep `ENABLE_LIVE_PAYMENTS=false` everywhere.
**Owner gate:** PR #17 must not be merged without the owner's separate approval. `main` has no GitHub Actions runs on record. PR #35, PR #34, PR #33, PR #32, PR #31, PR #30, and PR #17 must not be merged without separate owner approval. Keep `ENABLE_LIVE_PAYMENTS=false` everywhere.

PR #23 is **merged** at `7c5079e` (P3 Location Intelligence + M2 Remediation).
PR #25 is **merged** at `83a30a7` (Owner Addendum Ops: dynamic DB block list, branch archiving, Phase 5.6/5.7 reports).
Expand All @@ -51,6 +52,7 @@ PR #31 is **open / preserved** (`feature/m4-3-1-review-remediation`).
PR #32 is **open / preserved** (`feature/m4-3-2-financial-integrity`).
PR #33 is **open / preserved** (`feature/m4-3-3-provider-verification`).
PR #34 is **open / preserved** (`feature/m4-3-4-contract-decision`).
PR #35 is **open / preserved** (`feature/m5-telegram-operational-workflows`).

## 4. Migrations per environment

Expand Down Expand Up @@ -172,7 +174,8 @@ Per owner approval on 2026-11-03, each of the 19 fully merged remote branches wa
| M4.3.2 Financial Integrity Remediation | `feature/m4-3-2-financial-integrity` / #32 | **Completed** (Strict fail-closed merchant identity comparison across verified, stored, and configured identities; secure manual refund reconciliation with verified admin roles and verifiable external references; 33/33 UPI tests pass, 466/466 unit/integration tests pass, `ENABLE_LIVE_PAYMENTS=false` preserved) |
| M4.3.3 Final Provider Verification Integrity | `feature/m4-3-3-provider-verification` / #33 | **Completed** (Eliminated synthetic currency defaults and configured merchant status check fallbacks; fail-closed currency and merchant identity gating; documented CBS operational limitation and operator attestation labeling; 37/37 UPI tests pass, 470/470 unit/integration tests pass, `ENABLE_LIVE_PAYMENTS=false` preserved) |
| M4.3.4 Provider Contract Decision & M4 Closure | `feature/m4-3-4-contract-decision` | **Formally Closed (Live Payments Disabled)** (Adopted Outcome B: PhonePe v1 classified sandbox-only due to lack of machine-verifiable currency in payload; full 22-vector regression suite added; 38/38 UPI tests pass, 471/471 unit/integration tests pass, 157/157 Playwright E2E pass, 140/140 lint clean; `ENABLE_LIVE_PAYMENTS=false` preserved; all M4 exit criteria verified and passed) |
| M5 Zero-Trust Live Payment Provider Integration | `feature/m5-...` | **Recommended Next Milestone** (Integrate zero-trust payment provider supplying explicit ISO currency evidence and signed webhook entities, or release free-only MVP) |
| M5 Telegram & Operational Workflows | `feature/m5-telegram-operational-workflows` / #35 | **Completed** (Telegram identity & account linking, event private supergroup isolation without shared global group, operational recovery on /start, transactional outbox notifications with quiet hours, /report moderation workflow with emergency escalation; 16/16 tests in test/telegram-workflows.test.js, 487/487 unit/integration tests pass, ENABLE_LIVE_PAYMENTS=false preserved) |
| M6 Organiser Workspace & QR Check-in | `feature/m6-organiser-workspace` | **Recommended Next Milestone** (Organiser event management, door staff roles, dynamic QR check-in & verification) |

## 11. Where the detail lives

Expand Down
173 changes: 117 additions & 56 deletions docs/TELEGRAM_SETUP.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,125 @@
# Telegram setup (owner)

Status today: **NOT CONFIGURED.** All bot, invite, Mini App and Stars code is built and tested against a stubbed Bot API; nothing is live until the steps below are done. Never paste tokens or secrets into chat — set them only as Vercel environment variables.

## 1. Bot (BotFather)
1. In Telegram open **@BotFather** → `/newbot` (or pick your existing bot). Name: *StrangersMeetup*; username e.g. `StrangersMeetupBot` (must be one you own — the old site linked `@StrangersMeetupBot` without it being verified).
2. Set the profile photo to the site logo (`public/favicon.svg`) — Telegram shows it on the login prompt.
3. Copy the token. In Vercel → Project → Settings → Environment Variables add **`TELEGRAM_BOT_TOKEN`** (Preview + Production).
- The project currently has a variable named `Telegram_HTTP_API`. The code accepts it as a fallback *only if it is shaped exactly like a bot token*, but please rename it to `TELEGRAM_BOT_TOKEN` and add it to Preview too.
4. Add **`TELEGRAM_BOT_USERNAME`** (without `@`).
5. Generate a webhook secret (16–256 chars of `A-Z a-z 0-9 _ -`), e.g. `openssl rand -hex 32`, and add **`TELEGRAM_WEBHOOK_SECRET`**.
6. BotFather → your bot → **Bot Settings → Configure Mini App / Menu Button** → URL = your site (`PUBLIC_SITE_URL`). (`scripts/telegram-setup.js` also sets the menu button.)
7. BotFather → **Payments**: nothing to connect for Stars. For sandbox Stars testing use Telegram's *test environment* (separate test account + test bot) and set `TELEGRAM_TEST_ENV=true` on the preview that uses the test bot.

## 2. Website sign-in (Telegram Login, OpenID Connect)
Telegram's current web login is OpenID Connect (the old iframe widget is archived).
1. BotFather mini app → select bot → **Login Widget** → add Allowed URLs:
- `https://<your-domain>` and `https://<your-domain>/auth/telegram/callback`
- the same for the preview URL you test on.
2. Copy **Client ID** → `TELEGRAM_LOGIN_CLIENT_ID`, **Client Secret** → `TELEGRAM_LOGIN_CLIENT_SECRET`.
3. Without these two, `/signin` falls back to the legacy widget (needs `/setdomain` in BotFather for the site domain).

## 3. Private groups (one per membership tier + one for event coordination)
Create **private supergroups** (or channels for broadcast-only):

| Purpose | Env var |
|---|---|
| Event coordination group (venue reveal for paid attendees) | `TG_CHAT_ID_EVENTS` |
| Tier 1 — Insider | `TG_CHAT_ID_TIER_1` |
| Tier 2 — Social Plus | `TG_CHAT_ID_TIER_2` |
| Tier 3 — VIP Circle | `TG_CHAT_ID_TIER_3` |

For each chat:
1. Add the bot as **administrator** with: *Invite users via link* and *Ban users* (and *Delete messages* for moderation). Nothing else is needed.
2. Turn off "Approve new members" (invites are single-use, `member_limit: 1`, 24h expiry).
3. Get the chat id: after the webhook is set (step 4), add the bot and post any message; or temporarily add **@RawDataBot**/**@getidsbot**, read `chat.id` (starts with `-100…`), then remove it.
4. Put the id in the matching env var. A per-event group can instead be set on the event in **/admin → Events → Telegram chat id**.

## 4. Point Telegram at the site
After the env vars are on the deployment (and `PUBLIC_SITE_URL` is set to it):
```
# NIGHTLIFEZ — Telegram Setup & Operational Runbook (Owner)

Status today: **NOT CONFIGURED IN PRODUCTION.** All bot updates, single-use invite links, Mini App authentication, Stars digital memberships, and operational outbox workflows are fully built and verified against the automated test suite. Nothing is live until the owner performs the BotFather and environment setup below.

> [!IMPORTANT]
> **Owner Non-Negotiable Safeguards:**
> 1. Never paste bot tokens or webhook secrets into chat or public pull requests. Set them exclusively as Vercel environment variables.
> 2. Keep `ENABLE_LIVE_PAYMENTS=false` across all environments.
> 3. Do not create a single global attendee group for all events. Every event has its own dedicated private community group to prevent venue leaks and attendee cross-contamination.

---

## 1. Owner Setup Checklist (BotFather)

Complete these steps in Telegram by messaging **@BotFather**:

- [ ] **1. Create the Bot**:
- Open **@BotFather** → `/newbot`.
- Name: **NIGHTLIFEZ**.
- Username: e.g. `NightlifezBot` or `nightlifez_bot` (must end with `bot`).
- [ ] **2. Set Bot Profile & Branding**:
- `/setuserpic` → Upload the NIGHTLIFEZ official logo (`public/favicon.svg` or high-resolution icon).
- `/setdescription` → *"Curated weekend parties, nightlife, social mixers, dinner clubs and trips for adults 21+ across South India."*
- `/setabouttext` → *"NIGHTLIFEZ — Curated social experiences for adults 21+ in Bangalore, Chennai, Kochi, Goa and beyond."*
- [ ] **3. Configure Mini App Menu Button**:
- `/setmenubutton` → Select your bot → URL: `https://<your-site-domain>` (configured as `PUBLIC_SITE_URL`).
- Text: **Open NIGHTLIFEZ**.
- [ ] **4. Configure Web Login (Telegram Login / OpenID Connect)**:
- Bot Settings → **Configure Mini App / Login Widget** → Add Allowed Domains:
- `https://<your-site-domain>`
- `https://<your-site-domain>/auth/telegram/callback`
- Add preview domain for testing if required.
- Record the **Client ID** (`TELEGRAM_LOGIN_CLIENT_ID`) and **Client Secret** (`TELEGRAM_LOGIN_CLIENT_SECRET`).
- Run `/setdomain` for the legacy widget fallback as well.
- [ ] **5. Copy Bot Token to Vercel Environment Variables**:
- Copy the token from BotFather (`123456789:ABC...`).
- In Vercel Project Settings → Environment Variables, add:
- **`TELEGRAM_BOT_TOKEN`** (Preview + Production)
- **`TELEGRAM_BOT_USERNAME`** (bot username without `@`)
- [ ] **6. Generate & Configure Webhook Secret**:
- Generate a 32-character random hex string: `openssl rand -hex 32`
- In Vercel Project Settings → Environment Variables, add:
- **`TELEGRAM_WEBHOOK_SECRET`**

---

## 2. Event Communities & Membership Tier Groups

### A. Dedicated Private Groups for Events (Strict Isolation)
**Rule:** There is **NO global attendee group**. Each event has its own separate private community supergroup.

To set up an event's group:
1. Create a **private supergroup** in Telegram for the event (e.g., *"Indiranagar Mixer — 14 Nov"*).
2. Add the bot as an **Administrator** with these permissions:
- ✅ *Invite users via link* (required to generate single-use 24-hour links)
- ✅ *Ban users* (required to remove joiners who use leaked or unverified invite links, or refunded bookings)
- ✅ *Delete messages* (recommended for moderation)
3. Turn **OFF** *"Approve new members"* (each invite generated by the platform is cryptographically single-use with `member_limit: 1`).
4. Obtain the group's numeric Telegram Chat ID (starts with `-100...`):
- Add **@RawDataBot** or **@getidsbot** to read `chat.id`, then remove it.
5. In NIGHTLIFEZ Admin Dashboard (**Admin → Events → Edit Event**), enter the ID in the **Telegram chat ID** field (`events.telegram_chat_id`).

### B. Membership Tier Private Groups
Create dedicated private supergroups for membership tiers:

| Tier | Code | Environment Variable |
|---|---|---|
| Tier 1 — Insider | `tier_1` | `TG_CHAT_ID_TIER_1` |
| Tier 2 — Social Plus | `tier_2` | `TG_CHAT_ID_TIER_2` |
| Tier 3 — VIP Circle | `tier_3` | `TG_CHAT_ID_TIER_3` |

Follow the same administrator setup (Invite users + Ban users, no manual approval).

### C. Optional Public Broadcast Channel
| Purpose | Identifier / Env Var | Description |
|---|---|---|
| Public Broadcast Channel | `TELEGRAM_CHANNEL_PUBLIC` | Public channel for announcements. No secret attendee details or venue reveals are ever published here. |

---

## 3. Registering the Webhook & Commands

Once environment variables are deployed on Vercel:

```bash
# 1. Pull environment variables locally
vercel env pull .env.setup --environment=production # or preview

# 2. Register webhook and bot commands
set -a; . ./.env.setup; set +a
node scripts/telegram-setup.js
rm .env.setup
```
It calls `setWebhook` (with `secret_token` and `allowed_updates` including `chat_member`, which Telegram only sends when listed explicitly), `setMyCommands`, and the Mini App menu button. Re-runnable.

## 5. Verify
- `GET /api/health` → `telegramBot`, `telegramWebhookSecret`, `telegramEventsChat` all `configured`.
- DM the bot `/start` → welcome + "Open StrangersMeetup" button.
- `/events`, `/mybookings`, `/membership`, `/report test`, `/help`, `/terms`, `/paysupport` all answer.
- Admin: add yourself via `ADMIN_BOOTSTRAP_TELEGRAM_ID` (your numeric Telegram id) and open `/admin`.
The script idempotently configures:
- Webhook URL: `https://<PUBLIC_SITE_URL>/api/telegram/webhook` with `secret_token`
- Allowed update types: `message`, `callback_query`, `pre_checkout_query`, `chat_member`, `my_chat_member`
- Standard bot commands:
- `/start` — Open NIGHTLIFEZ
- `/events` — Upcoming meetups, mixers and trips
- `/mybookings` — Your bookings and private group invites
- `/claim` — Claim tickets bought on web / UPI
- `/membership` — Membership plans
- `/quiet_hours` — Notification quiet hours settings
- `/report` — Report a safety concern
- `/help` — Help and emergency numbers
- `/terms` — Terms and refunds
- `/paysupport` — Payment support

---

## 4. Bot Workflows & Operational Behavior

## What the bot does
| Trigger | Behaviour |
| Trigger / Command | Platform Behavior |
|---|---|
| `/start` | Welcome + Mini App button. `start=event_<slug>` shows that event; `start=booking_<id>` re-issues your invite; `start=alerts` opts in to new-event alerts (`/stopalerts` to opt out). |
| Paid booking (verified webhook) | DM with event details + single-use 24h invite to the event chat. |
| Stars membership paid | DM + invite to tier chat. Renewals extend expiry (DM, no new invite). |
| 3 days before expiry | Reminder DM. |
| Expiry (+1h grace) | Removed from tier chat (ban then unban — not a permanent ban) + DM. |
| Someone joins with another person's invite | Removed automatically; invite revoked; audit log entry. |
| `/report <text>` | Stored in `reports`, visible in admin. |
| **`/start`** | Welcomes attendee with branded NIGHTLIFEZ intro and Mini App button. Performs **operational recovery**: automatically checks if user has confirmed bookings with undelivered invites and delivers them immediately. |
| **`/start link_<token>`** | **Secure Account Linking**: Links authenticated web session or guest booking to this Telegram user without IDOR. |
| **`/start claim_upi_<orderId>_<guestToken>`** | Automatically claims a guest checkout booking for this Telegram user and delivers confirmation and group invite. |
| **`/start event_<slug>`** | Displays card for the specific event with direct link into the Mini App. |
| **`/start booking_<id>`** | Re-issues active 24h single-use group invite for an eligible confirmed booking. |
| **`/quiet_hours`** | Displays current quiet hours window (e.g. 23:00 to 08:00 IST). Toggle with `/quiet_hours on` or `/quiet_hours off`, or customize with `/quiet_hours HH:MM HH:MM`. Non-urgent alerts are deferred during quiet hours. |
| **`/report <text>`** | Submits confidential safety report directly to moderation outbox with rate-limiting protection (max 5/hr). Always displays national emergency helpline **112** and Women's Helpline **1091 / 181**. |
| **Confirmed Booking** | Sends transactional DM with start time, location, booking reference, and single-use 24h private group invite (if event group configured). |
| **Cancellation / Refund** | Automatically revokes active invite links (`revoked_at = now()`) and sends confirmation DM. If user previously joined the chat, removes them from the group. |
| **Unauthorized Joiner** | If an invite link is forwarded and joined by an unconfirmed user, bot immediately kicks the joiner, revokes the link, and writes an audit log (`invite.misuse_removed`). |
39 changes: 38 additions & 1 deletion lib/handlers/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import {
validateInitData, validateLoginWidget, consumeNonce,
oidcConfigured, pkcePair, authorizeUrl, exchangeCode, verifyIdToken
} from '../telegram/auth.js';
import { upsertTelegramUser, confirmAge as confirmAgeSvc } from '../services/users.js';
import {
upsertTelegramUser, confirmAge as confirmAgeSvc,
createAccountLinkToken, getAccountLinkStatus,
getNotificationPreferences, updateNotificationPreferences
} from '../services/users.js';
import {
createSession, setSessionCookie, clearSessionCookie, csrfTokenFor, getSessionUser, requireUser, requireCsrf,
revokeSession, revokeUserSessions
Expand Down Expand Up @@ -232,3 +236,36 @@ export async function confirmAge({ req, res }) {
await confirmAgeSvc(user.id);
sendJson(res, 200, { ok: true });
}

export async function createTelegramLinkToken({ req, res }) {
const user = await getSessionUser(req).catch(() => null);
const { token, linkUrl, expiresAt } = await createAccountLinkToken(user?.id || null);
sendJson(res, 200, { ok: true, token, linkUrl, expiresAt });
}

export async function getTelegramLinkStatus({ res, query }) {
const token = query?.get?.('token');
if (!token) throw new HttpError(400, 'validation', 'token is required');
const status = await getAccountLinkStatus(token);
if (!status) throw new HttpError(404, 'not_found', 'Token not found');
sendJson(res, 200, { ok: true, ...status });
}

export async function getMyNotificationPreferences({ req, res }) {
const user = await requireUser(req);
const prefs = await getNotificationPreferences(user.id);
sendJson(res, 200, { ok: true, preferences: prefs });
}

export async function updateMyNotificationPreferences({ req, res }) {
const user = await requireUser(req);
requireCsrf(req, user);
const body = await readJson(req);
const prefs = await updateNotificationPreferences(user.id, {
remindersEnabled: body.remindersEnabled,
quietHoursEnabled: body.quietHoursEnabled,
quietHoursStart: body.quietHoursStart,
quietHoursEnd: body.quietHoursEnd
});
sendJson(res, 200, { ok: true, preferences: prefs });
}
4 changes: 4 additions & 0 deletions lib/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ const ROUTES = [

// Auth & member
['POST', '/api/auth/telegram', h('auth', 'telegramAuth')],
['POST', '/api/auth/telegram/link-token', h('auth', 'createTelegramLinkToken')],
['GET', '/api/auth/telegram/link-status', h('auth', 'getTelegramLinkStatus')],
['POST', '/api/auth/dev', h('auth', 'devAuth')],
['POST', '/api/auth/preview-reviewer', h('auth', 'previewReviewerAuth')],
['POST', '/api/auth/logout', h('auth', 'logout')],
Expand All @@ -72,6 +74,8 @@ const ROUTES = [
['GET', '/api/me/bookings', h('bookings', 'myBookings')],
['GET', '/api/me/profile', h('profile', 'getProfile')],
['PATCH', '/api/me/profile', h('profile', 'updateProfile')],
['GET', '/api/me/notification-preferences', h('auth', 'getMyNotificationPreferences')],
['PATCH', '/api/me/notification-preferences', h('auth', 'updateMyNotificationPreferences')],
['GET', '/api/me/reports', h('safety', 'listMyReports')],
['POST', '/api/reports', h('safety', 'submitReport')],
['GET', '/api/me/blocks', h('safety', 'listBlocks')],
Expand Down
Loading
Loading