From 3af0af8066ffa0b64b5e0bf4ae07133b83835132 Mon Sep 17 00:00:00 2001 From: Mikayla Dobson <93477693+innocuous-symmetry@users.noreply.github.com> Date: Sat, 11 Feb 2023 16:25:30 -0600 Subject: [PATCH 1/6] server overhaul, new jwt strategy, some various patches --- server/auth/index.ts | 21 +++++++++--- server/auth/middlewares.ts | 30 ++++++++++++---- server/index.ts | 4 +-- server/loaders/express.ts | 30 ++++++---------- server/loaders/index.ts | 4 +-- server/loaders/passport.ts | 37 +++++++++++--------- server/package.json | 10 ++++-- server/routes/auth.ts | 57 +++++++++++++++---------------- server/routes/collection.ts | 10 +++--- server/routes/course.ts | 2 +- server/routes/friend.ts | 2 +- server/routes/groceryList.ts | 2 +- server/routes/index.ts | 35 ++++++++++++++++--- server/routes/ingredient.ts | 2 +- server/routes/recipe.ts | 2 +- server/routes/subscription.ts | 2 +- server/routes/users.ts | 2 +- server/schemas/index.ts | 2 +- server/util/ControllerResponse.ts | 6 +++- server/util/dev.sh | 2 +- server/util/types.ts | 1 + 21 files changed, 160 insertions(+), 103 deletions(-) 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 -- 2.49.1 From fd743825e2485d245f04a5058523acb71d4b6689 Mon Sep 17 00:00:00 2001 From: Mikayla Dobson <93477693+innocuous-symmetry@users.noreply.github.com> Date: Sat, 11 Feb 2023 17:35:11 -0600 Subject: [PATCH 2/6] refactored form component --- client/src/components/pages/Login.tsx | 39 +++--- client/src/components/ui/Form.tsx | 167 +++++++++++++------------- 2 files changed, 111 insertions(+), 95 deletions(-) diff --git a/client/src/components/pages/Login.tsx b/client/src/components/pages/Login.tsx index 8fd6a74..b46c068 100644 --- a/client/src/components/pages/Login.tsx +++ b/client/src/components/pages/Login.tsx @@ -2,23 +2,24 @@ import { useCallback, useContext, useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { AuthContext, useAuthContext } from "../../context/AuthContext"; import { attemptLogin } from "../../util/apiUtils"; -import { IUserAuth } from "../../schemas"; +import { IUser, IUserAuth } from "../../schemas"; import { Button, Form, Page, Panel } from "../ui"; +import { FormConfig } from "../ui/Form"; export default function Login() { const params = new URLSearchParams(window.location.search); const redirect = params.get("redirect"); const { user, setUser } = useContext(AuthContext); + const [form, setForm] = useState(); // setup and local state const navigate = useNavigate(); - const [form, setForm] = useState(); const [input, setInput] = useState({ email: '', password: '' }); // retrieve and store state from form const getFormState = useCallback((received: IUserAuth) => { setInput(received); - }, []) + }, [input]) const handleLogin = async () => { @@ -31,25 +32,35 @@ export default function Login() { // check for logged in user and mount form useEffect(() => { if (user) navigate('/'); - setForm( - new Form({ - parent: 'login', - keys: Object.keys(input), - labels: ["Email", "Password"], - dataTypes: Object.keys(input), - initialState: input, - getState: getFormState - }).mount() - ); }, []) + // useEffect(() => { + // setForm( + + // ) + // }, [getFormState]) + + useEffect(() => { + console.log(input); + }, [getFormState]) + return (

Hello! Nice to see you again.

- { form ||

Loading...

} + +
} /> + + diff --git a/client/src/components/ui/Form.tsx b/client/src/components/ui/Form.tsx index f0541a8..7704977 100644 --- a/client/src/components/ui/Form.tsx +++ b/client/src/components/ui/Form.tsx @@ -1,13 +1,6 @@ -import { ChangeEvent, FC } from "react"; -import { v4 } from 'uuid'; -import RichText from "./RichText"; - -/** - * For the generation of more complex form objects with - * larger stateful values; expects to receive an object of - * type T to a form which can mutate T with a state setter - * of type Dispatch> -**/ +import { ChangeEvent, FC, useEffect, useState } from "react" +import { v4 } from "uuid" +import RichText from "./RichText" export interface FormConfig { parent: string @@ -20,83 +13,95 @@ export interface FormConfig { extraStyles?: string } -export default class Form { - private parent: string; - private labels: string[]; - private keys: string[]; - private dataTypes: any[] - private state: T; - private getState: (received: T) => void - private richTextInitialValue?: string; - private extraStyles?: string +interface FormProps { + parent: any + _config: FormConfig +} - constructor(config: FormConfig){ - this.parent = config.parent; - this.keys = config.keys; - this.labels = config.labels || this.keys; - this.dataTypes = config.dataTypes || new Array(this.keys.length).fill('text'); - this.state = config.initialState; - this.getState = config.getState; - this.richTextInitialValue = config.richTextInitialValue; - this.extraStyles = config.extraStyles; +const Form: FC = ({ parent, _config }) => { + type T = typeof parent; + const { getState } = _config; - this.mount(); - } + const [config, setConfig] = useState>(); + const [state, setState] = useState(); + const [contents, setContents] = useState(); - update(e: ChangeEvent, idx: number) { - let newState = { - ...this.state, - [this.keys[idx]]: e.target['value' as keyof EventTarget] - } + // initial setup + useEffect(() => { + if (!config) setConfig({ + ..._config, + labels: _config.labels ?? _config.keys, + dataTypes: _config.dataTypes ?? new Array(_config.keys?.length).fill("text"), + }); - this.state = newState; - this.getState(newState); - } + if (!state) setState(_config.initialState); + }, []) - updateRichText(txt: string, idx: number) { - this.state = { - ...this.state, - [this.keys[idx]]: txt - } + // usecallback handling + useEffect(() => { + state && getState(state); + }, [state]); - this.getState(this.state); - } + // update methods + function updateRichText(txt: string, idx: number) { + if (!config) return; - mount() { - let output = new Array(); - - for (let i = 0; i < this.keys.length; i++) { - let input: JSX.Element | null; - - if (this.dataTypes[i] == 'custom picker') { - console.log('noted!'); - this.dataTypes[i] = 'text'; + setState((prev: T) => { + return { + ...prev, + [config.keys[idx]]: txt } - - if (this.dataTypes[i] == 'TINYMCE') { - input = ( -
- - this.updateRichText(txt, i)} /> -
- ) - } else { - input = ( -
- - this.update(e, i)} - value={this.state[i as keyof T] as string}> - -
- ) - } - - output.push(input); - } - - return
{output}
; + }) } -} \ No newline at end of file + + function update(e: ChangeEvent, idx: number) { + if (!config) return; + + setState((prev: T) => { + return { + ...prev, + [config.keys[idx]]: e.target['value' as keyof EventTarget] + } + }) + } + + // mount the form once config has been loaded + useEffect(() => { + if (state && config) { + const result = config.keys.map((each: string, i: number) => { + + if (config.dataTypes![i] == 'TINYMCE') { + return ( +
+ + updateRichText(txt, i)} /> +
+ ) + } else { + return ( +
+ + update(e, i)} + value={state[i as keyof T] as string}> + +
+ ) + } + }); + + setContents(result); + + } + }, [config]); + + return ( +
+ { contents } +
+ ) +} + +export default Form; \ No newline at end of file -- 2.49.1 From 514bcde809f5b606b11eff20b61db09129ea8781 Mon Sep 17 00:00:00 2001 From: Mikayla Dobson <93477693+innocuous-symmetry@users.noreply.github.com> Date: Sat, 11 Feb 2023 18:18:34 -0600 Subject: [PATCH 3/6] refactoring front end API access --- client/package.json | 2 + client/src/components/pages/Login.tsx | 6 - client/src/util/API.ts | 152 ++++++++++++++++++++++++++ client/src/util/apiUtils.tsx | 2 +- client/src/util/axiosInstance.ts | 24 ++++ 5 files changed, 179 insertions(+), 7 deletions(-) create mode 100644 client/src/util/API.ts create mode 100644 client/src/util/axiosInstance.ts diff --git a/client/package.json b/client/package.json index 6b97e4f..148c6da 100644 --- a/client/package.json +++ b/client/package.json @@ -11,6 +11,7 @@ "dependencies": { "@tinymce/tinymce-react": "^4.2.0", "axios": "^1.2.0", + "jwt-decode": "^3.1.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.4.3", @@ -18,6 +19,7 @@ "uuid": "^9.0.0" }, "devDependencies": { + "@types/jwt-decode": "^3.1.0", "@types/react": "^18.0.24", "@types/react-dom": "^18.0.8", "@types/uuid": "^8.3.4", diff --git a/client/src/components/pages/Login.tsx b/client/src/components/pages/Login.tsx index b46c068..4edad82 100644 --- a/client/src/components/pages/Login.tsx +++ b/client/src/components/pages/Login.tsx @@ -34,12 +34,6 @@ export default function Login() { if (user) navigate('/'); }, []) - // useEffect(() => { - // setForm( - - // ) - // }, [getFormState]) - useEffect(() => { console.log(input); }, [getFormState]) diff --git a/client/src/util/API.ts b/client/src/util/API.ts new file mode 100644 index 0000000..f1fbdc1 --- /dev/null +++ b/client/src/util/API.ts @@ -0,0 +1,152 @@ +import { AxiosHeaders, AxiosRequestHeaders } from "axios"; +import { IUser, IUserAuth, IFriendship, IRecipe, IIngredient, ICollection, IGroceryList } from "../schemas"; +import { default as _instance } from "./axiosInstance"; + +export module API { + const APISTRING = import.meta.env.APISTRING || "http://localhost:8080"; + + abstract class RestController { + protected instance = _instance; + protected endpoint: string; + protected token?: string; + protected headers?: any + + constructor(endpoint: string, token?: string) { + this.endpoint = endpoint; + this.token = token; + + if (token) { + this.headers = { + "Content-Type": "application/json", + "Authorization": ("Bearer " + token) + }; + } + } + + async getAll() { + if (!this.token) return null; + + const response = await this.instance.get(this.endpoint, this.headers); + return Promise.resolve(response.data); + } + + async getByID(id: string) { + if (!this.token) return null; + + const response = await this.instance.get(this.endpoint + "/" + id, this.headers); + return Promise.resolve(response.data); + } + + async postOne(data: T) { + if (!this.token) return null; + + const response = await this.instance.post(this.endpoint, data, this.headers); + return Promise.resolve(response.data); + } + + async put(id: string, data: T | Partial) { + if (!this.token) return null; + + const response = await this.instance.put(this.endpoint + "/" + id, data, this.headers); + return Promise.resolve(response.data); + } + + async delete(id: string) { + if (!this.token) return null; + + const response = await this.instance.delete(this.endpoint + '/' + id, this.headers); + return Promise.resolve(response.data); + } + } + + export class Auth { + private instance = _instance; + private endpoint = APISTRING + "/auth"; + + async login(data: IUserAuth | Partial) { + try { + const response = await this.instance.post(this.endpoint + "/login", data); + return Promise.resolve(response.data); + } catch (e: any) { + console.error(e); + } + } + + async register(data: IUser) { + try { + const response = await this.instance.post(this.endpoint + "/register", data); + return Promise.resolve(response.data); + } catch (e: any) { + console.error(e); + } + } + + async logout() { + try { + const response = await this.instance.delete(this.endpoint + '/logout'); + + // unset cookie data and send response + document.cookie = `token=;expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + return Promise.resolve(response.data); + } catch(err) { + console.error(err); + } + } + + // for future use + async registerGoogle() { + return; + } + + async loginGoogle() { + return; + } + + async logoutGoogle() { + return; + } + } + + export class User extends RestController { + constructor() { + super(APISTRING + "/app/users"); + } + } + + export class Friendship extends RestController { + constructor() { + super(APISTRING + "/app/friends"); + } + + async getPendingFriendRequests() { + if (!this.token) return null; + + const response = await this.instance.get(this.endpoint + "?pending=true", this.headers); + return Promise.resolve(response.data); + } + } + + export class Recipe extends RestController { + constructor() { + super(APISTRING + "/app/recipes"); + } + } + + export class Ingredient extends RestController { + constructor() { + super(APISTRING + "/app/ingredients"); + } + } + + export class Collection extends RestController { + constructor() { + super(APISTRING + "/app/collections"); + } + } + + export class GroceryList extends RestController { + constructor() { + super(APISTRING + "/app/grocery-list") + } + } +} \ No newline at end of file diff --git a/client/src/util/apiUtils.tsx b/client/src/util/apiUtils.tsx index c4e8a19..8adfaf0 100644 --- a/client/src/util/apiUtils.tsx +++ b/client/src/util/apiUtils.tsx @@ -1,5 +1,5 @@ import { ICollection, IUser, IUserAuth } from "../schemas"; -// import { IAuthContext } from "../context/AuthContext"; +import instance from "./axiosInstance"; import axios from "axios"; const API = import.meta.env.APISTRING || "http://localhost:8080"; diff --git a/client/src/util/axiosInstance.ts b/client/src/util/axiosInstance.ts new file mode 100644 index 0000000..ebaa5e4 --- /dev/null +++ b/client/src/util/axiosInstance.ts @@ -0,0 +1,24 @@ +import axios, { AxiosResponse } from 'axios' +import jwt_decode from 'jwt-decode' + +const apiUrl = import.meta.env.VITE_APIURL; + +const instance = axios.create({ + baseURL: apiUrl +}); + +instance.interceptors.response.use((res: AxiosResponse) => { + if (res?.data.token) { + document.cookie = `token=${res.data.token}`; + + return res; + } else { + console.error("Token was not found in response"); + + return res; + } +}, (err) => { + return Promise.reject(err); +}) + +export default instance; \ No newline at end of file -- 2.49.1 From 90a5bdf128c5b7de183355429ed8ab77395e9851 Mon Sep 17 00:00:00 2001 From: Mikayla Dobson <93477693+innocuous-symmetry@users.noreply.github.com> Date: Sat, 11 Feb 2023 20:50:16 -0600 Subject: [PATCH 4/6] reformatting data flow on frontend --- client/src/App.tsx | 73 ++++++++----------- client/src/components/pages/Login.tsx | 13 ++-- .../components/pages/Register/collection.tsx | 5 +- .../src/components/pages/Register/index.tsx | 10 +-- client/src/components/ui/Navbar/index.tsx | 39 +++------- client/src/components/ui/Navbar/variants.tsx | 34 ++++++--- client/src/context/AuthContext.tsx | 8 +- client/src/context/AuthProvider.tsx | 18 +++++ client/src/main.tsx | 9 ++- client/src/util/API.ts | 6 +- client/src/util/axiosInstance.ts | 8 +- client/src/util/types.ts | 6 +- 12 files changed, 122 insertions(+), 107 deletions(-) create mode 100644 client/src/context/AuthProvider.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 5e7c3a7..c0dc73e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,11 +1,10 @@ // framework tools and custom utils -import { useCallback, useContext, useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; -import { AuthContext, IAuthContext, useAuthContext } from './context/AuthContext'; -import { attemptLogout, checkCredientials } from './util/apiUtils'; -import { IUser } from './schemas'; +import { useAuthContext } from './context/AuthContext'; +import jwtDecode from 'jwt-decode'; -// pages, ui, styles +// pages, ui, components, styles import Subscriptions from './components/pages/Subscriptions/Subscriptions'; import Browser from './components/ui/Browser'; import Collection from './components/pages/Collection'; @@ -19,55 +18,41 @@ import CollectionBrowser from './components/pages/CollectionBrowser'; import { Navbar } from './components/ui'; import GroceryList from './components/pages/GroceryList'; import GroceryListCollection from './components/pages/GroceryListCollection'; +import { TokenType } from './util/types'; import './sass/App.scss'; function App() { - const [user, setUser] = useState(); - const parentState = { user, setUser }; - - const receiveChange = (() => {}); + const { setUser, setToken } = useAuthContext(); useEffect(() => { - const wrapper = async () => { - try { - const result: IAuthContext | undefined = await checkCredientials(); - - if (result == undefined) { - setUser({ user: undefined }); - } else { - setUser(result); - } - } catch(e) { - console.error(e); - } + if (document.cookie) { + const extractedToken: Partial = jwtDecode(document.cookie.split("=")[1]); + setToken(document.cookie.split("=")[1]); + setUser(extractedToken.user); } - - wrapper(); - }, []) + }, []); return ( - -
- - - } /> - } /> - } /> - } /> - } /> - } /> - {}} />} /> - } /> - } /> - } /> +
+ + + } /> + } /> + } /> + } /> + } /> + } /> + {}} />} /> + } /> + } /> + } /> - } /> - } /> - } /> - -
- + } /> + } /> + } /> +
+
) } diff --git a/client/src/components/pages/Login.tsx b/client/src/components/pages/Login.tsx index 4edad82..0fe25bd 100644 --- a/client/src/components/pages/Login.tsx +++ b/client/src/components/pages/Login.tsx @@ -1,10 +1,10 @@ import { useCallback, useContext, useEffect, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; import { AuthContext, useAuthContext } from "../../context/AuthContext"; -import { attemptLogin } from "../../util/apiUtils"; +import { useNavigate, useParams } from "react-router-dom"; import { IUser, IUserAuth } from "../../schemas"; import { Button, Form, Page, Panel } from "../ui"; import { FormConfig } from "../ui/Form"; +import API from "../../util/API"; export default function Login() { const params = new URLSearchParams(window.location.search); @@ -24,9 +24,12 @@ export default function Login() { const handleLogin = async () => { if (!input.email || !input.password) return; - const { data, ok } = await attemptLogin(input); - if (ok) setUser(data); - navigate(`/${redirect ?? ''}`); + const result = await new API.Auth().login(input); + console.log(result); + + // const { data, ok } = await attemptLogin(input); + // if (ok) setUser(data); + // navigate(`/${redirect ?? ''}`); } // check for logged in user and mount form diff --git a/client/src/components/pages/Register/collection.tsx b/client/src/components/pages/Register/collection.tsx index ee97dfc..5a41012 100644 --- a/client/src/components/pages/Register/collection.tsx +++ b/client/src/components/pages/Register/collection.tsx @@ -6,7 +6,7 @@ import { attemptLogin, createNewCollection } from "../../../util/apiUtils"; import { Button, Divider, Page, Panel } from "../../ui"; import TextField from "../../ui/TextField"; -const InitialCollection: RegisterVariantType = ({ transitionDisplay, receiveChange, input }) => { +const InitialCollection: RegisterVariantType = ({ transitionDisplay, input }) => { const [collectionName, setCollectionName] = useState(); const [view, setView] = useState(

Loading...

); const [user, setUser] = useState(); @@ -45,8 +45,7 @@ const InitialCollection: RegisterVariantType = ({ transitionDisplay, receiveChan } useEffect(() => { - if (user && receiveChange) { - receiveChange(user); + if (user) { setView(

Hi, {user.firstname}! Great to meet you.

diff --git a/client/src/components/pages/Register/index.tsx b/client/src/components/pages/Register/index.tsx index 4d42422..bf39270 100644 --- a/client/src/components/pages/Register/index.tsx +++ b/client/src/components/pages/Register/index.tsx @@ -19,17 +19,17 @@ export enum VariantLabel { FinishUp } -const Register: FC<{receiveChange: (change: IUser) => void}> = ({ receiveChange }) => { +const Register = () => { const [displayed, setDisplayed] = useState(); - const authContext = useAuthContext(); + const { user } = useAuthContext(); - const transitionDisplay = (variant: number | VariantLabel, user?: IUser) => { + const transitionDisplay = (variant: number | VariantLabel) => { switch (variant) { case 0: setDisplayed(); break; case 1: - setDisplayed(); + setDisplayed(); break; case 2: setDisplayed(); @@ -38,7 +38,7 @@ const Register: FC<{receiveChange: (change: IUser) => void}> = ({ receiveChange setDisplayed(); break; default: - setDisplayed(); + setDisplayed(); break; } } diff --git a/client/src/components/ui/Navbar/index.tsx b/client/src/components/ui/Navbar/index.tsx index b15b1db..8e3aca1 100644 --- a/client/src/components/ui/Navbar/index.tsx +++ b/client/src/components/ui/Navbar/index.tsx @@ -1,44 +1,25 @@ -import { FC, useCallback, useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useEffect, useState } from "react"; import { LoggedIn, NotLoggedIn, Registering } from "./variants"; import { useAuthContext } from "../../../context/AuthContext"; -import { IUser } from "../../../schemas"; import "/src/sass/components/Navbar.scss"; -const Navbar: FC<{receiveChange: (change: IUser) => void}> = ({ receiveChange }) => { +const Navbar = () => { // setup and local state - const navigate = useNavigate(); - const { user, setUser } = useAuthContext(); - const [received, setReceived] = useState(); - const [displayed, setDisplayed] = useState(); - - // lift and store state from navbar variants - const liftChange = useCallback((newValue: IUser | undefined) => { - if (!newValue) { - return; - } - - setUser(newValue); - setReceived(newValue); - }, []) + const { user } = useAuthContext(); + const [displayed, setDisplayed] = useState(

Loading...

); const variants = { - loggedin: , - notloggedin: , - registering: + loggedin: , + notloggedin: , + registering: } // side effects for live rendering useEffect(() => { - user && setReceived(user); - }, [user]) + setDisplayed(user ? variants.loggedin : variants.notloggedin); + }, [user]); - useEffect(() => { - if (received) receiveChange(received); - setDisplayed(received ? variants.loggedin : variants.notloggedin); - }, [received, setReceived]); - - return displayed ||

Loading...

; + return displayed; } export default Navbar; \ No newline at end of file diff --git a/client/src/components/ui/Navbar/variants.tsx b/client/src/components/ui/Navbar/variants.tsx index a9fa831..e76694c 100644 --- a/client/src/components/ui/Navbar/variants.tsx +++ b/client/src/components/ui/Navbar/variants.tsx @@ -1,15 +1,26 @@ -import { attemptLogout } from "../../../util/apiUtils"; +import API from "../../../util/API"; import { NavbarType } from "../../../util/types"; -import { Button, Dropdown } from '../.' +import { Button, Dropdown } from '..' import { useState } from "react"; +import { useAuthContext } from "../../../context/AuthContext"; +import { useNavigate } from "react-router-dom"; + +const LoggedIn = () => { + const { user, setUser, setToken } = useAuthContext(); + const navigate = useNavigate(); + const auth = new API.Auth(); -const LoggedIn: NavbarType = ({ received, liftChange, navigate }) => { const [dropdownActive, setDropdownActive] = useState(false); const [searchActive, setSearchActive] = useState(false); const handleLogout = async () => { - const success = await attemptLogout(); - if (success) liftChange!(undefined); + const success = await auth.logout(); + console.log(success); + + // nullify cookie and unset user/token data + document.cookie = `token=;expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + setUser(undefined); + setToken(undefined); navigate('/'); } @@ -36,7 +47,7 @@ const LoggedIn: NavbarType = ({ received, liftChange, navigate }) => { navigate('/')}>RECIPIN
-

Hi, {received?.firstname}.

+

Hi, {user?.firstname}.

@@ -64,7 +75,9 @@ const LoggedIn: NavbarType = ({ received, liftChange, navigate }) => { ) } -const NotLoggedIn: NavbarType = ({ navigate }) => { +const NotLoggedIn = () => { + const navigate = useNavigate(); + return (