server overhaul, new jwt strategy, some various patches

This commit is contained in:
Mikayla Dobson
2023-02-11 16:25:30 -06:00
parent 7aa5e80d4d
commit 3af0af8066
21 changed files with 160 additions and 103 deletions

View File

@@ -20,12 +20,25 @@ export default class AuthService {
// not allowed to use email address that already exists // not allowed to use email address that already exists
const user = await UserInstance.getOneByEmail(data.email); const user = await UserInstance.getOneByEmail(data.email);
if (user) throw createError('409', 'Email already in use'); if (user) {
return new ControllerResponse(StatusCode.Conflict, "Email already in use", false);
}
// check that all required fields are populated
let missingFields = new Array<string>();
let requiredFields: Array<keyof IUser> = ['firstname', 'lastname', 'handle', 'email', 'isadmin', 'password'];
for (let field of requiredFields) {
if (!(field in data)) {
missingFields.push(field as string);
}
}
if (missingFields.length) {
return new ControllerResponse(StatusCode.BadRequest, `Missing fields in output: ${missingFields.join(", ")}`, false);
}
// hash password and create new user record // hash password and create new user record
const salt = await bcrypt.genSalt(12); const salt = await bcrypt.genSalt(12);
console.log(salt);
console.log(data.password);
bcrypt.hash(data.password!, salt, (err, hash) => { bcrypt.hash(data.password!, salt, (err, hash) => {
if (err) throw err; if (err) throw err;
@@ -37,7 +50,7 @@ export default class AuthService {
UserInstance.post(newData); UserInstance.post(newData);
}) })
return true; return new ControllerResponse(StatusCode.NewContent, "registered successfully", true);
} catch (e: any) { } catch (e: any) {
throw new Error(e); throw new Error(e);
} }

View File

@@ -1,11 +1,23 @@
import e, { NextFunction, Request, Response } from "express" import { NextFunction, Request, Response } from "express"
import ControllerResponse from "../util/ControllerResponse"; import dotenv from "dotenv";
import { StatusCode } from "../util/types"; import { IUser } from "../schemas";
dotenv.config();
export function restrictAccess(req: Request, res: Response, next: NextFunction) { export function restrictAccess(req: Request, res: Response, next: NextFunction) {
if (req.session.user == undefined) { if (req.session.user == undefined) {
console.log("restricted") res.send("content restricted");
res.send(undefined); } else {
next();
}
}
export function requireSessionSecret(req: Request, res: Response, next: NextFunction) {
const secret = process.env.SESSIONSECRET;
if (!secret) {
res.sendStatus(500);
throw new Error("Express secret is undefined");
} else { } else {
next(); next();
} }
@@ -20,5 +32,11 @@ export function checkFriendStatus(req: Request, res: Response, next: NextFunctio
} }
export function checkIsAdmin(req: Request, res: Response, next: NextFunction) { export function checkIsAdmin(req: Request, res: Response, next: NextFunction) {
const user: IUser | undefined = req.user as IUser;
if (user.isadmin) {
next();
} else {
res.status(403).send("Unauthorized");
}
} }

View File

@@ -1,13 +1,11 @@
import express from 'express'; import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import { loaders } from './loaders'; import { loaders } from './loaders';
dotenv.config(); dotenv.config();
const port = 8080; const port = process.env.PORT || 8080;
const app = express(); const app = express();
app.use(cors());
async function main() { async function main() {
await loaders(app); await loaders(app);

View File

@@ -6,39 +6,31 @@ import cors from 'cors';
import session from 'express-session'; import session from 'express-session';
import pgSessionStore from '../db/sessionStore'; import pgSessionStore from '../db/sessionStore';
import { IUser } from '../schemas'; import { IUser } from '../schemas';
import { requireSessionSecret } from '../auth/middlewares';
declare module "express-session" { const origin = process.env.ORIGIN || 'http://localhost:5173';
const secret = process.env.SESSIONSECRET;
declare module 'express-session' {
interface SessionData { interface SessionData {
user: IUser user?: IUser
} }
} }
export const expressLoader = async (app: Express) => { export const expressLoader = async (app: Express) => {
app.use(cors({ app.use(cors({ origin: origin }));
origin: process.env.ORIGIN || 'http://localhost:5173',
credentials: true
}));
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser()); app.use(cookieParser());
// app.options("*", cors({ origin: 'http://localhost:5173', optionsSuccessStatus: 200 }));
// app.use(cors({ origin: "http://localhost:5173", optionsSuccessStatus: 200 }));
app.use(morgan('tiny')); app.use(morgan('tiny'));
app.use(requireSessionSecret);
app.get('/', (req, res) => {
res.cookie('name', 'express').send('cookie set');
})
const secret = process.env.SESSIONSECRET as string;
app.use(session({ app.use(session({
secret: secret, secret: secret as string,
cookie: { cookie: {
maxAge: 8 * 60 * 60 * 1000, maxAge: 8 * 60 * 60 * 1000,
secure: false secure: false,
httpOnly: false
}, },
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,

View File

@@ -6,7 +6,7 @@ import { passportLoader } from './passport';
export const loaders = async (app: Express) => { export const loaders = async (app: Express) => {
const expressApp = await expressLoader(app); const expressApp = await expressLoader(app);
const passportApp = await passportLoader(expressApp); await passportLoader(expressApp);
await swaggerLoader(expressApp); await swaggerLoader(expressApp);
await routes(expressApp, passportApp); await routes(expressApp);
} }

View File

@@ -1,32 +1,35 @@
import { Strategy as LocalStrategy } from "passport-local";
import passport from "passport"; import passport from "passport";
import { Express } from "express"; import { Express } from "express";
import AuthService from "../auth"; import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt";
import { IUserAuth } from "../schemas";
const AuthInstance = new AuthService();
export const passportLoader = async (app: Express) => { export const passportLoader = async (app: Express) => {
app.use(passport.initialize()); app.use(passport.initialize());
app.use(passport.session()); app.use(passport.session());
passport.serializeUser((user, done) => { passport.serializeUser((user: Express.User, done) => {
done(null, user); process.nextTick(() => {
done(null, user);
})
}) })
passport.deserializeUser((user: IUserAuth, done) => { passport.deserializeUser((user: Express.User, done) => {
done(null, user); process.nextTick(() => {
done(null, user);
})
}) })
// sign in method with passport local strategy // config for jwt strategy
passport.use(new LocalStrategy({ let opts = {
usernameField: 'email', jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
passwordField: 'password' secretOrKey: 'secret'
}, async (email, password, done) => { }
// jwt strategy
passport.use(new JwtStrategy(opts, async (token, done) => {
try { try {
const response = await AuthInstance.login({ email, password }); return done(null, token.user);
return done(null, response); } catch (error) {
} catch (e: any) { done(error);
return done(e);
} }
})) }))

View File

@@ -5,7 +5,7 @@
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
"build": "bash util/build.sh", "build": "bash util/build.sh",
"seed": "npm run build && ts-node-dev db/seed.ts", "seed": "npm run build && ts-node --files db/seed.ts",
"dev": "bash util/dev.sh", "dev": "bash util/dev.sh",
"prod": "npm run build && node dist/index.js", "prod": "npm run build && node dist/index.js",
"test": "jest --coverage", "test": "jest --coverage",
@@ -15,7 +15,6 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@types/cookie-parser": "^1.4.3",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"body-parser": "^1.20.1", "body-parser": "^1.20.1",
"connect-pg-simple": "^8.0.0", "connect-pg-simple": "^8.0.0",
@@ -28,8 +27,10 @@
"helmet": "^6.0.0", "helmet": "^6.0.0",
"http-errors": "^2.0.0", "http-errors": "^2.0.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pg": "^8.8.0", "pg": "^8.8.0",
"pg-promise": "^10.15.0", "pg-promise": "^10.15.0",
@@ -38,16 +39,18 @@
"devDependencies": { "devDependencies": {
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/connect-pg-simple": "^7.0.0", "@types/connect-pg-simple": "^7.0.0",
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/dotenv": "^8.2.0", "@types/dotenv": "^8.2.0",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
"@types/express-session": "^1.17.5", "@types/express-session": "^1.17.6",
"@types/http-errors": "^2.0.1", "@types/http-errors": "^2.0.1",
"@types/jest": "^29.2.4", "@types/jest": "^29.2.4",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/morgan": "^1.9.3", "@types/morgan": "^1.9.3",
"@types/node": "^18.11.9", "@types/node": "^18.11.9",
"@types/passport": "^1.0.11", "@types/passport": "^1.0.11",
"@types/passport-jwt": "^3.0.8",
"@types/passport-local": "^1.0.34", "@types/passport-local": "^1.0.34",
"@types/pg": "^8.6.5", "@types/pg": "^8.6.5",
"@types/pg-promise": "^5.4.3", "@types/pg-promise": "^5.4.3",
@@ -57,6 +60,7 @@
"nodemon": "^2.0.20", "nodemon": "^2.0.20",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
"ts-node": "^10.9.1",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"tslint": "^6.1.3", "tslint": "^6.1.3",
"typescript": "^4.9.3" "typescript": "^4.9.3"

View File

@@ -1,5 +1,6 @@
import { Express, Request, Router } from "express" import { Express, Router } from "express"
import { PassportStatic } from "passport"; import { PassportStatic } from "passport";
import jwt from "jsonwebtoken";
import { IUser, IUserAuth } from "../schemas"; import { IUser, IUserAuth } from "../schemas";
import AuthService from "../auth"; import AuthService from "../auth";
import { UserCtl } from "../controllers"; import { UserCtl } from "../controllers";
@@ -12,18 +13,9 @@ const UserInstance = new UserCtl();
const router = Router(); const router = Router();
export const authRoute = (app: Express, passport: PassportStatic) => { export const authRoute = (app: Express) => {
app.use('/auth', router); app.use('/auth', router);
// router.use((req, res, next) => {
// console.log(req.isAuthenticated());
// console.log(req.session.user);
// console.log(req.cookies);
// console.log();
// next();
// })
router.use((req, res, next) => { router.use((req, res, next) => {
console.log(req.session); console.log(req.session);
next(); next();
@@ -49,7 +41,7 @@ export const authRoute = (app: Express, passport: PassportStatic) => {
res.status(200).send({ message: "Cool restricted content!" }); res.status(200).send({ message: "Cool restricted content!" });
}) })
router.post('/login', passport.authenticate('local'), async (req, res, next) => { router.post('/login', async (req, res, next) => {
try { try {
const data: IUserAuth = req.body; const data: IUserAuth = req.body;
console.log(data); console.log(data);
@@ -59,19 +51,27 @@ export const authRoute = (app: Express, passport: PassportStatic) => {
if (response.ok) { if (response.ok) {
const user = response.data as IUser; const user = response.data as IUser;
req.session.regenerate((err) => { req.user = user;
if (err) next(err); req.session.user = user;
req.session.user = user;
req.session.save((err) => { const safeUserData = {
if (err) return next(err); id: user.id,
}) handle: user.handle,
email: user.email,
datecreated: user.datecreated,
datemodified: user.datemodified
}
const token = jwt.sign({ user: safeUserData }, process.env.SESSIONSECRET as string);
req.session.save((err) => {
return next(err);
}) })
res.cookie('userid', user.id, { maxAge: 1000 * 60 * 60 * 24 }); console.log(req.session);
res.send(response); res.cookie('token', token, { httpOnly: true });
res.end(); res.json({ token });
} else { } else {
res.status(401).send({ message: "Login unsuccessful" }); res.status(401).send({ message: "Login unsuccessful" });
} }
@@ -82,10 +82,11 @@ export const authRoute = (app: Express, passport: PassportStatic) => {
router.post('/register', async (req, res, next) => { router.post('/register', async (req, res, next) => {
try { try {
const data = req.body; const data: IUser = req.body;
const response = await AuthInstance.register(data); const response = await AuthInstance.register(data);
if (!response) res.status(400).send({ ok: false }); response.represent();
res.status(200).send({ ok: true });
res.status(response.code).send({ ok: response.ok, message: response.data });
} catch(e) { } catch(e) {
next(e); next(e);
} }
@@ -93,11 +94,9 @@ export const authRoute = (app: Express, passport: PassportStatic) => {
router.delete('/logout', async (req, res, next) => { router.delete('/logout', async (req, res, next) => {
try { try {
req.session.destroy((err) => { res.clearCookie('connect.sid').clearCookie('token');
if (err) throw err; res.status(204).send("logout successful");
}) res.end();
res.clearCookie('userid');
res.status(204).send({ ok: true });
} catch(e) { } catch(e) {
next(e); next(e);
} }

View File

@@ -1,14 +1,14 @@
import { Express, Router } from "express"; import { Express, Router } from "express";
import { restrictAccess } from "../auth/middlewares"; import { checkIsAdmin, restrictAccess } from "../auth/middlewares";
import CollectionCtl from "../controllers/CollectionCtl"; import CollectionCtl from "../controllers/CollectionCtl";
const CollectionInstance = new CollectionCtl(); const CollectionInstance = new CollectionCtl();
const router = Router(); const router = Router();
export const collectionRoute = (app: Express) => { export const collectionRoute = (app: Express) => {
app.use('/collection', router); app.use('/app/collection', router);
router.get('/:id', restrictAccess, async (req, res, next) => { router.get('/:id', async (req, res, next) => {
const { id } = req.params; const { id } = req.params;
try { try {
const { code, data } = await CollectionInstance.getOne(id); const { code, data } = await CollectionInstance.getOne(id);
@@ -19,7 +19,7 @@ export const collectionRoute = (app: Express) => {
}) })
// implement is admin on this route // implement is admin on this route
router.get('/', restrictAccess, async (req, res, next) => { router.get('/', checkIsAdmin, async (req, res, next) => {
try { try {
const { code, data } = await CollectionInstance.getAll(); const { code, data } = await CollectionInstance.getAll();
res.status(code).send(data); res.status(code).send(data);
@@ -28,7 +28,7 @@ export const collectionRoute = (app: Express) => {
} }
}) })
router.post('/', restrictAccess, async (req, res, next) => { router.post('/', async (req, res, next) => {
const data = req.body; const data = req.body;
console.log(data); console.log(data);

View File

@@ -4,7 +4,7 @@ const CourseInstance = new CourseCtl();
const router = Router(); const router = Router();
export const courseRouter = (app: Express) => { export const courseRouter = (app: Express) => {
app.use('/course', router); app.use('/app/course', router);
router.get('/', async (req, res, next) => { router.get('/', async (req, res, next) => {
try { try {

View File

@@ -7,7 +7,7 @@ const UserInstance = new UserCtl();
const router = Router(); const router = Router();
export const friendRouter = (app: Express) => { export const friendRouter = (app: Express) => {
app.use('/friend', router); app.use('/app/friend', router);
router.use((req, res, next) => { router.use((req, res, next) => {
let test = req.session.user; let test = req.session.user;

View File

@@ -5,7 +5,7 @@ const groceryinstance = new GroceryListCtl();
const router = Router(); const router = Router();
export const groceryListRoute = (app: Express) => { export const groceryListRoute = (app: Express) => {
app.use('/grocery-list', router); app.use('/app/grocery-list', router);
router.get('/', async (req, res, next) => { router.get('/', async (req, res, next) => {
const userid = req.query.userid as string; const userid = req.query.userid as string;

View File

@@ -1,5 +1,6 @@
import jwt from "jsonwebtoken";
import dotenv from 'dotenv';
import { Express } from "express" import { Express } from "express"
import { PassportStatic } from "passport";
import { userRoute } from "./users"; import { userRoute } from "./users";
import { recipeRoute } from "./recipe"; import { recipeRoute } from "./recipe";
import { collectionRoute } from "./collection"; import { collectionRoute } from "./collection";
@@ -11,14 +12,38 @@ import { friendRouter } from "./friend";
import { cuisineRouter } from "./cuisine"; import { cuisineRouter } from "./cuisine";
import { courseRouter } from "./course"; import { courseRouter } from "./course";
export const routes = async (app: Express, passport: PassportStatic) => { dotenv.config();
export const routes = async (app: Express) => {
// unprotected routes
authRoute(app);
// middleware to check for auth on cookies on each request in protected routes
app.use('/app', async (req, res, next) => {
// pull jwt from request headers
const token = req.headers['authorization']?.split(" ")[1];
console.log(token);
if (!token) {
res.status(403).send("Unauthorized");
} else {
jwt.verify(token, process.env.SESSIONSECRET as string, (err, data) => {
if (err) {
res.status(403).send(err);
} else {
console.log(data);
req.user = data;
next();
}
})
}
})
// protected routes
userRoute(app); userRoute(app);
friendRouter(app); friendRouter(app);
recipeRoute(app); recipeRoute(app);
ingredientRoute(app); ingredientRoute(app);
// to do: refactor for ctlresponse
authRoute(app, passport);
collectionRoute(app); collectionRoute(app);
subscriptionRoute(app); subscriptionRoute(app);
groceryListRoute(app); groceryListRoute(app);

View File

@@ -7,7 +7,7 @@ const IngredientInstance = new IngredientCtl();
const router = Router(); const router = Router();
export const ingredientRoute = (app: Express) => { export const ingredientRoute = (app: Express) => {
app.use('/ingredient', router); app.use('/app/ingredient', router);
router.get('/', async (req, res, next) => { router.get('/', async (req, res, next) => {
try { try {

View File

@@ -8,7 +8,7 @@ const recipectl = new RecipeCtl();
const router = Router(); const router = Router();
export const recipeRoute = (app: Express) => { export const recipeRoute = (app: Express) => {
app.use('/recipe', router); app.use('/app/recipe', router);
router.get('/:id', async (req, res, next) => { router.get('/:id', async (req, res, next) => {
const { id } = req.params; const { id } = req.params;

View File

@@ -5,7 +5,7 @@ const CollectionInstance = new CollectionCtl();
const router = Router(); const router = Router();
export const subscriptionRoute = (app: Express) => { export const subscriptionRoute = (app: Express) => {
app.use('/subscription', router); app.use('/app/subscription', router);
router.get('/', async (req, res, next) => { router.get('/', async (req, res, next) => {
// @ts-ignore // @ts-ignore

View File

@@ -6,7 +6,7 @@ const router = Router();
const userCtl = new UserCtl(); const userCtl = new UserCtl();
export const userRoute = (app: Express) => { export const userRoute = (app: Express) => {
app.use('/users', router); app.use('/app/users', router);
// get all users // get all users
router.get('/', async (req, res) => { router.get('/', async (req, res) => {

View File

@@ -19,7 +19,7 @@ export interface IUser extends HasHistory, CanDeactivate {
handle: string handle: string
email: string email: string
isadmin: boolean isadmin: boolean
password?: string password: string
} }
export interface IUserAuth { export interface IUserAuth {

View File

@@ -8,10 +8,14 @@ export default class ControllerResponse<T> implements CtlResponse<T> {
constructor(code: StatusCode, data: T | string, ok?: boolean) { constructor(code: StatusCode, data: T | string, ok?: boolean) {
this.code = code this.code = code
this.data = data this.data = data
this.ok = ok || (this.data !== null) this.ok = ok ?? (this.data !== null)
} }
send() { send() {
return { ok: this.ok, code: this.code, data: this.data } return { ok: this.ok, code: this.code, data: this.data }
} }
represent() {
console.log({ ok: this.ok, code: this.code, data: this.data });
}
} }

View File

@@ -1,3 +1,3 @@
#! /bin/bash #! /bin/bash
rm -rf dist && mkdir -p dist && cp ./swagger.yaml ./dist && ./node_modules/.bin/tsc --project ./tsconfig.json --watch & ts-node-dev index.ts rm -rf dist && mkdir -p dist && cp ./swagger.yaml ./dist && ./node_modules/.bin/tsc --project ./tsconfig.json --watch & ts-node --files index.ts

View File

@@ -13,5 +13,6 @@ export enum StatusCode {
Unauthorized = 401, Unauthorized = 401,
Forbidden = 403, Forbidden = 403,
NotFound = 404, NotFound = 404,
Conflict = 409,
ServerError = 500 ServerError = 500
} }