From b6cc5ed719e1a20640655fb86f874ab82d9f3a4f Mon Sep 17 00:00:00 2001 From: ben awad Date: Sat, 17 Jun 2017 11:12:47 -0500 Subject: [PATCH 01/31] Add cors --- index.js | 3 +++ package.json | 3 ++- yarn.lock | 11 +++++++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 376f4dd..cc660bb 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ import express from 'express'; import bodyParser from 'body-parser'; import { graphiqlExpress, graphqlExpress } from 'graphql-server-express'; import { makeExecutableSchema } from 'graphql-tools'; +import cors from 'cors'; import typeDefs from './schema'; import resolvers from './resolvers'; @@ -14,6 +15,8 @@ const schema = makeExecutableSchema({ const app = express(); +app.use(cors('*')); + app.use( '/graphiql', graphiqlExpress({ diff --git a/package.json b/package.json index ecedafb..2f32b30 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "license": "ISC", "dependencies": { "body-parser": "^1.17.2", + "cors": "^2.8.3", "express": "^4.15.3", "graphql-server-express": "^0.8.0", "graphql-tools": "^1.0.0", @@ -25,4 +26,4 @@ "graphql": "^0.10.1", "nodemon": "^1.11.0" } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 8e1e52f..b362328 100644 --- a/yarn.lock +++ b/yarn.lock @@ -881,6 +881,13 @@ core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" +cors@^2.8.3: + version "2.8.3" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.3.tgz#4cf78e1d23329a7496b2fc2225b77ca5bb5eb802" + dependencies: + object-assign "^4" + vary "^1" + cross-env@^3.1.2: version "3.2.4" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-3.2.4.tgz#9e0585f277864ed421ce756f81a980ff0d698aba" @@ -2199,7 +2206,7 @@ object-assign@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" -object-assign@^4.0.1, object-assign@^4.1.0: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -3043,7 +3050,7 @@ validator@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/validator/-/validator-6.3.0.tgz#47ce23ed8d4eaddfa9d4b8ef0071b6cf1078d7c8" -vary@~1.1.1: +vary@^1, vary@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" From a161655387c64b1ad0592e438e55513aada5d68d Mon Sep 17 00:00:00 2001 From: ben awad Date: Sat, 17 Jun 2017 11:57:41 -0500 Subject: [PATCH 02/31] readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 43ceccb..8ccfbf4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ # Template for an Express Server with GraphQL -[Watch the video to learn how it was made.](https://youtu.be/hqk30IVeYak -) +[Watch the video to learn how it was made.](https://youtu.be/TU-jZTpCig4) From aea0626e808300472183e4f46bd7045d83accf9d Mon Sep 17 00:00:00 2001 From: ben awad Date: Sun, 18 Jun 2017 16:41:11 -0500 Subject: [PATCH 03/31] add authentication --- index.js | 24 ++++++- models/user.js | 10 ++- package.json | 3 + resolvers.js | 54 +++++++++++++-- schema.js | 6 +- yarn.lock | 175 +++++++++++++++++++++++++++++++++++++++++++++---- 6 files changed, 248 insertions(+), 24 deletions(-) diff --git a/index.js b/index.js index cc660bb..d008ea4 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ import bodyParser from 'body-parser'; import { graphiqlExpress, graphqlExpress } from 'graphql-server-express'; import { makeExecutableSchema } from 'graphql-tools'; import cors from 'cors'; +import jwt from 'jsonwebtoken'; import typeDefs from './schema'; import resolvers from './resolvers'; @@ -13,9 +14,23 @@ const schema = makeExecutableSchema({ resolvers, }); +const SECRET = 'aslkdjlkaj10830912039jlkoaiuwerasdjflkasd'; + const app = express(); +const addUser = async (req) => { + const token = req.headers.authorization; + try { + const { user } = await jwt.verify(token, SECRET); + req.user = user; + } catch (err) { + console.log(err); + } + req.next(); +}; + app.use(cors('*')); +app.use(addUser); app.use( '/graphiql', @@ -27,7 +42,14 @@ app.use( app.use( '/graphql', bodyParser.json(), - graphqlExpress({ schema, context: { models } }), + graphqlExpress(req => ({ + schema, + context: { + models, + SECRET, + user: req.user, + }, + })), ); models.sequelize.sync().then(() => app.listen(3000)); diff --git a/models/user.js b/models/user.js index 31a1a0f..61524d5 100644 --- a/models/user.js +++ b/models/user.js @@ -1,6 +1,14 @@ export default (sequelize, DataTypes) => { const User = sequelize.define('User', { - username: DataTypes.STRING, + username: { + type: DataTypes.STRING, + unique: true, + }, + email: { + type: DataTypes.STRING, + unique: true, + }, + password: DataTypes.STRING, }); User.associate = (models) => { diff --git a/package.json b/package.json index 2f32b30..e7414b5 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,14 @@ "author": "", "license": "ISC", "dependencies": { + "bcrypt": "^1.0.2", "body-parser": "^1.17.2", "cors": "^2.8.3", "express": "^4.15.3", "graphql-server-express": "^0.8.0", "graphql-tools": "^1.0.0", + "jsonwebtoken": "^7.4.1", + "lodash": "^4.17.4", "sequelize": "^4.1.0" }, "devDependencies": { diff --git a/resolvers.js b/resolvers.js index 0d2f4a8..9bd5f4a 100644 --- a/resolvers.js +++ b/resolvers.js @@ -1,3 +1,7 @@ +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; +import _ from 'lodash'; + export default { User: { boards: ({ id }, args, { models }) => @@ -31,12 +35,18 @@ export default { }, Query: { allUsers: (parent, args, { models }) => models.User.findAll(), - getUser: (parent, { username }, { models }) => - models.User.findOne({ - where: { - username, - }, - }), + me: (parent, args, { models, user }) => { + if (user) { + // they are logged in + return models.User.findOne({ + where: { + id: user.id, + }, + }); + } + // not logged in user + return null; + }, userBoards: (parent, { owner }, { models }) => models.Board.findAll({ where: { @@ -52,7 +62,6 @@ export default { }, Mutation: { - createUser: (parent, args, { models }) => models.User.create(args), updateUser: (parent, { username, newUsername }, { models }) => models.User.update({ username: newUsername }, { where: { username } }), deleteUser: (parent, args, { models }) => @@ -60,5 +69,36 @@ export default { createBoard: (parent, args, { models }) => models.Board.create(args), createSuggestion: (parent, args, { models }) => models.Suggestion.create(args), + register: async (parent, args, { models }) => { + const user = args; + user.password = await bcrypt.hash(user.password, 12); + return models.User.create(user); + }, + login: async (parent, { email, password }, { models, SECRET }) => { + const user = await models.User.findOne({ where: { email } }); + if (!user) { + throw new Error('Not user with that email'); + } + + const valid = await bcrypt.compare(password, user.password); + if (!valid) { + throw new Error('Incorrect password'); + } + + // token = '12083098123414aslkjdasldf.asdhfaskjdh12982u793.asdlfjlaskdj10283491' + // verify: needs secret | use me for authentication + // decode: no secret | use me on the client side + const token = jwt.sign( + { + user: _.pick(user, ['id', 'username']), + }, + SECRET, + { + expiresIn: '1y', + }, + ); + + return token; + }, }, }; diff --git a/schema.js b/schema.js index 3114a8c..a759b18 100644 --- a/schema.js +++ b/schema.js @@ -16,6 +16,7 @@ export default ` type User { id: Int! username: String! + email: String! createdAt: String! updatedAt: String! boards: [Board!]! @@ -24,16 +25,17 @@ export default ` type Query { allUsers: [User!]! - getUser(username: String!): User + me: User userBoards(owner: String!): [Board!]! userSuggestions(creatorId: String!): [Suggestion!]! } type Mutation { - createUser(username: String!): User updateUser(username: String!, newUsername: String!): [Int!]! deleteUser(username: String!): Int! createBoard(owner: Int!, name: String): Board! createSuggestion(creatorId: Int!, text: String, boardId: Int!): Suggestion! + register(username: String!, email: String!, password: String!): User! + login(email: String!, password: String!): String! } `; diff --git a/yarn.lock b/yarn.lock index b362328..32f0845 100644 --- a/yarn.lock +++ b/yarn.lock @@ -676,16 +676,32 @@ balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" +base64url@2.0.0, base64url@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" + bcrypt-pbkdf@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" dependencies: tweetnacl "^0.14.3" +bcrypt@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-1.0.2.tgz#d05fc5d223173e0e28ec381c0f00cc25ffaf2736" + dependencies: + bindings "1.2.1" + nan "2.5.0" + node-pre-gyp "0.6.32" + binary-extensions@^1.0.0: version "1.8.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.8.0.tgz#48ec8d16df4377eae5fa5884682480af4d95c774" +bindings@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" + block-stream@*: version "0.0.9" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" @@ -732,6 +748,14 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + +buffer-shims@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" + builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -915,7 +939,7 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -debug@2.2.0: +debug@2.2.0, debug@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" dependencies: @@ -1016,6 +1040,13 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +ecdsa-sig-formatter@1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" + dependencies: + base64url "^2.0.0" + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -1365,7 +1396,7 @@ fsevents@^1.0.0: nan "^2.3.0" node-pre-gyp "^0.6.29" -fstream-ignore@^1.0.5: +fstream-ignore@^1.0.5, fstream-ignore@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" dependencies: @@ -1373,7 +1404,7 @@ fstream-ignore@^1.0.5: inherits "2" minimatch "^3.0.0" -fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: +fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2, fstream@~1.0.10: version "1.0.11" resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" dependencies: @@ -1804,6 +1835,10 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" +isemail@1.x.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/isemail/-/isemail-1.2.0.tgz#be03df8cc3e29de4d2c5df6501263f1fa4595e9a" + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -1822,6 +1857,15 @@ iterall@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.1.1.tgz#f7f0af11e9a04ec6426260f5019d9fcca4d50214" +joi@^6.10.1: + version "6.10.1" + resolved "https://registry.yarnpkg.com/joi/-/joi-6.10.1.tgz#4d50c318079122000fe5f16af1ff8e1917b77e06" + dependencies: + hoek "2.x.x" + isemail "1.x.x" + moment "2.x.x" + topo "1.x.x" + js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" @@ -1875,6 +1919,16 @@ jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" +jsonwebtoken@^7.4.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-7.4.1.tgz#7ca324f5215f8be039cd35a6c45bb8cb74a448fb" + dependencies: + joi "^6.10.1" + jws "^3.1.4" + lodash.once "^4.0.0" + ms "^2.0.0" + xtend "^4.0.1" + jsprim@^1.2.2: version "1.4.0" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918" @@ -1884,6 +1938,23 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.3.6" +jwa@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" + dependencies: + base64url "2.0.0" + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.9" + safe-buffer "^5.0.1" + +jws@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" + dependencies: + base64url "^2.0.0" + jwa "^1.1.4" + safe-buffer "^5.0.1" + kind-of@^3.0.2: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -1991,6 +2062,10 @@ lodash.keys@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" @@ -2082,7 +2157,7 @@ minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" -"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: +"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: @@ -2094,7 +2169,7 @@ moment-timezone@^0.5.4: dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0", moment@^2.13.0: +moment@2.x.x, "moment@>= 2.9.0", moment@^2.13.0: version "2.18.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" @@ -2102,7 +2177,7 @@ ms@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" -ms@2.0.0: +ms@2.0.0, ms@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -2110,6 +2185,10 @@ mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" +nan@2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.0.tgz#aa8f1e34531d807e9e27755b234b4a6ec0c152a8" + nan@^2.3.0: version "2.6.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" @@ -2128,6 +2207,20 @@ nested-error-stacks@^1.0.0: dependencies: inherits "~2.0.1" +node-pre-gyp@0.6.32: + version "0.6.32" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.32.tgz#fc452b376e7319b3d255f5f34853ef6fd8fe1fd5" + dependencies: + mkdirp "~0.5.1" + nopt "~3.0.6" + npmlog "^4.0.1" + rc "~1.1.6" + request "^2.79.0" + rimraf "~2.5.4" + semver "~5.3.0" + tar "~2.2.1" + tar-pack "~3.3.0" + node-pre-gyp@^0.6.29: version "0.6.36" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" @@ -2170,6 +2263,12 @@ nopt@~1.0.10: dependencies: abbrev "1" +nopt@~3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + dependencies: + abbrev "1" + normalize-package-data@^2.3.2: version "2.3.8" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.8.tgz#d819eda2a9dedbd1ffa563ea4071d936782295bb" @@ -2185,7 +2284,7 @@ normalize-path@^2.0.1: dependencies: remove-trailing-separator "^1.0.1" -npmlog@^4.0.2: +npmlog@^4.0.1, npmlog@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.0.tgz#dc59bee85f64f00ed424efb2af0783df25d1c0b5" dependencies: @@ -2229,7 +2328,7 @@ once@^1.3.0, once@^1.3.3: dependencies: wrappy "1" -once@~1.3.0: +once@~1.3.0, once@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" dependencies: @@ -2454,6 +2553,15 @@ rc@^1.0.1, rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +rc@~1.1.6: + version "1.1.7" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.7.tgz#c5ea564bb07aff9fd3a5b32e906c1d3a65940fea" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + read-all-stream@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa" @@ -2488,6 +2596,18 @@ readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.6, readable string_decoder "~1.0.0" util-deprecate "~1.0.1" +readable-stream@~2.1.4: + version "2.1.5" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0" + dependencies: + buffer-shims "^1.0.0" + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + readdirp@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" @@ -2568,7 +2688,7 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request@^2.81.0: +request@^2.79.0, request@^2.81.0: version "2.81.0" resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" dependencies: @@ -2633,6 +2753,12 @@ rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1: dependencies: glob "^7.0.5" +rimraf@~2.5.1, rimraf@~2.5.4: + version "2.5.4" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" + dependencies: + glob "^7.0.5" + run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" @@ -2659,7 +2785,7 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" -"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.3.0: +"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.3.0, semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" @@ -2844,6 +2970,10 @@ string-width@^2.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^3.0.0" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + string_decoder@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.2.tgz#b29e1f4e1125fa97a10382b8a533737b7491e179" @@ -2896,7 +3026,20 @@ tar-pack@^3.4.0: tar "^2.2.1" uid-number "^0.0.6" -tar@^2.2.1: +tar-pack@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.3.0.tgz#30931816418f55afc4d21775afdd6720cee45dae" + dependencies: + debug "~2.2.0" + fstream "~1.0.10" + fstream-ignore "~1.0.5" + once "~1.3.3" + readable-stream "~2.1.4" + rimraf "~2.5.1" + tar "~2.2.1" + uid-number "~0.0.6" + +tar@^2.2.1, tar@~2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" dependencies: @@ -2938,6 +3081,12 @@ to-fast-properties@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" +topo@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/topo/-/topo-1.1.0.tgz#e9d751615d1bb87dc865db182fa1ca0a5ef536d5" + dependencies: + hoek "2.x.x" + toposort-class@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988" @@ -2989,7 +3138,7 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -uid-number@^0.0.6: +uid-number@^0.0.6, uid-number@~0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" @@ -3106,7 +3255,7 @@ xdg-basedir@^2.0.0: dependencies: os-homedir "^1.0.0" -xtend@^4.0.0: +xtend@^4.0.0, xtend@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" From fd02ce49bce84ff1b1e44d600ef8560973de7875 Mon Sep 17 00:00:00 2001 From: ben awad Date: Sun, 18 Jun 2017 16:52:50 -0500 Subject: [PATCH 04/31] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ccfbf4..ec590ae 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # Template for an Express Server with GraphQL -[Watch the video to learn how it was made.](https://youtu.be/TU-jZTpCig4) +[Watch the video to learn how it was made.](https://youtu.be/eu2VJ9dtwiY) From 5735fea03e50d8534e9a222e344422058e3dbdeb Mon Sep 17 00:00:00 2001 From: ben awad Date: Tue, 27 Jun 2017 20:15:34 -0500 Subject: [PATCH 05/31] set up subscriptions on server --- index.js | 21 ++++++- package.json | 6 +- resolvers.js | 19 +++++++ schema.js | 11 ++++ yarn.lock | 157 ++++++++++++++++++++++++++++----------------------- 5 files changed, 140 insertions(+), 74 deletions(-) diff --git a/index.js b/index.js index d008ea4..b7263f3 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,9 @@ import { graphiqlExpress, graphqlExpress } from 'graphql-server-express'; import { makeExecutableSchema } from 'graphql-tools'; import cors from 'cors'; import jwt from 'jsonwebtoken'; +import { createServer } from 'http'; +import { execute, subscribe } from 'graphql'; +import { SubscriptionServer } from 'subscriptions-transport-ws'; import typeDefs from './schema'; import resolvers from './resolvers'; @@ -52,4 +55,20 @@ app.use( })), ); -models.sequelize.sync().then(() => app.listen(3000)); +const server = createServer(app); + +models.sequelize.sync().then(() => + server.listen(3000, () => { + new SubscriptionServer( + { + execute, + subscribe, + schema, + }, + { + server, + path: '/subscriptions', + }, + ); + }), +); diff --git a/package.json b/package.json index e7414b5..71839c2 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,14 @@ "body-parser": "^1.17.2", "cors": "^2.8.3", "express": "^4.15.3", + "graphql": "^0.10.3", "graphql-server-express": "^0.8.0", + "graphql-subscriptions": "^0.4.3", "graphql-tools": "^1.0.0", "jsonwebtoken": "^7.4.1", "lodash": "^4.17.4", - "sequelize": "^4.1.0" + "sequelize": "^4.1.0", + "subscriptions-transport-ws": "^0.7.3" }, "devDependencies": { "babel-cli": "^6.24.1", @@ -26,7 +29,6 @@ "eslint": "^4.0.0", "eslint-config-airbnb-base": "^11.2.0", "eslint-plugin-import": "^2.3.0", - "graphql": "^0.10.1", "nodemon": "^1.11.0" } } diff --git a/resolvers.js b/resolvers.js index 9bd5f4a..5f92e4f 100644 --- a/resolvers.js +++ b/resolvers.js @@ -1,8 +1,18 @@ import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import _ from 'lodash'; +import { PubSub } from 'graphql-subscriptions'; + +export const pubsub = new PubSub(); + +const USER_ADDED = 'USER_ADDED'; export default { + Subscription: { + userAdded: { + subscribe: () => pubsub.asyncIterator(USER_ADDED), + }, + }, User: { boards: ({ id }, args, { models }) => models.Board.findAll({ @@ -69,6 +79,15 @@ export default { createBoard: (parent, args, { models }) => models.Board.create(args), createSuggestion: (parent, args, { models }) => models.Suggestion.create(args), + createUser: async (parent, args, { models }) => { + const user = args; + user.password = 'idk'; + const userAdded = await models.User.create(user); + pubsub.publish(USER_ADDED, { + userAdded, + }); + return userAdded; + }, register: async (parent, args, { models }) => { const user = args; user.password = await bcrypt.hash(user.password, 12); diff --git a/schema.js b/schema.js index a759b18..4991ba0 100644 --- a/schema.js +++ b/schema.js @@ -1,5 +1,9 @@ export default ` + type Subscription { + userAdded: User! + } + type Suggestion { id: Int! text: String! @@ -37,5 +41,12 @@ export default ` createSuggestion(creatorId: Int!, text: String, boardId: Int!): Suggestion! register(username: String!, email: String!, password: String!): User! login(email: String!, password: String!): String! + createUser(username: String!): User! + } + + schema { + query: Query + mutation: Mutation + subscription: Subscription } `; diff --git a/yarn.lock b/yarn.lock index 32f0845..6146cf4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -38,6 +38,12 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" +"@types/ws@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-3.0.0.tgz#4682a04d385484e73f7d8275cabc8b672d66143c" + dependencies: + "@types/node" "*" + abbrev@1: version "1.1.0" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" @@ -672,6 +678,10 @@ babylon@^6.17.2: version "6.17.3" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.3.tgz#1327d709950b558f204e5352587fd0290f8d8e48" +backo2@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -1077,6 +1087,10 @@ es6-promise@^3.0.2: version "3.3.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" +es6-promise@^4.0.5: + version "4.1.0" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.0.tgz#dda03ca8f9f89bc597e689842929de7ba8cebdf0" + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -1215,6 +1229,10 @@ event-stream@~3.3.0: stream-combiner "~0.0.4" through "~2.3.1" +eventemitter3@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" + expand-brackets@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" @@ -1396,7 +1414,7 @@ fsevents@^1.0.0: nan "^2.3.0" node-pre-gyp "^0.6.29" -fstream-ignore@^1.0.5, fstream-ignore@~1.0.5: +fstream-ignore@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" dependencies: @@ -1404,7 +1422,7 @@ fstream-ignore@^1.0.5, fstream-ignore@~1.0.5: inherits "2" minimatch "^3.0.0" -fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2, fstream@~1.0.10: +fstream@^1.0.0, fstream@^1.0.2, fstream@~1.0.10: version "1.0.11" resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" dependencies: @@ -1532,6 +1550,18 @@ graphql-server-module-graphiql@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/graphql-server-module-graphiql/-/graphql-server-module-graphiql-0.8.0.tgz#469c2dd2a8aeb9bb50971b283a11cc0876d3d281" +graphql-subscriptions@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.4.3.tgz#2aed6ba87551cc747742b793497ff24b22991867" + dependencies: + "@types/graphql" "^0.9.1" + es6-promise "^4.0.5" + iterall "^1.1.1" + +graphql-tag@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.4.2.tgz#6a63297d8522d03a2b72d26f1b239aab343840cd" + graphql-tools@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-1.0.0.tgz#76b25e1dce0521b31d5566aac281b2f134bc49c8" @@ -1542,9 +1572,9 @@ graphql-tools@^1.0.0: optionalDependencies: "@types/graphql" "^0.9.0" -graphql@^0.10.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.10.1.tgz#75c93c2ce73aeb5bae2eefb555a8e9e39c36027d" +graphql@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.10.3.tgz#c313afd5518e673351bee18fb63e2a0e487407ab" dependencies: iterall "^1.1.0" @@ -1853,7 +1883,7 @@ isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" -iterall@^1.1.0: +iterall@^1.1.0, iterall@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.1.1.tgz#f7f0af11e9a04ec6426260f5019d9fcca4d50214" @@ -2035,6 +2065,10 @@ lodash.assign@^3.0.0: lodash._createassigner "^3.0.0" lodash.keys "^3.0.0" +lodash.assign@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" + lodash.cond@^4.3.0: version "4.5.2" resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5" @@ -2054,6 +2088,14 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" +lodash.isobject@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + lodash.keys@^3.0.0: version "3.1.2" resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" @@ -2185,14 +2227,10 @@ mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -nan@2.5.0: +nan@2.5.0, nan@^2.3.0: version "2.5.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.0.tgz#aa8f1e34531d807e9e27755b234b4a6ec0c152a8" -nan@^2.3.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -2207,7 +2245,7 @@ nested-error-stacks@^1.0.0: dependencies: inherits "~2.0.1" -node-pre-gyp@0.6.32: +node-pre-gyp@0.6.32, node-pre-gyp@^0.6.29: version "0.6.32" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.32.tgz#fc452b376e7319b3d255f5f34853ef6fd8fe1fd5" dependencies: @@ -2221,20 +2259,6 @@ node-pre-gyp@0.6.32: tar "~2.2.1" tar-pack "~3.3.0" -node-pre-gyp@^0.6.29: - version "0.6.36" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" - dependencies: - mkdirp "^0.5.1" - nopt "^4.0.1" - npmlog "^4.0.2" - rc "^1.1.7" - request "^2.81.0" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^2.2.1" - tar-pack "^3.4.0" - nodemon@^1.11.0: version "1.11.0" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.11.0.tgz#226c562bd2a7b13d3d7518b49ad4828a3623d06c" @@ -2250,13 +2274,6 @@ nodemon@^1.11.0: undefsafe "0.0.3" update-notifier "0.5.0" -nopt@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" - dependencies: - abbrev "1" - osenv "^0.1.4" - nopt@~1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" @@ -2284,7 +2301,7 @@ normalize-path@^2.0.1: dependencies: remove-trailing-separator "^1.0.1" -npmlog@^4.0.1, npmlog@^4.0.2: +npmlog@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.0.tgz#dc59bee85f64f00ed424efb2af0783df25d1c0b5" dependencies: @@ -2322,7 +2339,7 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" -once@^1.3.0, once@^1.3.3: +once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" dependencies: @@ -2359,7 +2376,7 @@ os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" -osenv@^0.1.0, osenv@^0.1.4: +osenv@^0.1.0: version "0.1.4" resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" dependencies: @@ -2544,16 +2561,7 @@ raw-body@~2.2.0: iconv-lite "0.4.15" unpipe "1.0.0" -rc@^1.0.1, rc@^1.1.7: - version "1.2.1" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" - dependencies: - deep-extend "~0.4.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -rc@~1.1.6: +rc@^1.0.1, rc@~1.1.6: version "1.1.7" resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.7.tgz#c5ea564bb07aff9fd3a5b32e906c1d3a65940fea" dependencies: @@ -2584,7 +2592,7 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" -readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2: +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.2: version "2.2.11" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.11.tgz#0796b31f8d7688007ff0b93a8088d34aa17c0f72" dependencies: @@ -2688,7 +2696,7 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request@^2.79.0, request@^2.81.0: +request@^2.79.0: version "2.81.0" resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" dependencies: @@ -2747,13 +2755,7 @@ retry-as-promised@^2.0.0: cross-env "^3.1.2" debug "^2.2.0" -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" - dependencies: - glob "^7.0.5" - -rimraf@~2.5.1, rimraf@~2.5.4: +rimraf@2, rimraf@^2.2.8, rimraf@~2.5.1, rimraf@~2.5.4: version "2.5.4" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" dependencies: @@ -2785,7 +2787,7 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" -"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.3.0, semver@~5.3.0: +"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" @@ -2998,6 +3000,21 @@ strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" +subscriptions-transport-ws@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.7.3.tgz#15858f03e013e1fc28f8c2d631014ec1548d38f0" + dependencies: + "@types/ws" "^3.0.0" + backo2 "^1.0.2" + eventemitter3 "^2.0.3" + graphql-subscriptions "^0.4.3" + graphql-tag "^2.0.0" + iterall "^1.1.1" + lodash.assign "^4.2.0" + lodash.isobject "^3.0.2" + lodash.isstring "^4.0.1" + ws "^3.0.0" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -3013,19 +3030,6 @@ table@^4.0.1: slice-ansi "0.0.4" string-width "^2.0.0" -tar-pack@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984" - dependencies: - debug "^2.2.0" - fstream "^1.0.10" - fstream-ignore "^1.0.5" - once "^1.3.3" - readable-stream "^2.1.4" - rimraf "^2.5.1" - tar "^2.2.1" - uid-number "^0.0.6" - tar-pack@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.3.0.tgz#30931816418f55afc4d21775afdd6720cee45dae" @@ -3039,7 +3043,7 @@ tar-pack@~3.3.0: tar "~2.2.1" uid-number "~0.0.6" -tar@^2.2.1, tar@~2.2.1: +tar@~2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" dependencies: @@ -3138,10 +3142,14 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -uid-number@^0.0.6, uid-number@~0.0.6: +uid-number@~0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" +ultron@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864" + undefsafe@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-0.0.3.tgz#ecca3a03e56b9af17385baac812ac83b994a962f" @@ -3249,6 +3257,13 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" +ws@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-3.0.0.tgz#98ddb00056c8390cb751e7788788497f99103b6c" + dependencies: + safe-buffer "~5.0.1" + ultron "~1.1.0" + xdg-basedir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2" From b2854696d468daa035f308245c2ec8f82665b967 Mon Sep 17 00:00:00 2001 From: ben awad Date: Fri, 30 Jun 2017 21:49:57 -0500 Subject: [PATCH 06/31] add permissions --- models/user.js | 4 ++++ permissions.js | 25 +++++++++++++++++++++++++ resolvers.js | 7 +++++-- schema.js | 2 +- 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 permissions.js diff --git a/models/user.js b/models/user.js index 61524d5..ee05ce2 100644 --- a/models/user.js +++ b/models/user.js @@ -8,6 +8,10 @@ export default (sequelize, DataTypes) => { type: DataTypes.STRING, unique: true, }, + isAdmin: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, password: DataTypes.STRING, }); diff --git a/permissions.js b/permissions.js new file mode 100644 index 0000000..9398dba --- /dev/null +++ b/permissions.js @@ -0,0 +1,25 @@ +const createResolver = (resolver) => { + const baseResolver = resolver; + baseResolver.createResolver = (childResolver) => { + const newResolver = async (parent, args, context) => { + await resolver(parent, args, context); + return childResolver(parent, args, context); + }; + return createResolver(newResolver); + }; + return baseResolver; +}; + +export const requiresAuth = createResolver((parent, args, context) => { + if (!context.user || !context.user.id) { + throw new Error('Not authenticated'); + } +}); + +export const requiresAdmin = requiresAuth.createResolver( + (parent, args, context) => { + if (!context.user.isAdmin) { + throw new Error('Requires admin access'); + } + }, +); diff --git a/resolvers.js b/resolvers.js index 5f92e4f..af7aca0 100644 --- a/resolvers.js +++ b/resolvers.js @@ -2,6 +2,7 @@ import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import _ from 'lodash'; import { PubSub } from 'graphql-subscriptions'; +import { requiresAuth, requiresAdmin } from './permissions'; export const pubsub = new PubSub(); @@ -76,7 +77,9 @@ export default { models.User.update({ username: newUsername }, { where: { username } }), deleteUser: (parent, args, { models }) => models.User.destroy({ where: args }), - createBoard: (parent, args, { models }) => models.Board.create(args), + createBoard: requiresAdmin.createResolver((parent, args, { models }) => + models.Board.create(args), + ), createSuggestion: (parent, args, { models }) => models.Suggestion.create(args), createUser: async (parent, args, { models }) => { @@ -109,7 +112,7 @@ export default { // decode: no secret | use me on the client side const token = jwt.sign( { - user: _.pick(user, ['id', 'username']), + user: _.pick(user, ['id', 'username', 'isAdmin']), }, SECRET, { diff --git a/schema.js b/schema.js index 4991ba0..dc6d9c1 100644 --- a/schema.js +++ b/schema.js @@ -39,7 +39,7 @@ export default ` deleteUser(username: String!): Int! createBoard(owner: Int!, name: String): Board! createSuggestion(creatorId: Int!, text: String, boardId: Int!): Suggestion! - register(username: String!, email: String!, password: String!): User! + register(username: String!, email: String!, password: String!, isAdmin: Boolean): User! login(email: String!, password: String!): String! createUser(username: String!): User! } From 1651a8c30163e02e71b7fd1cccb33ffabeba6f02 Mon Sep 17 00:00:00 2001 From: ben awad Date: Sat, 1 Jul 2017 13:33:15 -0500 Subject: [PATCH 07/31] add refresh tokens implementation --- auth.js | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 31 +++++++++++++++++------- resolvers.js | 34 +++++--------------------- schema.js | 9 ++++++- 4 files changed, 104 insertions(+), 37 deletions(-) create mode 100644 auth.js diff --git a/auth.js b/auth.js new file mode 100644 index 0000000..f09d104 --- /dev/null +++ b/auth.js @@ -0,0 +1,67 @@ +import jwt from 'jsonwebtoken'; +import _ from 'lodash'; +import bcrypt from 'bcrypt'; + +const createTokens = async (user, secret) => { + const createToken = jwt.sign( + { + user: _.pick(user, ['id', 'isAdmin']), + }, + secret, + { + expiresIn: '20m', + }, + ); + + const createRefreshToken = jwt.sign( + { + user: _.pick(user, 'id'), + }, + secret, + { + expiresIn: '7d', + }, + ); + + return Promise.all([createToken, createRefreshToken]); +}; + +export const refreshTokens = async (token, refreshToken, models, SECRET) => { + let userId = -1; + try { + const { user: { id } } = jwt.verify(refreshToken, SECRET); + userId = id; + } catch (err) { + return {}; + } + + const user = await models.User.findOne({ where: { id: userId }, raw: true }); + + const [newToken, newRefreshToken] = await createTokens(user, SECRET); + return { + token: newToken, + refreshToken: newRefreshToken, + user, + }; +}; + +export const tryLogin = async (email, password, models, SECRET) => { + const user = await models.User.findOne({ where: { email }, raw: true }); + if (!user) { + // user with provided email not found + throw new Error('Invalid login'); + } + + const valid = await bcrypt.compare(password, user.password); + if (!valid) { + // bad password + throw new Error('Invalid login'); + } + + const [token, refreshToken] = await createTokens(user, SECRET); + + return { + token, + refreshToken, + }; +}; diff --git a/index.js b/index.js index b7263f3..1d65b5f 100644 --- a/index.js +++ b/index.js @@ -11,6 +11,7 @@ import { SubscriptionServer } from 'subscriptions-transport-ws'; import typeDefs from './schema'; import resolvers from './resolvers'; import models from './models'; +import { refreshTokens } from './auth'; const schema = makeExecutableSchema({ typeDefs, @@ -21,15 +22,29 @@ const SECRET = 'aslkdjlkaj10830912039jlkoaiuwerasdjflkasd'; const app = express(); -const addUser = async (req) => { - const token = req.headers.authorization; - try { - const { user } = await jwt.verify(token, SECRET); - req.user = user; - } catch (err) { - console.log(err); +const addUser = async (req, res, next) => { + const token = req.headers['x-token']; + if (token) { + try { + const { user } = jwt.verify(token, SECRET); + req.user = user; + } catch (err) { + const refreshToken = req.headers['x-refresh-token']; + const newTokens = await refreshTokens( + token, + refreshToken, + models, + SECRET, + ); + if (newTokens.token && newTokens.refreshToken) { + res.set('Access-Control-Expose-Headers', 'x-token, x-refresh-token'); + res.set('x-token', newTokens.token); + res.set('x-refresh-token', newTokens.refreshToken); + } + req.user = newTokens.user; + } } - req.next(); + next(); }; app.use(cors('*')); diff --git a/resolvers.js b/resolvers.js index af7aca0..64b8d52 100644 --- a/resolvers.js +++ b/resolvers.js @@ -1,8 +1,8 @@ import bcrypt from 'bcrypt'; -import jwt from 'jsonwebtoken'; -import _ from 'lodash'; import { PubSub } from 'graphql-subscriptions'; + import { requiresAuth, requiresAdmin } from './permissions'; +import { refreshTokens, tryLogin } from './auth'; export const pubsub = new PubSub(); @@ -96,31 +96,9 @@ export default { user.password = await bcrypt.hash(user.password, 12); return models.User.create(user); }, - login: async (parent, { email, password }, { models, SECRET }) => { - const user = await models.User.findOne({ where: { email } }); - if (!user) { - throw new Error('Not user with that email'); - } - - const valid = await bcrypt.compare(password, user.password); - if (!valid) { - throw new Error('Incorrect password'); - } - - // token = '12083098123414aslkjdasldf.asdhfaskjdh12982u793.asdlfjlaskdj10283491' - // verify: needs secret | use me for authentication - // decode: no secret | use me on the client side - const token = jwt.sign( - { - user: _.pick(user, ['id', 'username', 'isAdmin']), - }, - SECRET, - { - expiresIn: '1y', - }, - ); - - return token; - }, + login: async (parent, { email, password }, { models, SECRET }) => + tryLogin(email, password, models, SECRET), + refreshTokens: (parent, { token, refreshToken }, { models, SECRET }) => + refreshTokens(token, refreshToken, models, SECRET), }, }; diff --git a/schema.js b/schema.js index dc6d9c1..fc5f7c4 100644 --- a/schema.js +++ b/schema.js @@ -25,6 +25,12 @@ export default ` updatedAt: String! boards: [Board!]! suggestions: [Suggestion!]! + isAdmin: Boolean! + } + + type AuthPayload { + token: String! + refreshToken: String! } type Query { @@ -40,8 +46,9 @@ export default ` createBoard(owner: Int!, name: String): Board! createSuggestion(creatorId: Int!, text: String, boardId: Int!): Suggestion! register(username: String!, email: String!, password: String!, isAdmin: Boolean): User! - login(email: String!, password: String!): String! + login(email: String!, password: String!): AuthPayload! createUser(username: String!): User! + refreshTokens(token: String!, refreshToken: String!): AuthPayload! } schema { From 770f94607adc59811a67152281fbfe3cc67b79ac Mon Sep 17 00:00:00 2001 From: ben awad Date: Sat, 1 Jul 2017 18:15:09 -0500 Subject: [PATCH 08/31] Add dataloader --- index.js | 19 +++++++++++++++++++ package.json | 1 + resolvers.js | 8 ++------ schema.js | 2 +- yarn.lock | 4 ++++ 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index 1d65b5f..18d45c6 100644 --- a/index.js +++ b/index.js @@ -7,11 +7,13 @@ import jwt from 'jsonwebtoken'; import { createServer } from 'http'; import { execute, subscribe } from 'graphql'; import { SubscriptionServer } from 'subscriptions-transport-ws'; +import _ from 'lodash'; import typeDefs from './schema'; import resolvers from './resolvers'; import models from './models'; import { refreshTokens } from './auth'; +import DataLoader from 'dataloader'; const schema = makeExecutableSchema({ typeDefs, @@ -57,6 +59,22 @@ app.use( }), ); +const batchSuggestions = async (keys, { Suggestion }) => { + // keys = [1, 2, 3 ..., 13] + const suggestions = await Suggestion.findAll({ + raw: true, + where: { + boardId: { + $in: keys, + }, + }, + }); + // suggestion = [{text:'hi', boardId: 1}, {text: 'bye', boardId: 2}, {text: 'bye2'. boardId: 2}] + const gs = _.groupBy(suggestions, 'boardId'); + // gs = {1: [{text:'hi', boardId: 1}], 2: [{text: 'bye', boardId: 2}, {text: 'bye2'. boardId: 2}]} + return keys.map(k => gs[k] || []); +}; + app.use( '/graphql', bodyParser.json(), @@ -66,6 +84,7 @@ app.use( models, SECRET, user: req.user, + suggestionLoader: new DataLoader(keys => batchSuggestions(keys, models)), }, })), ); diff --git a/package.json b/package.json index 71839c2..5262160 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "bcrypt": "^1.0.2", "body-parser": "^1.17.2", "cors": "^2.8.3", + "dataloader": "^1.3.0", "express": "^4.15.3", "graphql": "^0.10.3", "graphql-server-express": "^0.8.0", diff --git a/resolvers.js b/resolvers.js index 64b8d52..c76dac1 100644 --- a/resolvers.js +++ b/resolvers.js @@ -29,12 +29,8 @@ export default { }), }, Board: { - suggestions: ({ id }, args, { models }) => - models.Suggestion.findAll({ - where: { - boardId: id, - }, - }), + suggestions: ({ id }, args, { suggestionLoader }) => + suggestionLoader.load(id), }, Suggestion: { creator: ({ creatorId }, args, { models }) => diff --git a/schema.js b/schema.js index fc5f7c4..7004c1d 100644 --- a/schema.js +++ b/schema.js @@ -36,7 +36,7 @@ export default ` type Query { allUsers: [User!]! me: User - userBoards(owner: String!): [Board!]! + userBoards(owner: Int!): [Board!]! userSuggestions(creatorId: String!): [Suggestion!]! } diff --git a/yarn.lock b/yarn.lock index 6146cf4..96dee92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -949,6 +949,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +dataloader@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.3.0.tgz#6fec5be4b30a712e4afd30b86b4334566b97673b" + debug@2.2.0, debug@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" From c356b30d2240502e4b4990e8b6ad7d8632f74011 Mon Sep 17 00:00:00 2001 From: ben awad Date: Tue, 4 Jul 2017 13:35:40 -0500 Subject: [PATCH 09/31] facebook oauth --- index.js | 30 +++++++++++++++++++++++++++++- package.json | 2 ++ yarn.lock | 40 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 18d45c6..33ebd5d 100644 --- a/index.js +++ b/index.js @@ -8,12 +8,14 @@ import { createServer } from 'http'; import { execute, subscribe } from 'graphql'; import { SubscriptionServer } from 'subscriptions-transport-ws'; import _ from 'lodash'; +import DataLoader from 'dataloader'; +import passport from 'passport'; +import FacebookStrategy from 'passport-facebook'; import typeDefs from './schema'; import resolvers from './resolvers'; import models from './models'; import { refreshTokens } from './auth'; -import DataLoader from 'dataloader'; const schema = makeExecutableSchema({ typeDefs, @@ -24,6 +26,32 @@ const SECRET = 'aslkdjlkaj10830912039jlkoaiuwerasdjflkasd'; const app = express(); +passport.use( + new FacebookStrategy( + { + clientID: '324894734589925', + clientSecret: '95b6ed9340696269e95567452d78aee0', + callbackURL: 'https://8fc528a5.ngrok.io/auth/facebook/callback', + }, + (accessToken, refreshToken, profile, cb) => { + console.log(profile); + cb(null, profile); + }, + ), +); + +app.use(passport.initialize()); + +app.get('/flogin', passport.authenticate('facebook')); + +app.get( + '/auth/facebook/callback', + passport.authenticate('facebook', { session: false }), + (req, res) => { + res.send('AUTH WAS GOOD!'); + }, +); + const addUser = async (req, res, next) => { const token = req.headers['x-token']; if (token) { diff --git a/package.json b/package.json index 5262160..0a8a658 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "graphql-tools": "^1.0.0", "jsonwebtoken": "^7.4.1", "lodash": "^4.17.4", + "passport": "^0.3.2", + "passport-facebook": "^2.1.1", "sequelize": "^4.1.0", "subscriptions-transport-ws": "^0.7.3" }, diff --git a/yarn.lock b/yarn.lock index 96dee92..8d4ef73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2322,6 +2322,10 @@ oauth-sign@~0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" +oauth@0.9.x: + version "0.9.15" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" + object-assign@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" @@ -2431,6 +2435,32 @@ parseurl@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" +passport-facebook@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/passport-facebook/-/passport-facebook-2.1.1.tgz#c39d0b52ae4d59163245a4e21a7b9b6321303311" + dependencies: + passport-oauth2 "1.x.x" + +passport-oauth2@1.x.x: + version "1.4.0" + resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.4.0.tgz#f62f81583cbe12609be7ce6f160b9395a27b86ad" + dependencies: + oauth "0.9.x" + passport-strategy "1.x.x" + uid2 "0.0.x" + utils-merge "1.x.x" + +passport-strategy@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + +passport@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.3.2.tgz#9dd009f915e8fe095b0124a01b8f82da07510102" + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" @@ -2469,6 +2499,10 @@ pause-stream@0.0.11: dependencies: through "~2.3" +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" @@ -3150,6 +3184,10 @@ uid-number@~0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" +uid2@0.0.x: + version "0.0.3" + resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82" + ultron@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864" @@ -3182,7 +3220,7 @@ util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" -utils-merge@1.0.0: +utils-merge@1.0.0, utils-merge@1.x.x: version "1.0.0" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" From dda516298d731bbb6fea22f3d4e638e4a8aad863 Mon Sep 17 00:00:00 2001 From: ben awad Date: Tue, 4 Jul 2017 14:13:14 -0500 Subject: [PATCH 10/31] store user in db --- index.js | 29 +++++++++++++++++++++++++---- models/fbAuth.js | 12 ++++++++++++ models/index.js | 2 ++ models/localAuth.js | 15 +++++++++++++++ models/user.js | 5 ----- 5 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 models/fbAuth.js create mode 100644 models/localAuth.js diff --git a/index.js b/index.js index 33ebd5d..7ddc237 100644 --- a/index.js +++ b/index.js @@ -29,13 +29,34 @@ const app = express(); passport.use( new FacebookStrategy( { - clientID: '324894734589925', - clientSecret: '95b6ed9340696269e95567452d78aee0', + clientID: process.env.FACEBOOK_CLIENT_ID, + clientSecret: process.env.FACEBOOK_SECRET_ID, callbackURL: 'https://8fc528a5.ngrok.io/auth/facebook/callback', }, - (accessToken, refreshToken, profile, cb) => { + async (accessToken, refreshToken, profile, cb) => { + // 2 cases + // #1 first time login + // #2 other times + const { id, displayName } = profile; + // [] + const fbUsers = await models.FbAuth.findAll({ + limit: 1, + where: { fb_id: id }, + }); + + console.log(fbUsers); console.log(profile); - cb(null, profile); + + if (!fbUsers.length) { + const user = await models.User.create(); + await models.FbAuth.create({ + fb_id: id, + display_name: displayName, + user_id: user.id, + }); + } + + cb(null, {}); }, ), ); diff --git a/models/fbAuth.js b/models/fbAuth.js new file mode 100644 index 0000000..d0f79bd --- /dev/null +++ b/models/fbAuth.js @@ -0,0 +1,12 @@ +export default (sequelize, DataTypes) => { + const FbAuth = sequelize.define('fb_auth', { + fb_id: DataTypes.STRING, + display_name: DataTypes.STRING, + }); + + FbAuth.associate = (models) => { + FbAuth.belongsTo(models.User, { foreignKey: 'user_id' }); + }; + + return FbAuth; +}; diff --git a/models/index.js b/models/index.js index 0c2ab6d..7c98a6b 100644 --- a/models/index.js +++ b/models/index.js @@ -14,6 +14,8 @@ const db = { User: sequelize.import('./user'), Board: sequelize.import('./board'), Suggestion: sequelize.import('./suggestion'), + FbAuth: sequelize.import('./FbAuth'), + LocalAuth: sequelize.import('./localAuth'), }; Object.keys(db).forEach((modelName) => { diff --git a/models/localAuth.js b/models/localAuth.js new file mode 100644 index 0000000..53eaf07 --- /dev/null +++ b/models/localAuth.js @@ -0,0 +1,15 @@ +export default (sequelize, DataTypes) => { + const LocalAuth = sequelize.define('local_auth', { + email: { + type: DataTypes.STRING, + unique: true, + }, + password: DataTypes.STRING, + }); + + LocalAuth.associate = (models) => { + LocalAuth.belongsTo(models.User, { foreignKey: 'user_id' }); + }; + + return LocalAuth; +}; diff --git a/models/user.js b/models/user.js index ee05ce2..d32afe7 100644 --- a/models/user.js +++ b/models/user.js @@ -4,15 +4,10 @@ export default (sequelize, DataTypes) => { type: DataTypes.STRING, unique: true, }, - email: { - type: DataTypes.STRING, - unique: true, - }, isAdmin: { type: DataTypes.BOOLEAN, defaultValue: false, }, - password: DataTypes.STRING, }); User.associate = (models) => { From 14c400ec3c29def40f290a0bcedb4249dd15810e Mon Sep 17 00:00:00 2001 From: ben awad Date: Tue, 4 Jul 2017 14:46:15 -0500 Subject: [PATCH 11/31] pass token back to client --- auth.js | 4 ++-- index.js | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/auth.js b/auth.js index f09d104..9dd5b48 100644 --- a/auth.js +++ b/auth.js @@ -2,10 +2,10 @@ import jwt from 'jsonwebtoken'; import _ from 'lodash'; import bcrypt from 'bcrypt'; -const createTokens = async (user, secret) => { +export const createTokens = async (user, secret) => { const createToken = jwt.sign( { - user: _.pick(user, ['id', 'isAdmin']), + user: _.pick(user, 'id'), }, secret, { diff --git a/index.js b/index.js index 7ddc237..1985a98 100644 --- a/index.js +++ b/index.js @@ -15,7 +15,7 @@ import FacebookStrategy from 'passport-facebook'; import typeDefs from './schema'; import resolvers from './resolvers'; import models from './models'; -import { refreshTokens } from './auth'; +import { createTokens, refreshTokens } from './auth'; const schema = makeExecutableSchema({ typeDefs, @@ -49,14 +49,15 @@ passport.use( if (!fbUsers.length) { const user = await models.User.create(); - await models.FbAuth.create({ + const fbUser = await models.FbAuth.create({ fb_id: id, display_name: displayName, user_id: user.id, }); + fbUsers.push(fbUser); } - cb(null, {}); + cb(null, fbUsers[0]); }, ), ); @@ -68,8 +69,11 @@ app.get('/flogin', passport.authenticate('facebook')); app.get( '/auth/facebook/callback', passport.authenticate('facebook', { session: false }), - (req, res) => { - res.send('AUTH WAS GOOD!'); + async (req, res) => { + const [token, refreshToken] = await createTokens(req.user, SECRET); + res.redirect( + `http://localhost:8080/home?token=${token}&refreshToken=${refreshToken}`, + ); }, ); From b6ab71f18b0dd2e2250d35720f424b4dc7560372 Mon Sep 17 00:00:00 2001 From: ben awad Date: Wed, 5 Jul 2017 22:41:48 -0500 Subject: [PATCH 12/31] fix register resolver --- .babelrc | 3 +- package.json | 1 + resolvers.js | 14 ++++++-- yarn.lock | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 4 deletions(-) diff --git a/.babelrc b/.babelrc index 218b446..c462f13 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,6 @@ { "presets": [ - "latest" + "latest", + "stage-2" ] } \ No newline at end of file diff --git a/package.json b/package.json index 0a8a658..87c8d67 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "devDependencies": { "babel-cli": "^6.24.1", "babel-preset-latest": "^6.24.1", + "babel-preset-stage-2": "^6.24.1", "eslint": "^4.0.0", "eslint-config-airbnb-base": "^11.2.0", "eslint-plugin-import": "^2.3.0", diff --git a/resolvers.js b/resolvers.js index c76dac1..279e344 100644 --- a/resolvers.js +++ b/resolvers.js @@ -1,5 +1,6 @@ import bcrypt from 'bcrypt'; import { PubSub } from 'graphql-subscriptions'; +import _ from 'lodash'; import { requiresAuth, requiresAdmin } from './permissions'; import { refreshTokens, tryLogin } from './auth'; @@ -88,9 +89,16 @@ export default { return userAdded; }, register: async (parent, args, { models }) => { - const user = args; - user.password = await bcrypt.hash(user.password, 12); - return models.User.create(user); + const user = _.pick(args, ['username', 'isAdmin']); + const localAuth = _.pick(args, ['email', 'password']); + const passwordPromise = bcrypt.hash(localAuth.password, 12); + const createUserPromise = models.User.create(user); + const [password, createdUser] = await Promise.all([passwordPromise, createUserPromise]); + localAuth.password = password; + return models.LocalAuth.create({ + ...localAuth, + user_id: createdUser.id, + }); }, login: async (parent, { email, password }, { models, SECRET }) => tryLogin(email, password, models, SECRET), diff --git a/yarn.lock b/yarn.lock index 8d4ef73..b1734f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -242,6 +242,14 @@ babel-generator@^6.25.0: source-map "^0.5.0" trim-right "^1.0.1" +babel-helper-bindify-decorators@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz#14c19e5f142d7b47f19a52431e52b1ccbc40a330" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" @@ -276,6 +284,15 @@ babel-helper-explode-assignable-expression@^6.24.1: babel-traverse "^6.24.1" babel-types "^6.24.1" +babel-helper-explode-class@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz#7dc2a3910dee007056e1e31d640ced3d54eaa9eb" + dependencies: + babel-helper-bindify-decorators "^6.24.1" + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + babel-helper-function-name@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" @@ -359,14 +376,42 @@ babel-plugin-syntax-async-functions@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" +babel-plugin-syntax-async-generators@^6.5.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a" + +babel-plugin-syntax-class-properties@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" + +babel-plugin-syntax-decorators@^6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b" + +babel-plugin-syntax-dynamic-import@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da" + babel-plugin-syntax-exponentiation-operator@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" +babel-plugin-syntax-object-rest-spread@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" + babel-plugin-syntax-trailing-function-commas@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" +babel-plugin-transform-async-generator-functions@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz#f058900145fd3e9907a6ddf28da59f215258a5db" + dependencies: + babel-helper-remap-async-to-generator "^6.24.1" + babel-plugin-syntax-async-generators "^6.5.0" + babel-runtime "^6.22.0" + babel-plugin-transform-async-to-generator@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" @@ -375,6 +420,25 @@ babel-plugin-transform-async-to-generator@^6.24.1: babel-plugin-syntax-async-functions "^6.8.0" babel-runtime "^6.22.0" +babel-plugin-transform-class-properties@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac" + dependencies: + babel-helper-function-name "^6.24.1" + babel-plugin-syntax-class-properties "^6.8.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-decorators@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz#788013d8f8c6b5222bdf7b344390dfd77569e24d" + dependencies: + babel-helper-explode-class "^6.24.1" + babel-plugin-syntax-decorators "^6.13.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-types "^6.24.1" + babel-plugin-transform-es2015-arrow-functions@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" @@ -551,6 +615,13 @@ babel-plugin-transform-exponentiation-operator@^6.24.1: babel-plugin-syntax-exponentiation-operator "^6.8.0" babel-runtime "^6.22.0" +babel-plugin-transform-object-rest-spread@^6.22.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz#875d6bc9be761c58a2ae3feee5dc4895d8c7f921" + dependencies: + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-runtime "^6.22.0" + babel-plugin-transform-regenerator@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz#b8da305ad43c3c99b4848e4fe4037b770d23c418" @@ -622,6 +693,25 @@ babel-preset-latest@^6.24.1: babel-preset-es2016 "^6.24.1" babel-preset-es2017 "^6.24.1" +babel-preset-stage-2@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz#d9e2960fb3d71187f0e64eec62bc07767219bdc1" + dependencies: + babel-plugin-syntax-dynamic-import "^6.18.0" + babel-plugin-transform-class-properties "^6.24.1" + babel-plugin-transform-decorators "^6.24.1" + babel-preset-stage-3 "^6.24.1" + +babel-preset-stage-3@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz#836ada0a9e7a7fa37cb138fb9326f87934a48395" + dependencies: + babel-plugin-syntax-trailing-function-commas "^6.22.0" + babel-plugin-transform-async-generator-functions "^6.24.1" + babel-plugin-transform-async-to-generator "^6.24.1" + babel-plugin-transform-exponentiation-operator "^6.24.1" + babel-plugin-transform-object-rest-spread "^6.22.0" + babel-register@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.24.1.tgz#7e10e13a2f71065bdfad5a1787ba45bca6ded75f" From 11d1c8ae421229ce56431c9a422fd10acbe29e58 Mon Sep 17 00:00:00 2001 From: ben awad Date: Thu, 6 Jul 2017 19:41:56 -0500 Subject: [PATCH 13/31] get server ready for login --- auth.js | 10 ++++++---- index.js | 1 + resolvers.js | 2 +- schema.js | 3 +-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/auth.js b/auth.js index 9dd5b48..830b254 100644 --- a/auth.js +++ b/auth.js @@ -5,7 +5,7 @@ import bcrypt from 'bcrypt'; export const createTokens = async (user, secret) => { const createToken = jwt.sign( { - user: _.pick(user, 'id'), + user: _.pick(user, ['id', 'isAdmin']), }, secret, { @@ -46,18 +46,20 @@ export const refreshTokens = async (token, refreshToken, models, SECRET) => { }; export const tryLogin = async (email, password, models, SECRET) => { - const user = await models.User.findOne({ where: { email }, raw: true }); - if (!user) { + const localAuth = await models.LocalAuth.findOne({ where: { email }, raw: true }); + if (!localAuth) { // user with provided email not found throw new Error('Invalid login'); } - const valid = await bcrypt.compare(password, user.password); + const valid = await bcrypt.compare(password, localAuth.password); if (!valid) { // bad password throw new Error('Invalid login'); } + const user = await models.User.findOne({ where: { id: localAuth.user_id }, raw: true }); + const [token, refreshToken] = await createTokens(user, SECRET); return { diff --git a/index.js b/index.js index 1985a98..3987ef1 100644 --- a/index.js +++ b/index.js @@ -79,6 +79,7 @@ app.get( const addUser = async (req, res, next) => { const token = req.headers['x-token']; + console.log(token); if (token) { try { const { user } = jwt.verify(token, SECRET); diff --git a/resolvers.js b/resolvers.js index 279e344..bab6f96 100644 --- a/resolvers.js +++ b/resolvers.js @@ -42,7 +42,7 @@ export default { }), }, Query: { - allUsers: (parent, args, { models }) => models.User.findAll(), + allUsers: requiresAuth.createResolver((parent, args, { models }) => models.User.findAll()), me: (parent, args, { models, user }) => { if (user) { // they are logged in diff --git a/schema.js b/schema.js index 7004c1d..4fdefa4 100644 --- a/schema.js +++ b/schema.js @@ -19,8 +19,7 @@ export default ` type User { id: Int! - username: String! - email: String! + username: String createdAt: String! updatedAt: String! boards: [Board!]! From eb5fd8e6e34b3b4d876d28443600607c672600a6 Mon Sep 17 00:00:00 2001 From: Ben Awad Date: Thu, 6 Jul 2017 21:58:03 -0500 Subject: [PATCH 14/31] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ec590ae..18b8021 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # Template for an Express Server with GraphQL -[Watch the video to learn how it was made.](https://youtu.be/eu2VJ9dtwiY) +[Watch the video to learn how it was made.](https://youtu.be/HmwEGkBKb2s) From fe035929d858db4c0a81f934b944d59901dc974e Mon Sep 17 00:00:00 2001 From: Ben Awad Date: Sun, 16 Jul 2017 12:36:01 -0500 Subject: [PATCH 15/31] Update index.js --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 3987ef1..79b0de9 100644 --- a/index.js +++ b/index.js @@ -29,8 +29,8 @@ const app = express(); passport.use( new FacebookStrategy( { - clientID: process.env.FACEBOOK_CLIENT_ID, - clientSecret: process.env.FACEBOOK_SECRET_ID, + clientID: 'client_id', + clientSecret: 'client_secret', callbackURL: 'https://8fc528a5.ngrok.io/auth/facebook/callback', }, async (accessToken, refreshToken, profile, cb) => { From 445646ca616b157f24ca6844f1f8359be5a16455 Mon Sep 17 00:00:00 2001 From: ben awad Date: Wed, 26 Jul 2017 22:55:28 -0500 Subject: [PATCH 16/31] add limit and offset pagination --- resolvers.js | 15 ++++++--------- schema.js | 2 ++ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/resolvers.js b/resolvers.js index bab6f96..60fb9f0 100644 --- a/resolvers.js +++ b/resolvers.js @@ -30,8 +30,7 @@ export default { }), }, Board: { - suggestions: ({ id }, args, { suggestionLoader }) => - suggestionLoader.load(id), + suggestions: ({ id }, args, { suggestionLoader }) => suggestionLoader.load(id), }, Suggestion: { creator: ({ creatorId }, args, { models }) => @@ -42,6 +41,8 @@ export default { }), }, Query: { + suggestions: (parent, args, { models }) => models.Suggestion.findAll(), + someSuggestions: (parent, args, { models }) => models.Suggestion.findAll(args), allUsers: requiresAuth.createResolver((parent, args, { models }) => models.User.findAll()), me: (parent, args, { models, user }) => { if (user) { @@ -72,13 +73,9 @@ export default { Mutation: { updateUser: (parent, { username, newUsername }, { models }) => models.User.update({ username: newUsername }, { where: { username } }), - deleteUser: (parent, args, { models }) => - models.User.destroy({ where: args }), - createBoard: requiresAdmin.createResolver((parent, args, { models }) => - models.Board.create(args), - ), - createSuggestion: (parent, args, { models }) => - models.Suggestion.create(args), + deleteUser: (parent, args, { models }) => models.User.destroy({ where: args }), + createBoard: (parent, args, { models }) => models.Board.create(args), + createSuggestion: (parent, args, { models }) => models.Suggestion.create(args), createUser: async (parent, args, { models }) => { const user = args; user.password = 'idk'; diff --git a/schema.js b/schema.js index 4fdefa4..4e61674 100644 --- a/schema.js +++ b/schema.js @@ -37,6 +37,8 @@ export default ` me: User userBoards(owner: Int!): [Board!]! userSuggestions(creatorId: String!): [Suggestion!]! + suggestions: [Suggestion!]! + someSuggestions(limit: Int!, offset: Int!): [Suggestion!]! } type Mutation { From 0ac3128af433dff94cb5e8d482e6e40d9f0657f2 Mon Sep 17 00:00:00 2001 From: ben awad Date: Fri, 28 Jul 2017 18:48:17 -0500 Subject: [PATCH 17/31] cursor pagination for suggestions --- resolvers.js | 23 +++++++++++++++++++++++ schema.js | 2 ++ 2 files changed, 25 insertions(+) diff --git a/resolvers.js b/resolvers.js index 60fb9f0..6c4334b 100644 --- a/resolvers.js +++ b/resolvers.js @@ -43,6 +43,29 @@ export default { Query: { suggestions: (parent, args, { models }) => models.Suggestion.findAll(), someSuggestions: (parent, args, { models }) => models.Suggestion.findAll(args), + someSuggestions2: (parent, { limit, cursor }, { models }) => + models.Suggestion.findAll({ + limit, + where: { + id: { + $gt: cursor || -1, + }, + }, + order: ['id'], + }), + searchSuggestions: (parent, { query, limit, cursor }, { models }) => + models.Suggestion.findAll({ + limit, + where: { + text: { + $iLike: `%${query}%`, + }, + id: { + $gt: cursor || -1, + }, + }, + order: ['id'], + }), allUsers: requiresAuth.createResolver((parent, args, { models }) => models.User.findAll()), me: (parent, args, { models, user }) => { if (user) { diff --git a/schema.js b/schema.js index 4e61674..bcd987a 100644 --- a/schema.js +++ b/schema.js @@ -39,6 +39,8 @@ export default ` userSuggestions(creatorId: String!): [Suggestion!]! suggestions: [Suggestion!]! someSuggestions(limit: Int!, offset: Int!): [Suggestion!]! + someSuggestions2(limit: Int!, cursor: Int): [Suggestion!]! + searchSuggestions(query: String!, limit: Int!, cursor: Int): [Suggestion!]! } type Mutation { From cc68ac0a842091c72d221e0fe4dcec5be6da4832 Mon Sep 17 00:00:00 2001 From: Ben Awad Date: Fri, 28 Jul 2017 18:58:01 -0500 Subject: [PATCH 18/31] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 18b8021..3d42c22 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # Template for an Express Server with GraphQL -[Watch the video to learn how it was made.](https://youtu.be/HmwEGkBKb2s) +[Watch the video to learn how it was made.](https://youtu.be/-0mT8N19dLY) From 4e78fe72f86d3aa1fef8857430445a475006ae30 Mon Sep 17 00:00:00 2001 From: ben awad Date: Fri, 4 Aug 2017 22:11:03 -0500 Subject: [PATCH 19/31] ignore .env --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3c3629e..37d7e73 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +.env From c6787b17bae1665f25f493a4b9dd7d77100528b0 Mon Sep 17 00:00:00 2001 From: ben awad Date: Fri, 4 Aug 2017 22:56:46 -0500 Subject: [PATCH 20/31] add n to m relationship and graphql types and resolvers for mutations --- index.js | 11 ++--------- models/author.js | 22 ++++++++++++++++++++++ models/book.js | 22 ++++++++++++++++++++++ models/bookAuthor.js | 9 +++++++++ models/index.js | 18 ++++++++---------- resolvers.js | 18 ++++++++++++++++++ schema.js | 17 +++++++++++++++++ 7 files changed, 98 insertions(+), 19 deletions(-) create mode 100644 models/author.js create mode 100644 models/book.js create mode 100644 models/bookAuthor.js diff --git a/index.js b/index.js index 79b0de9..679a939 100644 --- a/index.js +++ b/index.js @@ -71,9 +71,7 @@ app.get( passport.authenticate('facebook', { session: false }), async (req, res) => { const [token, refreshToken] = await createTokens(req.user, SECRET); - res.redirect( - `http://localhost:8080/home?token=${token}&refreshToken=${refreshToken}`, - ); + res.redirect(`http://localhost:8080/home?token=${token}&refreshToken=${refreshToken}`); }, ); @@ -86,12 +84,7 @@ const addUser = async (req, res, next) => { req.user = user; } catch (err) { const refreshToken = req.headers['x-refresh-token']; - const newTokens = await refreshTokens( - token, - refreshToken, - models, - SECRET, - ); + const newTokens = await refreshTokens(token, refreshToken, models, SECRET); if (newTokens.token && newTokens.refreshToken) { res.set('Access-Control-Expose-Headers', 'x-token, x-refresh-token'); res.set('x-token', newTokens.token); diff --git a/models/author.js b/models/author.js new file mode 100644 index 0000000..445d656 --- /dev/null +++ b/models/author.js @@ -0,0 +1,22 @@ +export default (sequelize, DataTypes) => { + const Author = sequelize.define('author', { + firstname: { + type: DataTypes.STRING, + }, + lastname: { + type: DataTypes.STRING, + }, + }); + + Author.associate = (models) => { + // many to many with book + Author.belongsToMany(models.Book, { + through: { + model: models.BookAuthor, + }, + foreignKey: 'authorId', + }); + }; + + return Author; +}; diff --git a/models/book.js b/models/book.js new file mode 100644 index 0000000..b890693 --- /dev/null +++ b/models/book.js @@ -0,0 +1,22 @@ +export default (sequelize, DataTypes) => { + const Book = sequelize.define('book', { + title: { + type: DataTypes.STRING, + }, + price: { + type: DataTypes.DOUBLE, + }, + }); + + Book.associate = (models) => { + // many to many with book + Book.belongsToMany(models.Author, { + through: { + model: models.BookAuthor, + }, + foreignKey: 'bookId', + }); + }; + + return Book; +}; diff --git a/models/bookAuthor.js b/models/bookAuthor.js new file mode 100644 index 0000000..83ce862 --- /dev/null +++ b/models/bookAuthor.js @@ -0,0 +1,9 @@ +export default (sequelize, DataTypes) => { + const BookAuthor = sequelize.define('bookAuthor', { + primary: { + type: DataTypes.BOOLEAN, + }, + }); + + return BookAuthor; +}; diff --git a/models/index.js b/models/index.js index 7c98a6b..a8da38b 100644 --- a/models/index.js +++ b/models/index.js @@ -1,21 +1,19 @@ import Sequelize from 'sequelize'; -const sequelize = new Sequelize( - 'test_graphql_db', - 'test_graphql_admin', - 'iamapassword', - { - host: 'localhost', - dialect: 'postgres', - }, -); +const sequelize = new Sequelize('test_graphql_db', 'test_graphql_admin', 'iamapassword', { + host: 'localhost', + dialect: 'postgres', +}); const db = { User: sequelize.import('./user'), Board: sequelize.import('./board'), Suggestion: sequelize.import('./suggestion'), - FbAuth: sequelize.import('./FbAuth'), + FbAuth: sequelize.import('./fbAuth'), LocalAuth: sequelize.import('./localAuth'), + Book: sequelize.import('./book'), + Author: sequelize.import('./author'), + BookAuthor: sequelize.import('./bookAuthor'), }; Object.keys(db).forEach((modelName) => { diff --git a/resolvers.js b/resolvers.js index 6c4334b..35d8c6c 100644 --- a/resolvers.js +++ b/resolvers.js @@ -124,5 +124,23 @@ export default { tryLogin(email, password, models, SECRET), refreshTokens: (parent, { token, refreshToken }, { models, SECRET }) => refreshTokens(token, refreshToken, models, SECRET), + createBook: async (parent, args, { models }) => { + const book = await models.Book.create(args); + return { + ...book.dataValues, + authors: [], + }; + }, + createAuthor: async (parent, args, { models }) => { + const author = await models.Author.create(args); + return { + ...author.dataValues, + books: [], + }; + }, + addBookAuthor: async (parent, args, { models }) => { + await models.Author.create(args); + return true; + }, }, }; diff --git a/schema.js b/schema.js index bcd987a..b53d69b 100644 --- a/schema.js +++ b/schema.js @@ -32,6 +32,20 @@ export default ` refreshToken: String! } + type Author { + id: Int! + firstname: String! + lastname: String! + primary: Boolean + books: [Book!]! + } + + type Book { + id: Int! + title: String! + authors: [Author!]! + } + type Query { allUsers: [User!]! me: User @@ -44,6 +58,9 @@ export default ` } type Mutation { + createAuthor(firstname: String!, lastname: String!): Author! + createBook(title: String!): Book! + addBookAuthor(bookId: Int!, authorId: Int!, primary: Boolean!): Boolean! updateUser(username: String!, newUsername: String!): [Int!]! deleteUser(username: String!): Int! createBoard(owner: Int!, name: String): Board! From 42a1d0703edf771fdda1585dc4e75f66a9d6eea6 Mon Sep 17 00:00:00 2001 From: ben awad Date: Fri, 4 Aug 2017 23:06:33 -0500 Subject: [PATCH 21/31] remove price --- models/book.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/models/book.js b/models/book.js index b890693..f4e5a28 100644 --- a/models/book.js +++ b/models/book.js @@ -3,9 +3,6 @@ export default (sequelize, DataTypes) => { title: { type: DataTypes.STRING, }, - price: { - type: DataTypes.DOUBLE, - }, }); Book.associate = (models) => { From d03de9668d000588bc9be991fcd1a85dc8b7cfd4 Mon Sep 17 00:00:00 2001 From: ben awad Date: Fri, 4 Aug 2017 23:43:46 -0500 Subject: [PATCH 22/31] added join monster --- index.js | 4 ++++ joinMonsterMetadata.js | 49 ++++++++++++++++++++++++++++++++++++++++++ package.json | 2 ++ resolvers.js | 15 ++++++++++++- schema.js | 3 +++ yarn.lock | 36 +++++++++++++++++++++++++++++++ 6 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 joinMonsterMetadata.js diff --git a/index.js b/index.js index 679a939..bc4ed03 100644 --- a/index.js +++ b/index.js @@ -11,17 +11,21 @@ import _ from 'lodash'; import DataLoader from 'dataloader'; import passport from 'passport'; import FacebookStrategy from 'passport-facebook'; +import joinMonsterAdapt from 'join-monster-graphql-tools-adapter'; import typeDefs from './schema'; import resolvers from './resolvers'; import models from './models'; import { createTokens, refreshTokens } from './auth'; +import joinMonsterMetadata from './joinMonsterMetadata'; const schema = makeExecutableSchema({ typeDefs, resolvers, }); +joinMonsterAdapt(schema, joinMonsterMetadata); + const SECRET = 'aslkdjlkaj10830912039jlkoaiuwerasdjflkasd'; const app = express(); diff --git a/joinMonsterMetadata.js b/joinMonsterMetadata.js new file mode 100644 index 0000000..ecf89bd --- /dev/null +++ b/joinMonsterMetadata.js @@ -0,0 +1,49 @@ +export default { + Query: { + fields: { + getBook: { + where: (table, empty, args) => `${table}.id = ${args.id}`, + }, + }, + }, + Author: { + sqlTable: 'authors', + uniqueKey: 'id', + fields: { + books: { + junction: { + sqlTable: '"bookAuthors"', + include: { + primary: { + sqlColumn: 'primary', + }, + }, + sqlJoins: [ + (authorTable, junctionTable) => `${authorTable}.id = ${junctionTable}."authorId"`, + (junctionTable, bookTable) => `${junctionTable}."bookId" = ${bookTable}.id`, + ], + }, + }, + }, + }, + Book: { + sqlTable: 'books', + uniqueKey: 'id', + fields: { + authors: { + junction: { + sqlTable: '"bookAuthors"', + include: { + primary: { + sqlColumn: 'primary', + }, + }, + sqlJoins: [ + (bookTable, junctionTable) => `${bookTable}.id = ${junctionTable}."bookId"`, + (junctionTable, authorTable) => `${junctionTable}."authorId" = ${authorTable}.id`, + ], + }, + }, + }, + }, +}; diff --git a/package.json b/package.json index 87c8d67..38164f3 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "graphql-server-express": "^0.8.0", "graphql-subscriptions": "^0.4.3", "graphql-tools": "^1.0.0", + "join-monster": "^2.0.3", + "join-monster-graphql-tools-adapter": "^0.0.2", "jsonwebtoken": "^7.4.1", "lodash": "^4.17.4", "passport": "^0.3.2", diff --git a/resolvers.js b/resolvers.js index 35d8c6c..e65ec98 100644 --- a/resolvers.js +++ b/resolvers.js @@ -1,6 +1,7 @@ import bcrypt from 'bcrypt'; import { PubSub } from 'graphql-subscriptions'; import _ from 'lodash'; +import joinMonster from 'join-monster'; import { requiresAuth, requiresAdmin } from './permissions'; import { refreshTokens, tryLogin } from './auth'; @@ -41,6 +42,18 @@ export default { }), }, Query: { + allAuthors: (parent, args, { models }, info) => + joinMonster(info, args, sql => + models.sequelize.query(sql, { type: models.sequelize.QueryTypes.SELECT }), + ), + getBook: (parent, args, { models }, info) => + joinMonster(info, args, sql => + models.sequelize.query(sql, { type: models.sequelize.QueryTypes.SELECT }), + ), + allBooks: (parent, args, { models }, info) => + joinMonster(info, args, sql => + models.sequelize.query(sql, { type: models.sequelize.QueryTypes.SELECT }), + ), suggestions: (parent, args, { models }) => models.Suggestion.findAll(), someSuggestions: (parent, args, { models }) => models.Suggestion.findAll(args), someSuggestions2: (parent, { limit, cursor }, { models }) => @@ -139,7 +152,7 @@ export default { }; }, addBookAuthor: async (parent, args, { models }) => { - await models.Author.create(args); + await models.BookAuthor.create(args); return true; }, }, diff --git a/schema.js b/schema.js index b53d69b..a85f82a 100644 --- a/schema.js +++ b/schema.js @@ -47,6 +47,9 @@ export default ` } type Query { + getBook(id: Int!): Book + allBooks: [Book!]! + allAuthors: [Author!]! allUsers: [User!]! me: User userBoards(owner: Int!): [Board!]! diff --git a/yarn.lock b/yarn.lock index b1734f0..6e63f4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,12 @@ # yarn lockfile v1 +"@stem/nesthydrationjs@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@stem/nesthydrationjs/-/nesthydrationjs-0.4.0.tgz#5e797efd8b64ea33307ced2a6c4cde9c0227fcf4" + dependencies: + lodash "4.13.1" + "@types/express-serve-static-core@*": version "4.0.46" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.0.46.tgz#52040d5e37da132296e333be79e3befa1b02b34a" @@ -1093,6 +1099,10 @@ depd@1.1.0, depd@^1.1.0, depd@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" +deprecate@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/deprecate/-/deprecate-1.0.0.tgz#661490ed2428916a6c8883d8834e5646f4e4a4a8" + deprecated-decorator@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37" @@ -1552,6 +1562,10 @@ generate-object-property@^1.1.0: dependencies: is-property "^1.0.0" +generatorics@^1.0.8: + version "1.1.0" + resolved "https://registry.yarnpkg.com/generatorics/-/generatorics-1.1.0.tgz#695060bb8d88b909b30171a5cb3d424768661138" + generic-pool@^3.1.6: version "3.1.7" resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.1.7.tgz#dac22b2c7a7a04e41732f7d8d2d25a303c88f662" @@ -1624,6 +1638,10 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4: version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" +graphql-relay@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/graphql-relay/-/graphql-relay-0.5.2.tgz#40ff714efd60c2cd89e0bcc79e2afa6d87fa8673" + graphql-server-core@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/graphql-server-core/-/graphql-server-core-0.8.0.tgz#44cb0bc91c47fccbd1f6ccd60fcfaca4684c9fe3" @@ -1990,6 +2008,20 @@ joi@^6.10.1: moment "2.x.x" topo "1.x.x" +join-monster-graphql-tools-adapter@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/join-monster-graphql-tools-adapter/-/join-monster-graphql-tools-adapter-0.0.2.tgz#8f0ef874e104c2d1fab6f32ace190f2cd581d43f" + +join-monster@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/join-monster/-/join-monster-2.0.3.tgz#ecb57a0dde6be8b869cd4cdcf25f8c01dd7e55b9" + dependencies: + "@stem/nesthydrationjs" "0.4.0" + debug "^2.6.8" + deprecate "^1.0.0" + generatorics "^1.0.8" + graphql-relay "^0.5.0" + js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" @@ -2206,6 +2238,10 @@ lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" +lodash@4.13.1: + version "4.13.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.13.1.tgz#83e4b10913f48496d4d16fec4a560af2ee744b68" + lodash@^4.0.0, lodash@^4.17.1, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" From 0acae689894bdac1ad7e9b3989b3c074d1f5c4e3 Mon Sep 17 00:00:00 2001 From: ben awad Date: Sat, 5 Aug 2017 16:42:43 -0500 Subject: [PATCH 23/31] add pagination to allbooks --- joinMonsterMetadata.js | 5 +++++ resolvers.js | 21 +++++++++++++++------ schema.js | 2 +- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/joinMonsterMetadata.js b/joinMonsterMetadata.js index ecf89bd..c839546 100644 --- a/joinMonsterMetadata.js +++ b/joinMonsterMetadata.js @@ -4,6 +4,11 @@ export default { getBook: { where: (table, empty, args) => `${table}.id = ${args.id}`, }, + allBooks: { + limit: (table, { limit }) => limit, + orderBy: 'id', + where: (table, empty, { key }) => `${table}.id > ${key}`, + }, }, }, Author: { diff --git a/resolvers.js b/resolvers.js index e65ec98..d14c35c 100644 --- a/resolvers.js +++ b/resolvers.js @@ -43,16 +43,25 @@ export default { }, Query: { allAuthors: (parent, args, { models }, info) => - joinMonster(info, args, sql => - models.sequelize.query(sql, { type: models.sequelize.QueryTypes.SELECT }), + joinMonster( + info, + args, + sql => models.sequelize.query(sql, { type: models.sequelize.QueryTypes.SELECT }), + { dialect: 'pg' }, ), getBook: (parent, args, { models }, info) => - joinMonster(info, args, sql => - models.sequelize.query(sql, { type: models.sequelize.QueryTypes.SELECT }), + joinMonster( + info, + args, + sql => models.sequelize.query(sql, { type: models.sequelize.QueryTypes.SELECT }), + { dialect: 'pg' }, ), allBooks: (parent, args, { models }, info) => - joinMonster(info, args, sql => - models.sequelize.query(sql, { type: models.sequelize.QueryTypes.SELECT }), + joinMonster( + info, + args, + sql => models.sequelize.query(sql, { type: models.sequelize.QueryTypes.SELECT }), + { dialect: 'pg' }, ), suggestions: (parent, args, { models }) => models.Suggestion.findAll(), someSuggestions: (parent, args, { models }) => models.Suggestion.findAll(args), diff --git a/schema.js b/schema.js index a85f82a..f85830f 100644 --- a/schema.js +++ b/schema.js @@ -48,7 +48,7 @@ export default ` type Query { getBook(id: Int!): Book - allBooks: [Book!]! + allBooks(key: Int!, limit: Int!): [Book!]! allAuthors: [Author!]! allUsers: [User!]! me: User From e5afa8966174dd74a3281e4fab55f10c43bc5ea0 Mon Sep 17 00:00:00 2001 From: ben awad Date: Wed, 30 Aug 2017 10:55:50 -0500 Subject: [PATCH 24/31] split schema up --- schema/Board.js | 16 ++++++++++++ schema/Book.js | 17 +++++++++++++ schema/Suggestion.js | 19 ++++++++++++++ schema.js => schema/index.js | 49 ++++++++++++++---------------------- 4 files changed, 71 insertions(+), 30 deletions(-) create mode 100644 schema/Board.js create mode 100644 schema/Book.js create mode 100644 schema/Suggestion.js rename schema.js => schema/index.js (54%) diff --git a/schema/Board.js b/schema/Board.js new file mode 100644 index 0000000..bb99479 --- /dev/null +++ b/schema/Board.js @@ -0,0 +1,16 @@ +export const types = ` + type Board { + id: Int! + name: String! + suggestions: [Suggestion!]! + owner: Int! + } +`; + +export const queries = ` + userBoards(owner: Int!): [Board!]! +`; + +export const mutations = ` + createBoard(owner: Int!, name: String): Board! +`; diff --git a/schema/Book.js b/schema/Book.js new file mode 100644 index 0000000..7f6b375 --- /dev/null +++ b/schema/Book.js @@ -0,0 +1,17 @@ +export const types = ` + type Book { + id: Int! + title: String! + authors: [Author!]! + } +`; + +export const queries = ` + getBook(id: Int!): Book + allBooks(key: Int!, limit: Int!): [Book!]! +`; + +export const mutations = ` + createBook(title: String!): Book! + addBookAuthor(bookId: Int!, authorId: Int!, primary: Boolean!): Boolean! +`; diff --git a/schema/Suggestion.js b/schema/Suggestion.js new file mode 100644 index 0000000..61049af --- /dev/null +++ b/schema/Suggestion.js @@ -0,0 +1,19 @@ +export const types = ` + type Suggestion { + id: Int! + text: String! + creator: User! + } +`; + +export const queries = ` + userSuggestions(creatorId: String!): [Suggestion!]! + suggestions: [Suggestion!]! + someSuggestions(limit: Int!, offset: Int!): [Suggestion!]! + someSuggestions2(limit: Int!, cursor: Int): [Suggestion!]! + searchSuggestions(query: String!, limit: Int!, cursor: Int): [Suggestion!]! +`; + +export const mutations = ` + createSuggestion(creatorId: Int!, text: String, boardId: Int!): Suggestion! +`; diff --git a/schema.js b/schema/index.js similarity index 54% rename from schema.js rename to schema/index.js index f85830f..58cef01 100644 --- a/schema.js +++ b/schema/index.js @@ -1,21 +1,26 @@ +import * as Suggestion from './Suggestion'; +import * as Board from './Board'; +import * as Book from './Book'; + +const types = []; +const queries = []; +const mutations = []; + +const schemas = [Suggestion, Board, Book]; + +schemas.forEach((s) => { + types.push(s.types); + queries.push(s.queries); + mutations.push(s.mutations); +}); + export default ` type Subscription { userAdded: User! } - type Suggestion { - id: Int! - text: String! - creator: User! - } - - type Board { - id: Int! - name: String! - suggestions: [Suggestion!]! - owner: Int! - } + ${types.join('\n')} type User { id: Int! @@ -40,34 +45,18 @@ export default ` books: [Book!]! } - type Book { - id: Int! - title: String! - authors: [Author!]! - } - type Query { - getBook(id: Int!): Book - allBooks(key: Int!, limit: Int!): [Book!]! allAuthors: [Author!]! allUsers: [User!]! me: User - userBoards(owner: Int!): [Board!]! - userSuggestions(creatorId: String!): [Suggestion!]! - suggestions: [Suggestion!]! - someSuggestions(limit: Int!, offset: Int!): [Suggestion!]! - someSuggestions2(limit: Int!, cursor: Int): [Suggestion!]! - searchSuggestions(query: String!, limit: Int!, cursor: Int): [Suggestion!]! + ${queries.join('\n')} } type Mutation { + ${mutations.join('\n')} createAuthor(firstname: String!, lastname: String!): Author! - createBook(title: String!): Book! - addBookAuthor(bookId: Int!, authorId: Int!, primary: Boolean!): Boolean! updateUser(username: String!, newUsername: String!): [Int!]! deleteUser(username: String!): Int! - createBoard(owner: Int!, name: String): Board! - createSuggestion(creatorId: Int!, text: String, boardId: Int!): Suggestion! register(username: String!, email: String!, password: String!, isAdmin: Boolean): User! login(email: String!, password: String!): AuthPayload! createUser(username: String!): User! From 6a2e73552938d3dc2733cebc8a2f7d8c1729a3ea Mon Sep 17 00:00:00 2001 From: ben awad Date: Tue, 5 Sep 2017 14:42:53 -0500 Subject: [PATCH 25/31] removed facebook auth and extra stuff --- auth.js | 38 +++++++++++++++-------- index.js | 75 ++------------------------------------------- models/fbAuth.js | 12 -------- models/index.js | 2 -- models/localAuth.js | 15 --------- models/user.js | 5 +++ resolvers.js | 22 ++++++------- 7 files changed, 43 insertions(+), 126 deletions(-) delete mode 100644 models/fbAuth.js delete mode 100644 models/localAuth.js diff --git a/auth.js b/auth.js index 830b254..a61e43e 100644 --- a/auth.js +++ b/auth.js @@ -2,14 +2,14 @@ import jwt from 'jsonwebtoken'; import _ from 'lodash'; import bcrypt from 'bcrypt'; -export const createTokens = async (user, secret) => { +export const createTokens = async (user, secret, secret2) => { const createToken = jwt.sign( { user: _.pick(user, ['id', 'isAdmin']), }, secret, { - expiresIn: '20m', + expiresIn: '1m', }, ); @@ -17,7 +17,7 @@ export const createTokens = async (user, secret) => { { user: _.pick(user, 'id'), }, - secret, + secret2, { expiresIn: '7d', }, @@ -26,7 +26,7 @@ export const createTokens = async (user, secret) => { return Promise.all([createToken, createRefreshToken]); }; -export const refreshTokens = async (token, refreshToken, models, SECRET) => { +export const refreshTokens = async (token, refreshToken, models, SECRET, SECRET_2) => { let userId = -1; try { const { user: { id } } = jwt.verify(refreshToken, SECRET); @@ -35,9 +35,25 @@ export const refreshTokens = async (token, refreshToken, models, SECRET) => { return {}; } + if (!userId) { + return {}; + } + const user = await models.User.findOne({ where: { id: userId }, raw: true }); - const [newToken, newRefreshToken] = await createTokens(user, SECRET); + if (!user) { + return {}; + } + + const refreshSecret = SECRET_2 + user.password; + + try { + jwt.verify(refreshToken, refreshSecret); + } catch (err) { + return {}; + } + + const [newToken, newRefreshToken] = await createTokens(user, SECRET, refreshSecret); return { token: newToken, refreshToken: newRefreshToken, @@ -45,22 +61,20 @@ export const refreshTokens = async (token, refreshToken, models, SECRET) => { }; }; -export const tryLogin = async (email, password, models, SECRET) => { - const localAuth = await models.LocalAuth.findOne({ where: { email }, raw: true }); - if (!localAuth) { +export const tryLogin = async (email, password, models, SECRET, SECRET_2) => { + const user = await models.User.findOne({ where: { email }, raw: true }); + if (!user) { // user with provided email not found throw new Error('Invalid login'); } - const valid = await bcrypt.compare(password, localAuth.password); + const valid = await bcrypt.compare(password, user.password); if (!valid) { // bad password throw new Error('Invalid login'); } - const user = await models.User.findOne({ where: { id: localAuth.user_id }, raw: true }); - - const [token, refreshToken] = await createTokens(user, SECRET); + const [token, refreshToken] = await createTokens(user, SECRET, SECRET_2 + user.password); return { token, diff --git a/index.js b/index.js index bc4ed03..8889611 100644 --- a/index.js +++ b/index.js @@ -7,16 +7,12 @@ import jwt from 'jsonwebtoken'; import { createServer } from 'http'; import { execute, subscribe } from 'graphql'; import { SubscriptionServer } from 'subscriptions-transport-ws'; -import _ from 'lodash'; -import DataLoader from 'dataloader'; -import passport from 'passport'; -import FacebookStrategy from 'passport-facebook'; import joinMonsterAdapt from 'join-monster-graphql-tools-adapter'; import typeDefs from './schema'; import resolvers from './resolvers'; import models from './models'; -import { createTokens, refreshTokens } from './auth'; +import { refreshTokens } from './auth'; import joinMonsterMetadata from './joinMonsterMetadata'; const schema = makeExecutableSchema({ @@ -27,61 +23,12 @@ const schema = makeExecutableSchema({ joinMonsterAdapt(schema, joinMonsterMetadata); const SECRET = 'aslkdjlkaj10830912039jlkoaiuwerasdjflkasd'; +const SECRET_2 = 'ajsdklfjaskljgklasjoiquw01982310nlksas;sdlkfj'; const app = express(); -passport.use( - new FacebookStrategy( - { - clientID: 'client_id', - clientSecret: 'client_secret', - callbackURL: 'https://8fc528a5.ngrok.io/auth/facebook/callback', - }, - async (accessToken, refreshToken, profile, cb) => { - // 2 cases - // #1 first time login - // #2 other times - const { id, displayName } = profile; - // [] - const fbUsers = await models.FbAuth.findAll({ - limit: 1, - where: { fb_id: id }, - }); - - console.log(fbUsers); - console.log(profile); - - if (!fbUsers.length) { - const user = await models.User.create(); - const fbUser = await models.FbAuth.create({ - fb_id: id, - display_name: displayName, - user_id: user.id, - }); - fbUsers.push(fbUser); - } - - cb(null, fbUsers[0]); - }, - ), -); - -app.use(passport.initialize()); - -app.get('/flogin', passport.authenticate('facebook')); - -app.get( - '/auth/facebook/callback', - passport.authenticate('facebook', { session: false }), - async (req, res) => { - const [token, refreshToken] = await createTokens(req.user, SECRET); - res.redirect(`http://localhost:8080/home?token=${token}&refreshToken=${refreshToken}`); - }, -); - const addUser = async (req, res, next) => { const token = req.headers['x-token']; - console.log(token); if (token) { try { const { user } = jwt.verify(token, SECRET); @@ -110,22 +57,6 @@ app.use( }), ); -const batchSuggestions = async (keys, { Suggestion }) => { - // keys = [1, 2, 3 ..., 13] - const suggestions = await Suggestion.findAll({ - raw: true, - where: { - boardId: { - $in: keys, - }, - }, - }); - // suggestion = [{text:'hi', boardId: 1}, {text: 'bye', boardId: 2}, {text: 'bye2'. boardId: 2}] - const gs = _.groupBy(suggestions, 'boardId'); - // gs = {1: [{text:'hi', boardId: 1}], 2: [{text: 'bye', boardId: 2}, {text: 'bye2'. boardId: 2}]} - return keys.map(k => gs[k] || []); -}; - app.use( '/graphql', bodyParser.json(), @@ -134,8 +65,8 @@ app.use( context: { models, SECRET, + SECRET_2, user: req.user, - suggestionLoader: new DataLoader(keys => batchSuggestions(keys, models)), }, })), ); diff --git a/models/fbAuth.js b/models/fbAuth.js deleted file mode 100644 index d0f79bd..0000000 --- a/models/fbAuth.js +++ /dev/null @@ -1,12 +0,0 @@ -export default (sequelize, DataTypes) => { - const FbAuth = sequelize.define('fb_auth', { - fb_id: DataTypes.STRING, - display_name: DataTypes.STRING, - }); - - FbAuth.associate = (models) => { - FbAuth.belongsTo(models.User, { foreignKey: 'user_id' }); - }; - - return FbAuth; -}; diff --git a/models/index.js b/models/index.js index a8da38b..ec4f535 100644 --- a/models/index.js +++ b/models/index.js @@ -9,8 +9,6 @@ const db = { User: sequelize.import('./user'), Board: sequelize.import('./board'), Suggestion: sequelize.import('./suggestion'), - FbAuth: sequelize.import('./fbAuth'), - LocalAuth: sequelize.import('./localAuth'), Book: sequelize.import('./book'), Author: sequelize.import('./author'), BookAuthor: sequelize.import('./bookAuthor'), diff --git a/models/localAuth.js b/models/localAuth.js deleted file mode 100644 index 53eaf07..0000000 --- a/models/localAuth.js +++ /dev/null @@ -1,15 +0,0 @@ -export default (sequelize, DataTypes) => { - const LocalAuth = sequelize.define('local_auth', { - email: { - type: DataTypes.STRING, - unique: true, - }, - password: DataTypes.STRING, - }); - - LocalAuth.associate = (models) => { - LocalAuth.belongsTo(models.User, { foreignKey: 'user_id' }); - }; - - return LocalAuth; -}; diff --git a/models/user.js b/models/user.js index d32afe7..0e751a5 100644 --- a/models/user.js +++ b/models/user.js @@ -4,6 +4,11 @@ export default (sequelize, DataTypes) => { type: DataTypes.STRING, unique: true, }, + email: { + type: DataTypes.STRING, + unique: true, + }, + password: DataTypes.STRING, isAdmin: { type: DataTypes.BOOLEAN, defaultValue: false, diff --git a/resolvers.js b/resolvers.js index d14c35c..9d54713 100644 --- a/resolvers.js +++ b/resolvers.js @@ -131,21 +131,17 @@ export default { return userAdded; }, register: async (parent, args, { models }) => { - const user = _.pick(args, ['username', 'isAdmin']); - const localAuth = _.pick(args, ['email', 'password']); - const passwordPromise = bcrypt.hash(localAuth.password, 12); - const createUserPromise = models.User.create(user); - const [password, createdUser] = await Promise.all([passwordPromise, createUserPromise]); - localAuth.password = password; - return models.LocalAuth.create({ - ...localAuth, - user_id: createdUser.id, + const hashedPassword = await bcrypt.hash(args.password, 12); + const user = await models.User.create({ + ...args, + password: hashedPassword, }); + return user; }, - login: async (parent, { email, password }, { models, SECRET }) => - tryLogin(email, password, models, SECRET), - refreshTokens: (parent, { token, refreshToken }, { models, SECRET }) => - refreshTokens(token, refreshToken, models, SECRET), + login: async (parent, { email, password }, { models, SECRET, SECRET_2 }) => + tryLogin(email, password, models, SECRET, SECRET_2), + refreshTokens: (parent, { token, refreshToken }, { models, SECRET, SECRET_2 }) => + refreshTokens(token, refreshToken, models, SECRET, SECRET_2), createBook: async (parent, args, { models }) => { const book = await models.Book.create(args); return { From 475003db480314d81e95a470495abf547011cd25 Mon Sep 17 00:00:00 2001 From: ben awad Date: Tue, 5 Sep 2017 18:27:27 -0500 Subject: [PATCH 26/31] add second secret for refresh token --- auth.js | 2 +- index.js | 2 +- permissions.js | 18 ++++++------- resolvers.js | 12 ++++++++- schema/index.js => schema.js | 50 ++++++++++++++++++++++-------------- schema/Board.js | 16 ------------ schema/Book.js | 17 ------------ schema/Suggestion.js | 19 -------------- 8 files changed, 52 insertions(+), 84 deletions(-) rename schema/index.js => schema.js (52%) delete mode 100644 schema/Board.js delete mode 100644 schema/Book.js delete mode 100644 schema/Suggestion.js diff --git a/auth.js b/auth.js index a61e43e..b251286 100644 --- a/auth.js +++ b/auth.js @@ -29,7 +29,7 @@ export const createTokens = async (user, secret, secret2) => { export const refreshTokens = async (token, refreshToken, models, SECRET, SECRET_2) => { let userId = -1; try { - const { user: { id } } = jwt.verify(refreshToken, SECRET); + const { user: { id } } = jwt.decode(refreshToken); userId = id; } catch (err) { return {}; diff --git a/index.js b/index.js index 8889611..c313ca4 100644 --- a/index.js +++ b/index.js @@ -35,7 +35,7 @@ const addUser = async (req, res, next) => { req.user = user; } catch (err) { const refreshToken = req.headers['x-refresh-token']; - const newTokens = await refreshTokens(token, refreshToken, models, SECRET); + const newTokens = await refreshTokens(token, refreshToken, models, SECRET, SECRET_2); if (newTokens.token && newTokens.refreshToken) { res.set('Access-Control-Expose-Headers', 'x-token, x-refresh-token'); res.set('x-token', newTokens.token); diff --git a/permissions.js b/permissions.js index 9398dba..eda2543 100644 --- a/permissions.js +++ b/permissions.js @@ -1,9 +1,9 @@ const createResolver = (resolver) => { const baseResolver = resolver; baseResolver.createResolver = (childResolver) => { - const newResolver = async (parent, args, context) => { - await resolver(parent, args, context); - return childResolver(parent, args, context); + const newResolver = async (parent, args, context, info) => { + await resolver(parent, args, context, info); + return childResolver(parent, args, context, info); }; return createResolver(newResolver); }; @@ -16,10 +16,8 @@ export const requiresAuth = createResolver((parent, args, context) => { } }); -export const requiresAdmin = requiresAuth.createResolver( - (parent, args, context) => { - if (!context.user.isAdmin) { - throw new Error('Requires admin access'); - } - }, -); +export const requiresAdmin = requiresAuth.createResolver((parent, args, context) => { + if (!context.user.isAdmin) { + throw new Error('Requires admin access'); + } +}); diff --git a/resolvers.js b/resolvers.js index 9d54713..a29df7c 100644 --- a/resolvers.js +++ b/resolvers.js @@ -49,13 +49,14 @@ export default { sql => models.sequelize.query(sql, { type: models.sequelize.QueryTypes.SELECT }), { dialect: 'pg' }, ), - getBook: (parent, args, { models }, info) => + getBook: requiresAuth.createResolver((parent, args, { models }, info) => joinMonster( info, args, sql => models.sequelize.query(sql, { type: models.sequelize.QueryTypes.SELECT }), { dialect: 'pg' }, ), + ), allBooks: (parent, args, { models }, info) => joinMonster( info, @@ -142,6 +143,15 @@ export default { tryLogin(email, password, models, SECRET, SECRET_2), refreshTokens: (parent, { token, refreshToken }, { models, SECRET, SECRET_2 }) => refreshTokens(token, refreshToken, models, SECRET, SECRET_2), + forgetPassword: async (parent, { userId, newPassword }, { models }) => { + try { + const hashedPassword = await bcrypt.hash(newPassword, 12); + await models.User.update({ password: hashedPassword }, { where: { id: userId } }); + return true; + } catch (e) { + return false; + } + }, createBook: async (parent, args, { models }) => { const book = await models.Book.create(args); return { diff --git a/schema/index.js b/schema.js similarity index 52% rename from schema/index.js rename to schema.js index 58cef01..eae4916 100644 --- a/schema/index.js +++ b/schema.js @@ -1,26 +1,21 @@ -import * as Suggestion from './Suggestion'; -import * as Board from './Board'; -import * as Book from './Book'; - -const types = []; -const queries = []; -const mutations = []; - -const schemas = [Suggestion, Board, Book]; - -schemas.forEach((s) => { - types.push(s.types); - queries.push(s.queries); - mutations.push(s.mutations); -}); - export default ` type Subscription { userAdded: User! } - ${types.join('\n')} + type Suggestion { + id: Int! + text: String! + creator: User! + } + + type Board { + id: Int! + name: String! + suggestions: [Suggestion!]! + owner: Int! + } type User { id: Int! @@ -45,18 +40,35 @@ export default ` books: [Book!]! } + type Book { + id: Int! + title: String! + authors: [Author!]! + } + type Query { + getBook(id: Int!): Book + allBooks(key: Int!, limit: Int!): [Book!]! allAuthors: [Author!]! allUsers: [User!]! me: User - ${queries.join('\n')} + userBoards(owner: Int!): [Board!]! + userSuggestions(creatorId: String!): [Suggestion!]! + suggestions: [Suggestion!]! + someSuggestions(limit: Int!, offset: Int!): [Suggestion!]! + someSuggestions2(limit: Int!, cursor: Int): [Suggestion!]! + searchSuggestions(query: String!, limit: Int!, cursor: Int): [Suggestion!]! } type Mutation { - ${mutations.join('\n')} + forgetPassword(userId: Int!, newPassword: String!): Boolean! createAuthor(firstname: String!, lastname: String!): Author! + createBook(title: String!): Book! + addBookAuthor(bookId: Int!, authorId: Int!, primary: Boolean!): Boolean! updateUser(username: String!, newUsername: String!): [Int!]! deleteUser(username: String!): Int! + createBoard(owner: Int!, name: String): Board! + createSuggestion(creatorId: Int!, text: String, boardId: Int!): Suggestion! register(username: String!, email: String!, password: String!, isAdmin: Boolean): User! login(email: String!, password: String!): AuthPayload! createUser(username: String!): User! diff --git a/schema/Board.js b/schema/Board.js deleted file mode 100644 index bb99479..0000000 --- a/schema/Board.js +++ /dev/null @@ -1,16 +0,0 @@ -export const types = ` - type Board { - id: Int! - name: String! - suggestions: [Suggestion!]! - owner: Int! - } -`; - -export const queries = ` - userBoards(owner: Int!): [Board!]! -`; - -export const mutations = ` - createBoard(owner: Int!, name: String): Board! -`; diff --git a/schema/Book.js b/schema/Book.js deleted file mode 100644 index 7f6b375..0000000 --- a/schema/Book.js +++ /dev/null @@ -1,17 +0,0 @@ -export const types = ` - type Book { - id: Int! - title: String! - authors: [Author!]! - } -`; - -export const queries = ` - getBook(id: Int!): Book - allBooks(key: Int!, limit: Int!): [Book!]! -`; - -export const mutations = ` - createBook(title: String!): Book! - addBookAuthor(bookId: Int!, authorId: Int!, primary: Boolean!): Boolean! -`; diff --git a/schema/Suggestion.js b/schema/Suggestion.js deleted file mode 100644 index 61049af..0000000 --- a/schema/Suggestion.js +++ /dev/null @@ -1,19 +0,0 @@ -export const types = ` - type Suggestion { - id: Int! - text: String! - creator: User! - } -`; - -export const queries = ` - userSuggestions(creatorId: String!): [Suggestion!]! - suggestions: [Suggestion!]! - someSuggestions(limit: Int!, offset: Int!): [Suggestion!]! - someSuggestions2(limit: Int!, cursor: Int): [Suggestion!]! - searchSuggestions(query: String!, limit: Int!, cursor: Int): [Suggestion!]! -`; - -export const mutations = ` - createSuggestion(creatorId: Int!, text: String, boardId: Int!): Suggestion! -`; From 8f69e747aa3d8f279263d14ffe74d2e621c2e390 Mon Sep 17 00:00:00 2001 From: ben awad Date: Sun, 10 Sep 2017 13:41:40 -0500 Subject: [PATCH 27/31] new table for champion that has an image --- models/champion.js | 12 ++++++++++++ models/index.js | 1 + resolvers.js | 2 ++ schema.js | 8 ++++++++ 4 files changed, 23 insertions(+) create mode 100644 models/champion.js diff --git a/models/champion.js b/models/champion.js new file mode 100644 index 0000000..479d627 --- /dev/null +++ b/models/champion.js @@ -0,0 +1,12 @@ +export default (sequelize, DataTypes) => { + const Champion = sequelize.define('champion', { + name: { + type: DataTypes.STRING, + }, + publicId: { + type: DataTypes.STRING, + }, + }); + + return Champion; +}; diff --git a/models/index.js b/models/index.js index ec4f535..639e75f 100644 --- a/models/index.js +++ b/models/index.js @@ -12,6 +12,7 @@ const db = { Book: sequelize.import('./book'), Author: sequelize.import('./author'), BookAuthor: sequelize.import('./bookAuthor'), + Champion: sequelize.import('./champion'), }; Object.keys(db).forEach((modelName) => { diff --git a/resolvers.js b/resolvers.js index a29df7c..9669618 100644 --- a/resolvers.js +++ b/resolvers.js @@ -42,6 +42,7 @@ export default { }), }, Query: { + getChampion: (parent, { id }, { models }) => models.Champion.findOne({ where: { id } }), allAuthors: (parent, args, { models }, info) => joinMonster( info, @@ -117,6 +118,7 @@ export default { }, Mutation: { + createChampion: (parent, args, { models }) => models.Champion.create(args), updateUser: (parent, { username, newUsername }, { models }) => models.User.update({ username: newUsername }, { where: { username } }), deleteUser: (parent, args, { models }) => models.User.destroy({ where: args }), diff --git a/schema.js b/schema.js index eae4916..3566a77 100644 --- a/schema.js +++ b/schema.js @@ -46,7 +46,14 @@ export default ` authors: [Author!]! } + type Champion { + id: Int! + name: String! + publicId: String! + } + type Query { + getChampion(id: Int!): Champion getBook(id: Int!): Book allBooks(key: Int!, limit: Int!): [Book!]! allAuthors: [Author!]! @@ -61,6 +68,7 @@ export default ` } type Mutation { + createChampion(name: String!, publicId: String!): Champion! forgetPassword(userId: Int!, newPassword: String!): Boolean! createAuthor(firstname: String!, lastname: String!): Author! createBook(title: String!): Book! From 7c1d6f47716c38c8c03e2d506d7ee17460ab8e7e Mon Sep 17 00:00:00 2001 From: ben awad Date: Tue, 12 Sep 2017 14:28:44 -0500 Subject: [PATCH 28/31] search --- config/config.json | 9 + resolvers.js | 2 + schema.js | 1 + seeders/20170912173026-books.js | 22 + seeders/bookData.js | 1179 +++++++++++++++++++++++++++++++ 5 files changed, 1213 insertions(+) create mode 100644 config/config.json create mode 100644 seeders/20170912173026-books.js create mode 100644 seeders/bookData.js diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..f08ec28 --- /dev/null +++ b/config/config.json @@ -0,0 +1,9 @@ +{ + "development": { + "username": "test_graphql_admin", + "password": "iamapassword", + "database": "test_graphql_db", + "host": "localhost", + "dialect": "postgres" + } +} diff --git a/resolvers.js b/resolvers.js index 9669618..f0c1dab 100644 --- a/resolvers.js +++ b/resolvers.js @@ -42,6 +42,8 @@ export default { }), }, Query: { + searchBooks: (parent, { title }, { models }) => + models.Book.findAll({ where: { title: { $iLike: `%${title}%` } } }, { raw: true }), getChampion: (parent, { id }, { models }) => models.Champion.findOne({ where: { id } }), allAuthors: (parent, args, { models }, info) => joinMonster( diff --git a/schema.js b/schema.js index 3566a77..4ae1ac5 100644 --- a/schema.js +++ b/schema.js @@ -53,6 +53,7 @@ export default ` } type Query { + searchBooks(title: String!): [Book!]! getChampion(id: Int!): Champion getBook(id: Int!): Book allBooks(key: Int!, limit: Int!): [Book!]! diff --git a/seeders/20170912173026-books.js b/seeders/20170912173026-books.js new file mode 100644 index 0000000..2570ff0 --- /dev/null +++ b/seeders/20170912173026-books.js @@ -0,0 +1,22 @@ +const books = require('./bookData'); + +module.exports = { + up: (queryInterface) => { + const booksWithDates = books.map(b => ({ + title: b.title, + createdAt: new Date(), + updatedAt: new Date(), + })); + return queryInterface.bulkInsert('books', booksWithDates); + }, + + down: () => { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkDelete('Person', null, {}); + */ + }, +}; diff --git a/seeders/bookData.js b/seeders/bookData.js new file mode 100644 index 0000000..e02b6a2 --- /dev/null +++ b/seeders/bookData.js @@ -0,0 +1,1179 @@ +module.exports = [ + { title: 'I Win!' }, + { title: 'Robots' }, + { title: 'Danger!' }, + { title: 'Cloning' }, + { title: 'Hot Dog!' }, + { title: 'Cry Wolf' }, + { title: 'Wake Up!' }, + { title: "I'm Fine" }, + { title: 'Gambling' }, + { title: 'Mensa Man' }, + { title: 'Big Fart!' }, + { title: 'Hypnotism' }, + { title: 'Downpour!' }, + { title: 'Full Moon' }, + { title: 'Sea Birds' }, + { title: 'Teach Me!' }, + { title: 'I Say So!' }, + { title: 'Tug of War' }, + { title: 'Surprised!' }, + { title: 'Beekeeping' }, + { title: 'Good Works' }, + { title: 'Golly Gosh!' }, + { title: "It's Magic!" }, + { title: 'April Fool!' }, + { title: 'Come on in!' }, + { title: 'Parachuting' }, + { title: 'Get Moving!' }, + { title: 'I Like Fish' }, + { title: 'Leo Tolstoy' }, + { title: 'May Flowers' }, + { title: 'Pain Relief' }, + { title: "It's Unfair!" }, + { title: 'Armed Heists' }, + { title: 'How to Annoy' }, + { title: 'Racketeering' }, + { title: 'I Love Wills' }, + { title: 'Stop Arguing' }, + { title: 'Sofa so Good' }, + { title: 'Riel Ambush!' }, + { title: 'Falling Trees' }, + { title: 'Monkey Shines' }, + { title: 'Why Cars Stop' }, + { title: 'Turtle Racing' }, + { title: 'Military Rule' }, + { title: 'I Like Liquor' }, + { title: 'I Love Crowds' }, + { title: 'The Yellow River,' }, + { title: 'Off To Market' }, + { title: 'A Great Plenty' }, + { title: 'Mosquito Bites' }, + { title: 'My Lost Causes' }, + { title: 'Grave Mistakes' }, + { title: 'Get Out There!' }, + { title: 'Red Vegetables' }, + { title: 'Irish Flooring' }, + { title: 'Highway Travel' }, + { title: "It's a Shocker" }, + { title: 'Keep it Clean!' }, + { title: 'I Hit the Wall' }, + { title: 'Ship Mysteries' }, + { title: 'I Hate the Sun' }, + { title: "It's a Holdup!" }, + { title: 'He Disappeared!' }, + { title: 'I Hate Fighting' }, + { title: 'Mexican Revenge' }, + { title: "I Didn't Do It!" }, + { title: 'Life in Chicago' }, + { title: 'Without Warning' }, + { title: 'Pain in My Body' }, + { title: 'Desert Crossing' }, + { title: 'Candle-Vaulting' }, + { title: 'Happy New Year!' }, + { title: "You're Kidding!" }, + { title: "Webster's Words" }, + { title: 'Those Funny Dogs' }, + { title: 'Wind Instruments' }, + { title: 'Winning the Race' }, + { title: 'Crocodile Dundee' }, + { title: 'Covered Walkways' }, + { title: 'I Need Insurance' }, + { title: 'Whatchamacallit!' }, + { title: "Let's Do it Now!" }, + { title: "I'm Someone Else" }, + { title: 'Animal Illnesses' }, + { title: "He's Contagious!" }, + { title: 'The Great Escape' }, + { title: 'Music of the Sea' }, + { title: 'Breaking the Law' }, + { title: 'Cooking Spaghetti' }, + { title: 'Smart Beer Making' }, + { title: 'Good Housekeeping' }, + { title: 'Mountain Climbing' }, + { title: 'Theft and Robbery' }, + { title: 'Equine Leg Cramps' }, + { title: 'The Lion Attacked' }, + { title: 'Poetry in Baseball' }, + { title: 'I Love Mathematics' }, + { title: 'Exercise on Wheels' }, + { title: 'Measles Collision!' }, + { title: 'Unsolved Mysteries' }, + { title: 'I Lived in Detroit' }, + { title: 'Lots of Excitement' }, + { title: 'String Instruments' }, + { title: 'Outdoor Activities' }, + { title: 'Maritime Disasters' }, + { title: 'Smash His Lobster!' }, + { title: 'The Unknown Rodent' }, + { title: 'In the Arctic Ocean' }, + { title: 'Perverted Mushrooms' }, + { title: 'Modern Tree Watches' }, + { title: 'Noise is Forbidden!' }, + { title: 'I Must Fix the Car!' }, + { title: 'Snakes of the World' }, + { title: 'The Housing Problem' }, + { title: 'Artificial Clothing' }, + { title: 'More for Your Money' }, + { title: 'If I Invited Him...' }, + { title: 'Two Thousand Pounds!' }, + { title: 'Assault with Battery' }, + { title: 'Gunslingers with Gas' }, + { title: 'Soak Your Ex-Husband' }, + { title: 'And the Other People' }, + { title: 'Overweight Vegetables' }, + { title: 'A Trip to the Dentist' }, + { title: 'Mineralogy for Giants' }, + { title: "Bring to the Grocer's" }, + { title: 'Almost Missed the Bus' }, + { title: 'My Life in the Gutter' }, + { title: 'Things to Cook Meat In' }, + { title: 'Los Angeles Pachyderms' }, + { title: 'Clothes for Germ Kings' }, + { title: 'Tyrant of the Potatoes' }, + { title: 'I Hate Monday Mornings' }, + { title: 'The Fall of a Watermelon' }, + { title: "Pull with All You've Got!" }, + { title: 'I Love Fractions' }, + { title: 'Military Defeats' }, + { title: 'Judging Fast Food' }, + { title: 'I Lost My Balance' }, + { title: 'House Construction' }, + { title: 'Kangaroo Illnesses' }, + { title: 'Exotic Irish Plants' }, + { title: 'Musical Gunfighters' }, + { title: 'A Whole Lot of Cats' }, + { title: 'I Work with Diamonds' }, + { title: 'Lawyers of Suffering' }, + { title: 'Flogging in the Army' }, + { title: 'Errors and Accidents' }, + { title: 'Christmas for Baldies' }, + { title: 'Where to Find Islands' }, + { title: 'French Overpopulation' }, + { title: 'How to Tour the Prison' }, + { title: 'I Like Weeding Gardens' }, + { title: 'Who Killed Cock Robin?' }, + { title: 'The Effects of Alcohol' }, + { title: 'Daddy are We There Yet?' }, + { title: 'The Excitement of Trees' }, + { title: 'No More Circuit Breakers!' }, + { title: "You're a Bundle of Laughs" }, + { title: 'The Industrial Revolution' }, + { title: 'Artificial Weightlessness' }, + { title: 'The Palace Roof has a Hole' }, + { title: 'Ecclesiastical Infractions' }, + { title: "Preaching to Hell's Angels" }, + { title: "Songs from 'South Pacific'" }, + { title: 'I Was a Cloakroom Attendant' }, + { title: 'Fifty Yards to the Outhouse' }, + { title: 'Foot Problems of Big Lumberjacks' }, + { title: 'Naked' }, + { title: 'Bed of Roses' }, + { title: 'Generation Kill' }, + { title: 'Walker' }, + { title: 'Screwed in Tallinn (Torsk på Tallinn - En liten film om ensamhet)' }, + { title: 'Beloved/Friend (a.k.a. Amigo/Amado) (Amic/Amat)' }, + { title: "Eu Não Quero Voltar Sozinho (I Don't Want to Go Back Alone)" }, + { title: '10 to Midnight' }, + { title: 'Hothead (Coup de tête)' }, + { title: 'Wartorn: 1861-2010' }, + { title: 'How Do You Write a Joe Schermann Song' }, + { title: 'Chesty: A Tribute to a Legend' }, + { title: 'Rose Red' }, + { title: 'Just Buried' }, + { title: 'Nicholas Nickleby' }, + { title: 'Crazy Stranger, The (Gadjo Dilo)' }, + { title: "Garfield's Fun Fest" }, + { title: 'American President, The' }, + { title: 'Way of the Dragon, The (a.k.a. Return of the Dragon) (Meng long guo jiang)' }, + { title: 'Asoka (Ashoka the Great)' }, + { title: 'Hole, The (Dong)' }, + { title: 'Prick Up Your Ears' }, + { title: 'Planes, Trains & Automobiles' }, + { title: 'Fracture' }, + { title: 'Hot Shots! Part Deux' }, + { title: 'Twelve Monkeys (a.k.a. 12 Monkeys)' }, + { title: 'Karla' }, + { title: 'Tale of Two Cities, A' }, + { title: 'Marsh, The' }, + { title: 'Naked Gun 2 1/2: The Smell of Fear, The' }, + { title: 'Funny Bones' }, + { title: 'That Uncertain Feeling' }, + { title: 'Counselor, The' }, + { title: 'I Was a Male War Bride' }, + { title: 'Blackout, The' }, + { title: 'Come Early Morning' }, + { title: 'Century of the Self, The' }, + { title: 'Munger Road' }, + { title: 'Killing of a Chinese Bookie, The' }, + { title: 'Musketeer, The' }, + { title: 'Red Eye' }, + { title: 'Girl in the Red Velvet Swing, The' }, + { title: 'Man in Grey, The' }, + { title: 'Alibi' }, + { title: 'Wicked' }, + { title: 'Place Vendôme' }, + { title: 'Fast and the Furious, The' }, + { title: 'Corner Gas: The Movie' }, + { title: 'Nine' }, + { title: 'Replicant' }, + { title: 'Age of Stupid, The' }, + { title: 'Must Have Been Love' }, + { title: 'Appointment with Danger' }, + { title: 'Fright Night Part II' }, + { title: 'Congo' }, + { title: 'Creation' }, + { title: 'Klown: The Movie (Klovn)' }, + { title: 'Hollow Crown, The' }, + { title: 'Good Work (Beau travail)' }, + { title: 'Amazing Transparent Man, The' }, + { title: "Can't Buy Me Love" }, + { title: 'Food of the Gods II' }, + { title: 'Goal II: Living the Dream' }, + { title: 'Starry Eyes' }, + { title: 'Sherlock Holmes: The Woman in Green' }, + { title: 'Cabeza de Vaca' }, + { title: 'Walk to Remember, A' }, + { title: 'Resurrecting the Champ' }, + { title: 'Zazie dans le métro' }, + { title: 'Daisy Miller' }, + { title: 'The Chumscrubber' }, + { title: 'Wonderful World' }, + { title: 'Learning to Ride' }, + { title: 'Born in Flames' }, + { title: 'Commare secca, La (Grim Reaper, The)' }, + { title: 'Grass Harp, The' }, + { title: 'Darfur Now' }, + { title: 'Memento Mori (Yeogo goedam II)' }, + { title: 'Blow Job' }, + { title: 'Hypocrites' }, + { title: 'Wind and the Lion, The' }, + { title: 'Tomorrow We Move (Demain on déménage)' }, + { title: 'Munekata Sisters, The (Munekata kyôdai)' }, + { title: 'Diving Bell and the Butterfly, The (Scaphandre et le papillon, Le)' }, + { title: 'Ninja Cheerleaders' }, + { title: 'Bad Men of Missouri' }, + { title: 'Bonaerense, El' }, + { title: 'Art School Confidential' }, + { title: 'Honey, I Shrunk the Kids' }, + { title: 'Slight Case of Murder, A' }, + { title: 'Carmen' }, + { title: 'South Park: Imaginationland' }, + { title: "Corman's World: Exploits of a Hollywood Rebel" }, + { title: 'Red Eye' }, + { title: 'Jacknife' }, + { title: "Let's Get Those English Girls" }, + { title: 'Bereavement' }, + { title: 'Juno' }, + { title: 'History Is Made at Night' }, + { title: 'Babysitting' }, + { title: 'Rest Stop' }, + { title: 'Deadly Trap, The (La maison sous les arbres)' }, + { title: 'Trancers II' }, + { title: 'Revenge of the Green Dragons' }, + { title: 'No God, No Master' }, + { title: 'Peace, Propaganda & the Promised Land' }, + { title: 'Chasing Amy' }, + { title: 'Joan of Arc' }, + { title: 'Nugget, The' }, + { title: 'Smokers Only (Vagón Fumador)' }, + { title: 'Sophia de Mello Breyner Andresen' }, + { title: 'Women, The' }, + { title: 'Secret, The' }, + { title: "Coal Miner's Daughter" }, + { title: 'Cathedral, The (Katedra)' }, + { title: 'Uncle Sam' }, + { title: 'Hello Down There' }, + { title: 'Smashing Time' }, + { title: 'Game of Death' }, + { title: 'Offender' }, + { title: 'One A.M.' }, + { title: 'Firehouse Dog' }, + { title: 'Proprietor, The' }, + { title: "Don't Think About It (Non Pensarci)" }, + { title: 'Wu Tang Master (Tian shi zhuang xie)' }, + { title: 'Quills' }, + { title: 'Secret of the Grain, The (La graine et le mulet)' }, + { title: 'Pulse (Kairo)' }, + { title: 'Trouble Every Day' }, + { title: 'Game, The' }, + { title: 'Odette Toulemonde' }, + { title: 'Great Expectations' }, + { title: 'Steam' }, + { title: 'Howl' }, + { title: 'Things We Do For Love (Kaikella rakkaudella)' }, + { title: 'Random Hearts' }, + { title: 'Shark Attack' }, + { title: 'Innocence' }, + { title: 'Calendar Girls' }, + { title: 'Santitos' }, + { title: "That's What I Am" }, + { title: "Since Otar Left (Depuis qu'Otar est parti...)" }, + { title: 'Magic Man' }, + { title: 'Lili' }, + { title: 'Malice in Wonderland' }, + { title: 'Radio Inside' }, + { title: 'Pushing Tin' }, + { title: 'Friday the 13th Part VII: The New Blood' }, + { title: 'Sacrifice, The (Offret - Sacraficatio)' }, + { title: 'Me Myself I' }, + { title: 'Tooth Fairy' }, + { title: 'Spring Forward' }, + { title: 'By the Bluest of Seas (U samogo sinego morya)' }, + { title: 'Eye for an Eye, An (Oeil pour oeil) (Eyes of the Sahara)' }, + { title: "Illusionist, The (L'illusionniste)" }, + { title: 'Winter Solstice' }, + { title: 'In Fear' }, + { title: 'House of Fools' }, + { title: "Day's Pleasure, A (Ford Story, A)" }, + { title: "Dragonheart 3: The Sorcerer's Curse" }, + { title: 'Man from Down Under, The' }, + { title: 'Hotel Hell Vacation' }, + { title: 'Eye 2, The (Gin gwai 2)' }, + { title: 'Shaka Zulu: The Citadel' }, + { title: 'Siegfried & Roy: The Magic Box' }, + { title: "European Vacation (aka National Lampoon's European Vacation)" }, + { title: 'Bloody Bloody Bible Camp' }, + { title: 'Irene, Go Home! (Irena do domu!)' }, + { title: 'Holy Wars' }, + { title: 'Border Incident' }, + { title: 'Miss Representation' }, + { title: 'Avengers, The' }, + { title: 'Scourge' }, + { title: 'Branded' }, + { title: 'Lucas' }, + { title: 'Mr. Smith Goes to Washington' }, + { title: 'Road to Zanzibar' }, + { title: 'Free Money' }, + { title: 'Occupants, The' }, + { title: 'Way Back, The' }, + { title: 'Last Klezmer: Leopold Kozlowski, His Life and Music, The' }, + { title: 'Margaret' }, + { title: 'Crocodile Dundee II' }, + { title: 'Upperworld' }, + { title: 'Medium Cool' }, + { title: 'Heroine' }, + { title: 'Gamer' }, + { title: 'Inkwell, The' }, + { title: 'La Rabbia' }, + { title: 'TerrorStorm: A History of Government-Sponsored Terrorism' }, + { title: 'Sympathy for the Devil' }, + { title: 'Squall, The' }, + { title: 'Dishonored' }, + { title: 'Cry Wolf' }, + { title: 'Jeanne Dielman, 23 Quai du Commerce, 1080 Bruxelles' }, + { title: 'Tokyo Decadence (Topâzu)' }, + { title: 'Seventh Victim, The' }, + { title: 'Vigilante' }, + { title: 'Paradine Case, The' }, + { title: 'Quebrando o Tabu' }, + { title: 'Caged Heat' }, + { title: 'The Last Shark' }, + { title: 'Carmen' }, + { title: 'Purple Rose of Cairo, The' }, + { title: 'Invisible Target (Naam yi boon sik)' }, + { title: 'Toy, The (Le jouet)' }, + { title: 'Next Karate Kid, The' }, + { title: 'The Killers' }, + { title: 'One Hour with You' }, + { title: 'Houdini' }, + { title: 'Man from Monterey, The' }, + { title: 'Peanut Butter Solution, The' }, + { title: 'Taming of the Shrew, The' }, + { title: 'Panda! Go Panda! (Panda kopanda)' }, + { title: 'An Alligator Named Daisy' }, + { title: 'Sacco and Vanzetti (Sacco e Vanzetti)' }, + { title: 'No Way Out' }, + { title: 'A Lesson Before Dying' }, + { title: 'Vatel' }, + { title: 'Rebirth of Mothra' }, + { title: 'Magnificent Warriors (Zhong hua zhan shi)' }, + { title: "Mabel's Married Life" }, + { title: 'Liberty Heights' }, + { title: 'Skeletons' }, + { title: 'MacGruber' }, + { title: 'Clockstoppers' }, + { title: 'Vixen!' }, + { title: 'Conclave, The' }, + { title: 'Brood, The' }, + { title: "Marvin's Room" }, + { title: 'The Appointments of Dennis Jennings' }, + { title: 'Stille Nacht I: Dramolet' }, + { title: 'Lakeview Terrace' }, + { title: 'Breathing Room' }, + { title: 'Girl, Interrupted' }, + { title: 'Long Time Dead' }, + { title: 'Death at a Funeral' }, + { title: 'Outbreak' }, + { title: 'Sunny (Sseo-ni)' }, + { title: 'Mary of Scotland' }, + { title: 'Thirteen Conversations About One Thing (a.k.a. 13 Conversations)' }, + { title: 'Merry Madagascar' }, + { title: 'Ball, The (Le bal)' }, + { title: "Aces 'N' Eights" }, + { title: 'Pot v raj' }, + { title: 'Big Shot: Confessions of a Campus Bookie' }, + { title: 'Kronos' }, + { title: "I'm Not Scared (Io non ho paura)" }, + { title: 'Just a Sigh' }, + { title: 'Opening Night' }, + { title: 'Canterbury Tale, A' }, + { title: 'Music of the Heart' }, + { title: 'Man Who Knew Too Little, The' }, + { title: 'Deliver Us from Evil' }, + { title: 'Austin High' }, + { title: 'Outside the Law (Hors-la-loi)' }, + { title: 'Solaris (Solyaris)' }, + { title: 'Krakatoa, East of Java' }, + { title: 'Coca-Cola Kid, The' }, + { title: 'She' }, + { title: 'Queen: Days of Our Lives' }, + { title: 'Ladies of Leisure' }, + { title: 'Of Love and Shadows' }, + { title: 'For Your Eyes Only' }, + { title: 'Dragon Fist (Long quan)' }, + { title: 'White Shadows in the South Seas' }, + { title: 'Play' }, + { title: 'H.O.T.S.' }, + { title: 'Adventures of Rocky and Bullwinkle, The' }, + { title: 'Lost in Space' }, + { title: 'Lemonade Joe (Limonádový Joe aneb Konská opera)' }, + { title: 'Messengers, The' }, + { title: 'Trumbo' }, + { title: "'Neath the Arizona Skies" }, + { title: 'Pancho, the Millionaire Dog' }, + { title: 'Morituri' }, + { title: "Changing Sides (De l'autre côté du lit)" }, + { title: 'Oh, Sun (Soleil O)' }, + { title: 'Mudlark, The' }, + { title: 'Bridge on the River Kwai, The' }, + { title: 'Far' }, + { title: 'Under Suspicion' }, + { title: 'Not Cool' }, + { title: 'Other Woman, The' }, + { title: 'Cool as Ice' }, + { title: 'Brides (Nyfes)' }, + { title: 'Kiss, The' }, + { title: "Dr. Horrible's Sing-Along Blog" }, + { title: 'Chronicles of Riddick, The' }, + { title: 'Prodigal Son, The (Tuhlaajapoika)' }, + { title: 'UHF' }, + { title: '7 Boxes (7 cajas)' }, + { title: 'Mondays in the Sun (Lunes al sol, Los)' }, + { title: 'Paris by Night' }, + { title: 'No Deposit, No Return' }, + { title: "Turn Left at the End of the World (Sof Ha'Olam Smola)" }, + { title: 'Exorcist, The' }, + { title: 'Summer Palace (Yihe yuan)' }, + { title: 'Princess of Montpensier, The (La princesse de Montpensier)' }, + { title: 'Manhattan Melodrama' }, + { title: 'Bill Hicks: Revelations' }, + { title: 'Scream 4' }, + { title: 'Dr. Jekyll and Ms. Hyde' }, + { title: "Donovan's Brain" }, + { title: 'Aura, The (Aura, El)' }, + { title: 'Female' }, + { title: 'Carry On, Constable' }, + { title: 'Red Shoes, The' }, + { title: 'Attack Force Z (a.k.a. The Z Men) (Z-tzu te kung tui)' }, + { title: 'Ernest & Célestine (Ernest et Célestine)' }, + { title: 'Nightbreed' }, + { title: 'Father, The (Pedar)' }, + { title: 'Jerry Maguire' }, + { title: 'Semper Fi' }, + { title: 'House of Fools' }, + { title: 'Rings' }, + { title: 'Uprise' }, + { title: 'Mr. Belvedere Goes to College' }, + { title: "Satan's Sword (Daibosatsu tôge)" }, + { title: 'Highlander: The Source' }, + { title: 'Fast and the Furious, The' }, + { title: 'Bangkok Dangerous' }, + { title: 'There Will Be No Leave Today (Segodnya uvolneniya ne budet)' }, + { title: 'Proposal, The' }, + { title: 'Battle of China, The (Why We Fight, 6)' }, + { title: 'Pinchcliffe Grand Prix (Flåklypa Grand Prix)' }, + { title: '10.5' }, + { title: 'Zazie dans le métro' }, + { title: 'Heartbreakers' }, + { title: '1911 (Xinhai geming)' }, + { title: 'In the Bedroom' }, + { title: 'Ice Rink, The (La patinoire)' }, + { title: '6954 Kilometriä Kotiin' }, + { title: 'Mummy: Tomb of the Dragon Emperor, The' }, + { title: 'Zero Years, The' }, + { title: 'Bob & Carol & Ted & Alice' }, + { title: 'Last Kiss, The' }, + { title: 'Age of Ice' }, + { title: 'Eichmann' }, + { title: 'Century' }, + { title: 'For the Moment' }, + { title: 'Big Nothing' }, + { title: 'Freeway' }, + { title: 'Into the Night' }, + { title: 'Appleseed Alpha' }, + { title: 'American Carol, An' }, + { title: 'Promise Her Anything' }, + { title: 'Family, The' }, + { title: 'China Moon' }, + { title: 'American Psycho' }, + { title: "'Human' Factor, The (Human Factor, The)" }, + { title: 'Pictures of the Old World (Obrazy starého sveta)' }, + { title: 'Time of the Wolf, The (Le temps du loup)' }, + { title: 'Samurai Rebellion (Jôi-uchi: Hairyô tsuma shimatsu)' }, + { title: 'The Land Before Time IV: Journey Through the Mists' }, + { title: 'Sport, Sport, Sport' }, + { title: 'Waco: A New Revelation' }, + { title: "Your Sister's Sister" }, + { title: 'Blade of the Ripper' }, + { title: 'Name for Evil, A' }, + { title: 'Born to Be Wild' }, + { title: 'Fiston' }, + { title: "Thanksgiving Family Reunion (National Lampoon's Holiday Reunion)" }, + { title: 'Brotherhood of Death' }, + { title: 'Superhero Movie' }, + { title: 'Aankhen' }, + { title: 'Old Boy' }, + { title: 'Return, The (Vozvrashcheniye)' }, + { title: 'Moloch (Molokh)' }, + { title: 'I Will Buy You (Anata kaimasu)' }, + { title: 'Comme les 5 doigts de la main' }, + { title: "Il fiore dai petali d'acciaio" }, + { title: "Let's Get Lost" }, + { title: 'Bad Day on the Block' }, + { title: 'Incredible Rocky Mountain Race' }, + { title: 'Breaking In' }, + { title: 'One A.M.' }, + { title: 'Watchmen' }, + { title: 'Wonderland' }, + { title: 'Werewolf of London' }, + { title: 'Man Who Came to Dinner, The' }, + { title: 'No Time for Love' }, + { title: 'Three Guys Named Mike' }, + { title: 'Immortals, The' }, + { title: 'Paranormal Activity 3' }, + { title: 'Last Run, The' }, + { title: 'Last of the Red Hot Lovers' }, + { title: 'Man of the House' }, + { title: 'Condition Red (Beyond the Law)' }, + { title: 'Defender, The' }, + { title: 'Exit' }, + { title: 'Death Race 3: Inferno' }, + { title: 'Crush' }, + { title: 'Open Season' }, + { title: 'My One and Only' }, + { title: 'Strange Brew' }, + { title: 'Alex in Wonderland' }, + { title: 'Julius Caesar' }, + { title: 'The Pacific' }, + { title: "The Cat's Out" }, + { title: 'What Women Want' }, + { title: 'Sirens' }, + { title: 'Arnold' }, + { title: 'Miss Sadie Thompson' }, + { title: 'The Last Five Years' }, + { title: 'Besotted' }, + { title: 'Anima Mundi' }, + { title: 'J.C. Chávez (a.k.a. Chavez)' }, + { title: 'Hush... Hush, Sweet Charlotte' }, + { title: 'Iceman, The' }, + { title: 'Expect No Mercy' }, + { title: 'Tidal Wave' }, + { title: 'Two Women (Ciociara, La)' }, + { title: 'Way Down East' }, + { title: "Tyler Perry's Meet the Browns" }, + { title: 'Sun Shines Bright, The' }, + { title: 'Tic Code, The' }, + { title: 'Forever, Darling' }, + { title: 'Girl Who Leapt Through Time, The (Toki o kakeru shôjo)' }, + { title: 'Levitated Mass' }, + { title: 'Love Sick Love' }, + { title: 'Free Soul, A' }, + { title: 'Cat Came Back, The' }, + { title: "Charlotte's Web 2: Wilbur's Great Adventure" }, + { title: 'Nativity!' }, + { title: 'G' }, + { title: 'Keep Your Distance' }, + { title: 'Best of the Best 3: No Turning Back' }, + { title: 'Champ, The' }, + { title: 'Winnie the Pooh and the Blustery Day' }, + { title: "Charlie's Angels: Full Throttle" }, + { title: 'Man Who Left His Will on Film, The (Tôkyô sensô sengo hiwa)' }, + { title: 'Good Night to Die, A' }, + { title: 'Free the Nipple' }, + { title: 'Something Is Happening (Kuch Kuch Hota Hai)' }, + { title: 'Ape, The' }, + { title: 'Odd Thomas' }, + { title: 'Montana' }, + { title: 'American Idiots' }, + { title: 'The Land Unknown' }, + { title: 'Brood, The' }, + { title: 'Hole in the Soul, A (Rupa u dusi)' }, + { title: 'Unconditional' }, + { title: 'Misérables, Les' }, + { title: 'Under Ten Flags' }, + { title: 'Moonstruck' }, + { title: 'Phone Box, The (Cabina, La)' }, + { title: 'Dirties, The' }, + { title: 'Tadpole' }, + { title: 'Trails (Veredas)' }, + { title: 'Roger & Me' }, + { title: 'Shoot the Piano Player (Tirez sur le pianiste)' }, + { title: 'ComDads (les Compères)' }, + { title: 'Omega Doom' }, + { title: 'This Time Around' }, + { title: 'Wee Willie Winkie' }, + { title: 'Kiss the Girls' }, + { title: 'Crane World (Mundo grúa)' }, + { title: 'King David' }, + { title: 'Gojoe: Spirit War Chronicle (Gojo reisenki: Gojoe)' }, + { title: 'Battlestar Galactica: Blood & Chrome' }, + { title: 'Somebody Up There Likes Me' }, + { title: 'Rogue Cop' }, + { title: 'Great Dictator, The' }, + { title: 'Quod erat demonstrandum' }, + { title: 'Casper Meets Wendy' }, + { title: 'About Adam' }, + { title: 'Hills Have Eyes Part II, The' }, + { title: 'World War Z' }, + { title: 'Pictures of the Old World (Obrazy starého sveta)' }, + { title: 'Crush' }, + { title: 'On the Run' }, + { title: 'Human Stain, The' }, + { title: 'Man in the Iron Mask, The' }, + { title: 'Saw IV' }, + { title: 'Project Almanac' }, + { title: 'Death of Maria Malibran, The (Der Tod der Maria Malibran)' }, + { title: 'Evil That Men Do, The' }, + { title: 'Simply Irresistible' }, + { title: 'Littlest Rebel, The' }, + { title: 'Vigilante' }, + { title: 'Frozen City (Valkoinen kaupunki) ' }, + { title: "M. Hulot’s Holiday (Mr. Hulot's Holiday) (Vacances de Monsieur Hulot, Les)" }, + { title: 'Cold Weather' }, + { title: 'Choose Me' }, + { title: 'Adelheid' }, + { title: 'Moonlight Serenade' }, + { title: 'Lili' }, + { title: 'His Regeneration' }, + { title: 'Late Great Planet Earth, The' }, + { title: 'Rated X' }, + { title: 'Torque' }, + { title: 'Mr. & Mrs. Bridge' }, + { title: 'The Lazarus Effect' }, + { title: 'Escape from L.A.' }, + { title: 'Day After Tomorrow, The' }, + { title: 'In the Bleak Midwinter' }, + { title: 'Hero: Love Story of a Spy, The' }, + { title: 'Gate, The' }, + { title: 'The Disappearance of Eleanor Rigby: Her' }, + { title: 'Sheena' }, + { title: 'Loft (Rofuto)' }, + { title: 'Night Stalker, The' }, + { title: 'Annabel Takes a Tour (Annabel Takes a Trip)' }, + { title: 'Marshal of Finland, The (Suomen Marsalkka)' }, + { title: 'Fast Food, Fast Women' }, + { title: 'License to Drive' }, + { title: 'Columbus Circle' }, + { title: 'Barrier (Bariera)' }, + { title: 'Traveler, The (Mossafer)' }, + { title: 'Micki + Maude' }, + { title: 'Tokyo Fist (Tokyo ken)' }, + { title: 'Prophecy 3: The Ascent, The' }, + { title: 'Off Limits' }, + { title: 'Apple Dumpling Gang, The' }, + { title: 'Lords of Dogtown' }, + { title: 'Getaway, The' }, + { title: 'Arnulf Rainer' }, + { title: 'Circus' }, + { title: 'Ticker' }, + { title: 'Jonestown: Paradise Lost' }, + { title: 'Johnny Handsome' }, + { title: 'Blood Games ' }, + { title: 'Uninvited, The' }, + { title: 'Last Season, The' }, + { title: 'Desert Bloom' }, + { title: 'Body Bags' }, + { title: 'Exorcist II: The Heretic' }, + { title: 'Whom the Gods Wish to Destroy (Nibelungen, Teil 1: Siegfried, Die)' }, + { title: 'Before Sunrise' }, + { title: 'Victor and the Secret of Crocodile Mansion' }, + { title: 'Kingdom II, The (Riget II)' }, + { title: 'Rocker' }, + { title: 'Arizona Dream' }, + { title: 'Teenage' }, + { title: 'Farewell, The (Abschied - Brechts letzter Sommer)' }, + { title: 'Creepshow 3' }, + { title: 'Free Radicals: A History of Experimental Film' }, + { title: 'Revenge of the Nerds II: Nerds in Paradise' }, + { title: 'Total Recall' }, + { title: 'All American Chump' }, + { title: 'Boy' }, + { title: 'Dragon Age: Blood mage no seisen (a.k.a. Dragon Age: Dawn of the Seeker)' }, + { title: 'Animal' }, + { title: 'Ward, The' }, + { title: 'Every Which Way But Loose' }, + { title: 'Mr. Belvedere Goes to College' }, + { title: 'Spread' }, + { title: 'Major and the Minor, The' }, + { title: 'Critters 3' }, + { title: 'Jimmy Hollywood' }, + { title: 'Scientist, The' }, + { title: "Nobody's Children (I figli di nessuno)" }, + { title: 'Summer House' }, + { title: 'Five Graves to Cairo' }, + { title: 'We Were Here' }, + { title: 'Japon (a.k.a. Japan) (Japón)' }, + { title: 'Crime After Crime' }, + { title: 'Police' }, + { title: 'Toto le héros' }, + { title: "Murphy's War" }, + { title: 'Furry Vengeance' }, + { title: 'Eyes Wide Open (Einayim Petukhoth)' }, + { title: '54' }, + { title: 'Crashing' }, + { title: "Manual of Love 2 (Manuale d'amore (capitoli successivi))" }, + { title: 'Prometheus' }, + { title: 'Mexican Hayride' }, + { title: 'Cockleshell Heroes, The' }, + { title: 'Secrets of the Heart (Secretos del Corazón)' }, + { title: 'Lolita' }, + { title: 'Day of the Animals' }, + { title: 'Magnolia' }, + { title: 'Persona' }, + { title: 'Babylon 5: The Lost Tales - Voices in the Dark' }, + { title: 'Emigrants, The (Utvandrarna)' }, + { title: 'Christmas with the Kranks' }, + { title: 'Babysitting' }, + { title: 'Other Man, The' }, + { title: 'American Heist' }, + { title: 'Spinning Boris' }, + { title: 'Los Marziano' }, + { title: 'Je, tu, il, elle (I, You, He, She)' }, + { title: 'Ringers: Lord of the Fans' }, + { title: 'American Idiots' }, + { title: 'Richard III' }, + { title: 'My Little Pony: Equestria Girls' }, + { title: 'Quartet' }, + { title: 'Sacrifice (Zhao shi gu er)' }, + { title: "It's Me, It's Me (Ore Ore)" }, + { title: 'Antichrist' }, + { title: 'Nil By Mouth' }, + { title: '.45' }, + { title: 'Third Wave, The (Tredje vågen, Den)' }, + { title: 'Bedknobs and Broomsticks' }, + { title: 'Orwell Rolls in His Grave' }, + { title: 'Counterfeiters, The (Le cave se rebiffe)' }, + { title: 'Strings' }, + { title: 'His Secret Life (a.k.a. Ignorant Fairies, The) (Fate ignoranti, Le)' }, + { title: 'Last Night' }, + { title: 'Agent Cody Banks 2: Destination London' }, + { title: 'Born Romantic' }, + { title: 'Blind Chance (Przypadek)' }, + { title: 'Incredible Rocky Mountain Race' }, + { title: 'Sea Gull, The' }, + { title: 'The 21 Carat Snatch' }, + { title: 'Boys Life' }, + { title: 'Viva Cuba' }, + { title: 'Strictly Sexual' }, + { title: "Project A ('A' gai waak)" }, + { title: 'Me, Myself and Mum (Les garçons et Guillaume, à table!)' }, + { title: 'Empire' }, + { title: 'The Hearse' }, + { title: "Shackleton's Antarctic Adventure" }, + { title: 'Machine That Kills Bad People, The (La Macchina Ammazzacattivi)' }, + { title: 'Across the Sea of Time' }, + { title: 'Walk All Over Me' }, + { title: "Bon Voyage, Charlie Brown (and Don't Come Back!)" }, + { title: 'Land of the Blind' }, + { title: "Badman's Country" }, + { title: 'Crimson Gold (Talaye sorgh)' }, + { title: 'Cops' }, + { title: 'Gitmek: My Marlon and Brando (Gitmek: Benim Marlon ve Brandom)' }, + { title: 'Head-On (Gegen die Wand)' }, + { title: 'Ned Kelly' }, + { title: 'Kamome Diner' }, + { title: 'Cabin Boy' }, + { title: 'Spin' }, + { title: 'Dear John' }, + { title: 'Exit' }, + { title: 'Shakespeare-Wallah' }, + { title: 'Where the Money Is' }, + { title: 'Naturally Native' }, + { title: 'Forgiveness of Blood, The (Falja e gjakut)' }, + { title: 'Defenders of Riga' }, + { title: 'Aliyah (Alyah) ' }, + { title: "Nick and Norah's Infinite Playlist" }, + { title: 'Mission to Moscow' }, + { title: "The Gruffalo's Child" }, + { title: 'Humboldt County' }, + { title: '3, 2, 1... Frankie Go Boom (Frankie Go Boom)' }, + { title: 'Death of a Bureaucrat (La muerte de un burócrata)' }, + { title: 'Cartouche' }, + { title: 'Mushrooming (Seenelkäik)' }, + { title: 'Like Someone In Love' }, + { title: 'He Got Game' }, + { title: 'Ex, The' }, + { title: 'Friends, The (Les Amis)' }, + { title: 'Sleepwalking' }, + { title: 'Bridge at Remagen, The' }, + { title: '180° South (180 Degrees South) (180° South: Conquerors of the Useless)' }, + { title: 'Homeward Bound II: Lost in San Francisco' }, + { title: 'He Got Game' }, + { title: 'Acts of Worship ' }, + { title: 'Soul of a Man, The' }, + { title: 'Christmas on Mars' }, + { title: 'Mickey' }, + { title: 'Lacemaker, The (Dentellière, La)' }, + { title: 'Autumn Leaves' }, + { title: 'Gun Shy' }, + { title: 'Wild Parrots of Telegraph Hill, The' }, + { title: 'Letter From Death Row, A' }, + { title: 'Shooting War' }, + { title: 'Get Hard' }, + { title: 'Tristan & Isolde' }, + { title: 'Immensee' }, + { title: 'Dickson Experimental Sound Film' }, + { title: 'Palindromes' }, + { title: 'Hunters, The' }, + { title: 'Marriage Retreat' }, + { title: 'Carmina or Blow Up (Carmina o revienta)' }, + { title: 'Deja Vu' }, + { title: 'Stranger in Me, The (Das Fremde in mir)' }, + { title: 'Good Student, The (Mr. Gibb)' }, + { title: 'Boston Strangler, The' }, + { title: 'Hugo Pool' }, + { title: 'World, The (Shijie)' }, + { title: 'Wild Boys of the Road' }, + { title: 'Magnificent Ambersons, The' }, + { title: "Eternal Return, The (L'éternel retour)" }, + { title: 'Cloud 9' }, + { title: 'Like It Is' }, + { title: 'Private Confessions' }, + { title: 'Gay Purr-ee' }, + { title: 'Goldengirl' }, + { title: 'Ladies Man, The' }, + { title: 'American in Paris, An' }, + { title: 'Eat Pray Love' }, + { title: 'Balzac and the Little Chinese Seamstress (Xiao cai feng)' }, + { title: 'Moon-Spinners, The' }, + { title: 'Most Wanted' }, + { title: 'Congorama' }, + { title: '48 Hrs.' }, + { title: 'Inside Man' }, + { title: 'Big Easy Express' }, + { title: 'As Above, So Below' }, + { title: 'All the Boys Love Mandy Lane' }, + { title: 'Tentacles (Tentacoli)' }, + { title: 'Minuscule: Valley of the Lost Ants (Minuscule - La vallée des fourmis perdues)' }, + { title: 'Enron: The Smartest Guys in the Room' }, + { title: 'Little Trip to Heaven, A' }, + { title: 'Celine: Through the Eyes of the World' }, + { + title: + "Dexter the Dragon & Bumble the Bear (a.k.a. Dragon That Wasn't (Or Was He?), The) (Als je begrijpt wat ik bedoel)", + }, + { title: 'Cop' }, + { title: 'Batman Beyond: Return of the Joker' }, + { title: 'Lightning Bug' }, + { title: 'Nitro Circus: The Movie' }, + { title: 'Chronicle of an Escape (Crónica de una fuga)' }, + { title: "Making 'The New World'" }, + { title: 'Rolling' }, + { title: "Watch Out, We're Mad (...Altrimenti ci arrabbiamo!)" }, + { title: 'Sink the Bismark!' }, + { title: 'The End' }, + { title: 'Super Hero Party Clown' }, + { title: 'Concursante' }, + { title: 'Tea with Mussolini' }, + { title: 'Upstream Color' }, + { title: 'Grace Lee Project, The' }, + { title: 'When the Game Stands Tall' }, + { title: 'World of Henry Orient, The' }, + { title: 'Musikanten' }, + { title: 'Suur Tõll' }, + { title: 'Backfire' }, + { title: 'Hunted, The' }, + { title: 'Syrup' }, + { title: 'Free Willy' }, + { title: 'Broken Kingdom' }, + { title: 'Mr. Mom' }, + { title: 'Sarah Silverman: We Are Miracles' }, + { title: 'Boomerang' }, + { title: "Dr. Ehrlich's Magic Bullet" }, + { title: 'Save the Green Planet! (Jigureul jikyeora!)' }, + { title: 'Asphalt Jungle, The' }, + { title: 'Without Love' }, + { + title: + 'Why Are the Bells Ringing, Mitica? (a.k.a. Carnival Scenes) (De ce trag clopotele, Mitica?)', + }, + { title: 'Breathing Fire' }, + { title: 'Behind the Candelabra' }, + { title: 'Cat in the Hat, The' }, + { title: 'Flamingo Road' }, + { title: 'The Interview' }, + { title: 'Bodyguards and Assassins' }, + { title: 'Home of Dark Butterflies, The (Tummien perhosten koti)' }, + { title: 'Blue Juice' }, + { title: 'Magic Man' }, + { title: "It's My Mother's Birthday Today" }, + { title: 'Summer Catch' }, + { title: 'Life of David Gale, The' }, + { title: '112 Weddings' }, + { title: 'The Vixen' }, + { title: 'Zindagi Na Milegi Dobara' }, + { title: 'This Is Elvis' }, + { title: 'Kamchatka' }, + { title: 'Comradeship (Kameradschaft)' }, + { title: 'Bootmen' }, + { title: 'Indecent Proposal' }, + { title: 'When Pigs Have Wings' }, + { title: 'This So-Called Disaster' }, + { title: 'Oscar' }, + { title: 'Bank Dick, The' }, + { title: 'Alyce Kills' }, + { title: 'Captivity' }, + { title: 'Lady of Chance, A' }, + { title: 'Garlic Is As Good As Ten Mothers' }, + { title: 'Zatoichi and the Fugitives (Zatôichi hatashi-jô) (Zatôichi 18)' }, + { title: 'Kozara' }, + { title: 'Lust for Gold (Duhul aurului)' }, + { title: 'Revenge of the Ninja' }, + { title: "Lion's Den (Leonera)" }, + { title: 'Girls Will Be Girls' }, + { title: 'Whisky' }, + { title: 'Cow, The (Gaav)' }, + { title: 'Mexican, The' }, + { title: 'Full Body Massage' }, + { title: 'Electrick Children' }, + { title: 'The Improv: 50 Years Behind the Brick Wall' }, + { title: 'Three Men and a Cradle (3 hommes et un couffin)' }, + { title: 'Rolling Thunder' }, + { title: 'Shopping' }, + { title: 'Mystery of Picasso, The (Le mystère Picasso)' }, + { title: 'Brokedown Palace' }, + { title: 'Dancemaker' }, + { title: 'Way I Spent the End of the World, The (Cum mi-am petrecut sfarsitul lumii)' }, + { title: 'Desperadoes, The' }, + { title: 'Death of a President' }, + { title: 'Adam & Paul' }, + { title: 'Onibaba' }, + { title: 'King of Kings' }, + { title: 'Fly Away (Bis zum Horizont, dann links!)' }, + { title: 'Center of the World, The' }, + { title: 'Mumford' }, + { title: 'Two Lovers' }, + { title: 'Music Box, The' }, + { title: 'Golden Boys, The' }, + { title: 'Nil By Mouth' }, + { title: 'The Stanford Prison Experiment' }, + { title: 'Prison (Fängelse) ' }, + { title: "Hail Mary ('Je vous salue, Marie')" }, + { title: 'The Loft' }, + { title: 'Jim Jefferies: BARE' }, + { title: 'Black Hand' }, + { title: 'Flower & Garnet' }, + { title: 'Scarlet Letter, The (Der scharlachrote Buchstabe)' }, + { title: 'Union, The' }, + { title: 'American Bandits: Frank and Jesse James' }, + { title: 'Café Metropole' }, + { title: 'Tooth & Nail' }, + { title: 'Rebirth' }, + { + title: + 'Black Magic (Meeting at Midnight) (Charlie Chan in Meeting at Midnight) (Charlie Chan in Black Magic)', + }, + { title: 'Police Python 357' }, + { title: 'Son of the White Mare' }, + { title: 'Filly Brown' }, + { title: 'Between the Sheets (Entre Lençóis)' }, + { title: 'Scenic Route' }, + { title: 'Gallowwalkers' }, + { title: 'Living Wake, The' }, + { title: 'Lemonade Joe (Limonádový Joe aneb Konská opera)' }, + { title: 'Winter Passing' }, + { title: 'Crack-Up' }, + { title: 'Saint John of Las Vegas' }, + { title: 'The Hungover Games' }, + { title: 'Real Men' }, + { title: 'Ladies and Gentlemen, the Fabulous Stains (a.k.a. All Washed Up)' }, + { title: 'Stargate: Continuum' }, + { title: 'Jekyll & Hyde... Together Again' }, + { title: 'Shadow of the Holy Book (Pyhän kirjan varjo)' }, + { title: 'Guilty by Suspicion' }, + { title: 'Dark at the Top of the Stairs, The' }, + { title: 'The 3 Rs' }, + { title: 'Last Life in the Universe (Ruang rak noi nid mahasan)' }, + { title: 'Stepmom' }, + { title: 'Day They Robbed the Bank of England, The' }, + { title: 'Them (Ils)' }, + { title: "Bosko's Parlor Pranks" }, + { title: 'Demon Lover Diary' }, + { title: 'I Wish (Kiseki)' }, + { title: 'Top Floor Left Wing (Dernier étage gauche gauche)' }, + { title: 'Our Music (Notre musique)' }, + { title: 'Get the Gringo' }, + { title: 'Busses Roar (Buses Roar)' }, + { title: 'Operation Mad Ball' }, + { title: 'Elaine Stritch: Shoot Me' }, + { title: 'Prisoners of the Lost Universe' }, + { title: 'Leaving Las Vegas' }, + { title: 'Eat' }, + { title: 'Bloody Pit of Horror (Il boia scarlatto) (Virgins for the Hangman)' }, + { title: 'Alex Cross' }, + { title: 'Fly Away' }, + { title: 'Big, Large and Verdone' }, + { title: 'Eureka' }, + { title: 'Looking for Eric' }, + { title: 'That Awkward Moment' }, + { title: 'Neverwhere' }, + { title: 'Ash Wednesday' }, + { title: 'Assassination on the Tiber' }, + { title: 'Ziggy Stardust and the Spiders from Mars' }, + { title: 'In Therapy (Divã)' }, + { title: 'Countdown to Looking Glass' }, + { title: 'High Heels and Low Lifes' }, + { title: 'Green Chair (Noksaek uija)' }, + { title: "It's Good to Be Alive" }, + { title: 'Dead Heat on a Merry-Go-Round' }, + { title: 'YellowBrickRoad' }, + { title: "Nobody's Fool" }, + { title: 'Clueless' }, + { title: 'I as in Icarus (I... comme Icare)' }, + { title: "Iceberg, L'" }, + { title: 'Palindromes' }, + { title: 'Meteor Man, The' }, + { title: 'American Addict' }, + { title: 'Butterflies Have No Memories' }, + { title: 'Made in U.S.A.' }, + { title: 'Haunted Echoes' }, + { title: 'Into the Wild' }, + { title: 'Gun Crazy (a.k.a. Deadly Is the Female)' }, + { title: 'Dressed to Kill' }, + { title: 'Strange Days' }, + { title: 'Bastards of the Party' }, + { title: 'Annie Get Your Gun' }, + { title: 'Sleepless in Seattle' }, + { title: 'Man of the Year' }, + { title: 'Starship Troopers 3: Marauder' }, + { title: 'Bug' }, + { title: 'Unmade Beds' }, + { title: 'Bordertown' }, + { title: 'Costa Brava' }, + { title: 'Dead Man Down' }, + { title: 'Alex in Wonderland' }, + { title: 'Witch Way Love (Un amour de sorcière)' }, + { title: 'Diplomacy (Diplomatie)' }, + { title: 'Wrong Turn' }, + { title: 'Dan in Real Life' }, + { title: 'Coco Chanel' }, + { title: 'Why Man Creates' }, + { title: 'Razortooth' }, + { title: 'Voyage to the End of the Universe (Ikarie XB 1)' }, + { title: 'Yu-Gi-Oh!' }, + { title: 'Ninja' }, + { title: 'Party, The' }, + { title: 'Homeward Bound II: Lost in San Francisco' }, + { title: 'Mister Roberts' }, + { title: 'Kiss Me, Guido' }, + { title: 'Great Sinner, The' }, + { title: 'The Thirteen Assassins' }, + { title: 'King Boxer: Five Fingers of Death (Tian xia di yi quan)' }, + { title: 'Fast and the Furious, The' }, + { title: 'Ichi' }, + { title: "Charm's Incidents (Charms Zwischenfälle)" }, + { title: 'Blue Bird, The' }, + { title: '3 Backyards' }, + { title: 'Man from Laramie, The' }, + { title: 'String, The (Le fil)' }, + { title: 'Milky Way, The (Voie lactée, La)' }, + { title: 'Suicide Club' }, + { title: "Dead Man's Letters (Pisma myortvogo cheloveka)" }, + { title: 'Skylark' }, + { title: 'Everlasting Piece, An' }, + { title: 'Dementia 13' }, + { title: 'Red Squirrel, The (Ardilla roja, La)' }, + { title: 'CrissCross' }, + { title: 'Fat, Sick & Nearly Dead' }, + { title: 'First Daughter' }, + { title: 'Foolish Wives' }, + { title: 'Luther the Geek' }, + { title: 'Bohemian Life, The (La vie de bohème)' }, + { title: 'Beautiful Boy' }, + { title: 'Quarantine' }, + { title: 'Blue Smoke' }, + { title: 'Everybody Street' }, + { title: 'I Thank a Fool' }, + { title: 'Crooked Arrows' }, + { title: 'Celluloid Closet, The' }, + { title: "Child's Play" }, + { title: 'Getting Any? (Minnâ-yatteruka!)' }, + { title: 'Pete Smalls Is Dead' }, + { title: 'Outpost' }, + { title: 'Pax Americana and the Weaponization of Space' }, + { title: 'Arsène Lupin' }, + { title: 'Lola Versus' }, + { title: 'Invention of Lying, The' }, + { title: 'Playing God' }, + { title: 'Seven Thieves' }, + { title: 'History of the Eagles' }, + { title: 'Dentist, The' }, + { title: 'Scandal' }, + { title: 'Prime Suspect 3' }, + { title: 'Chasing Papi (a.k.a. Papi Chulo)' }, + { title: 'Cutting Edge: The Magic of Movie Editing, The' }, + { title: 'Girl in the Cadillac' }, + { title: 'Ten Seconds to Hell' }, + { title: 'Bye Bye Braverman' }, + { title: 'Thief' }, + { title: 'Man of the East' }, + { title: 'Salting the Battlefield' }, + { title: 'Dementia 13' }, + { title: 'Freddy Got Fingered' }, + { title: 'Bugs Bunny / Road Runner Movie, The (a.k.a. The Great American Chase)' }, + { title: 'Roommate, The' }, + { title: 'In Old Arizona' }, + { title: 'Shanghai Surprise' }, + { title: 'I Belong (Som du ser meg)' }, + { title: 'Lost Embrace (Abrazo partido, El)' }, + { title: 'Romance in a Minor Key (Romanze in Moll)' }, + { title: 'Intermezzo' }, + { title: 'Faithful' }, + { title: 'Paris Was a Woman' }, + { title: 'Tank on the Moon' }, + { title: 'Fantastic Night, The (Nuit fantastique, La)' }, + { title: 'Whip Hand, The' }, + { title: 'V.I.P.s, The' }, + { title: 'Flodder' }, + { title: 'Dhoom' }, + { title: 'Visitors, The' }, + { title: 'Earth vs. the Flying Saucers' }, + { title: 'Bucky Larson: Born to Be a Star' }, + { title: 'Get Shorty' }, + { title: 'Houseboat' }, + { title: 'Forget Me Not' }, + { title: 'Rambo: First Blood Part II' }, + { title: 'Blast' }, + { title: "Combat dans L'Ile, Le (Fire and Ice)" }, + { title: 'Crucified Lovers, The (Chikamatsu monogatari)' }, + { title: 'Antique (Sayangkoldong yangkwajajeom aentikeu)' }, + { title: 'Shoot to Kill' }, + { title: 'Haunted Honeymoon' }, + { title: 'Cremaster 5' }, + { title: 'Bourne Ultimatum, The' }, + { title: 'Day in the Country, A (Partie de campagne)' }, + { title: 'Frankenweenie' }, + { title: "Gulliver's Travels" }, + { title: 'Now You See Me' }, + { title: 'Alan Partridge: Alpha Papa' }, + { title: 'Billy Two Hats (Lady and the Outlaw, The)' }, + { title: 'Fear, The' }, + { title: 'Un vampiro para dos' }, + { title: 'Troy' }, + { title: 'After the Rehearsal (Efter repetitionen)' }, + { title: 'Naughty Room, The' }, + { title: 'Battle Royale (Batoru rowaiaru)' }, + { title: 'Four Feathers, The' }, + { title: 'Drumline' }, +]; From 6a6f4e4e4f01d9257dc6bf8f9d0ab35edcf31ff5 Mon Sep 17 00:00:00 2001 From: ben awad Date: Fri, 15 Sep 2017 11:15:20 -0500 Subject: [PATCH 29/31] merging accounts --- index.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++++-- models/user.js | 37 +++++++++++++++++++----------- package.json | 1 + resolvers.js | 19 ++++++++++++---- yarn.lock | 4 ++++ 5 files changed, 104 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index c313ca4..46b34ef 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,4 @@ +import 'dotenv/config'; import express from 'express'; import bodyParser from 'body-parser'; import { graphiqlExpress, graphqlExpress } from 'graphql-server-express'; @@ -8,11 +9,13 @@ import { createServer } from 'http'; import { execute, subscribe } from 'graphql'; import { SubscriptionServer } from 'subscriptions-transport-ws'; import joinMonsterAdapt from 'join-monster-graphql-tools-adapter'; +import passport from 'passport'; +import FacebookStrategy from 'passport-facebook'; import typeDefs from './schema'; import resolvers from './resolvers'; import models from './models'; -import { refreshTokens } from './auth'; +import { createTokens, refreshTokens } from './auth'; import joinMonsterMetadata from './joinMonsterMetadata'; const schema = makeExecutableSchema({ @@ -27,6 +30,61 @@ const SECRET_2 = 'ajsdklfjaskljgklasjoiquw01982310nlksas;sdlkfj'; const app = express(); +passport.use( + new FacebookStrategy( + { + clientID: process.env.FACEBOOK_CLIENT_ID, + clientSecret: process.env.FACEBOOK_CLIENT_SECRET, + callbackURL: 'http://localhost:3000/auth/facebook/callback', + scope: ['email'], + profileFields: ['id', 'emails'], + }, + async (accessToken, refreshToken, profile, cb) => { + // 2 cases + // #1 first time login + // #2 previously logged in with facebook + // #3 previously registered with email + const { id, emails: [{ value }] } = profile; + // [] + let fbUser = await models.User.findOne({ + where: { $or: [{ fbId: id }, { email: value }] }, + }); + + console.log(fbUser); + console.log(profile); + + if (!fbUser) { + // case #1 + fbUser = await models.User.create({ + fbId: id, + email: value, + }); + } else if (!fbUser.fbId) { + // case #3 + // add email to user + await fbUser.update({ + fbId: id, + }); + } + + cb(null, fbUser); + }, + ), +); + +app.use(passport.initialize()); + +app.get('/flogin', passport.authenticate('facebook')); + +app.get( + '/auth/facebook/callback', + passport.authenticate('facebook', { session: false }), + async (req, res) => { + const [token, refreshToken] = await createTokens(req.user, SECRET, SECRET_2); + res.redirect(`http://localhost:3001/home?token=${token}&refreshToken=${refreshToken}`); + }, +); + const addUser = async (req, res, next) => { const token = req.headers['x-token']; if (token) { @@ -73,7 +131,7 @@ app.use( const server = createServer(app); -models.sequelize.sync().then(() => +models.sequelize.sync({ force: true }).then(() => server.listen(3000, () => { new SubscriptionServer( { diff --git a/models/user.js b/models/user.js index 0e751a5..096e3e4 100644 --- a/models/user.js +++ b/models/user.js @@ -1,19 +1,30 @@ export default (sequelize, DataTypes) => { - const User = sequelize.define('User', { - username: { - type: DataTypes.STRING, - unique: true, + const User = sequelize.define( + 'User', + { + fbId: DataTypes.BIGINT(20), + username: { + type: DataTypes.STRING, + unique: true, + }, + email: { + type: DataTypes.STRING, + unique: true, + }, + password: DataTypes.STRING, + isAdmin: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, }, - email: { - type: DataTypes.STRING, - unique: true, + { + indexes: [ + { + fields: ['"fbId"'], + }, + ], }, - password: DataTypes.STRING, - isAdmin: { - type: DataTypes.BOOLEAN, - defaultValue: false, - }, - }); + ); User.associate = (models) => { // 1 to many with board diff --git a/package.json b/package.json index 38164f3..8275a7f 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "body-parser": "^1.17.2", "cors": "^2.8.3", "dataloader": "^1.3.0", + "dotenv": "^4.0.0", "express": "^4.15.3", "graphql": "^0.10.3", "graphql-server-express": "^0.8.0", diff --git a/resolvers.js b/resolvers.js index f0c1dab..da25702 100644 --- a/resolvers.js +++ b/resolvers.js @@ -136,11 +136,22 @@ export default { return userAdded; }, register: async (parent, args, { models }) => { - const hashedPassword = await bcrypt.hash(args.password, 12); - const user = await models.User.create({ - ...args, - password: hashedPassword, + const previousAccount = await models.User.findOne({ + where: { $and: [{ email: args.email }, { password: { $eq: null } }] }, }); + const hashedPassword = await bcrypt.hash(args.password, 12); + let user = null; + if (previousAccount) { + previousAccount.update({ + username: args.username, + password: hashedPassword, + }); + } else { + user = await models.User.create({ + ...args, + password: hashedPassword, + }); + } return user; }, login: async (parent, { email, password }, { models, SECRET, SECRET_2 }) => diff --git a/yarn.lock b/yarn.lock index 6e63f4a..72cd3de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1131,6 +1131,10 @@ doctrine@^2.0.0: esutils "^2.0.2" isarray "^1.0.0" +dotenv@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" + dottie@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dottie/-/dottie-2.0.0.tgz#da191981c8b8d713ca0115d5898cf397c2f0ddd0" From d68eb66e1ec8c4651a223abd2cb3bfd5d9d855c8 Mon Sep 17 00:00:00 2001 From: ben awad Date: Fri, 15 Sep 2017 11:28:29 -0500 Subject: [PATCH 30/31] make sure to return user --- resolvers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resolvers.js b/resolvers.js index da25702..32f3ad6 100644 --- a/resolvers.js +++ b/resolvers.js @@ -142,7 +142,7 @@ export default { const hashedPassword = await bcrypt.hash(args.password, 12); let user = null; if (previousAccount) { - previousAccount.update({ + user = await previousAccount.update({ username: args.username, password: hashedPassword, }); From 629c52ad4cdda1cf5e871921a670071d5a93afeb Mon Sep 17 00:00:00 2001 From: Ben Awad Date: Fri, 15 Sep 2017 12:19:15 -0500 Subject: [PATCH 31/31] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d42c22..77b2a44 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # Template for an Express Server with GraphQL -[Watch the video to learn how it was made.](https://youtu.be/-0mT8N19dLY) +[Watch the video to learn how it was made.](https://youtu.be/X6pNLEOs-10)