Add environment variable validation and parsing

Adds a validation schema for the application's environment variables using the Zod library. The parsed environment variables are then exported as constants that can be imported throughout the application, replacing the direct use of process.env.
This commit is contained in:
Aaron William Po
2023-04-06 23:38:03 -04:00
parent 6b65e09c17
commit 0d3785ad1a
21 changed files with 171 additions and 69 deletions

View File

@@ -1,15 +1,13 @@
import { NextApiResponse } from 'next';
import { serialize, parse } from 'cookie';
import { SessionRequest } from './types';
const TOKEN_NAME = 'token';
export const MAX_AGE = 60 * 60 * 8; // 8 hours
import { NODE_ENV, SESSION_MAX_AGE, SESSION_TOKEN_NAME } from '../env';
export function setTokenCookie(res: NextApiResponse, token: string) {
const cookie = serialize(TOKEN_NAME, token, {
maxAge: MAX_AGE,
const cookie = serialize(SESSION_TOKEN_NAME, token, {
maxAge: SESSION_MAX_AGE,
httpOnly: false,
secure: process.env.NODE_ENV === 'production',
secure: NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
});
@@ -18,7 +16,7 @@ export function setTokenCookie(res: NextApiResponse, token: string) {
}
export function removeTokenCookie(res: NextApiResponse) {
const cookie = serialize(TOKEN_NAME, '', { maxAge: -1, path: '/' });
const cookie = serialize(SESSION_TOKEN_NAME, '', { maxAge: -1, path: '/' });
res.setHeader('Set-Cookie', cookie);
}
@@ -33,5 +31,5 @@ export function parseCookies(req: SessionRequest) {
export function getTokenCookie(req: SessionRequest) {
const cookies = parseCookies(req);
return cookies[TOKEN_NAME];
return cookies[SESSION_TOKEN_NAME];
}

View File

@@ -6,11 +6,10 @@ import {
UserSessionSchema,
} from '@/config/auth/types';
import { z } from 'zod';
import { MAX_AGE, setTokenCookie, getTokenCookie } from './cookie';
import { SESSION_MAX_AGE, SESSION_SECRET } from '@/config/env';
import { setTokenCookie, getTokenCookie } from './cookie';
import ServerError from '../util/ServerError';
const { SESSION_SECRET } = process.env;
export async function setLoginSession(
res: NextApiResponse,
session: z.infer<typeof BasicUserInfoSchema>,
@@ -19,7 +18,7 @@ export async function setLoginSession(
throw new ServerError('Authentication is not configured.', 500);
}
const createdAt = Date.now();
const obj = { ...session, createdAt, maxAge: MAX_AGE };
const obj = { ...session, createdAt, maxAge: SESSION_MAX_AGE };
const token = await Iron.seal(obj, SESSION_SECRET, Iron.defaults);
setTokenCookie(res, token);

View File

@@ -1,16 +1,8 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { v2 as cloudinary } from 'cloudinary';
import { CloudinaryStorage } from 'multer-storage-cloudinary';
import ServerError from '../util/ServerError';
const { CLOUDINARY_CLOUD_NAME, CLOUDINARY_KEY, CLOUDINARY_SECRET } = process.env;
if (!(CLOUDINARY_CLOUD_NAME && CLOUDINARY_KEY && CLOUDINARY_SECRET)) {
throw new ServerError(
'The cloudinary credentials were not found in the environment variables.',
500,
);
}
import { CLOUDINARY_CLOUD_NAME, CLOUDINARY_KEY, CLOUDINARY_SECRET } from '../env';
cloudinary.config({
cloud_name: CLOUDINARY_CLOUD_NAME,

136
config/env/index.ts vendored Normal file
View File

@@ -0,0 +1,136 @@
/* eslint-disable prefer-destructuring */
import { z } from 'zod';
import process from 'process';
import ServerError from '../util/ServerError';
import 'dotenv/config';
const envSchema = z.object({
BASE_URL: z.string(),
CLOUDINARY_CLOUD_NAME: z.string(),
CLOUDINARY_KEY: z.string(),
CLOUDINARY_SECRET: z.string(),
CONFIRMATION_TOKEN_SECRET: z.string(),
SESSION_SECRET: z.string(),
SESSION_TOKEN_NAME: z.string(),
SESSION_MAX_AGE: z.coerce.number().positive(),
DATABASE_URL: z.string(),
NODE_ENV: z.enum(['development', 'production', 'test']),
SPARKPOST_API_KEY: z.string(),
SPARKPOST_SENDER_ADDRESS: z.string().email(),
});
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
throw new ServerError('Invalid environment variables', 500);
}
/**
* Base URL of the application.
*
* @example
* 'https://example.com';
*/
export const BASE_URL = parsed.data.BASE_URL;
/**
* Cloudinary cloud name.
*
* @example
* 'my-cloud';
*
* @see https://cloudinary.com/documentation/cloudinary_references
* @see https://cloudinary.com/console
*/
export const CLOUDINARY_CLOUD_NAME = parsed.data.CLOUDINARY_CLOUD_NAME;
/**
* Cloudinary API key.
*
* @example
* '123456789012345';
*
* @see https://cloudinary.com/documentation/cloudinary_references
* @see https://cloudinary.com/console
*/
export const CLOUDINARY_KEY = parsed.data.CLOUDINARY_KEY;
/**
* Cloudinary API secret.
*
* @example
* 'abcdefghijklmnopqrstuvwxyz123456';
*
* @see https://cloudinary.com/documentation/cloudinary_references
* @see https://cloudinary.com/console
*/
export const CLOUDINARY_SECRET = parsed.data.CLOUDINARY_SECRET;
/**
* Fsd Fds Secret key for signing confirmation tokens.
*
* @example
* 'abcdefghijklmnopqrstuvwxyz123456';
*/
export const CONFIRMATION_TOKEN_SECRET = parsed.data.CONFIRMATION_TOKEN_SECRET;
/**
* Secret key for signing session cookies.
*
* @example
* 'abcdefghijklmnopqrstuvwxyz123456';
*/
export const SESSION_SECRET = parsed.data.SESSION_SECRET;
/**
* Name of the session cookie.
*
* @example
* 'my-app-session';
*/
export const SESSION_TOKEN_NAME = parsed.data.SESSION_TOKEN_NAME;
/**
* Maximum age of the session cookie in milliseconds.
*
* @example
* '86400000'; // 24 hours
*/
export const SESSION_MAX_AGE = parsed.data.SESSION_MAX_AGE;
/**
* URL of the CockroachDB database. CockroachDB uses the PostgreSQL wire protocol.
*
* @example
* 'postgres://username:password@localhost/my-database';
*/
export const DATABASE_URL = parsed.data.DATABASE_URL;
/**
* Node environment.
*
* @example
* 'production';
*
* @see https://nodejs.org/api/process.html#process_process_env
*/
export const NODE_ENV = parsed.data.NODE_ENV;
/**
* SparkPost API key.
*
* @example
* 'abcdefghijklmnopqrstuvwxyz123456';
*
* @see https://app.sparkpost.com/account/api-keys
*/
export const SPARKPOST_API_KEY = parsed.data.SPARKPOST_API_KEY;
/**
* Sender email address for SparkPost.
*
* @example
* 'noreply@example.com';
*/
export const SPARKPOST_SENDER_ADDRESS = parsed.data.SPARKPOST_SENDER_ADDRESS;

View File

@@ -1,12 +1,7 @@
import { BasicUserInfoSchema } from '@/config/auth/types';
import jwt from 'jsonwebtoken';
import { z } from 'zod';
const { CONFIRMATION_TOKEN_SECRET } = process.env;
if (!CONFIRMATION_TOKEN_SECRET) {
throw new Error('CONFIRMATION_TOKEN_SECRET is not defined');
}
import { CONFIRMATION_TOKEN_SECRET } from '../env';
type User = z.infer<typeof BasicUserInfoSchema>;

View File

@@ -6,6 +6,7 @@ import { z } from 'zod';
import logger from '../pino/logger';
import ServerError from '../util/ServerError';
import { NODE_ENV } from '../env';
type NextConnectOptionsT = HandlerOptions<
RequestHandler<
@@ -23,7 +24,7 @@ const NextConnectOptions: NextConnectOptionsT = {
});
},
onError(error, req, res) {
if (process.env.NODE_ENV !== 'production') {
if (NODE_ENV !== 'production') {
logger.error(error);
}

View File

@@ -1,10 +1,5 @@
import SparkPost from 'sparkpost';
const { SPARKPOST_API_KEY } = process.env;
if (!SPARKPOST_API_KEY) {
throw new Error('SPARKPOST_API_KEY is not defined');
}
import { SPARKPOST_API_KEY } from '../env';
const client = new SparkPost(SPARKPOST_API_KEY);

View File

@@ -1,3 +1,4 @@
import { SPARKPOST_SENDER_ADDRESS } from '../env';
import client from './client';
interface EmailParams {
@@ -7,12 +8,6 @@ interface EmailParams {
subject: string;
}
const { SPARKPOST_SENDER_ADDRESS } = process.env;
if (!SPARKPOST_SENDER_ADDRESS) {
throw new Error('SPARKPOST_SENDER_ADDRESS env variable is not set.');
}
const sendEmail = async ({ address, text, html, subject }: EmailParams) => {
const from = SPARKPOST_SENDER_ADDRESS;