mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 18:52:06 +00:00
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:
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
136
config/env/index.ts
vendored
Normal 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;
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user