Package breakdown - initial commit 1.0.0
This commit is contained in:
32
lib/app.ts
Normal file
32
lib/app.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import Koa from 'koa';
|
||||
import bodyparser from 'koa-bodyparser';
|
||||
import cookie from 'koa-cookie';
|
||||
import session from 'koa-session';
|
||||
|
||||
import passport from './passport';
|
||||
import { performanceLogger, performanceTimer } from './middleware/performance';
|
||||
import { errorHandler } from './middleware/errorHandler';
|
||||
import { authRouter } from './controllers/auth';
|
||||
import { SESSION_KEY } from '../constants/env';
|
||||
|
||||
const app: Koa = new Koa();
|
||||
|
||||
app.use(errorHandler);
|
||||
app.use(performanceTimer);
|
||||
app.use(performanceLogger);
|
||||
app.use(bodyparser());
|
||||
app.use(cookie());
|
||||
|
||||
app.keys = [SESSION_KEY];
|
||||
app.use(session({}, app));
|
||||
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
app.use(authRouter.routes());
|
||||
app.use(authRouter.allowedMethods());
|
||||
|
||||
// Application error logging.
|
||||
app.on('error', console.error);
|
||||
|
||||
export default app;
|
||||
9
lib/constants/action.ts
Normal file
9
lib/constants/action.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export enum Action {
|
||||
AUTHENTICATE = 'AUTHENTICATE',
|
||||
AUTHENTICATE_FAILURE = 'AUTHENTICATE_FAILURE',
|
||||
CREATE = 'CREATE',
|
||||
DELETE = 'DELETE',
|
||||
RESET = 'RESET',
|
||||
RESET_REQUEST = 'RESET_REQUEST',
|
||||
UPDATE = 'UPDATE',
|
||||
}
|
||||
8
lib/constants/auth.ts
Normal file
8
lib/constants/auth.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export enum Status {
|
||||
ACTIVE,
|
||||
BLOCK_HARD,
|
||||
BLOCK_SOFT,
|
||||
DELETED,
|
||||
INACTIVE,
|
||||
UNVERIFIED,
|
||||
}
|
||||
10
lib/constants/db.ts
Normal file
10
lib/constants/db.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const DB_HOST = process.env.DB_HOST;
|
||||
export const DB_PORT = process.env.DB_PORT || 27017;
|
||||
export const DB_USERNAME = process.env.DB_USERNAME;
|
||||
export const DB_PASSWORD = process.env.DB_PASSWORD;
|
||||
export const DB_NAME = process.env.DB_NAME;
|
||||
|
||||
export const COLL_AUTH = 'Auth';
|
||||
export const COLL_LOG = 'Log';
|
||||
export const COLL_STRATEGY = 'Strategy';
|
||||
export const COLL_TOKEN = 'Token';
|
||||
20
lib/constants/env.ts
Normal file
20
lib/constants/env.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const PACKAGE_NAME = '@mifi/auth';
|
||||
export const PORT = process.env.PORT || 9000;
|
||||
|
||||
export const SESSION_KEY = process.env.SESSION_KEY || 'secret-key';
|
||||
|
||||
export const JWT_AUDIENCE = process.env.JWT_AUDIENCE || 'mifi.dev';
|
||||
export const JWT_ISSUER = process.env.JWT_ISSUER || PACKAGE_NAME;
|
||||
export const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
||||
|
||||
export const LOGIN_VALID_TIMEOUT = process.env.LOGIN_VALID_TIMEOUT || '12h'; // ###d|h|m
|
||||
export const RESET_VALID_TIMEOUT = process.env.RESET_VALID_TIMEOUT || '15m'; // ###d|h|m
|
||||
export const VERIFY_VALID_TIMEOUT = process.env.VERIFY_VALID_TIMEOUT || '60d'; // ###d|h|m
|
||||
export const DEFAULT_TOKEN_DAYS = process.env.DEFAULT_TOKEN_DAYS || 365;
|
||||
|
||||
export const ROUTE_PREFIX = process.env.ROUTE_PREFIX || '/auth';
|
||||
export const LOGIN_ROUTE = process.env.LOGIN_ROUTE || '/login';
|
||||
export const RESET_ROUTE = process.env.RESET_ROUTE || '/reset';
|
||||
export const VERIFICATION_ROUTE = process.env.VERIFICATION_ROUTE || '/verification';
|
||||
|
||||
export const REQUIRE_VERIFICATION = process.env.REQUIRE_VERIFICATION || true;
|
||||
12
lib/constants/errors.ts
Normal file
12
lib/constants/errors.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export enum ErrorCodes {
|
||||
RESET_REQUEST_DATA = 'RESET_REQUEST_DATA',
|
||||
}
|
||||
|
||||
export const ErrorMessages = {
|
||||
[ErrorCodes.RESET_REQUEST_DATA]: 'A valid username and password must be provided',
|
||||
};
|
||||
|
||||
export const getErrorBody = (code: ErrorCodes) => ({
|
||||
code,
|
||||
message: ErrorMessages[code],
|
||||
});
|
||||
7
lib/constants/strategies.ts
Normal file
7
lib/constants/strategies.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum STRATEGIES {
|
||||
LOCAL,
|
||||
APPLE,
|
||||
FACEBOOK,
|
||||
FIDO2,
|
||||
GOOGLE,
|
||||
}
|
||||
4
lib/constants/tokens.ts
Normal file
4
lib/constants/tokens.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum TokenType {
|
||||
RESET = 'RESET',
|
||||
VERIFICATION = 'VERIFICATION',
|
||||
}
|
||||
80
lib/controllers/auth.ts
Normal file
80
lib/controllers/auth.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
import Koa from 'koa';
|
||||
import Router from 'koa-router';
|
||||
import { StringSchemaDefinition } from 'mongoose';
|
||||
|
||||
import { Auth } from '@mifi/services-common/lib/db';
|
||||
import { create } from '@mifi/services-common/lib/db/dao/create';
|
||||
import { resetPasswordPost } from '@mifi/services-common/lib/db/api/resetPasswordPost';
|
||||
import { resetPasswordGet } from '@mifi/services-common/lib/db/api/resetPasswordGet';
|
||||
import { deleteById } from '@mifi/services-common/lib/db/dao/deleteById';
|
||||
import { deleteStrategy } from '@mifi/services-common/lib/db/api/deleteStrategy';
|
||||
import { AuthDocument } from '@mifi/services-common/lib/db/schema/auth';
|
||||
|
||||
import { ROUTE_PREFIX as prefix, RESET_ROUTE } from '../constants/env';
|
||||
import passport from '../passport';
|
||||
import { ErrorCodes, getErrorBody } from '../constants/errors';
|
||||
import { authenticated } from '../middleware/authenication';
|
||||
|
||||
const routerOpts: Router.IRouterOptions = { prefix };
|
||||
const router: Router = new Router(routerOpts);
|
||||
|
||||
router.get('/info', (ctx) => {
|
||||
ctx.body = {
|
||||
service: process.env.SERVICE_NAME,
|
||||
};
|
||||
});
|
||||
|
||||
router.post('/', async (ctx) => {
|
||||
console.log('POST: /auth [ctx]', ctx);
|
||||
const data = await create(<AuthDocument & { password: string }>ctx.request.body).catch((err) =>
|
||||
console.error('POST: /auth [err]', err),
|
||||
);
|
||||
console.log('POST: /auth [data]', data);
|
||||
ctx.body = { success: !!data, data };
|
||||
});
|
||||
|
||||
router.delete('/strategy/:id', async (ctx) => {
|
||||
ctx.body = { success: await deleteStrategy(ctx.params.id as StringSchemaDefinition) };
|
||||
});
|
||||
|
||||
router.delete('/:id', async (ctx) => {
|
||||
ctx.body = { success: await deleteById(ctx.params.id as StringSchemaDefinition) };
|
||||
});
|
||||
|
||||
router.post('/login', async (ctx, next) => {
|
||||
return passport.authenticate('local', (err, user) => {
|
||||
ctx.body = user;
|
||||
return user ? ctx.login(user) : ctx.throw(StatusCodes.UNAUTHORIZED);
|
||||
})(ctx, next);
|
||||
});
|
||||
|
||||
router.post(process.env.RESET_ROUTE || RESET_ROUTE, async (ctx) => {
|
||||
const { password, token, username } = ctx.request.body as { token?: string; password?: string; username?: string };
|
||||
let response: false | { record: StringSchemaDefinition; token: string } = false;
|
||||
|
||||
if (username) {
|
||||
response = await resetPasswordGet(username);
|
||||
} else if (token && password) {
|
||||
response = await resetPasswordPost(token, password);
|
||||
}
|
||||
|
||||
ctx.body = { success: !!response, ...(response || getErrorBody(ErrorCodes.RESET_REQUEST_DATA)) };
|
||||
|
||||
if (!response) {
|
||||
ctx.throw(StatusCodes.BAD_REQUEST);
|
||||
}
|
||||
});
|
||||
|
||||
router.patch('/:record', authenticated(), (ctx: Koa.Context) => {
|
||||
if (ctx.user !== ctx.param.record) {
|
||||
ctx.throw(StatusCodes.UNAUTHORIZED);
|
||||
}
|
||||
const data = Auth.findOneAndUpdate({ record: ctx.params.record });
|
||||
if (!data) {
|
||||
ctx.throw(StatusCodes.NOT_FOUND);
|
||||
}
|
||||
ctx.body = { success: true, data };
|
||||
});
|
||||
|
||||
export { router as authRouter };
|
||||
11
lib/index.ts
Normal file
11
lib/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import app from './app';
|
||||
import { connection } from '../db';
|
||||
import { PORT } from '../constants/env';
|
||||
|
||||
connection.then(
|
||||
() => {
|
||||
app.listen(PORT);
|
||||
console.debug('Server up and listening', { env: process.env });
|
||||
},
|
||||
(err) => console.error('Could not reach database', { err, env: process.env }),
|
||||
);
|
||||
13
lib/middleware/authenication.ts
Normal file
13
lib/middleware/authenication.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Middleware } from 'koa';
|
||||
|
||||
import { LOGIN_ROUTE } from '../constants/env';
|
||||
|
||||
export const authenticated = (): Middleware => {
|
||||
return (ctx, next) => {
|
||||
if (ctx.isAuthenticated()) {
|
||||
return next();
|
||||
} else {
|
||||
ctx.redirect(process.env.LOGIN_ROUTE || LOGIN_ROUTE);
|
||||
}
|
||||
};
|
||||
};
|
||||
13
lib/middleware/errorHandler.ts
Normal file
13
lib/middleware/errorHandler.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
import { Context, Next } from 'koa';
|
||||
|
||||
export const errorHandler = async (ctx: Context, next: Next) => {
|
||||
try {
|
||||
await next();
|
||||
} catch (error: any) {
|
||||
ctx.status = error.statusCode || error.status || StatusCodes.INTERNAL_SERVER_ERROR;
|
||||
error.status = ctx.status;
|
||||
ctx.body = { error };
|
||||
ctx.app.emit('error', error, ctx);
|
||||
}
|
||||
};
|
||||
14
lib/middleware/performance.ts
Normal file
14
lib/middleware/performance.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Context, Next } from 'koa';
|
||||
|
||||
export const performanceLogger = async (ctx: Context, next: Next) => {
|
||||
await next();
|
||||
const rt = ctx.response.get('X-Response-Time');
|
||||
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
|
||||
};
|
||||
|
||||
export const performanceTimer = async (ctx: Context, next: Next) => {
|
||||
const start = Date.now();
|
||||
await next();
|
||||
const ms = Date.now() - start;
|
||||
ctx.set('X-Response-Time', `${ms}ms`);
|
||||
};
|
||||
23
lib/passport/index.ts
Normal file
23
lib/passport/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import passport from 'koa-passport';
|
||||
import { Types } from 'mongoose';
|
||||
|
||||
import { AuthDocument } from '@mifi/services-common/lib/db/schema/auth';
|
||||
import { readOneByRecord } from '@mifi/services-common/lib/db/dao/readOneByRecord';
|
||||
import { readOneById } from '@mifi/services-common/lib/db/dao/readOneById';
|
||||
|
||||
import LocalStrategy from './strategies/local';
|
||||
import JwtStrategy from './strategies/jwt';
|
||||
|
||||
passport.use(LocalStrategy);
|
||||
passport.use(JwtStrategy);
|
||||
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, (user as AuthDocument).record || (user as AuthDocument).id);
|
||||
});
|
||||
|
||||
passport.deserializeUser(async (id, done) => {
|
||||
const user = await readOneByRecord(<Types.ObjectId>id).catch(async () => await readOneById(<Types.ObjectId>id));
|
||||
done(user ? null : 'user not found', user);
|
||||
});
|
||||
|
||||
export default passport;
|
||||
17
lib/passport/strategies/jwt.ts
Normal file
17
lib/passport/strategies/jwt.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ExtractJwt, Strategy as JwtStrategy } from 'passport-jwt';
|
||||
|
||||
import { readOneByRecord } from '@mifi/services-common/lib/db/dao/readOneByRecord';
|
||||
|
||||
import { JWT_SECRET } from '../../constants/env';
|
||||
|
||||
const opts = {
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: JWT_SECRET,
|
||||
issuer: process.env.JWT_ISSUER,
|
||||
audience: process.env.JWT_AUDIENCE,
|
||||
};
|
||||
|
||||
export default new JwtStrategy(opts, async ({ sub }, done) => {
|
||||
const auth = await readOneByRecord(sub);
|
||||
return done(null, auth || false);
|
||||
});
|
||||
9
lib/passport/strategies/local.ts
Normal file
9
lib/passport/strategies/local.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// eslint-disable-next-line import/named
|
||||
import { Strategy as LocalStrategy } from 'passport-local';
|
||||
|
||||
import { authenticate } from '@mifi/services-common/lib/db/api/authenticate';
|
||||
|
||||
export default new LocalStrategy(async (username: string, password: string, done: any) => {
|
||||
const user = await authenticate(username, password);
|
||||
done(null, user);
|
||||
});
|
||||
35
lib/utils/jwt.ts
Normal file
35
lib/utils/jwt.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { JWT_AUDIENCE, JWT_ISSUER, JWT_SECRET } from '../constants/env';
|
||||
export interface TokenProps {
|
||||
aud?: string;
|
||||
exp?: number | Date;
|
||||
iss?: string;
|
||||
sub: string | null;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type SignProps = string | TokenProps | void;
|
||||
|
||||
export const sign = (props: SignProps) => {
|
||||
const today = new Date();
|
||||
const { sub = null, ...rest }: TokenProps =
|
||||
typeof props === 'string' || typeof props === 'undefined' ? { sub: props || null } : props;
|
||||
let { exp } = rest;
|
||||
if (!exp) {
|
||||
exp = new Date(today);
|
||||
exp.setDate(today.getDate() + parseInt(process.env.JWT_DAYS_VALID as string));
|
||||
exp = exp.getTime() / 1000;
|
||||
}
|
||||
return jwt.sign(
|
||||
{
|
||||
exp,
|
||||
sub,
|
||||
aud: rest.aud || JWT_AUDIENCE,
|
||||
iat: today.getTime(),
|
||||
iss: rest.iss || JWT_ISSUER,
|
||||
},
|
||||
JWT_SECRET,
|
||||
);
|
||||
};
|
||||
|
||||
export const verify = (token: string) => jwt.verify(token, JWT_SECRET);
|
||||
5
lib/utils/links.ts
Normal file
5
lib/utils/links.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { RESET_ROUTE, ROUTE_PREFIX, VERIFICATION_ROUTE } from '../constants/env';
|
||||
|
||||
export const getPasswordResetPath = (token: string) => `${ROUTE_PREFIX}${RESET_ROUTE}?t=${token}`;
|
||||
|
||||
export const getVerificationPath = (token: string) => `${ROUTE_PREFIX}${VERIFICATION_ROUTE}?t=${token}`;
|
||||
13
lib/utils/parseTimeoutToMs.ts
Normal file
13
lib/utils/parseTimeoutToMs.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export const parseTimeoutToMs = (timeout: string) => {
|
||||
const match = timeout.match(/(?<number>\d+)(?<unit>d|h|m)/gi)?.groups || {};
|
||||
const { number, unit } = match;
|
||||
switch (unit) {
|
||||
case 'd':
|
||||
return 1000 * 60 * 60 * 24 * parseInt(number);
|
||||
case 'h':
|
||||
return 1000 * 60 * 60 * parseInt(number);
|
||||
case 'm':
|
||||
default:
|
||||
return 1000 * 60 * parseInt(number) || 1;
|
||||
}
|
||||
};
|
||||
12
lib/utils/password.ts
Normal file
12
lib/utils/password.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { pbkdf2Sync, randomBytes } from 'crypto';
|
||||
|
||||
export const encrypt = (password: string) => {
|
||||
const salt = randomBytes(16).toString('hex');
|
||||
const hash = pbkdf2Sync(password, salt, 10000, 512, 'sha512').toString('hex');
|
||||
return `${salt}:${hash}`;
|
||||
};
|
||||
|
||||
export const verify = (test: string, secret: string) => {
|
||||
const [salt, hash] = secret.split(':');
|
||||
return pbkdf2Sync(test, salt, 10000, 512, 'sha512').toString('hex') === hash;
|
||||
};
|
||||
11
lib/utils/tokens.ts
Normal file
11
lib/utils/tokens.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { sign } from './jwt';
|
||||
import { LOGIN_VALID_TIMEOUT } from '../constants/env';
|
||||
import { Status } from '../constants/auth';
|
||||
import { parseTimeoutToMs } from './parseTimeoutToMs';
|
||||
|
||||
export const generateLoginToken = (sub: string, status: Status) =>
|
||||
sign({
|
||||
sub,
|
||||
status,
|
||||
exp: Date.now() + parseTimeoutToMs(LOGIN_VALID_TIMEOUT),
|
||||
});
|
||||
Reference in New Issue
Block a user