optimize the way we consume env variables

This commit is contained in:
EGOIST 2022-05-06 21:18:25 +08:00
parent f1428397f2
commit 2ee4f3197d
23 changed files with 120 additions and 88 deletions

View File

@ -9,9 +9,6 @@ GOOGLE_CLIENT_SECRET=
AUTH_COOKIE_NAME=app.session-token
# openssl rand -base64 32
AUTH_SECRET=xxx
OUR_DOMAIN=localhost:3000
S3_BUCKET_NAME=xxx
@ -19,6 +16,7 @@ S3_REGION=xxx
S3_ACCESS_KEY_ID=xxx
S3_SECRET_ACCESS_KEY=xxx
S3_ENDPOINT=s3.us-west-004.backblazeb2.com
S3_CDN_PREFIX=xxx
MAILGUN_APIKEY=xxx
MAILGUN_DOMAIN=xxx

View File

@ -1,14 +1,13 @@
import { Link } from "@remix-run/react"
import { logout } from "~/lib/auth.client"
import { APP_NAME } from "~/lib/env"
import { useStore } from "~/lib/store"
export function MainLayout({
isLoggedIn,
appName,
children,
}: {
isLoggedIn: boolean
appName: string
children: React.ReactNode
}) {
const setLoginModalOpened = useStore((store) => store.setLoginModalOpened)
@ -42,7 +41,7 @@ export function MainLayout({
<div className="max-w-screen-md mx-auto px-5">
<div>
<span className="inline-block text-3xl lg:text-7xl px-3 rounded-lg py-2 font-bold bg-indigo-600 text-white">
{appName}
{APP_NAME}
</span>
</div>
<div className="italic text-zinc-500 text-sm mt-1">

View File

@ -9,7 +9,8 @@ import { SubscribeModal } from "../common/SubscribeModal"
import { Avatar } from "../ui/Avatar"
import { Button } from "../ui/Button"
import { UniLink } from "../ui/UniLink"
import { IS_PROD, OUR_DOMAIN } from "~/lib/config.shared"
import { IS_PROD } from "~/lib/constants"
import { OUR_DOMAIN } from "~/lib/env"
type MenuLink = {
text: string

View File

@ -1,9 +1,9 @@
import type { User, Membership } from "@prisma/client"
import { AUTH_COOKIE_NAME } from "./config.server"
import { prismaRead } from "./db.server"
import dayjs from "dayjs"
import { type CookieSerializeOptions, createCookie } from "@remix-run/node"
import { IS_PROD, OUR_DOMAIN } from "./config.shared"
import { AUTH_COOKIE_NAME, OUR_DOMAIN } from "./env"
import { IS_PROD } from "./constants"
export type AuthUser = User & {
memberships: Membership[]

View File

@ -1,17 +0,0 @@
export const AUTH_COOKIE_NAME = process.env.AUTH_COOKIE_NAME
export const S3_REGION = process.env.S3_REGION
export const S3_ACCESS_KEY_ID = process.env.S3_ACCESS_KEY_ID
export const S3_SECRET_ACCESS_KEY = process.env.S3_SECRET_ACCESS_KEY
export const S3_BUCKET_NAME = process.env.S3_BUCKET_NAME
export const S3_ENDPOINT = process.env.S3_ENDPOINT
export const MAILGUN_APIKEY = process.env.MAILGUN_APIKEY
export const MAILGUN_DOMAIN = process.env.MAILGUN_DOMAIN
export const MAILGUN_EU = process.env.MAILGUN_EU
// Primary Fly region
export const PRIMARY_REGION = process.env.PRIMARY_REGION
// Curreny Fly region
export const FLY_REGION = process.env.FLY_REGION
export const IS_PRIMARY_REGION = Boolean(
FLY_REGION && PRIMARY_REGION === FLY_REGION
)

View File

@ -1,12 +1,9 @@
import { description } from "../../package.json"
export const IS_BROWSER = typeof window !== "undefined"
export const IS_BROWSER = typeof document !== "undefined"
export const IS_PROD = IS_BROWSER
? !location.hostname.endsWith("localhost")
: process.env.NODE_ENV === "production"
export const APP_NAME = IS_BROWSER ? ENV.APP_NAME : process.env.APP_NAME
export const OUR_DOMAIN = IS_BROWSER ? ENV.OUR_DOMAIN : process.env.OUR_DOMAIN
export const APP_DESCRIPTION = description

View File

@ -1,6 +1,6 @@
import { PrismaClient } from "@prisma/client"
import { IS_PRIMARY_REGION } from "./config.server"
import { IS_PROD } from "./config.shared"
import { IS_PROD } from "./constants"
import { isPrimaryRegion } from "./env"
import { singleton } from "./singleton.server"
export const prismaWrite = /* @__PURE__ */ singleton(
@ -12,7 +12,7 @@ export const prismaWrite = /* @__PURE__ */ singleton(
export const prismaRead = /* @__PURE__ */ singleton("prisma-read", () => {
// 5433 is the read-replica port
let url = process.env.DATABASE_URL
if (!IS_PRIMARY_REGION && IS_PROD) {
if (!isPrimaryRegion() && IS_PROD) {
url = url.replace(":5432", ":5433")
}
console.log("read replica url", url)

40
app/lib/env.ts Normal file
View File

@ -0,0 +1,40 @@
import { IS_BROWSER } from "./constants"
/**
* Can be called in browser and server
* only exposing env variables that're available in browser
*/
export const getCommonEnv = <T extends keyof BrowserEnv>(
key: T
): BrowserEnv[T] => {
return IS_BROWSER ? ENV[key] : process.env[key]
}
export const getServerEnv = <T extends keyof ServerEnv>(
key: T
): ServerEnv[T] => {
if (IS_BROWSER) throw new Error(`getServerEnv() called in browser`)
return process.env[key]
}
export const isPrimaryRegion = () => {
const FLY_REGION = getServerEnv("FLY_REGION")
const PRIMARY_REGION = getServerEnv("PRIMARY_REGION")
return Boolean(FLY_REGION && PRIMARY_REGION === FLY_REGION)
}
// Use /* @__PURE__ */ annotation to make tree-shaking work
export const AUTH_COOKIE_NAME = /* @__PURE__ */ getServerEnv("AUTH_COOKIE_NAME")
export const APP_NAME = /* @__PURE__ */ getCommonEnv("APP_NAME")
export const OUR_DOMAIN = /* @__PURE__ */ getCommonEnv("OUR_DOMAIN")
export const S3_CDN_PREFIX = /* @__PURE__ */ getCommonEnv("S3_CDN_PREFIX")
export const S3_REGION = /* @__PURE__ */ getServerEnv("S3_REGION")
export const S3_ACCESS_KEY_ID = /* @__PURE__ */ getServerEnv("S3_ACCESS_KEY_ID")
export const S3_SECRET_ACCESS_KEY = /* @__PURE__ */ getServerEnv(
"S3_SECRET_ACCESS_KEY"
)
export const S3_BUCKET_NAME = /* @__PURE__ */ getServerEnv("S3_BUCKET_NAME")
export const S3_ENDPOINT = /* @__PURE__ */ getServerEnv("S3_ENDPOINT")
export const MAILGUN_APIKEY = /* @__PURE__ */ getServerEnv("MAILGUN_APIKEY")
export const MAILGUN_DOMAIN = /* @__PURE__ */ getServerEnv("MAILGUN_DOMAIN")
export const MAILGUN_EU = /* @__PURE__ */ getServerEnv("MAILGUN_EU")

View File

@ -1,4 +1,5 @@
import { IS_PROD, OUR_DOMAIN } from "./config.shared"
import { IS_PROD } from "./constants"
import { OUR_DOMAIN } from "./env"
export const getSiteLink = ({ subdomain }: { subdomain: string }) => {
return `${IS_PROD ? "https" : "http"}://${subdomain}.${OUR_DOMAIN}`

View File

@ -1,9 +1,15 @@
import Mailgun from "mailgun.js"
import FormData from "form-data"
import { singleton } from "./singleton.server"
import { MAILGUN_APIKEY, MAILGUN_DOMAIN, MAILGUN_EU } from "~/lib/config.server"
import {
APP_NAME,
MAILGUN_APIKEY,
MAILGUN_DOMAIN,
MAILGUN_EU,
OUR_DOMAIN,
} from "~/lib/env"
import type { MailgunMessageData } from "mailgun.js/interfaces/Messages"
import { APP_NAME, IS_PROD, OUR_DOMAIN } from "./config.shared"
import { IS_PROD } from "./constants"
const getClient = () =>
singleton("mailgun", () => {

View File

@ -4,7 +4,7 @@ import {
S3_SECRET_ACCESS_KEY,
S3_BUCKET_NAME,
S3_ENDPOINT,
} from "~/lib/config.server"
} from "~/lib/env"
import {
S3Client,
PutObjectCommand,

View File

@ -1,4 +1,4 @@
import { OUR_DOMAIN } from "./config.shared"
import { OUR_DOMAIN } from "./env"
export const getTenant = (request: Request) => {
const host = request.headers.get("host")

View File

@ -1,4 +1,5 @@
import { IS_PROD, OUR_DOMAIN } from "./config.shared"
import { IS_PROD } from "./constants"
import { S3_CDN_PREFIX } from "./env"
export function getUserContentsUrl(filename: string): string
export function getUserContentsUrl(filename: undefined | null): undefined
@ -8,7 +9,7 @@ export function getUserContentsUrl<T extends string | undefined | null>(
export function getUserContentsUrl(filename: string | undefined | null) {
if (!filename) return undefined
if (IS_PROD) {
return `https://usercontents.${OUR_DOMAIN}/${filename}`
return `${S3_CDN_PREFIX}/${filename}`
}
return `/dev-s3-proxy?${new URLSearchParams({
filename: filename,

View File

@ -6,7 +6,6 @@ import {
} from "@remix-run/node"
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
@ -17,7 +16,7 @@ import LoginModal from "~/components/common/LoginModal"
import { createStore, StoreProvider } from "./lib/store"
import { Toaster } from "react-hot-toast"
import css from "./generated.css"
import { APP_NAME } from "./lib/config.shared"
import { APP_NAME } from "./lib/env"
export const meta: MetaFunction = () => {
return {
@ -31,13 +30,14 @@ export const links: LinksFunction = () => {
return [{ href: css, rel: "stylesheet", type: "text/css" }]
}
type LoaderData = { ENV: Record<string, string> }
type LoaderData = { ENV: BrowserEnv }
export const loader: LoaderFunction = async () => {
return json<LoaderData>({
ENV: {
APP_NAME: process.env.APP_NAME,
OUR_DOMAIN: process.env.OUR_DOMAIN,
S3_CDN_PREFIX: process.env.S3_CDN_PREFIX,
},
})
}

View File

@ -1,8 +1,9 @@
import { type LoaderFunction, redirect } from "@remix-run/node"
import { z } from "zod"
import { generateCookie } from "~/lib/auth.server"
import { IS_PROD, OUR_DOMAIN } from "~/lib/config.shared"
import { IS_PROD } from "~/lib/constants"
import { prismaRead, prismaWrite } from "~/lib/db.server"
import { OUR_DOMAIN } from "~/lib/env"
export const loader: LoaderFunction = async ({ request }) => {
const url = new URL(request.url)

View File

@ -4,8 +4,9 @@ import { prismaRead, prismaWrite } from "~/lib/db.server"
import { nanoid } from "nanoid"
import UAParser from "ua-parser-js"
import dayjs from "dayjs"
import { IS_PROD, OUR_DOMAIN } from "~/lib/config.shared"
import { generateCookie } from "~/lib/auth.server"
import { OUR_DOMAIN } from "~/lib/env"
import { IS_PROD } from "~/lib/constants"
export const loader: LoaderFunction = async ({ request }) => {
const url = new URL(request.url)

View File

@ -1,8 +1,9 @@
import { ActionFunction, redirect } from "@remix-run/node"
import { ActionFunction } from "@remix-run/node"
import dayjs from "dayjs"
import { z } from "zod"
import { IS_PROD, OUR_DOMAIN } from "~/lib/config.shared"
import { IS_PROD } from "~/lib/constants"
import { prismaWrite } from "~/lib/db.server"
import { OUR_DOMAIN } from "~/lib/env"
import { sendLoginEmail } from "~/lib/mailgun.server"
export const action: ActionFunction = async ({ request }) => {

View File

@ -6,7 +6,7 @@ import { Button } from "~/components/ui/Button"
import { Input } from "~/components/ui/Input"
import { siteController } from "~/controllers/site.controller"
import { getAuthUser } from "~/lib/auth.server"
import { OUR_DOMAIN } from "~/lib/config.shared"
import { OUR_DOMAIN } from "~/lib/env"
export const loader: LoaderFunction = async ({ request, params }) => {
await getAuthUser(request, true)

View File

@ -10,7 +10,7 @@ import { Button } from "~/components/ui/Button"
import { Input } from "~/components/ui/Input"
import { siteController } from "~/controllers/site.controller"
import { getAuthUser } from "~/lib/auth.server"
import { OUR_DOMAIN } from "~/lib/config.shared"
import { OUR_DOMAIN } from "~/lib/env"
export const loader: LoaderFunction = async ({ request }) => {
const user = await getAuthUser(request, true)

View File

@ -1,6 +1,6 @@
import { LoaderFunction } from "@remix-run/node"
import { S3_BUCKET_NAME, S3_ENDPOINT } from "~/lib/config.server"
import { IS_PROD } from "~/lib/config.shared"
import { type LoaderFunction } from "@remix-run/node"
import { IS_PROD } from "~/lib/constants"
import { S3_BUCKET_NAME, S3_ENDPOINT } from "~/lib/env"
export const loader: LoaderFunction = ({ request }) => {
if (IS_PROD) {

View File

@ -5,7 +5,8 @@ import { SiteHome } from "~/components/site/SiteHome"
import { SiteLayout, type SiteLayoutProps } from "~/components/site/SiteLayout"
import { siteController } from "~/controllers/site.controller"
import { getAuthUser } from "~/lib/auth.server"
import { APP_DESCRIPTION, APP_NAME } from "~/lib/config.shared"
import { APP_DESCRIPTION } from "~/lib/constants"
import { APP_NAME } from "~/lib/env"
import { getTenant } from "~/lib/tenant.server"
import { PageVisibilityEnum, type PostOnSiteHome } from "~/lib/types"
import { getSubscription } from "~/models/site.model"
@ -13,13 +14,11 @@ import { getSubscription } from "~/models/site.model"
type LoaderData =
| {
type: "main"
appName: string
isLoggedIn: boolean
}
| {
type: "tenant"
isLoggedIn: boolean
appName: string
tenant?: string
posts: PostOnSiteHome[]
site: SiteLayoutProps["site"]
@ -62,7 +61,6 @@ export const loader: LoaderFunction = async (ctx) => {
return json<LoaderData>({
type: "tenant",
isLoggedIn,
appName: APP_NAME,
tenant,
posts: pages,
site: {
@ -75,7 +73,10 @@ export const loader: LoaderFunction = async (ctx) => {
})
}
return json<LoaderData>({ type: "main", isLoggedIn, appName: APP_NAME })
return json<LoaderData>({
type: "main",
isLoggedIn,
})
}
export default function Home() {
@ -93,9 +94,5 @@ export default function Home() {
)
}
return (
<MainLayout isLoggedIn={data.isLoggedIn} appName={data.appName}>
{""}
</MainLayout>
)
return <MainLayout isLoggedIn={data.isLoggedIn}>{""}</MainLayout>
}

View File

@ -1,13 +1,12 @@
import { type RequestHandler } from "express"
import {
FLY_REGION,
IS_PRIMARY_REGION,
PRIMARY_REGION,
} from "~/lib/config.server"
import { IS_PROD } from "~/lib/config.shared"
import { IS_PROD } from "~/lib/constants"
import { getServerEnv, isPrimaryRegion } from "~/lib/env"
const PRIMARY_REGION = getServerEnv("PRIMARY_REGION")
const FLY_REGION = getServerEnv("FLY_REGION")
export const setFlyRegionHeader: RequestHandler = (req, res, next) => {
res.setHeader("x-fly-region", FLY_REGION || "unknown")
res.setHeader("x-fly-region", PRIMARY_REGION || "unknown")
next()
}
@ -17,7 +16,7 @@ export const getReplayResponse: RequestHandler = (req, res, next) => {
if (
!IS_PROD ||
["GET", "OPTIONS", "HEAD"].includes(method) ||
IS_PRIMARY_REGION
isPrimaryRegion()
) {
return next()
}

43
types.d.ts vendored
View File

@ -1,23 +1,30 @@
declare namespace NodeJS {
interface ProcessEnv {
// Additional environment variables
APP_NAME: string
DATABASE_URL: string
AUTH_COOKIE_NAME: string
AUTH_SECRET: string
OUR_DOMAIN: string
S3_REGION: string
S3_ACCESS_KEY_ID: string
S3_SECRET_ACCESS_KEY: string
S3_BUCKET_NAME: string
S3_ENDPOINT?: string
MAILGUN_APIKEY: string
MAILGUN_DOMAIN: string
MAILGUN_EU?: string
}
declare interface ServerEnv {
// Additional environment variables
APP_NAME: string
DATABASE_URL: string
AUTH_COOKIE_NAME: string
OUR_DOMAIN: string
S3_REGION: string
S3_ACCESS_KEY_ID: string
S3_SECRET_ACCESS_KEY: string
S3_BUCKET_NAME: string
S3_ENDPOINT?: string
MAILGUN_APIKEY: string
MAILGUN_DOMAIN: string
MAILGUN_EU?: string
S3_CDN_PREFIX: string
FLY_REGION?: string
PRIMARY_REGION?: string
}
declare const ENV: {
declare interface BrowserEnv {
APP_NAME: string
OUR_DOMAIN: string
S3_CDN_PREFIX: string
}
declare namespace NodeJS {
interface ProcessEnv extends ServerEnv {}
}
declare const ENV: BrowserEnv