summaryrefslogtreecommitdiffstats
path: root/stepped-solutions
diff options
context:
space:
mode:
authorWes Bos <wesbos@gmail.com>2018-08-15 14:47:25 -0400
committerWes Bos <wesbos@gmail.com>2018-08-15 14:47:25 -0400
commit124f227a8235e08bddf8376e0b3d01d33c4414f3 (patch)
tree1ea23e539e42c35ca0b609e2615c043aa6fc4219 /stepped-solutions
parentee09df7dee16a7b44711159c60c54cc9365be35e (diff)
Permissions is doneeee
Diffstat (limited to 'stepped-solutions')
-rwxr-xr-xstepped-solutions/36/frontend/components/Permissions.js104
-rwxr-xr-xstepped-solutions/36/frontend/components/User.js27
-rwxr-xr-xstepped-solutions/37/backend/src/resolvers/Mutation.js207
-rwxr-xr-xstepped-solutions/37/backend/src/schema.graphql32
-rwxr-xr-xstepped-solutions/37/frontend/components/Permissions.js131
-rwxr-xr-xstepped-solutions/37/frontend/components/styles/Table.js35
-rwxr-xr-xstepped-solutions/38/backend/src/resolvers/Mutation.js215
-rwxr-xr-xstepped-solutions/38/frontend/components/DeleteItem.js50
8 files changed, 801 insertions, 0 deletions
diff --git a/stepped-solutions/36/frontend/components/Permissions.js b/stepped-solutions/36/frontend/components/Permissions.js
new file mode 100755
index 0000000..10b59ab
--- /dev/null
+++ b/stepped-solutions/36/frontend/components/Permissions.js
@@ -0,0 +1,104 @@
+import { Query } from 'react-apollo';
+import Error from './ErrorMessage';
+import gql from 'graphql-tag';
+import Table from './styles/Table';
+import SickButton from './styles/SickButton';
+import PropTypes from 'prop-types';
+
+const possiblePermissions = [
+ 'ADMIN',
+ 'USER',
+ 'ITEMCREATE',
+ 'ITEMUPDATE',
+ 'ITEMDELETE',
+ 'PERMISSIONUPDATE',
+];
+
+const ALL_USERS_QUERY = gql`
+ query {
+ users {
+ id
+ name
+ email
+ permissions
+ }
+ }
+`;
+
+const Permissions = props => (
+ <Query query={ALL_USERS_QUERY}>
+ {({ data, loading, error }) => (
+ <div>
+ <Error error={error} />
+ <div>
+ <h2>Manage Permissions</h2>
+ <Table>
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Email</th>
+ {possiblePermissions.map(permission => <th key={permission}>{permission}</th>)}
+ <th>👇🏻</th>
+ </tr>
+ </thead>
+ <tbody>{data.users.map(user => <UserPermissions user={user} key={user.id} />)}</tbody>
+ </Table>
+ </div>
+ </div>
+ )}
+ </Query>
+);
+
+class UserPermissions extends React.Component {
+ static propTypes = {
+ user: PropTypes.shape({
+ name: PropTypes.string,
+ email: PropTypes.string,
+ id: PropTypes.string,
+ permissions: PropTypes.array,
+ }).isRequired,
+ };
+ state = {
+ permissions: this.props.user.permissions,
+ };
+ handlePermissionChange = e => {
+ const checkbox = e.target;
+ // take a copy of the current permissions
+ let updatedPermissions = [...this.state.permissions];
+ // figure out if we need to remove or add this permission
+ if (checkbox.checked) {
+ // add it in!
+ updatedPermissions.push(checkbox.value);
+ } else {
+ updatedPermissions = updatedPermissions.filter(permission => permission !== checkbox.value);
+ }
+ this.setState({ permissions: updatedPermissions });
+ console.log(updatedPermissions);
+ };
+ render() {
+ const user = this.props.user;
+ return (
+ <tr>
+ <td>{user.name}</td>
+ <td>{user.email}</td>
+ {possiblePermissions.map(permission => (
+ <td key={permission}>
+ <label htmlFor={`${user.id}-permission-${permission}`}>
+ <input
+ type="checkbox"
+ checked={this.state.permissions.includes(permission)}
+ value={permission}
+ onChange={this.handlePermissionChange}
+ />
+ </label>
+ </td>
+ ))}
+ <td>
+ <SickButton>Update</SickButton>
+ </td>
+ </tr>
+ );
+ }
+}
+
+export default Permissions;
diff --git a/stepped-solutions/36/frontend/components/User.js b/stepped-solutions/36/frontend/components/User.js
new file mode 100755
index 0000000..a33649b
--- /dev/null
+++ b/stepped-solutions/36/frontend/components/User.js
@@ -0,0 +1,27 @@
+import { Query } from 'react-apollo';
+import gql from 'graphql-tag';
+import PropTypes from 'prop-types';
+
+const CURRENT_USER_QUERY = gql`
+ query {
+ me {
+ id
+ email
+ name
+ permissions
+ }
+ }
+`;
+
+const User = props => (
+ <Query {...props} query={CURRENT_USER_QUERY}>
+ {payload => console.log(payload) || props.children(payload)}
+ </Query>
+);
+
+User.propTypes = {
+ children: PropTypes.func.isRequired,
+};
+
+export default User;
+export { CURRENT_USER_QUERY };
diff --git a/stepped-solutions/37/backend/src/resolvers/Mutation.js b/stepped-solutions/37/backend/src/resolvers/Mutation.js
new file mode 100755
index 0000000..f2ecc16
--- /dev/null
+++ b/stepped-solutions/37/backend/src/resolvers/Mutation.js
@@ -0,0 +1,207 @@
+const bcrypt = require('bcryptjs');
+const jwt = require('jsonwebtoken');
+const { randomBytes } = require('crypto');
+const { promisify } = require('util');
+const { transport, makeANiceEmail } = require('../mail');
+const { hasPermission } = require('../utils');
+
+const Mutations = {
+ async createItem(parent, args, ctx, info) {
+ if (!ctx.request.userId) {
+ throw new Error('You must be logged in to do that!');
+ }
+
+ const item = await ctx.db.mutation.createItem(
+ {
+ data: {
+ // This is how to create a relationship between the Item and the User
+ user: {
+ connect: {
+ id: ctx.request.userId,
+ },
+ },
+ ...args,
+ },
+ },
+ info
+ );
+
+ console.log(item);
+
+ return item;
+ },
+ updateItem(parent, args, ctx, info) {
+ // first take a copy of the updates
+ const updates = { ...args };
+ // remove the ID from the updates
+ delete updates.id;
+ // run the update method
+ return ctx.db.mutation.updateItem(
+ {
+ data: updates,
+ where: {
+ id: args.id,
+ },
+ },
+ info
+ );
+ },
+ async deleteItem(parent, args, ctx, info) {
+ const where = { id: args.id };
+ // 1. find the item
+ const item = await ctx.db.query.item({ where }, `{ id title}`);
+ // 2. Check if they own that item, or have the permissions
+ // TODO
+ // 3. Delete it!
+ return ctx.db.mutation.deleteItem({ where }, info);
+ },
+ async signup(parent, args, ctx, info) {
+ // lowercase their email
+ args.email = args.email.toLowerCase();
+ // hash their password
+ const password = await bcrypt.hash(args.password, 10);
+ // create the user in the database
+ const user = await ctx.db.mutation.createUser(
+ {
+ data: {
+ ...args,
+ password,
+ permissions: { set: ['USER'] },
+ },
+ },
+ info
+ );
+ // create the JWT token for them
+ const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
+ // We set the jwt as a cookie on the response
+ ctx.response.cookie('token', token, {
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 365, // 1 year cookie
+ });
+ // Finalllllly we return the user to the browser
+ return user;
+ },
+ async signin(parent, { email, password }, ctx, info) {
+ // 1. check if there is a user with that email
+ const user = await ctx.db.query.user({ where: { email } });
+ if (!user) {
+ throw new Error(`No such user found for email ${email}`);
+ }
+ // 2. Check if their password is correct
+ const valid = await bcrypt.compare(password, user.password);
+ if (!valid) {
+ throw new Error('Invalid Password!');
+ }
+ // 3. generate the JWT Token
+ const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
+ // 4. Set the cookie with the token
+ ctx.response.cookie('token', token, {
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 365,
+ });
+ // 5. Return the user
+ return user;
+ },
+ signout(parent, args, ctx, info) {
+ ctx.response.clearCookie('token');
+ return { message: 'Goodbye!' };
+ },
+ async requestReset(parent, args, ctx, info) {
+ // 1. Check if this is a real user
+ const user = await ctx.db.query.user({ where: { email: args.email } });
+ if (!user) {
+ throw new Error(`No such user found for email ${args.email}`);
+ }
+ // 2. Set a reset token and expiry on that user
+ const randomBytesPromiseified = promisify(randomBytes);
+ const resetToken = (await randomBytesPromiseified(20)).toString('hex');
+ const resetTokenExpiry = Date.now() + 3600000; // 1 hour from now
+ const res = await ctx.db.mutation.updateUser({
+ where: { email: args.email },
+ data: { resetToken, resetTokenExpiry },
+ });
+ // 3. Email them that reset token
+ const mailRes = await transport.sendMail({
+ from: 'wes@wesbos.com',
+ to: user.email,
+ subject: 'Your Password Reset Token',
+ html: makeANiceEmail(`Your Password Reset Token is here!
+ \n\n
+ <a href="${process.env
+ .FRONTEND_URL}/reset?resetToken=${resetToken}">Click Here to Reset</a>`),
+ });
+
+ // 4. Return the message
+ return { message: 'Thanks!' };
+ },
+ async resetPassword(parent, args, ctx, info) {
+ // 1. check if the passwords match
+ if (args.password !== args.confirmPassword) {
+ throw new Error("Yo Passwords don't match!");
+ }
+ // 2. check if its a legit reset token
+ // 3. Check if its expired
+ const [user] = await ctx.db.query.users({
+ where: {
+ resetToken: args.resetToken,
+ resetTokenExpiry_gte: Date.now() - 3600000,
+ },
+ });
+ if (!user) {
+ throw new Error('This token is either invalid or expired!');
+ }
+ // 4. Hash their new password
+ const password = await bcrypt.hash(args.password, 10);
+ // 5. Save the new password to the user and remove old resetToken fields
+ const updatedUser = await ctx.db.mutation.updateUser({
+ where: { email: user.email },
+ data: {
+ password,
+ resetToken: null,
+ resetTokenExpiry: null,
+ },
+ });
+ // 6. Generate JWT
+ const token = jwt.sign({ userId: updatedUser.id }, process.env.APP_SECRET);
+ // 7. Set the JWT cookie
+ ctx.response.cookie('token', token, {
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 365,
+ });
+ // 8. return the new user
+ return updatedUser;
+ },
+ async updatePermissions(parent, args, ctx, info) {
+ // 1. Check if they are logged in
+ if (!ctx.request.userId) {
+ throw new Error('You must be logged in!');
+ }
+ // 2. Query the current user
+ const currentUser = await ctx.db.query.user(
+ {
+ where: {
+ id: ctx.request.userId,
+ },
+ },
+ info
+ );
+ // 3. Check if they have permissions to do this
+ hasPermission(currentUser, ['ADMIN', 'PERMISSIONUPDATE']);
+ // 4. Update the permissions
+ return ctx.db.mutation.updateUser(
+ {
+ data: {
+ permissions: {
+ set: args.permissions,
+ },
+ },
+ where: {
+ id: args.userId,
+ },
+ },
+ info
+ );
+ },
+};
+
+module.exports = Mutations;
diff --git a/stepped-solutions/37/backend/src/schema.graphql b/stepped-solutions/37/backend/src/schema.graphql
new file mode 100755
index 0000000..c59d53a
--- /dev/null
+++ b/stepped-solutions/37/backend/src/schema.graphql
@@ -0,0 +1,32 @@
+# import * from './generated/prisma.graphql'
+
+type SuccessMessage {
+ message: String
+}
+
+type Mutation {
+ createItem(title: String, description: String, price: Int, image: String, largeImage: String): Item!
+ updateItem(id: ID!, title: String, description: String, price: Int): Item!
+ deleteItem(id: ID!): Item
+ signup(email: String!, password: String!, name: String!): User!
+ signin(email: String!, password: String!): User!
+ signout: SuccessMessage
+ requestReset(email: String!): SuccessMessage
+ resetPassword(resetToken: String!, password: String!, confirmPassword: String!): User!
+ updatePermissions(permissions: [Permission], userId: ID!): User
+}
+
+type Query {
+ items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, first: Int): [Item]!
+ item(where: ItemWhereUniqueInput!): Item
+ itemsConnection(where: ItemWhereInput): ItemConnection!
+ me: User
+ users: [User]!
+}
+
+type User{
+ id: ID!
+ name: String!
+ email: String!
+ permissions: [Permission!]!
+}
diff --git a/stepped-solutions/37/frontend/components/Permissions.js b/stepped-solutions/37/frontend/components/Permissions.js
new file mode 100755
index 0000000..4b08c39
--- /dev/null
+++ b/stepped-solutions/37/frontend/components/Permissions.js
@@ -0,0 +1,131 @@
+import { Query, Mutation } from 'react-apollo';
+import Error from './ErrorMessage';
+import gql from 'graphql-tag';
+import Table from './styles/Table';
+import SickButton from './styles/SickButton';
+import PropTypes from 'prop-types';
+
+const possiblePermissions = [
+ 'ADMIN',
+ 'USER',
+ 'ITEMCREATE',
+ 'ITEMUPDATE',
+ 'ITEMDELETE',
+ 'PERMISSIONUPDATE',
+];
+
+const UPDATE_PERMISSIONS_MUTATION = gql`
+ mutation updatePermissions($permissions: [Permission], $userId: ID!) {
+ updatePermissions(permissions: $permissions, userId: $userId) {
+ id
+ permissions
+ name
+ email
+ }
+ }
+`;
+
+const ALL_USERS_QUERY = gql`
+ query {
+ users {
+ id
+ name
+ email
+ permissions
+ }
+ }
+`;
+
+const Permissions = props => (
+ <Query query={ALL_USERS_QUERY}>
+ {({ data, loading, error }) => (
+ <div>
+ <Error error={error} />
+ <div>
+ <h2>Manage Permissions</h2>
+ <Table>
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Email</th>
+ {possiblePermissions.map(permission => <th key={permission}>{permission}</th>)}
+ <th>👇🏻</th>
+ </tr>
+ </thead>
+ <tbody>{data.users.map(user => <UserPermissions user={user} key={user.id} />)}</tbody>
+ </Table>
+ </div>
+ </div>
+ )}
+ </Query>
+);
+
+class UserPermissions extends React.Component {
+ static propTypes = {
+ user: PropTypes.shape({
+ name: PropTypes.string,
+ email: PropTypes.string,
+ id: PropTypes.string,
+ permissions: PropTypes.array,
+ }).isRequired,
+ };
+ state = {
+ permissions: this.props.user.permissions,
+ };
+ handlePermissionChange = (e) => {
+ const checkbox = e.target;
+ // take a copy of the current permissions
+ let updatedPermissions = [...this.state.permissions];
+ // figure out if we need to remove or add this permission
+ if (checkbox.checked) {
+ // add it in!
+ updatedPermissions.push(checkbox.value);
+ } else {
+ updatedPermissions = updatedPermissions.filter(permission => permission !== checkbox.value);
+ }
+ this.setState({ permissions: updatedPermissions });
+ };
+ render() {
+ const user = this.props.user;
+ return (
+ <Mutation
+ mutation={UPDATE_PERMISSIONS_MUTATION}
+ variables={{
+ permissions: this.state.permissions,
+ userId: this.props.user.id,
+ }}
+ >
+ {(updatePermissions, { loading, error }) => (
+ <>
+ {error && <tr><td colspan="8"><Error error={error} /></td></tr>}
+ < tr >
+ <td>{user.name}</td>
+ <td>{user.email}</td>
+ {possiblePermissions.map(permission => (
+ <td key={permission}>
+ <label htmlFor={`${user.id}-permission-${permission}`}>
+ <input
+ id={`${user.id}-permission-${permission}`}
+ type="checkbox"
+ checked={this.state.permissions.includes(permission)}
+ value={permission}
+ onChange={this.handlePermissionChange}
+ />
+ </label>
+ </td>
+ ))}
+ <td>
+ <SickButton type="button" disabled={loading} onClick={updatePermissions}>
+ Updat{loading ? 'ing' : 'e'}
+ </SickButton>
+ </td>
+ </tr>
+ </>
+ )
+ }
+ </Mutation>
+ );
+ }
+}
+
+export default Permissions;
diff --git a/stepped-solutions/37/frontend/components/styles/Table.js b/stepped-solutions/37/frontend/components/styles/Table.js
new file mode 100755
index 0000000..b2cd6c4
--- /dev/null
+++ b/stepped-solutions/37/frontend/components/styles/Table.js
@@ -0,0 +1,35 @@
+import styled from 'styled-components';
+
+const Table = styled.table`
+ border-spacing: 0;
+ width: 100%;
+ border: 1px solid ${props => props.theme.offWhite};
+ thead {
+ font-size: 10px;
+ }
+ td,
+ th {
+ border-bottom: 1px solid ${props => props.theme.offWhite};
+ border-right: 1px solid ${props => props.theme.offWhite};
+ padding: 5px;
+ position: relative;
+ &:last-child {
+ border-right: none;
+ width: 150px;
+ button {
+ width: 100%;
+ }
+ }
+ label {
+ padding: 10px 5px;
+ display: block;
+ }
+ }
+ tr {
+ &:hover {
+ background: ${props => props.theme.offWhite};
+ }
+ }
+`;
+
+export default Table;
diff --git a/stepped-solutions/38/backend/src/resolvers/Mutation.js b/stepped-solutions/38/backend/src/resolvers/Mutation.js
new file mode 100755
index 0000000..b48be39
--- /dev/null
+++ b/stepped-solutions/38/backend/src/resolvers/Mutation.js
@@ -0,0 +1,215 @@
+const bcrypt = require('bcryptjs');
+const jwt = require('jsonwebtoken');
+const { randomBytes } = require('crypto');
+const { promisify } = require('util');
+const { transport, makeANiceEmail } = require('../mail');
+const { hasPermission } = require('../utils');
+
+const Mutations = {
+ async createItem(parent, args, ctx, info) {
+ if (!ctx.request.userId) {
+ throw new Error('You must be logged in to do that!');
+ }
+
+ const item = await ctx.db.mutation.createItem(
+ {
+ data: {
+ // This is how to create a relationship between the Item and the User
+ user: {
+ connect: {
+ id: ctx.request.userId,
+ },
+ },
+ ...args,
+ },
+ },
+ info
+ );
+
+ console.log(item);
+
+ return item;
+ },
+ updateItem(parent, args, ctx, info) {
+ // first take a copy of the updates
+ const updates = { ...args };
+ // remove the ID from the updates
+ delete updates.id;
+ // run the update method
+ return ctx.db.mutation.updateItem(
+ {
+ data: updates,
+ where: {
+ id: args.id,
+ },
+ },
+ info
+ );
+ },
+ async deleteItem(parent, args, ctx, info) {
+ const where = { id: args.id };
+ // 1. find the item
+ const item = await ctx.db.query.item({ where }, `{ id title user { id }}`);
+ // 2. Check if they own that item, or have the permissions
+ const ownsItem = item.user.id === ctx.request.userId;
+ const hasPermissions = ctx.request.user.permissions.some(permission =>
+ ['ADMIN', 'ITEMDELETE'].includes(permission)
+ );
+
+ if (!ownsItem && hasPermissions) {
+ throw new Error("You don't have permission to do that!");
+ }
+
+ // 3. Delete it!
+ return ctx.db.mutation.deleteItem({ where }, info);
+ },
+ async signup(parent, args, ctx, info) {
+ // lowercase their email
+ args.email = args.email.toLowerCase();
+ // hash their password
+ const password = await bcrypt.hash(args.password, 10);
+ // create the user in the database
+ const user = await ctx.db.mutation.createUser(
+ {
+ data: {
+ ...args,
+ password,
+ permissions: { set: ['USER'] },
+ },
+ },
+ info
+ );
+ // create the JWT token for them
+ const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
+ // We set the jwt as a cookie on the response
+ ctx.response.cookie('token', token, {
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 365, // 1 year cookie
+ });
+ // Finalllllly we return the user to the browser
+ return user;
+ },
+ async signin(parent, { email, password }, ctx, info) {
+ // 1. check if there is a user with that email
+ const user = await ctx.db.query.user({ where: { email } });
+ if (!user) {
+ throw new Error(`No such user found for email ${email}`);
+ }
+ // 2. Check if their password is correct
+ const valid = await bcrypt.compare(password, user.password);
+ if (!valid) {
+ throw new Error('Invalid Password!');
+ }
+ // 3. generate the JWT Token
+ const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
+ // 4. Set the cookie with the token
+ ctx.response.cookie('token', token, {
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 365,
+ });
+ // 5. Return the user
+ return user;
+ },
+ signout(parent, args, ctx, info) {
+ ctx.response.clearCookie('token');
+ return { message: 'Goodbye!' };
+ },
+ async requestReset(parent, args, ctx, info) {
+ // 1. Check if this is a real user
+ const user = await ctx.db.query.user({ where: { email: args.email } });
+ if (!user) {
+ throw new Error(`No such user found for email ${args.email}`);
+ }
+ // 2. Set a reset token and expiry on that user
+ const randomBytesPromiseified = promisify(randomBytes);
+ const resetToken = (await randomBytesPromiseified(20)).toString('hex');
+ const resetTokenExpiry = Date.now() + 3600000; // 1 hour from now
+ const res = await ctx.db.mutation.updateUser({
+ where: { email: args.email },
+ data: { resetToken, resetTokenExpiry },
+ });
+ // 3. Email them that reset token
+ const mailRes = await transport.sendMail({
+ from: 'wes@wesbos.com',
+ to: user.email,
+ subject: 'Your Password Reset Token',
+ html: makeANiceEmail(`Your Password Reset Token is here!
+ \n\n
+ <a href="${process.env
+ .FRONTEND_URL}/reset?resetToken=${resetToken}">Click Here to Reset</a>`),
+ });
+
+ // 4. Return the message
+ return { message: 'Thanks!' };
+ },
+ async resetPassword(parent, args, ctx, info) {
+ // 1. check if the passwords match
+ if (args.password !== args.confirmPassword) {
+ throw new Error("Yo Passwords don't match!");
+ }
+ // 2. check if its a legit reset token
+ // 3. Check if its expired
+ const [user] = await ctx.db.query.users({
+ where: {
+ resetToken: args.resetToken,
+ resetTokenExpiry_gte: Date.now() - 3600000,
+ },
+ });
+ if (!user) {
+ throw new Error('This token is either invalid or expired!');
+ }
+ // 4. Hash their new password
+ const password = await bcrypt.hash(args.password, 10);
+ // 5. Save the new password to the user and remove old resetToken fields
+ const updatedUser = await ctx.db.mutation.updateUser({
+ where: { email: user.email },
+ data: {
+ password,
+ resetToken: null,
+ resetTokenExpiry: null,
+ },
+ });
+ // 6. Generate JWT
+ const token = jwt.sign({ userId: updatedUser.id }, process.env.APP_SECRET);
+ // 7. Set the JWT cookie
+ ctx.response.cookie('token', token, {
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 365,
+ });
+ // 8. return the new user
+ return updatedUser;
+ },
+ async updatePermissions(parent, args, ctx, info) {
+ // 1. Check if they are logged in
+ if (!ctx.request.userId) {
+ throw new Error('You must be logged in!');
+ }
+ // 2. Query the current user
+ const currentUser = await ctx.db.query.user(
+ {
+ where: {
+ id: ctx.request.userId,
+ },
+ },
+ info
+ );
+ // 3. Check if they have permissions to do this
+ hasPermission(currentUser, ['ADMIN', 'PERMISSIONUPDATE']);
+ // 4. Update the permissions
+ return ctx.db.mutation.updateUser(
+ {
+ data: {
+ permissions: {
+ set: args.permissions,
+ },
+ },
+ where: {
+ id: args.userId,
+ },
+ },
+ info
+ );
+ },
+};
+
+module.exports = Mutations;
diff --git a/stepped-solutions/38/frontend/components/DeleteItem.js b/stepped-solutions/38/frontend/components/DeleteItem.js
new file mode 100755
index 0000000..e5e4752
--- /dev/null
+++ b/stepped-solutions/38/frontend/components/DeleteItem.js
@@ -0,0 +1,50 @@
+import React, { Component } from 'react';
+import { Mutation } from 'react-apollo';
+import gql from 'graphql-tag';
+import { ALL_ITEMS_QUERY } from './Items';
+
+const DELETE_ITEM_MUTATION = gql`
+ mutation DELETE_ITEM_MUTATION($id: ID!) {
+ deleteItem(id: $id) {
+ id
+ }
+ }
+`;
+
+class DeleteItem extends Component {
+ update = (cache, payload) => {
+ // manually update the cache on the client, so it matches the server
+ // 1. Read the cache for the items we want
+ const data = cache.readQuery({ query: ALL_ITEMS_QUERY });
+ console.log(data, payload);
+ // 2. Filter the deleted itemout of the page
+ data.items = data.items.filter(item => item.id !== payload.data.deleteItem.id);
+ // 3. Put the items back!
+ cache.writeQuery({ query: ALL_ITEMS_QUERY, data });
+ };
+ render() {
+ return (
+ <Mutation
+ mutation={DELETE_ITEM_MUTATION}
+ variables={{ id: this.props.id }}
+ update={this.update}
+ >
+ {(deleteItem, { error }) => (
+ <button
+ onClick={() => {
+ if (confirm('Are you sure you want to delete this item?')) {
+ deleteItem().catch(err => {
+ alert(err.message);
+ });
+ }
+ }}
+ >
+ {this.props.children}
+ </button>
+ )}
+ </Mutation>
+ );
+ }
+}
+
+export default DeleteItem;