Skip to content

Commit

Permalink
feat: add token + user models
Browse files Browse the repository at this point in the history
  • Loading branch information
saisilinus committed Dec 15, 2021
1 parent dd226d5 commit 02ffcf8
Show file tree
Hide file tree
Showing 10 changed files with 25,815 additions and 5,790 deletions.
19,751 changes: 19,751 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"prettier"
],
"devDependencies": {
"@types/bcryptjs": "^2.4.2",
"@types/compression": "^1.7.2",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "2.8.8",
Expand All @@ -57,6 +58,7 @@
"@types/passport-jwt": "^3.0.6",
"@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3",
"@types/validator": "^13.7.0",
"@typescript-eslint/eslint-plugin": "^5.7.0",
"@typescript-eslint/parser": "^5.7.0",
"coveralls": "^3.1.1",
Expand Down
49 changes: 49 additions & 0 deletions src/Tokens/tokens.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import mongoose, { ObjectId, Schema, model } from 'mongoose';
import ITokens from './tokens.types';
import toJSON from '../plugins/toJSON';

interface IToken {
token: string;
user: ObjectId;
type: string;
expires: Date;
blacklisted: boolean;
}

const tokenSchema = new Schema<IToken>(
{
token: {
type: String,
required: true,
index: true,
},
user: {
type: mongoose.SchemaTypes.ObjectId,
ref: 'User',
required: true,
},
type: {
type: String,
enum: [ITokens.REFRESH, ITokens.RESET_PASSWORD, ITokens.VERIFY_EMAIL],
required: true,
},
expires: {
type: Date,
required: true,
},
blacklisted: {
type: Boolean,
default: false,
},
},
{
timestamps: true,
}
);

// add plugin that converts mongoose to json
tokenSchema.plugin(toJSON);

const Token = model<IToken>('Token', tokenSchema);

export default Token;
7 changes: 7 additions & 0 deletions src/tokens/tokens.types.ts → src/Tokens/tokens.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,10 @@ export default {
RESET_PASSWORD: 'resetPassword',
VERIFY_EMAIL: 'verifyEmail',
};

export enum TokensEnum {
'access',
'refresh',
'resetPassword',
'verifyEmail',
}
102 changes: 102 additions & 0 deletions src/Users/users.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Schema, model, Model } from 'mongoose';
import validator from 'validator';
import bcrypt from 'bcryptjs';
import toJSON from '../plugins/toJSON';
import paginate from '../plugins/paginate';
import { roles } from '../config/roles';

interface IUser {
name: string;
email: string;
password: string;
role?: string;
isEmailVerified?: boolean;
}

interface IStatics extends Model<IUser> {
isEmailTaken(): Promise<boolean>;
isPasswordMatch(): Promise<boolean>;
}

const userSchema = new Schema<IUser, IStatics>(
{
name: {
type: String,
required: true,
trim: true,
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
validate(value) {
if (!validator.isEmail(value)) {
throw new Error('Invalid email');
}
},
},
password: {
type: String,
required: true,
trim: true,
minlength: 8,
validate(value) {
if (!value.match(/\d/) || !value.match(/[a-zA-Z]/)) {
throw new Error('Password must contain at least one letter and one number');
}
},
private: true, // used by the toJSON plugin
},
role: {
type: String,
enum: roles,
default: 'user',
},
isEmailVerified: {
type: Boolean,
default: false,
},
},
{
timestamps: true,
}
);

// add plugin that converts mongoose to json
userSchema.plugin(toJSON);
userSchema.plugin(paginate);

/**
* Check if email is taken
* @param {string} email - The user's email
* @param {ObjectId} [excludeUserId] - The id of the user to be excluded
* @returns {Promise<boolean>}
*/
userSchema.statics.isEmailTaken = async function (email, excludeUserId) {
const user = await this.findOne({ email, _id: { $ne: excludeUserId } });
return !!user;
};

/**
* Check if password matches the user's password
* @param {string} password
* @returns {Promise<boolean>}
*/
userSchema.methods.isPasswordMatch = async function (password) {
const user = this;
return bcrypt.compare(password, user.password);
};

userSchema.pre('save', async function (next) {
const user = this;
if (user.isModified('password')) {
user.password = await bcrypt.hash(user.password, 8);
}
next();
});

const User = model<IUser>('User', userSchema);

export default User;
5 changes: 2 additions & 3 deletions src/config/passport.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Request } from 'express';
import { Strategy as JwtStrategy } from 'passport-jwt';
import tokenTypes from '../tokens/tokens.types';
import tokenTypes from '../Tokens/tokens.types';
import config from './config';

const { User } = require('../models');
import User from '../Users/users.model';

interface Payload {
sub: string;
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/inlineToJSON.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable no-param-reassign */
import { Document } from 'mongoose';

/**
* A mongoose schema plugin which allows user to hide fields dynamically using a hide option
Expand All @@ -7,7 +8,7 @@
const inlineToJSON = (schema: any) => {
schema.options.toJSON = {};
schema.options.toJSON.hide = '';
schema.options.toJSON.transform = function (doc: any, ret: any, options: Record<string, any>) {
schema.options.toJSON.transform = function (doc: Document, ret: any, options: Record<string, any>) {
if (options['hide']) {
options['hide'].split(' ').forEach(function (prop: string) {
delete ret[prop];
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/paginate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable no-param-reassign */
import { Schema } from 'mongoose';

const paginate = (schema: any) => {
const paginate = (schema: Schema) => {
/**
* @typedef {Object} QueryResult
* @property {Document[]} results - Results found
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/toJSON.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable no-param-reassign */
import { Document } from 'mongoose';

/**
* A mongoose schema plugin which applies the following in the toJSON transform call:
Expand All @@ -21,7 +22,7 @@ const toJSON = (schema: any) => {
}

schema.options.toJSON = Object.assign(schema.options.toJSON || {}, {
transform(doc: any, ret: any, options: Record<string, any>) {
transform(doc: Document, ret: any, options: Record<string, any>) {
Object.keys(schema.paths).forEach((path) => {
if (schema.paths[path].options && schema.paths[path].options.private) {
deleteAtPath(ret, path.split('.'), 0);
Expand Down

0 comments on commit 02ffcf8

Please sign in to comment.