Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions api/controllers/pack.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 },
Expand All@@ -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));
});

Expand Down
3 changes: 3 additions & 0 deletions api/models/pack.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,6 +43,9 @@ const pack = (sequelize, DataTypes) => {
},
gender: {
type: DataTypes.ENUM(Object.values(gender))
},
userId: {
type: DataTypes.INTEGER
}
});

Expand Down
24 changes: 23 additions & 1 deletion api/utils/jwt.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,4 +36,26 @@ export const authenticate = (req, res, next) => {
next();
})
.catch(() => res.sendStatus(400));
};
};

/*
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));
}
}
17 changes: 15 additions & 2 deletions frontend/src/app/Pack/Pack.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<PackSpecs.Props> = ({ getPack, weightUnit, packId }) => {
const [loading, setLoading] = React.useState<boolean>(true);
const [packForbidden, setPackForbidden] = React.useState<boolean>(true);
const [pack, setPack] = React.useState<PackType | null>(null);
const [unit, setUnit] = React.useState<WeightUnit>(weightUnit);
const { dispatch } = useSidebar();
Expand All@@ -34,9 +36,16 @@ const Pack: React.FC<PackSpecs.Props> = ({ 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() {
Expand DownExpand Up@@ -68,6 +77,10 @@ const Pack: React.FC<PackSpecs.Props> = ({ getPack, weightUnit, packId }) => {
if (loading) {
return <Loading size="large"/>
}

if (packForbidden) {
return <FullPageError text="This pack is private. If you're sure this is your pack, make sure you're logged in."></FullPageError>
}

if (!pack) {
return <p>Pack not found!</p>
Expand Down
13 changes: 11 additions & 2 deletions frontend/src/app/PackForm/PackForm.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<PackFormSpecs.Props> = ({ history, packId, getPack, exportItems, getItems, createPack, updatePack, user }) => {
const [loading, setLoading] = React.useState<boolean>(true);
Expand DownExpand Up@@ -119,7 +120,8 @@ const PackForm: React.FC<PackFormSpecs.Props> = ({ 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.")
Expand DownExpand Up@@ -244,6 +246,13 @@ const PackForm: React.FC<PackFormSpecs.Props> = ({ history, packId, getPack, exp
/>
</Col>
</Row>
<SwitchInput label="Pack Privacy"
checked = {values.public}
checkedText="Public"
uncheckedText="Private"
onChange={v => setFieldValue('public', v)}
tip="When public, the pack will be viewable by anyone with a link"
></SwitchInput>
</div>
</Grid>
</Box>
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/app/components/FormFields/SwitchInput.tsx
Original file line numberDiff line numberDiff line change
@@ -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<SwitchProps> = ({ checked, checkedText, uncheckedText, onChange, label, tip, style }) => {
const handleChange = (value: any) => onChange(value);
return (
<InputContainer {...{ label, tip, style }} labelStyle = {{display:"inline", paddingRight: '8px'}}>
<Switch
checked = {checked}
checkedChildren={checkedText}
unCheckedChildren={uncheckedText}
onChange={handleChange}
/>
</InputContainer>
);
};

export default SwitchInput;
2 changes: 1 addition & 1 deletion frontend/src/app/components/FormFields/styles.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,4 +115,4 @@ export const CharacterCounter = styled.small`
&.full {
color:red;
}
`;
`;
1 change: 1 addition & 0 deletions frontend/src/app/components/FormFields/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,5 +15,6 @@ export interface SharedInputProps {
tip?: string;
last?: boolean;
style?: CSSProperties;
labelStyle?: CSSProperties;
onBlur?: () => void;
}
4 changes: 2 additions & 2 deletions frontend/src/app/components/FormFields/utils.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,10 +18,10 @@ const tooltip = (tip: string) => (
</Tooltip>
);

export const InputContainer: React.FC<SharedInputProps> = ({ label, errorMsg, error, tip, last, style, children }) => (
export const InputContainer: React.FC<SharedInputProps> = ({ label, errorMsg, error, tip, last, style, children, labelStyle }) => (
<Row className={last ? 'last' : ''} style={style}>
{label && (
<Label>
<Label style={labelStyle}>
{label}
{tip && tooltip(tip)}
</Label>
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/app/components/FullPageError/index.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
import * as React from 'react';
import { PageDescription, PageTitle } from 'styles/common';

interface FullPageErrorProps {
text?: string;
styles? : React.CSSProperties;
}

const FullPageError: React.FC<FullPageErrorProps> = ({ text, styles }) => {
const defaultStyles: React.CSSProperties = {};

return (
<div style={defaultStyles}>
<PageTitle><h1>Oops!</h1></PageTitle>
<PageDescription><p>{text}</p></PageDescription>
</div>
)
};

export default FullPageError;
3 changes: 2 additions & 1 deletion frontend/src/app/components/Statistics/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ const Statistics: React.FC<StatProps> = ({ pack }) => {
return `${duration} ${duration_unit}`;
};

let packUrlLabel = "Pack URL " + (pack.public ? "" : "(private)");
const packUrl = `https://packstack.io${getPackPath(id, title)}`;

return (
Expand All@@ -32,7 +33,7 @@ const Statistics: React.FC<StatProps> = ({ pack }) => {
<Stat label="Temp Range" value={temp_range} icon={<Icon component={TempIcon}/>}/>
<Stat label="Duration" value={durationValue()} icon={<Icon component={FootprintsIcon}/>}/>
<Stat label="Gender" value={getGenderName(gender)}/>
<Stat label="Pack URL" value={packUrl} icon={<Icon type="link"/>}/>
<Stat label={packUrlLabel} value={packUrl} icon={<Icon type="link"/>}></Stat>
</StatCollection>
)

Expand Down
1 change: 1 addition & 0 deletions frontend/src/types/pack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ export interface BasePack {
temp_range?: string;
season?: string;
gender?: Gender;
public: boolean;
}

export interface Pack extends BasePack {
Expand Down