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