From a7f3fd6e10a08509abde13f6d460d4bc62f59645 Mon Sep 17 00:00:00 2001 From: Mikayla Dobson <93477693+innocuous-symmetry@users.noreply.github.com> Date: Sat, 18 Feb 2023 10:58:58 -0600 Subject: [PATCH] api maintenance --- client/src/App.tsx | 6 +- client/src/components/pages/AddFriends.tsx | 2 +- client/src/components/pages/AddRecipe.tsx | 5 +- client/src/components/pages/Collection.tsx | 2 +- .../components/pages/CollectionBrowser.tsx | 5 +- client/src/components/pages/Login.tsx | 2 +- client/src/components/pages/Profile.tsx | 2 +- client/src/components/pages/Recipe.tsx | 5 +- .../src/components/pages/StatusPages/403.tsx | 16 +++++ .../src/components/pages/StatusPages/404.tsx | 16 +++++ client/src/components/ui/Browser.tsx | 2 +- client/src/util/API.ts | 5 ++ client/src/util/Protect.tsx | 54 +++++++++++------ client/src/util/types.ts | 7 +++ server/controllers/UserCtl.d.ts | 4 ++ server/controllers/UserCtl.ts | 58 +++++++++++++++++++ server/models/user.ts | 12 ++++ server/routes/friend.ts | 9 ++- 18 files changed, 180 insertions(+), 32 deletions(-) create mode 100644 client/src/components/pages/StatusPages/403.tsx create mode 100644 client/src/components/pages/StatusPages/404.tsx create mode 100644 server/controllers/UserCtl.d.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 7d48201..62389e5 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -50,9 +50,12 @@ function App() {
+ {/* Base access privileges */} } /> } /> } /> + + {/* Protected routes */} } /> } /> } /> @@ -61,10 +64,11 @@ function App() { } /> } /> } /> - } /> } /> } /> + + {/* For dev use */} } />
diff --git a/client/src/components/pages/AddFriends.tsx b/client/src/components/pages/AddFriends.tsx index 8c2fe86..a7fe3ff 100644 --- a/client/src/components/pages/AddFriends.tsx +++ b/client/src/components/pages/AddFriends.tsx @@ -4,7 +4,7 @@ import FriendSearchWidget from "../ui/Widgets/NewFriendWidget" const AddFriends = () => { return ( - +

Search for New Friends

diff --git a/client/src/components/pages/AddRecipe.tsx b/client/src/components/pages/AddRecipe.tsx index 40af9a5..6e26aa7 100644 --- a/client/src/components/pages/AddRecipe.tsx +++ b/client/src/components/pages/AddRecipe.tsx @@ -6,6 +6,7 @@ import API from "../../util/API"; import { useSelectorContext } from "../../context/SelectorContext"; import IngredientSelector from "../derived/IngredientSelector"; import { v4 } from "uuid"; +import Protect from "../../util/Protect"; const AddRecipe = () => { const { user, token } = useAuthContext(); @@ -99,7 +100,7 @@ const AddRecipe = () => { } return ( - +

Add a New Recipe

@@ -140,7 +141,7 @@ const AddRecipe = () => {
{ toast }
-
+
) } diff --git a/client/src/components/pages/Collection.tsx b/client/src/components/pages/Collection.tsx index a4ba9b4..7fe49eb 100644 --- a/client/src/components/pages/Collection.tsx +++ b/client/src/components/pages/Collection.tsx @@ -62,7 +62,7 @@ const Collection = () => { }, [data, recipes]) return ( - + { content } ) diff --git a/client/src/components/pages/CollectionBrowser.tsx b/client/src/components/pages/CollectionBrowser.tsx index 2ee8aca..d05c564 100644 --- a/client/src/components/pages/CollectionBrowser.tsx +++ b/client/src/components/pages/CollectionBrowser.tsx @@ -3,6 +3,7 @@ import { v4 } from "uuid"; import { useAuthContext } from "../../context/AuthContext"; import { ICollection } from "../../schemas"; import API from "../../util/API"; +import Protect from "../../util/Protect"; import { Page, Panel } from "../ui"; const CollectionBrowser = () => { @@ -47,7 +48,7 @@ const CollectionBrowser = () => { }, [list]) return ( - + { list && ( <>

Browsing your {list.length} collection{ (list.length !== 1) && "s" }:

@@ -62,7 +63,7 @@ const CollectionBrowser = () => { })} )} -
+
) } diff --git a/client/src/components/pages/Login.tsx b/client/src/components/pages/Login.tsx index 78c0ed6..5461b4d 100644 --- a/client/src/components/pages/Login.tsx +++ b/client/src/components/pages/Login.tsx @@ -29,7 +29,7 @@ export default function Login() { setToken(result.token); // if there is a redirect, go there, else go home - navigate(`/${redirect ?? ''}`); + navigate(redirect ?? '/'); } // check for logged in user and mount form diff --git a/client/src/components/pages/Profile.tsx b/client/src/components/pages/Profile.tsx index 8dd06af..9c3af5e 100644 --- a/client/src/components/pages/Profile.tsx +++ b/client/src/components/pages/Profile.tsx @@ -160,7 +160,7 @@ export default function Profile() { // if this is the current user's profile setContents( - +

{user!.firstname}'s Profile

diff --git a/client/src/components/pages/Recipe.tsx b/client/src/components/pages/Recipe.tsx index d6b2720..c65eb3a 100644 --- a/client/src/components/pages/Recipe.tsx +++ b/client/src/components/pages/Recipe.tsx @@ -3,6 +3,7 @@ import { useParams } from "react-router-dom"; import { Page, Panel } from "../ui"; import { IRecipe } from "../../schemas"; import { getRecipeByID } from "../../util/apiUtils"; +import Protect from "../../util/Protect"; export default function Recipe() { const [recipe, setRecipe] = useState(); @@ -23,7 +24,7 @@ export default function Recipe() { }, []) return ( - + { recipe && (

{recipe.name}

@@ -31,6 +32,6 @@ export default function Recipe() {

{recipe.preptime}

)} -
+ ) } \ No newline at end of file diff --git a/client/src/components/pages/StatusPages/403.tsx b/client/src/components/pages/StatusPages/403.tsx new file mode 100644 index 0000000..1e22e42 --- /dev/null +++ b/client/src/components/pages/StatusPages/403.tsx @@ -0,0 +1,16 @@ +import { useNavigate } from "react-router-dom"; +import { Button, Divider, Page } from "../../ui"; + +export default function AccessForbidden({ children = <> }) { + const navigate = useNavigate(); + + return ( + +

403: Unauthorized

+ { children } + + + +
+ ) +} \ No newline at end of file diff --git a/client/src/components/pages/StatusPages/404.tsx b/client/src/components/pages/StatusPages/404.tsx new file mode 100644 index 0000000..6907a76 --- /dev/null +++ b/client/src/components/pages/StatusPages/404.tsx @@ -0,0 +1,16 @@ +import { useNavigate } from "react-router-dom"; +import { Button, Divider, Page } from "../../ui"; + +export default function ResourceNotFound({ children = <> }) { + const navigate = useNavigate(); + + return ( + +

404: We didn't find what you are looking for

+ { children } + + + +
+ ) +} \ No newline at end of file diff --git a/client/src/components/ui/Browser.tsx b/client/src/components/ui/Browser.tsx index 8516ca0..ec23f34 100644 --- a/client/src/components/ui/Browser.tsx +++ b/client/src/components/ui/Browser.tsx @@ -16,7 +16,7 @@ const Browser: FC = ({ children, header, searchFunction }) => { }) return ( - +

{header}

) diff --git a/client/src/util/API.ts b/client/src/util/API.ts index cfae924..139d59a 100644 --- a/client/src/util/API.ts +++ b/client/src/util/API.ts @@ -172,6 +172,11 @@ module API { return Promise.resolve(response.data); } + async getActiveFriends() { + const response = await this.instance.get(this.endpoint + "?accepted=true", this.headers); + return Promise.resolve(response.data); + } + async addFriend(id: string | number) { const response = await this.instance.post(this.endpoint + `/${id}`, this.headers); return Promise.resolve(response.data); diff --git a/client/src/util/Protect.tsx b/client/src/util/Protect.tsx index 8ba895b..edc7b9a 100644 --- a/client/src/util/Protect.tsx +++ b/client/src/util/Protect.tsx @@ -1,31 +1,47 @@ +import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; +import AccessForbidden from "../components/pages/StatusPages/403"; import { Button, Page } from "../components/ui"; -import Divider from "../components/ui/Divider"; import { useAuthContext } from "../context/AuthContext"; +import API from "./API"; import { ProtectPortal } from "./types"; -const Protect: ProtectPortal = ({ children, redirect = '' }) => { - const { user } = useAuthContext(); +const Protect: ProtectPortal = ({ children, redirect = '', accessRules = null }) => { + const [view, setView] = useState(

Loading...

); + const { user, token } = useAuthContext(); const navigate = useNavigate(); - if (!user) { - return ( - -
-

Hi there! You don't look too familiar.

+ useEffect(() => { + if (!user || !token) { + setView( + + <> +

Hi there! You don't look too familiar.

To view the content on this page, please log in below:

- -
-
- ) - } else { - return ( - - { children || <> } - - ) - } + + + ) + + return; + } + + if (accessRules !== null) { + if (accessRules.mustBeRecipinAdmin && !(user.isadmin)) { + setView( + + <> +

This page requires administrator access.

+

If you believe you are receiving this message in error, please contact Recipin support.

+ +
+ ) + } + } + }, [user, token]) + + + return view; } export default Protect; \ No newline at end of file diff --git a/client/src/util/types.ts b/client/src/util/types.ts index caef66f..fd5ba5d 100644 --- a/client/src/util/types.ts +++ b/client/src/util/types.ts @@ -15,8 +15,15 @@ interface ButtonParams extends PortalBase { disabledText?: string } +export interface AccessRules { + mustBeRecipinAdmin: boolean + mustBeFriend: boolean + mustBeSubscribed: boolean +} + export interface ProtectParams extends PortalBase { redirect?: string + accessRules?: AccessRules | null } interface UserCardProps extends PortalBase { diff --git a/server/controllers/UserCtl.d.ts b/server/controllers/UserCtl.d.ts new file mode 100644 index 0000000..3a56a5b --- /dev/null +++ b/server/controllers/UserCtl.d.ts @@ -0,0 +1,4 @@ +/** + * @method getAll + * @returns { ControllerResponse } + */ \ No newline at end of file diff --git a/server/controllers/UserCtl.ts b/server/controllers/UserCtl.ts index a33b7cf..03f515a 100644 --- a/server/controllers/UserCtl.ts +++ b/server/controllers/UserCtl.ts @@ -5,6 +5,18 @@ import { StatusCode } from '../util/types'; const UserInstance = new User(); export default class UserCtl { + /* * * * * * * * * * * * * * * * * * * * * * * * * * * + * FIRST SECTION: + * METHODS SPECIFIC TO USERS AND USER DATA + * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + /** + * ### @method getAll + * returns all available user entries + * + * @params (none) + * @returns list of users, or an explanatory string if no response is received + */ async getAll() { try { // attempt to get users from database @@ -22,6 +34,11 @@ export default class UserCtl { } } + /** + * ### @method post + * @param body - serialized user data as { IUser } + * @returns the newly inserted user entry, or an explanatory string + */ async post(body: IUser) { try { const response = await UserInstance.post(body); @@ -34,6 +51,11 @@ export default class UserCtl { } } + /** + * ### @method getOne + * @param id - user id to query + * @returns the user entry, if found, or an explanatory string if none was found + */ async getOne(id: number | string) { try { const user = await UserInstance.getOneByID(id); @@ -46,6 +68,12 @@ export default class UserCtl { } } + /** + * ### @method updateOne + * @param id - user id to update + * @param body - the new user body to update with + * @returns the updated user body, or an explanatory string + */ async updateOne(id: number | string, body: IUser) { try { const result = await UserInstance.updateOneByID(id, body); @@ -58,6 +86,16 @@ export default class UserCtl { } } + /* * * * * * * * * * * * * * * * * * * * * * * * * * * + * SECOND SECTION: + * METHODS SPECIFIC TO FRIENDSHIPS BETWEEN USERS + * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + /** + * ### @method getFriends + * @param id - get all friendship entries for a user, regardless of status + * @returns a list of friendship entries, or an explanatory string if none are found + */ async getFriends(id: number | string) { try { const result = await UserInstance.getFriends(id); @@ -70,6 +108,12 @@ export default class UserCtl { } } + /** + * ### @method getFriendshipByID + * @param id - the ID of the friendship in question + * @param userid - the user ID of the logged in user, to verify permissions + * @returns a friendship entry, or an explanatory string + */ async getFriendshipByID(id: number | string, userid: number | string) { try { const { ok, code, result } = await UserInstance.getFriendshipByID(id, userid); @@ -79,6 +123,11 @@ export default class UserCtl { } } + /** + * ### @method getPendingFriendRequests + * + * *IMPORTANT*: I don't think this one works the way I think it does + */ async getPendingFriendRequests(recipient: string | number) { try { const { ok, code, result } = await UserInstance.getPendingFriendRequests(recipient); @@ -88,6 +137,15 @@ export default class UserCtl { } } + async getAcceptedFriends(userid: number | string) { + try { + const { code, result } = await UserInstance.getAcceptedFriends(userid); + return new ControllerResponse(code, result); + } catch (e: any) { + throw new Error(e); + } + } + async addFriendship(userid: number | string, targetid: number | string) { try { const result = await UserInstance.addFriendship(userid, targetid); diff --git a/server/models/user.ts b/server/models/user.ts index caf6561..28dcb3e 100644 --- a/server/models/user.ts +++ b/server/models/user.ts @@ -140,6 +140,18 @@ export class User { } } + async getAcceptedFriends(userid: number | string) { + try { + const statement = `SELECT * FROM recipin.cmp_userfriendships WHERE active = true AND (senderid = $1) OR (targetid = $1);` + const result = await pool.query(statement, [userid]); + + if (result.rows.length) return { ok: true, code: StatusCode.OK, result: result.rows } + return { ok: true, code: StatusCode.NotFound, result: "No pending friend requests found" } + } catch (e: any) { + throw new Error(e); + } + } + async addFriendship(userid: number | string, targetid: number | string) { try { const statement = ` diff --git a/server/routes/friend.ts b/server/routes/friend.ts index c353fb8..1d4d305 100644 --- a/server/routes/friend.ts +++ b/server/routes/friend.ts @@ -2,6 +2,7 @@ import { Express, Router } from 'express'; import { restrictAccess } from '../auth/middlewares'; import { UserCtl } from '../controllers'; import { IUser } from '../schemas'; +import { StatusCode } from '../util/types'; const UserInstance = new UserCtl(); const router = Router(); @@ -24,12 +25,15 @@ export const friendRouter = (app: Express) => { // get all friendships for a user router.get('/', async (req, res, next) => { const user = req.user as IUser; - const { pending, targetUser } = req.query; + const { pending, accepted, targetUser } = req.query; try { if (pending) { const { code, data } = await UserInstance.getPendingFriendRequests(user.id as number); res.status(code).send(data); + } else if (accepted) { + const { code, data } = await UserInstance.getAcceptedFriends(user.id as number); + res.status(code).send(data); } else { if (targetUser) { const { code, data } = await UserInstance.getFriends(parseInt(targetUser as string)); @@ -39,6 +43,9 @@ export const friendRouter = (app: Express) => { res.status(code).send(data); } } + + // send server error in case any of these conditions not landing + res.status(StatusCode.ServerError).json({ message: "An unexpected error occurred." }); } catch(e) { next(e); }