From 03ec2bf38c8e956a8b0aef94d1671ad52e33504a Mon Sep 17 00:00:00 2001 From: Mikayla Dobson <93477693+innocuous-symmetry@users.noreply.github.com> Date: Sat, 26 Nov 2022 12:52:59 -0600 Subject: [PATCH] added functionality for user subscriptions --- client/src/components/ui/BigButton.tsx | 0 client/src/components/ui/Checkbox.tsx | 16 +++ client/src/components/ui/Form.tsx | 14 ++- client/src/util/types.ts | 16 ++- server/auth/index.ts | 3 +- server/auth/middlewares.ts | 12 +++ server/controllers/CollectionCtl.ts | 16 +++ server/db/populate.ts | 12 +-- server/db/sql/create/createappusers.sql | 1 + server/db/sql/derived/checksubscription.sql | 3 + server/db/sql/get/getsubscriptions.sql | 13 +++ server/models/collection.ts | 102 +++++++++++++++++++- server/models/user.ts | 10 +- server/routes/auth.ts | 8 +- server/routes/collection.ts | 35 ++++++- server/routes/index.ts | 2 + server/routes/subscriptions.ts | 39 ++++++++ server/schemas/index.ts | 17 ++-- 18 files changed, 279 insertions(+), 40 deletions(-) delete mode 100644 client/src/components/ui/BigButton.tsx create mode 100644 server/db/sql/derived/checksubscription.sql create mode 100644 server/db/sql/get/getsubscriptions.sql create mode 100644 server/routes/subscriptions.ts diff --git a/client/src/components/ui/BigButton.tsx b/client/src/components/ui/BigButton.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/client/src/components/ui/Checkbox.tsx b/client/src/components/ui/Checkbox.tsx index e69de29..6b31eeb 100644 --- a/client/src/components/ui/Checkbox.tsx +++ b/client/src/components/ui/Checkbox.tsx @@ -0,0 +1,16 @@ +import { v4 } from 'uuid'; +import { CheckboxType } from '../../util/types'; + +// designed to be consumed by the Form class +const Checkbox = () => { + return ( <> + //
+ // + // FormElement.update(e, idx)}> + // + //
+ ) +} \ No newline at end of file diff --git a/client/src/components/ui/Form.tsx b/client/src/components/ui/Form.tsx index 5593047..3a72e84 100644 --- a/client/src/components/ui/Form.tsx +++ b/client/src/components/ui/Form.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent } from "react"; +import { ChangeEvent, ChangeEventHandler } from "react"; import { v4 } from 'uuid'; /** @@ -35,9 +35,15 @@ export default class Form{ } update(e: ChangeEvent, idx: number) { - let newState = { - ...this.state, - [this.keys[idx]]: e.target['value' as keyof EventTarget] + let newState; + + if (this.dataTypes[idx] == 'checkbox') { + newState = { ...this.state } + } else { + newState = { + ...this.state, + [this.keys[idx]]: e.target['value' as keyof EventTarget] + } } this.state = newState; diff --git a/client/src/util/types.ts b/client/src/util/types.ts index 51b688a..a70d8f1 100644 --- a/client/src/util/types.ts +++ b/client/src/util/types.ts @@ -1,5 +1,6 @@ -import { Dispatch, FC, ReactNode, SetStateAction } from "react"; +import { ChangeEvent, ChangeEventHandler, Dispatch, FC, ReactNode, SetStateAction } from "react"; import { useNavigate } from "react-router-dom"; +import { Form } from "../components/ui"; import { IUser } from "../schemas"; interface PortalBase { @@ -25,9 +26,20 @@ interface NavbarProps { liftChange?: (newValue: IUser | undefined) => void } +interface CheckboxProps { + rowid: string + id: string + idx: number + label: string + value: string + onChange: (e: ChangeEvent, idx: number) => void + FormElement: typeof Form +} + export type PageComponent = FC export type PanelComponent = FC export type ButtonComponent = FC export type ProtectPortal = FC export type UserCardType = FC -export type NavbarType = FC \ No newline at end of file +export type NavbarType = FC +export type CheckboxType = FC \ No newline at end of file diff --git a/server/auth/index.ts b/server/auth/index.ts index ccaa811..86b9a04 100644 --- a/server/auth/index.ts +++ b/server/auth/index.ts @@ -14,6 +14,7 @@ export default class AuthService { data.datecreated = now; data.datemodified = now; data.active = true; + data.isadmin = false; try { const user = await UserInstance.getOneByEmail(email); @@ -29,7 +30,6 @@ export default class AuthService { } const result = await UserInstance.post(newData); - if (result) console.log(result); return result; }) }) @@ -46,7 +46,6 @@ export default class AuthService { const user = await UserInstance.getOneByEmail(email); if (!user) return { ok: false, user: null } const match = await bcrypt.compare(password, user.password); - console.log(match); return { ok: match, user: match ? user : null diff --git a/server/auth/middlewares.ts b/server/auth/middlewares.ts index d036ff0..8746d7f 100644 --- a/server/auth/middlewares.ts +++ b/server/auth/middlewares.ts @@ -6,4 +6,16 @@ export function restrictAccess(req: Request, res: Response, next: NextFunction) } else { res.send({ ok: false, user: undefined }) } +} + +export function checkSubscription(req: Request, res: Response, next: NextFunction) { + +} + +export function checkFriendStatus(req: Request, res: Response, next: NextFunction) { + +} + +export function checkIsAdmin(req: Request, res: Response, next: NextFunction) { + } \ No newline at end of file diff --git a/server/controllers/CollectionCtl.ts b/server/controllers/CollectionCtl.ts index bc12576..6970a7b 100644 --- a/server/controllers/CollectionCtl.ts +++ b/server/controllers/CollectionCtl.ts @@ -21,4 +21,20 @@ export default class CollectionCtl { if (!result) throw createError('400', 'Bad request'); return result; } + + async getSubscriptions(userid: string) { + const result = await CollectionInstance.getSubscriptions(userid); + if (!result) throw createError('404', 'No subscriptions found'); + return result; + } + + async postSubscription(collectionid: string, userid: string) { + const { ok, code, data } = await CollectionInstance.postSubscription(collectionid, userid); + + if (ok) { + return data; + } else { + throw createError(code, data); + } + } } \ No newline at end of file diff --git a/server/db/populate.ts b/server/db/populate.ts index b2c2128..aae7103 100644 --- a/server/db/populate.ts +++ b/server/db/populate.ts @@ -6,13 +6,13 @@ export default async function populate() { const populateUsers = ` INSERT INTO recipin.appusers - (firstname, lastname, handle, email, password, active, datecreated, datemodified) + (firstname, lastname, handle, email, password, active, isadmin, datecreated, datemodified) VALUES - ('Mikayla', 'Dobson', 'innocuoussymmetry', 'mikaylaherself@gmail.com', 'password1', true, $1, $1), - ('Emily', 'Dobson', 'emjdobson', 'emily@email.com', 'password2', true, $1, $1), - ('Montanna', 'Dobson', 'delayedlemon', 'montanna@email.com', 'password3', true, $1, $1), - ('Christine', 'Riley', 'christine', 'christine@email.com', 'password4', true, $1, $1), - ('Someone', 'Not active', 'someone', 'someone@email.com', 'notactive', false, $1, $1) + ('Mikayla', 'Dobson', 'innocuoussymmetry', 'mikaylaherself@gmail.com', 'password1', true, true, $1, $1), + ('Emily', 'Dobson', 'emjdobson', 'emily@email.com', 'password2', true, false, $1, $1), + ('Montanna', 'Dobson', 'delayedlemon', 'montanna@email.com', 'password3', true, false, $1, $1), + ('Christine', 'Riley', 'christine', 'christine@email.com', 'password4', true, false, $1, $1), + ('Someone', 'Not active', 'someone', 'someone@email.com', 'notactive', false, false, $1, $1) ; ` diff --git a/server/db/sql/create/createappusers.sql b/server/db/sql/create/createappusers.sql index b1fd575..33bd589 100644 --- a/server/db/sql/create/createappusers.sql +++ b/server/db/sql/create/createappusers.sql @@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS recipin.appusers ( email varchar NOT NULL UNIQUE, password varchar NOT NULL, active boolean NOT NULL, + isadmin boolean NOT NULL, datecreated varchar NOT NULL, datemodified varchar NOT NULL ); \ No newline at end of file diff --git a/server/db/sql/derived/checksubscription.sql b/server/db/sql/derived/checksubscription.sql new file mode 100644 index 0000000..f84afd9 --- /dev/null +++ b/server/db/sql/derived/checksubscription.sql @@ -0,0 +1,3 @@ +-- SELECT * FROM cmp_usersubscriptions +-- WHERE usermemberid = $1 +-- AND collectionid = $2; \ No newline at end of file diff --git a/server/db/sql/get/getsubscriptions.sql b/server/db/sql/get/getsubscriptions.sql new file mode 100644 index 0000000..c019fdc --- /dev/null +++ b/server/db/sql/get/getsubscriptions.sql @@ -0,0 +1,13 @@ +SELECT + recipin.cmp_usersubscriptions.collectionid as collectionid, + recipin.collection.ownerid as ownerid, + recipin.cmp_usersubscriptions.usermemberid as memberid, + recipin.collection.name as collectionname, + recipin.appusers.firstname as owner_first, + recipin.appusers.lastname as owner_last +FROM recipin.collection +INNER JOIN recipin.appusers +ON recipin.collection.ownerid = recipin.appusers.id +INNER JOIN recipin.cmp_usersubscriptions +ON recipin.collection.id = recipin.cmp_usersubscriptions.collectionid +WHERE recipin.cmp_usersubscriptions.usermemberid = $1; \ No newline at end of file diff --git a/server/models/collection.ts b/server/models/collection.ts index c485042..1c739e7 100644 --- a/server/models/collection.ts +++ b/server/models/collection.ts @@ -1,6 +1,10 @@ -import { ICollection } from "../schemas"; +import { ICollection, IUser } from "../schemas"; +import { User } from "./user"; +import { appRoot } from "../appRoot"; +import now from "../util/now"; import pool from "../db"; - +import fs from 'fs'; +const UserInstance = new User(); export class Collection { async getOne(id: string) { try { @@ -31,11 +35,11 @@ export class Collection { try { const statement = ` INSERT INTO recipin.collection - (name, active, ismaincollection, ownerid) - VALUES ($1, $2, $3, $4) + (name, active, ismaincollection, ownerid, datecreated, datemodified) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING *; ` - const values = [name, active, ismaincollection, ownerid]; + const values = [name, (active || true), (ismaincollection || false), ownerid, now, now]; const result = await pool.query(statement, values); if (result.rows.length) return result.rows; return null; @@ -43,4 +47,92 @@ export class Collection { throw new Error(e); } } + + async getSubscriptions(userid: string) { + try { + const sql = fs.readFileSync(appRoot + '/db/sql/get/getsubscriptions.sql').toString(); + console.log(sql); + const result = await pool.query(sql, [userid]); + if (result.rows.length) return result.rows; + return null; + } catch (e: any) { + throw new Error(e); + } + } + + async postSubscription(collectionid: string, userid: string): Promise<{ ok: boolean, code: number, data: string | any[] }> { + try { + // ensure user exists + const user: IUser | null = await UserInstance.getOneByID(userid); + if (!user) { + return { + ok: false, + code: 404, + data: "User not found" + } + } + + // ensure collection exists + const target: ICollection | null = await this.getOne(collectionid); + if (!target) { + return { + ok: false, + code: 404, + data: "Collection not found" + } + } + + // ensure a user cannot subscribe to their own collection + if (target.ownerid == parseInt(userid)) { + return { + ok: false, + code: 403, + data: "User cannot subscribe to their own collection" + } + } + + // ensure a duplicate subscription does not exist + const allSubscriptions = ` + SELECT * FROM recipin.cmp_usersubscriptions + WHERE collectionid = $1; + ` + const subscriptionResult = await pool.query(allSubscriptions, [collectionid]); + if (subscriptionResult.rows?.length) { + for (let row of subscriptionResult.rows) { + if (row.usermemberid == parseInt(userid)) { + return { + ok: false, + code: 403, + data: "This user is already subscribed" + } + } + } + } + + // finally, execute insertion + const statement = ` + INSERT INTO recipin.cmp_usersubscriptions + (collectionid, usermemberid) + VALUES ($1, $2) + RETURNING *; + ` + + const result = await pool.query(statement, [collectionid, userid]); + if (result.rows.length) { + return { + ok: true, + code: 201, + data: result.rows + } + } + + return { + ok: false, + code: 400, + data: "Bad request. No data returned." + } + } catch (e: any) { + throw new Error(e); + } + } } \ No newline at end of file diff --git a/server/models/user.ts b/server/models/user.ts index 48be583..e1dc5e0 100644 --- a/server/models/user.ts +++ b/server/models/user.ts @@ -3,7 +3,7 @@ import fs from "fs"; import pgPromise from "pg-promise"; import pool from '../db'; import now from "../util/now"; -import { appRoot } from ".."; +import { appRoot } from "../appRoot"; const pgp = pgPromise({ capSQL: true }); export class User { @@ -73,7 +73,7 @@ export class User { async post(data: IUser) { - const { firstname, lastname, handle, email, password, active } = data; + const { firstname, lastname, handle, email, password, active, isadmin } = data; const datecreated = now; const datemodified = now; @@ -81,11 +81,11 @@ export class User { const statement = ` INSERT INTO recipin.appusers ( firstname, lastname, handle, email, password, - active, datecreated, datemodified) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + active, isadmin, datecreated, datemodified) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *; `; - const params = [firstname, lastname, handle, email, password, active, datecreated, datemodified]; + const params = [firstname, lastname, handle, email, password, active, isadmin, datecreated, datemodified]; const result = await pool.query(statement, params); if (result.rows.length) return result.rows; return null; diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 2232a47..3e7594c 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -17,7 +17,7 @@ export const authRoute = (app: Express, passport: PassportStatic) => { router.get('/', restrictAccess, (req, res, next) => { // @ts-ignore: does not recognize structure of req.user const user = req.user?.user; - const userData: IUser = { + const userData = { id: user.id, firstname: user.firstname, lastname: user.lastname, @@ -41,9 +41,7 @@ export const authRoute = (app: Express, passport: PassportStatic) => { if (error) throw error; console.log('login successful'); }) - // const { id, email, handle, firstname, lastname } = response.user; - // await UserControl.updateOne(response.user.id, { ...response.user, datemodified: now }) - // res.status(200).send({ id: id, handle: handle, firstname: firstname, lastname: lastname }); + res.cookie('userid', response.user.id, { maxAge: 1000 * 60 * 60 * 24 }); res.send(response); res.end(); @@ -70,8 +68,6 @@ export const authRoute = (app: Express, passport: PassportStatic) => { router.post('/register', async (req, res, next) => { try { const data: IUser = req.body; - const now = new Intl.DateTimeFormat('en-US', {}) - const response = await AuthInstance.register(data); res.status(200).send(response); } catch(e) { diff --git a/server/routes/collection.ts b/server/routes/collection.ts index 2ac45fd..2dd8356 100644 --- a/server/routes/collection.ts +++ b/server/routes/collection.ts @@ -1,4 +1,5 @@ import { Express, Router } from "express"; +import { restrictAccess } from "../auth/middlewares"; import CollectionCtl from "../controllers/CollectionCtl"; const CollectionInstance = new CollectionCtl(); @@ -7,7 +8,7 @@ const router = Router(); export const collectionRoute = (app: Express) => { app.use('/collection', router); - router.get('/:id', async (req, res, next) => { + router.get('/:id', restrictAccess, async (req, res, next) => { const { id } = req.params; try { const result = await CollectionInstance.getOne(id); @@ -17,7 +18,8 @@ export const collectionRoute = (app: Express) => { } }) - router.get('/', async (req, res, next) => { + // implement is admin on this route + router.get('/', restrictAccess, async (req, res, next) => { try { const result = await CollectionInstance.getAll(); res.status(200).send(result); @@ -26,7 +28,7 @@ export const collectionRoute = (app: Express) => { } }) - router.post('/', async (req, res, next) => { + router.post('/', restrictAccess, async (req, res, next) => { const data = req.body; try { const result = await CollectionInstance.post(data); @@ -35,4 +37,31 @@ export const collectionRoute = (app: Express) => { next(e); } }) + + // router.get('/subscriptions', restrictAccess, async (req, res, next) => { + // res.send('sanity check'); + // // // @ts-ignore + // // const { user } = req.user; + // // if (!user) return; + + // // try { + // // const result = await CollectionInstance.getSubscriptions("9"); + // // res.status(200).send(result); + // // } catch(e) { + // // next(e); + // // } + // }) + + // router.post('/subscribe', restrictAccess, async (req, res, next) => { + // // @ts-ignore + // const { user } = req.user; + // const { collection } = req.query; + + // try { + // const result = await CollectionInstance.postSubscription(collection as string, user.id as string); + // res.status(201).send(result); + // } catch(e) { + // next(e); + // } + // }) } \ No newline at end of file diff --git a/server/routes/index.ts b/server/routes/index.ts index f10fdc8..20d725f 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -6,6 +6,7 @@ import { collectionRoute } from "./collection"; import { ingredientRoute } from "./ingredient"; import { groceryListRoute } from "./groceryList"; import { authRoute } from "./auth"; +import { subscriptionRoute } from "./subscriptions"; export const routes = async (app: Express, passport: PassportStatic) => { console.log('routes called'); @@ -14,6 +15,7 @@ export const routes = async (app: Express, passport: PassportStatic) => { userRoute(app); recipeRoute(app); collectionRoute(app); + subscriptionRoute(app); ingredientRoute(app); groceryListRoute(app); } \ No newline at end of file diff --git a/server/routes/subscriptions.ts b/server/routes/subscriptions.ts new file mode 100644 index 0000000..2f559d8 --- /dev/null +++ b/server/routes/subscriptions.ts @@ -0,0 +1,39 @@ +import { Express, Router } from "express" +import { restrictAccess } from "../auth/middlewares"; +import { CollectionCtl } from "../controllers"; +const CollectionInstance = new CollectionCtl(); +const router = Router(); + +export const subscriptionRoute = (app: Express) => { + app.use('/subscription', router); + + router.get('/', async (req, res, next) => { + // @ts-ignore + const { user } = req.user; + if (!user) return; + + try { + const result = await CollectionInstance.getSubscriptions(user.id as string); + res.status(200).send(result); + } catch(e) { + next(e); + } + }) + + router.post('/', restrictAccess, async (req, res, next) => { + // @ts-ignore + const { user } = req.user; + const { collection } = req.query; + + try { + const result = await CollectionInstance.postSubscription(collection as string, user.id as string); + res.status(201).send(result); + } catch(e) { + next(e); + } + }) + + router.put('/', async (req, res, next) => { + + }) +} \ No newline at end of file diff --git a/server/schemas/index.ts b/server/schemas/index.ts index b359e5c..78ada8e 100644 --- a/server/schemas/index.ts +++ b/server/schemas/index.ts @@ -1,3 +1,8 @@ +// defined shared characteristics for DB entities +interface DBEntity { + id?: number +} + interface HasHistory extends DBEntity { datecreated?: string datemodified?: string @@ -7,15 +12,13 @@ interface CanDeactivate extends DBEntity { active?: boolean } -interface DBEntity { - id?: number -} - +// data models export interface IUser extends HasHistory, CanDeactivate { firstname: string lastname: string handle: string email: string + isadmin: boolean password?: string } @@ -28,7 +31,7 @@ export interface IRecipe extends HasHistory, CanDeactivate { name: string description?: string preptime: string - authoruserid?: IUser["id"] + authoruserid: IUser["id"] } export interface IIngredient extends HasHistory { @@ -39,10 +42,10 @@ export interface IIngredient extends HasHistory { export interface ICollection extends HasHistory, CanDeactivate { name: string ismaincollection: boolean - ownerid?: IUser["id"] + ownerid: IUser["id"] } export interface IGroceryList extends HasHistory, CanDeactivate { name: string - ownerid?: IUser["id"] + ownerid: IUser["id"] } \ No newline at end of file