This commit is contained in:
5
.eslintrc.js
Normal file
5
.eslintrc.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: [
|
||||||
|
'semistandard'
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -18,10 +18,10 @@ app.use(cookie());
|
|||||||
app.keys = [process.env.SESSION_KEYS as string];
|
app.keys = [process.env.SESSION_KEYS as string];
|
||||||
app.use(session({}, app));
|
app.use(session({}, app));
|
||||||
|
|
||||||
app.use(passport.initialize())
|
app.use(passport.initialize());
|
||||||
app.use(passport.session())
|
app.use(passport.session());
|
||||||
|
|
||||||
// Application error logging.
|
// Application error logging.
|
||||||
app.on('error', console.error);
|
app.on('error', console.error);
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export enum STRATEGIES {
|
export enum STRATEGIES {
|
||||||
LOCAL,
|
LOCAL,
|
||||||
APPLE,
|
APPLE,
|
||||||
FACEBOOK,
|
FACEBOOK,
|
||||||
FIDO2,
|
FIDO2,
|
||||||
GOOGLE,
|
GOOGLE,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,149 @@
|
|||||||
// const errors = require('restify-errors');
|
// // const errors = require('restify-errors');
|
||||||
|
|
||||||
// const config = require('../config');
|
// // const config = require('../config');
|
||||||
|
|
||||||
// const handlePassportResponse = (req, res, next) => (err, user, info) => {
|
// // const handlePassportResponse = (req, res, next) => (err, user, info) => {
|
||||||
|
// // if (err) {
|
||||||
|
// // return next(err);
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // const isVerifiedUser = user &&
|
||||||
|
// // user.isRegistrationVerified();
|
||||||
|
|
||||||
|
// // if (user && isVerifiedUser) {
|
||||||
|
// // return res.send({ ...user.toAuthJSON() });
|
||||||
|
// // } else if (user && !isVerifiedUser){
|
||||||
|
// // return res.send({
|
||||||
|
// // registrationSuccess: true,
|
||||||
|
// // nextSteps: 'Check your email for our confirmation email, you will not be able to login without confirming.'
|
||||||
|
// // });
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // return res.send(400, info);
|
||||||
|
// // };
|
||||||
|
|
||||||
|
// // module.exports = function (server, auth) {
|
||||||
|
// // const { passport } = auth;
|
||||||
|
|
||||||
|
// // /* Local Auth */
|
||||||
|
// // server.post('/auth', (req, res, next) => {
|
||||||
|
// // const { body: { username = null, password = null } = {} } = req;
|
||||||
|
|
||||||
|
// // if (!username || !password) {
|
||||||
|
// // let errors = {};
|
||||||
|
|
||||||
|
// // if (!username) {
|
||||||
|
// // errors.username = 'is required';
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // if (!password) {
|
||||||
|
// // errors.password = 'is required';
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // return res.send(422, { errors });
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // const callback = handlePassportResponse(req, res, next);
|
||||||
|
// // return passport.authenticate('local', { session: false }.then(callback)(req, res, next);
|
||||||
|
// // });
|
||||||
|
|
||||||
|
// // /**
|
||||||
|
// // * SERVICES
|
||||||
|
// // */
|
||||||
|
|
||||||
|
// // /* Google */
|
||||||
|
// // server.get(
|
||||||
|
// // '/auth/google',
|
||||||
|
// // passport.authenticate('google', { scope: 'profile email', session: false }),
|
||||||
|
// // );
|
||||||
|
|
||||||
|
// // server.get(
|
||||||
|
// // '/auth/google/callback',
|
||||||
|
// // (req, res, next) => {
|
||||||
|
// // const callback = handlePassportResponse(req, res, next);
|
||||||
|
// // return passport.authenticate(
|
||||||
|
// // 'google',
|
||||||
|
// // { failureRedirect: '/login' },
|
||||||
|
// // callback,
|
||||||
|
// // )(req, res, next);
|
||||||
|
// // },
|
||||||
|
// // );
|
||||||
|
|
||||||
|
// // /* Facebook */
|
||||||
|
// // server.get(
|
||||||
|
// // '/auth/facebook/login',
|
||||||
|
// // passport.authenticate('facebook', {
|
||||||
|
// // scope: ['email', 'public_profile'],
|
||||||
|
// // session: false,
|
||||||
|
// // }),
|
||||||
|
// // );
|
||||||
|
|
||||||
|
// // server.get(
|
||||||
|
// // '/auth/facebook/loggedin',
|
||||||
|
// // (req, res, next) => {
|
||||||
|
// // const callback = handlePassportResponse(req, res, next);
|
||||||
|
// // return passport.authenticate(
|
||||||
|
// // 'facebook',
|
||||||
|
// // { failureRedirect: '/login' },
|
||||||
|
// // callback,
|
||||||
|
// // )(req, res, next);
|
||||||
|
// // }
|
||||||
|
// // );
|
||||||
|
|
||||||
|
// // server.get(
|
||||||
|
// // '/auth/facebook/link',
|
||||||
|
// // auth.secure,
|
||||||
|
// // (req, res, next) => {
|
||||||
|
// // req.user.record.setLinkCheckBit((err, linkCheckBit) => {
|
||||||
|
// // passport.authenticate('facebookLink', {
|
||||||
|
// // scope: ['email', 'public_profile'],
|
||||||
|
// // session: false,
|
||||||
|
// // state: linkCheckbit,
|
||||||
|
// // })(req, res, next);
|
||||||
|
// // });
|
||||||
|
// // },
|
||||||
|
// // );
|
||||||
|
// //
|
||||||
|
// // server.get(
|
||||||
|
// // '/auth/facebook/linked',
|
||||||
|
// // (req, res, next) => {
|
||||||
|
// // const linkCheckBit = req.query.state;
|
||||||
|
// //
|
||||||
|
// // return passport.authenticate(
|
||||||
|
// // 'facebook',
|
||||||
|
// // { failureRedirect: '/profile' },
|
||||||
|
// // (err, profile) => {
|
||||||
|
// // if (err) {
|
||||||
|
// // return next(err);
|
||||||
|
// // }
|
||||||
|
// //
|
||||||
|
// // User.linkFacebookProfile(linkCheckBit, profile, (err, user) => {
|
||||||
|
// // if (err) {
|
||||||
|
// // return next(err);
|
||||||
|
// // }
|
||||||
|
// //
|
||||||
|
// // if (!user) {
|
||||||
|
// // return next(err, false, 'Linking the account to Facebook was unsuccessful, please try again.');
|
||||||
|
// // }
|
||||||
|
// //
|
||||||
|
// // res.send({
|
||||||
|
// // success: true,
|
||||||
|
// // info: 'Facerbook account successfully linked',
|
||||||
|
// // });
|
||||||
|
// // });
|
||||||
|
// // },
|
||||||
|
// // )(req, res, next);
|
||||||
|
// // }
|
||||||
|
// // );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// import Koa from 'koa';
|
||||||
|
// import Router from 'koa-router';
|
||||||
|
// import { StatusCodes } from 'http-status-codes';
|
||||||
|
|
||||||
|
// import Users from 'grow-db/lib/models/users';
|
||||||
|
|
||||||
|
// const handlePassportResponse = (ctx: Koa.Context) => (err, user, info) => {
|
||||||
// if (err) {
|
// if (err) {
|
||||||
// return next(err);
|
// return next(err);
|
||||||
// }
|
// }
|
||||||
@@ -22,210 +163,65 @@
|
|||||||
// return res.send(400, info);
|
// return res.send(400, info);
|
||||||
// };
|
// };
|
||||||
|
|
||||||
// module.exports = function (server, auth) {
|
// const routerOpts: Router.IRouterOptions = {
|
||||||
// const { passport } = auth;
|
// prefix: '/auth',
|
||||||
|
// };
|
||||||
|
|
||||||
// /* Local Auth */
|
// const router: Router = new Router(routerOpts);
|
||||||
// server.post('/auth', (req, res, next) => {
|
|
||||||
// const { body: { username = null, password = null } = {} } = req;
|
|
||||||
|
|
||||||
// if (!username || !password) {
|
// router.get('/', async (ctx: Koa.Context) => {
|
||||||
// let errors = {};
|
// const data = await Customers.find({}).exec();
|
||||||
|
// ctx.body = { data };
|
||||||
|
// });
|
||||||
|
|
||||||
// if (!username) {
|
// router.get('/:customer_id', async (ctx: Koa.Context) => {
|
||||||
// errors.username = 'is required';
|
// const data = await Customers.findById(ctx.params.customer_id).populate('person').exec();
|
||||||
// }
|
// if (!data) {
|
||||||
|
// ctx.throw(StatusCodes.NOT_FOUND);
|
||||||
|
// }
|
||||||
|
// ctx.body = { data };
|
||||||
|
// });
|
||||||
|
|
||||||
// if (!password) {
|
// router.delete('/:customer_id', async (ctx: Koa.Context) => {
|
||||||
// errors.password = 'is required';
|
// const data = await Customers.findByIdAndDelete(ctx.params.customer_id).exec();
|
||||||
// }
|
// if (!data) {
|
||||||
|
// ctx.throw(StatusCodes.NOT_FOUND);
|
||||||
|
// }
|
||||||
|
// ctx.body = { success: true, data };
|
||||||
|
// });
|
||||||
|
|
||||||
// return res.send(422, { errors });
|
// router.post('/', async (ctx: Koa.Context) => {
|
||||||
|
// const data = await Customers.create(ctx.body);
|
||||||
|
// data.save();
|
||||||
|
// ctx.body = { success: true, data };
|
||||||
|
// });
|
||||||
|
|
||||||
|
// router.post('/', async (ctx: Koa.Context) => {
|
||||||
|
// const { body: { username = null, password = null } = {} } = ctx;
|
||||||
|
|
||||||
|
// if (!username || !password) {
|
||||||
|
// let errors = {};
|
||||||
|
|
||||||
|
// if (!username) {
|
||||||
|
// errors.username = 'is required';
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// const callback = handlePassportResponse(req, res, next);
|
// if (!password) {
|
||||||
// return passport.authenticate('local', { session: false }.then(callback)(req, res, next);
|
// errors.password = 'is required';
|
||||||
// });
|
// }
|
||||||
|
|
||||||
// /**
|
// ctx.status = StatusCodes.UNPROCESSABLE_ENTITY;
|
||||||
// * SERVICES
|
// ctx.throw(422, { errors });
|
||||||
// */
|
// }
|
||||||
|
|
||||||
// /* Google */
|
// const callback = handlePassportResponse(req, res, next);
|
||||||
// server.get(
|
// return passport.authenticate('local', { session: false }, callback)(req, res, next);
|
||||||
// '/auth/google',
|
// });
|
||||||
// passport.authenticate('google', { scope: 'profile email', session: false }),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// server.get(
|
// router.patch('/:customer_id', async (ctx: Koa.Context) => {
|
||||||
// '/auth/google/callback',
|
// const data = await Customers.findByIdAndUpdate(ctx.params.customer_id);
|
||||||
// (req, res, next) => {
|
// if (!data) {
|
||||||
// const callback = handlePassportResponse(req, res, next);
|
// ctx.throw(StatusCodes.NOT_FOUND);
|
||||||
// return passport.authenticate(
|
// }
|
||||||
// 'google',
|
// ctx.body = { success: true, data };
|
||||||
// { failureRedirect: '/login' },
|
// });
|
||||||
// callback,
|
|
||||||
// )(req, res, next);
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
|
|
||||||
// /* Facebook */
|
|
||||||
// server.get(
|
|
||||||
// '/auth/facebook/login',
|
|
||||||
// passport.authenticate('facebook', {
|
|
||||||
// scope: ['email', 'public_profile'],
|
|
||||||
// session: false,
|
|
||||||
// }),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// server.get(
|
|
||||||
// '/auth/facebook/loggedin',
|
|
||||||
// (req, res, next) => {
|
|
||||||
// const callback = handlePassportResponse(req, res, next);
|
|
||||||
// return passport.authenticate(
|
|
||||||
// 'facebook',
|
|
||||||
// { failureRedirect: '/login' },
|
|
||||||
// callback,
|
|
||||||
// )(req, res, next);
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
// server.get(
|
|
||||||
// '/auth/facebook/link',
|
|
||||||
// auth.secure,
|
|
||||||
// (req, res, next) => {
|
|
||||||
// req.user.record.setLinkCheckBit((err, linkCheckBit) => {
|
|
||||||
// passport.authenticate('facebookLink', {
|
|
||||||
// scope: ['email', 'public_profile'],
|
|
||||||
// session: false,
|
|
||||||
// state: linkCheckbit,
|
|
||||||
// })(req, res, next);
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// server.get(
|
|
||||||
// '/auth/facebook/linked',
|
|
||||||
// (req, res, next) => {
|
|
||||||
// const linkCheckBit = req.query.state;
|
|
||||||
//
|
|
||||||
// return passport.authenticate(
|
|
||||||
// 'facebook',
|
|
||||||
// { failureRedirect: '/profile' },
|
|
||||||
// (err, profile) => {
|
|
||||||
// if (err) {
|
|
||||||
// return next(err);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// User.linkFacebookProfile(linkCheckBit, profile, (err, user) => {
|
|
||||||
// if (err) {
|
|
||||||
// return next(err);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (!user) {
|
|
||||||
// return next(err, false, 'Linking the account to Facebook was unsuccessful, please try again.');
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// res.send({
|
|
||||||
// success: true,
|
|
||||||
// info: 'Facerbook account successfully linked',
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
// )(req, res, next);
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
};
|
|
||||||
|
|
||||||
import Koa from 'koa';
|
|
||||||
import Router from 'koa-router';
|
|
||||||
import { StatusCodes } from 'http-status-codes';
|
|
||||||
|
|
||||||
import Users from 'grow-db/lib/models/users';
|
|
||||||
|
|
||||||
const handlePassportResponse = (ctx: Koa.Context) => (err, user, info) => {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isVerifiedUser = user &&
|
|
||||||
user.isRegistrationVerified();
|
|
||||||
|
|
||||||
if (user && isVerifiedUser) {
|
|
||||||
return res.send({ ...user.toAuthJSON() });
|
|
||||||
} else if (user && !isVerifiedUser){
|
|
||||||
return res.send({
|
|
||||||
registrationSuccess: true,
|
|
||||||
nextSteps: 'Check your email for our confirmation email, you will not be able to login without confirming.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.send(400, info);
|
|
||||||
};
|
|
||||||
|
|
||||||
const routerOpts: Router.IRouterOptions = {
|
|
||||||
prefix: '/auth',
|
|
||||||
};
|
|
||||||
|
|
||||||
const router: Router = new Router(routerOpts);
|
|
||||||
|
|
||||||
router.get('/', async (ctx: Koa.Context) => {
|
|
||||||
const data = await Customers.find({}).exec();
|
|
||||||
ctx.body = { data };
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
router.get('/:customer_id', async (ctx: Koa.Context) => {
|
|
||||||
const data = await Customers.findById(ctx.params.customer_id).populate('person').exec();
|
|
||||||
if (!data) {
|
|
||||||
ctx.throw(StatusCodes.NOT_FOUND);
|
|
||||||
}
|
|
||||||
ctx.body = { data };
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete('/:customer_id', async (ctx: Koa.Context) => {
|
|
||||||
const data = await Customers.findByIdAndDelete(ctx.params.customer_id).exec();
|
|
||||||
if (!data) {
|
|
||||||
ctx.throw(StatusCodes.NOT_FOUND);
|
|
||||||
}
|
|
||||||
ctx.body = { success: true, data };
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
router.post('/', async (ctx: Koa.Context) => {
|
|
||||||
const data = await Customers.create(ctx.body);
|
|
||||||
data.save();
|
|
||||||
ctx.body = { success: true, data };
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
router.post('/', async (ctx: Koa.Context) => {
|
|
||||||
const { body: { username = null, password = null } = {} } = ctx;
|
|
||||||
|
|
||||||
if (!username || !password) {
|
|
||||||
let errors = {};
|
|
||||||
|
|
||||||
if (!username) {
|
|
||||||
errors.username = 'is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!password) {
|
|
||||||
errors.password = 'is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.status = StatusCodes.UNPROCESSABLE_ENTITY;
|
|
||||||
ctx.throw(422, { errors });
|
|
||||||
}
|
|
||||||
|
|
||||||
const callback = handlePassportResponse(req, res, next);
|
|
||||||
return passport.authenticate('local', { session: false }, callback)(req, res, next);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
router.patch('/:customer_id', async (ctx: Koa.Context) => {
|
|
||||||
const data = await Customers.findByIdAndUpdate(ctx.params.customer_id);
|
|
||||||
if (!data) {
|
|
||||||
ctx.throw(StatusCodes.NOT_FOUND);
|
|
||||||
}
|
|
||||||
ctx.body = { success: true, data };
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ const DB_PORT = process.env.DB_PORT || 27017;
|
|||||||
const DB_NAME = process.env.DB_NAME || 'auth';
|
const DB_NAME = process.env.DB_NAME || 'auth';
|
||||||
|
|
||||||
export const connection = mongoose.connect(
|
export const connection = mongoose.connect(
|
||||||
`${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}`
|
`${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from 'http-status-codes';
|
||||||
import { Context, Next } from "koa";
|
import { Context, Next } from 'koa';
|
||||||
|
|
||||||
export const errorHandler = async (ctx: Context, next: Next) => {
|
export const errorHandler = async (ctx: Context, next: Next) => {
|
||||||
try {
|
try {
|
||||||
await next();
|
await next();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
ctx.status = error.statusCode || error.status || StatusCodes.INTERNAL_SERVER_ERROR;
|
ctx.status =
|
||||||
error.status = ctx.status;
|
error.statusCode || error.status || StatusCodes.INTERNAL_SERVER_ERROR;
|
||||||
ctx.body = { error };
|
error.status = ctx.status;
|
||||||
ctx.app.emit('error', error, ctx);
|
ctx.body = { error };
|
||||||
}
|
ctx.app.emit('error', error, ctx);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { Next } from 'koa';
|
|||||||
import { KoaContext } from '../types/KoaContext';
|
import { KoaContext } from '../types/KoaContext';
|
||||||
|
|
||||||
export const performanceLogger = async (ctx: KoaContext, next: Next) => {
|
export const performanceLogger = async (ctx: KoaContext, next: Next) => {
|
||||||
await next();
|
await next();
|
||||||
const rt = ctx.response.get('X-Response-Time');
|
const rt = ctx.response.get('X-Response-Time');
|
||||||
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
|
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const perfromanceTimer = async (ctx: KoaContext, next: Next) => {
|
export const perfromanceTimer = async (ctx: KoaContext, next: Next) => {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
await next();
|
await next();
|
||||||
const ms = Date.now() - start;
|
const ms = Date.now() - start;
|
||||||
ctx.set('X-Response-Time', `${ms}ms`);
|
ctx.set('X-Response-Time', `${ms}ms`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { JwtPayload } from 'jsonwebtoken';
|
import { JwtPayload } from 'jsonwebtoken';
|
||||||
import { Document, InferSchemaType, Model, Schema, StringSchemaDefinition, Types } from 'mongoose';
|
import {
|
||||||
|
InferSchemaType,
|
||||||
|
Model,
|
||||||
|
Schema,
|
||||||
|
StringSchemaDefinition,
|
||||||
|
Types,
|
||||||
|
} from 'mongoose';
|
||||||
|
|
||||||
import { Strategy } from './strategy';
|
import { Strategy } from './strategy';
|
||||||
import { STRATEGIES } from '../constants/strategies';
|
import { STRATEGIES } from '../constants/strategies';
|
||||||
@@ -8,127 +14,139 @@ import { encrypt, verify as verifyPassword } from '../utils/password';
|
|||||||
import { generateResetToken } from '../utils/tokens';
|
import { generateResetToken } from '../utils/tokens';
|
||||||
|
|
||||||
export type Auth = {
|
export type Auth = {
|
||||||
is2FA?: boolean;
|
is2FA?: boolean;
|
||||||
record: StringSchemaDefinition;
|
record: StringSchemaDefinition;
|
||||||
username: string;
|
username: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type AuthPrivate = Auth & {
|
export type AuthPrivate = Auth & {
|
||||||
strategies: Types.ArraySubdocument<Strategy>;
|
strategies: Types.ArraySubdocument<Strategy>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface AuthMethods {
|
export interface AuthMethods {
|
||||||
authenticate(password: string): boolean;
|
authenticate(password: string): boolean;
|
||||||
getAuthStrategy(method?: STRATEGIES): Strategy | false;
|
getAuthStrategy(method?: STRATEGIES): Strategy | false;
|
||||||
getResetLink(route: string): Promise<string | undefined>;
|
getResetLink(route: string): Promise<string | undefined>;
|
||||||
getResetToken(): Promise<string | undefined>;
|
getResetToken(): Promise<string | undefined>;
|
||||||
getToken(props?: Omit<TokenProps, 'sub'>): string;
|
getToken(props?: Omit<TokenProps, 'sub'>): string;
|
||||||
setPassword(password: string): Promise<boolean>;
|
setPassword(password: string): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthModel extends Model<AuthPrivate, {}, AuthMethods> {
|
export interface AuthModel extends Model<AuthPrivate, {}, AuthMethods> {
|
||||||
authenticate(password: any): boolean;
|
authenticate(password: any): boolean;
|
||||||
findByUsername(username: string): Promise<AuthModel & AuthPrivate>;
|
findByUsername(username: string): Promise<AuthModel & AuthPrivate>;
|
||||||
isUsernameAvailable(username: string): Promise<boolean>;
|
isUsernameAvailable(username: string): Promise<boolean>;
|
||||||
findUserForReset(strategy: STRATEGIES, token: string): Promise<Strategy | undefined>;
|
findUserForReset(
|
||||||
resetPassword(token: string, password: string): Promise<boolean>;
|
strategy: STRATEGIES,
|
||||||
}
|
token: string
|
||||||
|
): Promise<Strategy | undefined>;
|
||||||
|
resetPassword(token: string, password: string): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
export const AuthSchema = new Schema<AuthPrivate, AuthModel, AuthMethods>(
|
export const AuthSchema = new Schema<AuthPrivate, AuthModel, AuthMethods>(
|
||||||
{
|
{
|
||||||
is2FA: { type: Boolean, default: false },
|
is2FA: { type: Boolean, default: false },
|
||||||
record: { type: Types.ObjectId },
|
record: { type: Types.ObjectId },
|
||||||
strategies: { type: Types.ArraySubdocument<Strategy>, required: true },
|
strategies: { type: Types.ArraySubdocument<Strategy>, required: true },
|
||||||
username: { type: String, required: true, unique: true },
|
username: { type: String, required: true, unique: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
minimize: true,
|
minimize: true,
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
AuthSchema.methods = {
|
AuthSchema.methods = {
|
||||||
authenticate: function(password: string) {
|
authenticate: function (password: string) {
|
||||||
const strategy = this.getAuthStrategy(STRATEGIES.LOCAL);
|
const strategy = this.getAuthStrategy(STRATEGIES.LOCAL);
|
||||||
return !!strategy && verifyPassword(password, strategy.key);
|
return !!strategy && verifyPassword(password, strategy.key);
|
||||||
},
|
},
|
||||||
|
|
||||||
getAuthStrategy: function(method = STRATEGIES.LOCAL) {
|
getAuthStrategy: function (method = STRATEGIES.LOCAL) {
|
||||||
return this.strategies.filter((strategy: Strategy) => strategy.method === method).pop() || false;
|
return (
|
||||||
},
|
this.strategies
|
||||||
|
.filter((strategy: Strategy) => strategy.method === method)
|
||||||
|
.pop() || false
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
getToken: function(props = {}) {
|
getToken: function (props = {}) {
|
||||||
return sign({
|
return sign({
|
||||||
sub: this._id,
|
sub: this._id,
|
||||||
...props,
|
...props,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getResetLink: async function (route) {
|
getResetLink: async function (route) {
|
||||||
const resetToken = await this.getResetToken();
|
const resetToken = await this.getResetToken();
|
||||||
if (resetToken) {
|
if (resetToken) {
|
||||||
let resetRoute = route;
|
let resetRoute = route;
|
||||||
resetRoute = resetRoute.replace(':user_id', this._id);
|
resetRoute = resetRoute.replace(':user_id', this._id);
|
||||||
resetRoute = resetRoute.replace(':reset_token?', resetToken);
|
resetRoute = resetRoute.replace(':reset_token?', resetToken);
|
||||||
const resetUrl = `${process.env.URL}${resetRoute}`;
|
const resetUrl = `${process.env.URL}${resetRoute}`;
|
||||||
console.log('[sendPasswordReset] resetUrl:', resetUrl);
|
console.log('[sendPasswordReset] resetUrl:', resetUrl);
|
||||||
return resetUrl;
|
return resetUrl;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getResetToken: async function () {
|
getResetToken: async function () {
|
||||||
const { key, token } = generateResetToken(this._id);
|
const { key, token } = generateResetToken(this._id);
|
||||||
this.resetCheckBit = key;
|
this.resetCheckBit = key;
|
||||||
await this.save().catch(() => undefined);
|
await this.save().catch(() => undefined);
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
|
|
||||||
setPassword: async function (password) {
|
setPassword: async function (password) {
|
||||||
const key = encrypt(password);
|
const key = encrypt(password);
|
||||||
const hasLocalStrategy = !!this.getAuthStrategy(STRATEGIES.LOCAL);
|
const hasLocalStrategy = !!this.getAuthStrategy(STRATEGIES.LOCAL);
|
||||||
const strategy = {
|
const strategy = {
|
||||||
key,
|
key,
|
||||||
method: STRATEGIES.LOCAL,
|
method: STRATEGIES.LOCAL,
|
||||||
resetToken: undefined,
|
resetToken: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hasLocalStrategy) {
|
if (hasLocalStrategy) {
|
||||||
await this.model('User').findOneAndUpdate(
|
await this.model('User')
|
||||||
{ _id: this._id, 'strategies.method': STRATEGIES.LOCAL },
|
.findOneAndUpdate(
|
||||||
{ $set: { 'strategies.$': strategy } },
|
{ _id: this._id, 'strategies.method': STRATEGIES.LOCAL },
|
||||||
{ upsert: true },
|
{ $set: { 'strategies.$': strategy } },
|
||||||
).catch();
|
{ upsert: true },
|
||||||
return true;
|
)
|
||||||
}
|
.catch();
|
||||||
this.credentials.push(strategy);
|
return true;
|
||||||
await this.save().catch(() => false);
|
}
|
||||||
return true;
|
this.credentials.push(strategy);
|
||||||
},
|
await this.save().catch(() => false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
AuthSchema.statics = {
|
AuthSchema.statics = {
|
||||||
// authenticateAndGetRecordLocator: async function (username, password) {
|
// authenticateAndGetRecordLocator: async function (username, password) {
|
||||||
// const auth = await this.findByUserName(username);
|
// const auth = await this.findByUserName(username);
|
||||||
// if (auth && auth.authenticate(password)) {
|
// if (auth && auth.authenticate(password)) {
|
||||||
// return auth?.record;
|
// return auth?.record;
|
||||||
// }
|
// }
|
||||||
// return false;
|
// return false;
|
||||||
// },
|
// },
|
||||||
|
|
||||||
findByUsername: async function (username) {
|
findByUsername: async function (username) {
|
||||||
return this.findOne({ username });
|
return this.findOne({ username });
|
||||||
},
|
},
|
||||||
|
|
||||||
isUsernameAvailable: async function (username) {
|
isUsernameAvailable: async function (username) {
|
||||||
return !!!this.findByUsername(username);
|
return !this.findByUsername(username);
|
||||||
},
|
},
|
||||||
|
|
||||||
resetPassword: async function (token, password) {
|
resetPassword: async function (token, password) {
|
||||||
const decoded = verifyJwt(token);
|
const decoded = verifyJwt(token);
|
||||||
const { sub, key } = decoded as JwtPayload;
|
const { sub, key } = decoded as JwtPayload;
|
||||||
const auth = await this.findOne({ _id: sub, 'strategies.resetToken': key }).catch();
|
const auth = await this.findOne({
|
||||||
return !!auth && auth.setPassword(password);
|
_id: sub,
|
||||||
},
|
'strategies.resetToken': key,
|
||||||
|
}).catch();
|
||||||
|
return !!auth && auth.setPassword(password);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuthSchema = InferSchemaType<typeof AuthSchema>;
|
export type AuthSchema = InferSchemaType<typeof AuthSchema>;
|
||||||
|
|||||||
@@ -2,24 +2,24 @@ import { InferSchemaType, Schema, Types } from 'mongoose';
|
|||||||
import { STRATEGIES } from '../constants/strategies';
|
import { STRATEGIES } from '../constants/strategies';
|
||||||
|
|
||||||
export const Strategy = new Schema(
|
export const Strategy = new Schema(
|
||||||
{
|
{
|
||||||
method: {
|
method: {
|
||||||
type: Number,
|
type: Number,
|
||||||
enum: Object.values(STRATEGIES),
|
enum: Object.values(STRATEGIES),
|
||||||
index: true,
|
index: true,
|
||||||
required: true,
|
required: true,
|
||||||
unique: true,
|
unique: true
|
||||||
},
|
|
||||||
externalId: { type: String, index: true },
|
|
||||||
key: { type: String, required: true, trim: true },
|
|
||||||
profile: {},
|
|
||||||
resetToken: { type: String },
|
|
||||||
forceReset: { type: Boolean },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
minimize: true,
|
|
||||||
timestamps: true,
|
|
||||||
},
|
},
|
||||||
|
externalId: { type: String, index: true },
|
||||||
|
key: { type: String, required: true, trim: true },
|
||||||
|
profile: {},
|
||||||
|
resetToken: { type: String },
|
||||||
|
forceReset: { type: Boolean }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
minimize: true,
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type Strategy = InferSchemaType<typeof Strategy>;
|
export type Strategy = InferSchemaType<typeof Strategy>;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
import app from './app';
|
import app from './app';
|
||||||
import { connection } from './database/database.connection';
|
import { connection } from './database/database.connection';
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
const PORT: number = Number(process.env.PORT) || 9000;
|
const PORT: number = Number(process.env.PORT) || 9000;
|
||||||
|
|
||||||
connection.then(
|
connection.then(
|
||||||
() => app.listen(PORT),
|
() => app.listen(PORT),
|
||||||
(err) => console.error('ERROR!', err),
|
(err) => console.error('ERROR!', err),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import passport from 'koa-passport';
|
import passport from 'koa-passport';
|
||||||
import { Strategy } from 'passport-local';
|
import { Strategy } from 'passport-local';
|
||||||
import bcrypt from 'bcrypt';
|
|
||||||
|
|
||||||
import Auth from '../model/auth';
|
import Auth from '../model/auth';
|
||||||
import { AuthSchema } from '../schema/auth';
|
|
||||||
|
|
||||||
export const LocalStrategy = passport.use(new Strategy(async (username, password, done) => {
|
export const localStrategy = passport.use(
|
||||||
|
new Strategy(async (username, password, done) => {
|
||||||
const user = await Auth.findOne({
|
const user = await Auth.findOne({
|
||||||
where: {
|
where: {
|
||||||
username,
|
username,
|
||||||
}
|
}
|
||||||
}).catch();
|
}).catch();
|
||||||
if (user && user.authenticate(password)) {
|
if (user && user.authenticate(password)) {
|
||||||
done(null, user);
|
done(null, user);
|
||||||
} else {
|
} else {
|
||||||
done(null, false);
|
done(null, false);
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
));
|
);
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ import Auth from '../model/auth';
|
|||||||
import { AuthModel, AuthPrivate } from '../schema/auth';
|
import { AuthModel, AuthPrivate } from '../schema/auth';
|
||||||
import { sign } from './jwt';
|
import { sign } from './jwt';
|
||||||
|
|
||||||
export const getAuthenticationBundle = async (username: string, password: string) => {
|
export const getAuthenticationBundle = async (
|
||||||
const auth = await Auth.findByUsername(username).catch();
|
username: string,
|
||||||
const isAuthenticated = !!auth && (auth as AuthModel).authenticate(password);
|
password: string,
|
||||||
const record = isAuthenticated ? (auth as AuthPrivate).record : null;
|
) => {
|
||||||
const token = isAuthenticated ? (auth as AuthModel).getToken() : sign();
|
const auth = await Auth.findByUsername(username).catch();
|
||||||
return {
|
const isAuthenticated = !!auth && (auth as AuthModel).authenticate(password);
|
||||||
record,
|
const record = isAuthenticated ? (auth as AuthPrivate).record : null;
|
||||||
token,
|
const token = isAuthenticated ? (auth as AuthModel).getToken() : sign();
|
||||||
};
|
return {
|
||||||
|
record,
|
||||||
|
token,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,31 +1,40 @@
|
|||||||
import jwt, { JwtPayload } from 'jsonwebtoken';
|
import jwt, { JwtPayload } from 'jsonwebtoken';
|
||||||
|
|
||||||
export interface TokenProps {
|
export interface TokenProps {
|
||||||
aud?: string;
|
aud?: string;
|
||||||
exp?: number | Date;
|
exp?: number | Date;
|
||||||
iss?: string;
|
iss?: string;
|
||||||
sub: string | null;
|
sub: string | null;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SignProps = string | TokenProps | void;
|
export type SignProps = string | TokenProps | void;
|
||||||
|
|
||||||
export const sign = (props: SignProps) => {
|
export const sign = (props: SignProps) => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const { sub = null, ...rest }: TokenProps = typeof props === 'string' || typeof props === 'undefined' ? { sub: props || null } : props;
|
const { sub = null, ...rest }: TokenProps =
|
||||||
let exp = rest.exp;
|
typeof props === 'string' || typeof props === 'undefined'
|
||||||
if (!exp) {
|
? { sub: props || null }
|
||||||
exp = new Date(today);
|
: props;
|
||||||
exp.setDate(today.getDate() + parseInt(process.env.JWT_DAYS_VALID as string));
|
let exp = rest.exp;
|
||||||
exp = exp.getTime() / 1000;
|
if (!exp) {
|
||||||
}
|
exp = new Date(today);
|
||||||
return jwt.sign({
|
exp.setDate(
|
||||||
aud: rest.aud || process.env.JWT_AUDIENCE,
|
today.getDate() + parseInt(process.env.JWT_DAYS_VALID as string),
|
||||||
exp,
|
);
|
||||||
iat: today.getTime(),
|
exp = exp.getTime() / 1000;
|
||||||
iss: rest.iss || process.env.JWT_ISSUER,
|
}
|
||||||
sub,
|
return jwt.sign(
|
||||||
}, process.env.JWT_SECRET || 'secret');
|
{
|
||||||
|
exp,
|
||||||
|
sub,
|
||||||
|
aud: rest.aud || process.env.JWT_AUDIENCE,
|
||||||
|
iat: today.getTime(),
|
||||||
|
iss: rest.iss || process.env.JWT_ISSUER,
|
||||||
|
},
|
||||||
|
process.env.JWT_SECRET || 'secret',
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verify = (token: string) => jwt.verify(token, process.env.JWT_SECRET || 'secret');
|
export const verify = (token: string) =>
|
||||||
|
jwt.verify(token, process.env.JWT_SECRET || 'secret');
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { pbkdf2Sync, randomBytes } from 'crypto';
|
import { pbkdf2Sync, randomBytes } from 'crypto';
|
||||||
|
|
||||||
export const encrypt = (password: string) => {
|
export const encrypt = (password: string) => {
|
||||||
const salt = randomBytes(16).toString('hex');
|
const salt = randomBytes(16).toString('hex');
|
||||||
const hash = pbkdf2Sync(password, salt, 10000, 512, 'sha512').toString('hex');
|
const hash = pbkdf2Sync(password, salt, 10000, 512, 'sha512').toString('hex');
|
||||||
return `${salt}:${hash}`;
|
return `${salt}:${hash}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verify = (test: string, secret: string) => {
|
export const verify = (test: string, secret: string) => {
|
||||||
const [salt, hash] = secret.split(':');
|
const [salt, hash] = secret.split(':');
|
||||||
return pbkdf2Sync(test, salt, 10000, 512, 'sha512').toString('hex') === hash;
|
return pbkdf2Sync(test, salt, 10000, 512, 'sha512').toString('hex') === hash;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
import { sign } from "./jwt";
|
import { sign } from './jwt';
|
||||||
|
|
||||||
export const generateResetToken = (sub: string) => {
|
export const generateResetToken = (sub: string) => {
|
||||||
const key = crypto.randomBytes(16).toString('hex');
|
const key = crypto.randomBytes(16).toString('hex');
|
||||||
const token = sign({
|
const token = sign({
|
||||||
sub,
|
sub,
|
||||||
key,
|
key,
|
||||||
exp: (Date.now() + (24 * 60 * 60 * 1000)),
|
exp: Date.now() + 24 * 60 * 60 * 1000,
|
||||||
});
|
});
|
||||||
return { key, token };
|
return { key, token };
|
||||||
};
|
};
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -6,14 +6,12 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"lint": "tslint --project tsconfig.json --format stylish",
|
"lint": "tslint --project tsconfig.json --format stylish",
|
||||||
"prettier:fix": "prettier-eslint --eslint-config-path ./.eslintrc.js --write '**/*.ts'",
|
"prettier:fix": "prettier-eslint --write '**/*.ts'",
|
||||||
"serve": "ts-node src/server.ts",
|
"serve": "ts-node src/server.ts",
|
||||||
"start": "nodemon"
|
"start": "nodemon"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node16": "^1.0.3",
|
"@tsconfig/node16": "^1.0.3",
|
||||||
"@types/bcrypt": "^5.0.0",
|
|
||||||
"@types/crypto-js": "^4.1.1",
|
|
||||||
"@types/dotenv": "^8.2.0",
|
"@types/dotenv": "^8.2.0",
|
||||||
"@types/http-status-codes": "^1.2.0",
|
"@types/http-status-codes": "^1.2.0",
|
||||||
"@types/jsonwebtoken": "^9.0.1",
|
"@types/jsonwebtoken": "^9.0.1",
|
||||||
@@ -32,6 +30,12 @@
|
|||||||
"@types/passport-google-oauth": "^1.0.42",
|
"@types/passport-google-oauth": "^1.0.42",
|
||||||
"@types/passport-jwt": "^3.0.8",
|
"@types/passport-jwt": "^3.0.8",
|
||||||
"@types/passport-local": "^1.0.35",
|
"@types/passport-local": "^1.0.35",
|
||||||
|
"eslint": "^8.13.0",
|
||||||
|
"eslint-config-semistandard": "latest",
|
||||||
|
"eslint-config-standard": "^17.0.0",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-n": "^15.0.0",
|
||||||
|
"eslint-plugin-promise": "^6.0.0",
|
||||||
"jest": "^29.4.2",
|
"jest": "^29.4.2",
|
||||||
"nodemon": "^2.0.20",
|
"nodemon": "^2.0.20",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^2.8.4",
|
||||||
@@ -45,8 +49,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/server": "^7.2.0",
|
"@simplewebauthn/server": "^7.2.0",
|
||||||
"bcrypt": "^5.1.0",
|
|
||||||
"crypto": "^1.0.1",
|
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"http-status-codes": "^2.2.0",
|
"http-status-codes": "^2.2.0",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user