mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
Reported by @ArnasDon: a teammate accepting an invite landed in the
shared account but the WhatsApp Config tab still showed "not
connected." Inbox banner read "WhatsApp not connected" too.
Cause: PR 8 of the multi-user series converted the *webhook* and
the *automations/flows engines* to account-scoped lookups, but
left every user-facing API route + UI page still filtering by
`user_id`. Post-017 `whatsapp_config` and `message_templates` are
account-scoped (one row per account, with `account_id NOT NULL`),
so `.eq('user_id', user.id)` returned zero rows for any teammate
who didn't personally save the config — including everyone who
joined via invite.
This PR completes the conversion across the user-facing surface.
Surface (11 files)
- `src/components/settings/whatsapp-config.tsx` — read accountId
from `useAuth()`; trigger fetch on profile-ready, not just
user-ready; pass accountId through to the supabase query.
- `src/app/(dashboard)/inbox/page.tsx` — resolve account_id from
profiles, query whatsapp_config by account_id for the
connected-banner state.
- `src/app/api/whatsapp/config/route.ts` — GET/POST/DELETE all
resolve account_id from profile. The phone_number_id collision
check (service-role) now compares against `account_id`, not
`user_id` — two accounts still can't share a Meta number, two
teammates in one account share one row by design. INSERT
populates `account_id` (tenancy) + `user_id` (audit).
- `src/app/api/whatsapp/config/verify-registration/route.ts` —
same conversion.
- `src/app/api/whatsapp/send/route.ts` — conversation,
whatsapp_config, message_templates lookups all switch to
account_id; rate-limit key stays per-user.
- `src/app/api/whatsapp/react/route.ts` — conversation +
whatsapp_config switch to account_id.
- `src/app/api/whatsapp/broadcast/route.ts` — whatsapp_config +
message_templates + broadcasts switch to account_id.
- `src/app/api/whatsapp/media/[mediaId]/route.ts` — config
lookup switches to account_id.
- `src/app/api/whatsapp/templates/submit/route.ts` — config
lookup + every message_templates SELECT/upsert now uses
account_id. The upsert ROW gains `account_id` so the
NOT NULL constraint added by migration 017 stops rejecting
new templates (this was an incidental bug — every new
template post-017 would have been broken regardless of
multi-user).
- `src/app/api/whatsapp/templates/sync/route.ts` — same.
- `src/app/api/whatsapp/templates/[id]/route.ts` PATCH +
DELETE — same.
Pattern at every route:
const { data: profile } = await supabase
.from('profiles')
.select('account_id')
.eq('user_id', user.id)
.maybeSingle()
const accountId = profile?.account_id as string | undefined
if (!accountId) return 403
then every downstream `.eq('user_id', user.id)` → `.eq('account_id', accountId)`.
Known follow-up not in this PR
- `message_templates` still has its UNIQUE index on
`(user_id, name, language)` from migration 014; the submit
upsert's `onConflict` targets that same triple. As a result,
two teammates can still INSERT same-named templates without
conflicting. The fix needs a small migration (drop legacy
index, add `(account_id, name, language)` index, switch the
`onConflict` target) — TODO comment left in the route file so
it's not forgotten.
Verification
- `npm run lint` → 0 errors (same 19 pre-existing warnings)
- `npm run typecheck` → clean
- `npm test` → 366/366 pass
- `npm run build` → succeeds
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
91 lines
2.4 KiB
TypeScript
91 lines
2.4 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { createClient } from '@/lib/supabase/server'
|
|
import { getMediaUrl, downloadMedia } from '@/lib/whatsapp/meta-api'
|
|
import { decrypt } from '@/lib/whatsapp/encryption'
|
|
|
|
export async function GET(
|
|
request: Request,
|
|
{ params }: { params: Promise<{ mediaId: string }> }
|
|
) {
|
|
try {
|
|
const { mediaId } = await params
|
|
|
|
if (!mediaId) {
|
|
return NextResponse.json(
|
|
{ error: 'Media ID is required' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
const supabase = await createClient()
|
|
|
|
const {
|
|
data: { user },
|
|
error: authError,
|
|
} = await supabase.auth.getUser()
|
|
|
|
if (authError || !user) {
|
|
return NextResponse.json(
|
|
{ error: 'Unauthorized' },
|
|
{ status: 401 }
|
|
)
|
|
}
|
|
|
|
// Resolve the caller's account_id — whatsapp_config is one-per-
|
|
// account post-multi-user, so a teammate fetching media for a
|
|
// conversation in the shared inbox needs the account's config,
|
|
// not their personal (non-existent) row.
|
|
const { data: profile } = await supabase
|
|
.from('profiles')
|
|
.select('account_id')
|
|
.eq('user_id', user.id)
|
|
.maybeSingle()
|
|
const accountId = profile?.account_id as string | undefined
|
|
if (!accountId) {
|
|
return NextResponse.json(
|
|
{ error: 'Your profile is not linked to an account.' },
|
|
{ status: 403 },
|
|
)
|
|
}
|
|
|
|
// Fetch and decrypt WhatsApp config
|
|
const { data: config, error: configError } = await supabase
|
|
.from('whatsapp_config')
|
|
.select('*')
|
|
.eq('account_id', accountId)
|
|
.single()
|
|
|
|
if (configError || !config) {
|
|
return NextResponse.json(
|
|
{ error: 'WhatsApp not configured' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
const accessToken = decrypt(config.access_token)
|
|
|
|
// Get the download URL from Meta
|
|
const mediaInfo = await getMediaUrl({ mediaId, accessToken })
|
|
|
|
// Download the binary data
|
|
const { buffer, contentType } = await downloadMedia({
|
|
downloadUrl: mediaInfo.url,
|
|
accessToken,
|
|
})
|
|
|
|
return new Response(new Uint8Array(buffer), {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': contentType || mediaInfo.mimeType || 'application/octet-stream',
|
|
'Cache-Control': 'public, max-age=86400',
|
|
},
|
|
})
|
|
} catch (error) {
|
|
console.error('Error in WhatsApp media GET:', error)
|
|
return NextResponse.json(
|
|
{ error: 'Failed to fetch media' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|