added functionality for user subscriptions

This commit is contained in:
Mikayla Dobson
2022-11-26 12:52:59 -06:00
parent 99c48d2a6e
commit 03ec2bf38c
18 changed files with 279 additions and 40 deletions

View File

@@ -0,0 +1,16 @@
import { v4 } from 'uuid';
import { CheckboxType } from '../../util/types';
// designed to be consumed by the Form class
const Checkbox = () => {
return ( <></>
// <div id={rowid} key={v4()}>
// <label>{label}</label>
// <input
// type="checkbox"
// id={id}
// onChange={(e) => FormElement.update(e, idx)}>
// </input>
// </div>
)
}

View File

@@ -1,4 +1,4 @@
import { ChangeEvent } from "react"; import { ChangeEvent, ChangeEventHandler } from "react";
import { v4 } from 'uuid'; import { v4 } from 'uuid';
/** /**
@@ -35,9 +35,15 @@ export default class Form<T>{
} }
update(e: ChangeEvent<HTMLElement>, idx: number) { update(e: ChangeEvent<HTMLElement>, idx: number) {
let newState = { let newState;
...this.state,
[this.keys[idx]]: e.target['value' as keyof EventTarget] if (this.dataTypes[idx] == 'checkbox') {
newState = { ...this.state }
} else {
newState = {
...this.state,
[this.keys[idx]]: e.target['value' as keyof EventTarget]
}
} }
this.state = newState; this.state = newState;

View File

@@ -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 { useNavigate } from "react-router-dom";
import { Form } from "../components/ui";
import { IUser } from "../schemas"; import { IUser } from "../schemas";
interface PortalBase { interface PortalBase {
@@ -25,9 +26,20 @@ interface NavbarProps {
liftChange?: (newValue: IUser | undefined) => void liftChange?: (newValue: IUser | undefined) => void
} }
interface CheckboxProps {
rowid: string
id: string
idx: number
label: string
value: string
onChange: (e: ChangeEvent<HTMLElement>, idx: number) => void
FormElement: typeof Form
}
export type PageComponent = FC<PortalBase> export type PageComponent = FC<PortalBase>
export type PanelComponent = FC<PortalBase> export type PanelComponent = FC<PortalBase>
export type ButtonComponent = FC<ButtonParams> export type ButtonComponent = FC<ButtonParams>
export type ProtectPortal = FC<MultiChildPortal> export type ProtectPortal = FC<MultiChildPortal>
export type UserCardType = FC<UserCardProps> export type UserCardType = FC<UserCardProps>
export type NavbarType = FC<NavbarProps> export type NavbarType = FC<NavbarProps>
export type CheckboxType = FC<CheckboxProps>

View File

@@ -14,6 +14,7 @@ export default class AuthService {
data.datecreated = now; data.datecreated = now;
data.datemodified = now; data.datemodified = now;
data.active = true; data.active = true;
data.isadmin = false;
try { try {
const user = await UserInstance.getOneByEmail(email); const user = await UserInstance.getOneByEmail(email);
@@ -29,7 +30,6 @@ export default class AuthService {
} }
const result = await UserInstance.post(newData); const result = await UserInstance.post(newData);
if (result) console.log(result);
return result; return result;
}) })
}) })
@@ -46,7 +46,6 @@ export default class AuthService {
const user = await UserInstance.getOneByEmail(email); const user = await UserInstance.getOneByEmail(email);
if (!user) return { ok: false, user: null } if (!user) return { ok: false, user: null }
const match = await bcrypt.compare(password, user.password); const match = await bcrypt.compare(password, user.password);
console.log(match);
return { return {
ok: match, ok: match,
user: match ? user : null user: match ? user : null

View File

@@ -6,4 +6,16 @@ export function restrictAccess(req: Request, res: Response, next: NextFunction)
} else { } else {
res.send({ ok: false, user: undefined }) 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) {
} }

View File

@@ -21,4 +21,20 @@ export default class CollectionCtl {
if (!result) throw createError('400', 'Bad request'); if (!result) throw createError('400', 'Bad request');
return result; 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);
}
}
} }

View File

@@ -6,13 +6,13 @@ export default async function populate() {
const populateUsers = ` const populateUsers = `
INSERT INTO recipin.appusers INSERT INTO recipin.appusers
(firstname, lastname, handle, email, password, active, datecreated, datemodified) (firstname, lastname, handle, email, password, active, isadmin, datecreated, datemodified)
VALUES VALUES
('Mikayla', 'Dobson', 'innocuoussymmetry', 'mikaylaherself@gmail.com', 'password1', true, $1, $1), ('Mikayla', 'Dobson', 'innocuoussymmetry', 'mikaylaherself@gmail.com', 'password1', true, true, $1, $1),
('Emily', 'Dobson', 'emjdobson', 'emily@email.com', 'password2', true, $1, $1), ('Emily', 'Dobson', 'emjdobson', 'emily@email.com', 'password2', true, false, $1, $1),
('Montanna', 'Dobson', 'delayedlemon', 'montanna@email.com', 'password3', true, $1, $1), ('Montanna', 'Dobson', 'delayedlemon', 'montanna@email.com', 'password3', true, false, $1, $1),
('Christine', 'Riley', 'christine', 'christine@email.com', 'password4', true, $1, $1), ('Christine', 'Riley', 'christine', 'christine@email.com', 'password4', true, false, $1, $1),
('Someone', 'Not active', 'someone', 'someone@email.com', 'notactive', false, $1, $1) ('Someone', 'Not active', 'someone', 'someone@email.com', 'notactive', false, false, $1, $1)
; ;
` `

View File

@@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS recipin.appusers (
email varchar NOT NULL UNIQUE, email varchar NOT NULL UNIQUE,
password varchar NOT NULL, password varchar NOT NULL,
active boolean NOT NULL, active boolean NOT NULL,
isadmin boolean NOT NULL,
datecreated varchar NOT NULL, datecreated varchar NOT NULL,
datemodified varchar NOT NULL datemodified varchar NOT NULL
); );

View File

@@ -0,0 +1,3 @@
-- SELECT * FROM cmp_usersubscriptions
-- WHERE usermemberid = $1
-- AND collectionid = $2;

View File

@@ -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;

View File

@@ -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 pool from "../db";
import fs from 'fs';
const UserInstance = new User();
export class Collection { export class Collection {
async getOne(id: string) { async getOne(id: string) {
try { try {
@@ -31,11 +35,11 @@ export class Collection {
try { try {
const statement = ` const statement = `
INSERT INTO recipin.collection INSERT INTO recipin.collection
(name, active, ismaincollection, ownerid) (name, active, ismaincollection, ownerid, datecreated, datemodified)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *; RETURNING *;
` `
const values = [name, active, ismaincollection, ownerid]; const values = [name, (active || true), (ismaincollection || false), ownerid, now, now];
const result = await pool.query(statement, values); const result = await pool.query(statement, values);
if (result.rows.length) return result.rows; if (result.rows.length) return result.rows;
return null; return null;
@@ -43,4 +47,92 @@ export class Collection {
throw new Error(e); 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);
}
}
} }

View File

@@ -3,7 +3,7 @@ import fs from "fs";
import pgPromise from "pg-promise"; import pgPromise from "pg-promise";
import pool from '../db'; import pool from '../db';
import now from "../util/now"; import now from "../util/now";
import { appRoot } from ".."; import { appRoot } from "../appRoot";
const pgp = pgPromise({ capSQL: true }); const pgp = pgPromise({ capSQL: true });
export class User { export class User {
@@ -73,7 +73,7 @@ export class User {
async post(data: IUser) { 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 datecreated = now;
const datemodified = now; const datemodified = now;
@@ -81,11 +81,11 @@ export class User {
const statement = ` const statement = `
INSERT INTO recipin.appusers ( INSERT INTO recipin.appusers (
firstname, lastname, handle, email, password, firstname, lastname, handle, email, password,
active, datecreated, datemodified) active, isadmin, datecreated, datemodified)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *; 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); const result = await pool.query(statement, params);
if (result.rows.length) return result.rows; if (result.rows.length) return result.rows;
return null; return null;

View File

@@ -17,7 +17,7 @@ export const authRoute = (app: Express, passport: PassportStatic) => {
router.get('/', restrictAccess, (req, res, next) => { router.get('/', restrictAccess, (req, res, next) => {
// @ts-ignore: does not recognize structure of req.user // @ts-ignore: does not recognize structure of req.user
const user = req.user?.user; const user = req.user?.user;
const userData: IUser = { const userData = {
id: user.id, id: user.id,
firstname: user.firstname, firstname: user.firstname,
lastname: user.lastname, lastname: user.lastname,
@@ -41,9 +41,7 @@ export const authRoute = (app: Express, passport: PassportStatic) => {
if (error) throw error; if (error) throw error;
console.log('login successful'); 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.cookie('userid', response.user.id, { maxAge: 1000 * 60 * 60 * 24 });
res.send(response); res.send(response);
res.end(); res.end();
@@ -70,8 +68,6 @@ export const authRoute = (app: Express, passport: PassportStatic) => {
router.post('/register', async (req, res, next) => { router.post('/register', async (req, res, next) => {
try { try {
const data: IUser = req.body; const data: IUser = req.body;
const now = new Intl.DateTimeFormat('en-US', {})
const response = await AuthInstance.register(data); const response = await AuthInstance.register(data);
res.status(200).send(response); res.status(200).send(response);
} catch(e) { } catch(e) {

View File

@@ -1,4 +1,5 @@
import { Express, Router } from "express"; import { Express, Router } from "express";
import { restrictAccess } from "../auth/middlewares";
import CollectionCtl from "../controllers/CollectionCtl"; import CollectionCtl from "../controllers/CollectionCtl";
const CollectionInstance = new CollectionCtl(); const CollectionInstance = new CollectionCtl();
@@ -7,7 +8,7 @@ const router = Router();
export const collectionRoute = (app: Express) => { export const collectionRoute = (app: Express) => {
app.use('/collection', router); app.use('/collection', router);
router.get('/:id', async (req, res, next) => { router.get('/:id', restrictAccess, async (req, res, next) => {
const { id } = req.params; const { id } = req.params;
try { try {
const result = await CollectionInstance.getOne(id); 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 { try {
const result = await CollectionInstance.getAll(); const result = await CollectionInstance.getAll();
res.status(200).send(result); 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; const data = req.body;
try { try {
const result = await CollectionInstance.post(data); const result = await CollectionInstance.post(data);
@@ -35,4 +37,31 @@ export const collectionRoute = (app: Express) => {
next(e); 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);
// }
// })
} }

View File

@@ -6,6 +6,7 @@ import { collectionRoute } from "./collection";
import { ingredientRoute } from "./ingredient"; import { ingredientRoute } from "./ingredient";
import { groceryListRoute } from "./groceryList"; import { groceryListRoute } from "./groceryList";
import { authRoute } from "./auth"; import { authRoute } from "./auth";
import { subscriptionRoute } from "./subscriptions";
export const routes = async (app: Express, passport: PassportStatic) => { export const routes = async (app: Express, passport: PassportStatic) => {
console.log('routes called'); console.log('routes called');
@@ -14,6 +15,7 @@ export const routes = async (app: Express, passport: PassportStatic) => {
userRoute(app); userRoute(app);
recipeRoute(app); recipeRoute(app);
collectionRoute(app); collectionRoute(app);
subscriptionRoute(app);
ingredientRoute(app); ingredientRoute(app);
groceryListRoute(app); groceryListRoute(app);
} }

View File

@@ -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) => {
})
}

View File

@@ -1,3 +1,8 @@
// defined shared characteristics for DB entities
interface DBEntity {
id?: number
}
interface HasHistory extends DBEntity { interface HasHistory extends DBEntity {
datecreated?: string datecreated?: string
datemodified?: string datemodified?: string
@@ -7,15 +12,13 @@ interface CanDeactivate extends DBEntity {
active?: boolean active?: boolean
} }
interface DBEntity { // data models
id?: number
}
export interface IUser extends HasHistory, CanDeactivate { export interface IUser extends HasHistory, CanDeactivate {
firstname: string firstname: string
lastname: string lastname: string
handle: string handle: string
email: string email: string
isadmin: boolean
password?: string password?: string
} }
@@ -28,7 +31,7 @@ export interface IRecipe extends HasHistory, CanDeactivate {
name: string name: string
description?: string description?: string
preptime: string preptime: string
authoruserid?: IUser["id"] authoruserid: IUser["id"]
} }
export interface IIngredient extends HasHistory { export interface IIngredient extends HasHistory {
@@ -39,10 +42,10 @@ export interface IIngredient extends HasHistory {
export interface ICollection extends HasHistory, CanDeactivate { export interface ICollection extends HasHistory, CanDeactivate {
name: string name: string
ismaincollection: boolean ismaincollection: boolean
ownerid?: IUser["id"] ownerid: IUser["id"]
} }
export interface IGroceryList extends HasHistory, CanDeactivate { export interface IGroceryList extends HasHistory, CanDeactivate {
name: string name: string
ownerid?: IUser["id"] ownerid: IUser["id"]
} }