diff --git a/.env.example b/.env.example index 328d7160f..c73c01da1 100644 --- a/.env.example +++ b/.env.example @@ -12,22 +12,16 @@ # Next.js environment variables NODE_ENV="development" NEXT_PUBLIC_NODE_ENV="development" -BASE_URL="http://localhost:3000" NEXT_PUBLIC_BASE_URL="http://localhost:3000" -# Next.js collects completely anonymous telemetry data about general usage. Learn more here: https://nextjs.org/telemetry -# Uncomment the following line to disable telemetry at run time -NEXT_TELEMETRY_DISABLED=1 +# Disable telemetry at run time +NEXT_TELEMETRY_DISABLED=1 # Injected in Dockerfile # Prisma -# https://www.prisma.io/docs/reference/database-reference/connection-urls#env DATABASE_URL="postgres://captable:password@pg:5432/captable" # Next Auth -# You can generate a new secret on the command line with: -# https://next-auth.js.org/configuration/options#secret - -# openssl rand -base64 32 +# Run `openssl rand -base64 32` to generate a new secret NEXTAUTH_SECRET="xxxxxxxxxx" NEXTAUTH_URL="http://localhost:3000" @@ -36,24 +30,13 @@ GOOGLE_CLIENT_ID="xxxxxxxxxx" GOOGLE_CLIENT_SECRET="xxxxxxxxxx" # SMTP -EMAIL_SERVER_HOST="localhost" -EMAIL_SERVER_PORT=1025 -EMAIL_SERVER_USERNAME="captable" -EMAIL_SERVER_PASSWORD="password" -EMAIL_SERVER_SECURE=0 -# EMAIL_SERVER=smtp://captable:password@127.0.0.1:2500 -EMAIL_FROM=hello@cap.new +EMAIL_FROM="'Captable, Inc.' " +EMAIL_SERVER=smtp://captable:password@127.0.0.1:2500 # Uploads -UPLOAD_PROVIDER="s3" UPLOAD_ENDPOINT="http://127.0.0.1:9002" -NEXT_PUBLIC_UPLOAD_DOMAIN="http://127.0.0.1:9002" - -# value should be 'auto' while using r2 -UPLOAD_REGION="us-east-1" +UPLOAD_REGION="us-east-1" # value should be 'auto' while using r2 UPLOAD_ACCESS_KEY_ID="captable" UPLOAD_SECRET_ACCESS_KEY="password" - UPLOAD_BUCKET_PUBLIC="captable-public-bucket" UPLOAD_BUCKET_PRIVATE="captable-private-bucket" - diff --git a/.gitpod.yml b/.gitpod.yml index ee9b96c67..0a51253bc 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -3,7 +3,6 @@ tasks: pnpm install && cp .env.example .env && export NEXTAUTH_SECRET="$(openssl rand -base64 32)" && - export BASE_URL="$(gp url 3000)" && export NEXT_PUBLIC_BASE_URL="$(gp url 3000)" && export EMAIL_SERVER_PORT=2500 command: pnpm db:migrate && pnpm dx diff --git a/SELF-HOSTING.md b/SELF-HOSTING.md index d67621d0e..4c34ce89c 100644 --- a/SELF-HOSTING.md +++ b/SELF-HOSTING.md @@ -14,4 +14,52 @@ If you like to self-host Captable, Inc., please schedule a call with us, and we - **Official Docker Images**: - Docker hub - - Github registry + + +- **Environment Variables**:\ +Following envrionment variables are required + +```bash +NODE_ENV="production" +DATABASE_URL="postgres://user:password@host:port/dbname" +NEXTAUTH_SECRET="xxx" # Generated by `openssl rand -base64 32` +NEXTAUTH_URL="https://your-domain.com" +NEXT_PUBLIC_BASE_URL="https://your-domain.com" + +# Email server environment variables +EMAIL_FROM="your@email.com" +EMAIL_SERVER="smtp://username:password@host:port" + +# File uplod environment variables +UPLOAD_REGION="us-west-1" # auto when using Cloudflare R2 +UPLOAD_ENDPOINT="https://xxx.r2.cloudflarestorage.com" +UPLOAD_ACCESS_KEY_ID="xxx" +UPLOAD_SECRET_ACCESS_KEY="xxx" +UPLOAD_BUCKET_PUBLIC="public-bucket-name" +UPLOAD_BUCKET_PRIVATE="private-bucket-name" +``` + +- **Setup CORS for file uploads**:\ + Some of the services including Cloudflare R2 may require you to setup CORS for file uploads.\ + + > Here is an sample CORS configuration for Cloudflare R2. + +```json +[ + { + "AllowedOrigins": [ + "https://your-domain.com" + ], + "AllowedMethods": [ + "HEAD", + "GET", + "POST", + "PUT", + "DELETE" + ], + "AllowedHeaders": [ + "*" + ] + } +] +``` \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index a00b9ff01..608dbebd8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -39,7 +39,6 @@ FROM base AS runner WORKDIR /app ENV NODE_ENV production -# Uncomment the following line in case you want to disable telemetry during runtime. ENV NEXT_TELEMETRY_DISABLED 1 ENV DOCKER_OUTPUT 1 diff --git a/src/common/uploads.ts b/src/common/uploads.ts index 1d20ae2db..80c0d574e 100644 --- a/src/common/uploads.ts +++ b/src/common/uploads.ts @@ -3,7 +3,6 @@ import { getPresignedPutUrl, type getPresignedUrlOptions, } from "@/server/file-uploads"; -import { env } from "next-runtime-env"; /** * usage * ```js @@ -47,12 +46,7 @@ export const uploadFile = async ( } const { name, type, size } = file; - let fileUrl = bucketUrl; - - const uploadDomain = env("NEXT_PUBLIC_UPLOAD_DOMAIN"); - if (bucketMode === "publicBucket" && uploadDomain) { - fileUrl = `${uploadDomain}/${key}`; - } + const fileUrl = bucketUrl; return { key, diff --git a/src/env.js b/src/env.js index 2fa6cf5f7..efbd84191 100644 --- a/src/env.js +++ b/src/env.js @@ -1,26 +1,12 @@ import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod"; -// https://env.t3.gg/docs/recipes#booleans -const COERCED_BOOLEAN = z - .string() - // transform to boolean using preferred coercion logic - .transform((s) => s !== "false" && s !== "0"); - -// const ONLY_BOOLEAN = z -// .string() -// // only allow "true" or "false" -// .refine((s) => s === "true" || s === "false") -// // transform to boolean -// .transform((s) => s === "true"); - export const env = createEnv({ /** * Specify your server-side environment variables schema here. This way you can ensure the app * isn't built with invalid env vars. */ server: { - BASE_URL: z.string().url(), DATABASE_URL: z .string() .url() @@ -36,25 +22,13 @@ export const env = createEnv({ EMAIL_SERVER: z.string().optional(), EMAIL_FROM: z.string(), - /// smtp - - EMAIL_SERVER_HOST: z.string(), - EMAIL_SERVER_PORT: z.coerce.number(), - EMAIL_SERVER_SECURE: COERCED_BOOLEAN, - EMAIL_SERVER_USERNAME: z.string().optional(), - EMAIL_SERVER_PASSWORD: z.string().optional(), - - //flags - // upload - UPLOAD_ENDPOINT: z.string(), UPLOAD_REGION: z.string(), UPLOAD_BUCKET_PUBLIC: z.string(), UPLOAD_BUCKET_PRIVATE: z.string(), UPLOAD_ACCESS_KEY_ID: z.string().optional(), UPLOAD_SECRET_ACCESS_KEY: z.string().optional(), - UPLOAD_PROVIDER: z.enum(["s3", "r2"]), // google GOOGLE_CLIENT_ID: z.string().optional(), @@ -70,7 +44,6 @@ export const env = createEnv({ // NEXT_PUBLIC_CLIENTVAR: z.string(), NEXT_PUBLIC_BASE_URL: z.string(), NEXT_PUBLIC_NODE_ENV: z.string().default("development"), - NEXT_PUBLIC_UPLOAD_DOMAIN: z.string().optional(), }, /** @@ -79,7 +52,6 @@ export const env = createEnv({ */ runtimeEnv: { NODE_ENV: process.env.NODE_ENV, - BASE_URL: process.env.BASE_URL, NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL, NEXT_PUBLIC_NODE_ENV: process.env.NEXT_PUBLIC_NODE_ENV, DATABASE_URL: process.env.DATABASE_URL, @@ -88,20 +60,12 @@ export const env = createEnv({ EMAIL_SERVER: process.env.EMAIL_SERVER, EMAIL_FROM: process.env.EMAIL_FROM, - EMAIL_SERVER_HOST: process.env.EMAIL_SERVER_HOST, - EMAIL_SERVER_PORT: process.env.EMAIL_SERVER_PORT, - EMAIL_SERVER_SECURE: process.env.EMAIL_SERVER_SECURE, - EMAIL_SERVER_USERNAME: process.env.EMAIL_SERVER_USERNAME, - EMAIL_SERVER_PASSWORD: process.env.EMAIL_SERVER_PASSWORD, - UPLOAD_ENDPOINT: process.env.UPLOAD_ENDPOINT, UPLOAD_REGION: process.env.UPLOAD_REGION, UPLOAD_BUCKET_PUBLIC: process.env.UPLOAD_BUCKET_PUBLIC, UPLOAD_BUCKET_PRIVATE: process.env.UPLOAD_BUCKET_PRIVATE, UPLOAD_ACCESS_KEY_ID: process.env.UPLOAD_ACCESS_KEY_ID, UPLOAD_SECRET_ACCESS_KEY: process.env.UPLOAD_SECRET_ACCESS_KEY, - UPLOAD_PROVIDER: process.env.UPLOAD_PROVIDER, - NEXT_PUBLIC_UPLOAD_DOMAIN: process.env.NEXT_PUBLIC_UPLOAD_DOMAIN, GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, diff --git a/src/jobs/auth-verification-email.ts b/src/jobs/auth-verification-email.ts index 94bcdc2e2..7ae583aa8 100644 --- a/src/jobs/auth-verification-email.ts +++ b/src/jobs/auth-verification-email.ts @@ -14,7 +14,7 @@ export const sendAuthVerificationEmail = async ( payload: AuthVerificationPayloadType, ) => { const { email, token } = payload; - const baseUrl = env.BASE_URL; + const baseUrl = env.NEXT_PUBLIC_BASE_URL; const confirmLink = `${baseUrl}/verify-email/${token}`; diff --git a/src/jobs/esign-email.ts b/src/jobs/esign-email.ts index 2fd5cb711..32e27bae9 100644 --- a/src/jobs/esign-email.ts +++ b/src/jobs/esign-email.ts @@ -34,7 +34,7 @@ export type ExtendedEsignPayloadType = EsignEmailPayloadType & export const sendEsignEmail = async (payload: ExtendedEsignPayloadType) => { const { email, token, ...rest } = payload; - const baseUrl = env.BASE_URL; + const baseUrl = env.NEXT_PUBLIC_BASE_URL; const html = await render( EsignEmail({ signingLink: `${baseUrl}/esign/${token}`, diff --git a/src/jobs/member-inivite-email.ts b/src/jobs/member-inivite-email.ts index 2086a1989..b275b142f 100644 --- a/src/jobs/member-inivite-email.ts +++ b/src/jobs/member-inivite-email.ts @@ -25,7 +25,7 @@ export const sendMemberInviteEmail = async ( ) => { const { email, token, verificationToken, company, user } = payload; - const baseUrl = env.BASE_URL; + const baseUrl = env.NEXT_PUBLIC_BASE_URL; const callbackUrl = `${baseUrl}/verify-member/${verificationToken}`; const params = new URLSearchParams({ diff --git a/src/jobs/password-reset-email.ts b/src/jobs/password-reset-email.ts index f01d049fa..92cb3e3a2 100644 --- a/src/jobs/password-reset-email.ts +++ b/src/jobs/password-reset-email.ts @@ -14,7 +14,7 @@ export const sendPasswordResetEmail = async ( payload: PasswordResetPayloadType, ) => { const { email, token } = payload; - const baseUrl = env.BASE_URL; + const baseUrl = env.NEXT_PUBLIC_BASE_URL; const confirmLink = `${baseUrl}/reset-password/${token}`; diff --git a/src/lib/authenticator.ts b/src/lib/authenticator.ts index 2e756734a..8a608acf1 100644 --- a/src/lib/authenticator.ts +++ b/src/lib/authenticator.ts @@ -5,13 +5,13 @@ import { env } from "@/env"; * Extracts common fields to identify the RP (relying party) */ export const getAuthenticatorOptions = () => { - const webAppBaseUrl = new URL(env.BASE_URL); + const webAppBaseUrl = new URL(env.NEXT_PUBLIC_BASE_URL); const rpId = webAppBaseUrl.hostname; return { rpName: "Captable", rpId, - origin: env.BASE_URL, + origin: env.NEXT_PUBLIC_BASE_URL, timeout: PASSKEY_TIMEOUT, }; }; diff --git a/src/trpc/routers/data-room-router/router.ts b/src/trpc/routers/data-room-router/router.ts index ebd3ff46e..5d667e91b 100644 --- a/src/trpc/routers/data-room-router/router.ts +++ b/src/trpc/routers/data-room-router/router.ts @@ -168,6 +168,7 @@ export const dataRoomRouter = createTRPCRouter({ data: room, }; } catch (error) { + console.error(error); return { success: false, message: @@ -207,7 +208,7 @@ export const dataRoomRouter = createTRPCRouter({ const company = dataRoom.company; const upsertManyRecipients = async () => { - const baseUrl = env.BASE_URL; + const baseUrl = env.NEXT_PUBLIC_BASE_URL; const recipients = [...others, ...selectedContacts]; for (const recipient of recipients) { @@ -251,7 +252,7 @@ export const dataRoomRouter = createTRPCRouter({ const link = `${baseUrl}/data-rooms/${dataRoom.publicId}?token=${token}`; const payload: DataRoomEmailPayloadType = { - senderName: senderName!, + senderName: `${senderName}`, recipientName: recipient.name, companyName: company.name, dataRoom: dataRoom.name, diff --git a/src/trpc/routers/update/procedures/share-update.ts b/src/trpc/routers/update/procedures/share-update.ts index edcb8847e..b48b8c7a5 100644 --- a/src/trpc/routers/update/procedures/share-update.ts +++ b/src/trpc/routers/update/procedures/share-update.ts @@ -39,7 +39,7 @@ export const shareUpdateProcedure = withAuth const company = update.company; const upsertManyRecipients = async () => { - const baseUrl = env.BASE_URL; + const baseUrl = env.NEXT_PUBLIC_BASE_URL; const recipients = [...others, ...selectedContacts]; for (const recipient of recipients) { @@ -84,7 +84,7 @@ export const shareUpdateProcedure = withAuth const link = `${baseUrl}/updates/${update.publicId}?token=${token}`; const payload: UpdateSharePayloadType = { - senderName: senderName!, + senderName: `${senderName}`, recipientName: recipient.name, companyName: company.name, update: { diff --git a/src/trpc/shared.ts b/src/trpc/shared.ts index 68ac9b1c5..d0638e007 100644 --- a/src/trpc/shared.ts +++ b/src/trpc/shared.ts @@ -1,17 +1,18 @@ -import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server"; +import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import superjson from "superjson"; -import { type AppRouter } from "@/trpc/api/root"; +import type { AppRouter } from "@/trpc/api/root"; export const transformer = superjson; function getBaseUrl() { if (typeof window !== "undefined") return ""; - return process.env.BASE_URL; + return process.env.NEXT_PUBLIC_BASE_URL; } export function getUrl() { - return getBaseUrl() + "/api/trpc"; + const baseUrl = getBaseUrl(); + return `${baseUrl}/api/trpc`; } /**