mirror of
https://github.com/Mail-0/Zero.git
synced 2026-06-30 07:46:15 +00:00
feat: agnostic image with runtime variables + typed config
This commit is contained in:
@@ -81,7 +81,7 @@ You can set up Zero in two ways:
|
||||
```
|
||||
- Configure your environment variables (see below)
|
||||
- Setup cloudflare with `bun run cf-install`, you will need to run this everytime there is a `.env` change
|
||||
- Start the database with the provided docker compose setup: `bun docker:up`
|
||||
- Start the database with the provided docker compose setup: `bun docker:db:up`
|
||||
- Initialize the database: `bun db:push`
|
||||
|
||||
3. **Start the App**
|
||||
|
||||
@@ -10,7 +10,7 @@ import { TriangleAlert } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import { toast } from 'sonner';
|
||||
import Link from 'next/link';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
interface EnvVarStatus {
|
||||
name: string;
|
||||
@@ -121,7 +121,7 @@ function LoginClientContent({ providers, isProd }: LoginClientProps) {
|
||||
toast.promise(
|
||||
signIn.social({
|
||||
provider: provider.id as any,
|
||||
callbackURL: `${process.env.NEXT_PUBLIC_APP_URL}/mail`,
|
||||
callbackURL: `${env.NEXT_PUBLIC_APP_URL}/mail`,
|
||||
}),
|
||||
{
|
||||
error: 'Login redirect failed',
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import { authProviders, customProviders, isProviderEnabled } from '@zero/server/auth-providers';
|
||||
import { LoginClient } from './login-client';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
export default function LoginPage() {
|
||||
const envNodeEnv = process.env.NODE_ENV;
|
||||
const envNodeEnv = env.NODE_ENV;
|
||||
const isProd = envNodeEnv === 'production';
|
||||
|
||||
const authProviderStatus = authProviders(process.env as Record<string, string>).map(
|
||||
const authProviderStatus = authProviders(env as unknown as Record<string, string>).map(
|
||||
(provider) => {
|
||||
const envVarStatus =
|
||||
provider.envVarInfo?.map((envVar) => ({
|
||||
name: envVar.name,
|
||||
set: !!process.env[envVar.name],
|
||||
source: envVar.source,
|
||||
defaultValue: envVar.defaultValue,
|
||||
})) || [];
|
||||
provider.envVarInfo?.map((envVar) => {
|
||||
const envVarName = envVar.name as keyof typeof env;
|
||||
return {
|
||||
name: envVar.name,
|
||||
set: !!env[envVarName],
|
||||
source: envVar.source,
|
||||
defaultValue: envVar.defaultValue,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
return {
|
||||
id: provider.id,
|
||||
name: provider.name,
|
||||
enabled: isProviderEnabled(provider, process.env as Record<string, string>),
|
||||
enabled: isProviderEnabled(provider, env as Record<string, string>),
|
||||
required: provider.required,
|
||||
envVarInfo: provider.envVarInfo,
|
||||
envVarStatus,
|
||||
|
||||
@@ -24,6 +24,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { env } from '@/lib/env';
|
||||
import Image from 'next/image';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -147,7 +148,7 @@ export default function ConnectionsPage() {
|
||||
onClick={async () => {
|
||||
await authClient.linkSocial({
|
||||
provider: connection.providerId,
|
||||
callbackURL: `${process.env.NEXT_PUBLIC_APP_URL}/settings/connections`,
|
||||
callbackURL: `${env.NEXT_PUBLIC_APP_URL}/settings/connections`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ClientProviders } from '@/providers/client-providers';
|
||||
import { ServerProviders } from '@/providers/server-providers';
|
||||
import { SpeedInsights } from '@vercel/speed-insights/next';
|
||||
import { Geist, Geist_Mono } from 'next/font/google';
|
||||
import { PublicEnvScript } from 'next-runtime-env';
|
||||
import { siteConfig } from '@/lib/site-config';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import type { Viewport } from 'next';
|
||||
@@ -33,6 +34,7 @@ export default async function RootLayout({ children }: PropsWithChildren) {
|
||||
<html suppressHydrationWarning>
|
||||
<head>
|
||||
<Script src="https://unpkg.com/web-streams-polyfill/dist/polyfill.js" />
|
||||
<PublicEnvScript />
|
||||
</head>
|
||||
<body className={cn(geistSans.variable, geistMono.variable, 'antialiased')}>
|
||||
<ServerProviders>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function GET() {
|
||||
@@ -18,7 +20,7 @@ export async function GET() {
|
||||
}
|
||||
|
||||
try {
|
||||
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
|
||||
const appUrl = NEXT_PUBLIC_APP_URL;
|
||||
if (!appUrl) {
|
||||
throw new Error('NEXT_PUBLIC_APP_URL is not defined');
|
||||
}
|
||||
@@ -27,7 +29,7 @@ export async function GET() {
|
||||
if (!mailResponse.ok) {
|
||||
throw new Error('Failed to fetch SVG');
|
||||
}
|
||||
|
||||
|
||||
const mailBuffer = await mailResponse.arrayBuffer();
|
||||
const mailBase64 = btoa(String.fromCharCode(...new Uint8Array(mailBuffer)));
|
||||
const mail = `data:image/svg+xml;base64,${mailBase64}`;
|
||||
@@ -52,7 +54,10 @@ export async function GET() {
|
||||
<span tw="text-[#A1A1A1]">is here</span>
|
||||
</div>
|
||||
|
||||
<div tw="text-[36px] text-center text-neutral-400 mt-10" style={{ fontFamily: 'light' }}>
|
||||
<div
|
||||
tw="text-[36px] text-center text-neutral-400 mt-10"
|
||||
style={{ fontFamily: 'light' }}
|
||||
>
|
||||
Experience email the way you want with 0 - the first open source email app that puts
|
||||
your privacy and safety first.
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useTranslations } from 'next-intl';
|
||||
import { Button } from '../ui/button';
|
||||
import { motion } from 'motion/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { env } from '@/lib/env';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const AddConnectionDialog = ({
|
||||
@@ -110,7 +111,7 @@ export const AddConnectionDialog = ({
|
||||
onClick={async () =>
|
||||
await authClient.linkSocial({
|
||||
provider: provider.providerId,
|
||||
callbackURL: `${process.env.NEXT_PUBLIC_APP_URL}/${pathname}`,
|
||||
callbackURL: `${env.NEXT_PUBLIC_APP_URL}/${pathname}`,
|
||||
})
|
||||
}
|
||||
>
|
||||
|
||||
@@ -22,6 +22,7 @@ import { format } from 'date-fns-tz';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { Input } from '../ui/input';
|
||||
import { useState } from 'react';
|
||||
import { env } from '@/lib/env';
|
||||
import VoiceChat from './voice';
|
||||
import Image from 'next/image';
|
||||
import { toast } from 'sonner';
|
||||
@@ -142,7 +143,7 @@ export function AIChat() {
|
||||
const { attach, track, refetch: refetchBilling } = useBilling();
|
||||
|
||||
const { messages, input, setInput, error, handleSubmit, status, stop } = useChat({
|
||||
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/chat`,
|
||||
api: `${env.NEXT_PUBLIC_BACKEND_URL}/api/chat`,
|
||||
fetch: (url, options) => fetch(url, { ...options, credentials: 'include' }),
|
||||
maxSteps: 5,
|
||||
body: {
|
||||
|
||||
@@ -17,6 +17,9 @@ import { useSession } from '@/lib/auth-client';
|
||||
import type { Sender } from '@/types';
|
||||
import dedent from 'dedent';
|
||||
|
||||
// Utils
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
interface EmailContent {
|
||||
metadata: {
|
||||
isUnread: boolean;
|
||||
@@ -129,7 +132,7 @@ const VoiceChat = ({ onClose }: VoiceChatProps) => {
|
||||
const emailContext = emailContent.join('\n\n');
|
||||
|
||||
const conversationId = await conversation.startSession({
|
||||
agentId: process.env.NEXT_PUBLIC_ELEVENLABS_AGENT_ID!,
|
||||
agentId: env.NEXT_PUBLIC_ELEVENLABS_AGENT_ID,
|
||||
dynamicVariables: {
|
||||
user_name: userName,
|
||||
email_context: emailContext,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { usePartySocket } from 'partysocket/react';
|
||||
import { useThreads } from '@/hooks/use-threads';
|
||||
import { useLabels } from '@/hooks/use-labels';
|
||||
import { useSession } from '@/lib/auth-client';
|
||||
import { env } from '@/lib/env';
|
||||
import { funnel } from 'remeda';
|
||||
|
||||
const DEBOUNCE_DELAY = 10_000; // 10 seconds is appropriate for real-time notifications
|
||||
@@ -36,7 +37,7 @@ export const NotificationProvider = ({ headers }: { headers: Record<string, stri
|
||||
query: {
|
||||
token: headers['cookie'],
|
||||
},
|
||||
host: process.env.NEXT_PUBLIC_BACKEND_URL!,
|
||||
host: env.NEXT_PUBLIC_BACKEND_URL,
|
||||
onMessage: async (message: MessageEvent<string>) => {
|
||||
console.warn('party message', message);
|
||||
const [threadId, type] = message.data.split(':');
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
if (env.NEXT_RUNTIME === 'nodejs') {
|
||||
await import('./sentry.server.config');
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
if (env.NEXT_RUNTIME === 'edge') {
|
||||
await import('./sentry.edge.config');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { customSessionClient } from 'better-auth/client/plugins';
|
||||
import { createAuthClient } from 'better-auth/react';
|
||||
import type { Auth } from '@zero/server/auth';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: process.env.NEXT_PUBLIC_BACKEND_URL,
|
||||
baseURL: env.NEXT_PUBLIC_BACKEND_URL,
|
||||
fetchOptions: {
|
||||
credentials: 'include',
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
export const I18N_LOCALE_COOKIE_NAME = 'i18n:locale';
|
||||
export const SIDEBAR_COOKIE_NAME = 'sidebar:state';
|
||||
export const AI_SIDEBAR_COOKIE_NAME = 'ai-sidebar:state';
|
||||
@@ -6,7 +8,7 @@ export const SIDEBAR_WIDTH = '14rem';
|
||||
export const SIDEBAR_WIDTH_MOBILE = '14rem';
|
||||
export const SIDEBAR_WIDTH_ICON = '3rem';
|
||||
export const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||
export const BASE_URL = process.env.NEXT_PUBLIC_APP_URL;
|
||||
export const BASE_URL = env.NEXT_PUBLIC_APP_URL;
|
||||
export const MAX_URL_LENGTH = 2000;
|
||||
export const CACHE_BURST_KEY = 'cache-burst:v0.0.2';
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getListUnsubscribeAction } from '@/lib/email-utils';
|
||||
import { trpcClient } from '@/providers/query-provider';
|
||||
import type { ParsedMessage } from '@/types';
|
||||
import { track } from '@vercel/analytics';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
export const handleUnsubscribe = async ({ emailData }: { emailData: ParsedMessage }) => {
|
||||
try {
|
||||
@@ -109,10 +110,10 @@ const forceExternalLinks = (html: string): string => {
|
||||
|
||||
const getProxiedUrl = (url: string) => {
|
||||
if (url.startsWith('data:') || url.startsWith('blob:')) return url;
|
||||
|
||||
const proxyUrl = process.env.NEXT_PUBLIC_IMAGE_PROXY?.trim();
|
||||
|
||||
const proxyUrl = env.NEXT_PUBLIC_IMAGE_PROXY?.trim();
|
||||
if (!proxyUrl) return url;
|
||||
|
||||
|
||||
return proxyUrl + encodeURIComponent(url);
|
||||
};
|
||||
|
||||
@@ -123,12 +124,15 @@ const proxyImageUrls = (html: string): string => {
|
||||
doc.querySelectorAll('img').forEach((img) => {
|
||||
const src = img.getAttribute('src');
|
||||
if (!src) return;
|
||||
|
||||
|
||||
const proxiedUrl = getProxiedUrl(src);
|
||||
if (proxiedUrl !== src) {
|
||||
img.setAttribute('data-original-src', src);
|
||||
img.setAttribute('src', proxiedUrl);
|
||||
img.setAttribute('onerror', `this.onerror=null; this.src=this.getAttribute('data-original-src');`);
|
||||
img.setAttribute(
|
||||
'onerror',
|
||||
`this.onerror=null; this.src=this.getAttribute('data-original-src');`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -256,7 +260,7 @@ export const template = async (html: string, imagesEnabled: boolean = false) =>
|
||||
if (typeof DOMParser === 'undefined') return html;
|
||||
const nonce = generateNonce();
|
||||
let processedHtml = forceExternalLinks(html);
|
||||
|
||||
|
||||
if (imagesEnabled) {
|
||||
processedHtml = proxyImageUrls(processedHtml);
|
||||
}
|
||||
|
||||
42
apps/mail/lib/env.ts
Normal file
42
apps/mail/lib/env.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { env as runtimeEnv } from 'next-runtime-env';
|
||||
import { createEnv } from '@t3-oss/env-nextjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
const getEnv = (variable: string) => runtimeEnv(variable) ?? process.env[variable];
|
||||
|
||||
export const env = createEnv({
|
||||
skipValidation: true,
|
||||
|
||||
server: {
|
||||
DATABASE_URL: z.string().url(),
|
||||
NEXT_RUNTIME: z.string().optional(),
|
||||
NODE_ENV: z.string().optional(),
|
||||
DOCKER_BUILD: z.coerce.boolean().optional(),
|
||||
CI: z.coerce.boolean().optional(),
|
||||
GROQ_API_KEY: z.string().optional(),
|
||||
AI_SYSTEM_PROMPT: z.string().optional(),
|
||||
RESEND_API_KEY: z.string().optional(),
|
||||
REDIS_URL: z.string().url(),
|
||||
REDIS_TOKEN: z.string(),
|
||||
},
|
||||
|
||||
client: {
|
||||
NEXT_PUBLIC_APP_URL: z.string().url(),
|
||||
NEXT_PUBLIC_BACKEND_URL: z.string().url(),
|
||||
NEXT_PUBLIC_ELEVENLABS_AGENT_ID: z.string(),
|
||||
NEXT_PUBLIC_IMAGE_PROXY: z.string().url().optional(),
|
||||
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
|
||||
NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(),
|
||||
NEXT_PUBLIC_IMAGE_API_URL: z.string().optional(),
|
||||
},
|
||||
|
||||
experimental__runtimeEnv: {
|
||||
NEXT_PUBLIC_BACKEND_URL: getEnv('NEXT_PUBLIC_BACKEND_URL') ?? 'http://REPLACE-BACKEND-URL.com',
|
||||
NEXT_PUBLIC_APP_URL: getEnv('NEXT_PUBLIC_APP_URL') ?? 'http://REPLACE-APP-URL.com',
|
||||
NEXT_PUBLIC_ELEVENLABS_AGENT_ID: getEnv('NEXT_PUBLIC_ELEVENLABS_AGENT_ID'),
|
||||
NEXT_PUBLIC_IMAGE_PROXY: getEnv('NEXT_PUBLIC_IMAGE_PROXY'),
|
||||
NEXT_PUBLIC_POSTHOG_KEY: getEnv('NEXT_PUBLIC_POSTHOG_KEY'),
|
||||
NEXT_PUBLIC_POSTHOG_HOST: getEnv('NEXT_PUBLIC_POSTHOG_HOST'),
|
||||
NEXT_PUBLIC_IMAGE_API_URL: getEnv('NEXT_PUBLIC_IMAGE_API_URL'),
|
||||
},
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { env } from '@/lib/env';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const groqChatCompletionSchema = z.object({
|
||||
@@ -106,7 +107,7 @@ export async function generateCompletions({
|
||||
embeddings,
|
||||
userName,
|
||||
}: CompletionsParams) {
|
||||
if (!process.env.GROQ_API_KEY) throw new Error('Groq API Key is missing');
|
||||
if (!env.GROQ_API_KEY) throw new Error('Groq API Key is missing');
|
||||
|
||||
// Map OpenAI model names to Groq equivalents if needed
|
||||
const groqModel = MODEL_MAPPING[model] || model;
|
||||
@@ -134,7 +135,7 @@ export async function generateCompletions({
|
||||
// Enhance the system prompt for email generation to improve formatting
|
||||
if (enhancedSystemPrompt.toLowerCase().includes('email')) {
|
||||
enhancedSystemPrompt =
|
||||
process.env.AI_SYSTEM_PROMPT ||
|
||||
env.AI_SYSTEM_PROMPT ||
|
||||
`You are an email assistant helping ${userName} write professional and concise email replies.
|
||||
|
||||
Important instructions:
|
||||
@@ -188,7 +189,7 @@ export async function generateCompletions({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${process.env.GROQ_API_KEY}`,
|
||||
Authorization: `Bearer ${env.GROQ_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
@@ -5,15 +5,16 @@ import { PostHogProvider as PHProvider } from 'posthog-js/react';
|
||||
import { useSession } from '@/lib/auth-client';
|
||||
import { useEffect } from 'react';
|
||||
import posthog from 'posthog-js';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
||||
const { data: session } = useSession();
|
||||
|
||||
useEffect(() => {
|
||||
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) return;
|
||||
if (!env.NEXT_PUBLIC_POSTHOG_KEY) return;
|
||||
try {
|
||||
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY as string, {
|
||||
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY as string, {
|
||||
api_host: env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
capture_pageview: true,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Redis } from '@upstash/redis';
|
||||
import { env } from '@/lib/env';
|
||||
import { Resend } from 'resend';
|
||||
|
||||
export const resend = process.env.RESEND_API_KEY
|
||||
? new Resend(process.env.RESEND_API_KEY)
|
||||
export const resend = env.RESEND_API_KEY
|
||||
? new Resend(env.RESEND_API_KEY)
|
||||
: { emails: { send: async (...args: any[]) => console.log(args) } };
|
||||
|
||||
export const redis = new Redis({ url: process.env.REDIS_URL, token: process.env.REDIS_TOKEN });
|
||||
export const redis = new Redis({ url: env.REDIS_URL, token: env.REDIS_TOKEN });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type Metadata } from 'next';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
const TITLE = 'Zero';
|
||||
const DESCRIPTION =
|
||||
@@ -17,7 +18,7 @@ export const siteConfig: Metadata = {
|
||||
description: DESCRIPTION,
|
||||
images: [
|
||||
{
|
||||
url: `${process.env.NEXT_PUBLIC_APP_URL}/og-api/home`,
|
||||
url: `${env.NEXT_PUBLIC_APP_URL}/og-api/home`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: TITLE,
|
||||
@@ -26,7 +27,7 @@ export const siteConfig: Metadata = {
|
||||
},
|
||||
category: 'Email Client',
|
||||
alternates: {
|
||||
canonical: process.env.NEXT_PUBLIC_APP_URL,
|
||||
canonical: env.NEXT_PUBLIC_APP_URL,
|
||||
},
|
||||
keywords: [
|
||||
'Mail',
|
||||
@@ -50,5 +51,5 @@ export const siteConfig: Metadata = {
|
||||
'Email Service',
|
||||
'Web Application',
|
||||
],
|
||||
metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL!),
|
||||
metadataBase: new URL(env.NEXT_PUBLIC_APP_URL!),
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import { twMerge } from 'tailwind-merge';
|
||||
import type { JSONContent } from 'novel';
|
||||
import type { Sender } from '@/types';
|
||||
import LZString from 'lz-string';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
export const FOLDERS = {
|
||||
SPAM: 'spam',
|
||||
@@ -357,8 +358,8 @@ export const createAIJsonContent = (text: string): JSONContent => {
|
||||
};
|
||||
|
||||
export const getEmailLogo = (email: string) => {
|
||||
if (!process.env.NEXT_PUBLIC_IMAGE_API_URL) return '';
|
||||
return process.env.NEXT_PUBLIC_IMAGE_API_URL + email;
|
||||
if (!env.NEXT_PUBLIC_IMAGE_API_URL) return '';
|
||||
return env.NEXT_PUBLIC_IMAGE_API_URL + email;
|
||||
};
|
||||
|
||||
export const generateConversationId = (): string => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server';
|
||||
import { navigationConfig } from '@/config/navigation';
|
||||
import { geolocation } from '@vercel/functions';
|
||||
import { EU_COUNTRIES } from './lib/countries';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
const disabledRoutes = Object.values(navigationConfig)
|
||||
.flatMap((section) => section.sections)
|
||||
@@ -19,7 +20,7 @@ export function middleware(request: NextRequest) {
|
||||
const isEuRegion = EU_COUNTRIES.includes(country);
|
||||
response.headers.set('x-user-eu-region', String(isEuRegion));
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (env.NODE_ENV === 'development') {
|
||||
response.headers.set('x-user-eu-region', 'true');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
import { withSentryConfig } from '@sentry/nextjs';
|
||||
import type { NextConfig } from 'next';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
// devIndicators: false,
|
||||
output: process.env.DOCKER_BUILD ? 'standalone' : undefined,
|
||||
output: env.DOCKER_BUILD ? 'standalone' : undefined,
|
||||
compiler: {
|
||||
removeConsole:
|
||||
process.env.NODE_ENV === 'production'
|
||||
env.NODE_ENV === 'production'
|
||||
? {
|
||||
exclude: ['warn', 'error'],
|
||||
}
|
||||
@@ -53,7 +54,7 @@ const nextConfig: NextConfig = {
|
||||
return [
|
||||
{
|
||||
source: '/api/mailto-handler',
|
||||
destination: `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/mailto-handler`,
|
||||
destination: `${env.NEXT_PUBLIC_BACKEND_URL}/api/mailto-handler`,
|
||||
},
|
||||
];
|
||||
},
|
||||
@@ -69,7 +70,7 @@ export default withSentryConfig(withNextIntl(nextConfig), {
|
||||
project: 'nextjs',
|
||||
|
||||
// Only print logs for uploading source maps in CI
|
||||
silent: !process.env.CI,
|
||||
silent: !env.CI,
|
||||
|
||||
// For all available options, see:
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"@react-email/render": "^1.0.6",
|
||||
"@sentry/cli": "^2.45.0",
|
||||
"@sentry/nextjs": "^9.19.0",
|
||||
"@t3-oss/env-nextjs": "^0.13.4",
|
||||
"@tanstack/query-sync-storage-persister": "^5.75.0",
|
||||
"@tanstack/react-query": "^5.74.4",
|
||||
"@tanstack/react-query-persist-client": "^5.75.2",
|
||||
@@ -117,6 +118,7 @@
|
||||
"motion": "12.4.7",
|
||||
"next": "15.3.1",
|
||||
"next-intl": "3.26.5",
|
||||
"next-runtime-env": "^3.3.0",
|
||||
"next-themes": "0.4.4",
|
||||
"novel": "1.0.2",
|
||||
"nuqs": "2.4.0",
|
||||
|
||||
@@ -13,6 +13,7 @@ import { CACHE_BURST_KEY } from '@/lib/constants';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { get, set, del } from 'idb-keyval';
|
||||
import superjson from 'superjson';
|
||||
import { env } from '@/lib/env';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
function createIDBPersister(idbValidKey: IDBValidKey = 'zero-query-cache') {
|
||||
@@ -92,7 +93,7 @@ const getQueryClient = (session: Session | null) => {
|
||||
};
|
||||
|
||||
const getUrl = () => {
|
||||
return process.env.NEXT_PUBLIC_BACKEND_URL + '/api/trpc';
|
||||
return env.NEXT_PUBLIC_BACKEND_URL + '/api/trpc';
|
||||
};
|
||||
|
||||
export const { TRPCProvider, useTRPC, useTRPCClient } = createTRPCContext<AppRouter>();
|
||||
|
||||
46
bun.lock
46
bun.lock
@@ -65,6 +65,7 @@
|
||||
"@react-email/render": "^1.0.6",
|
||||
"@sentry/cli": "^2.45.0",
|
||||
"@sentry/nextjs": "^9.19.0",
|
||||
"@t3-oss/env-nextjs": "^0.13.4",
|
||||
"@tanstack/query-sync-storage-persister": "^5.75.0",
|
||||
"@tanstack/react-query": "^5.74.4",
|
||||
"@tanstack/react-query-persist-client": "^5.75.2",
|
||||
@@ -124,6 +125,7 @@
|
||||
"motion": "12.4.7",
|
||||
"next": "15.3.1",
|
||||
"next-intl": "3.26.5",
|
||||
"next-runtime-env": "^3.3.0",
|
||||
"next-themes": "0.4.4",
|
||||
"novel": "1.0.2",
|
||||
"nuqs": "2.4.0",
|
||||
@@ -295,6 +297,10 @@
|
||||
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"@ark/schema": ["@ark/schema@0.46.0", "", { "dependencies": { "@ark/util": "0.46.0" } }, "sha512-c2UQdKgP2eqqDArfBqQIJppxJHvNNXuQPeuSPlDML4rjw+f1cu0qAlzOG4b8ujgm9ctIDWwhpyw6gjG5ledIVQ=="],
|
||||
|
||||
"@ark/util": ["@ark/util@0.46.0", "", {}, "sha512-JPy/NGWn/lvf1WmGCPw2VGpBg5utZraE84I7wli18EDF3p3zc/e9WolT35tINeZO3l7C77SjqRJeAUoT0CvMRg=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.27.2", "", {}, "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ=="],
|
||||
@@ -637,6 +643,8 @@
|
||||
|
||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw=="],
|
||||
|
||||
"@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.28", "", { "os": "win32", "cpu": "ia32" }, "sha512-+Kcp1T3jHZnJ9v9VTJ/yf1t/xmtFAc/Sge4v7mVc1z+NYfYzisi8kJ9AsY8itbgq+WgEwMtOpiLLJsUy2qnXZw=="],
|
||||
|
||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ=="],
|
||||
|
||||
"@noble/ciphers": ["@noble/ciphers@0.6.0", "", {}, "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ=="],
|
||||
@@ -985,6 +993,10 @@
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||
|
||||
"@t3-oss/env-core": ["@t3-oss/env-core@0.13.4", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0-beta.0" }, "optionalPeers": ["typescript", "valibot", "zod"] }, "sha512-zVOiYO0+CF7EnBScz8s0O5JnJLPTU0lrUi8qhKXfIxIJXvI/jcppSiXXsEJwfB4A6XZawY/Wg/EQGKANi/aPmQ=="],
|
||||
|
||||
"@t3-oss/env-nextjs": ["@t3-oss/env-nextjs@0.13.4", "", { "dependencies": { "@t3-oss/env-core": "0.13.4" }, "peerDependencies": { "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0-beta.0" }, "optionalPeers": ["typescript", "valibot", "zod"] }, "sha512-6ecXR7SH7zJKVcBODIkB7wV9QLMU23uV8D9ec6P+ULHJ5Ea/YXEHo+Z/2hSYip5i9ptD/qZh8VuOXyldspvTTg=="],
|
||||
|
||||
"@tailwindcss/typography": ["@tailwindcss/typography@0.5.16", "", { "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.75.0", "", {}, "sha512-rk8KQuCdhoRkzjRVF3QxLgAfFUyS0k7+GCQjlGEpEGco+qazJ0eMH6aO1DjDjibH7/ik383nnztua3BG+lOnwg=="],
|
||||
@@ -1333,6 +1345,8 @@
|
||||
|
||||
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||
|
||||
"arktype": ["arktype@2.1.20", "", { "dependencies": { "@ark/schema": "0.46.0", "@ark/util": "0.46.0" } }, "sha512-IZCEEXaJ8g+Ijd59WtSYwtjnqXiwM8sWQ5EjGamcto7+HVN9eK0C4p0zDlCuAwWhpqr6fIBkxPuYDl4/Mcj/+Q=="],
|
||||
|
||||
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
||||
|
||||
"array-includes": ["array-includes@3.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ=="],
|
||||
@@ -2231,6 +2245,8 @@
|
||||
|
||||
"next-intl": ["next-intl@3.26.5", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.5.4", "negotiator": "^1.0.0", "use-intl": "^3.26.5" }, "peerDependencies": { "next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" } }, "sha512-EQlCIfY0jOhRldiFxwSXG+ImwkQtDEfQeSOEQp6ieAGSLWGlgjdb/Ck/O7wMfC430ZHGeUKVKax8KGusTPKCgg=="],
|
||||
|
||||
"next-runtime-env": ["next-runtime-env@3.3.0", "", { "dependencies": { "next": "^14", "react": "^18" } }, "sha512-JgKVnog9mNbjbjH9csVpMnz2tB2cT5sLF+7O47i6Ze/s/GoiKdV7dHhJHk1gwXpo6h5qPj5PTzryldtSjvrHuQ=="],
|
||||
|
||||
"next-themes": ["next-themes@0.4.4", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ=="],
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
@@ -3407,6 +3423,10 @@
|
||||
|
||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
"next-runtime-env/next": ["next@14.2.28", "", { "dependencies": { "@next/env": "14.2.28", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", "styled-jsx": "5.1.1" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.2.28", "@next/swc-darwin-x64": "14.2.28", "@next/swc-linux-arm64-gnu": "14.2.28", "@next/swc-linux-arm64-musl": "14.2.28", "@next/swc-linux-x64-gnu": "14.2.28", "@next/swc-linux-x64-musl": "14.2.28", "@next/swc-win32-arm64-msvc": "14.2.28", "@next/swc-win32-ia32-msvc": "14.2.28", "@next/swc-win32-x64-msvc": "14.2.28" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-QLEIP/kYXynIxtcKB6vNjtWLVs3Y4Sb+EClTC/CSVzdLD1gIuItccpu/n1lhmduffI32iPGEK2cLLxxt28qgYA=="],
|
||||
|
||||
"next-runtime-env/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
|
||||
|
||||
"novel/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="],
|
||||
|
||||
"novel/@tiptap/extension-placeholder": ["@tiptap/extension-placeholder@2.11.7", "", { "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0" } }, "sha512-/06zXV4HIjYoiaUq1fVJo/RcU8pHbzx21evOpeG/foCfNpMI4xLU/vnxdUi6/SQqpZMY0eFutDqod1InkSOqsg=="],
|
||||
@@ -3809,6 +3829,32 @@
|
||||
|
||||
"motion/framer-motion/motion-utils": ["motion-utils@12.8.3", "", {}, "sha512-GYVauZEbca8/zOhEiYOY9/uJeedYQld6co/GJFKOy//0c/4lDqk0zB549sBYqqV2iMuX+uHrY1E5zd8A2L+1Lw=="],
|
||||
|
||||
"next-runtime-env/next/@next/env": ["@next/env@14.2.28", "", {}, "sha512-PAmWhJfJQlP+kxZwCjrVd9QnR5x0R3u0mTXTiZDgSd4h5LdXmjxCCWbN9kq6hkZBOax8Rm3xDW5HagWyJuT37g=="],
|
||||
|
||||
"next-runtime-env/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.2.28", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kzGChl9setxYWpk3H6fTZXXPFFjg7urptLq5o5ZgYezCrqlemKttwMT5iFyx/p1e/JeglTwDFRtb923gTJ3R1w=="],
|
||||
|
||||
"next-runtime-env/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.2.28", "", { "os": "darwin", "cpu": "x64" }, "sha512-z6FXYHDJlFOzVEOiiJ/4NG8aLCeayZdcRSMjPDysW297Up6r22xw6Ea9AOwQqbNsth8JNgIK8EkWz2IDwaLQcw=="],
|
||||
|
||||
"next-runtime-env/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.2.28", "", { "os": "linux", "cpu": "arm64" }, "sha512-9ARHLEQXhAilNJ7rgQX8xs9aH3yJSj888ssSjJLeldiZKR4D7N08MfMqljk77fAwZsWwsrp8ohHsMvurvv9liQ=="],
|
||||
|
||||
"next-runtime-env/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.2.28", "", { "os": "linux", "cpu": "arm64" }, "sha512-p6gvatI1nX41KCizEe6JkF0FS/cEEF0u23vKDpl+WhPe/fCTBeGkEBh7iW2cUM0rvquPVwPWdiUR6Ebr/kQWxQ=="],
|
||||
|
||||
"next-runtime-env/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.2.28", "", { "os": "linux", "cpu": "x64" }, "sha512-nsiSnz2wO6GwMAX2o0iucONlVL7dNgKUqt/mDTATGO2NY59EO/ZKnKEr80BJFhuA5UC1KZOMblJHWZoqIJddpA=="],
|
||||
|
||||
"next-runtime-env/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.2.28", "", { "os": "linux", "cpu": "x64" }, "sha512-+IuGQKoI3abrXFqx7GtlvNOpeExUH1mTIqCrh1LGFf8DnlUcTmOOCApEnPJUSLrSbzOdsF2ho2KhnQoO0I1RDw=="],
|
||||
|
||||
"next-runtime-env/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.2.28", "", { "os": "win32", "cpu": "arm64" }, "sha512-l61WZ3nevt4BAnGksUVFKy2uJP5DPz2E0Ma/Oklvo3sGj9sw3q7vBWONFRgz+ICiHpW5mV+mBrkB3XEubMrKaA=="],
|
||||
|
||||
"next-runtime-env/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.2.28", "", { "os": "win32", "cpu": "x64" }, "sha512-1gCmpvyhz7DkB1srRItJTnmR2UwQPAUXXIg9r0/56g3O8etGmwlX68skKXJOp9EejW3hhv7nSQUJ2raFiz4MoA=="],
|
||||
|
||||
"next-runtime-env/next/@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="],
|
||||
|
||||
"next-runtime-env/next/caniuse-lite": ["caniuse-lite@1.0.30001718", "", {}, "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw=="],
|
||||
|
||||
"next-runtime-env/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
"next-runtime-env/next/styled-jsx": ["styled-jsx@5.1.1", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" } }, "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw=="],
|
||||
|
||||
"novel/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
|
||||
|
||||
"novel/cmdk/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
|
||||
|
||||
@@ -3,21 +3,20 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/app/Dockerfile
|
||||
args:
|
||||
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-zerodotemail}
|
||||
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-my-better-auth-secret}
|
||||
BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000}
|
||||
BETTER_AUTH_TRUSTED_ORIGINS: ${BETTER_AUTH_TRUSTED_ORIGINS:-http://localhost:3000}
|
||||
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
|
||||
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
|
||||
REDIS_URL: http://upstash-proxy:80
|
||||
REDIS_TOKEN: ${REDIS_TOKEN:-upstash-local-token}
|
||||
RESEND_API_KEY: ${RESEND_API_KEY}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
AI_SYSTEM_PROMPT: ${AI_SYSTEM_PROMPT}
|
||||
GROQ_API_KEY: ${GROQ_API_KEY}
|
||||
GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY}
|
||||
environment:
|
||||
NEXT_PUBLIC_BACKEND_URL: ${NEXT_PUBLIC_BACKEND_URL:-http://cf-worker.example}
|
||||
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-zerodotemail}
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
REDIS_TOKEN: ${REDIS_TOKEN:-upstash-local-token}
|
||||
RESEND_API_KEY: ${RESEND_API_KEY}
|
||||
AI_SYSTEM_PROMPT: ${AI_SYSTEM_PROMPT}
|
||||
GROQ_API_KEY: ${GROQ_API_KEY}
|
||||
NEXT_PUBLIC_ELEVENLABS_AGENT_ID: ${NEXT_PUBLIC_ELEVENLABS_AGENT_ID}
|
||||
NEXT_PUBLIC_IMAGE_PROXY: ${NEXT_PUBLIC_IMAGE_PROXY}
|
||||
NEXT_PUBLIC_POSTHOG_KEY: ${NEXT_PUBLIC_POSTHOG_KEY}
|
||||
NEXT_PUBLIC_POSTHOG_HOST: ${NEXT_PUBLIC_POSTHOG_HOST}
|
||||
NEXT_PUBLIC_IMAGE_API_URL: ${NEXT_PUBLIC_IMAGE_API_URL}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -43,7 +42,8 @@ services:
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-zerodotemail}
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
condition: service_healthy
|
||||
command: ['bun', 'run', 'db:migrate']
|
||||
restart: 'no'
|
||||
|
||||
|
||||
@@ -37,43 +37,13 @@ RUN bun install --omit dev --ignore-scripts
|
||||
WORKDIR /app/apps/mail
|
||||
RUN bun install sharp
|
||||
|
||||
# Define build arguments
|
||||
ARG NEXT_PUBLIC_APP_URL \
|
||||
DATABASE_URL \
|
||||
BETTER_AUTH_SECRET \
|
||||
BETTER_AUTH_URL \
|
||||
BETTER_AUTH_TRUSTED_ORIGINS \
|
||||
GOOGLE_CLIENT_ID \
|
||||
GOOGLE_CLIENT_SECRET \
|
||||
REDIS_URL \
|
||||
REDIS_TOKEN \
|
||||
RESEND_API_KEY \
|
||||
OPENAI_API_KEY \
|
||||
AI_SYSTEM_PROMPT \
|
||||
GROQ_API_KEY \
|
||||
GOOGLE_GENERATIVE_AI_API_KEY
|
||||
|
||||
# Set environment variables for build
|
||||
ENV NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL} \
|
||||
DATABASE_URL=${DATABASE_URL} \
|
||||
BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} \
|
||||
BETTER_AUTH_URL=${BETTER_AUTH_URL} \
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS} \
|
||||
GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} \
|
||||
GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} \
|
||||
REDIS_URL=${REDIS_URL} \
|
||||
REDIS_TOKEN=${REDIS_TOKEN} \
|
||||
RESEND_API_KEY=${RESEND_API_KEY} \
|
||||
OPENAI_API_KEY=${OPENAI_API_KEY} \
|
||||
AI_SYSTEM_PROMPT=${AI_SYSTEM_PROMPT} \
|
||||
GROQ_API_KEY=${GROQ_API_KEY} \
|
||||
GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
ENV NEXT_TELEMETRY_DISABLED=1 \
|
||||
NODE_ENV=production \
|
||||
DOCKER_BUILD=true
|
||||
|
||||
WORKDIR /app
|
||||
RUN bun run build
|
||||
RUN cd apps/mail && bun run build
|
||||
|
||||
# ========================================
|
||||
# Runner Stage: Production Environment
|
||||
@@ -84,42 +54,16 @@ WORKDIR /app
|
||||
RUN addgroup -S -g 1001 bunjs && \
|
||||
adduser -S -u 1001 nextjs -G bunjs
|
||||
|
||||
# Define build arguments
|
||||
ARG NEXT_PUBLIC_APP_URL \
|
||||
DATABASE_URL \
|
||||
BETTER_AUTH_SECRET \
|
||||
BETTER_AUTH_URL \
|
||||
BETTER_AUTH_TRUSTED_ORIGINS \
|
||||
GOOGLE_CLIENT_ID \
|
||||
GOOGLE_CLIENT_SECRET \
|
||||
REDIS_URL \
|
||||
REDIS_TOKEN \
|
||||
RESEND_API_KEY \
|
||||
OPENAI_API_KEY \
|
||||
AI_SYSTEM_PROMPT \
|
||||
GROQ_API_KEY \
|
||||
GOOGLE_GENERATIVE_AI_API_KEY
|
||||
|
||||
# Set environment variables for build
|
||||
ENV NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL} \
|
||||
DATABASE_URL=${DATABASE_URL} \
|
||||
BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} \
|
||||
BETTER_AUTH_URL=${BETTER_AUTH_URL} \
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS} \
|
||||
GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} \
|
||||
GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} \
|
||||
REDIS_URL=${REDIS_URL} \
|
||||
REDIS_TOKEN=${REDIS_TOKEN} \
|
||||
RESEND_API_KEY=${RESEND_API_KEY} \
|
||||
OPENAI_API_KEY=${OPENAI_API_KEY} \
|
||||
AI_SYSTEM_PROMPT=${AI_SYSTEM_PROMPT} \
|
||||
GROQ_API_KEY=${GROQ_API_KEY} \
|
||||
GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
|
||||
NODE_ENV=production \
|
||||
ENV NODE_ENV=production \
|
||||
PORT=3000 \
|
||||
HOSTNAME="0.0.0.0" \
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Copy entrypoint and run it
|
||||
COPY scripts/docker/ /app/scripts
|
||||
RUN chmod -R +x /app/scripts/*
|
||||
|
||||
# Copy public assets
|
||||
COPY --from=builder --chown=nextjs:bunjs /app/apps/mail/public ./apps/mail/public
|
||||
|
||||
@@ -134,4 +78,4 @@ USER nextjs
|
||||
EXPOSE 3000
|
||||
|
||||
# Start the server
|
||||
CMD ["bun", "apps/mail/server.js"]
|
||||
CMD ["/app/scripts/entrypoint.sh"]
|
||||
10
scripts/docker/entrypoint.sh
Normal file
10
scripts/docker/entrypoint.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
set -x
|
||||
|
||||
# Replacing placeholder urls to runtime variables, since we're using rewrites in nextjs, this is required.
|
||||
# Everything else which doesn't compile URLs at build should already be able to use runtime variables.
|
||||
|
||||
/app/scripts/replace-placeholder.sh "http://REPLACE-BACKEND-URL.com" "$NEXT_PUBLIC_BACKEND_URL"
|
||||
/app/scripts/replace-placeholder.sh "http://REPLACE-APP-URL.com" "$NEXT_PUBLIC_APP_URL"
|
||||
|
||||
exec bun /app/apps/mail/server.js
|
||||
36
scripts/docker/replace-placeholder.sh
Normal file
36
scripts/docker/replace-placeholder.sh
Normal file
@@ -0,0 +1,36 @@
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) Cal.com
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
FROM=$1
|
||||
TO=$2
|
||||
|
||||
if [ "${FROM}" = "${TO}" ]; then
|
||||
echo "Nothing to replace, the value is already set to ${TO}."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Only peform action if $FROM and $TO are different.
|
||||
echo "Replacing all statically built instances of $FROM with $TO."
|
||||
|
||||
for file in $(egrep -r -l "${FROM}" /app/apps/mail); do
|
||||
sed -i -e "s|$FROM|$TO|g" "$file"
|
||||
done
|
||||
Reference in New Issue
Block a user