diff --git a/README.md b/README.md index 0fdaaf8..754a4a1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,50 @@ -# Awesome Project Build with TypeORM +# Node.js, Express, TypeORM, PostgreSQL: CRUD Rest API -Steps to run this project: +In this article, you'll learn how to build CRUD RESTful API with Node.js, ExpressJs, TypeORM, and PostgreSQL. We will define the database schema with TypeORM and run the migration command to push the TypeORM schema to the database. Next, we will create higher-level CRUD function to perform the CRUD operations. -1. Run `npm i` command -2. Setup database settings inside `data-source.ts` file -3. Run `npm start` command +![Node.js, Express, TypeORM, PostgreSQL: CRUD Rest API](https://codevoweb.com/wp-content/uploads/2022/05/Node.js-Express-TypeORM-PostgreSQL-CRUD-Rest-API.webp) + +## Topics Covered + +- Node.js, TypeORM, PostgreSQL CRUD RESTful API Overview +- Project Structure +- Model Data with TypeORM and PostgreSQL + - Define a Base Entity + - Add One-to-Many Relationship to User Entity + - Complete User Entity + - Create a Post Entity +- Create Validation Schemas with Zod +- Create Services to Communicate with Database +- Create Route Controllers + - Create a New Post Controller + - Get a Single Post Controller + - Get All Posts Controller + - Update a Single Post Controller + - Delete a Single Post Controller +- Create Routes with Express +- Add the Routes to the Express Middleware Pipeline +- Run Database Migration with TypeORM + +Read the entire article here: [https://codevoweb.com/node-express-typeorm-postgresql-rest-api](https://codevoweb.com/node-express-typeorm-postgresql-rest-api) + +Articles in this series: + +### 1. API with Node.js + PostgreSQL + TypeORM: Project Setup + +[API with Node.js + PostgreSQL + TypeORM: Project Setup](https://codevoweb.com/api-node-postgresql-typeorm-project-setup) + +### 2. API with Node.js + PostgreSQL + TypeORM: JWT Authentication + +[API with Node.js + PostgreSQL + TypeORM: JWT Authentication](https://codevoweb.com/api-node-postgresql-typeorm-jwt-authentication) + +### 3. API with Node.js + PostgreSQL + TypeORM: Send Emails + +[API with Node.js + PostgreSQL + TypeORM: Send Emails](https://codevoweb.com/api-node-postgresql-typeorm-send-emails) + +### 4. Node.js, Express, TypeORM, PostgreSQL: CRUD Rest API + +[Node.js, Express, TypeORM, PostgreSQL: CRUD Rest API](https://codevoweb.com/node-express-typeorm-postgresql-rest-api) + +### 5. Node.js and PostgreSQL: Upload and Resize Multiple Images + +[Node.js and PostgreSQL: Upload and Resize Multiple Images](https://codevoweb.com/node-postgresql-upload-resize-multiple-images) diff --git a/config/custom-environment-variables.ts b/config/custom-environment-variables.ts index db297d4..6e26e9e 100644 --- a/config/custom-environment-variables.ts +++ b/config/custom-environment-variables.ts @@ -7,8 +7,16 @@ export default { password: 'POSTGRES_PASSWORD', database: 'POSTGRES_DB', }, + accessTokenPrivateKey: 'JWT_ACCESS_TOKEN_PRIVATE_KEY', accessTokenPublicKey: 'JWT_ACCESS_TOKEN_PUBLIC_KEY', refreshTokenPrivateKey: 'JWT_REFRESH_TOKEN_PRIVATE_KEY', refreshTokenPublicKey: 'JWT_REFRESH_TOKEN_PUBLIC_KEY', + + smtp: { + host: 'EMAIL_HOST', + pass: 'EMAIL_PASS', + port: 'EMAIL_PORT', + user: 'EMAIL_USER', + }, }; diff --git a/config/default.ts b/config/default.ts index 86b5716..e2dd0fc 100644 --- a/config/default.ts +++ b/config/default.ts @@ -3,4 +3,5 @@ export default { accessTokenExpiresIn: 15, refreshTokenExpiresIn: 60, redisCacheExpiresIn: 60, + emailFrom: 'contact@codevoweb.com', }; diff --git a/docker-compose.yml b/docker-compose.yml index 8c6ca7c..404888a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: ports: - '6500:5432' volumes: - - progresDB:/data/postgres + - progresDB:/var/lib/postgresql/data env_file: - ./.env diff --git a/example.env b/example.env index e0298cf..789f0ba 100644 --- a/example.env +++ b/example.env @@ -7,6 +7,10 @@ POSTGRES_USER=admin POSTGRES_PASSWORD=password123 POSTGRES_DB=node_typeorm +EMAIL_USER=ypi5eci55z2an5pm@ethereal.email +EMAIL_PASS=5B2N9Pn1ETvmXxDCye +EMAIL_HOST=smtp.ethereal.email +EMAIL_PORT=587 JWT_ACCESS_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT1FJQkFBSkJBSjdVblpyNUxpUGJxbDRENlo3VHVKK2NFMkI0Y3FzbnUzeUJaUHo2NmtqZDhJT1RFdjlNCkpEdmhMQ05PczYyWHBZcmFZYU5HS3UrN3Q4YVVjcWNoRzJNQ0F3RUFBUUpBRS84YXRKY29tdlVkOXVZeE5JRGQKWHFMc3dabUlma25yVGRxUWwxVVR5QWFPRWpIRGFnR0lGdEhRZE5IZTAybkp6a2Z1WkdWSkJVRmo1aTJJVyszMQpxUUloQU9LQVRxOVpSZHR0T0JDWWJLR3VpbjZnd1FVZ2YzUGkwSjh3Snh3cjNRVDFBaUVBczRRZ0lhR1c1V3NaCmNqWUQ3bVpwcXBiRkdubnBvMVR6QU1YS2psQ2dKL2NDSUQyZVZFbWx5cmhnSlNGMnBnN3lNZUV6RUcrNW9KTEIKUUtvZDZuWGloUFZGQWlCY0ZuUXhMR1p1NjhEUzhOaVZiQjNhYjV0TzJLazhxekE0L2ozSlFaeld3d0lnU2N1awoyZWlBM3E5ci9EdDQ3OUZHek4xSEVSdzJ2TXZjeEV6REZWRnV5aDg9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t diff --git a/package.json b/package.json index 0cb8a4e..aca721f 100644 --- a/package.json +++ b/package.json @@ -5,22 +5,45 @@ "license": "MIT", "scripts": { "start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts", - "typeorm": "typeorm-ts-node-commonjs" + "build": "tsc -p .", + "typeorm": "typeorm-ts-node-commonjs", + "migrate": "rm -rf build && yarn build && yarn typeorm migration:generate ./src/migrations/added-user-entity -d ./src/utils/data-source.ts", + "db:push": "rm -rf build && yarn build && yarn typeorm migration:run -d src/utils/data-source.ts", + "importData": "npx ts-node-dev --transpile-only --exit-child src/data/seeder.ts" }, "dependencies": { + "bcryptjs": "^2.4.3", + "body-parser": "^1.19.1", "config": "^3.3.7", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", "dotenv": "^16.0.0", "envalid": "^7.3.1", "express": "^4.18.1", + "html-to-text": "^8.2.0", + "jsonwebtoken": "^8.5.1", + "nodemailer": "^6.7.5", "pg": "^8.4.0", + "pug": "^3.0.2", "redis": "^4.1.0", "reflect-metadata": "^0.1.13", - "typeorm": "0.3.6" + "typeorm": "0.3.6", + "zod": "^3.14.4" }, "devDependencies": { + "@faker-js/faker": "^6.3.1", + "@types/bcryptjs": "^2.4.2", "@types/config": "^0.0.41", + "@types/cookie-parser": "^1.4.3", + "@types/cors": "^2.8.12", "@types/express": "^4.17.13", + "@types/html-to-text": "^8.1.0", + "@types/jsonwebtoken": "^8.5.8", + "@types/morgan": "^1.9.3", "@types/node": "^16.11.10", + "@types/nodemailer": "^6.4.4", + "@types/pug": "^2.0.6", + "morgan": "^1.10.0", "ts-node": "10.7.0", "ts-node-dev": "^1.1.8", "typescript": "4.5.2" diff --git a/public/posts/user-abc56714-68a8-474f-934e-420bd3d508ce-1651885329214.jpeg b/public/posts/user-abc56714-68a8-474f-934e-420bd3d508ce-1651885329214.jpeg new file mode 100644 index 0000000..352d2a2 Binary files /dev/null and b/public/posts/user-abc56714-68a8-474f-934e-420bd3d508ce-1651885329214.jpeg differ diff --git a/public/posts/user-dc52af76-cb5b-46a8-a62b-6a0074caf6b3-1652127001770.jpeg b/public/posts/user-dc52af76-cb5b-46a8-a62b-6a0074caf6b3-1652127001770.jpeg new file mode 100644 index 0000000..5cde6c5 Binary files /dev/null and b/public/posts/user-dc52af76-cb5b-46a8-a62b-6a0074caf6b3-1652127001770.jpeg differ diff --git a/src/app.ts b/src/app.ts index 9a3742c..c01b612 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,10 +1,23 @@ require('dotenv').config(); -import express, { Response } from 'express'; +import express, { NextFunction, Request, Response } from 'express'; import config from 'config'; -import validateEnv from './utils/validateEnv'; +import morgan from 'morgan'; +import cookieParser from 'cookie-parser'; +import cors from 'cors'; import { AppDataSource } from './utils/data-source'; +import AppError from './utils/appError'; +import authRouter from './routes/auth.routes'; +import userRouter from './routes/user.routes'; +import postRouter from './routes/post.routes'; +import validateEnv from './utils/validateEnv'; import redisClient from './utils/connectRedis'; +// import nodemailer from 'nodemailer'; +// (async function () { +// const credentials = await nodemailer.createTestAccount(); +// console.log(credentials); +// })(); + AppDataSource.initialize() .then(async () => { // VALIDATE ENV @@ -12,22 +25,38 @@ AppDataSource.initialize() const app = express(); + // TEMPLATE ENGINE + app.set('view engine', 'pug'); + app.set('views', `${__dirname}/views`); + // MIDDLEWARE // 1. Body parser app.use(express.json({ limit: '10kb' })); // 2. Logger + if (process.env.NODE_ENV === 'development') app.use(morgan('dev')); // 3. Cookie Parser + app.use(cookieParser()); // 4. Cors + app.use( + cors({ + origin: config.get('origin'), + credentials: true, + }) + ); // ROUTES + app.use('/api/auth', authRouter); + app.use('/api/users', userRouter); + app.use('/api/posts', postRouter); // HEALTH CHECKER - app.get('/api/healthchecker', async (_, res: Response) => { + app.get('/api/healthChecker', async (_, res: Response) => { const message = await redisClient.get('try'); + res.status(200).json({ status: 'success', message, @@ -35,8 +64,22 @@ AppDataSource.initialize() }); // UNHANDLED ROUTE + app.all('*', (req: Request, res: Response, next: NextFunction) => { + next(new AppError(404, `Route ${req.originalUrl} not found`)); + }); // GLOBAL ERROR HANDLER + app.use( + (error: AppError, req: Request, res: Response, next: NextFunction) => { + error.status = error.status || 'error'; + error.statusCode = error.statusCode || 500; + + res.status(error.statusCode).json({ + status: error.status, + message: error.message, + }); + } + ); const port = config.get('port'); app.listen(port); diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts new file mode 100644 index 0000000..12dfa78 --- /dev/null +++ b/src/controllers/auth.controller.ts @@ -0,0 +1,260 @@ +import { CookieOptions, NextFunction, Request, Response } from 'express'; +import config from 'config'; +import crypto from 'crypto'; +import { + CreateUserInput, + LoginUserInput, + VerifyEmailInput, +} from '../schemas/user.schema'; +import { + createUser, + findUser, + findUserByEmail, + findUserById, + signTokens, +} from '../services/user.service'; +import AppError from '../utils/appError'; +import redisClient from '../utils/connectRedis'; +import { signJwt, verifyJwt } from '../utils/jwt'; +import { User } from '../entities/user.entity'; +import Email from '../utils/email'; + +const cookiesOptions: CookieOptions = { + httpOnly: true, + sameSite: 'lax', +}; + +if (process.env.NODE_ENV === 'production') cookiesOptions.secure = true; + +const accessTokenCookieOptions: CookieOptions = { + ...cookiesOptions, + expires: new Date( + Date.now() + config.get('accessTokenExpiresIn') * 60 * 1000 + ), + maxAge: config.get('accessTokenExpiresIn') * 60 * 1000, +}; + +const refreshTokenCookieOptions: CookieOptions = { + ...cookiesOptions, + expires: new Date( + Date.now() + config.get('refreshTokenExpiresIn') * 60 * 1000 + ), + maxAge: config.get('refreshTokenExpiresIn') * 60 * 1000, +}; + +export const registerUserHandler = async ( + req: Request<{}, {}, CreateUserInput>, + res: Response, + next: NextFunction +) => { + try { + const { name, password, email } = req.body; + + const newUser = await createUser({ + name, + email: email.toLowerCase(), + password, + }); + + const { hashedVerificationCode, verificationCode } = + User.createVerificationCode(); + newUser.verificationCode = hashedVerificationCode; + await newUser.save(); + + // Send Verification Email + const redirectUrl = `${config.get( + 'origin' + )}/verifyemail/${verificationCode}`; + + try { + await new Email(newUser, redirectUrl).sendVerificationCode(); + + res.status(201).json({ + status: 'success', + message: + 'An email with a verification code has been sent to your email', + }); + } catch (error) { + newUser.verificationCode = null; + await newUser.save(); + + return res.status(500).json({ + status: 'error', + message: 'There was an error sending email, please try again', + }); + } + } catch (err: any) { + if (err.code === '23505') { + return res.status(409).json({ + status: 'fail', + message: 'User with that email already exist', + }); + } + next(err); + } +}; + +export const loginUserHandler = async ( + req: Request<{}, {}, LoginUserInput>, + res: Response, + next: NextFunction +) => { + try { + const { email, password } = req.body; + const user = await findUserByEmail({ email: email.toLowerCase() }); + + // 1. Check if user exist + if (!user) { + return next(new AppError(400, 'Invalid email or password')); + } + + // 2.Check if user is verified + if (!user.verified) { + return next( + new AppError( + 401, + 'You are not verified, check your email to verify your account' + ) + ); + } + + //3. Check if password is valid + if (!(await User.comparePasswords(password, user.password))) { + return next(new AppError(400, 'Invalid email or password')); + } + + // 4. Sign Access and Refresh Tokens + const { access_token, refresh_token } = await signTokens(user); + + // 5. Add Cookies + res.cookie('access_token', access_token, accessTokenCookieOptions); + res.cookie('refresh_token', refresh_token, refreshTokenCookieOptions); + res.cookie('logged_in', true, { + ...accessTokenCookieOptions, + httpOnly: false, + }); + + // 6. Send response + res.status(200).json({ + status: 'success', + access_token, + }); + } catch (err: any) { + next(err); + } +}; + +export const verifyEmailHandler = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const verificationCode = crypto + .createHash('sha256') + .update(req.params.verificationCode) + .digest('hex'); + + const user = await findUser({ verificationCode }); + + if (!user) { + return next(new AppError(401, 'Could not verify email')); + } + + user.verified = true; + user.verificationCode = null; + await user.save(); + + res.status(200).json({ + status: 'success', + message: 'Email verified successfully', + }); + } catch (err: any) { + next(err); + } +}; + +export const refreshAccessTokenHandler = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const refresh_token = req.cookies.refresh_token; + + const message = 'Could not refresh access token'; + + if (!refresh_token) { + return next(new AppError(403, message)); + } + + // Validate refresh token + const decoded = verifyJwt<{ sub: string }>( + refresh_token, + 'refreshTokenPublicKey' + ); + + if (!decoded) { + return next(new AppError(403, message)); + } + + // Check if user has a valid session + const session = await redisClient.get(decoded.sub); + + if (!session) { + return next(new AppError(403, message)); + } + + // Check if user still exist + const user = await findUserById(JSON.parse(session).id); + + if (!user) { + return next(new AppError(403, message)); + } + + // Sign new access token + const access_token = signJwt({ sub: user.id }, 'accessTokenPrivateKey', { + expiresIn: `${config.get('accessTokenExpiresIn')}m`, + }); + + // 4. Add Cookies + res.cookie('access_token', access_token, accessTokenCookieOptions); + res.cookie('logged_in', true, { + ...accessTokenCookieOptions, + httpOnly: false, + }); + + // 5. Send response + res.status(200).json({ + status: 'success', + access_token, + }); + } catch (err: any) { + next(err); + } +}; + +const logout = (res: Response) => { + res.cookie('access_token', '', { maxAge: 1 }); + res.cookie('refresh_token', '', { maxAge: 1 }); + res.cookie('logged_in', '', { maxAge: 1 }); +}; + +export const logoutHandler = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const user = res.locals.user; + + await redisClient.del(user.id); + logout(res); + + res.status(200).json({ + status: 'success', + }); + } catch (err: any) { + next(err); + } +}; diff --git a/src/controllers/post.controller.ts b/src/controllers/post.controller.ts new file mode 100644 index 0000000..4366a6d --- /dev/null +++ b/src/controllers/post.controller.ts @@ -0,0 +1,130 @@ +import { NextFunction, Request, Response } from 'express'; +import { + CreatePostInput, + DeletePostInput, + GetPostInput, + UpdatePostInput, +} from '../schemas/post.schema'; +import { createPost, findPosts, getPost } from '../services/post.service'; +import { findUserById } from '../services/user.service'; +import AppError from '../utils/appError'; + +export const createPostHandler = async ( + req: Request<{}, {}, CreatePostInput>, + res: Response, + next: NextFunction +) => { + try { + const user = await findUserById(res.locals.user.id as string); + + const post = await createPost(req.body, user!); + + res.status(201).json({ + status: 'success', + data: { + post, + }, + }); + } catch (err: any) { + if (err.code === '23505') { + return res.status(409).json({ + status: 'fail', + message: 'Post with that title already exist', + }); + } + next(err); + } +}; + +export const getPostHandler = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const post = await getPost(req.params.postId); + + if (!post) { + return next(new AppError(404, 'Post with that ID not found')); + } + + res.status(200).json({ + status: 'success', + data: { + post, + }, + }); + } catch (err: any) { + next(err); + } +}; + +export const getPostsHandler = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const posts = await findPosts(req); + + res.status(200).json({ + status: 'success', + results: posts.length, + data: { + posts, + }, + }); + } catch (err: any) { + next(err); + } +}; + +export const updatePostHandler = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const post = await getPost(req.params.postId); + + if (!post) { + return next(new AppError(404, 'Post with that ID not found')); + } + + Object.assign(post, req.body); + + const updatedPost = await post.save(); + + res.status(200).json({ + status: 'success', + data: { + post: updatedPost, + }, + }); + } catch (err: any) { + next(err); + } +}; + +export const deletePostHandler = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const post = await getPost(req.params.postId); + + if (!post) { + return next(new AppError(404, 'Post with that ID not found')); + } + + await post.remove(); + + res.status(204).json({ + status: 'success', + data: null, + }); + } catch (err: any) { + next(err); + } +}; diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts new file mode 100644 index 0000000..75b9105 --- /dev/null +++ b/src/controllers/user.controller.ts @@ -0,0 +1,20 @@ +import { NextFunction, Request, Response } from 'express'; + +export const getMeHandler = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const user = res.locals.user; + + res.status(200).status(200).json({ + status: 'success', + data: { + user, + }, + }); + } catch (err: any) { + next(err); + } +}; diff --git a/src/data/seeder.ts b/src/data/seeder.ts new file mode 100644 index 0000000..151de12 --- /dev/null +++ b/src/data/seeder.ts @@ -0,0 +1,34 @@ +import { faker } from '@faker-js/faker'; +import { Post } from '../entities/post.entity'; +import { User } from '../entities/user.entity'; +import { AppDataSource } from '../utils/data-source'; + +const postRepository = AppDataSource.getRepository(Post); +const userRepository = AppDataSource.getRepository(User); + +AppDataSource.initialize() + .then(async () => { + const user = await userRepository.findOne({ + where: { id: '3acc4249-c7c7-4ce0-b87c-01564dd9ba4e' }, + }); + console.log('Connected to database...'); + (async function () { + try { + for (let i = 0; i < 50; i++) { + const postInput: Partial = { + title: faker.lorem.words(4), + content: faker.lorem.words(10), + user: user!, + image: faker.image.imageUrl(), + }; + + await postRepository.save(postRepository.create(postInput)); + console.log(`Added ${postInput.title} to database`); + } + } catch (error) { + console.log(error); + process.exit(1); + } + })(); + }) + .catch((error: any) => console.log(error)); diff --git a/src/entities/model.entity.ts b/src/entities/model.entity.ts new file mode 100644 index 0000000..4081f02 --- /dev/null +++ b/src/entities/model.entity.ts @@ -0,0 +1,17 @@ +import { + CreateDateColumn, + UpdateDateColumn, + PrimaryGeneratedColumn, + BaseEntity, +} from 'typeorm'; + +export default abstract class Model extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @CreateDateColumn() + created_at: Date; + + @UpdateDateColumn() + updated_at: Date; +} diff --git a/src/entities/post.entity.ts b/src/entities/post.entity.ts new file mode 100644 index 0000000..6d2bc2d --- /dev/null +++ b/src/entities/post.entity.ts @@ -0,0 +1,23 @@ +import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; +import Model from './model.entity'; +import { User } from './user.entity'; + +@Entity('posts') +export class Post extends Model { + @Column({ + unique: true, + }) + title: string; + + @Column() + content: string; + + @Column({ + default: 'default-post.png', + }) + image: string; + + @ManyToOne(() => User, (user) => user.posts) + @JoinColumn() + user!: User; +} diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts new file mode 100644 index 0000000..13583a9 --- /dev/null +++ b/src/entities/user.entity.ts @@ -0,0 +1,84 @@ +import crypto from 'crypto'; +import { Entity, Column, Index, BeforeInsert, OneToMany } from 'typeorm'; +import bcrypt from 'bcryptjs'; +import Model from './model.entity'; +import { Post } from './post.entity'; + +export enum RoleEnumType { + USER = 'user', + ADMIN = 'admin', +} + +@Entity('users') +export class User extends Model { + @Column() + name: string; + + @Index('email_index') + @Column({ + unique: true, + }) + email: string; + + @Column() + password: string; + + @Column({ + type: 'enum', + enum: RoleEnumType, + default: RoleEnumType.USER, + }) + role: RoleEnumType.USER; + + @Column({ + default: 'default.png', + }) + photo: string; + + @Column({ + default: false, + }) + verified: boolean; + + @Index('verificationCode_index') + @Column({ + type: 'text', + nullable: true, + }) + verificationCode!: string | null; + + @OneToMany(() => Post, (post) => post.user) + posts: Post[]; + + @BeforeInsert() + async hashPassword() { + this.password = await bcrypt.hash(this.password, 12); + } + + static async comparePasswords( + candidatePassword: string, + hashedPassword: string + ) { + return await bcrypt.compare(candidatePassword, hashedPassword); + } + + static createVerificationCode() { + const verificationCode = crypto.randomBytes(32).toString('hex'); + + const hashedVerificationCode = crypto + .createHash('sha256') + .update(verificationCode) + .digest('hex'); + + return { verificationCode, hashedVerificationCode }; + } + + toJSON() { + return { + ...this, + password: undefined, + verified: undefined, + verificationCode: undefined, + }; + } +} diff --git a/src/middleware/deserializeUser.ts b/src/middleware/deserializeUser.ts new file mode 100644 index 0000000..cf27ee4 --- /dev/null +++ b/src/middleware/deserializeUser.ts @@ -0,0 +1,59 @@ +import { NextFunction, Request, Response } from 'express'; +import { findUserById } from '../services/user.service'; +import AppError from '../utils/appError'; +import redisClient from '../utils/connectRedis'; +import { verifyJwt } from '../utils/jwt'; + +export const deserializeUser = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + let access_token; + + if ( + req.headers.authorization && + req.headers.authorization.startsWith('Bearer') + ) { + access_token = req.headers.authorization.split(' ')[1]; + } else if (req.cookies.access_token) { + access_token = req.cookies.access_token; + } + + if (!access_token) { + return next(new AppError(401, 'You are not logged in')); + } + + // Validate the access token + const decoded = verifyJwt<{ sub: string }>( + access_token, + 'accessTokenPublicKey' + ); + + if (!decoded) { + return next(new AppError(401, `Invalid token or user doesn't exist`)); + } + + // Check if the user has a valid session + const session = await redisClient.get(decoded.sub); + + if (!session) { + return next(new AppError(401, `Invalid token or session has expired`)); + } + + // Check if the user still exist + const user = await findUserById(JSON.parse(session).id); + + if (!user) { + return next(new AppError(401, `Invalid token or session has expired`)); + } + + // Add user to res.locals + res.locals.user = user; + + next(); + } catch (err: any) { + next(err); + } +}; diff --git a/src/middleware/requireUser.ts b/src/middleware/requireUser.ts new file mode 100644 index 0000000..5515d62 --- /dev/null +++ b/src/middleware/requireUser.ts @@ -0,0 +1,22 @@ +import { NextFunction, Request, Response } from 'express'; +import AppError from '../utils/appError'; + +export const requireUser = ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const user = res.locals.user; + + if (!user) { + return next( + new AppError(400, `Session has expired or user doesn't exist`) + ); + } + + next(); + } catch (err: any) { + next(err); + } +}; diff --git a/src/middleware/validate.ts b/src/middleware/validate.ts new file mode 100644 index 0000000..8e1bc0a --- /dev/null +++ b/src/middleware/validate.ts @@ -0,0 +1,24 @@ +import { Request, Response, NextFunction } from 'express'; +import { AnyZodObject, ZodError } from 'zod'; + +export const validate = + (schema: AnyZodObject) => + (req: Request, res: Response, next: NextFunction) => { + try { + schema.parse({ + params: req.params, + query: req.query, + body: req.body, + }); + + next(); + } catch (error) { + if (error instanceof ZodError) { + return res.status(400).json({ + status: 'fail', + errors: error.errors, + }); + } + next(error); + } + }; diff --git a/src/migrations/1651860744841-removed-enum.ts b/src/migrations/1651860744841-removed-enum.ts deleted file mode 100644 index ca43c34..0000000 --- a/src/migrations/1651860744841-removed-enum.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class removedEnum1651860744841 implements MigrationInterface { - name = 'removedEnum1651860744841' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "email" character varying NOT NULL, "password" character varying NOT NULL, "role" character varying NOT NULL DEFAULT 'user', "photo" character varying NOT NULL DEFAULT 'default.png', "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email"), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "email_index" ON "users" ("email") `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."email_index"`); - await queryRunner.query(`DROP TABLE "users"`); - } - -} diff --git a/src/migrations/1651860818633-added-extendable-model.ts b/src/migrations/1651860818633-added-extendable-model.ts deleted file mode 100644 index ceb68d7..0000000 --- a/src/migrations/1651860818633-added-extendable-model.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class addedExtendableModel1651860818633 implements MigrationInterface { - name = 'addedExtendableModel1651860818633' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "role"`); - await queryRunner.query(`CREATE TYPE "public"."users_role_enum" AS ENUM('user', 'admin')`); - await queryRunner.query(`ALTER TABLE "users" ADD "role" "public"."users_role_enum" NOT NULL DEFAULT 'user'`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "role"`); - await queryRunner.query(`DROP TYPE "public"."users_role_enum"`); - await queryRunner.query(`ALTER TABLE "users" ADD "role" character varying NOT NULL DEFAULT 'user'`); - } - -} diff --git a/src/migrations/1652710160806-added-user-entity.ts b/src/migrations/1652710160806-added-user-entity.ts new file mode 100644 index 0000000..b45a893 --- /dev/null +++ b/src/migrations/1652710160806-added-user-entity.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class addedUserEntity1652710160806 implements MigrationInterface { + name = 'addedUserEntity1652710160806' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "public"."users_role_enum" AS ENUM('user', 'admin')`); + await queryRunner.query(`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "name" character varying NOT NULL, "email" character varying NOT NULL, "password" character varying NOT NULL, "role" "public"."users_role_enum" NOT NULL DEFAULT 'user', "photo" character varying NOT NULL DEFAULT 'default.png', "verified" boolean NOT NULL DEFAULT false, "verificationCode" text, CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email"), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "email_index" ON "users" ("email") `); + await queryRunner.query(`CREATE INDEX "verificationCode_index" ON "users" ("verificationCode") `); + await queryRunner.query(`CREATE TABLE "posts" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "title" character varying NOT NULL, "content" character varying NOT NULL, "image" character varying NOT NULL DEFAULT 'default-post.png', "userId" uuid, CONSTRAINT "UQ_2d82eb2bb2ddd7a6bfac8804d8a" UNIQUE ("title"), CONSTRAINT "PK_2829ac61eff60fcec60d7274b9e" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "posts" ADD CONSTRAINT "FK_ae05faaa55c866130abef6e1fee" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "posts" DROP CONSTRAINT "FK_ae05faaa55c866130abef6e1fee"`); + await queryRunner.query(`DROP TABLE "posts"`); + await queryRunner.query(`DROP INDEX "public"."verificationCode_index"`); + await queryRunner.query(`DROP INDEX "public"."email_index"`); + await queryRunner.query(`DROP TABLE "users"`); + await queryRunner.query(`DROP TYPE "public"."users_role_enum"`); + } + +} diff --git a/src/routes/auth.routes.ts b/src/routes/auth.routes.ts new file mode 100644 index 0000000..637e9d3 --- /dev/null +++ b/src/routes/auth.routes.ts @@ -0,0 +1,39 @@ +import express from 'express'; +import { + loginUserHandler, + logoutHandler, + refreshAccessTokenHandler, + registerUserHandler, + verifyEmailHandler, +} from '../controllers/auth.controller'; +import { deserializeUser } from '../middleware/deserializeUser'; +import { requireUser } from '../middleware/requireUser'; +import { validate } from '../middleware/validate'; +import { + createUserSchema, + loginUserSchema, + verifyEmailSchema, +} from '../schemas/user.schema'; + +const router = express.Router(); + +// Register user +router.post('/register', validate(createUserSchema), registerUserHandler); + +// Login user +router.post('/login', validate(loginUserSchema), loginUserHandler); + +// Logout user +router.get('/logout', deserializeUser, requireUser, logoutHandler); + +// Refresh access token +router.get('/refresh', refreshAccessTokenHandler); + +// Verify Email Address +router.get( + '/verifyemail/:verificationCode', + validate(verifyEmailSchema), + verifyEmailHandler +); + +export default router; diff --git a/src/routes/post.routes.ts b/src/routes/post.routes.ts new file mode 100644 index 0000000..7890630 --- /dev/null +++ b/src/routes/post.routes.ts @@ -0,0 +1,33 @@ +import express from 'express'; +import { + createPostHandler, + deletePostHandler, + getPostHandler, + getPostsHandler, + updatePostHandler, +} from '../controllers/post.controller'; +import { deserializeUser } from '../middleware/deserializeUser'; +import { requireUser } from '../middleware/requireUser'; +import { validate } from '../middleware/validate'; +import { + createPostSchema, + deletePostSchema, + getPostSchema, + updatePostSchema, +} from '../schemas/post.schema'; + +const router = express.Router(); + +router.use(deserializeUser, requireUser); +router + .route('/') + .post(validate(createPostSchema), createPostHandler) + .get(getPostsHandler); + +router + .route('/:postId') + .get(validate(getPostSchema), getPostHandler) + .patch(validate(updatePostSchema), updatePostHandler) + .delete(validate(deletePostSchema), deletePostHandler); + +export default router; diff --git a/src/routes/user.routes.ts b/src/routes/user.routes.ts new file mode 100644 index 0000000..cafa410 --- /dev/null +++ b/src/routes/user.routes.ts @@ -0,0 +1,13 @@ +import express from 'express'; +import { getMeHandler } from '../controllers/user.controller'; +import { deserializeUser } from '../middleware/deserializeUser'; +import { requireUser } from '../middleware/requireUser'; + +const router = express.Router(); + +router.use(deserializeUser, requireUser); + +// Get currently logged in user +router.get('/me', getMeHandler); + +export default router; diff --git a/src/schemas/post.schema.ts b/src/schemas/post.schema.ts new file mode 100644 index 0000000..41f9542 --- /dev/null +++ b/src/schemas/post.schema.ts @@ -0,0 +1,43 @@ +import { object, string, TypeOf } from 'zod'; + +export const createPostSchema = object({ + body: object({ + title: string({ + required_error: 'Title is required', + }), + content: string({ + required_error: 'Content is required', + }), + image: string({ + required_error: 'Image is required', + }), + }), +}); + +const params = { + params: object({ + postId: string(), + }), +}; + +export const getPostSchema = object({ + ...params, +}); + +export const updatePostSchema = object({ + ...params, + body: object({ + title: string(), + content: string(), + image: string(), + }).partial(), +}); + +export const deletePostSchema = object({ + ...params, +}); + +export type CreatePostInput = TypeOf['body']; +export type GetPostInput = TypeOf['params']; +export type UpdatePostInput = TypeOf; +export type DeletePostInput = TypeOf['params']; diff --git a/src/schemas/user.schema.ts b/src/schemas/user.schema.ts new file mode 100644 index 0000000..478f0da --- /dev/null +++ b/src/schemas/user.schema.ts @@ -0,0 +1,50 @@ +import { object, string, TypeOf, z } from 'zod'; +import { RoleEnumType } from '../entities/user.entity'; + +export const createUserSchema = object({ + body: object({ + name: string({ + required_error: 'Name is required', + }), + email: string({ + required_error: 'Email address is required', + }).email('Invalid email address'), + password: string({ + required_error: 'Password is required', + }) + .min(8, 'Password must be more than 8 characters') + .max(32, 'Password must be less than 32 characters'), + passwordConfirm: string({ + required_error: 'Please confirm your password', + }), + role: z.optional(z.nativeEnum(RoleEnumType)), + }).refine((data) => data.password === data.passwordConfirm, { + path: ['passwordConfirm'], + message: 'Passwords do not match', + }), +}); + +export const loginUserSchema = object({ + body: object({ + email: string({ + required_error: 'Email address is required', + }).email('Invalid email address'), + password: string({ + required_error: 'Password is required', + }).min(8, 'Invalid email or password'), + }), +}); + +export const verifyEmailSchema = object({ + params: object({ + verificationCode: string(), + }), +}); + +export type CreateUserInput = Omit< + TypeOf['body'], + 'passwordConfirm' +>; + +export type LoginUserInput = TypeOf['body']; +export type VerifyEmailInput = TypeOf['params']; diff --git a/src/services/post.service.ts b/src/services/post.service.ts new file mode 100644 index 0000000..d0fc779 --- /dev/null +++ b/src/services/post.service.ts @@ -0,0 +1,31 @@ +import { Request } from 'express'; +import { Post } from '../entities/post.entity'; +import { User } from '../entities/user.entity'; +import { AppDataSource } from '../utils/data-source'; + +const postRepository = AppDataSource.getRepository(Post); + +export const createPost = async (input: Partial, user: User) => { + return await postRepository.save(postRepository.create({ ...input, user })); +}; + +export const getPost = async (postId: string) => { + return await postRepository.findOneBy({ id: postId }); +}; + +export const findPosts = async (req: Request) => { + const builder = postRepository.createQueryBuilder('post'); + + if (req.query.search) { + builder.where('post.title LIKE :search OR post.content LIKE :search', { + search: `%${req.query.search}%`, + }); + } + + if (req.query.sort) { + const sortQuery = req.query.sort === '-price' ? 'DESC' : 'ASC'; + builder.orderBy('post.title', sortQuery); + } + + return await builder.getMany(); +}; diff --git a/src/services/user.service.ts b/src/services/user.service.ts new file mode 100644 index 0000000..e26d525 --- /dev/null +++ b/src/services/user.service.ts @@ -0,0 +1,40 @@ +import config from 'config'; +import { User } from '../entities/user.entity'; +import redisClient from '../utils/connectRedis'; +import { AppDataSource } from '../utils/data-source'; +import { signJwt } from '../utils/jwt'; + +const userRepository = AppDataSource.getRepository(User); + +export const createUser = async (input: Partial) => { + return await userRepository.save(userRepository.create(input)); +}; + +export const findUserByEmail = async ({ email }: { email: string }) => { + return await userRepository.findOneBy({ email }); +}; + +export const findUserById = async (userId: string) => { + return await userRepository.findOneBy({ id: userId }); +}; + +export const findUser = async (query: Object) => { + return await userRepository.findOneBy(query); +}; +export const signTokens = async (user: User) => { + // 1. Create Session + redisClient.set(user.id, JSON.stringify(user), { + EX: config.get('redisCacheExpiresIn') * 60, + }); + + // 2. Create Access and Refresh tokens + const access_token = signJwt({ sub: user.id }, 'accessTokenPrivateKey', { + expiresIn: `${config.get('accessTokenExpiresIn')}m`, + }); + + const refresh_token = signJwt({ sub: user.id }, 'refreshTokenPrivateKey', { + expiresIn: `${config.get('refreshTokenExpiresIn')}m`, + }); + + return { access_token, refresh_token }; +}; diff --git a/src/utils/appError.ts b/src/utils/appError.ts new file mode 100644 index 0000000..fb49745 --- /dev/null +++ b/src/utils/appError.ts @@ -0,0 +1,11 @@ +export default class AppError extends Error { + status: string; + isOperational: boolean; + constructor(public statusCode: number = 500, public message: string) { + super(message); + this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; + this.isOperational = true; + + Error.captureStackTrace(this, this.constructor); + } +} diff --git a/src/utils/connectRedis.ts b/src/utils/connectRedis.ts index 57edff6..b909f68 100644 --- a/src/utils/connectRedis.ts +++ b/src/utils/connectRedis.ts @@ -9,7 +9,7 @@ const redisClient = createClient({ const connectRedis = async () => { try { await redisClient.connect(); - console.log('Redis client connected successfully'); + console.log('Redis client connect successfully'); redisClient.set('try', 'Hello Welcome to Express with TypeORM'); } catch (error) { console.log(error); diff --git a/src/utils/email.ts b/src/utils/email.ts new file mode 100644 index 0000000..96622c3 --- /dev/null +++ b/src/utils/email.ts @@ -0,0 +1,69 @@ +import nodemailer from 'nodemailer'; +import { User } from '../entities/user.entity'; +import config from 'config'; +import pug from 'pug'; +import { convert } from 'html-to-text'; + +const smtp = config.get<{ + host: string; + port: number; + user: string; + pass: string; +}>('smtp'); + +export default class Email { + firstName: string; + to: string; + from: string; + constructor(public user: User, public url: string) { + this.firstName = user.name.split(' ')[0]; + this.to = user.email; + this.from = `Codevo ${config.get('emailFrom')}`; + } + + private newTransport() { + // if (process.env.NODE_ENV === 'production') { + // console.log('Hello') + // } + + return nodemailer.createTransport({ + ...smtp, + auth: { + user: smtp.user, + pass: smtp.pass, + }, + }); + } + + private async send(template: string, subject: string) { + // Generate HTML template based on the template string + const html = pug.renderFile(`${__dirname}/../views/${template}.pug`, { + firstName: this.firstName, + subject, + url: this.url, + }); + // Create mailOptions + const mailOptions = { + from: this.from, + to: this.to, + subject, + text: convert(html), + html, + }; + + // Send email + const info = await this.newTransport().sendMail(mailOptions); + console.log(nodemailer.getTestMessageUrl(info)); + } + + async sendVerificationCode() { + await this.send('verificationCode', 'Your account verification code'); + } + + async sendPasswordResetToken() { + await this.send( + 'resetPassword', + 'Your password reset token (valid for only 10 minutes)' + ); + } +} diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts new file mode 100644 index 0000000..e04e2f7 --- /dev/null +++ b/src/utils/jwt.ts @@ -0,0 +1,34 @@ +import jwt, { SignOptions } from 'jsonwebtoken'; +import config from 'config'; + +export const signJwt = ( + payload: Object, + keyName: 'accessTokenPrivateKey' | 'refreshTokenPrivateKey', + options: SignOptions +) => { + const privateKey = Buffer.from( + config.get(keyName), + 'base64' + ).toString('ascii'); + return jwt.sign(payload, privateKey, { + ...(options && options), + algorithm: 'RS256', + }); +}; + +export const verifyJwt = ( + token: string, + keyName: 'accessTokenPublicKey' | 'refreshTokenPublicKey' +): T | null => { + try { + const publicKey = Buffer.from( + config.get(keyName), + 'base64' + ).toString('ascii'); + const decoded = jwt.verify(token, publicKey) as T; + + return decoded; + } catch (error) { + return null; + } +}; diff --git a/src/views/_styles.pug b/src/views/_styles.pug new file mode 100644 index 0000000..007d190 --- /dev/null +++ b/src/views/_styles.pug @@ -0,0 +1,293 @@ +style. + /* ------------------------------------- + GLOBAL RESETS + ------------------------------------- */ + /*All the styling goes here*/ + img { + border: none; + -ms-interpolation-mode: bicubic; + max-width: 100%; + } + body { + background-color: #f6f6f6; + font-family: sans-serif; + -webkit-font-smoothing: antialiased; + font-size: 14px; + line-height: 1.4; + margin: 0; + padding: 0; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + } + table { + border-collapse: separate; + mso-table-lspace: 0pt; + mso-table-rspace: 0pt; + width: 100%; } + table td { + font-family: sans-serif; + font-size: 14px; + vertical-align: top; + } + /* ------------------------------------- + BODY & CONTAINER + ------------------------------------- */ + .body { + background-color: #f6f6f6; + width: 100%; + } + /* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */ + .container { + display: block; + margin: 0 auto !important; + /* makes it centered */ + max-width: 580px; + padding: 10px; + width: 580px; + } + /* This should also be a block element, so that it will fill 100% of the .container */ + .content { + box-sizing: border-box; + display: block; + margin: 0 auto; + max-width: 580px; + padding: 10px; + } + /* ------------------------------------- + HEADER, FOOTER, MAIN + ------------------------------------- */ + .main { + background: #ffffff; + border-radius: 3px; + width: 100%; + } + .wrapper { + box-sizing: border-box; + padding: 20px; + } + .content-block { + padding-bottom: 10px; + padding-top: 10px; + } + .footer { + clear: both; + margin-top: 10px; + text-align: center; + width: 100%; + } + .footer td, + .footer p, + .footer span, + .footer a { + color: #999999; + font-size: 12px; + text-align: center; + } + /* ------------------------------------- + TYPOGRAPHY + ------------------------------------- */ + h1, + h2, + h3, + h4 { + color: #000000; + font-family: sans-serif; + font-weight: 400; + line-height: 1.4; + margin: 0; + margin-bottom: 30px; + } + h1 { + font-size: 35px; + font-weight: 300; + text-align: center; + text-transform: capitalize; + } + p, + ul, + ol { + font-family: sans-serif; + font-size: 14px; + font-weight: normal; + margin: 0; + margin-bottom: 15px; + } + p li, + ul li, + ol li { + list-style-position: inside; + margin-left: 5px; + } + a { + color: #3498db; + text-decoration: underline; + } + /* ------------------------------------- + BUTTONS + ------------------------------------- */ + .btn { + box-sizing: border-box; + width: 100%; } + .btn > tbody > tr > td { + padding-bottom: 15px; } + .btn table { + width: auto; + } + .btn table td { + background-color: #ffffff; + border-radius: 5px; + text-align: center; + } + .btn a { + background-color: #ffffff; + border: solid 1px #3498db; + border-radius: 5px; + box-sizing: border-box; + color: #3498db; + cursor: pointer; + display: inline-block; + font-size: 14px; + font-weight: bold; + margin: 0; + padding: 12px 25px; + text-decoration: none; + text-transform: capitalize; + } + .btn-primary table td { + background-color: #3498db; + } + .btn-primary a { + background-color: #3498db; + border-color: #3498db; + color: #ffffff; + } + /* ------------------------------------- + OTHER STYLES THAT MIGHT BE USEFUL + ------------------------------------- */ + .last { + margin-bottom: 0; + } + .first { + margin-top: 0; + } + .align-center { + text-align: center; + } + .align-right { + text-align: right; + } + .align-left { + text-align: left; + } + .clear { + clear: both; + } + .mt0 { + margin-top: 0; + } + .mb0 { + margin-bottom: 0; + } + .preheader { + color: transparent; + display: none; + height: 0; + max-height: 0; + max-width: 0; + opacity: 0; + overflow: hidden; + mso-hide: all; + visibility: hidden; + width: 0; + } + .powered-by a { + text-decoration: none; + } + hr { + border: 0; + border-bottom: 1px solid #f6f6f6; + margin: 20px 0; + } + /* ------------------------------------- + RESPONSIVE AND MOBILE FRIENDLY STYLES + ------------------------------------- */ + @media only screen and (max-width: 620px) { + table.body h1 { + font-size: 28px !important; + margin-bottom: 10px !important; + } + table.body p, + table.body ul, + table.body ol, + table.body td, + table.body span, + table.body a { + font-size: 16px !important; + } + table.body .wrapper, + table.body .article { + padding: 10px !important; + } + table.body .content { + padding: 0 !important; + } + table.body .container { + padding: 0 !important; + width: 100% !important; + } + table.body .main { + border-left-width: 0 !important; + border-radius: 0 !important; + border-right-width: 0 !important; + } + table.body .btn table { + width: 100% !important; + } + table.body .btn a { + width: 100% !important; + } + table.body .img-responsive { + height: auto !important; + max-width: 100% !important; + width: auto !important; + } + } + /* ------------------------------------- + PRESERVE THESE STYLES IN THE HEAD + ------------------------------------- */ + @media all { + .ExternalClass { + width: 100%; + } + .ExternalClass, + .ExternalClass p, + .ExternalClass span, + .ExternalClass font, + .ExternalClass td, + .ExternalClass div { + line-height: 100%; + } + .apple-link a { + color: inherit !important; + font-family: inherit !important; + font-size: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; + text-decoration: none !important; + } + #MessageViewBody a { + color: inherit; + text-decoration: none; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + line-height: inherit; + } + .btn-primary table td:hover { + background-color: #34495e !important; + } + .btn-primary a:hover { + background-color: #34495e !important; + border-color: #34495e !important; + } + } \ No newline at end of file diff --git a/src/views/base.pug b/src/views/base.pug new file mode 100644 index 0000000..c0c24f8 --- /dev/null +++ b/src/views/base.pug @@ -0,0 +1,45 @@ +doctype html +html + head + meta(name='viewport' content='width=device-width, initial-scale=1.0') + meta(http-equiv='Content-Type' content='text/html; charset=UTF-8') + title #{subject} + include _styles + body + table.body(role='presentation' border='0' cellpadding='0' cellspacing='0') + tbody + tr + td   + td.container + .content + // START CENTERED WHITE CONTAINER + table.main(role='presentation') + // START MAIN CONTENT AREA + tbody + tr + td.wrapper + table(role='presentation' border='0' cellpadding='0' cellspacing='0') + tbody + tr + td + block content + // END MAIN CONTENT AREA + // END CENTERED WHITE CONTAINER + // START FOOTER + .footer + table(role='presentation' border='0' cellpadding='0' cellspacing='0') + tbody + tr + td.content-block + span.apple-link Company Inc, 3 Abbey Road, San Francisco CA 94102 + br + | Don't like these emails? + a(href="iframe.php?url=http%3A%2F%2Fi.imgur.com%2FCScmqnj.gif") Unsubscribe + | . + tr + td.content-block.powered-by + | Powered by + a(href="iframe.php?url=http%3A%2F%2Fhtmlemail.io") HTMLemail + | . + // END FOOTER + td   diff --git a/src/views/resetPassword.pug b/src/views/resetPassword.pug new file mode 100644 index 0000000..9f85385 --- /dev/null +++ b/src/views/resetPassword.pug @@ -0,0 +1,16 @@ +extends base + +block content + p Hi #{firstName}, + p Forgot password? Send a PATCH request to with your password and passwordConfirm to #{url} + table.btn.btn-primary(role='presentation' border='0' cellpadding='0' cellspacing='0') + tbody + tr + td(align='left') + table(role='presentation' border='0' cellpadding='0' cellspacing='0') + tbody + tr + td + a(href=`${url}` target='_blank') Reset password + p If you didn't forget your password, please ignore this email + p Good luck! Codevo CEO. \ No newline at end of file diff --git a/src/views/verificationCode.pug b/src/views/verificationCode.pug new file mode 100644 index 0000000..bdd3053 --- /dev/null +++ b/src/views/verificationCode.pug @@ -0,0 +1,15 @@ +extends base + +block content + p Hi #{firstName}, + p Please verify your account to be able to login + table.btn.btn-primary(role='presentation' border='0' cellpadding='0' cellspacing='0') + tbody + tr + td(align='left') + table(role='presentation' border='0' cellpadding='0' cellspacing='0') + tbody + tr + td + a(href=`${url}` target='_blank') Verify your account + p Good luck! Codevo CEO. \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index a40463b..98bb9ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,24 @@ # yarn lockfile v1 +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + +"@babel/parser@^7.6.0", "@babel/parser@^7.9.6": + version "7.17.10" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.10.tgz#873b16db82a8909e0fbd7f115772f4b739f6ce78" + integrity sha512-n2Q6i+fnJqzOaq2VkdXxy2TCPCWQZHiCo0XqmrCvDWcZQKRyZzYi4Z0yxlBuN0w+r2ZHmre+Q087DSrw3pbJDQ== + +"@babel/types@^7.6.1", "@babel/types@^7.9.6": + version "7.17.10" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.10.tgz#d35d7b4467e439fcf06d195f8100e0fea7fc82c4" + integrity sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + "@cspotcode/source-map-consumer@0.8.0": version "0.8.0" resolved "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz" @@ -14,6 +32,11 @@ dependencies: "@cspotcode/source-map-consumer" "0.8.0" +"@faker-js/faker@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-6.3.1.tgz#1ae963dd40405450a2945408cba553e1afa3e0fb" + integrity sha512-8YXBE2ZcU/pImVOHX7MWrSR/X5up7t6rPWZlk34RwZEcdr3ua6X+32pSd6XuOQRN+vbuvYNfA6iey8NbrjuMFQ== + "@redis/bloom@1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.0.2.tgz#42b82ec399a92db05e29fffcdfd9235a5fc15cdf" @@ -48,6 +71,14 @@ resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.3.tgz#4cfca8e564228c0bddcdf4418cba60c20b224ac4" integrity sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA== +"@selderee/plugin-htmlparser2@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.6.0.tgz#27e994afd1c2cb647ceb5406a185a5574188069d" + integrity sha512-J3jpy002TyBjd4N/p6s+s90eX42H2eRhK3SbsZuvTDv977/E8p2U3zikdiehyJja66do7FlxLomZLPlvl2/xaA== + dependencies: + domhandler "^4.2.0" + selderee "^0.6.0" + "@sqltools/formatter@^1.2.2": version "1.2.3" resolved "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.3.tgz" @@ -73,6 +104,11 @@ resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz" integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== +"@types/bcryptjs@^2.4.2": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.2.tgz#e3530eac9dd136bfdfb0e43df2c4c5ce1f77dfae" + integrity sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ== + "@types/body-parser@*": version "1.19.2" resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz" @@ -93,6 +129,18 @@ dependencies: "@types/node" "*" +"@types/cookie-parser@^1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.3.tgz#3a01df117c5705cf89a84c876b50c5a1fd427a21" + integrity sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w== + dependencies: + "@types/express" "*" + +"@types/cors@^2.8.12": + version "2.8.12" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" + integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== + "@types/express-serve-static-core@^4.17.18": version "4.17.28" resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz" @@ -102,7 +150,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@^4.17.13": +"@types/express@*", "@types/express@^4.17.13": version "4.17.13" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== @@ -112,16 +160,47 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/html-to-text@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@types/html-to-text/-/html-to-text-8.1.0.tgz#dad0bf5d199f7e3f67eae50a36c13eadb1b56d1b" + integrity sha512-54YF2fGmN4g62/w+T85uQ8n0FyBhMY5cjKZ1imsbIh4Pgbeno1mAaQktC/pv/+C2ToUYkTZis9ADgn9GRRz9nQ== + +"@types/jsonwebtoken@^8.5.8": + version "8.5.8" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz#01b39711eb844777b7af1d1f2b4cf22fda1c0c44" + integrity sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A== + dependencies: + "@types/node" "*" + "@types/mime@^1": version "1.3.2" resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== +"@types/morgan@^1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@types/morgan/-/morgan-1.9.3.tgz#ae04180dff02c437312bc0cfb1e2960086b2f540" + integrity sha512-BiLcfVqGBZCyNCnCH3F4o2GmDLrpy0HeBVnNlyZG4fo88ZiE9SoiBe3C+2ezuwbjlEyT+PDZ17//TAlRxAn75Q== + dependencies: + "@types/node" "*" + "@types/node@*", "@types/node@^16.11.10": version "16.11.33" resolved "https://registry.npmjs.org/@types/node/-/node-16.11.33.tgz" integrity sha512-0PJ0vg+JyU0MIan58IOIFRtSvsb7Ri+7Wltx2qAg94eMOrpg4+uuP3aUHCpxXc1i0jCXiC+zIamSZh3l9AbcQA== +"@types/nodemailer@^6.4.4": + version "6.4.4" + resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b" + integrity sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw== + dependencies: + "@types/node" "*" + +"@types/pug@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.6.tgz#f830323c88172e66826d0bde413498b61054b5a6" + integrity sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg== + "@types/qs@*": version "6.9.7" resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz" @@ -163,6 +242,11 @@ acorn-walk@^8.1.1: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== +acorn@^7.1.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + acorn@^8.4.1: version "8.7.1" resolved "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz" @@ -213,6 +297,23 @@ array-flatten@1.1.1: resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +assert-never@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.2.1.tgz#11f0e363bf146205fb08193b5c7b90f4d1cf44fe" + integrity sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw== + +babel-walk@3.0.0-canary-5: + version "3.0.0-canary-5" + resolved "https://registry.yarnpkg.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz#f66ecd7298357aee44955f235a6ef54219104b11" + integrity sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw== + dependencies: + "@babel/types" "^7.9.6" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" @@ -223,14 +324,26 @@ base64-js@^1.3.1: resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + +bcryptjs@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" + integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms= + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -body-parser@1.20.0: +body-parser@1.20.0, body-parser@^1.19.1: version "1.20.0" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== dependencies: bytes "3.1.2" @@ -261,6 +374,11 @@ braces@~3.0.2: dependencies: fill-range "^7.0.1" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" @@ -284,7 +402,7 @@ bytes@3.1.2: resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -call-bind@^1.0.0: +call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== @@ -300,6 +418,13 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +character-parser@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0" + integrity sha1-x84o821LzZdE5f/CxfzeHHMmH8A= + dependencies: + is-regex "^1.0.3" + chokidar@^3.5.1: version "3.5.3" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" @@ -353,6 +478,11 @@ color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +commander@^2.19.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" @@ -365,6 +495,14 @@ config@^3.3.7: dependencies: json5 "^2.1.1" +constantinople@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-4.0.1.tgz#0def113fa0e4dc8de83331a5cf79c8b325213151" + integrity sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw== + dependencies: + "@babel/parser" "^7.6.0" + "@babel/types" "^7.6.1" + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" @@ -377,16 +515,37 @@ content-type@~1.0.4: resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +cookie-parser@^1.4.6: + version "1.4.6" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.6.tgz#3ac3a7d35a7a03bbc7e365073a26074824214594" + integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA== + dependencies: + cookie "0.4.1" + cookie-signature "1.0.6" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= +cookie@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + cookie@0.5.0: version "0.5.0" resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" @@ -411,7 +570,12 @@ debug@^4.3.3: dependencies: ms "2.1.2" -depd@2.0.0: +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + +depd@2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -426,6 +590,46 @@ diff@^4.0.1: resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +discontinuous-range@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" + integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo= + +doctypes@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9" + integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk= + +dom-serializer@^1.0.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^4.0.0, domhandler@^4.2.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +domutils@^2.5.2: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + dotenv@^16.0.0: version "16.0.0" resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz" @@ -438,6 +642,13 @@ dynamic-dedupe@^0.3.0: dependencies: xtend "^4.0.0" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" @@ -453,6 +664,11 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + envalid@^7.3.1: version "7.3.1" resolved "https://registry.yarnpkg.com/envalid/-/envalid-7.3.1.tgz#5bf6bbb4effab2d64a1991d8078b4ae38924f0d2" @@ -600,11 +816,18 @@ has-flag@^4.0.0: resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.1: +has-symbols@^1.0.1, has-symbols@^1.0.2: version "1.0.3" resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + has@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" @@ -612,11 +835,38 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + highlight.js@^10.7.1: version "10.7.3" resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz" integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== +html-to-text@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-8.2.0.tgz#8b35e280ba7fc27710b7aa76d4500aab30731924" + integrity sha512-CLXExYn1b++Lgri+ZyVvbUEFwzkLZppjjZOwB7X1qv2jIi8MrMEvxWX5KQ7zATAzTvcqgmtO00M2kCRMtEdOKQ== + dependencies: + "@selderee/plugin-htmlparser2" "^0.6.0" + deepmerge "^4.2.2" + he "^1.2.0" + htmlparser2 "^6.1.0" + minimist "^1.2.6" + selderee "^0.6.0" + +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + http-errors@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" @@ -637,7 +887,7 @@ iconv-lite@0.4.24: ieee754@^1.2.1: version "1.2.1" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== inflight@^1.0.4: @@ -672,6 +922,14 @@ is-core-module@^2.8.1: dependencies: has "^1.0.3" +is-expression@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-4.0.0.tgz#c33155962abf21d0afd2552514d67d2ec16fd2ab" + integrity sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A== + dependencies: + acorn "^7.1.1" + object-assign "^4.1.1" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" @@ -694,6 +952,24 @@ is-number@^7.0.0: resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-promise@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== + +is-regex@^1.0.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +js-stringify@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" + integrity sha1-Fzb939lyTyijaCrcYjCufk6Weds= + js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" @@ -706,6 +982,82 @@ json5@^2.1.1: resolved "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz" integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== +jsonwebtoken@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + +jstransformer@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3" + integrity sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM= + dependencies: + is-promise "^2.0.0" + promise "^7.0.1" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + make-error@^1.1.1: version "1.3.6" resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" @@ -750,9 +1102,9 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.5: +minimist@^1.2.5, minimist@^1.2.6: version "1.2.6" - resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== mkdirp@^1.0.4: @@ -760,6 +1112,22 @@ mkdirp@^1.0.4: resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +moo@^0.5.0, moo@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4" + integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w== + +morgan@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.0.2" + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -770,7 +1138,7 @@ ms@2.1.2: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3: +ms@2.1.3, ms@^2.1.1: version "2.1.3" resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -784,19 +1152,34 @@ mz@^2.4.0: object-assign "^4.0.1" thenify-all "^1.0.0" +nearley@^2.20.1: + version "2.20.1" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474" + integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ== + dependencies: + commander "^2.19.0" + moo "^0.5.0" + railroad-diagrams "^1.0.0" + randexp "0.4.6" + negotiator@0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +nodemailer@^6.7.5: + version "6.7.5" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.5.tgz#b30b1566f5fa2249f7bd49ced4c58bec6b25915e" + integrity sha512-6VtMpwhsrixq1HDYSBBHvW0GwiWawE75dS3oal48VqRhUvKJNnKnJo2RI/bCVQubj1vgrgscMNW4DHaD6xtMCg== + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -object-assign@^4.0.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= object-inspect@^1.9.0: @@ -811,9 +1194,21 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + once@^1.3.0: version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" @@ -840,6 +1235,14 @@ parse5@^6.0.1: resolved "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== +parseley@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/parseley/-/parseley-0.7.0.tgz#9949e3a0ed05c5072adb04f013c2810cf49171a8" + integrity sha512-xyOytsdDu077M3/46Am+2cGXEKM9U9QclBDv7fimY7e+BBlxh2JcBp2mgNsmkyA9uvgyTjVzDi7cP1v4hcFxbw== + dependencies: + moo "^0.5.1" + nearley "^2.20.1" + parseurl@~1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" @@ -938,6 +1341,13 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" +promise@^7.0.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== + dependencies: + asap "~2.0.3" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" @@ -946,6 +1356,109 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +pug-attrs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-3.0.0.tgz#b10451e0348165e31fad1cc23ebddd9dc7347c41" + integrity sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA== + dependencies: + constantinople "^4.0.1" + js-stringify "^1.0.2" + pug-runtime "^3.0.0" + +pug-code-gen@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-3.0.2.tgz#ad190f4943133bf186b60b80de483100e132e2ce" + integrity sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg== + dependencies: + constantinople "^4.0.1" + doctypes "^1.1.0" + js-stringify "^1.0.2" + pug-attrs "^3.0.0" + pug-error "^2.0.0" + pug-runtime "^3.0.0" + void-elements "^3.1.0" + with "^7.0.0" + +pug-error@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-2.0.0.tgz#5c62173cb09c34de2a2ce04f17b8adfec74d8ca5" + integrity sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ== + +pug-filters@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-4.0.0.tgz#d3e49af5ba8472e9b7a66d980e707ce9d2cc9b5e" + integrity sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A== + dependencies: + constantinople "^4.0.1" + jstransformer "1.0.0" + pug-error "^2.0.0" + pug-walk "^2.0.0" + resolve "^1.15.1" + +pug-lexer@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-5.0.1.tgz#ae44628c5bef9b190b665683b288ca9024b8b0d5" + integrity sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w== + dependencies: + character-parser "^2.2.0" + is-expression "^4.0.0" + pug-error "^2.0.0" + +pug-linker@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-4.0.0.tgz#12cbc0594fc5a3e06b9fc59e6f93c146962a7708" + integrity sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw== + dependencies: + pug-error "^2.0.0" + pug-walk "^2.0.0" + +pug-load@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-3.0.0.tgz#9fd9cda52202b08adb11d25681fb9f34bd41b662" + integrity sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ== + dependencies: + object-assign "^4.1.1" + pug-walk "^2.0.0" + +pug-parser@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-6.0.0.tgz#a8fdc035863a95b2c1dc5ebf4ecf80b4e76a1260" + integrity sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw== + dependencies: + pug-error "^2.0.0" + token-stream "1.0.0" + +pug-runtime@^3.0.0, pug-runtime@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-3.0.1.tgz#f636976204723f35a8c5f6fad6acda2a191b83d7" + integrity sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg== + +pug-strip-comments@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz#f94b07fd6b495523330f490a7f554b4ff876303e" + integrity sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ== + dependencies: + pug-error "^2.0.0" + +pug-walk@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-2.0.0.tgz#417aabc29232bb4499b5b5069a2b2d2a24d5f5fe" + integrity sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ== + +pug@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/pug/-/pug-3.0.2.tgz#f35c7107343454e43bc27ae0ff76c731b78ea535" + integrity sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw== + dependencies: + pug-code-gen "^3.0.2" + pug-filters "^4.0.0" + pug-lexer "^5.0.1" + pug-linker "^4.0.0" + pug-load "^3.0.0" + pug-parser "^6.0.0" + pug-runtime "^3.0.1" + pug-strip-comments "^2.0.0" + qs@6.10.3: version "6.10.3" resolved "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz" @@ -953,6 +1466,19 @@ qs@6.10.3: dependencies: side-channel "^1.0.4" +railroad-diagrams@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" + integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234= + +randexp@0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" + integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== + dependencies: + discontinuous-range "1.0.0" + ret "~0.1.10" + range-parser@~1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" @@ -997,15 +1523,20 @@ require-directory@^2.1.1: resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= -resolve@^1.0.0: +resolve@^1.0.0, resolve@^1.15.1: version "1.22.0" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== dependencies: is-core-module "^2.8.1" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + rimraf@^2.6.1: version "2.7.1" resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" @@ -1013,6 +1544,11 @@ rimraf@^2.6.1: dependencies: glob "^7.1.3" +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-buffer@5.2.1, safe-buffer@^5.0.1: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" @@ -1028,6 +1564,18 @@ sax@>=0.6.0: resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +selderee@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/selderee/-/selderee-0.6.0.tgz#f3bee66cfebcb6f33df98e4a1df77388b42a96f7" + integrity sha512-ibqWGV5aChDvfVdqNYuaJP/HnVBhlRGSRrlbttmlMpHcLuTqqbMH36QkSs9GEgj5M88JDYLI8eyP94JaQ8xRlg== + dependencies: + parseley "^0.7.0" + +semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + send@0.18.0: version "0.18.0" resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" @@ -1104,7 +1652,7 @@ statuses@2.0.1: string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" @@ -1125,7 +1673,7 @@ strip-bom@^3.0.0: strip-json-comments@^2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= supports-color@^7.1.0: @@ -1154,6 +1702,11 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" @@ -1166,6 +1719,11 @@ toidentifier@1.0.1: resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +token-stream@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-1.0.0.tgz#cc200eab2613f4166d27ff9afc7ca56d49df6eb4" + integrity sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ= + tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz" @@ -1294,11 +1852,26 @@ v8-compile-cache-lib@^3.0.0: resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -vary@~1.1.2: +vary@^1, vary@~1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= +void-elements@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk= + +with@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/with/-/with-7.0.2.tgz#ccee3ad542d25538a7a7a80aad212b9828495bac" + integrity sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w== + dependencies: + "@babel/parser" "^7.9.6" + "@babel/types" "^7.9.6" + assert-never "^1.2.1" + babel-walk "3.0.0-canary-5" + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" @@ -1381,3 +1954,8 @@ yn@3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +zod@^3.14.4: + version "3.15.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.15.1.tgz#9e404cd8002ccffb03baa94cff2e1638ed49d82f" + integrity sha512-WAdjcoOxa4S9oc/u7fTbC3CC7uVqptLLU0LKqS8RDBOrCXp2t5avM8BUfgNVZJymGWAx6SEUYxWPPoYuQ5rgwQ==