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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ web_modules/

# Next.js build output
.next
.open-next
next-env.d.ts
out

Expand All @@ -110,6 +111,7 @@ dist

# Serverless directories
.serverless/
.wrangler/

# FuseBox cache
.fusebox/
Expand Down
1 change: 1 addition & 0 deletions apps/web/.dev.vars
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NEXTJS_ENV=development
3 changes: 3 additions & 0 deletions apps/web/.env.development.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ UMAMI_TRACKING_ID=
STORAGE_ENDPOINT=localhost
STORAGE_PORT=9002
STORAGE_USESSL=false
STORAGE_FORCE_PATH_STYLE=true
STORAGE_ACCESS_KEY=formbase
STORAGE_SECRET_KEY=password
STORAGE_BUCKET=formbase
STORAGE_REGION=us-east-1
CRON_SECRET=
7 changes: 5 additions & 2 deletions apps/web/.env.production.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Production environment (.env.production)
# Use these values in Vercel environment settings.
# production environment (.env.production)
# use these values in Cloudflare Workers build variables and runtime secrets.

# Drizzle / libSQL (Turso)
DATABASE_URL=libsql://<db-name>-<org>.turso.io
Expand Down Expand Up @@ -27,6 +27,9 @@ UMAMI_TRACKING_ID=
STORAGE_ENDPOINT=<account-id>.r2.cloudflarestorage.com
STORAGE_PORT=443
STORAGE_USESSL=true
STORAGE_FORCE_PATH_STYLE=true
STORAGE_ACCESS_KEY=
STORAGE_SECRET_KEY=
STORAGE_BUCKET=
STORAGE_REGION=auto
CRON_SECRET=
29 changes: 27 additions & 2 deletions apps/web/next.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
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 @@ -11,6 +30,7 @@ const nextConfig = {
protocol: 'http',
hostname: 'localhost',
},
...appImageRemotePatterns,
],
},
transpilePackages: [
Expand All @@ -20,9 +40,14 @@ const nextConfig = {
'@formbase/env',
'@formbase/ui',
'@formbase/utils',
"@formbase/tailwind",
'@formbase/tailwind',
],
serverExternalPackages: [
'libsql',
'@libsql/client',
'@libsql/isomorphic-fetch',
'@libsql/isomorphic-ws',
],
serverExternalPackages: ['libsql', '@libsql/client'],
typescript: {
ignoreBuildErrors: true,
},
Expand Down
3 changes: 3 additions & 0 deletions apps/web/open-next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineCloudflareConfig } from '@opennextjs/cloudflare';

export default defineCloudflareConfig();
9 changes: 7 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
"type": "module",
"scripts": {
"build": "next build",
"build:worker": "opennextjs-cloudflare build",
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"dev": "next dev -p 3000",
"lint": "eslint . --cache --max-warnings 0",
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"start": "next start",
"typecheck": "tsc --noEmit --tsBuildInfoFile .tsbuildinfo"
},
Expand All @@ -19,6 +23,7 @@
"@formbase/ui": "workspace:*",
"@formbase/utils": "workspace:*",
"@hookform/resolvers": "^3.4.2",
"@opennextjs/cloudflare": "1.19.5",
"@radix-ui/react-icons": "^1.3.0",
"@react-email/components": "^1.0.12",
"@react-email/render": "^2.0.8",
Expand All @@ -36,7 +41,6 @@
"disposable-email-domains": "^1.0.62",
"framer-motion": "^11.2.10",
"lucide-react": "^1.14.0",
"minio": "^8.0.7",
"next": "16.2.4",
"next-themes": "^0.4.6",
"react": "^19.2.5",
Expand All @@ -60,6 +64,7 @@
"eslint-config-formbase": "workspace:*",
"postcss": "^8.5.12",
"shiki": "^1.6.3",
"tailwindcss": "^4.2.4"
"tailwindcss": "^4.2.4",
"wrangler": "4.87.0"
}
}
2 changes: 2 additions & 0 deletions apps/web/public/_headers
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/_next/static/*
Cache-Control: public,max-age=31536000,immutable
13 changes: 5 additions & 8 deletions apps/web/src/app/(landing)/_components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import Link from 'next/link';

import { type User } from '@formbase/auth';
import { env } from '@formbase/env';

import { Logo } from '../../(auth)/_components/logo';
import { MobileNavigation } from './mobile-navigation';

const routes = [{ name: 'Docs', href: 'https://docs.formbase.dev' }] as const;

type LandingHeaderProps = {
user: User | null;
isLoggedIn?: boolean;
};

export const Header = ({ user }: LandingHeaderProps) => {
const isLoggedIn = user !== null;
export const Header = ({ isLoggedIn = false }: LandingHeaderProps) => {
const showAuthLinks = process.env['ALLOW_SIGNIN_SIGNUP']?.trim() !== 'false';

return (
<header>
Expand Down Expand Up @@ -46,7 +43,7 @@ export const Header = ({ user }: LandingHeaderProps) => {
</div>

<>
{env.ALLOW_SIGNIN_SIGNUP === 'false' ? null : (
{showAuthLinks ? (
<>
{isLoggedIn ? (
<Link
Expand All @@ -72,7 +69,7 @@ export const Header = ({ user }: LandingHeaderProps) => {
</div>
)}
</>
)}
) : null}
</>
</div>

Expand Down
25 changes: 18 additions & 7 deletions apps/web/src/app/(landing)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
import { type ReactNode } from 'react';
import { redirect } from 'next/navigation';

import { getSession } from '@formbase/auth/server';

import { Header } from './_components/header';
import { SiteFooter } from './_components/site-footer';

export const dynamic = 'force-dynamic';

async function LandingPageLayout({ children }: { children: ReactNode }) {
const session = await getSession();
const user = session?.user ?? null;
const databaseUrl = process.env['DATABASE_URL'];
const hasSessionRuntimeEnv =
!!databaseUrl &&
(!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;

if (hasSessionRuntimeEnv) {
const { getSession } = await import('@formbase/auth/server');
isLoggedIn = !!(await getSession())?.user;

if (user) {
redirect('/dashboard');
if (isLoggedIn) {
redirect('/dashboard');
}
}

return (
<div className="bg-white dark:bg-black">
<Header user={user} />
<Header isLoggedIn={isLoggedIn} />
{children}

<SiteFooter />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import { toast } from 'sonner';

import { LoadingButton } from '~/components/loading-button';

import { revalidateFromClient } from '../../_actions/revalidateDashboard';

type SendFormSubmissionButton = {
formId: string | null;
};
Expand All @@ -31,7 +29,6 @@ export default function SendFormSubmissionButton({

toast.success('Form submission sent!');

void revalidateFromClient(`/form/${formId}`);
router.push(`/form/${formId}`);
});
};
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/app/api/cron/cleanup-audit-logs/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { db } from '@formbase/db';
import { cleanupOldAuditLogs } from '@formbase/api/lib/audit-log';
import { db } from '@formbase/db';
import { env } from '@formbase/env';

export const dynamic = 'force-dynamic';

export async function GET(request: Request) {
const authHeader = request.headers.get('authorization');

if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
if (!env.CRON_SECRET || authHeader !== `Bearer ${env.CRON_SECRET}`) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}

Expand Down
26 changes: 26 additions & 0 deletions apps/web/src/app/api/files/[...key]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createFileDownloadUrl } from '~/lib/upload-file';

export const dynamic = 'force-dynamic';

const isFileKey = (key: string) =>
/^[0-9A-Za-z]{15}\.[+0-9A-Za-z._-]+$/.test(key);

export async function GET(
_request: Request,
{ params }: { params: Promise<{ key: string[] }> },
) {
const { key } = await params;
const fileKey = key.join('/');

if (!isFileKey(fileKey)) {
return new Response('File not found', { status: 404 });
}

return new Response(null, {
status: 302,
headers: {
'Cache-Control': 'private, no-store, max-age=0',
Location: await createFileDownloadUrl(fileKey),
},
});
}
7 changes: 4 additions & 3 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { Metadata, Viewport } from 'next';

import { Toaster } from 'sonner';

import { env } from '@formbase/env';
import { TooltipProvider } from '@formbase/ui/primitives/tooltip';
import { cn } from '@formbase/ui/utils/cn';

Expand All @@ -19,6 +18,8 @@ const fontSans = FontSans({
variable: '--font-sans',
});

const umamiTrackingId = process.env['UMAMI_TRACKING_ID'];

export const metadata: Metadata = {
title: {
default: 'Formbase',
Expand Down Expand Up @@ -59,11 +60,11 @@ export default function RootLayout({
</TRPCReactProvider>
<Toaster />
</ThemeProvider>
{env.UMAMI_TRACKING_ID && (
{umamiTrackingId && (
<Script
async
src="https://analytics.duncan.land/script.js"
data-website-id={env.UMAMI_TRACKING_ID}
data-website-id={umamiTrackingId}
/>
)}
</body>
Expand Down
Loading
Loading