CognxSafeTrack commited on
Commit ·
de46926
1
Parent(s): ec8f103
feat(whatsapp-setup): make token optional when org already has one stored
Browse filesIf an org already has a systemUserToken, the /whatsapp-setup route now
reuses it automatically. Admins can reconfigure WABA ID + Phone Number ID
without re-entering the token — useful for adding a new phone number to
an existing connected account.
apps/admin/src/pages/SettingsPage.tsx
CHANGED
|
@@ -284,13 +284,15 @@ export default function SettingsPage() {
|
|
| 284 |
/>
|
| 285 |
</div>
|
| 286 |
<div>
|
| 287 |
-
<label className="block text-xs text-slate-400 mb-1">
|
|
|
|
|
|
|
|
|
|
| 288 |
<input
|
| 289 |
-
required
|
| 290 |
type="password"
|
| 291 |
value={waForm.accessToken}
|
| 292 |
onChange={e => setWaForm(f => ({ ...f, accessToken: e.target.value }))}
|
| 293 |
-
placeholder=
|
| 294 |
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-xs font-mono focus:ring-2 focus:ring-emerald-500 outline-none"
|
| 295 |
/>
|
| 296 |
</div>
|
|
|
|
| 284 |
/>
|
| 285 |
</div>
|
| 286 |
<div>
|
| 287 |
+
<label className="block text-xs text-slate-400 mb-1">
|
| 288 |
+
System User Token
|
| 289 |
+
{org.systemUserToken && <span className="ml-2 text-emerald-400">(optionnel — token existant conservé)</span>}
|
| 290 |
+
</label>
|
| 291 |
<input
|
|
|
|
| 292 |
type="password"
|
| 293 |
value={waForm.accessToken}
|
| 294 |
onChange={e => setWaForm(f => ({ ...f, accessToken: e.target.value }))}
|
| 295 |
+
placeholder={org.systemUserToken ? 'Laisser vide pour conserver le token actuel' : 'EAAxxxxxxx...'}
|
| 296 |
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-xs font-mono focus:ring-2 focus:ring-emerald-500 outline-none"
|
| 297 |
/>
|
| 298 |
</div>
|
apps/api/src/routes/organizations.ts
CHANGED
|
@@ -133,31 +133,42 @@ export async function organizationRoutes(fastify: FastifyInstance) {
|
|
| 133 |
|
| 134 |
const { phoneNumber, phoneNumberId, wabaId } = body.data;
|
| 135 |
|
| 136 |
-
// Resolve token: request body
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
}
|
| 141 |
|
| 142 |
// Exchange OAuth code for a long-lived token if needed.
|
| 143 |
// Meta Embedded Signup returns a short-lived OAuth code (not starting with EAA).
|
| 144 |
-
|
| 145 |
-
if (!accessToken.startsWith('EAA')) {
|
| 146 |
const appId = process.env.META_APP_ID;
|
| 147 |
const appSecret = process.env.WHATSAPP_APP_SECRET;
|
| 148 |
if (!appId || !appSecret) {
|
| 149 |
return reply.code(500).send({ error: 'META_APP_ID and WHATSAPP_APP_SECRET must be configured to exchange OAuth code' });
|
| 150 |
}
|
| 151 |
-
// Step 1: code → short-lived user access token
|
| 152 |
const exchangeRes = await fetch(
|
| 153 |
-
`https://graph.facebook.com/oauth/access_token?client_id=${appId}&client_secret=${appSecret}&code=${
|
| 154 |
);
|
| 155 |
const exchangeData = await exchangeRes.json() as { access_token?: string; error?: { message: string } };
|
| 156 |
if (!exchangeData.access_token) {
|
| 157 |
logger.error({ detail: exchangeData.error?.message }, '[WHATSAPP-SETUP] OAuth code exchange failed');
|
| 158 |
return reply.code(400).send({ error: 'Failed to exchange OAuth code with Meta', detail: exchangeData.error?.message });
|
| 159 |
}
|
| 160 |
-
// Step 2: short-lived → long-lived token (60 days)
|
| 161 |
const longLivedRes = await fetch(
|
| 162 |
`https://graph.facebook.com/oauth/access_token?grant_type=fb_exchange_token&client_id=${appId}&client_secret=${appSecret}&fb_exchange_token=${exchangeData.access_token}`
|
| 163 |
);
|
|
@@ -170,7 +181,7 @@ export async function organizationRoutes(fastify: FastifyInstance) {
|
|
| 170 |
await prisma.$transaction(async (tx) => {
|
| 171 |
await tx.organization.update({
|
| 172 |
where: { id },
|
| 173 |
-
data: encryptSecrets({ systemUserToken: realToken, wabaId, systemUserTokenIssuedAt: new Date() })
|
| 174 |
});
|
| 175 |
|
| 176 |
if (phoneNumberId) {
|
|
|
|
| 133 |
|
| 134 |
const { phoneNumber, phoneNumberId, wabaId } = body.data;
|
| 135 |
|
| 136 |
+
// Resolve token: request body → env var → existing stored token (so re-entry is not needed)
|
| 137 |
+
let realToken: string | null = body.data.accessToken || process.env.WHATSAPP_ACCESS_TOKEN || null;
|
| 138 |
+
|
| 139 |
+
if (!realToken) {
|
| 140 |
+
// Fall back to the token already stored for this org
|
| 141 |
+
const existing = await prisma.organization.findUnique({
|
| 142 |
+
where: { id },
|
| 143 |
+
select: { systemUserToken: true }
|
| 144 |
+
});
|
| 145 |
+
if (existing?.systemUserToken) {
|
| 146 |
+
const { decryptSecrets } = await import('../services/organization');
|
| 147 |
+
const decrypted = decryptSecrets(existing as any);
|
| 148 |
+
realToken = (decrypted as any).systemUserToken ?? null;
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
if (!realToken) {
|
| 153 |
+
return reply.code(400).send({ error: 'accessToken required — no token provided and none stored for this organization' });
|
| 154 |
}
|
| 155 |
|
| 156 |
// Exchange OAuth code for a long-lived token if needed.
|
| 157 |
// Meta Embedded Signup returns a short-lived OAuth code (not starting with EAA).
|
| 158 |
+
if (!realToken.startsWith('EAA')) {
|
|
|
|
| 159 |
const appId = process.env.META_APP_ID;
|
| 160 |
const appSecret = process.env.WHATSAPP_APP_SECRET;
|
| 161 |
if (!appId || !appSecret) {
|
| 162 |
return reply.code(500).send({ error: 'META_APP_ID and WHATSAPP_APP_SECRET must be configured to exchange OAuth code' });
|
| 163 |
}
|
|
|
|
| 164 |
const exchangeRes = await fetch(
|
| 165 |
+
`https://graph.facebook.com/oauth/access_token?client_id=${appId}&client_secret=${appSecret}&code=${realToken}&redirect_uri=`
|
| 166 |
);
|
| 167 |
const exchangeData = await exchangeRes.json() as { access_token?: string; error?: { message: string } };
|
| 168 |
if (!exchangeData.access_token) {
|
| 169 |
logger.error({ detail: exchangeData.error?.message }, '[WHATSAPP-SETUP] OAuth code exchange failed');
|
| 170 |
return reply.code(400).send({ error: 'Failed to exchange OAuth code with Meta', detail: exchangeData.error?.message });
|
| 171 |
}
|
|
|
|
| 172 |
const longLivedRes = await fetch(
|
| 173 |
`https://graph.facebook.com/oauth/access_token?grant_type=fb_exchange_token&client_id=${appId}&client_secret=${appSecret}&fb_exchange_token=${exchangeData.access_token}`
|
| 174 |
);
|
|
|
|
| 181 |
await prisma.$transaction(async (tx) => {
|
| 182 |
await tx.organization.update({
|
| 183 |
where: { id },
|
| 184 |
+
data: encryptSecrets({ systemUserToken: realToken!, wabaId, systemUserTokenIssuedAt: new Date() })
|
| 185 |
});
|
| 186 |
|
| 187 |
if (phoneNumberId) {
|