diff --git a/api/controllers/pack.js b/api/controllers/pack.js index d567f94..f82f0a5 100644 --- a/api/controllers/pack.js +++ b/api/controllers/pack.js @@ -4,11 +4,11 @@ import Sequelize from 'sequelize'; let router = express.Router(); import models from '../models'; -import { authenticate } from "../utils/jwt"; +import { authenticate, authenicatePublicRequest } from "../utils/jwt"; import { csvItems, packItemPayload, packPayload } from "../utils/build-payload"; // Get -router.get('/:id', async (req, res) => { +router.get('/:id', authenicatePublicRequest, async (req, res) => { let { id } = req.params; models.Pack.findOne({ where: { id }, @@ -21,7 +21,17 @@ router.get('/:id', async (req, res) => { { model: models.User, attributes: ['id', 'username'] }, ] }) - .then(pack => res.json(pack)) + .then(pack => { + if (pack.public) { + res.json(pack); + } else { //when the pack is private, only allow the owner of the pack to view + if (req.user && pack.userId == req.user.id) { + res.json(pack); + } else { + res.sendStatus(403); //return 'forbidden' + } + } + }) .catch(err => res.json(err)); }); diff --git a/api/models/pack.js b/api/models/pack.js index db93b56..6b41dce 100644 --- a/api/models/pack.js +++ b/api/models/pack.js @@ -43,6 +43,9 @@ const pack = (sequelize, DataTypes) => { }, gender: { type: DataTypes.ENUM(Object.values(gender)) + }, + userId: { + type: DataTypes.INTEGER } }); diff --git a/api/utils/jwt.js b/api/utils/jwt.js index e7b9c47..50406ef 100644 --- a/api/utils/jwt.js +++ b/api/utils/jwt.js @@ -36,4 +36,26 @@ export const authenticate = (req, res, next) => { next(); }) .catch(() => res.sendStatus(400)); -}; \ No newline at end of file +}; + +/* +This method will authenticate the user if one is logged in, but does not return a 400/401 +if there the user is not logged in to allow for public viewing of packs. +This could also be used for public/private profiles. +*/ +export const authenicatePublicRequest = (req, res, next) => { + const authHeader = req.get('authorization'); + if (!authHeader) { + req.user = undefined; + next(); + } else { + const token = authHeader.split(' ')[1]; + const decoded = jwt.verify(token, process.env.JWT_SECRET); + models.User.findOne({where: {id: decoded.id}}) + .then(user => { + req.user = user; + next(); + }) + .catch(() => res.sendStatus(400)); + } +} \ No newline at end of file diff --git a/frontend/src/app/Pack/Pack.tsx b/frontend/src/app/Pack/Pack.tsx index f8d2825..6960080 100644 --- a/frontend/src/app/Pack/Pack.tsx +++ b/frontend/src/app/Pack/Pack.tsx @@ -21,9 +21,11 @@ import Items from './Items'; import { getWeightByCategory } from 'lib/utils/weight'; import { Credit, PackWrapper, SectionHeader, SectionTitle, TripDescription } from "./styles"; +import FullPageError from 'app/components/FullPageError'; const Pack: React.FC = ({ getPack, weightUnit, packId }) => { const [loading, setLoading] = React.useState(true); + const [packForbidden, setPackForbidden] = React.useState(true); const [pack, setPack] = React.useState(null); const [unit, setUnit] = React.useState(weightUnit); const { dispatch } = useSidebar(); @@ -34,9 +36,16 @@ const Pack: React.FC = ({ getPack, weightUnit, packId }) => { .then(pack => { setPack(pack); setLoading(false); + setPackForbidden(false); }) - .catch(() => { - alertError({ message: 'Unable to retrieve pack.' }); + .catch(e => { + setLoading(false); + if (e.message.includes("failed with status code 403")){//when forbidden to view + setPackForbidden(true); + } + else { + alertError({ message: 'Unable to retrieve pack.' }); + } }); return function cleanup() { @@ -68,6 +77,10 @@ const Pack: React.FC = ({ getPack, weightUnit, packId }) => { if (loading) { return } + + if (packForbidden) { + return + } if (!pack) { return

Pack not found!

diff --git a/frontend/src/app/PackForm/PackForm.tsx b/frontend/src/app/PackForm/PackForm.tsx index 80e7280..ce11895 100644 --- a/frontend/src/app/PackForm/PackForm.tsx +++ b/frontend/src/app/PackForm/PackForm.tsx @@ -21,7 +21,8 @@ import Loading from "app/components/Loading"; import { useSidebar } from "app/components/Sidebar/Context"; import { NavigationConfirmModal } from 'react-router-navigation-confirm'; -import { PageTitle, Controls, Box, Grid, PageDescription } from "styles/common"; +import { PageTitle, Controls, Box, Grid } from "styles/common"; +import SwitchInput from "app/components/FormFields/SwitchInput"; const PackForm: React.FC = ({ history, packId, getPack, exportItems, getItems, createPack, updatePack, user }) => { const [loading, setLoading] = React.useState(true); @@ -119,7 +120,8 @@ const PackForm: React.FC = ({ history, packId, getPack, exp duration_unit: packData ? packData.duration_unit : undefined, temp_range: packData ? packData.temp_range : '', season: packData ? packData.season : '', - gender: packData ? packData.gender : undefined + gender: packData ? packData.gender : undefined, + public: packData ? packData.public : false }} validationSchema={Yup.object().shape({ title: Yup.string().required("Trail name or location is required.") @@ -244,6 +246,13 @@ const PackForm: React.FC = ({ history, packId, getPack, exp /> + setFieldValue('public', v)} + tip="When public, the pack will be viewable by anyone with a link" + > diff --git a/frontend/src/app/components/FormFields/SwitchInput.tsx b/frontend/src/app/components/FormFields/SwitchInput.tsx new file mode 100644 index 0000000..bb04b1a --- /dev/null +++ b/frontend/src/app/components/FormFields/SwitchInput.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { Switch } from 'antd'; +import { SharedInputProps } from "./types"; +import { InputContainer } from "./utils"; + +interface SwitchProps extends SharedInputProps { + checked: boolean; + checkedText?: string | undefined; + uncheckedText?: string | undefined; + onChange: (checked: boolean) => void; +} + +const SwitchInput: React.FC = ({ checked, checkedText, uncheckedText, onChange, label, tip, style }) => { + const handleChange = (value: any) => onChange(value); + return ( + + + + ); +}; + +export default SwitchInput; \ No newline at end of file diff --git a/frontend/src/app/components/FormFields/styles.ts b/frontend/src/app/components/FormFields/styles.ts index 6ab2155..791419b 100644 --- a/frontend/src/app/components/FormFields/styles.ts +++ b/frontend/src/app/components/FormFields/styles.ts @@ -115,4 +115,4 @@ export const CharacterCounter = styled.small` &.full { color:red; } -`; \ No newline at end of file +`; diff --git a/frontend/src/app/components/FormFields/types.ts b/frontend/src/app/components/FormFields/types.ts index 845d6e1..5518aab 100644 --- a/frontend/src/app/components/FormFields/types.ts +++ b/frontend/src/app/components/FormFields/types.ts @@ -15,5 +15,6 @@ export interface SharedInputProps { tip?: string; last?: boolean; style?: CSSProperties; + labelStyle?: CSSProperties; onBlur?: () => void; } \ No newline at end of file diff --git a/frontend/src/app/components/FormFields/utils.tsx b/frontend/src/app/components/FormFields/utils.tsx index 7fe465b..28d0d83 100644 --- a/frontend/src/app/components/FormFields/utils.tsx +++ b/frontend/src/app/components/FormFields/utils.tsx @@ -18,10 +18,10 @@ const tooltip = (tip: string) => ( ); -export const InputContainer: React.FC = ({ label, errorMsg, error, tip, last, style, children }) => ( +export const InputContainer: React.FC = ({ label, errorMsg, error, tip, last, style, children, labelStyle }) => ( {label && ( -