Skip to content

Commit

Permalink
feat: add user, auth, token services
Browse files Browse the repository at this point in the history
  • Loading branch information
saisilinus committed Dec 16, 2021
1 parent a92dd01 commit 26736f7
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 42 deletions.
93 changes: 93 additions & 0 deletions src/Auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import httpStatus from 'http-status';
import Token from '../Tokens/token.model';
import ApiError from '../utils/ApiError';
import tokenTypes from '../Tokens/token.types';
import { getUserByEmail, getUserById, updateUserById } from '../Users/user.service';
import { IUserDoc } from '../Users/user.interfaces';
import { generateAuthTokens, verifyToken } from '../Tokens/token.service';
import { AccessAndRefreshTokens } from '../Tokens/token.interfaces';

/**
* Login with username and password
* @param {string} email
* @param {string} password
* @returns {Promise<IUserDoc>}
*/
export const loginUserWithEmailAndPassword = async (email: string, password: string): Promise<IUserDoc> => {
const user = await getUserByEmail(email);
if (!user || !(await user.isPasswordMatch(password))) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Incorrect email or password');
}
return user;
};

/**
* Logout
* @param {string} refreshToken
* @returns {Promise<void>}
*/
export const logout = async (refreshToken: string): Promise<void> => {
const refreshTokenDoc = await Token.findOne({ token: refreshToken, type: tokenTypes.REFRESH, blacklisted: false });
if (!refreshTokenDoc) {
throw new ApiError(httpStatus.NOT_FOUND, 'Not found');
}
await refreshTokenDoc.remove();
};

/**
* Refresh auth tokens
* @param {string} refreshToken
* @returns {Promise<AccessAndRefreshTokens>}
*/
export const refreshAuth = async (refreshToken: string): Promise<AccessAndRefreshTokens> => {
try {
const refreshTokenDoc = await verifyToken(refreshToken, tokenTypes.REFRESH);
const user = await getUserById(refreshTokenDoc.user);
if (!user) {
throw new Error();
}
await refreshTokenDoc.remove();
return await generateAuthTokens(user);
} catch (error) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
}
};

/**
* Reset password
* @param {string} resetPasswordToken
* @param {string} newPassword
* @returns {Promise<void>}
*/
export const resetPassword = async (resetPasswordToken: string, newPassword: string): Promise<void> => {
try {
const resetPasswordTokenDoc = await verifyToken(resetPasswordToken, tokenTypes.RESET_PASSWORD);
const user = await getUserById(resetPasswordTokenDoc.user);
if (!user) {
throw new Error();
}
await updateUserById(user.id, { password: newPassword });
await Token.deleteMany({ user: user.id, type: tokenTypes.RESET_PASSWORD });
} catch (error) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Password reset failed');
}
};

/**
* Verify email
* @param {string} verifyEmailToken
* @returns {Promise<void>}
*/
export const verifyEmail = async (verifyEmailToken: string): Promise<void> => {
try {
const verifyEmailTokenDoc = await verifyToken(verifyEmailToken, tokenTypes.VERIFY_EMAIL);
const user = await getUserById(verifyEmailTokenDoc.user);
if (!user) {
throw new Error();
}
await Token.deleteMany({ user: user.id, type: tokenTypes.VERIFY_EMAIL });
await updateUserById(user.id, { isEmailVerified: true });
} catch (error) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Email verification failed');
}
};
8 changes: 7 additions & 1 deletion src/Tokens/token.interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ObjectId } from 'mongoose';
import { ObjectId, Document, Model, LeanDocument } from 'mongoose';
import { JwtPayload } from 'jsonwebtoken';

export interface IToken {
Expand All @@ -9,6 +9,12 @@ export interface IToken {
blacklisted: boolean;
}

export interface ITokenDoc extends IToken, Document {}

export interface ITokenModel extends Model<ITokenDoc> {
toJSON(): LeanDocument<this>;
}

export interface IPayload extends JwtPayload {
sub: string;
iat: number;
Expand Down
6 changes: 3 additions & 3 deletions src/Tokens/token.model.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import mongoose, { Schema, model } from 'mongoose';
import tokenTypes from './token.types';
import toJSON from '../plugins/toJSON';
import { IToken } from './token.interfaces';
import { ITokenDoc, ITokenModel } from './token.interfaces';

const tokenSchema = new Schema<IToken>(
const tokenSchema = new Schema<ITokenDoc, ITokenModel>(
{
token: {
type: String,
Expand Down Expand Up @@ -37,6 +37,6 @@ const tokenSchema = new Schema<IToken>(
// add plugin that converts mongoose to json
tokenSchema.plugin(toJSON);

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

export default Token;
20 changes: 10 additions & 10 deletions src/Tokens/token.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import config from '../config/config';
import Token from './token.model';
import ApiError from '../utils/ApiError';
import tokenTypes from './token.types';
import { AccessAndRefreshTokens, IToken } from './token.interfaces';
import { ToJSONUser } from '../Users/user.interfaces';
import { AccessAndRefreshTokens, ITokenDoc } from './token.interfaces';
import { IUserDoc } from '../Users/user.interfaces';
import { getUserByEmail } from '../Users/user.service';

/**
Expand Down Expand Up @@ -40,15 +40,15 @@ export const generateToken = (
* @param {Moment} expires
* @param {string} type
* @param {boolean} [blacklisted]
* @returns {Promise<IToken>}
* @returns {Promise<ITokenDoc>}
*/
export const saveToken = async (
token: string,
userId: ObjectId,
expires: Moment,
type: string,
blacklisted: boolean = false
): Promise<IToken> => {
): Promise<ITokenDoc> => {
const tokenDoc = await Token.create({
token,
user: userId,
Expand All @@ -63,9 +63,9 @@ export const saveToken = async (
* Verify token and return token doc (or throw an error if it is not valid)
* @param {string} token
* @param {string} type
* @returns {Promise<IToken>}
* @returns {Promise<ITokenDoc>}
*/
export const verifyToken = async (token: string, type: string): Promise<IToken> => {
export const verifyToken = async (token: string, type: string): Promise<ITokenDoc> => {
const payload = jwt.verify(token, config.jwt.secret);
let tokenDoc;
if (typeof payload.sub === 'string') {
Expand All @@ -84,10 +84,10 @@ export const verifyToken = async (token: string, type: string): Promise<IToken>

/**
* Generate auth tokens
* @param {ToJSONUser} user
* @param {IUserDoc} user
* @returns {Promise<AccessAndRefreshTokens>}
*/
export const generateAuthTokens = async (user: ToJSONUser): Promise<AccessAndRefreshTokens> => {
export const generateAuthTokens = async (user: IUserDoc): Promise<AccessAndRefreshTokens> => {
const accessTokenExpires = moment().add(config.jwt.accessExpirationMinutes, 'minutes');
const accessToken = generateToken(user.id, accessTokenExpires, tokenTypes.ACCESS);

Expand Down Expand Up @@ -125,10 +125,10 @@ export const generateResetPasswordToken = async (email: string): Promise<string>

/**
* Generate verify email token
* @param {ToJSONUser} user
* @param {IUserDoc} user
* @returns {Promise<string>}
*/
export const generateVerifyEmailToken = async (user: ToJSONUser): Promise<string> => {
export const generateVerifyEmailToken = async (user: IUserDoc): Promise<string> => {
const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes');
const verifyEmailToken = generateToken(user.id, expires, tokenTypes.VERIFY_EMAIL);
await saveToken(verifyEmailToken, user.id, expires, tokenTypes.VERIFY_EMAIL);
Expand Down
28 changes: 18 additions & 10 deletions src/Users/user.interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Model, ObjectId } from 'mongoose';
import { Model, ObjectId, LeanDocument, Document } from 'mongoose';
import { QueryResult } from '../plugins/paginate';

export interface IUser {
Expand All @@ -9,26 +9,30 @@ export interface IUser {
isEmailVerified?: boolean;
}

export interface ToJSONUser {
name: string;
email: string;
password: string;
role: string;
isEmailVerified: boolean;
id: ObjectId;
export interface IUserDoc extends IUser, Document {
isPasswordMatch(password: string): Promise<boolean>;
}

export interface IUserModel extends Model<IUserDoc> {
isEmailTaken(email: string, excludeUserId?: ObjectId): Promise<boolean>;
paginate(filter: Record<string, any>, options: Record<string, any>): Promise<QueryResult>;
toJSON(): LeanDocument<this>;
}

export interface IUserStatics extends Model<IUser> {
isEmailTaken(email: string, excludeUserId?: ObjectId): Promise<boolean>;
isPasswordMatch(password: string): Promise<boolean>;
paginate(filter: Record<string, any>, options: Record<string, any>): Promise<QueryResult>;
toJSON(): void;
toJSON(): LeanDocument<this>;
}

export type IUserAndUserStatics = IUser & IUserStatics;

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

export interface NewRegisteredUser {
Expand All @@ -43,3 +47,7 @@ export interface NewCreatedUser {
password: string;
role: string;
}

export interface ToJSONUser extends IUserDoc {
id: ObjectId;
}
6 changes: 3 additions & 3 deletions src/Users/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import bcrypt from 'bcryptjs';
import toJSON from '../plugins/toJSON';
import paginate from '../plugins/paginate';
import { roles } from '../config/roles';
import { IUser, IUserStatics } from './user.interfaces';
import { IUserDoc, IUserModel } from './user.interfaces';

const userSchema = new Schema<IUser, IUserStatics>(
const userSchema = new Schema<IUserDoc, IUserModel>(
{
name: {
type: String,
Expand Down Expand Up @@ -85,6 +85,6 @@ userSchema.pre('save', async function (next) {
next();
});

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

export default User;
27 changes: 12 additions & 15 deletions src/Users/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import httpStatus from 'http-status';
import { Document, ObjectId } from 'mongoose';
import { ObjectId } from 'mongoose';
import User from './user.model';
import ApiError from '../utils/ApiError';
import { IOptions, QueryResult } from '../plugins/paginate';
import { NewCreatedUser, UpdateUserBody } from './user.interfaces';
import { NewCreatedUser, UpdateUserBody, IUserDoc } from './user.interfaces';

/**
* Create a user
* @param {NewCreatedUser} userBody
* @returns {Promise<Document>}
* @returns {Promise<IUserDoc>}
*/
export const createUser = async (userBody: NewCreatedUser): Promise<Document> => {
export const createUser = async (userBody: NewCreatedUser): Promise<IUserDoc> => {
if (await User.isEmailTaken(userBody.email)) {
throw new ApiError(httpStatus.BAD_REQUEST, 'Email already taken');
}
Expand All @@ -31,31 +31,28 @@ export const queryUsers = async (filter: Record<string, any>, options: IOptions)
/**
* Get user by id
* @param {ObjectId} id
* @returns {Promise<Document<any, any, any> | null>}
* @returns {Promise<IUserDoc | null>}
*/
export const getUserById = async (id: ObjectId): Promise<Document<any, any, any> | null> => {
export const getUserById = async (id: ObjectId): Promise<IUserDoc | null> => {
return User.findById(id);
};

/**
* Get user by email
* @param {string} email
* @returns {Promise<Document<any, any, any> | null>}
* @returns {Promise<IUserDoc | null>}
*/
export const getUserByEmail = async (email: string): Promise<Document<any, any, any> | null> => {
export const getUserByEmail = async (email: string): Promise<IUserDoc | null> => {
return User.findOne({ email });
};

/**
* Update user by id
* @param {ObjectId} userId
* @param {UpdateUserBody} updateBody
* @returns {Promise<Document<any, any, any> | null>}
* @returns {Promise<IUserDoc | null>}
*/
export const updateUserById = async (
userId: ObjectId,
updateBody: UpdateUserBody
): Promise<Document<any, any, any> | null> => {
export const updateUserById = async (userId: ObjectId, updateBody: UpdateUserBody): Promise<IUserDoc | null> => {
const user = await getUserById(userId);
if (!user) {
throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
Expand All @@ -71,9 +68,9 @@ export const updateUserById = async (
/**
* Delete user by id
* @param {ObjectId} userId
* @returns {Promise<Document<any, any, any> | null>}
* @returns {Promise<IUserDoc | null>}
*/
export const deleteUserById = async (userId: ObjectId): Promise<Document<any, any, any> | null> => {
export const deleteUserById = async (userId: ObjectId): Promise<IUserDoc | null> => {
const user = await getUserById(userId);
if (!user) {
throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
Expand Down

0 comments on commit 26736f7

Please sign in to comment.