diff --git a/src/index.ts b/src/index.ts index ba664cd..a50c072 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,8 @@ import fileUpload from 'express-fileupload'; import { PrismaClient } from '@prisma/client'; import * as eta from 'eta'; import bodyParser from 'body-parser'; +import session from 'express-session'; +import passport from 'passport'; // Sentry import * as Sentry from '@sentry/node'; @@ -28,6 +30,7 @@ export const log = { core: coreLogger, db: coreLogger.scope('DB'), web: coreLogger.scope('WEB'), + auth: coreLogger.scope('AUTH'), helper: coreLogger.scope('HELPER') }; @@ -37,9 +40,22 @@ export const config = new ConfigHandler(__path + '/config.json', { http_listen_address: '127.0.0.1', http_port: 3000, sentry_dsn: 'https://ID@sentry.example.com/PROJECTID', - debug: false + debug: false, + auth: { + local: { + active: true, + users: { + user: 'password', + user1: 'password' + } + }, + oidc: { + active: false + } + } }); +// TODO: Add errorhandling with some sort of message. export const prisma = new PrismaClient({ datasources: { db: { @@ -67,16 +83,16 @@ Sentry.init({ environment: config.global.debug ? 'development' : 'production' }); -app.locals.versionRevLong = require('child_process') -.execSync('git rev-parse HEAD') -.toString().trim() -app.locals.versionRev = require('child_process') -.execSync('git rev-parse --short HEAD') -.toString().trim() -app.locals.versionRevLatest = require('child_process') -.execSync('git ls-remote --refs -q') -.toString().trim().split("\t")[0] -log.core.info(`Running revision ${app.locals.versionRevLong} (${app.locals.versionRevLatest} latest)`); +// TODO: Version check need to be rewritten. +app.locals.versionRevLong = require('child_process').execSync('git rev-parse HEAD').toString().trim(); +app.locals.versionRev = require('child_process').execSync('git rev-parse --short HEAD').toString().trim(); +app.locals.versionRevLatest = require('child_process').execSync('git ls-remote --refs -q').toString().trim().split('\t')[0]; + +if (app.locals.versionRevLong === app.locals.versionRevLatest) { + log.core.info(`Running Latest Version (${app.locals.versionRevLong})`); +} else { + log.core.info(`Running Version: ${app.locals.versionRevLong} (Latest: ${app.locals.versionRevLatest})`); +} // RequestHandler creates a separate execution context using domains, so that every // transaction/span/breadcrumb is attached to its own Hub instance @@ -93,14 +109,23 @@ app.use(bodyParser.urlencoded({ extended: false })); // Using bodyParser to parse JSON bodies into JS objects app.use(bodyParser.json()); +// Session store +// TODO: Move secret to config -> Autogenerate. +app.use( + session({ + secret: 'keyboard cat', + resave: false, + saveUninitialized: false, + cookie: { secure: false } + }) +); +app.use(passport.authenticate('session')); + app.use(fileUpload()); app.use(express.static(__path + '/static')); app.use(routes); -// The error handler must be before any other error middleware and after all controllers -app.use(Sentry.Handlers.errorHandler()); - app.listen(config.global.http_port, config.global.http_listen_address, () => { log.web.info(`Listening at http://${config.global.http_listen_address}:${config.global.http_port}`); }); diff --git a/src/middleware/auth.mw.ts b/src/middleware/auth.mw.ts new file mode 100644 index 0000000..6e9cd26 --- /dev/null +++ b/src/middleware/auth.mw.ts @@ -0,0 +1,23 @@ +/* +function checkAuthentication(req: any, res: any, next: Function) { + if (req.isAuthenticated()) { + //req.isAuthenticated() will return true if user is logged in + next(); + } else { + res.redirect('/auth/login'); + } +} + +const checkIsInRole = (...roles) => (req, res, next) => { + if (!req.user) { + return res.redirect('/login') + } + + const hasRole = roles.find(role => req.user.role === role) + if (!hasRole) { + return res.redirect('/login') + } + + return next() +} +*/ diff --git a/src/routes/api/v1/index.ts b/src/routes/api/v1/index.ts index b978e1d..3e1943a 100644 --- a/src/routes/api/v1/index.ts +++ b/src/routes/api/v1/index.ts @@ -1,4 +1,5 @@ import express from 'express'; +import passport from 'passport'; // Route imports import testRoute from './test.js'; diff --git a/src/routes/auth/index.ts b/src/routes/auth/index.ts new file mode 100644 index 0000000..26dc027 --- /dev/null +++ b/src/routes/auth/index.ts @@ -0,0 +1,96 @@ +import passport from 'passport'; +import { Strategy as LocalStrategy } from 'passport-local'; +import express, { Request, Response } from 'express'; +import { prisma, __path, log, config, app } from '../../index.js'; + +/* Configure password authentication strategy. + * + * The `LocalStrategy` authenticates users by verifying a username and password. + * The strategy parses the username and password from the request and calls the + * `verify` function. + * + * The `verify` function queries the database for the user record and verifies + * the password by hashing the password supplied by the user and comparing it to + * the hashed password stored in the database. If the comparison succeeds, the + * user is authenticated; otherwise, not. + */ +passport.use( + new LocalStrategy(function verify(username, password, cb) { + //log.auth.debug('LocalStrategy:', username, password); + + for (const [user, pass] of Object.entries(config.global.auth.local.users)) { + //log.auth.debug('Loop(REQ):', username, password); + //log.auth.debug('Loop(CFG):', user, pass); + + if (user === username && pass === password) { + log.auth.debug('LocalStrategy: success'); + return cb(null, { username: username }); // This is the user object. + } + } + log.auth.debug('LocalStrategy: failed'); + return cb(null, false, { message: 'Incorrect username or password.' }); + }) + /* + 1. If the user not found in DB, + done (null, false) + + 2. If the user found in DB, but password does not match, + done (null, false) + + 3. If user found in DB and password match, + done (null, {authenticated_user}) + */ +); + +/* Configure session management. + * + * When a login session is established, information about the user will be + * stored in the session. This information is supplied by the `serializeUser` + * function, which is yielding the user ID and username. + * + * As the user interacts with the app, subsequent requests will be authenticated + * by verifying the session. The same user information that was serialized at + * session establishment will be restored when the session is authenticated by + * the `deserializeUser` function. + * + */ +passport.serializeUser(function (user: any, cb) { + process.nextTick(function () { + log.auth.debug('Called seriealizeUser'); + log.auth.debug('user:', user); + return cb(null, { + username: user.username + }); + }); +}); + +passport.deserializeUser(function (user, cb) { + process.nextTick(function () { + log.auth.debug('Called deseriealizeUser'); + return cb(null, user); + }); +}); + +// Route imports +import testRoute from './test.js'; +import loginRoute from './login.js'; +//import logoutRoute from './login.js' + +// Router base is '/auth' +const Router = express.Router({ strict: false }); + +Router.route('/login').get(loginRoute.get); +Router.route('/login').post(passport.authenticate('local', { successRedirect: '/', failureRedirect: '/auth/login?failed' })); + +Router.route('/test').get(checkAuthentication, testRoute.get); + +export default Router; + +function checkAuthentication(req: Request, res: Response, next: Function) { + if (req.isAuthenticated()) { + //req.isAuthenticated() will return true if user is logged in + next(); + } else { + res.redirect('/auth/login'); + } +} diff --git a/src/routes/auth/login.ts b/src/routes/auth/login.ts new file mode 100644 index 0000000..0007b26 --- /dev/null +++ b/src/routes/auth/login.ts @@ -0,0 +1,10 @@ +import passport from 'passport'; + +import express, { Request, Response } from 'express'; +import { prisma, __path, log } from '../../index.js'; + +function get(req: Request, res: Response) { + res.render(__path + '/src/frontend/auth/login.eta.html'); //, { items: items }); +} + +export default { get }; diff --git a/src/routes/auth/test.ts b/src/routes/auth/test.ts new file mode 100644 index 0000000..e6bdfa8 --- /dev/null +++ b/src/routes/auth/test.ts @@ -0,0 +1,7 @@ +import express, { Request, Response } from 'express'; + +function get(req: Request, res: Response) { + res.status(200).send('Auth Test Successful!'); +}; + +export default { get }; diff --git a/src/routes/index.ts b/src/routes/index.ts index 1ef45b3..47d23a2 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,17 +1,23 @@ import express, { Express } from 'express'; import { __path, prisma } from '../index.js'; +import * as Sentry from '@sentry/node'; // Route imports import frontend_routes from './frontend/index.js'; import static_routes from './static/index.js'; import api_routes from './api/index.js'; +import auth_routes from './auth/index.js'; const Router = express.Router({ strict: false }); Router.use('/static', static_routes); Router.use('/api', api_routes); +Router.use('/auth', auth_routes); Router.use('/', frontend_routes); +// The error handler must be before any other error middleware and after all controllers +Router.use(Sentry.Handlers.errorHandler()); + // Default route. Router.all('*', function (req, res) { // TODO: Respond based on content-type (with req.is('application/json')) @@ -23,3 +29,4 @@ Router.all('*', function (req, res) { }); export default Router; +