From 9dbda69db175a4f181b1d04a9bc554bf0b319697 Mon Sep 17 00:00:00 2001 From: debu-2 Date: Tue, 13 Oct 2026 01:33:49 +0530 Subject: [PATCH] token --- backend/app/routes_admin.py | 76 ++++++++++++- frontend/src/features/admin/AdminPortal.tsx | 12 +- .../src/features/admin/AdminTokensPage.tsx | 103 ++++++++++++++++++ frontend/src/index.css | 8 ++ frontend/src/lib/api.ts | 26 +++++ 5 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 frontend/src/features/admin/AdminTokensPage.tsx diff --git a/backend/app/routes_admin.py b/backend/app/routes_admin.py index 7ed7882..e232272 100644 --- a/backend/app/routes_admin.py +++ b/backend/app/routes_admin.py @@ -5,7 +5,7 @@ from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Response, status from pydantic import BaseModel, Field from sqlalchemy import select -from sqlalchemy.orm import Session, joinedload +from sqlalchemy.orm import Session, joinedload, selectinload from .auth import ADMIN_COOKIE_NAME, create_admin_token, current_admin from .config import settings @@ -23,7 +23,10 @@ from .models import ( TemplateConfig, UrmiOtpProject, UserAccount, + WhatsAppBusinessAccount, WhatsAppMessage, + WhatsAppMetaConnection, + MetaConnection, ) from .phone_number_validators import normalize_phone_number, validate_per_min_price, validate_price from .routes_urmiotp import serialize_project @@ -578,6 +581,77 @@ def _whatsapp_client(db: Session) -> WhatsAppGraphClient: ) +def _token_account(connection: WhatsAppMetaConnection, account: WhatsAppBusinessAccount) -> dict: + return { + "id": str(account.id), + "name": account.name, + "status": account.status, + "source": "token" if connection.granted_scopes == "manual" else "signup", + "access_token": connection.access_token or "", + "waba_id": account.waba_id, + "phones": [ + { + "id": str(phone.id), + "display_phone": phone.display_phone, + "phone_number_id": phone.phone_number_id, + "status": phone.status, + } + for phone in account.phones + ], + "updated_at": account.updated_at, + } + + +@router.get("/connection-tokens") +def admin_connection_tokens(db: Session = Depends(get_db), _: str = Depends(current_admin)) -> dict: + """Every customer's saved WhatsApp and Facebook tokens. Admin session only.""" + users = db.scalars(select(UserAccount).order_by(UserAccount.email.asc())).all() + whatsapp_rows = db.scalars( + select(WhatsAppMetaConnection).options( + selectinload(WhatsAppMetaConnection.accounts).selectinload(WhatsAppBusinessAccount.phones) + ) + ).all() + facebook_rows = db.scalars(select(MetaConnection)).all() + whatsapp_by_user: dict = {} + for connection in whatsapp_rows: + saved = [_token_account(connection, account) for account in connection.accounts] + if not saved and connection.access_token: + saved.append({ + "id": str(connection.id), + "name": connection.business_name or "WhatsApp", + "status": connection.status, + "source": "token" if connection.granted_scopes == "manual" else "signup", + "access_token": connection.access_token, + "waba_id": "", + "phones": [], + "updated_at": connection.updated_at, + }) + if saved: + whatsapp_by_user.setdefault(connection.user_id, []).extend(saved) + facebook_by_user = {row.user_id: row for row in facebook_rows} + payload = [] + for user in users: + facebook = facebook_by_user.get(user.id) + accounts = whatsapp_by_user.get(user.id, []) + facebook_token = facebook.access_token if facebook and facebook.status != "disconnected" else "" + if not accounts and not facebook_token: + continue + payload.append({ + "id": str(user.id), + "name": user.display_name, + "email": user.email, + "whatsapp_accounts": accounts, + "facebook": None if facebook is None or not facebook_token else { + "status": facebook.status, + "user_name": facebook.meta_user_name, + "access_token": facebook_token, + "expires_at": facebook.token_expires_at, + "connected_at": facebook.connected_at, + }, + }) + return {"users": payload} + + @router.get("/whatsapp/config") def admin_whatsapp_config(db: Session = Depends(get_db), _: str = Depends(current_admin)) -> dict: return { diff --git a/frontend/src/features/admin/AdminPortal.tsx b/frontend/src/features/admin/AdminPortal.tsx index 0cdad0f..8c298cf 100644 --- a/frontend/src/features/admin/AdminPortal.tsx +++ b/frontend/src/features/admin/AdminPortal.tsx @@ -10,6 +10,7 @@ import { type AdminTemplateConfig, type AdminUrmiOtpProject, } from '../../lib/api' +import { AdminTokensPage } from './AdminTokensPage' import { AdminWhatsAppPage } from './AdminWhatsAppPage' const KYC_DOC_LABELS: Record = { @@ -392,7 +393,7 @@ function AdminCompliance() { export function AdminApp() { const [authenticated, setAuthenticated] = useState(null) - const [section, setSection] = useState<'compliance' | 'whatsapp'>('compliance') + const [section, setSection] = useState<'compliance' | 'whatsapp' | 'tokens'>('compliance') useEffect(() => { adminApi.me().then(() => setAuthenticated(true)).catch(() => setAuthenticated(false)) @@ -426,6 +427,13 @@ export function AdminApp() { > WhatsApp Business + @@ -433,7 +441,7 @@ export function AdminApp() {
{authenticated - ? (section === 'compliance' ? : ) + ? (section === 'compliance' ? : section === 'tokens' ? : ) : setAuthenticated(true)} />}
diff --git a/frontend/src/features/admin/AdminTokensPage.tsx b/frontend/src/features/admin/AdminTokensPage.tsx new file mode 100644 index 0000000..d404687 --- /dev/null +++ b/frontend/src/features/admin/AdminTokensPage.tsx @@ -0,0 +1,103 @@ +import { Copy } from 'lucide-react' +import { useEffect, useState } from 'react' +import { adminApi, type AdminConnectionTokens } from '../../lib/api' + +type UserRow = AdminConnectionTokens['users'][number] +type WhatsAppAccount = UserRow['whatsapp_accounts'][number] + +function TokenValue({ label, value }: { label: string; value: string }) { + const [copied, setCopied] = useState(false) + const shown = value || 'Not saved' + + async function copy() { + if (!value) return + await navigator.clipboard.writeText(value) + setCopied(true) + window.setTimeout(() => setCopied(false), 1500) + } + + return ( +
+
+ {label} + {value && ( + + )} +
+ {shown} +
+ ) +} + +function WhatsAppTokens({ account }: { account: WhatsAppAccount }) { + const phoneIds = account.phones.map((phone) => phone.phone_number_id).filter(Boolean) + return ( +
+
+
+

{account.name}

+

{account.source === 'token' ? 'Pasted token' : 'Facebook signup'} · {account.status}

+
+
+ + + + {account.phones.length > 0 && ( +

+ {account.phones.map((phone) => phone.display_phone || phone.phone_number_id).join(' · ')} +

+ )} +
+ ) +} + +export function AdminTokensPage() { + const [users, setUsers] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + + useEffect(() => { + adminApi.connectionTokens() + .then((payload) => setUsers(payload.users)) + .catch((reason) => setError(reason instanceof Error ? reason.message : 'Could not load tokens')) + .finally(() => setLoading(false)) + }, []) + + if (loading) return
Loading tokens…
+ + return ( +
+
+ Connections +

Saved tokens

+

Each customer account, with the WhatsApp access token, account ID, and phone number ID, plus the Facebook token when one is saved.

+
+ {error &&

{error}

} + {!error && users.length === 0 &&

No tokens have been saved yet.

} + {users.map((user) => ( +
+
+
+

{user.name}

+

{user.email}

+
+
+ {user.whatsapp_accounts.map((account) => )} + {user.facebook && ( +
+
+
+

Facebook

+

{user.facebook.user_name || 'Facebook user'} · {user.facebook.status}

+
+
+ +
+ )} +
+ ))} +
+ ) +} diff --git a/frontend/src/index.css b/frontend/src/index.css index a51c14b..3146890 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -864,6 +864,14 @@ main { padding: 30px clamp(20px, 3vw, 44px) 50px; } .admin-login-page { min-height: calc(100vh - 72px); display: grid; place-items: center; } .admin-login-card { width: min(420px, 100%); } .admin-kyc-card { margin-bottom: 18px; } +.admin-token-account { margin-top: 14px; padding-top: 12px; border-top: 1px solid #e6ebf2; } +.admin-token-account h3 { margin: 0; color: #14233b; font-size: 16px; } +.admin-token-account .panel-heading p { margin: 4px 0 0; } +.admin-token-value { display: grid; gap: 6px; margin-top: 12px; } +.admin-token-label { display: flex; align-items: center; justify-content: space-between; gap: 10px; } +.admin-token-label strong { color: #35435a; font-size: 13px; } +.admin-token-value code { display: block; white-space: pre-wrap; word-break: break-all; padding: 10px 12px; border-radius: 8px; background: #f6f8fb; border: 1px solid #e5eaf1; color: #17253e; font-size: 12px; } +.admin-token-phones { margin: 8px 0 0; color: #6d7c90; font-size: 12px; } .admin-actions { display: flex; gap: 8px; } /* Message settings accordion */ diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index d1256ed..aa2d3d0 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -948,6 +948,31 @@ export type AdminWhatsAppSendPayload = { reaction_emoji?: string } +export type AdminConnectionTokens = { + users: Array<{ + id: string + name: string + email: string + whatsapp_accounts: Array<{ + id: string + name: string + status: string + source: 'token' | 'signup' + access_token: string + waba_id: string + phones: Array<{ id: string; display_phone: string; phone_number_id: string; status: string }> + updated_at: string + }> + facebook: { + status: string + user_name: string + access_token: string + expires_at: string | null + connected_at: string | null + } | null + }> +} + export const adminApi = { me: () => jsonRequest<{ username: string }>('/api/v1/admin/me'), login: (username: string, password: string) => @@ -1008,6 +1033,7 @@ export const adminApi = { method: 'PATCH', body: JSON.stringify(payload), }), + connectionTokens: () => jsonRequest('/api/v1/admin/connection-tokens'), whatsappConfig: () => jsonRequest('/api/v1/admin/whatsapp/config'), updateWhatsAppConfig: (payload: { phone_number_id?: string