diff options
| -rw-r--r-- | finished-application/backend/src/resolvers/Query.js | 7 | ||||
| -rw-r--r-- | finished-application/backend/src/schema.graphql | 1 | ||||
| -rw-r--r-- | finished-application/frontend/components/PleaseSignIn.js | 2 | ||||
| -rwxr-xr-x | stepped-solutions/33/backend/datamodel.graphql | 28 | ||||
| -rwxr-xr-x | stepped-solutions/33/backend/src/generated/prisma.graphql | 850 | ||||
| -rwxr-xr-x | stepped-solutions/33/backend/src/resolvers/Mutation.js | 175 | ||||
| -rwxr-xr-x | stepped-solutions/34/frontend/components/PleaseSignIn.js | 22 | ||||
| -rwxr-xr-x | stepped-solutions/34/frontend/pages/sell.js | 12 | ||||
| -rwxr-xr-x | stepped-solutions/35/backend/src/index.js | 46 | ||||
| -rwxr-xr-x | stepped-solutions/35/backend/src/resolvers/Query.js | 34 | ||||
| -rwxr-xr-x | stepped-solutions/35/backend/src/schema.graphql | 31 | ||||
| -rwxr-xr-x | stepped-solutions/35/frontend/components/Permissions.js | 73 | ||||
| -rwxr-xr-x | stepped-solutions/35/frontend/pages/permissions.js | 12 |
13 files changed, 1292 insertions, 1 deletions
diff --git a/finished-application/backend/src/resolvers/Query.js b/finished-application/backend/src/resolvers/Query.js index 51c2c42..a51ead5 100644 --- a/finished-application/backend/src/resolvers/Query.js +++ b/finished-application/backend/src/resolvers/Query.js @@ -5,6 +5,13 @@ const { forwardTo } = require('prisma-binding'); const Query = { items: forwardTo('db'), itemsConnection: forwardTo('db'), + async users(parent, args, ctx, info) { + if (!ctx.request.userId) { + throw new Error('Insufficient Permissions'); + } + + return ctx.db.query.users({}, info); + }, async order(parent, args, ctx, info) { // 1. make sure they are signed in diff --git a/finished-application/backend/src/schema.graphql b/finished-application/backend/src/schema.graphql index 07533eb..9c4a200 100644 --- a/finished-application/backend/src/schema.graphql +++ b/finished-application/backend/src/schema.graphql @@ -3,6 +3,7 @@ type Query { me: User + users: [User]! order(id: ID!): Order! orders(where: OrderWhereInput, orderBy: OrderOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Order]! items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Item]! diff --git a/finished-application/frontend/components/PleaseSignIn.js b/finished-application/frontend/components/PleaseSignIn.js index 5646749..3cd7267 100644 --- a/finished-application/frontend/components/PleaseSignIn.js +++ b/finished-application/frontend/components/PleaseSignIn.js @@ -18,7 +18,7 @@ const PleaseSignIn = props => ( } // check if they need permissions if (props.allowedPermissions) { - // check if they NO permissions, or they don't meet the requmrenets + // check if they have NO permissions, or they don't meet the requmrenets if ( !data.me.permissions || !props.allowedPermissions.some(permission => data.me.permissions.includes(permission)) diff --git a/stepped-solutions/33/backend/datamodel.graphql b/stepped-solutions/33/backend/datamodel.graphql new file mode 100755 index 0000000..e9b8009 --- /dev/null +++ b/stepped-solutions/33/backend/datamodel.graphql @@ -0,0 +1,28 @@ +enum Permission { + ADMIN + USER + ITEMCREATE + ITEMUPDATE + ITEMDELETE + PERMISSIONUPDATE +} + +type User { + id: ID! @unique + name: String! + email: String! @unique + password: String! + resetToken: String + resetTokenExpiry: String + permissions: [Permission] +} + +type Item { + id: ID! @unique + title: String! + description: String! + image: String + largeImage: String + price: Int! + user: User! +} diff --git a/stepped-solutions/33/backend/src/generated/prisma.graphql b/stepped-solutions/33/backend/src/generated/prisma.graphql new file mode 100755 index 0000000..40a64c0 --- /dev/null +++ b/stepped-solutions/33/backend/src/generated/prisma.graphql @@ -0,0 +1,850 @@ +# source: https://us1.prisma.sh/wesbos/siccccccccck-fits/dev +# timestamp: Tue Aug 14 2018 13:23:45 GMT-0400 (EDT) + +type AggregateItem { + count: Int! +} + +type AggregateUser { + count: Int! +} + +type BatchPayload { + """The number of nodes that have been affected by the Batch operation.""" + count: Long! +} + +type Item implements Node { + id: ID! + title: String! + description: String! + image: String + largeImage: String + price: Int! + user(where: UserWhereInput): User! +} + +"""A connection to a list of items.""" +type ItemConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [ItemEdge]! + aggregate: AggregateItem! +} + +input ItemCreateInput { + title: String! + description: String! + image: String + largeImage: String + price: Int! + user: UserCreateOneInput! +} + +"""An edge in a connection.""" +type ItemEdge { + """The item at the end of the edge.""" + node: Item! + + """A cursor for use in pagination.""" + cursor: String! +} + +enum ItemOrderByInput { + id_ASC + id_DESC + title_ASC + title_DESC + description_ASC + description_DESC + image_ASC + image_DESC + largeImage_ASC + largeImage_DESC + price_ASC + price_DESC + updatedAt_ASC + updatedAt_DESC + createdAt_ASC + createdAt_DESC +} + +type ItemPreviousValues { + id: ID! + title: String! + description: String! + image: String + largeImage: String + price: Int! +} + +type ItemSubscriptionPayload { + mutation: MutationType! + node: Item + updatedFields: [String!] + previousValues: ItemPreviousValues +} + +input ItemSubscriptionWhereInput { + """Logical AND on all given filters.""" + AND: [ItemSubscriptionWhereInput!] + + """Logical OR on all given filters.""" + OR: [ItemSubscriptionWhereInput!] + + """Logical NOT on all given filters combined by AND.""" + NOT: [ItemSubscriptionWhereInput!] + + """ + The subscription event gets dispatched when it's listed in mutation_in + """ + mutation_in: [MutationType!] + + """ + The subscription event gets only dispatched when one of the updated fields names is included in this list + """ + updatedFields_contains: String + + """ + The subscription event gets only dispatched when all of the field names included in this list have been updated + """ + updatedFields_contains_every: [String!] + + """ + The subscription event gets only dispatched when some of the field names included in this list have been updated + """ + updatedFields_contains_some: [String!] + node: ItemWhereInput +} + +input ItemUpdateInput { + title: String + description: String + image: String + largeImage: String + price: Int + user: UserUpdateOneInput +} + +input ItemWhereInput { + """Logical AND on all given filters.""" + AND: [ItemWhereInput!] + + """Logical OR on all given filters.""" + OR: [ItemWhereInput!] + + """Logical NOT on all given filters combined by AND.""" + NOT: [ItemWhereInput!] + id: ID + + """All values that are not equal to given value.""" + id_not: ID + + """All values that are contained in given list.""" + id_in: [ID!] + + """All values that are not contained in given list.""" + id_not_in: [ID!] + + """All values less than the given value.""" + id_lt: ID + + """All values less than or equal the given value.""" + id_lte: ID + + """All values greater than the given value.""" + id_gt: ID + + """All values greater than or equal the given value.""" + id_gte: ID + + """All values containing the given string.""" + id_contains: ID + + """All values not containing the given string.""" + id_not_contains: ID + + """All values starting with the given string.""" + id_starts_with: ID + + """All values not starting with the given string.""" + id_not_starts_with: ID + + """All values ending with the given string.""" + id_ends_with: ID + + """All values not ending with the given string.""" + id_not_ends_with: ID + title: String + + """All values that are not equal to given value.""" + title_not: String + + """All values that are contained in given list.""" + title_in: [String!] + + """All values that are not contained in given list.""" + title_not_in: [String!] + + """All values less than the given value.""" + title_lt: String + + """All values less than or equal the given value.""" + title_lte: String + + """All values greater than the given value.""" + title_gt: String + + """All values greater than or equal the given value.""" + title_gte: String + + """All values containing the given string.""" + title_contains: String + + """All values not containing the given string.""" + title_not_contains: String + + """All values starting with the given string.""" + title_starts_with: String + + """All values not starting with the given string.""" + title_not_starts_with: String + + """All values ending with the given string.""" + title_ends_with: String + + """All values not ending with the given string.""" + title_not_ends_with: String + description: String + + """All values that are not equal to given value.""" + description_not: String + + """All values that are contained in given list.""" + description_in: [String!] + + """All values that are not contained in given list.""" + description_not_in: [String!] + + """All values less than the given value.""" + description_lt: String + + """All values less than or equal the given value.""" + description_lte: String + + """All values greater than the given value.""" + description_gt: String + + """All values greater than or equal the given value.""" + description_gte: String + + """All values containing the given string.""" + description_contains: String + + """All values not containing the given string.""" + description_not_contains: String + + """All values starting with the given string.""" + description_starts_with: String + + """All values not starting with the given string.""" + description_not_starts_with: String + + """All values ending with the given string.""" + description_ends_with: String + + """All values not ending with the given string.""" + description_not_ends_with: String + image: String + + """All values that are not equal to given value.""" + image_not: String + + """All values that are contained in given list.""" + image_in: [String!] + + """All values that are not contained in given list.""" + image_not_in: [String!] + + """All values less than the given value.""" + image_lt: String + + """All values less than or equal the given value.""" + image_lte: String + + """All values greater than the given value.""" + image_gt: String + + """All values greater than or equal the given value.""" + image_gte: String + + """All values containing the given string.""" + image_contains: String + + """All values not containing the given string.""" + image_not_contains: String + + """All values starting with the given string.""" + image_starts_with: String + + """All values not starting with the given string.""" + image_not_starts_with: String + + """All values ending with the given string.""" + image_ends_with: String + + """All values not ending with the given string.""" + image_not_ends_with: String + largeImage: String + + """All values that are not equal to given value.""" + largeImage_not: String + + """All values that are contained in given list.""" + largeImage_in: [String!] + + """All values that are not contained in given list.""" + largeImage_not_in: [String!] + + """All values less than the given value.""" + largeImage_lt: String + + """All values less than or equal the given value.""" + largeImage_lte: String + + """All values greater than the given value.""" + largeImage_gt: String + + """All values greater than or equal the given value.""" + largeImage_gte: String + + """All values containing the given string.""" + largeImage_contains: String + + """All values not containing the given string.""" + largeImage_not_contains: String + + """All values starting with the given string.""" + largeImage_starts_with: String + + """All values not starting with the given string.""" + largeImage_not_starts_with: String + + """All values ending with the given string.""" + largeImage_ends_with: String + + """All values not ending with the given string.""" + largeImage_not_ends_with: String + price: Int + + """All values that are not equal to given value.""" + price_not: Int + + """All values that are contained in given list.""" + price_in: [Int!] + + """All values that are not contained in given list.""" + price_not_in: [Int!] + + """All values less than the given value.""" + price_lt: Int + + """All values less than or equal the given value.""" + price_lte: Int + + """All values greater than the given value.""" + price_gt: Int + + """All values greater than or equal the given value.""" + price_gte: Int + user: UserWhereInput +} + +input ItemWhereUniqueInput { + id: ID +} + +""" +The `Long` scalar type represents non-fractional signed whole numeric values. +Long can represent values between -(2^63) and 2^63 - 1. +""" +scalar Long + +type Mutation { + createItem(data: ItemCreateInput!): Item! + createUser(data: UserCreateInput!): User! + updateItem(data: ItemUpdateInput!, where: ItemWhereUniqueInput!): Item + updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User + deleteItem(where: ItemWhereUniqueInput!): Item + deleteUser(where: UserWhereUniqueInput!): User + upsertItem(where: ItemWhereUniqueInput!, create: ItemCreateInput!, update: ItemUpdateInput!): Item! + upsertUser(where: UserWhereUniqueInput!, create: UserCreateInput!, update: UserUpdateInput!): User! + updateManyItems(data: ItemUpdateInput!, where: ItemWhereInput): BatchPayload! + updateManyUsers(data: UserUpdateInput!, where: UserWhereInput): BatchPayload! + deleteManyItems(where: ItemWhereInput): BatchPayload! + deleteManyUsers(where: UserWhereInput): BatchPayload! +} + +enum MutationType { + CREATED + UPDATED + DELETED +} + +"""An object with an ID""" +interface Node { + """The id of the object.""" + id: ID! +} + +"""Information about pagination in a connection.""" +type PageInfo { + """When paginating forwards, are there more items?""" + hasNextPage: Boolean! + + """When paginating backwards, are there more items?""" + hasPreviousPage: Boolean! + + """When paginating backwards, the cursor to continue.""" + startCursor: String + + """When paginating forwards, the cursor to continue.""" + endCursor: String +} + +enum Permission { + ADMIN + USER + ITEMCREATE + ITEMUPDATE + ITEMDELETE + PERMISSIONUPDATE +} + +type Query { + items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Item]! + users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]! + item(where: ItemWhereUniqueInput!): Item + user(where: UserWhereUniqueInput!): User + itemsConnection(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): ItemConnection! + usersConnection(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): UserConnection! + + """Fetches an object given its ID""" + node( + """The ID of an object""" + id: ID! + ): Node +} + +type Subscription { + item(where: ItemSubscriptionWhereInput): ItemSubscriptionPayload + user(where: UserSubscriptionWhereInput): UserSubscriptionPayload +} + +type User implements Node { + id: ID! + name: String! + email: String! + password: String! + resetToken: String + resetTokenExpiry: String + permissions: [Permission!]! +} + +"""A connection to a list of items.""" +type UserConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [UserEdge]! + aggregate: AggregateUser! +} + +input UserCreateInput { + name: String! + email: String! + password: String! + resetToken: String + resetTokenExpiry: String + permissions: UserCreatepermissionsInput +} + +input UserCreateOneInput { + create: UserCreateInput + connect: UserWhereUniqueInput +} + +input UserCreatepermissionsInput { + set: [Permission!] +} + +"""An edge in a connection.""" +type UserEdge { + """The item at the end of the edge.""" + node: User! + + """A cursor for use in pagination.""" + cursor: String! +} + +enum UserOrderByInput { + id_ASC + id_DESC + name_ASC + name_DESC + email_ASC + email_DESC + password_ASC + password_DESC + resetToken_ASC + resetToken_DESC + resetTokenExpiry_ASC + resetTokenExpiry_DESC + updatedAt_ASC + updatedAt_DESC + createdAt_ASC + createdAt_DESC +} + +type UserPreviousValues { + id: ID! + name: String! + email: String! + password: String! + resetToken: String + resetTokenExpiry: String + permissions: [Permission!]! +} + +type UserSubscriptionPayload { + mutation: MutationType! + node: User + updatedFields: [String!] + previousValues: UserPreviousValues +} + +input UserSubscriptionWhereInput { + """Logical AND on all given filters.""" + AND: [UserSubscriptionWhereInput!] + + """Logical OR on all given filters.""" + OR: [UserSubscriptionWhereInput!] + + """Logical NOT on all given filters combined by AND.""" + NOT: [UserSubscriptionWhereInput!] + + """ + The subscription event gets dispatched when it's listed in mutation_in + """ + mutation_in: [MutationType!] + + """ + The subscription event gets only dispatched when one of the updated fields names is included in this list + """ + updatedFields_contains: String + + """ + The subscription event gets only dispatched when all of the field names included in this list have been updated + """ + updatedFields_contains_every: [String!] + + """ + The subscription event gets only dispatched when some of the field names included in this list have been updated + """ + updatedFields_contains_some: [String!] + node: UserWhereInput +} + +input UserUpdateDataInput { + name: String + email: String + password: String + resetToken: String + resetTokenExpiry: String + permissions: UserUpdatepermissionsInput +} + +input UserUpdateInput { + name: String + email: String + password: String + resetToken: String + resetTokenExpiry: String + permissions: UserUpdatepermissionsInput +} + +input UserUpdateOneInput { + create: UserCreateInput + connect: UserWhereUniqueInput + delete: Boolean + update: UserUpdateDataInput + upsert: UserUpsertNestedInput +} + +input UserUpdatepermissionsInput { + set: [Permission!] +} + +input UserUpsertNestedInput { + update: UserUpdateDataInput! + create: UserCreateInput! +} + +input UserWhereInput { + """Logical AND on all given filters.""" + AND: [UserWhereInput!] + + """Logical OR on all given filters.""" + OR: [UserWhereInput!] + + """Logical NOT on all given filters combined by AND.""" + NOT: [UserWhereInput!] + id: ID + + """All values that are not equal to given value.""" + id_not: ID + + """All values that are contained in given list.""" + id_in: [ID!] + + """All values that are not contained in given list.""" + id_not_in: [ID!] + + """All values less than the given value.""" + id_lt: ID + + """All values less than or equal the given value.""" + id_lte: ID + + """All values greater than the given value.""" + id_gt: ID + + """All values greater than or equal the given value.""" + id_gte: ID + + """All values containing the given string.""" + id_contains: ID + + """All values not containing the given string.""" + id_not_contains: ID + + """All values starting with the given string.""" + id_starts_with: ID + + """All values not starting with the given string.""" + id_not_starts_with: ID + + """All values ending with the given string.""" + id_ends_with: ID + + """All values not ending with the given string.""" + id_not_ends_with: ID + name: String + + """All values that are not equal to given value.""" + name_not: String + + """All values that are contained in given list.""" + name_in: [String!] + + """All values that are not contained in given list.""" + name_not_in: [String!] + + """All values less than the given value.""" + name_lt: String + + """All values less than or equal the given value.""" + name_lte: String + + """All values greater than the given value.""" + name_gt: String + + """All values greater than or equal the given value.""" + name_gte: String + + """All values containing the given string.""" + name_contains: String + + """All values not containing the given string.""" + name_not_contains: String + + """All values starting with the given string.""" + name_starts_with: String + + """All values not starting with the given string.""" + name_not_starts_with: String + + """All values ending with the given string.""" + name_ends_with: String + + """All values not ending with the given string.""" + name_not_ends_with: String + email: String + + """All values that are not equal to given value.""" + email_not: String + + """All values that are contained in given list.""" + email_in: [String!] + + """All values that are not contained in given list.""" + email_not_in: [String!] + + """All values less than the given value.""" + email_lt: String + + """All values less than or equal the given value.""" + email_lte: String + + """All values greater than the given value.""" + email_gt: String + + """All values greater than or equal the given value.""" + email_gte: String + + """All values containing the given string.""" + email_contains: String + + """All values not containing the given string.""" + email_not_contains: String + + """All values starting with the given string.""" + email_starts_with: String + + """All values not starting with the given string.""" + email_not_starts_with: String + + """All values ending with the given string.""" + email_ends_with: String + + """All values not ending with the given string.""" + email_not_ends_with: String + password: String + + """All values that are not equal to given value.""" + password_not: String + + """All values that are contained in given list.""" + password_in: [String!] + + """All values that are not contained in given list.""" + password_not_in: [String!] + + """All values less than the given value.""" + password_lt: String + + """All values less than or equal the given value.""" + password_lte: String + + """All values greater than the given value.""" + password_gt: String + + """All values greater than or equal the given value.""" + password_gte: String + + """All values containing the given string.""" + password_contains: String + + """All values not containing the given string.""" + password_not_contains: String + + """All values starting with the given string.""" + password_starts_with: String + + """All values not starting with the given string.""" + password_not_starts_with: String + + """All values ending with the given string.""" + password_ends_with: String + + """All values not ending with the given string.""" + password_not_ends_with: String + resetToken: String + + """All values that are not equal to given value.""" + resetToken_not: String + + """All values that are contained in given list.""" + resetToken_in: [String!] + + """All values that are not contained in given list.""" + resetToken_not_in: [String!] + + """All values less than the given value.""" + resetToken_lt: String + + """All values less than or equal the given value.""" + resetToken_lte: String + + """All values greater than the given value.""" + resetToken_gt: String + + """All values greater than or equal the given value.""" + resetToken_gte: String + + """All values containing the given string.""" + resetToken_contains: String + + """All values not containing the given string.""" + resetToken_not_contains: String + + """All values starting with the given string.""" + resetToken_starts_with: String + + """All values not starting with the given string.""" + resetToken_not_starts_with: String + + """All values ending with the given string.""" + resetToken_ends_with: String + + """All values not ending with the given string.""" + resetToken_not_ends_with: String + resetTokenExpiry: String + + """All values that are not equal to given value.""" + resetTokenExpiry_not: String + + """All values that are contained in given list.""" + resetTokenExpiry_in: [String!] + + """All values that are not contained in given list.""" + resetTokenExpiry_not_in: [String!] + + """All values less than the given value.""" + resetTokenExpiry_lt: String + + """All values less than or equal the given value.""" + resetTokenExpiry_lte: String + + """All values greater than the given value.""" + resetTokenExpiry_gt: String + + """All values greater than or equal the given value.""" + resetTokenExpiry_gte: String + + """All values containing the given string.""" + resetTokenExpiry_contains: String + + """All values not containing the given string.""" + resetTokenExpiry_not_contains: String + + """All values starting with the given string.""" + resetTokenExpiry_starts_with: String + + """All values not starting with the given string.""" + resetTokenExpiry_not_starts_with: String + + """All values ending with the given string.""" + resetTokenExpiry_ends_with: String + + """All values not ending with the given string.""" + resetTokenExpiry_not_ends_with: String +} + +input UserWhereUniqueInput { + id: ID + email: String +} diff --git a/stepped-solutions/33/backend/src/resolvers/Mutation.js b/stepped-solutions/33/backend/src/resolvers/Mutation.js new file mode 100755 index 0000000..ada7d3c --- /dev/null +++ b/stepped-solutions/33/backend/src/resolvers/Mutation.js @@ -0,0 +1,175 @@ +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const { randomBytes } = require('crypto'); +const { promisify } = require('util'); +const { transport, makeANiceEmail } = require('../mail'); + +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; + }, +}; + +module.exports = Mutations; diff --git a/stepped-solutions/34/frontend/components/PleaseSignIn.js b/stepped-solutions/34/frontend/components/PleaseSignIn.js new file mode 100755 index 0000000..80cfdbf --- /dev/null +++ b/stepped-solutions/34/frontend/components/PleaseSignIn.js @@ -0,0 +1,22 @@ +import { Query } from 'react-apollo'; +import { CURRENT_USER_QUERY } from './User'; +import Signin from './Signin'; + +const PleaseSignIn = props => ( + <Query query={CURRENT_USER_QUERY}> + {({ data, loading }) => { + if (loading) return <p>Loading...</p>; + if (!data.me) { + return ( + <div> + <p>Please Sign In before Continuing</p> + <Signin /> + </div> + ); + } + return props.children; + }} + </Query> +); + +export default PleaseSignIn; diff --git a/stepped-solutions/34/frontend/pages/sell.js b/stepped-solutions/34/frontend/pages/sell.js new file mode 100755 index 0000000..b60ae80 --- /dev/null +++ b/stepped-solutions/34/frontend/pages/sell.js @@ -0,0 +1,12 @@ +import CreateItem from '../components/CreateItem'; +import PleaseSignIn from '../components/PleaseSignIn'; + +const Sell = props => ( + <div> + <PleaseSignIn> + <CreateItem /> + </PleaseSignIn> + </div> +); + +export default Sell; diff --git a/stepped-solutions/35/backend/src/index.js b/stepped-solutions/35/backend/src/index.js new file mode 100755 index 0000000..804d6d6 --- /dev/null +++ b/stepped-solutions/35/backend/src/index.js @@ -0,0 +1,46 @@ +const cookieParser = require('cookie-parser'); +const jwt = require('jsonwebtoken'); + +require('dotenv').config({ path: 'variables.env' }); +const createServer = require('./createServer'); +const db = require('./db'); + +const server = createServer(); + +server.express.use(cookieParser()); + +// decode the JWT so we can get the user Id on each request +server.express.use((req, res, next) => { + const { token } = req.cookies; + if (token) { + const { userId } = jwt.verify(token, process.env.APP_SECRET); + // put the userId onto the req for future requests to access + req.userId = userId; + } + next(); +}); + +// 2. Create a middleware that populates the user on each request + +server.express.use(async (req, res, next) => { + // if they aren't logged in, skip this + if (!req.userId) return next(); + const user = await db.query.user( + { where: { id: req.userId } }, + '{ id, permissions, email, name }' + ); + req.user = user; + next(); +}); + +server.start( + { + cors: { + credentials: true, + origin: process.env.FRONTEND_URL, + }, + }, + deets => { + console.log(`Server is now running on port http://localhost:${deets.port}`); + } +); diff --git a/stepped-solutions/35/backend/src/resolvers/Query.js b/stepped-solutions/35/backend/src/resolvers/Query.js new file mode 100755 index 0000000..8af7b6c --- /dev/null +++ b/stepped-solutions/35/backend/src/resolvers/Query.js @@ -0,0 +1,34 @@ +const { forwardTo } = require('prisma-binding'); +const { hasPermission } = require('../utils'); + +const Query = { + items: forwardTo('db'), + item: forwardTo('db'), + itemsConnection: forwardTo('db'), + me(parent, args, ctx, info) { + // check if there is a current user ID + if (!ctx.request.userId) { + return null; + } + return ctx.db.query.user( + { + where: { id: ctx.request.userId }, + }, + info + ); + }, + async users(parent, args, ctx, info) { + // 1. Check if they are logged in + if (!ctx.request.userId) { + throw new Error('You must be logged in!'); + } + console.log(ctx.request.userId); + // 2. Check if the user has the permissions to query all the users + hasPermission(ctx.request.user, ['ADMIN', 'PERMISSIONUPDATE']); + + // 2. if they do, query all the users! + return ctx.db.query.users({}, info); + }, +}; + +module.exports = Query; diff --git a/stepped-solutions/35/backend/src/schema.graphql b/stepped-solutions/35/backend/src/schema.graphql new file mode 100755 index 0000000..323b1dc --- /dev/null +++ b/stepped-solutions/35/backend/src/schema.graphql @@ -0,0 +1,31 @@ +# 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! +} + +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/35/frontend/components/Permissions.js b/stepped-solutions/35/frontend/components/Permissions.js new file mode 100755 index 0000000..ca42ef6 --- /dev/null +++ b/stepped-solutions/35/frontend/components/Permissions.js @@ -0,0 +1,73 @@ +import { Query } from 'react-apollo'; +import Error from './ErrorMessage'; +import gql from 'graphql-tag'; +import Table from './styles/Table'; +import SickButton from './styles/SickButton'; + +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>{permission}</th>)} + <th>👇🏻</th> + </tr> + </thead> + <tbody>{data.users.map(user => <User user={user} />)}</tbody> + </Table> + </div> + </div> + )} + </Query> +); + +class User extends React.Component { + render() { + const user = this.props.user; + return ( + <tr> + <td>{user.name}</td> + <td>{user.email}</td> + {possiblePermissions.map(permission => ( + <td> + <label htmlFor={`${user.id}-permission-${permission}`}> + <input type="checkbox" /> + </label> + </td> + ))} + <td> + <SickButton>Update</SickButton> + </td> + </tr> + ); + } +} + +export default Permissions; diff --git a/stepped-solutions/35/frontend/pages/permissions.js b/stepped-solutions/35/frontend/pages/permissions.js new file mode 100755 index 0000000..de58c51 --- /dev/null +++ b/stepped-solutions/35/frontend/pages/permissions.js @@ -0,0 +1,12 @@ +import PleaseSignIn from '../components/PleaseSignIn'; +import Permissions from '../components/Permissions'; + +const PermissionsPage = props => ( + <div> + <PleaseSignIn> + <Permissions /> + </PleaseSignIn> + </div> +); + +export default PermissionsPage; |
