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 }
+
+
+ navigate('/')}>Home
+
+ )
+}
\ 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 }
+
+
+ navigate('/')}>Home
+
+ )
+}
\ 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:
-
navigate(redirect ? `/login?redirect=${redirect}` : '/login')}>Log In
-
-
- )
- } 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);
}