mirror of
https://github.com/Mail-0/Zero.git
synced 2026-06-29 07:16:19 +00:00
feat: type safe env (#161)
This commit is contained in:
committed by
Jacob Samorowski
parent
1a46ce98a9
commit
abffebccc1
@@ -9,13 +9,14 @@ BETTER_AUTH_URL=http://localhost:3000
|
||||
# Change to your project's client ID and secret, these work with localhost:3000 and localhost:3001
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
GOOGLE_REDIRECT_URI=http://localhost:3000/api/v1/mail/auth/google/callback
|
||||
|
||||
NEXT_PUBLIC_POSTHOG_KEY=
|
||||
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||
|
||||
# Upstash Redis Instance
|
||||
REDIS_URL=""
|
||||
REDIS_TOKEN=""
|
||||
# Upstash/Local Redis Instance
|
||||
REDIS_URL="http://localhost:8079"
|
||||
REDIS_TOKEN="upstash-local-token"
|
||||
|
||||
# Resend API Key
|
||||
RESEND_API_KEY=
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { earlyAccess } from "@/db/schema";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { env } from "@/lib/env";
|
||||
import { db } from "@/db";
|
||||
|
||||
type PostgresError = {
|
||||
@@ -88,7 +89,7 @@ export async function POST(req: NextRequest) {
|
||||
});
|
||||
|
||||
// Return more detailed error in development
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
if (env.NODE_ENV === "development") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Internal server error",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ParsedMessage } from "@/types";
|
||||
import { google } from "googleapis";
|
||||
import { env } from "@/lib/env";
|
||||
import * as he from "he";
|
||||
|
||||
interface MailManager {
|
||||
@@ -53,9 +54,9 @@ const findHtmlBody = (parts: any[]): string => {
|
||||
|
||||
const googleDriver = async (config: IConfig): Promise<MailManager> => {
|
||||
const auth = new google.auth.OAuth2(
|
||||
process.env.GOOGLE_CLIENT_ID as string,
|
||||
process.env.GOOGLE_CLIENT_SECRET as string,
|
||||
process.env.GOOGLE_REDIRECT_URI as string,
|
||||
env.GOOGLE_CLIENT_ID as string,
|
||||
env.GOOGLE_CLIENT_SECRET as string,
|
||||
env.GOOGLE_REDIRECT_URI as string,
|
||||
);
|
||||
|
||||
const getScope = () =>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { createDriver } from "@/app/api/driver";
|
||||
import { connection } from "@/db/schema";
|
||||
import { env } from "@/lib/env";
|
||||
import { db } from "@/db";
|
||||
|
||||
export async function GET(
|
||||
@@ -12,9 +13,7 @@ export async function GET(
|
||||
const state = searchParams.get("state");
|
||||
|
||||
if (!code || !state) {
|
||||
return NextResponse.redirect(
|
||||
`${process.env.NEXT_PUBLIC_APP_URL}/settings/email?error=missing_params`,
|
||||
);
|
||||
return NextResponse.redirect(`${env.NEXT_PUBLIC_APP_URL}/settings/email?error=missing_params`);
|
||||
}
|
||||
|
||||
const { providerId } = await params;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
|
||||
import * as schema from "./schema";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
/**
|
||||
* Cache the database connection in development. This avoids creating a new connection on every HMR
|
||||
@@ -11,7 +12,7 @@ const globalForDb = globalThis as unknown as {
|
||||
conn: postgres.Sql | undefined;
|
||||
};
|
||||
|
||||
const conn = globalForDb.conn ?? postgres(process.env.DATABASE_URL!);
|
||||
if (process.env.NODE_ENV !== "production") globalForDb.conn = conn;
|
||||
const conn = globalForDb.conn ?? postgres(env.DATABASE_URL!);
|
||||
if (env.NODE_ENV !== "production") globalForDb.conn = conn;
|
||||
|
||||
export const db = drizzle(conn, { schema });
|
||||
|
||||
@@ -2,16 +2,38 @@ services:
|
||||
db:
|
||||
container_name: mail0-db
|
||||
image: postgres:17
|
||||
restart: always
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: mail0
|
||||
PGDATA: /var/lib/postgresql/data/pgdata
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
|
||||
valkey:
|
||||
container_name: mail0-redis
|
||||
image: docker.io/bitnami/valkey:8.0
|
||||
environment:
|
||||
- ALLOW_EMPTY_PASSWORD=yes
|
||||
- VALKEY_DISABLE_COMMANDS=FLUSHDB,FLUSHALL
|
||||
ports:
|
||||
- 6379:6379
|
||||
volumes:
|
||||
- valkey-data:/bitnami/valkey/data
|
||||
|
||||
upstash-proxy:
|
||||
container_name: mail0-upstash-proxy
|
||||
image: hiett/serverless-redis-http:latest
|
||||
environment:
|
||||
SRH_MODE: env
|
||||
SRH_TOKEN: upstash-local-token
|
||||
SRH_CONNECTION_STRING: "redis://valkey:6379"
|
||||
ports:
|
||||
- 8079:80
|
||||
|
||||
volumes:
|
||||
valkey-data:
|
||||
postgres-data:
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { type Config } from "drizzle-kit";
|
||||
import { env } from "./lib/env";
|
||||
|
||||
export default {
|
||||
schema: "./db/schema.ts",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
url: env.DATABASE_URL!,
|
||||
},
|
||||
out: "./db/migrations",
|
||||
tablesFilter: ["mail0_*"],
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { Redis } from "@upstash/redis";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
const url = process.env.REDIS_URL;
|
||||
const token = process.env.REDIS_TOKEN;
|
||||
|
||||
if (!url || !token) {
|
||||
throw new Error("Missing Redis URL or token");
|
||||
}
|
||||
|
||||
export const redis = new Redis({ url, token });
|
||||
export const redis = new Redis({ url: env.REDIS_URL, token: env.REDIS_TOKEN });
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { customSessionClient } from "better-auth/client/plugins";
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
import type { auth } from "@/lib/auth"; // Import the auth instance as a type
|
||||
|
||||
const BASE_URL = process.env.BASE_URL as string;
|
||||
import { env } from "./env";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: BASE_URL, // the base url of your auth server
|
||||
baseURL: env.NEXT_PUBLIC_APP_URL,
|
||||
plugins: [customSessionClient<typeof auth>()],
|
||||
});
|
||||
|
||||
|
||||
11
lib/auth.ts
11
lib/auth.ts
@@ -5,12 +5,13 @@ import { connection, user as _user } from "@/db/schema";
|
||||
import { customSession } from "better-auth/plugins";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Resend } from "resend";
|
||||
import { env } from "./env";
|
||||
import { db } from "@/db";
|
||||
|
||||
// If there is no resend key, it might be a local dev environment
|
||||
// In that case, we don't want to send emails and just log them
|
||||
const resend = process.env.RESEND_API_KEY
|
||||
? new Resend(process.env.RESEND_API_KEY)
|
||||
const resend = env.RESEND_API_KEY
|
||||
? new Resend(env.RESEND_API_KEY)
|
||||
: { emails: { send: async (...args: any[]) => console.log(args) } };
|
||||
|
||||
const options = {
|
||||
@@ -27,8 +28,8 @@ const options = {
|
||||
prompt: "consent",
|
||||
accessType: "offline",
|
||||
scope: ["https://mail.google.com/"],
|
||||
clientId: process.env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
|
||||
clientId: env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
||||
},
|
||||
},
|
||||
emailAndPassword: {
|
||||
@@ -52,7 +53,7 @@ const options = {
|
||||
sendOnSignUp: true,
|
||||
autoSignInAfterVerification: true,
|
||||
sendVerificationEmail: async ({ user, token }) => {
|
||||
const verificationUrl = `${process.env.BASE_URL}/api/auth/verify-email?token=${token}&callbackURL=/connect-emails`;
|
||||
const verificationUrl = `${env.NEXT_PUBLIC_APP_URL}/api/auth/verify-email?token=${token}&callbackURL=/connect-emails`;
|
||||
|
||||
await resend.emails.send({
|
||||
from: "Mail0 <onboarding@mail0.io>",
|
||||
|
||||
28
lib/env.ts
Normal file
28
lib/env.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { createEnv } from "@t3-oss/env-nextjs";
|
||||
import { z } from "zod";
|
||||
|
||||
export const env = createEnv({
|
||||
server: {
|
||||
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
||||
DATABASE_URL: z.string().min(1),
|
||||
BETTER_AUTH_SECRET: z.string().min(1),
|
||||
BETTER_AUTH_URL: z.string().min(1).url(),
|
||||
GOOGLE_CLIENT_ID: z.string().min(1),
|
||||
GOOGLE_CLIENT_SECRET: z.string().min(1),
|
||||
GOOGLE_REDIRECT_URI: z.string().min(1).url(),
|
||||
REDIS_URL: z.string().min(1).url(),
|
||||
REDIS_TOKEN: z.string().min(1),
|
||||
RESEND_API_KEY: z.string().min(1).optional(),
|
||||
},
|
||||
client: {
|
||||
NEXT_PUBLIC_APP_URL: z.string().min(1).url(),
|
||||
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
|
||||
NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(),
|
||||
},
|
||||
experimental__runtimeEnv: {
|
||||
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
|
||||
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
||||
...process.env,
|
||||
},
|
||||
});
|
||||
@@ -38,6 +38,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@radix-ui/react-visually-hidden": "^1.1.2",
|
||||
"@t3-oss/env-nextjs": "^0.12.0",
|
||||
"@upstash/ratelimit": "^2.0.5",
|
||||
"@upstash/redis": "^1.34.4",
|
||||
"axios": "^1.7.9",
|
||||
|
||||
43
pnpm-lock.yaml
generated
43
pnpm-lock.yaml
generated
@@ -62,6 +62,9 @@ importers:
|
||||
'@radix-ui/react-visually-hidden':
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@t3-oss/env-nextjs':
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.0(typescript@5.7.3)(zod@3.24.1)
|
||||
'@upstash/ratelimit':
|
||||
specifier: ^2.0.5
|
||||
version: 2.0.5(@upstash/redis@1.34.4)
|
||||
@@ -1491,6 +1494,34 @@ packages:
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
'@t3-oss/env-core@0.12.0':
|
||||
resolution: {integrity: sha512-lOPj8d9nJJTt81mMuN9GMk8x5veOt7q9m11OSnCBJhwp1QrL/qR+M8Y467ULBSm9SunosryWNbmQQbgoiMgcdw==}
|
||||
peerDependencies:
|
||||
typescript: '>=5.0.0'
|
||||
valibot: ^1.0.0-beta.7 || ^1.0.0
|
||||
zod: ^3.24.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
valibot:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@t3-oss/env-nextjs@0.12.0':
|
||||
resolution: {integrity: sha512-rFnvYk1049RnNVUPvY8iQ55AuQh1Rr+qZzQBh3t++RttCGK4COpXGNxS4+45afuQq02lu+QAOy/5955aU8hRKw==}
|
||||
peerDependencies:
|
||||
typescript: '>=5.0.0'
|
||||
valibot: ^1.0.0-beta.7 || ^1.0.0
|
||||
zod: ^3.24.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
valibot:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/typography@0.5.16':
|
||||
resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==}
|
||||
peerDependencies:
|
||||
@@ -5018,6 +5049,18 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@t3-oss/env-core@0.12.0(typescript@5.7.3)(zod@3.24.1)':
|
||||
optionalDependencies:
|
||||
typescript: 5.7.3
|
||||
zod: 3.24.1
|
||||
|
||||
'@t3-oss/env-nextjs@0.12.0(typescript@5.7.3)(zod@3.24.1)':
|
||||
dependencies:
|
||||
'@t3-oss/env-core': 0.12.0(typescript@5.7.3)(zod@3.24.1)
|
||||
optionalDependencies:
|
||||
typescript: 5.7.3
|
||||
zod: 3.24.1
|
||||
|
||||
'@tailwindcss/typography@0.5.16(tailwindcss@3.4.17)':
|
||||
dependencies:
|
||||
lodash.castarray: 4.4.0
|
||||
|
||||
Reference in New Issue
Block a user