fix: route browser→API calls through BFF to avoid CORS

- Create /api/onboard/request-otp and /api/auth/member/login BFF routes
- Update OnboardingForm and member-login page to call BFF instead of API directly
- Add vercel.json with monorepo build config and rootDirectory hint
- Add .vercelignore to exclude .turbo, node_modules, .next (was uploading 2.3GB)
- Add apps/web/.gitignore and update root .gitignore for tsbuildinfo files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 21:01:05 +05:30
parent a3798b3059
commit 5a0f7aa58f
8 changed files with 52 additions and 6 deletions
+1
View File
@@ -8,3 +8,4 @@ coverage
.env.production
sessions/
*.tsbuildinfo
.vercel
+14
View File
@@ -0,0 +1,14 @@
node_modules
.turbo
.next
dist
build
coverage
*.tsbuildinfo
.git
apps/worker/sessions
apps/api/sessions
sessions
.env
.env.*
!.env.example
+1
View File
@@ -0,0 +1 @@
.vercel
@@ -0,0 +1,14 @@
import { getApiBaseUrl, jsonResponse } from '../../../../_lib/api';
export const dynamic = 'force-dynamic';
export async function POST(req: Request): Promise<Response> {
const body = await req.json().catch(() => ({}));
const upstream = await fetch(`${getApiBaseUrl()}/public/auth/member-login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(body),
cache: 'no-store',
});
return jsonResponse(await upstream.json().catch(() => ({})), upstream.status);
}
@@ -0,0 +1,14 @@
import { getApiBaseUrl, jsonResponse } from '../../../_lib/api';
export const dynamic = 'force-dynamic';
export async function POST(req: Request): Promise<Response> {
const body = await req.json().catch(() => ({}));
const upstream = await fetch(`${getApiBaseUrl()}/public/auth/request-otp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(body),
cache: 'no-store',
});
return jsonResponse(await upstream.json().catch(() => ({})), upstream.status);
}
+1 -3
View File
@@ -11,8 +11,6 @@ import { Input } from '../_components/ui/input';
import { Button } from '../_components/ui/button';
import Link from 'next/link';
const API_BASE = process.env['NEXT_PUBLIC_API_URL'] ?? 'http://localhost:3001';
const phoneSchema = z.object({ phone: z.string().min(8, 'Enter your WhatsApp number') });
const otpSchema = z.object({ code: z.string().length(6, 'Enter the 6-digit code') });
type PhoneValues = z.infer<typeof phoneSchema>;
@@ -28,7 +26,7 @@ export default function MemberLoginPage() {
const otpForm = useForm<OtpValues>({ resolver: zodResolver(otpSchema), defaultValues: { code: '' } });
async function onPhone(values: PhoneValues) {
const res = await fetch(`${API_BASE}/public/auth/member-login`, {
const res = await fetch('/api/auth/member/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: values.phone }),
+1 -3
View File
@@ -43,8 +43,6 @@ const SCOPE_META: ScopeMeta[] = [
},
];
const API_BASE = process.env['NEXT_PUBLIC_API_URL'] ?? 'http://localhost:3001';
export function OnboardingForm({ token, groupName, tenantName, defaultScopes, defaultRetentionDays }: Props) {
const [step, setStep] = useState<Step>('welcome');
const [phone, setPhone] = useState('');
@@ -62,7 +60,7 @@ export function OnboardingForm({ token, groupName, tenantName, defaultScopes, de
setError(null);
setBusy(true);
try {
const res = await fetch(`${API_BASE}/public/auth/request-otp`, {
const res = await fetch('/api/onboard/request-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ onboardingToken: token, phone }),
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "nextjs",
"buildCommand": "cd ../.. && pnpm turbo build --filter=@tower/web",
"installCommand": "cd ../.. && pnpm install --frozen-lockfile"
}