From dc72cefece72f7bc9a6fa07444343a9e40b8942c Mon Sep 17 00:00:00 2001 From: mifi Date: Wed, 3 May 2023 11:12:59 -0400 Subject: [PATCH] Reorganizing --- lib/constants/auth.ts | 8 +++ lib/constants/constants.ts | 15 ++++++ lib/constants/defaults.ts | 6 --- lib/constants/errors.ts | 12 +++++ lib/controllers/auth.ts | 40 --------------- lib/{ => db}/model/auth.ts | 0 lib/{ => db}/schema/auth.ts | 35 ++++++++----- lib/{ => db}/schema/strategy.ts | 2 +- lib/{ => server}/app.ts | 4 ++ lib/server/controllers/auth.ts | 50 +++++++++++++++++++ .../database/database.connection.ts | 0 lib/server/middleware/authenication.ts | 12 +++++ lib/{ => server}/middleware/errorHandler.ts | 0 .../middleware/jwt.ts} | 0 lib/{ => server}/middleware/performance.ts | 0 lib/{ => server}/passport/index.ts | 4 +- lib/{ => server}/passport/strategies/jwt.ts | 4 +- lib/{ => server}/passport/strategies/local.ts | 3 +- lib/{ => server}/server.ts | 4 +- lib/utils/auth.ts | 8 +-- lib/utils/jwt.ts | 12 ++--- lib/utils/links.ts | 8 +-- lib/utils/tokens.ts | 23 ++++++++- 23 files changed, 163 insertions(+), 87 deletions(-) create mode 100644 lib/constants/auth.ts create mode 100644 lib/constants/constants.ts delete mode 100644 lib/constants/defaults.ts create mode 100644 lib/constants/errors.ts delete mode 100644 lib/controllers/auth.ts rename lib/{ => db}/model/auth.ts (100%) rename lib/{ => db}/schema/auth.ts (79%) rename lib/{ => db}/schema/strategy.ts (91%) rename lib/{ => server}/app.ts (85%) create mode 100644 lib/server/controllers/auth.ts rename lib/{ => server}/database/database.connection.ts (100%) create mode 100644 lib/server/middleware/authenication.ts rename lib/{ => server}/middleware/errorHandler.ts (100%) rename lib/{middleware/authenication.ts => server/middleware/jwt.ts} (100%) rename lib/{ => server}/middleware/performance.ts (100%) rename lib/{ => server}/passport/index.ts (83%) rename lib/{ => server}/passport/strategies/jwt.ts (85%) rename lib/{ => server}/passport/strategies/local.ts (85%) rename lib/{ => server}/server.ts (64%) diff --git a/lib/constants/auth.ts b/lib/constants/auth.ts new file mode 100644 index 0000000..9e2094c --- /dev/null +++ b/lib/constants/auth.ts @@ -0,0 +1,8 @@ +export enum Status { + ACTIVE, + BLOCK_HARD, + BLOCK_SOFT, + DELETED, + INACTIVE, + UNVERIFIED, +} diff --git a/lib/constants/constants.ts b/lib/constants/constants.ts new file mode 100644 index 0000000..66f8ab3 --- /dev/null +++ b/lib/constants/constants.ts @@ -0,0 +1,15 @@ + +export const PACKAGE_NAME = '@mifi/latch'; +export const PORT = process.env.PORT || 9000; + +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_TIME = process.env.LOGIN_VALID_TIME || '12H'; // ###D|H|M +export const RESET_VALID_MINUTES = process.env.RESET_VALID_MINUTES || 24; +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'; diff --git a/lib/constants/defaults.ts b/lib/constants/defaults.ts deleted file mode 100644 index c730d8b..0000000 --- a/lib/constants/defaults.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const PORT = 9000; -export const API_PATH = '/api'; -export const AUTH_ROUTE = '/auth'; -export const RESET_ROUTE = `${AUTH_ROUTE}/reset`; - -export const JWT_SECRET = 'secret'; diff --git a/lib/constants/errors.ts b/lib/constants/errors.ts new file mode 100644 index 0000000..8761979 --- /dev/null +++ b/lib/constants/errors.ts @@ -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], +}); diff --git a/lib/controllers/auth.ts b/lib/controllers/auth.ts deleted file mode 100644 index 8e59cc5..0000000 --- a/lib/controllers/auth.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Koa from 'koa'; -import Router from 'koa-router'; -import { StatusCodes } from 'http-status-codes'; -import { API_PATH } from '../constants/defaults'; - -import Auth from '../model/auth'; -import passport from '../passport'; -import { sign } from '../utils/jwt'; - -const routerOpts: Router.IRouterOptions = { - prefix: process.env.API_PATH || API_PATH, -}; - -const router: Router = new Router(routerOpts); - -router.post('/', async (ctx: Koa.Context) => { - const data = await Auth.create(ctx.body); - data.save(); - ctx.body = { success: true, data }; -}); - -router.post('/login', async (ctx: Koa.Context, next) => { - return passport.authenticate('local', (err, user) => { - if (user === false) { - ctx.body = { token: sign() }; - ctx.throw(StatusCodes.UNAUTHORIZED); - } - ctx.body = { token: sign(user) }; - return ctx.login(user); - })(ctx, next); - await next(); -}); - -router.patch('/:customer_id', async (ctx: Koa.Context) => { - const data = await Auth.findByIdAndUpdate(ctx.params.customer_id); - if (!data) { - ctx.throw(StatusCodes.NOT_FOUND); - } - ctx.body = { success: true, data }; -}); diff --git a/lib/model/auth.ts b/lib/db/model/auth.ts similarity index 100% rename from lib/model/auth.ts rename to lib/db/model/auth.ts diff --git a/lib/schema/auth.ts b/lib/db/schema/auth.ts similarity index 79% rename from lib/schema/auth.ts rename to lib/db/schema/auth.ts index 96ea824..074049b 100644 --- a/lib/schema/auth.ts +++ b/lib/db/schema/auth.ts @@ -2,11 +2,12 @@ import { JwtPayload } from 'jsonwebtoken'; import { InferSchemaType, Model, Schema, StringSchemaDefinition, Types } from 'mongoose'; import { Strategy } from './strategy'; -import { STRATEGIES } from '../constants/strategies'; -import { TokenProps, sign, verify as verifyJwt } from '../utils/jwt'; -import { encrypt, verify as verifyPassword } from '../utils/password'; -import { generateResetToken } from '../utils/tokens'; -import { getPasswordResetLink } from '../utils/links'; +import { STRATEGIES } from '../../constants/strategies'; +import { TokenProps, verify as verifyJwt } from '../../utils/jwt'; +import { encrypt, verify as verifyPassword } from '../../utils/password'; +import { generateLoginToken, generateResetToken } from '../../utils/tokens'; +import { getPasswordResetLink } from '../../utils/links'; +import { Status } from '../../constants/auth'; export type Auth = { is2FA?: boolean; @@ -15,6 +16,7 @@ export type Auth = { }; export type AuthPrivate = Auth & { + status: Status; strategies: Types.ArraySubdocument; }; @@ -24,15 +26,16 @@ export interface AuthMethods { getResetLink(route: string): Promise; getResetToken(): Promise; getToken(props?: Omit | void): string; + isActive(): boolean; setPassword(password: string): Promise; } export interface AuthModel extends Model { - authenticate(password: any): boolean; + authenticate(username: string, password?: string): string | false; findByUsername(username: string): Promise; isUsernameAvailable(username: string): Promise; findUserForReset(strategy: STRATEGIES, token: string): Promise; - resetPassword(token: string, password: string): Promise; + resetPassword(token: string, password: string): Promise; } export const AuthSchema = new Schema( @@ -40,6 +43,7 @@ export const AuthSchema = new Schema( is2FA: { type: Boolean, default: false }, record: { type: Types.ObjectId }, strategies: { type: Types.ArraySubdocument, required: true }, + status: { type: Number, enum: Object.values(Status), default: Status.UNVERIFIED }, username: { type: String, required: true, unique: true }, }, { @@ -59,10 +63,7 @@ AuthSchema.methods = { }, getToken(props = {}) { - return sign({ - sub: this._id, - ...props, - }); + return generateLoginToken(this._id, this.status); }, async getResetLink(route) { @@ -81,6 +82,10 @@ AuthSchema.methods = { return token; }, + isActive() { + return this.status === Status.ACTIVE; + }, + async setPassword(password) { const key = encrypt(password); const hasLocalStrategy = !!this.getAuthStrategy(STRATEGIES.LOCAL); @@ -107,7 +112,7 @@ AuthSchema.methods = { }; AuthSchema.statics = { - authenticateAndGetRecordLocator: async function (username, password) { + authenticate: async function (username, password) { const auth = await this.findByUsername(username); if (auth && auth.authenticate(password)) { return auth.record; @@ -130,7 +135,11 @@ AuthSchema.statics = { _id: sub, 'strategies.resetToken': key, }).catch(); - return !!auth && auth.setPassword(password); + if (auth) { + await auth.setPassword(password).catch(); + return auth.getToken(); + } + return false; }, }; diff --git a/lib/schema/strategy.ts b/lib/db/schema/strategy.ts similarity index 91% rename from lib/schema/strategy.ts rename to lib/db/schema/strategy.ts index 5a04818..65f143e 100644 --- a/lib/schema/strategy.ts +++ b/lib/db/schema/strategy.ts @@ -1,5 +1,5 @@ import { InferSchemaType, Schema, Types } from 'mongoose'; -import { STRATEGIES } from '../constants/strategies'; +import { STRATEGIES } from '../../constants/strategies'; export const Strategy = new Schema( { diff --git a/lib/app.ts b/lib/server/app.ts similarity index 85% rename from lib/app.ts rename to lib/server/app.ts index a8194af..ebe227e 100644 --- a/lib/app.ts +++ b/lib/server/app.ts @@ -6,6 +6,7 @@ import session from 'koa-session'; import passport from './passport'; import { performanceLogger, perfromanceTimer } from './middleware/performance'; import { errorHandler } from './middleware/errorHandler'; +import { authRouter } from './controllers/auth'; const app: Koa = new Koa(); @@ -21,6 +22,9 @@ 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); diff --git a/lib/server/controllers/auth.ts b/lib/server/controllers/auth.ts new file mode 100644 index 0000000..a943555 --- /dev/null +++ b/lib/server/controllers/auth.ts @@ -0,0 +1,50 @@ +import Koa from 'koa'; +import Router from 'koa-router'; +import { StatusCodes } from 'http-status-codes'; + +import { ROUTE_PREFIX as prefix, RESET_ROUTE } from '../../constants/constants'; +import Auth from '../../db/model/auth'; +import { sign } from '../../utils/jwt'; +import passport from '../passport'; +import { ErrorCodes, getErrorBody } from '../../constants/errors'; + +const routerOpts: Router.IRouterOptions = { prefix }; +const router: Router = new Router(routerOpts); + +router.post('/', async (ctx) => { + const data = (await Auth.create(ctx.body)).save(); + ctx.body = { success: true, data: { ...data, strategies: undefined } }; +}); + +router.post('/login', async (ctx, next) => { + return passport.authenticate('local', (err, user) => { + if (user === false) { + ctx.body = { token: null }; + ctx.throw(StatusCodes.UNAUTHORIZED); + } + ctx.body = { token: sign(user) }; + return ctx.login(user); + })(ctx, next); +}); + +router.post(process.env.RESET_ROUTE || RESET_ROUTE, async (ctx, next) => { + const { token = null, password = null } = ctx.request.body as { token?: string, password?: string }; + if (token && password) { + const loginToken = await Auth.resetPassword(token, password).catch(); + ctx.body({ token: loginToken }); + next(); + } + ctx.body = { success: false, ...getErrorBody(ErrorCodes.RESET_REQUEST_DATA) }; +}); + +router.patch('/:record', (ctx: Koa.Context) => { + const data = Auth.findOneAndUpdate( + { record: ctx.params.record }, + ); + if (!data) { + ctx.throw(StatusCodes.NOT_FOUND); + } + ctx.body = { success: true, data }; +}); + +export { router as authRouter }; \ No newline at end of file diff --git a/lib/database/database.connection.ts b/lib/server/database/database.connection.ts similarity index 100% rename from lib/database/database.connection.ts rename to lib/server/database/database.connection.ts diff --git a/lib/server/middleware/authenication.ts b/lib/server/middleware/authenication.ts new file mode 100644 index 0000000..93a02da --- /dev/null +++ b/lib/server/middleware/authenication.ts @@ -0,0 +1,12 @@ +import { Middleware } from 'koa'; +import { LOGIN_ROUTE } from '../../constants/constants'; + +export const authenticated = (): Middleware => { + return (ctx, next) => { + if (ctx.isAuthenticated()) { + return next(); + } else { + ctx.redirect(process.env.LOGIN_ROUTE || LOGIN_ROUTE); + } + }; +}; diff --git a/lib/middleware/errorHandler.ts b/lib/server/middleware/errorHandler.ts similarity index 100% rename from lib/middleware/errorHandler.ts rename to lib/server/middleware/errorHandler.ts diff --git a/lib/middleware/authenication.ts b/lib/server/middleware/jwt.ts similarity index 100% rename from lib/middleware/authenication.ts rename to lib/server/middleware/jwt.ts diff --git a/lib/middleware/performance.ts b/lib/server/middleware/performance.ts similarity index 100% rename from lib/middleware/performance.ts rename to lib/server/middleware/performance.ts diff --git a/lib/passport/index.ts b/lib/server/passport/index.ts similarity index 83% rename from lib/passport/index.ts rename to lib/server/passport/index.ts index 9e26329..38bc8c9 100644 --- a/lib/passport/index.ts +++ b/lib/server/passport/index.ts @@ -1,7 +1,7 @@ import passport from 'koa-passport'; -import Auth from '../model/auth'; -import { Auth as AuthRecord } from '../schema/auth'; +import Auth from '../../model/auth'; +import { Auth as AuthRecord } from '../../db/schema/auth'; import LocalStrategy from './strategies/local'; import JwtStrategy from './strategies/jwt'; diff --git a/lib/passport/strategies/jwt.ts b/lib/server/passport/strategies/jwt.ts similarity index 85% rename from lib/passport/strategies/jwt.ts rename to lib/server/passport/strategies/jwt.ts index 23dfc3b..6adefc1 100644 --- a/lib/passport/strategies/jwt.ts +++ b/lib/server/passport/strategies/jwt.ts @@ -1,8 +1,8 @@ // eslint-disable-next-line import/named import { ExtractJwt, Strategy as JwtStrategy } from 'passport-jwt'; -import Auth from '../../model/auth'; -import { getJwtSecret } from '../../utils/jwt'; +import Auth from '../../../model/auth'; +import { getJwtSecret } from '../../../utils/jwt'; const opts = { jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), diff --git a/lib/passport/strategies/local.ts b/lib/server/passport/strategies/local.ts similarity index 85% rename from lib/passport/strategies/local.ts rename to lib/server/passport/strategies/local.ts index e1f1353..9d8c969 100644 --- a/lib/passport/strategies/local.ts +++ b/lib/server/passport/strategies/local.ts @@ -1,8 +1,7 @@ -import passport from 'koa-passport'; // eslint-disable-next-line import/named import { Strategy as LocalStrategy } from 'passport-local'; -import Auth from '../../model/auth'; +import Auth from '../../../model/auth'; export default new LocalStrategy(async (username: string, password: string, done: any) => { const user = await Auth.findOne({ diff --git a/lib/server.ts b/lib/server/server.ts similarity index 64% rename from lib/server.ts rename to lib/server/server.ts index 2c57423..2599b93 100644 --- a/lib/server.ts +++ b/lib/server/server.ts @@ -2,12 +2,10 @@ import dotenv from 'dotenv'; import app from './app'; import { connection } from './database/database.connection'; -import { PORT as DEFAULT_PORT } from './constants/defaults'; +import { PORT } from '../constants/constants'; dotenv.config(); -const PORT: number = Number(process.env.PORT) || DEFAULT_PORT; - connection.then( () => app.listen(PORT), (err) => console.error('ERROR!', err), diff --git a/lib/utils/auth.ts b/lib/utils/auth.ts index 0cd5b95..aeccb3c 100644 --- a/lib/utils/auth.ts +++ b/lib/utils/auth.ts @@ -1,11 +1,11 @@ -import Auth from '../model/auth'; -import { AuthModel, AuthPrivate } from '../schema/auth'; +import Auth from '../db/model/auth'; +import { AuthModel, AuthPrivate } from '../db/schema/auth'; import { sign } from './jwt'; export const getAuthenticationBundle = async (username: string, password: string) => { const auth = await Auth.findByUsername(username).catch(); - const isAuthenticated = !!auth && (auth as AuthModel).authenticate(password); - const record = isAuthenticated ? ((auth as AuthPrivate).record as string) : null; + const isAuthenticated = !!auth && (auth).authenticate(password); + const record = isAuthenticated ? (auth).record : null; const token = sign(record || undefined); return { record, diff --git a/lib/utils/jwt.ts b/lib/utils/jwt.ts index 140e218..d4c4ee5 100644 --- a/lib/utils/jwt.ts +++ b/lib/utils/jwt.ts @@ -1,7 +1,5 @@ import jwt from 'jsonwebtoken'; -import { JWT_SECRET } from '../constants/defaults'; - -export const getJwtSecret = () => process.env.JWT_SECRET || JWT_SECRET; +import { JWT_AUDIENCE, JWT_ISSUER, JWT_SECRET } from '../constants/constants'; export interface TokenProps { aud?: string; exp?: number | Date; @@ -26,12 +24,12 @@ export const sign = (props: SignProps) => { { exp, sub, - aud: rest.aud || process.env.JWT_AUDIENCE, + aud: rest.aud || JWT_AUDIENCE, iat: today.getTime(), - iss: rest.iss || process.env.JWT_ISSUER, + iss: rest.iss || JWT_ISSUER, }, - getJwtSecret(), + JWT_SECRET, ); }; -export const verify = (token: string) => jwt.verify(token, getJwtSecret()); +export const verify = (token: string) => jwt.verify(token, JWT_SECRET); diff --git a/lib/utils/links.ts b/lib/utils/links.ts index e1875e5..01e87a7 100644 --- a/lib/utils/links.ts +++ b/lib/utils/links.ts @@ -1,7 +1,3 @@ -import { API_PATH, PORT, RESET_ROUTE } from '../constants/defaults'; +import { RESET_ROUTE, ROUTE_PREFIX } from '../constants/constants'; -export const getPasswordResetLink = (token: string) => { - const hostname = process.env.HOST_NAME || `localhost:${process.env.PORT || PORT}`; - const path = `${process.env.API_PATH || API_PATH}${process.env.RESET_ROUTE || RESET_ROUTE}`; - return `https://${hostname}${path}?t=${token}`; -}; +export const getPasswordResetPath = (token: string) => `${ROUTE_PREFIX}${RESET_ROUTE}?t=${token}`; diff --git a/lib/utils/tokens.ts b/lib/utils/tokens.ts index 2163efb..c5446b3 100644 --- a/lib/utils/tokens.ts +++ b/lib/utils/tokens.ts @@ -1,13 +1,34 @@ import crypto from 'crypto'; import { sign } from './jwt'; +import { LOGIN_VALID_TIME, RESET_VALID_MINUTES } from '../constants/constants'; +import { Status } from '../constants/auth'; + +const parseLoginValid = () => { + const [number, unit] = process.env.LOGIN_VALID_TIME || LOGIN_VALID_TIME; + return [ + unit === 'd' ? parseInt(number) : 1, + unit === 'h' ? parseInt(number) : (unit === 'm' && 1) || 24, + unit === 'm' ? parseInt(number) : 60, + ]; +}; + +export const generateLoginToken = (sub: string, status: Status) => { + const [days, hours, mins] = parseLoginValid(); + return sign({ + sub, + status, + exp: Date.now() + days * hours * mins * 60 * 1000, + }); +}; export const generateResetToken = (sub: string) => { + const hoursValid = (process.env.RESET_VALID_HOURS || RESET_VALID_MINUTES); const key = crypto.randomBytes(16).toString('hex'); const token = sign({ sub, key, - exp: Date.now() + 24 * 60 * 60 * 1000, + exp: Date.now() + hoursValid * 60 * 60 * 1000, }); return { key, token }; };