Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/web/.env.development.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
# Drizzle / libSQL (local sqlite)
DATABASE_URL=file:./local.db
TURSO_AUTH_TOKEN=
NEXT_PUBLIC_APP_URL=http://localhost:3000
BETTER_AUTH_SECRET=dev-secret-please-change-dev-secret-32ch

# OAuth (optional)
Expand Down
1 change: 0 additions & 1 deletion apps/web/.env.production.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
# Drizzle / libSQL (Turso)
DATABASE_URL=libsql://<db-name>-<org>.turso.io
TURSO_AUTH_TOKEN=
NEXT_PUBLIC_APP_URL=https://<your-domain>
BETTER_AUTH_SECRET=

# OAuth (optional)
Expand Down
16 changes: 0 additions & 16 deletions apps/web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,6 @@ import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare';

initOpenNextCloudflareForDev();

const appUrl = process.env.NEXT_PUBLIC_APP_URL
? new URL(process.env.NEXT_PUBLIC_APP_URL)
: null;

const appImageRemotePatterns = appUrl
? [
{
protocol: appUrl.protocol.replace(':', ''),
hostname: appUrl.hostname,
port: appUrl.port,
pathname: '/api/files/**',
},
]
: [];

/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
Expand All @@ -30,7 +15,6 @@ const nextConfig = {
protocol: 'http',
hostname: 'localhost',
},
...appImageRemotePatterns,
],
},
transpilePackages: [
Expand Down
8 changes: 4 additions & 4 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
"private": true,
"type": "module",
"scripts": {
"build": "next build",
"build:worker": "opennextjs-cloudflare build",
"build": "SKIP_ENV_VALIDATION=true next build",
"build:worker": "SKIP_ENV_VALIDATION=true opennextjs-cloudflare build",
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"deploy": "SKIP_ENV_VALIDATION=true opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"dev": "next dev -p 3000",
"lint": "eslint . --cache --max-warnings 0",
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"preview": "SKIP_ENV_VALIDATION=true opennextjs-cloudflare build && opennextjs-cloudflare preview",
"start": "next start",
"typecheck": "tsc --noEmit --tsBuildInfoFile .tsbuildinfo"
},
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/(landing)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ async function LandingPageLayout({ children }: { children: ReactNode }) {
const databaseUrl = process.env['DATABASE_URL'];
const hasSessionRuntimeEnv =
!!databaseUrl &&
(!databaseUrl.startsWith('libsql://') || !!process.env['TURSO_AUTH_TOKEN']) &&
(!databaseUrl.startsWith('libsql://') ||
!!process.env['TURSO_AUTH_TOKEN']) &&
!!process.env['BETTER_AUTH_SECRET'] &&
!!process.env['NEXT_PUBLIC_APP_URL'] &&
!!process.env['ALLOW_SIGNIN_SIGNUP'];
let isLoggedIn = false;

Expand Down
3 changes: 0 additions & 3 deletions apps/web/src/app/(main)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import * as React from 'react';
import { type Metadata } from 'next';

import { env } from '@formbase/env';

import { api } from '~/lib/trpc/server';

import { Forms } from './_components/forms';
import { CreateFormDialog } from './_components/new-form-dialog';
import { FormsSkeleton } from './_components/posts-skeleton';

export const metadata: Metadata = {
metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),
title: 'Forms',
description: 'Manage your form endpoints',
};
Expand Down
2 changes: 0 additions & 2 deletions apps/web/src/app/(main)/dashboard/settings/api-keys/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import { redirect } from 'next/navigation';
import type { Metadata } from 'next';

import { getSession } from '@formbase/auth/server';
import { env } from '@formbase/env';
import { Separator } from '@formbase/ui/primitives/separator';

import { ApiKeysSection } from './api-keys-section';

export const metadata: Metadata = {
metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),
title: 'API Keys | Formbase',
description: 'Manage your API keys for programmatic access',
};
Expand Down
2 changes: 0 additions & 2 deletions apps/web/src/app/(main)/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import { redirect } from 'next/navigation';
import type { Metadata } from 'next';

import { getSession } from '@formbase/auth/server';
import { env } from '@formbase/env';
import { Separator } from '@formbase/ui/primitives/separator';

import { ProfileForm } from './profile-form';

export const metadata: Metadata = {
metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),
title: 'Settings | Formbase',
description: 'Manage your account settings',
};
Expand Down
13 changes: 9 additions & 4 deletions apps/web/src/app/api/s/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { userAgent } from 'next/server';

import { type RouterOutputs } from '@formbase/api';
import { env } from '@formbase/env';

import { sendMail } from '~/lib/email/mailer';
import { renderNewSubmissionEmail } from '~/lib/email/templates/new-submission';
Expand Down Expand Up @@ -64,14 +63,15 @@ async function getFormData(request: Request): Promise<FormDataResult> {
async function processFileUploads(
formData: Record<string, Blob | string | undefined>,
formDataFromRequest: FormData,
requestOrigin: string,
) {
const fileKeys = Object.keys(formData).filter(
(key) => formData[key] instanceof Blob,
);

for (const key of fileKeys) {
const file = formDataFromRequest.get(key) as File;
const fileUrl = await uploadFileFromBlob({ file });
const fileUrl = await uploadFileFromBlob({ file, origin: requestOrigin });
assignFileOrImage({ formData, key, fileUrl });
}
}
Expand Down Expand Up @@ -103,6 +103,7 @@ export async function POST(
const { id } = await params;

const formId = id;
const requestOrigin = new URL(request.url).origin;
const form = await api.form.getFormById({ formId });
if (!form) {
return new Response('Form not found', {
Expand All @@ -117,7 +118,11 @@ export async function POST(
const { data: formData } = formDataResult;

if (formDataResult.source === 'formData') {
await processFileUploads(formData, formDataResult.rawFormData);
await processFileUploads(
formData,
formDataResult.rawFormData,
requestOrigin,
);
}

const honeypotField = form.honeypotField;
Expand Down Expand Up @@ -158,7 +163,7 @@ export async function POST(
return new Response(null, {
status: 303,
headers: {
Location: `${env.NEXT_PUBLIC_APP_URL}/s/${formId}`,
Location: new URL(`/s/${formId}`, requestOrigin).toString(),
...CORS_HEADERS,
},
});
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/app/api/v1/openapi.json/route.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { generateOpenApiDocument } from 'trpc-to-openapi';

import { apiV1Router } from '@formbase/api/routers/api-v1';
import { env } from '@formbase/env';

export const dynamic = 'force-dynamic';

export function GET() {
export function GET(request: Request) {
const openApiDocument = generateOpenApiDocument(apiV1Router, {
title: 'Formbase API',
version: '1.0.0',
baseUrl: `${env.NEXT_PUBLIC_APP_URL}/api/v1`,
baseUrl: `${new URL(request.url).origin}/api/v1`,
description: 'Public REST API for managing forms and submissions.',
securitySchemes: {
bearerAuth: {
Expand Down
19 changes: 17 additions & 2 deletions apps/web/src/app/robots.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
import { type MetadataRoute } from 'next';
import { headers } from 'next/headers';

import { absoluteUrl } from '@formbase/utils/server';

export default function robots(): MetadataRoute.Robots {
export default async function robots(): Promise<MetadataRoute.Robots> {
const headersList = await headers();
const host = headersList.get('x-forwarded-host') ?? headersList.get('host');

if (!host) {
throw new Error('Host header is required to build robots URLs');
}

const protocol =
headersList.get('x-forwarded-proto') ??
(host.startsWith('localhost') || host.startsWith('127.')
? 'http'
: 'https');
const origin = `${protocol}://${host}`;

return {
rules: {
userAgent: '*',
allow: '/',
},
sitemap: absoluteUrl('/sitemap.xml'),
sitemap: absoluteUrl('/sitemap.xml', origin),
};
}
17 changes: 15 additions & 2 deletions apps/web/src/app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { type MetadataRoute } from 'next';
import { headers } from 'next/headers';

import { absoluteUrl } from '@formbase/utils/server';

// eslint-disable-next-line @typescript-eslint/require-await
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const headersList = await headers();
const host = headersList.get('x-forwarded-host') ?? headersList.get('host');

if (!host) {
throw new Error('Host header is required to build sitemap URLs');
}

const protocol =
headersList.get('x-forwarded-proto') ??
(host.startsWith('localhost') || host.startsWith('127.')
? 'http'
: 'https');
const origin = `${protocol}://${host}`;
const routes = ['/', '/dashboard'].map((route) => ({
url: absoluteUrl(route),
url: absoluteUrl(route, origin),
lastModified: new Date().toISOString(),
}));

Expand Down
39 changes: 31 additions & 8 deletions apps/web/src/lib/upload-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,15 @@ const toHex = (buffer: ArrayBuffer) =>
.join('');

async function hmac(key: ArrayBuffer | Uint8Array | string, value: string) {
const keyData =
typeof key === 'string'
? encoder.encode(key)
: key instanceof Uint8Array
? new Uint8Array(key).buffer
: key;
const cryptoKey = await crypto.subtle.importKey(
'raw',
typeof key === 'string' ? encoder.encode(key) : key,
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
Expand Down Expand Up @@ -162,8 +168,10 @@ const createStorageUrl = (config: StorageConfig, key?: string) => {
);
};

const createFileUrl = (key: string) =>
new URL(`/api/files/${encodePath(key)}`, env.NEXT_PUBLIC_APP_URL).toString();
const createFileUrl = (key: string, origin?: string) => {
const path = `/api/files/${encodePath(key)}`;
return origin ? new URL(path, origin).toString() : path;
};

async function createPresignedUrl({
config,
Expand Down Expand Up @@ -244,16 +252,29 @@ async function sendStorageRequest({
key?: string;
method: 'PUT';
}) {
const url = await createPresignedUrl({ config, headers, key, method });
return fetch(url, { body, headers, method });
const url = await createPresignedUrl({
config,
method,
...(headers ? { headers } : {}),
...(key ? { key } : {}),
});
return fetch(url, {
method,
...(body === undefined ? {} : { body }),
...(headers ? { headers } : {}),
});
}

const getFileExtension = (mimetype: string) => {
const subtype = mimetype.split('/')[1];
return subtype?.split('+').at(0) ?? 'bin';
};

export async function uploadFile(file: BodyInit, mimetype: string) {
export async function uploadFile(
file: BodyInit,
mimetype: string,
origin?: string,
) {
const config = getStorageConfig();
const name = `${generateId(15)}.${getFileExtension(mimetype)}`;
const contentType = mimetype || 'application/octet-stream';
Expand All @@ -272,15 +293,17 @@ export async function uploadFile(file: BodyInit, mimetype: string) {
throw new Error(`Storage upload failed with status ${response.status}`);
}

return createFileUrl(name);
return createFileUrl(name, origin);
}

export async function uploadFileFromBlob({
file,
origin,
}: {
file: Blob;
origin?: string;
}): Promise<string> {
return uploadFile(file, file.type);
return uploadFile(file, file.type, origin);
}

export function assignFileOrImage({
Expand Down
2 changes: 1 addition & 1 deletion apps/web/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "node_modules/wrangler/config-schema.json",
"name": "formbase-web",
"main": ".open-next/worker.js",
"workers_dev": false,
"workers_dev": true,
"preview_urls": false,
"compatibility_date": "2026-05-01",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
Expand Down
4 changes: 1 addition & 3 deletions packages/auth/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { createAuthClient } from 'better-auth/react';

export const authClient = createAuthClient({
baseURL: process.env['NEXT_PUBLIC_APP_URL'],
});
export const authClient = createAuthClient();

export const { signIn, signUp, signOut, useSession } = authClient;
Loading
Loading