diff --git a/server/auth/index.ts b/server/auth/index.ts index d7880fd..68992d0 100644 --- a/server/auth/index.ts +++ b/server/auth/index.ts @@ -20,12 +20,25 @@ export default class AuthService { // not allowed to use email address that already exists 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(); + let requiredFields: Array = ['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 const salt = await bcrypt.genSalt(12); - console.log(salt); - console.log(data.password); bcrypt.hash(data.password!, salt, (err, hash) => { if (err) throw err; @@ -37,7 +50,7 @@ export default class AuthService { UserInstance.post(newData); }) - return true; + return new ControllerResponse(StatusCode.NewContent, "registered successfully", true); } catch (e: any) { throw new Error(e); } diff --git a/server/auth/middlewares.ts b/server/auth/middlewares.ts index 231cb98..dd09d7c 100644 --- a/server/auth/middlewares.ts +++ b/server/auth/middlewares.ts @@ -1,11 +1,23 @@ -import e, { NextFunction, Request, Response } from "express" -import ControllerResponse from "../util/ControllerResponse"; -import { StatusCode } from "../util/types"; +import { NextFunction, Request, Response } from "express" +import dotenv from "dotenv"; +import { IUser } from "../schemas"; + +dotenv.config(); export function restrictAccess(req: Request, res: Response, next: NextFunction) { if (req.session.user == undefined) { - console.log("restricted") - res.send(undefined); + res.send("content restricted"); + } 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 { next(); } @@ -20,5 +32,11 @@ export function checkFriendStatus(req: Request, res: Response, next: NextFunctio } 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"); + } } \ No newline at end of file diff --git a/server/index.ts b/server/index.ts index 902c8c2..9727fd8 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,13 +1,11 @@ import express from 'express'; -import cors from 'cors'; import dotenv from 'dotenv'; import { loaders } from './loaders'; dotenv.config(); -const port = 8080; +const port = process.env.PORT || 8080; const app = express(); -app.use(cors()); async function main() { await loaders(app); diff --git a/server/loaders/express.ts b/server/loaders/express.ts index 9537efd..f66098b 100644 --- a/server/loaders/express.ts +++ b/server/loaders/express.ts @@ -6,39 +6,31 @@ import cors from 'cors'; import session from 'express-session'; import pgSessionStore from '../db/sessionStore'; 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 { - user: IUser + user?: IUser } } export const expressLoader = async (app: Express) => { - app.use(cors({ - origin: process.env.ORIGIN || 'http://localhost:5173', - credentials: true - })); - + app.use(cors({ origin: origin })); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); 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.get('/', (req, res) => { - res.cookie('name', 'express').send('cookie set'); - }) - - const secret = process.env.SESSIONSECRET as string; + app.use(requireSessionSecret); app.use(session({ - secret: secret, + secret: secret as string, cookie: { maxAge: 8 * 60 * 60 * 1000, - secure: false + secure: false, + httpOnly: false }, resave: false, saveUninitialized: false, diff --git a/server/loaders/index.ts b/server/loaders/index.ts index 396365d..0b6837f 100644 --- a/server/loaders/index.ts +++ b/server/loaders/index.ts @@ -6,7 +6,7 @@ import { passportLoader } from './passport'; export const loaders = async (app: Express) => { const expressApp = await expressLoader(app); - const passportApp = await passportLoader(expressApp); + await passportLoader(expressApp); await swaggerLoader(expressApp); - await routes(expressApp, passportApp); + await routes(expressApp); } \ No newline at end of file diff --git a/server/loaders/passport.ts b/server/loaders/passport.ts index 92876f5..0bca409 100644 --- a/server/loaders/passport.ts +++ b/server/loaders/passport.ts @@ -1,32 +1,35 @@ -import { Strategy as LocalStrategy } from "passport-local"; import passport from "passport"; import { Express } from "express"; -import AuthService from "../auth"; -import { IUserAuth } from "../schemas"; -const AuthInstance = new AuthService(); +import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt"; export const passportLoader = async (app: Express) => { app.use(passport.initialize()); app.use(passport.session()); - passport.serializeUser((user, done) => { - done(null, user); + passport.serializeUser((user: Express.User, done) => { + process.nextTick(() => { + done(null, user); + }) }) - passport.deserializeUser((user: IUserAuth, done) => { - done(null, user); + passport.deserializeUser((user: Express.User, done) => { + process.nextTick(() => { + done(null, user); + }) }) - // sign in method with passport local strategy - passport.use(new LocalStrategy({ - usernameField: 'email', - passwordField: 'password' - }, async (email, password, done) => { + // config for jwt strategy + let opts = { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: 'secret' + } + + // jwt strategy + passport.use(new JwtStrategy(opts, async (token, done) => { try { - const response = await AuthInstance.login({ email, password }); - return done(null, response); - } catch (e: any) { - return done(e); + return done(null, token.user); + } catch (error) { + done(error); } })) diff --git a/server/package.json b/server/package.json index 8870e6b..97cd52c 100644 --- a/server/package.json +++ b/server/package.json @@ -5,7 +5,7 @@ "main": "dist/index.js", "scripts": { "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", "prod": "npm run build && node dist/index.js", "test": "jest --coverage", @@ -15,7 +15,6 @@ "author": "", "license": "ISC", "dependencies": { - "@types/cookie-parser": "^1.4.3", "bcrypt": "^5.1.0", "body-parser": "^1.20.1", "connect-pg-simple": "^8.0.0", @@ -28,8 +27,10 @@ "helmet": "^6.0.0", "http-errors": "^2.0.0", "js-yaml": "^4.1.0", + "jsonwebtoken": "^9.0.0", "morgan": "^1.10.0", "passport": "^0.6.0", + "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pg": "^8.8.0", "pg-promise": "^10.15.0", @@ -38,16 +39,18 @@ "devDependencies": { "@types/bcrypt": "^5.0.0", "@types/connect-pg-simple": "^7.0.0", + "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/dotenv": "^8.2.0", "@types/express": "^4.17.14", - "@types/express-session": "^1.17.5", + "@types/express-session": "^1.17.6", "@types/http-errors": "^2.0.1", "@types/jest": "^29.2.4", "@types/js-yaml": "^4.0.5", "@types/morgan": "^1.9.3", "@types/node": "^18.11.9", "@types/passport": "^1.0.11", + "@types/passport-jwt": "^3.0.8", "@types/passport-local": "^1.0.34", "@types/pg": "^8.6.5", "@types/pg-promise": "^5.4.3", @@ -57,6 +60,7 @@ "nodemon": "^2.0.20", "supertest": "^6.3.3", "ts-jest": "^29.0.3", + "ts-node": "^10.9.1", "ts-node-dev": "^2.0.0", "tslint": "^6.1.3", "typescript": "^4.9.3" diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 775d88c..73fe157 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -1,5 +1,6 @@ -import { Express, Request, Router } from "express" +import { Express, Router } from "express" import { PassportStatic } from "passport"; +import jwt from "jsonwebtoken"; import { IUser, IUserAuth } from "../schemas"; import AuthService from "../auth"; import { UserCtl } from "../controllers"; @@ -12,18 +13,9 @@ const UserInstance = new UserCtl(); const router = Router(); -export const authRoute = (app: Express, passport: PassportStatic) => { +export const authRoute = (app: Express) => { 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) => { console.log(req.session); next(); @@ -49,7 +41,7 @@ export const authRoute = (app: Express, passport: PassportStatic) => { 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 { const data: IUserAuth = req.body; console.log(data); @@ -59,19 +51,27 @@ export const authRoute = (app: Express, passport: PassportStatic) => { if (response.ok) { const user = response.data as IUser; - req.session.regenerate((err) => { - if (err) next(err); - req.session.user = user; + req.user = user; + req.session.user = user; - req.session.save((err) => { - if (err) return next(err); - }) + const safeUserData = { + 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.end(); + res.cookie('token', token, { httpOnly: true }); + res.json({ token }); } else { 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) => { try { - const data = req.body; + const data: IUser = req.body; const response = await AuthInstance.register(data); - if (!response) res.status(400).send({ ok: false }); - res.status(200).send({ ok: true }); + response.represent(); + + res.status(response.code).send({ ok: response.ok, message: response.data }); } catch(e) { next(e); } @@ -93,11 +94,9 @@ export const authRoute = (app: Express, passport: PassportStatic) => { router.delete('/logout', async (req, res, next) => { try { - req.session.destroy((err) => { - if (err) throw err; - }) - res.clearCookie('userid'); - res.status(204).send({ ok: true }); + res.clearCookie('connect.sid').clearCookie('token'); + res.status(204).send("logout successful"); + res.end(); } catch(e) { next(e); } diff --git a/server/routes/collection.ts b/server/routes/collection.ts index 28c62b1..8654504 100644 --- a/server/routes/collection.ts +++ b/server/routes/collection.ts @@ -1,14 +1,14 @@ import { Express, Router } from "express"; -import { restrictAccess } from "../auth/middlewares"; +import { checkIsAdmin, restrictAccess } from "../auth/middlewares"; import CollectionCtl from "../controllers/CollectionCtl"; const CollectionInstance = new CollectionCtl(); const router = Router(); 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; try { const { code, data } = await CollectionInstance.getOne(id); @@ -19,7 +19,7 @@ export const collectionRoute = (app: Express) => { }) // implement is admin on this route - router.get('/', restrictAccess, async (req, res, next) => { + router.get('/', checkIsAdmin, async (req, res, next) => { try { const { code, data } = await CollectionInstance.getAll(); 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; console.log(data); diff --git a/server/routes/course.ts b/server/routes/course.ts index 5882e2f..a8066dd 100644 --- a/server/routes/course.ts +++ b/server/routes/course.ts @@ -4,7 +4,7 @@ const CourseInstance = new CourseCtl(); const router = Router(); export const courseRouter = (app: Express) => { - app.use('/course', router); + app.use('/app/course', router); router.get('/', async (req, res, next) => { try { diff --git a/server/routes/friend.ts b/server/routes/friend.ts index f1d6353..42037a0 100644 --- a/server/routes/friend.ts +++ b/server/routes/friend.ts @@ -7,7 +7,7 @@ const UserInstance = new UserCtl(); const router = Router(); export const friendRouter = (app: Express) => { - app.use('/friend', router); + app.use('/app/friend', router); router.use((req, res, next) => { let test = req.session.user; diff --git a/server/routes/groceryList.ts b/server/routes/groceryList.ts index b5da130..6dcf2a7 100644 --- a/server/routes/groceryList.ts +++ b/server/routes/groceryList.ts @@ -5,7 +5,7 @@ const groceryinstance = new GroceryListCtl(); const router = Router(); export const groceryListRoute = (app: Express) => { - app.use('/grocery-list', router); + app.use('/app/grocery-list', router); router.get('/', async (req, res, next) => { const userid = req.query.userid as string; diff --git a/server/routes/index.ts b/server/routes/index.ts index 1a82f7e..a971eb3 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -1,5 +1,6 @@ +import jwt from "jsonwebtoken"; +import dotenv from 'dotenv'; import { Express } from "express" -import { PassportStatic } from "passport"; import { userRoute } from "./users"; import { recipeRoute } from "./recipe"; import { collectionRoute } from "./collection"; @@ -11,14 +12,38 @@ import { friendRouter } from "./friend"; import { cuisineRouter } from "./cuisine"; 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); friendRouter(app); recipeRoute(app); ingredientRoute(app); - - // to do: refactor for ctlresponse - authRoute(app, passport); collectionRoute(app); subscriptionRoute(app); groceryListRoute(app); diff --git a/server/routes/ingredient.ts b/server/routes/ingredient.ts index 220cb1a..3d8a010 100644 --- a/server/routes/ingredient.ts +++ b/server/routes/ingredient.ts @@ -7,7 +7,7 @@ const IngredientInstance = new IngredientCtl(); const router = Router(); export const ingredientRoute = (app: Express) => { - app.use('/ingredient', router); + app.use('/app/ingredient', router); router.get('/', async (req, res, next) => { try { diff --git a/server/routes/recipe.ts b/server/routes/recipe.ts index 93fdc65..add3ea5 100644 --- a/server/routes/recipe.ts +++ b/server/routes/recipe.ts @@ -8,7 +8,7 @@ const recipectl = new RecipeCtl(); const router = Router(); export const recipeRoute = (app: Express) => { - app.use('/recipe', router); + app.use('/app/recipe', router); router.get('/:id', async (req, res, next) => { const { id } = req.params; diff --git a/server/routes/subscription.ts b/server/routes/subscription.ts index 0ca65e3..d887154 100644 --- a/server/routes/subscription.ts +++ b/server/routes/subscription.ts @@ -5,7 +5,7 @@ const CollectionInstance = new CollectionCtl(); const router = Router(); export const subscriptionRoute = (app: Express) => { - app.use('/subscription', router); + app.use('/app/subscription', router); router.get('/', async (req, res, next) => { // @ts-ignore diff --git a/server/routes/users.ts b/server/routes/users.ts index 417f3d0..ff64789 100644 --- a/server/routes/users.ts +++ b/server/routes/users.ts @@ -6,7 +6,7 @@ const router = Router(); const userCtl = new UserCtl(); export const userRoute = (app: Express) => { - app.use('/users', router); + app.use('/app/users', router); // get all users router.get('/', async (req, res) => { diff --git a/server/schemas/index.ts b/server/schemas/index.ts index 03fc6f2..2546a8e 100644 --- a/server/schemas/index.ts +++ b/server/schemas/index.ts @@ -19,7 +19,7 @@ export interface IUser extends HasHistory, CanDeactivate { handle: string email: string isadmin: boolean - password?: string + password: string } export interface IUserAuth { diff --git a/server/util/ControllerResponse.ts b/server/util/ControllerResponse.ts index 7bdb4ee..c72fa98 100644 --- a/server/util/ControllerResponse.ts +++ b/server/util/ControllerResponse.ts @@ -8,10 +8,14 @@ export default class ControllerResponse implements CtlResponse { constructor(code: StatusCode, data: T | string, ok?: boolean) { this.code = code this.data = data - this.ok = ok || (this.data !== null) + this.ok = ok ?? (this.data !== null) } send() { return { ok: this.ok, code: this.code, data: this.data } } + + represent() { + console.log({ ok: this.ok, code: this.code, data: this.data }); + } } \ No newline at end of file diff --git a/server/util/dev.sh b/server/util/dev.sh index a633e84..1c9e33f 100644 --- a/server/util/dev.sh +++ b/server/util/dev.sh @@ -1,3 +1,3 @@ #! /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 diff --git a/server/util/types.ts b/server/util/types.ts index 65efb6a..b3a6691 100644 --- a/server/util/types.ts +++ b/server/util/types.ts @@ -13,5 +13,6 @@ export enum StatusCode { Unauthorized = 401, Forbidden = 403, NotFound = 404, + Conflict = 409, ServerError = 500 } \ No newline at end of file