From 97b6de6aa437f7a913a33b9e6aa18a919faf2caf Mon Sep 17 00:00:00 2001 From: Wes Bos Date: Thu, 12 Apr 2018 16:31:01 -0400 Subject: a whole bunch of things that should be in their own commits --- backend/database/datamodel.graphql | 1 - backend/database/prisma.yml | 4 - backend/database/seed.graphql | 24 ---- backend/src/index.js | 6 - backend/src/mail.js | 18 ++- backend/src/resolvers/Mutation.js | 95 ++++++++------- backend/src/resolvers/Query.js | 48 +++++--- backend/src/schema.graphql | 3 +- backend/src/utils.js | 35 +----- frontend/components/AddToCart.js | 6 +- frontend/components/ErrorMessage.js | 7 +- frontend/components/Item.js | 2 +- frontend/components/Items.js | 10 +- frontend/components/Order.js | 12 +- frontend/components/Page.js | 14 ++- frontend/components/Permissions.js | 113 +++++++---------- frontend/components/PleaseSignIn.js | 11 +- frontend/components/Reset.js | 2 +- frontend/components/ResetRequest.js | 4 +- frontend/components/Search.js | 13 +- frontend/components/styles/SickButton.js | 5 + frontend/components/styles/Table.js | 31 +++++ frontend/pages/admin/update.js | 19 --- frontend/pages/buy.js | 16 --- frontend/pages/diagram.js | 202 +++++++++++++++++++++++++++++++ frontend/pages/update.js | 19 +++ frontend/queries/index.js | 6 +- 27 files changed, 466 insertions(+), 260 deletions(-) delete mode 100644 backend/database/seed.graphql create mode 100644 frontend/components/styles/Table.js delete mode 100644 frontend/pages/admin/update.js delete mode 100644 frontend/pages/buy.js create mode 100644 frontend/pages/diagram.js create mode 100644 frontend/pages/update.js diff --git a/backend/database/datamodel.graphql b/backend/database/datamodel.graphql index 43ae403..9465524 100644 --- a/backend/database/datamodel.graphql +++ b/backend/database/datamodel.graphql @@ -5,7 +5,6 @@ enum Permission { ITEMUPDATE ITEMDELETE PERMISSIONUPDATE - NUKE } type User { diff --git a/backend/database/prisma.yml b/backend/database/prisma.yml index 8f4cbe1..9782b52 100644 --- a/backend/database/prisma.yml +++ b/backend/database/prisma.yml @@ -11,10 +11,6 @@ disableAuth: true # the file path pointing to your data model datamodel: datamodel.graphql -# uncomment the following two lines to seed your service with initial data -# seed: -# import: seed.graphql - # cluster: ${env:PRISMA_CLUSTER} # cluster: local diff --git a/backend/database/seed.graphql b/backend/database/seed.graphql deleted file mode 100644 index 5779328..0000000 --- a/backend/database/seed.graphql +++ /dev/null @@ -1,24 +0,0 @@ -mutation { - createUser(data: { - email: "developer@example.com" - password: "$2a$10$hACwQ5/HQI6FhbIISOUVeusy3sKyUDhSq36fF5d/54aAdiygJPFzm" # plaintext password: "nooneknows" - name: "Sarah" - posts: { - create: [{ - title: "Hello World" - text: "This is my first blog post ever!" - isPublished: true - }, { - title: "My Second Post" - text: "My first post was good, but this one is better!" - isPublished: true - }, { - title: "Solving World Hunger" - text: "This is a draft..." - isPublished: false - }] - } - }) { - id - } -} \ No newline at end of file diff --git a/backend/src/index.js b/backend/src/index.js index 8cf48fc..9f759ef 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -30,9 +30,3 @@ server.express.use(async (req, res, next) => { server.start({ port: 4444 }, deets => { console.log(`Server is running on http://localhost:${deets.port}`); }); - -// overwrite console.log -const chalk = require('chalk'); - -// global.console.l = (...butta) => console.log(chalk.bold.yellow(...butta)); -global.console.l = console.log; diff --git a/backend/src/mail.js b/backend/src/mail.js index 3159fe2..fc4a47b 100644 --- a/backend/src/mail.js +++ b/backend/src/mail.js @@ -9,4 +9,20 @@ const transport = nodemailer.createTransport({ }, }); -module.exports = transport; +const makeANiceEmail = text => ` +
+

Hello There

+

${text}

+ +

😘 Wes Bos

+
+`; + +exports.transport = transport; +exports.makeANiceEmail = makeANiceEmail; diff --git a/backend/src/resolvers/Mutation.js b/backend/src/resolvers/Mutation.js index daaa7de..5886595 100644 --- a/backend/src/resolvers/Mutation.js +++ b/backend/src/resolvers/Mutation.js @@ -1,13 +1,11 @@ const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); -const { getUserId, Context, hasPermission } = require('../utils'); +const { hasPermission } = require('../utils'); const { randomBytes } = require('crypto'); const { promisify } = require('util'); const mail = require('../mail'); const stripe = require('../stripe'); -const wait = amount => new Promise(resolve => setTimeout(resolve, amount)); - const mutations = { // Signup Mutations async signup(parent, args, ctx, info) { @@ -29,7 +27,6 @@ const mutations = { async signin(parent, { email, password }, ctx, info) { const user = await ctx.db.query.user({ where: { email } }); - console.log(user); if (!user) { throw new Error(`No such user found for email: ${email}`); } @@ -44,7 +41,7 @@ const mutations = { }; }, - // Creation of Post Mutations + // Create An Item async createItem(parent, args, ctx, info) { if (!ctx.request.userId) { throw new Error('You must be logged in to create an item'); @@ -63,7 +60,6 @@ const mutations = { }, info ); - console.log(item); return item; }, @@ -85,19 +81,23 @@ const mutations = { async updateItem(parent, args, ctx, info) { const user = ctx.request.user; const item = await ctx.db.query.item({ where: { id: args.id } }, `{ user { id } }`); - if (item.user.id !== user.id || !user.permissions.includes('ADMIN')) { + + if (item.user.id !== user.id || !hasPermission(user, ['ADMIN'])) { throw new Error('You are not allowed to update that item!'); } const updates = { ...args }; // remove the ID because you can't update that delete updates.id; - return ctx.db.mutation.updateItem({ - where: { id: args.id }, - data: { - ...updates, + return ctx.db.mutation.updateItem( + { + where: { id: args.id }, + data: { + ...updates, + }, }, - }); + info + ); }, // Send password request @@ -117,14 +117,17 @@ const mutations = { data: { resetToken, resetTokenExpiry }, }); - console.log(res); // 3. Send them their token via email - const mailRes = await mail.sendMail({ + const mailRes = await mail.transport.sendMail({ from: 'wesbos@gmail.com', to: user.email, subject: 'Your password reset token', // TODO: don't hardcore localhost here - html: `Here is your reset link: http://localhost:3000/reset?resetToken=${resetToken}`, + html: mail.makeANiceEmail( + `Your password reset link is here! \n\nClick Here to resets` + ), }); console.log(mailRes); return res.updateUser; @@ -175,8 +178,8 @@ const mutations = { Add to cart */ async addToCart(parent, args, ctx, info) { - console.l('Add to cart called'); - const userId = getUserId(ctx); + const userId = ctx.request.userId; + if (!userId) { throw new Error('You must be signed in to add to cart!'); } @@ -190,7 +193,6 @@ const mutations = { }); if (existingCartItem) { - console.log('Existing'); return ctx.db.mutation.updateCartItem( { where: { id: existingCartItem.id }, @@ -220,40 +222,46 @@ const mutations = { // delete that cart item async removeFromCart(parent, args, ctx, info) { - // TODO: add userId to where - return ctx.db.mutation.deleteCartItem({ - where: { id: args.id }, - }); + return ctx.db.mutation.deleteManyCartItems( + { + where: { + id: args.id, + user: { + id: ctx.request.userId, + }, + }, + }, + info + ); }, async createOrder(parent, args, ctx, info) { - const userId = getUserId(ctx); + const userId = ctx.request.userId; const user = await ctx.db.query.user( { where: { id: userId } }, // TODO - can we just pass info here? '{ id, name, email, cart { id, quantity, item { title, price, id, description, image } }}' ); // 1. Recalculate the total for the price - const amount = user.cart.reduce((tally, cartItem) => tally + cartItem.item.price * cartItem.quantity, 0); - // TODO Error Handling - // 2.1 Create a Stripe Customer - const customer = await stripe.customers.create({ - email: user.email, - }); - // 2.3 Charge the stripe token + const amount = user.cart.reduce( + (tally, cartItem) => tally + cartItem.item.price * cartItem.quantity, + 0 + ); + // 2. Create a stripe charge const charge = await stripe.charges.create({ amount, currency: 'usd', source: args.token, }); + // 3. convert the items they want to OrderItems const orderItems = user.cart.map(cartItem => { const orderItem = { quantity: cartItem.quantity, // copy all the item details so it's there forever ...cartItem.item, item: { - // realtionship to the Item incase we need it + // relationship to the Item incase we need it connect: { id: cartItem.item.id }, }, user: { connect: { id: user.id } }, @@ -263,7 +271,7 @@ const mutations = { return orderItem; }); - // Create the Order + // 4. Create the Order const order = await ctx.db.mutation.createOrder({ data: { total: charge.amount, @@ -279,8 +287,8 @@ const mutations = { }, }, }); - console.log('Gonna delete some items'); - // 6. Clean up, clear the users cart adn send back { user, order } + + // 5. Clean up, clear the users cart and send back { user, order } // Delete the users current cart items const cartItemIds = user.cart.map(cartItem => cartItem.id); await ctx.db.mutation.deleteManyCartItems({ @@ -289,21 +297,24 @@ const mutations = { }, }); - // 5. Send the order back to the client + // 6. Send the order back to the client return order; - // 4. TODO: Send an email with their order }, + async updateUser(parent, args, ctx, info) { - const userId = getUserId(ctx); - const updatedUser = await ctx.db.mutation.updateUser({ - data: args, - where: { id: userId }, - }); + const userId = ctx.request.userId; + const updatedUser = await ctx.db.mutation.updateUser( + { + data: args, + where: { id: userId }, + }, + info + ); return updatedUser; }, async updatePermissions(parent, args, ctx, info) { - const userId = getUserId(ctx); + const userId = ctx.request.userId; const currentUser = await ctx.db.query.user({ where: { id: userId } }, info); if (!currentUser) throw new Error('You Must be logged in to updat permissions!'); hasPermission(currentUser, ['ADMIN', 'PERMISSIONUPDATE']); diff --git a/backend/src/resolvers/Query.js b/backend/src/resolvers/Query.js index 22dcd2a..276f7fa 100644 --- a/backend/src/resolvers/Query.js +++ b/backend/src/resolvers/Query.js @@ -1,26 +1,52 @@ -const { getUserId, Context, checkForUserId } = require('../utils'); +const { hasPermission } = require('../utils'); + const { forwardTo } = require('prisma-binding'); const Query = { items(parent, args, ctx, info) { - console.log('ITEMS!'); - // check auth return ctx.db.query.items({ ...args }, info); }, itemsConnection: forwardTo('db'), // TODO: Make sure they own this order before looking it up - order: forwardTo('db'), + // order: forwardTo('db'), + async order(parent, args, ctx, info) { + // 1. make sure they are signed in + if (!ctx.request.userId) { + throw new Error('You Must be signed in to view an order'); + } + + // 2. Create the query + const where = { + id: args.id, + user: { + id: ctx.request.userId, + }, + }; + // 3. Fire off the query + const [order] = await ctx.db.query.orders({ where }, info); + + // 4. Check that they are allowed to view the order + if (order.user.id !== ctx.request.userId || hasPermission(ctx.request.user, ['ADMIN'])) { + throw new Error("You don't have permission"); + } + // 5. If everything checks out, return the order + return order; + }, me(parent, args, ctx, info) { const Authorization = ctx.request.get('Authorization'); if (!Authorization || Authorization === 'null') { - console.log('Authorization is null'); - return null; + return null; // don't error out, just return nothing } - const id = getUserId(ctx); - return ctx.db.query.user({ where: { id } }, info); + + return ctx.db.query.user( + { + where: { id: ctx.request.userId }, + }, + info + ); }, async orders(parent, args, ctx, info) { @@ -37,12 +63,6 @@ const Query = { info ); }, - - async users(parent, args, ctx, info) { - // TODO Permissions - const userId = getUserId(ctx); - return ctx.db.query.users({}, info); - }, }; module.exports = Query; diff --git a/backend/src/schema.graphql b/backend/src/schema.graphql index 6613004..4318d2a 100644 --- a/backend/src/schema.graphql +++ b/backend/src/schema.graphql @@ -1,4 +1,4 @@ -# import Permission, Order, OrderItem, CartItem, ItemWhereInput, ItemOrderByInput, allItems, Item, ItemCreateInput, ItemOrderByInput, ItemWhereInput, Query.order, Query.orders, Query.users from './generated/prisma.graphql' +# import Permission, Order, OrderItem, CartItem, ItemWhereInput, ItemOrderByInput, allItems, Item, ItemCreateInput, ItemOrderByInput, ItemWhereInput, Query.orders, Query.users from './generated/prisma.graphql' type Query { me: User @@ -17,6 +17,7 @@ type Query { skip: Int first: Int ): [Item]! + order(id: ID!): Order! } # import Mutation from './generated/prisma.graphql' diff --git a/backend/src/utils.js b/backend/src/utils.js index 2988b3d..faf028b 100644 --- a/backend/src/utils.js +++ b/backend/src/utils.js @@ -1,16 +1,3 @@ -const jwt = require('jsonwebtoken'); - -function getUserId(ctx) { - const Authorization = ctx.request.get('Authorization'); - if (Authorization) { - const token = Authorization.replace('Bearer ', ''); - const { userId } = jwt.verify(token, process.env.APP_SECRET); - return userId; - } - // TODO: Don't throw when they aren't logged in - // throw new Error('Sorry, you must be logged in to do that!'); -} - function hasPermission(user, permissionsNeeded) { const matchedPermissions = user.permissions.filter(permissionTheyHave => permissionsNeeded.includes(permissionTheyHave) @@ -27,24 +14,4 @@ function hasPermission(user, permissionsNeeded) { } } -function checkForUserId(ctx) { - const Authorization = ctx.request.get('Authorization'); - if (Authorization) { - const token = Authorization.replace('Bearer ', ''); - const { userId } = jwt.verify(token, process.env.APP_SECRET); - return userId; - } -} - -class AuthError extends Error { - constructor() { - super('Not authorized'); - } -} - -module.exports = { - getUserId, - AuthError, - checkForUserId, - hasPermission, -}; +exports.hasPermission = hasPermission; diff --git a/frontend/components/AddToCart.js b/frontend/components/AddToCart.js index 3566c0c..18186d8 100644 --- a/frontend/components/AddToCart.js +++ b/frontend/components/AddToCart.js @@ -18,7 +18,11 @@ class AddToCart extends Component { const existingIndex = data.me.cart.findIndex(cartItem => cartItem.id === newCartItem.id); if (existingIndex >= 0) { // already in cache, just replace it - data.me.cart = [...data.me.cart.slice(0, existingIndex), newCartItem, ...data.me.cart.slice(existingIndex + 1)]; + data.me.cart = [ + ...data.me.cart.slice(0, existingIndex), + newCartItem, + ...data.me.cart.slice(existingIndex + 1), + ]; } else { data.me.cart = [...data.me.cart, newCartItem]; } diff --git a/frontend/components/ErrorMessage.js b/frontend/components/ErrorMessage.js index 3731554..c73a2b7 100644 --- a/frontend/components/ErrorMessage.js +++ b/frontend/components/ErrorMessage.js @@ -18,14 +18,14 @@ const StyledError = styled.div` } `; -const DisplayError = ({ error }) => { +const DisplayError = ({ error, refetch }) => { if (!error || !error.message) return null; if (error.networkError && error.networkError.result && error.networkError.result.errors.length) { return error.networkError.result.errors.map((error, i) => (

Shoot! - {error.message} + {error.message.replace('GraphQL error: ', '')}

)); @@ -34,7 +34,8 @@ const DisplayError = ({ error }) => {

Shoot! - {error.message} + {error.message.replace('GraphQL error: ', '')} +

); diff --git a/frontend/components/Item.js b/frontend/components/Item.js index 2ed5742..99d8d93 100644 --- a/frontend/components/Item.js +++ b/frontend/components/Item.js @@ -83,7 +83,7 @@ class ItemComponent extends React.Component {
diff --git a/frontend/components/Items.js b/frontend/components/Items.js index 3c28de2..22e2d75 100644 --- a/frontend/components/Items.js +++ b/frontend/components/Items.js @@ -32,8 +32,6 @@ class ItemList extends React.Component { }; render() { const fetchPolicy = this.state.refetch ? 'network-only' : 'cache-first'; - console.log(this.state.refetch, this.props.page); - console.log(fetchPolicy); return (
@@ -46,13 +44,9 @@ class ItemList extends React.Component { fetchPolicy={fetchPolicy} > {({ data, error, loading }) => { - if (loading) return
Loading
; + if (loading) return null; if (error) return
Error
; - return ( - - {data.items.map(item => )} - - ); + return {data.items.map(item => )}; }} diff --git a/frontend/components/Order.js b/frontend/components/Order.js index b38254f..f6743fa 100644 --- a/frontend/components/Order.js +++ b/frontend/components/Order.js @@ -7,6 +7,7 @@ import styled from 'styled-components'; import { SINGLE_ORDER_QUERY } from '../queries'; import formatMoney from '../lib/formatMoney'; import Dump from './Dump'; +import Error from './ErrorMessage'; const OrderStyles = styled.div` max-width: 1000px; @@ -51,10 +52,15 @@ class Order extends Component { render() { return ( - - {({ data: { order }, error, loading }) => { + + {({ data, error, loading, refetch }) => { if (loading) return

Loading...

; - if (!order || error) return

No Order Found!

; + if (error) return ; + const order = data.order; return ( diff --git a/frontend/components/Page.js b/frontend/components/Page.js index 3c6e3b1..ab14b10 100644 --- a/frontend/components/Page.js +++ b/frontend/components/Page.js @@ -51,10 +51,15 @@ const StyledPage = styled.div` `; class Page extends React.Component { + static propTypes = { + children: PropTypes.node.isRequired, + }; componentDidMount() { - // console.log('ComponentDidMount'); - // When the page loads, re-refetch the current user query - // client.query({ query: CURRENT_USER_QUERY, fetchPolicy: 'network-only' }); + // The first time we load in the client, we need to refetch the current user data + if (typeof window !== 'undefined' && !window.__CLIENTLOADED__) { + client.query({ query: CURRENT_USER_QUERY, fetchPolicy: 'network-only' }); + window.__CLIENTLOADED__ = true; + } } render() { return ( @@ -68,8 +73,5 @@ class Page extends React.Component { ); } } -Page.propTypes = { - children: PropTypes.node.isRequired, -}; export default Page; diff --git a/frontend/components/Permissions.js b/frontend/components/Permissions.js index 0b3976c..69a621b 100644 --- a/frontend/components/Permissions.js +++ b/frontend/components/Permissions.js @@ -1,44 +1,18 @@ import React from 'react'; import { Query, Mutation } from 'react-apollo'; -import styled from 'styled-components'; -import { BarLoader } from 'react-spinners'; -import { perPage } from '../config'; import { ALL_USERS_QUERY, UPDATE_PERMISSIONS_MUTATION } from '../queries/index'; import Error from './ErrorMessage'; -import Form from './styles/Form'; import SickButton from './styles/SickButton'; +import Table from './styles/Table'; -const PermissionsBox = styled.div` - border: 1px solid ${props => props.theme.offWhite}; - box-shadow: ${props => props.theme.bs}; - margin-bottom: 5rem; - padding: 2rem; - label { - cursor: pointer; - span { - transition: all 0.1s; - padding: 0 1rem; - display: block; - border: 1px solid ${props => props.theme.offWhite}; - border-left-width: 20px; - } - input { - display: none; - } - input:checked + span { - border-color: red; - } - margin-right: 1rem; - margin-bottom: 1rem; - } - .labels { - display: flex; - flex-wrap: wrap; - & > * { - flex: 0 1 auto; - } - } -`; +const possiblePermissions = [ + 'ADMIN', + 'USER', + 'ITEMCREATE', + 'ITEMUPDATE', + 'ITEMDELETE', + 'PERMISSIONUPDATE', +]; class User extends React.Component { state = { @@ -60,22 +34,12 @@ class User extends React.Component { return ( {(updatePermissions, { loading, error }) => ( - - + -

- {user.name} -- {user.email} -

-
- {[ - 'ADMIN', - 'USER', - 'ITEMCREATE', - 'ITEMUPDATE', - 'ITEMDELETE', - 'PERMISSIONUPDATE', - 'NUKE', - ].map(permission => ( + {user.name} + {user.email} + {possiblePermissions.map(permission => ( + - ))} -
- { - const res = await updatePermissions({ - variables: { - permissions: this.state.permissions, - userId: this.props.user.id, - }, - }); - console.log(res); - }} - > - Update Permissions - -
+ + ))} + + { + const res = await updatePermissions({ + variables: { + permissions: this.state.permissions, + userId: this.props.user.id, + }, + }); + }} + > + Updat{loading ? 'ing' : 'e'} + + + )}
); @@ -118,7 +83,17 @@ const Permissions = () => ( return (

Manage User Permissions

- {data.users.map(user => )} + + + + + + {possiblePermissions.map(p => )} + + + + {data.users.map(user => )} +
NameEmail{p}👇🏻
); }} diff --git a/frontend/components/PleaseSignIn.js b/frontend/components/PleaseSignIn.js index 1533dbd..ff3b956 100644 --- a/frontend/components/PleaseSignIn.js +++ b/frontend/components/PleaseSignIn.js @@ -20,9 +20,16 @@ const PleaseSignIn = props => ( // check if they NO permissions, or they don't meet the requmrenets if ( !data.me.permissions || - !props.allowedPermissions.some(permission => data.me.permissions.contains(permission)) + !props.allowedPermissions.some(permission => data.me.permissions.includes(permission)) ) { - return

Insufficient Permissions to Manage Permissions

; + return ( +

+ Insufficient Permissions to Manage Permissions. You have: + {data.me.permissions} + and you need + {props.allowedPermissions.join(' OR ')} +

+ ); } } return props.children; diff --git a/frontend/components/Reset.js b/frontend/components/Reset.js index 5bc7f98..bbfc436 100644 --- a/frontend/components/Reset.js +++ b/frontend/components/Reset.js @@ -40,7 +40,7 @@ class Reset extends React.Component { }} refetchQueries={[{ query: CURRENT_USER_QUERY }]} > - {(resetMutation, { error, loading }) => ( + {(resetMutation, { error, loading, called }) => (
this.resetPassword(e, resetMutation)}>
diff --git a/frontend/components/ResetRequest.js b/frontend/components/ResetRequest.js index fb6f660..4ed902e 100644 --- a/frontend/components/ResetRequest.js +++ b/frontend/components/ResetRequest.js @@ -3,6 +3,7 @@ import { Mutation } from 'react-apollo'; import { REQUEST_RESET_MUTATION } from '../queries'; import Form from './styles/Form'; import Error from './ErrorMessage'; +import Dump from './Dump'; class ResetRequest extends React.Component { state = { @@ -12,7 +13,7 @@ class ResetRequest extends React.Component { render() { return ( - {(resetMutation, { loading, error }) => ( + {(resetMutation, { loading, error, called, data }) => ( { e.preventDefault(); @@ -21,6 +22,7 @@ class ResetRequest extends React.Component { }} > + {!error && called && !loading &&

Success! Check Your Email!

}