api maintenance

This commit is contained in:
Mikayla Dobson
2023-02-18 10:58:58 -06:00
parent 9e146f0825
commit a7f3fd6e10
18 changed files with 180 additions and 32 deletions

View File

@@ -50,9 +50,12 @@ function App() {
<div className="App"> <div className="App">
<Navbar /> <Navbar />
<Routes> <Routes>
{/* Base access privileges */}
<Route path="/" element={<Welcome />} /> <Route path="/" element={<Welcome />} />
<Route path="/register" element={<Register />} /> <Route path="/register" element={<Register />} />
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
{/* Protected routes */}
<Route path="/profile" element={<Profile />} /> <Route path="/profile" element={<Profile />} />
<Route path="/collections" element={<CollectionBrowser />} /> <Route path="/collections" element={<CollectionBrowser />} />
<Route path="/collections/:id" element={<Collection />} /> <Route path="/collections/:id" element={<Collection />} />
@@ -61,10 +64,11 @@ function App() {
<Route path="/recipe/:id" element={<Recipe />} /> <Route path="/recipe/:id" element={<Recipe />} />
<Route path="/subscriptions" element={<Subscriptions />} /> <Route path="/subscriptions" element={<Subscriptions />} />
<Route path="/subscriptions/:id" element={<Collection />} /> <Route path="/subscriptions/:id" element={<Collection />} />
<Route path="/add-recipe" element={<AddRecipe />} /> <Route path="/add-recipe" element={<AddRecipe />} />
<Route path="/grocery-list" element={<GroceryListCollection />} /> <Route path="/grocery-list" element={<GroceryListCollection />} />
<Route path="/grocery-list/:id" element={<GroceryList />} /> <Route path="/grocery-list/:id" element={<GroceryList />} />
{/* For dev use */}
<Route path="/sandbox" element={<Sandbox />} /> <Route path="/sandbox" element={<Sandbox />} />
</Routes> </Routes>
</div> </div>

View File

@@ -4,7 +4,7 @@ import FriendSearchWidget from "../ui/Widgets/NewFriendWidget"
const AddFriends = () => { const AddFriends = () => {
return ( return (
<Protect> <Protect redirect="/add-friends">
<h1>Search for New Friends</h1> <h1>Search for New Friends</h1>
<Divider /> <Divider />

View File

@@ -6,6 +6,7 @@ import API from "../../util/API";
import { useSelectorContext } from "../../context/SelectorContext"; import { useSelectorContext } from "../../context/SelectorContext";
import IngredientSelector from "../derived/IngredientSelector"; import IngredientSelector from "../derived/IngredientSelector";
import { v4 } from "uuid"; import { v4 } from "uuid";
import Protect from "../../util/Protect";
const AddRecipe = () => { const AddRecipe = () => {
const { user, token } = useAuthContext(); const { user, token } = useAuthContext();
@@ -99,7 +100,7 @@ const AddRecipe = () => {
} }
return ( return (
<Page> <Protect redirect="/add-recipe">
<h1>Add a New Recipe</h1> <h1>Add a New Recipe</h1>
<Divider /> <Divider />
@@ -140,7 +141,7 @@ const AddRecipe = () => {
<div id="toast">{ toast }</div> <div id="toast">{ toast }</div>
</Panel> </Panel>
</Page> </Protect>
) )
} }

View File

@@ -62,7 +62,7 @@ const Collection = () => {
}, [data, recipes]) }, [data, recipes])
return ( return (
<Protect> <Protect redirect={`/collections/${id}`}>
{ content } { content }
</Protect> </Protect>
) )

View File

@@ -3,6 +3,7 @@ import { v4 } from "uuid";
import { useAuthContext } from "../../context/AuthContext"; import { useAuthContext } from "../../context/AuthContext";
import { ICollection } from "../../schemas"; import { ICollection } from "../../schemas";
import API from "../../util/API"; import API from "../../util/API";
import Protect from "../../util/Protect";
import { Page, Panel } from "../ui"; import { Page, Panel } from "../ui";
const CollectionBrowser = () => { const CollectionBrowser = () => {
@@ -47,7 +48,7 @@ const CollectionBrowser = () => {
}, [list]) }, [list])
return ( return (
<Page> <Protect redirect="/collections">
{ list && ( { list && (
<> <>
<h1>Browsing your {list.length} collection{ (list.length !== 1) && "s" }:</h1> <h1>Browsing your {list.length} collection{ (list.length !== 1) && "s" }:</h1>
@@ -62,7 +63,7 @@ const CollectionBrowser = () => {
})} })}
</> </>
)} )}
</Page> </Protect>
) )
} }

View File

@@ -29,7 +29,7 @@ export default function Login() {
setToken(result.token); setToken(result.token);
// if there is a redirect, go there, else go home // if there is a redirect, go there, else go home
navigate(`/${redirect ?? ''}`); navigate(redirect ?? '/');
} }
// check for logged in user and mount form // check for logged in user and mount form

View File

@@ -160,7 +160,7 @@ export default function Profile() {
// if this is the current user's profile // if this is the current user's profile
setContents( setContents(
<Protect redirect="profile"> <Protect redirect="/profile">
<div className="profile-authenticated"> <div className="profile-authenticated">
<h1>{user!.firstname}'s Profile</h1> <h1>{user!.firstname}'s Profile</h1>

View File

@@ -3,6 +3,7 @@ import { useParams } from "react-router-dom";
import { Page, Panel } from "../ui"; import { Page, Panel } from "../ui";
import { IRecipe } from "../../schemas"; import { IRecipe } from "../../schemas";
import { getRecipeByID } from "../../util/apiUtils"; import { getRecipeByID } from "../../util/apiUtils";
import Protect from "../../util/Protect";
export default function Recipe() { export default function Recipe() {
const [recipe, setRecipe] = useState<IRecipe>(); const [recipe, setRecipe] = useState<IRecipe>();
@@ -23,7 +24,7 @@ export default function Recipe() {
}, []) }, [])
return ( return (
<Page> <Protect redirect={`/recipe/${id}`}>
{ recipe && ( { recipe && (
<Panel> <Panel>
<h1>{recipe.name}</h1> <h1>{recipe.name}</h1>
@@ -31,6 +32,6 @@ export default function Recipe() {
<p>{recipe.preptime}</p> <p>{recipe.preptime}</p>
</Panel> </Panel>
)} )}
</Page> </Protect>
) )
} }

View File

@@ -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 (
<Page>
<h1>403: Unauthorized</h1>
{ children }
<Divider />
<Button onClick={() => navigate('/')}>Home</Button>
</Page>
)
}

View File

@@ -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 (
<Page>
<h1>404: We didn't find what you are looking for</h1>
{ children }
<Divider />
<Button onClick={() => navigate('/')}>Home</Button>
</Page>
)
}

View File

@@ -16,7 +16,7 @@ const Browser: FC<BrowserProps> = ({ children, header, searchFunction }) => {
}) })
return ( return (
<Protect> <Protect redirect="/explore">
<h1>{header}</h1> <h1>{header}</h1>
</Protect> </Protect>
) )

View File

@@ -172,6 +172,11 @@ module API {
return Promise.resolve(response.data); 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) { async addFriend(id: string | number) {
const response = await this.instance.post(this.endpoint + `/${id}`, this.headers); const response = await this.instance.post(this.endpoint + `/${id}`, this.headers);
return Promise.resolve(response.data); return Promise.resolve(response.data);

View File

@@ -1,31 +1,47 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import AccessForbidden from "../components/pages/StatusPages/403";
import { Button, Page } from "../components/ui"; import { Button, Page } from "../components/ui";
import Divider from "../components/ui/Divider";
import { useAuthContext } from "../context/AuthContext"; import { useAuthContext } from "../context/AuthContext";
import API from "./API";
import { ProtectPortal } from "./types"; import { ProtectPortal } from "./types";
const Protect: ProtectPortal = ({ children, redirect = '' }) => { const Protect: ProtectPortal = ({ children, redirect = '', accessRules = null }) => {
const { user } = useAuthContext(); const [view, setView] = useState(<Page><h1>Loading...</h1></Page>);
const { user, token } = useAuthContext();
const navigate = useNavigate(); const navigate = useNavigate();
if (!user) { useEffect(() => {
return ( if (!user || !token) {
<Page> setView(
<div className="content-unauthorized"> <AccessForbidden>
<h1>Hi there! You don't look too familiar.</h1> <>
<h2>Hi there! You don't look too familiar.</h2>
<p>To view the content on this page, please log in below:</p> <p>To view the content on this page, please log in below:</p>
<Divider />
<Button onClick={() => navigate(redirect ? `/login?redirect=${redirect}` : '/login')}>Log In</Button> <Button onClick={() => navigate(redirect ? `/login?redirect=${redirect}` : '/login')}>Log In</Button>
</div> </>
</Page> </AccessForbidden>
) )
} else {
return ( return;
<Page> }
{ children || <></> }
</Page> if (accessRules !== null) {
) if (accessRules.mustBeRecipinAdmin && !(user.isadmin)) {
} setView(
<AccessForbidden>
<>
<h2>This page requires administrator access.</h2>
<p>If you believe you are receiving this message in error, please contact Recipin support.</p>
</>
</AccessForbidden>
)
}
}
}, [user, token])
return view;
} }
export default Protect; export default Protect;

View File

@@ -15,8 +15,15 @@ interface ButtonParams extends PortalBase {
disabledText?: string disabledText?: string
} }
export interface AccessRules {
mustBeRecipinAdmin: boolean
mustBeFriend: boolean
mustBeSubscribed: boolean
}
export interface ProtectParams extends PortalBase { export interface ProtectParams extends PortalBase {
redirect?: string redirect?: string
accessRules?: AccessRules | null
} }
interface UserCardProps extends PortalBase { interface UserCardProps extends PortalBase {

4
server/controllers/UserCtl.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
/**
* @method getAll
* @returns { ControllerResponse<IUser[] | string> }
*/

View File

@@ -5,6 +5,18 @@ import { StatusCode } from '../util/types';
const UserInstance = new User(); const UserInstance = new User();
export default class UserCtl { 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() { async getAll() {
try { try {
// attempt to get users from database // 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) { async post(body: IUser) {
try { try {
const response = await UserInstance.post(body); 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) { async getOne(id: number | string) {
try { try {
const user = await UserInstance.getOneByID(id); 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) { async updateOne(id: number | string, body: IUser) {
try { try {
const result = await UserInstance.updateOneByID(id, body); 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) { async getFriends(id: number | string) {
try { try {
const result = await UserInstance.getFriends(id); 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) { async getFriendshipByID(id: number | string, userid: number | string) {
try { try {
const { ok, code, result } = await UserInstance.getFriendshipByID(id, userid); 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) { async getPendingFriendRequests(recipient: string | number) {
try { try {
const { ok, code, result } = await UserInstance.getPendingFriendRequests(recipient); 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) { async addFriendship(userid: number | string, targetid: number | string) {
try { try {
const result = await UserInstance.addFriendship(userid, targetid); const result = await UserInstance.addFriendship(userid, targetid);

View File

@@ -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) { async addFriendship(userid: number | string, targetid: number | string) {
try { try {
const statement = ` const statement = `

View File

@@ -2,6 +2,7 @@ import { Express, Router } from 'express';
import { restrictAccess } from '../auth/middlewares'; import { restrictAccess } from '../auth/middlewares';
import { UserCtl } from '../controllers'; import { UserCtl } from '../controllers';
import { IUser } from '../schemas'; import { IUser } from '../schemas';
import { StatusCode } from '../util/types';
const UserInstance = new UserCtl(); const UserInstance = new UserCtl();
const router = Router(); const router = Router();
@@ -24,12 +25,15 @@ export const friendRouter = (app: Express) => {
// get all friendships for a user // get all friendships for a user
router.get('/', async (req, res, next) => { router.get('/', async (req, res, next) => {
const user = req.user as IUser; const user = req.user as IUser;
const { pending, targetUser } = req.query; const { pending, accepted, targetUser } = req.query;
try { try {
if (pending) { if (pending) {
const { code, data } = await UserInstance.getPendingFriendRequests(user.id as number); const { code, data } = await UserInstance.getPendingFriendRequests(user.id as number);
res.status(code).send(data); res.status(code).send(data);
} else if (accepted) {
const { code, data } = await UserInstance.getAcceptedFriends(user.id as number);
res.status(code).send(data);
} else { } else {
if (targetUser) { if (targetUser) {
const { code, data } = await UserInstance.getFriends(parseInt(targetUser as string)); const { code, data } = await UserInstance.getFriends(parseInt(targetUser as string));
@@ -39,6 +43,9 @@ export const friendRouter = (app: Express) => {
res.status(code).send(data); 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) { } catch(e) {
next(e); next(e);
} }