mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 20:13:49 +00:00
Merge pull request #17 from aaronpo97/env
Add environment variable validation and parsing
This commit is contained in:
@@ -83,7 +83,7 @@ SPARKPOST_SENDER_ADDRESS=" > .env
|
|||||||
- `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_KEY`, and `CLOUDINARY_SECRET` are the credentials for your Cloudinary account.
|
- `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_KEY`, and `CLOUDINARY_SECRET` are the credentials for your Cloudinary account.
|
||||||
- You can create a free account [here](https://cloudinary.com/users/register/free).
|
- You can create a free account [here](https://cloudinary.com/users/register/free).
|
||||||
- `CONFIRMATION_TOKEN_SECRET` is the secret used to sign the confirmation token used for email confirmation.
|
- `CONFIRMATION_TOKEN_SECRET` is the secret used to sign the confirmation token used for email confirmation.
|
||||||
- You can generate a random string using the`openssl rand -hex 32` command.
|
- You can generate a random string using the`openssl rand -base64 127` command.
|
||||||
- `SESSION_SECRET` is the secret used to sign the session cookie.
|
- `SESSION_SECRET` is the secret used to sign the session cookie.
|
||||||
- Use the same command as above to generate a random string.
|
- Use the same command as above to generate a random string.
|
||||||
- `DATABASE_URL` is the URL of your CockroachDB database.
|
- `DATABASE_URL` is the URL of your CockroachDB database.
|
||||||
@@ -118,10 +118,10 @@ npm run dev
|
|||||||
|
|
||||||
The Biergarten App is licensed under the GNU General Public License v3.0. This means that anyone is free to use, modify, and distribute the code as long as they also distribute their modifications under the same license.
|
The Biergarten App is licensed under the GNU General Public License v3.0. This means that anyone is free to use, modify, and distribute the code as long as they also distribute their modifications under the same license.
|
||||||
|
|
||||||
I encourage anyone who uses this code for educational purposes to attribute the original author (i.e. me) and provide a link to the original repository.
|
I encourage anyone who uses this code for educational purposes to attribute me as the original author, and to provide a link to this repository.
|
||||||
|
|
||||||
By contributing to this repository, you agree to license your contributions under the same license as the project.
|
By contributing to this repository, you agree to license your contributions under the same license as the project.
|
||||||
|
|
||||||
If you have any questions or concerns about the license, please feel free to reach out to me..
|
If you have any questions or concerns about the license, please feel free to submit an issue to this repository.
|
||||||
|
|
||||||
I hope that this project will be useful to other developers and beer enthusiasts who are interested in learning about web development with Next.js, Prisma, CockroachDB, and other technologies.
|
I hope that this project will be useful to other developers and beer enthusiasts who are interested in learning about web development with Next.js, Prisma, CockroachDB, and other technologies.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
|
|||||||
<div className="flex items-center justify-center" id="comments-pagination">
|
<div className="flex items-center justify-center" id="comments-pagination">
|
||||||
<div className="btn-group">
|
<div className="btn-group">
|
||||||
<Link
|
<Link
|
||||||
className={`btn btn-ghost ${
|
className={`btn-ghost btn ${
|
||||||
commentsPageNum === 1
|
commentsPageNum === 1
|
||||||
? 'btn-disabled pointer-events-none'
|
? 'btn-disabled pointer-events-none'
|
||||||
: 'pointer-events-auto'
|
: 'pointer-events-auto'
|
||||||
@@ -32,9 +32,9 @@ const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
|
|||||||
>
|
>
|
||||||
<FaArrowLeft />
|
<FaArrowLeft />
|
||||||
</Link>
|
</Link>
|
||||||
<button className="btn btn-ghost pointer-events-none">{commentsPageNum}</button>
|
<button className="btn-ghost btn pointer-events-none">{commentsPageNum}</button>
|
||||||
<Link
|
<Link
|
||||||
className={`btn btn-ghost ${
|
className={`btn-ghost btn ${
|
||||||
commentsPageNum === commentsPageCount
|
commentsPageNum === commentsPageCount
|
||||||
? 'btn-disabled pointer-events-none'
|
? 'btn-disabled pointer-events-none'
|
||||||
: 'pointer-events-auto'
|
: 'pointer-events-auto'
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ const EditBeerPostForm: FC<EditBeerPostFormProps> = ({ previousValues }) => {
|
|||||||
{isSubmitting ? 'Submitting...' : 'Submit'}
|
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||||
</Button>
|
</Button>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-primary w-full rounded-xl ${isSubmitting ? 'loading' : ''}`}
|
className={`btn-primary btn w-full rounded-xl ${isSubmitting ? 'loading' : ''}`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const Navbar = () => {
|
|||||||
return (
|
return (
|
||||||
<nav className="navbar bg-primary text-primary-content">
|
<nav className="navbar bg-primary text-primary-content">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Link className="btn btn-ghost text-3xl normal-case" href="/">
|
<Link className="btn-ghost btn text-3xl normal-case" href="/">
|
||||||
<span className="cursor-pointer text-xl font-bold">The Biergarten App</span>
|
<span className="cursor-pointer text-xl font-bold">The Biergarten App</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,7 +69,7 @@ const Navbar = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-none lg:hidden">
|
<div className="flex-none lg:hidden">
|
||||||
<div className="dropdown dropdown-end">
|
<div className="dropdown dropdown-end">
|
||||||
<label tabIndex={0} className="btn btn-ghost btn-circle">
|
<label tabIndex={0} className="btn-ghost btn-circle btn">
|
||||||
<span className="w-10 rounded-full">
|
<span className="w-10 rounded-full">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const ErrorAlert: FC<ErrorAlertProps> = ({ error, setError }) => {
|
|||||||
|
|
||||||
<div className="flex-none">
|
<div className="flex-none">
|
||||||
<button
|
<button
|
||||||
className="btn btn-ghost btn-sm"
|
className="btn-ghost btn-sm btn"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setError('');
|
setError('');
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const Button: FunctionComponent<FormButtonProps> = ({
|
|||||||
// eslint-disable-next-line react/button-has-type
|
// eslint-disable-next-line react/button-has-type
|
||||||
<button
|
<button
|
||||||
type={type}
|
type={type}
|
||||||
className={`btn btn-primary w-full rounded-xl ${isSubmitting ? 'loading' : ''}`}
|
className={`btn-primary btn w-full rounded-xl ${isSubmitting ? 'loading' : ''}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const FormPageLayout: FC<FormPageLayoutProps> = ({
|
|||||||
<div className="align-center my-20 flex h-fit flex-col items-center justify-center">
|
<div className="align-center my-20 flex h-fit flex-col items-center justify-center">
|
||||||
<div className="w-8/12">
|
<div className="w-8/12">
|
||||||
<div className="tooltip tooltip-bottom absolute" data-tip={backLinkText}>
|
<div className="tooltip tooltip-bottom absolute" data-tip={backLinkText}>
|
||||||
<Link href={backLink} className="btn btn-ghost btn-sm p-0">
|
<Link href={backLink} className="btn-ghost btn-sm btn p-0">
|
||||||
<BiArrowBack className="text-xl" />
|
<BiArrowBack className="text-xl" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
import { serialize, parse } from 'cookie';
|
import { serialize, parse } from 'cookie';
|
||||||
import { SessionRequest } from './types';
|
import { SessionRequest } from './types';
|
||||||
|
import { NODE_ENV, SESSION_MAX_AGE, SESSION_TOKEN_NAME } from '../env';
|
||||||
const TOKEN_NAME = 'token';
|
|
||||||
export const MAX_AGE = 60 * 60 * 8; // 8 hours
|
|
||||||
|
|
||||||
export function setTokenCookie(res: NextApiResponse, token: string) {
|
export function setTokenCookie(res: NextApiResponse, token: string) {
|
||||||
const cookie = serialize(TOKEN_NAME, token, {
|
const cookie = serialize(SESSION_TOKEN_NAME, token, {
|
||||||
maxAge: MAX_AGE,
|
maxAge: SESSION_MAX_AGE,
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: NODE_ENV === 'production',
|
||||||
path: '/',
|
path: '/',
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
});
|
});
|
||||||
@@ -18,7 +16,7 @@ export function setTokenCookie(res: NextApiResponse, token: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function removeTokenCookie(res: NextApiResponse) {
|
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);
|
res.setHeader('Set-Cookie', cookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,5 +31,5 @@ export function parseCookies(req: SessionRequest) {
|
|||||||
|
|
||||||
export function getTokenCookie(req: SessionRequest) {
|
export function getTokenCookie(req: SessionRequest) {
|
||||||
const cookies = parseCookies(req);
|
const cookies = parseCookies(req);
|
||||||
return cookies[TOKEN_NAME];
|
return cookies[SESSION_TOKEN_NAME];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ import {
|
|||||||
UserSessionSchema,
|
UserSessionSchema,
|
||||||
} from '@/config/auth/types';
|
} from '@/config/auth/types';
|
||||||
import { z } from 'zod';
|
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';
|
import ServerError from '../util/ServerError';
|
||||||
|
|
||||||
const { SESSION_SECRET } = process.env;
|
|
||||||
|
|
||||||
export async function setLoginSession(
|
export async function setLoginSession(
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
session: z.infer<typeof BasicUserInfoSchema>,
|
session: z.infer<typeof BasicUserInfoSchema>,
|
||||||
@@ -19,7 +18,7 @@ export async function setLoginSession(
|
|||||||
throw new ServerError('Authentication is not configured.', 500);
|
throw new ServerError('Authentication is not configured.', 500);
|
||||||
}
|
}
|
||||||
const createdAt = Date.now();
|
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);
|
const token = await Iron.seal(obj, SESSION_SECRET, Iron.defaults);
|
||||||
|
|
||||||
setTokenCookie(res, token);
|
setTokenCookie(res, token);
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import { v2 as cloudinary } from 'cloudinary';
|
import { v2 as cloudinary } from 'cloudinary';
|
||||||
import { CloudinaryStorage } from 'multer-storage-cloudinary';
|
import { CloudinaryStorage } from 'multer-storage-cloudinary';
|
||||||
import ServerError from '../util/ServerError';
|
|
||||||
|
|
||||||
const { CLOUDINARY_CLOUD_NAME, CLOUDINARY_KEY, CLOUDINARY_SECRET } = process.env;
|
import { CLOUDINARY_CLOUD_NAME, CLOUDINARY_KEY, CLOUDINARY_SECRET } from '../env';
|
||||||
|
|
||||||
if (!(CLOUDINARY_CLOUD_NAME && CLOUDINARY_KEY && CLOUDINARY_SECRET)) {
|
|
||||||
throw new ServerError(
|
|
||||||
'The cloudinary credentials were not found in the environment variables.',
|
|
||||||
500,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
cloudinary.config({
|
cloudinary.config({
|
||||||
cloud_name: CLOUDINARY_CLOUD_NAME,
|
cloud_name: CLOUDINARY_CLOUD_NAME,
|
||||||
|
|||||||
147
config/env/index.ts
vendored
Normal file
147
config/env/index.ts
vendored
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
/* eslint-disable prefer-destructuring */
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { env } from 'process';
|
||||||
|
import ServerError from '../util/ServerError';
|
||||||
|
|
||||||
|
import 'dotenv/config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment variables are validated at runtime to ensure that they are present and have
|
||||||
|
* the correct type. This is done using the zod library.
|
||||||
|
*/
|
||||||
|
const envSchema = z.object({
|
||||||
|
BASE_URL: z.string().url(),
|
||||||
|
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().url(),
|
||||||
|
NODE_ENV: z.enum(['development', 'production', 'test']),
|
||||||
|
SPARKPOST_API_KEY: z.string(),
|
||||||
|
SPARKPOST_SENDER_ADDRESS: z.string().email(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = envSchema.safeParse(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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secret key for signing confirmation tokens.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* 'abcdefghijklmnopqrstuvwxyz123456';
|
||||||
|
*
|
||||||
|
* @see README.md for instructions on generating a secret key.
|
||||||
|
*/
|
||||||
|
export const CONFIRMATION_TOKEN_SECRET = parsed.data.CONFIRMATION_TOKEN_SECRET;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secret key for signing session cookies.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* 'abcdefghijklmnopqrstuvwxyz123456';
|
||||||
|
*
|
||||||
|
* @see README.md for instructions on generating a secret key.
|
||||||
|
*/
|
||||||
|
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';
|
||||||
|
*
|
||||||
|
* @see https://app.sparkpost.com/domains/list/sending
|
||||||
|
*/
|
||||||
|
export const SPARKPOST_SENDER_ADDRESS = parsed.data.SPARKPOST_SENDER_ADDRESS;
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
import { BasicUserInfoSchema } from '@/config/auth/types';
|
import { BasicUserInfoSchema } from '@/config/auth/types';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { CONFIRMATION_TOKEN_SECRET } from '../env';
|
||||||
const { CONFIRMATION_TOKEN_SECRET } = process.env;
|
|
||||||
|
|
||||||
if (!CONFIRMATION_TOKEN_SECRET) {
|
|
||||||
throw new Error('CONFIRMATION_TOKEN_SECRET is not defined');
|
|
||||||
}
|
|
||||||
|
|
||||||
type User = z.infer<typeof BasicUserInfoSchema>;
|
type User = z.infer<typeof BasicUserInfoSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { z } from 'zod';
|
|||||||
import logger from '../pino/logger';
|
import logger from '../pino/logger';
|
||||||
|
|
||||||
import ServerError from '../util/ServerError';
|
import ServerError from '../util/ServerError';
|
||||||
|
import { NODE_ENV } from '../env';
|
||||||
|
|
||||||
type NextConnectOptionsT = HandlerOptions<
|
type NextConnectOptionsT = HandlerOptions<
|
||||||
RequestHandler<
|
RequestHandler<
|
||||||
@@ -23,7 +24,7 @@ const NextConnectOptions: NextConnectOptionsT = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError(error, req, res) {
|
onError(error, req, res) {
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (NODE_ENV !== 'production') {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import SparkPost from 'sparkpost';
|
import SparkPost from 'sparkpost';
|
||||||
|
import { SPARKPOST_API_KEY } from '../env';
|
||||||
const { SPARKPOST_API_KEY } = process.env;
|
|
||||||
|
|
||||||
if (!SPARKPOST_API_KEY) {
|
|
||||||
throw new Error('SPARKPOST_API_KEY is not defined');
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = new SparkPost(SPARKPOST_API_KEY);
|
const client = new SparkPost(SPARKPOST_API_KEY);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { SPARKPOST_SENDER_ADDRESS } from '../env';
|
||||||
import client from './client';
|
import client from './client';
|
||||||
|
|
||||||
interface EmailParams {
|
interface EmailParams {
|
||||||
@@ -7,12 +8,6 @@ interface EmailParams {
|
|||||||
subject: string;
|
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 sendEmail = async ({ address, text, html, subject }: EmailParams) => {
|
||||||
const from = SPARKPOST_SENDER_ADDRESS;
|
const from = SPARKPOST_SENDER_ADDRESS;
|
||||||
|
|
||||||
|
|||||||
5
package-lock.json
generated
5
package-lock.json
generated
@@ -18,6 +18,7 @@
|
|||||||
"cloudinary": "^1.35.0",
|
"cloudinary": "^1.35.0",
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"multer": "^2.0.0-rc.4",
|
"multer": "^2.0.0-rc.4",
|
||||||
@@ -3355,7 +3356,6 @@
|
|||||||
"version": "16.0.3",
|
"version": "16.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
|
||||||
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
|
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -12414,8 +12414,7 @@
|
|||||||
"dotenv": {
|
"dotenv": {
|
||||||
"version": "16.0.3",
|
"version": "16.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
|
||||||
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
|
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"dotenv-cli": {
|
"dotenv-cli": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"format": "npx prettier . --write",
|
"format": "npx prettier . --write",
|
||||||
"prestart": "npm run build",
|
|
||||||
"seed": "npx ts-node ./prisma/seed/index.ts"
|
"seed": "npx ts-node ./prisma/seed/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -22,6 +21,7 @@
|
|||||||
"cloudinary": "^1.35.0",
|
"cloudinary": "^1.35.0",
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"multer": "^2.0.0-rc.4",
|
"multer": "^2.0.0-rc.4",
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost, beerRecommendations }
|
|||||||
<div className="my-12 flex w-full items-center justify-center ">
|
<div className="my-12 flex w-full items-center justify-center ">
|
||||||
<div className="w-11/12 space-y-3 xl:w-9/12">
|
<div className="w-11/12 space-y-3 xl:w-9/12">
|
||||||
<BeerInfoHeader beerPost={beerPost} />
|
<BeerInfoHeader beerPost={beerPost} />
|
||||||
<div className="mt-4 flex flex-col space-y-3 md:flex-row md:space-y-0 md:space-x-3">
|
<div className="mt-4 flex flex-col space-y-3 md:flex-row md:space-x-3 md:space-y-0">
|
||||||
<BeerPostCommentsSection beerPost={beerPost} />
|
<BeerPostCommentsSection beerPost={beerPost} />
|
||||||
<div className="md:w-[40%]">
|
<div className="md:w-[40%]">
|
||||||
<BeerRecommendations beerRecommendations={beerRecommendations} />
|
<BeerRecommendations beerRecommendations={beerRecommendations} />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { NODE_ENV } from '@/config/env';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
const globalForPrisma = global as unknown as { prisma: PrismaClient };
|
const globalForPrisma = global as unknown as { prisma: PrismaClient };
|
||||||
@@ -10,7 +11,7 @@ const DBClient = {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (NODE_ENV !== 'production') {
|
||||||
globalForPrisma.prisma = DBClient.instance;
|
globalForPrisma.prisma = DBClient.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
import { generateConfirmationToken } from '@/config/jwt';
|
import { generateConfirmationToken } from '@/config/jwt';
|
||||||
import sendEmail from '@/config/sparkpost/sendEmail';
|
import sendEmail from '@/config/sparkpost/sendEmail';
|
||||||
|
|
||||||
import ServerError from '@/config/util/ServerError';
|
|
||||||
import Welcome from '@/emails/Welcome';
|
import Welcome from '@/emails/Welcome';
|
||||||
import { render } from '@react-email/render';
|
import { render } from '@react-email/render';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { BASE_URL } from '@/config/env';
|
||||||
import GetUserSchema from './schema/GetUserSchema';
|
import GetUserSchema from './schema/GetUserSchema';
|
||||||
|
|
||||||
const { BASE_URL } = process.env;
|
|
||||||
|
|
||||||
if (!BASE_URL) {
|
|
||||||
throw new ServerError('BASE_URL env variable is not set.', 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserSchema = z.infer<typeof GetUserSchema>;
|
type UserSchema = z.infer<typeof GetUserSchema>;
|
||||||
|
|
||||||
const sendConfirmationEmail = async ({ id, username, email }: UserSchema) => {
|
const sendConfirmationEmail = async ({ id, username, email }: UserSchema) => {
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ module.exports = {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [require('daisyui'), require('tailwindcss-animate')],
|
||||||
require('daisyui'),
|
|
||||||
require('tailwindcss-animate')
|
|
||||||
],
|
|
||||||
|
|
||||||
daisyui: {
|
daisyui: {
|
||||||
logs: false,
|
logs: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user