CognxSafeTrack commited on
Commit
de46926
·
1 Parent(s): ec8f103

feat(whatsapp-setup): make token optional when org already has one stored

Browse files

If 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">System User Token *</label>
 
 
 
288
  <input
289
- required
290
  type="password"
291
  value={waForm.accessToken}
292
  onChange={e => setWaForm(f => ({ ...f, accessToken: e.target.value }))}
293
- placeholder="EAAxxxxxxx..."
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 first, then env var (for platform-owner direct setup)
137
- const accessToken = body.data.accessToken || process.env.WHATSAPP_ACCESS_TOKEN;
138
- if (!accessToken) {
139
- return reply.code(400).send({ error: 'accessToken required — set it in the request or configure WHATSAPP_ACCESS_TOKEN env var' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- let realToken = accessToken;
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=${accessToken}&redirect_uri=`
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) {