From e36d1ef7efc59ec32e3f02cf57139bbd16d131b7 Mon Sep 17 00:00:00 2001 From: Theodor Diaconu Date: Wed, 28 Mar 2018 06:52:40 +0300 Subject: [PATCH] initial commit, code taken from: https://github.com/orionsoft/meteor-apollo-accounts --- .gitignore | 1 + .versions | 62 +++++++++++ CHANGELOG.md | 80 ++++++++++++++ README.md | 1 + package.js | 39 +++++++ src/Auth.js | 32 ++++++ src/LoginMethodResponse.js | 11 ++ src/Mutation/changePassword.js | 5 + src/Mutation/createUser.js | 15 +++ src/Mutation/forgotPassword.js | 8 ++ src/Mutation/hashPassword.js | 131 +++++++++++++++++++++++ src/Mutation/index.js | 29 +++++ src/Mutation/loginWithPassword.js | 34 ++++++ src/Mutation/logout.js | 12 +++ src/Mutation/oauth/getUserLoginMethod.js | 18 ++++ src/Mutation/oauth/hasService.js | 19 ++++ src/Mutation/oauth/index.js | 38 +++++++ src/Mutation/oauth/loginWithFacebook.js | 33 ++++++ src/Mutation/oauth/loginWithGoogle.js | 37 +++++++ src/Mutation/oauth/loginWithLinkedIn.js | 56 ++++++++++ src/Mutation/oauth/resolver.js | 35 ++++++ src/Mutation/resendVerificationEmail.js | 8 ++ src/Mutation/resetPassword.js | 5 + src/Mutation/verifyEmail.js | 5 + src/Mutations.js | 63 +++++++++++ src/callMethod.js | 22 ++++ src/getConnection.js | 10 ++ src/index.js | 29 +++++ 28 files changed, 838 insertions(+) create mode 100644 .gitignore create mode 100644 .versions create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 package.js create mode 100644 src/Auth.js create mode 100644 src/LoginMethodResponse.js create mode 100644 src/Mutation/changePassword.js create mode 100644 src/Mutation/createUser.js create mode 100644 src/Mutation/forgotPassword.js create mode 100644 src/Mutation/hashPassword.js create mode 100644 src/Mutation/index.js create mode 100644 src/Mutation/loginWithPassword.js create mode 100644 src/Mutation/logout.js create mode 100644 src/Mutation/oauth/getUserLoginMethod.js create mode 100644 src/Mutation/oauth/hasService.js create mode 100644 src/Mutation/oauth/index.js create mode 100644 src/Mutation/oauth/loginWithFacebook.js create mode 100644 src/Mutation/oauth/loginWithGoogle.js create mode 100644 src/Mutation/oauth/loginWithLinkedIn.js create mode 100644 src/Mutation/oauth/resolver.js create mode 100644 src/Mutation/resendVerificationEmail.js create mode 100644 src/Mutation/resetPassword.js create mode 100644 src/Mutation/verifyEmail.js create mode 100644 src/Mutations.js create mode 100644 src/callMethod.js create mode 100644 src/getConnection.js create mode 100644 src/index.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.versions b/.versions new file mode 100644 index 0000000..740aec8 --- /dev/null +++ b/.versions @@ -0,0 +1,62 @@ +accounts-base@1.2.17 +accounts-oauth@1.1.15 +allow-deny@1.0.5 +babel-compiler@6.18.2 +babel-runtime@1.0.1 +base64@1.0.10 +binary-heap@1.0.10 +blaze@2.3.2 +blaze-tools@1.0.10 +boilerplate-generator@1.0.11 +callback-hook@1.0.10 +check@1.2.5 +ddp@1.2.5 +ddp-client@1.3.4 +ddp-common@1.2.8 +ddp-rate-limiter@1.0.7 +ddp-server@1.3.14 +deps@1.0.12 +diff-sequence@1.0.7 +ecmascript@0.7.3 +ecmascript-runtime@0.3.15 +ejson@1.0.13 +geojson-utils@1.0.10 +html-tools@1.0.11 +htmljs@1.0.11 +http@1.2.12 +id-map@1.0.9 +jquery@1.11.10 +local-test:nicolaslopezj:apollo-accounts@3.2.2 +localstorage@1.0.12 +logging@1.1.17 +meteor@1.6.1 +minimongo@1.0.23 +modules@0.8.2 +modules-runtime@0.7.10 +mongo@1.1.17 +mongo-id@1.0.6 +nicolaslopezj:apollo-accounts@3.2.2 +npm-bcrypt@0.9.2 +npm-mongo@2.2.24 +oauth@1.1.13 +oauth2@1.1.11 +observe-sequence@1.0.16 +ordered-dict@1.0.9 +promise@0.8.8 +random@1.0.10 +rate-limit@1.0.8 +reactive-var@1.0.11 +reload@1.1.11 +retry@1.0.9 +routepolicy@1.0.12 +service-configuration@1.0.11 +spacebars@1.0.13 +spacebars-compiler@1.1.0 +tinytest@1.0.12 +tmeasday:check-npm-versions@0.3.1 +tracker@1.1.3 +ui@1.0.12 +underscore@1.0.10 +url@1.1.0 +webapp@1.3.15 +webapp-hashing@1.0.9 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..173f5a8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,80 @@ +# Changelog + +### v3.2.3 + +* Changed graphql-loader to graphql-load to have more control + +### v3.1.0 + +* Use http instead of meteor to make Facebook login. + +### v3.0.1 + +* Fix bug with `tmeasday:check-npm-versions`. + +### v3.0.0 + +* Use [orionsoft:graphql-loader](https://github.com/orionsoft/graphql-loader) to initialize package. +* Add user field to LoginMethodResponse [#23](https://github.com/nicolaslopezj/meteor-apollo-accounts/issues/23). + +### v2.2.0 + +* Use async in all mutations. + +### v2.1.0 + +* Add plainPassword option to createUser. + +### v2.0.0 + +* Make `SchemaMutations`, `SchemaTypes` and `Resolvers` functions that receive options. + +To migrate change: + +* `SchemaMutations` to `SchemaMutations()` +* `SchemaTypes` to `SchemaTypes()` +* `Resolvers` to `Resolvers()` + +### v1.4.0 + +* Login with linkedin. + +### v1.3.7 + +* Fix destroyToken in logout. + +### v1.3.6 + +* Fix error throwing on `loginWithPassword`. + +### v1.3.5 + +* Fix production `standard-minifier-js` error. + +### v1.3.4 + +* Add some oauth dependencies to only require installation of accounts-xx. +* Fix oauth login not throwing errors. + +### v1.3.3 + +* Add `accounts-password` to weak dependencies. + +### v1.3.2 + +* Fix `Mutation.createUser defined in resolvers, but not in schema` + +### v1.3.1 + +* Don't use graphql-compiler +* Add conditional to password service mutations. + +### v1.3.0 + +* Pass login method in error message when user tries to log in with the incorrect service. + +### v1.2.0 + +* Conditional mutation if service is installed +* Accounts facebook +* Accounts google diff --git a/README.md b/README.md new file mode 100644 index 0000000..7c01d52 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Meteor Apollo Accounts diff --git a/package.js b/package.js new file mode 100644 index 0000000..bc96e4f --- /dev/null +++ b/package.js @@ -0,0 +1,39 @@ +/* global Package */ + +Package.describe({ + name: 'cultofcoders:apollo-accounts', + version: '3.2.3', + // Brief, one-line summary of the package. + summary: 'Meteor accounts in GraphQL', + // URL to the Git repository containing the source code for this package. + git: 'https://github.com/cultofcoders/meteor-apollo-accounts', + // By default, Meteor will default to using README.md for documentation. + // To avoid submitting documentation, set this field to null. + documentation: 'README.md', +}); + +Package.onUse(function(api) { + api.versionsFrom('1.4.1.2'); + + api.use( + [ + 'tmeasday:check-npm-versions@0.3.1', + 'check', + 'accounts-base', + 'oauth2', + 'npm-bcrypt', + 'random', + 'ecmascript', + 'http', + 'random', + 'oauth', + 'service-configuration', + 'accounts-oauth', + ], + 'server' + ); + + api.mainModule('src/index.js', 'server'); +}); + +Package.onTest(function(api) {}); diff --git a/src/Auth.js b/src/Auth.js new file mode 100644 index 0000000..c3cd5d1 --- /dev/null +++ b/src/Auth.js @@ -0,0 +1,32 @@ +export default function (options) { + return ` +# Type returned when the user logs in +type LoginMethodResponse { + # Id of the user logged in user + id: String! + # Token of the connection + token: String! + # Expiration date for the token + tokenExpires: Float! + # The logged in user + user: User +} + +input CreateUserProfileInput { + ${options.CreateUserProfileInput} +} + +type SuccessResponse { + # True if it succeeded + success: Boolean +} + +# A hashsed password +input HashedPassword { + # The hashed password + digest: String! + # Algorithm used to hash the password + algorithm: String! +} +` +} diff --git a/src/LoginMethodResponse.js b/src/LoginMethodResponse.js new file mode 100644 index 0000000..da53afd --- /dev/null +++ b/src/LoginMethodResponse.js @@ -0,0 +1,11 @@ +import {Meteor} from 'meteor/meteor' + +export default function (options) { + return { + LoginMethodResponse: { + user ({id}) { + return Meteor.users.findOne(id) + } + } + } +} diff --git a/src/Mutation/changePassword.js b/src/Mutation/changePassword.js new file mode 100644 index 0000000..b2a7d53 --- /dev/null +++ b/src/Mutation/changePassword.js @@ -0,0 +1,5 @@ +import callMethod from '../callMethod' + +export default async function (root, {oldPassword, newPassword}, context) { + return callMethod(context, 'changePassword', oldPassword, newPassword) +} diff --git a/src/Mutation/createUser.js b/src/Mutation/createUser.js new file mode 100644 index 0000000..c30e38c --- /dev/null +++ b/src/Mutation/createUser.js @@ -0,0 +1,15 @@ +import callMethod from '../callMethod' +import hashPassword from './hashPassword' +import {Meteor} from 'meteor/meteor' + +export default async function (root, options, context) { + Meteor._nodeCodeMustBeInFiber() + if (!options.password && !options.plainPassword) { + throw new Error('Password is required') + } + if (!options.password) { + options.password = hashPassword(options.plainPassword) + delete options.plainPassword + } + return callMethod(context, 'createUser', options) +} diff --git a/src/Mutation/forgotPassword.js b/src/Mutation/forgotPassword.js new file mode 100644 index 0000000..c7184ff --- /dev/null +++ b/src/Mutation/forgotPassword.js @@ -0,0 +1,8 @@ +import callMethod from '../callMethod' + +export default async function (root, {email}, context) { + callMethod(context, 'forgotPassword', {email}) + return { + success: true + } +} diff --git a/src/Mutation/hashPassword.js b/src/Mutation/hashPassword.js new file mode 100644 index 0000000..b45b22f --- /dev/null +++ b/src/Mutation/hashPassword.js @@ -0,0 +1,131 @@ +/* eslint-disable */ + +export const SHA256 = function (s) { + var chrsz = 8; + var hexcase = 0; + + function safe_add (x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + } + + function S (X, n) { return ( X >>> n ) | (X << (32 - n)); } + function R (X, n) { return ( X >>> n ); } + function Ch(x, y, z) { return ((x & y) ^ ((~x) & z)); } + function Maj(x, y, z) { return ((x & y) ^ (x & z) ^ (y & z)); } + function Sigma0256(x) { return (S(x, 2) ^ S(x, 13) ^ S(x, 22)); } + function Sigma1256(x) { return (S(x, 6) ^ S(x, 11) ^ S(x, 25)); } + function Gamma0256(x) { return (S(x, 7) ^ S(x, 18) ^ R(x, 3)); } + function Gamma1256(x) { return (S(x, 17) ^ S(x, 19) ^ R(x, 10)); } + + function core_sha256 (m, l) { + var K = [0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0xFC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x6CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2]; + var HASH = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]; + var W = new Array(64); + var a, b, c, d, e, f, g, h; + var T1, T2; + + m[l >> 5] |= 0x80 << (24 - l % 32); + m[((l + 64 >> 9) << 4) + 15] = l; + + for ( let i = 0; i>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32); + } + return bin; + } + + function Utf8Encode(string) { + // METEOR change: + // The webtoolkit.info version of this code added this + // Utf8Encode function (which does seem necessary for dealing + // with arbitrary Unicode), but the following line seems + // problematic: + // + // string = string.replace(/\r\n/g,"\n"); + var utftext = ""; + + for (var n = 0; n < string.length; n++) { + + var c = string.charCodeAt(n); + + if (c < 128) { + utftext += String.fromCharCode(c); + } + else if((c > 127) && (c < 2048)) { + utftext += String.fromCharCode((c >> 6) | 192); + utftext += String.fromCharCode((c & 63) | 128); + } + else { + utftext += String.fromCharCode((c >> 12) | 224); + utftext += String.fromCharCode(((c >> 6) & 63) | 128); + utftext += String.fromCharCode((c & 63) | 128); + } + + } + + return utftext; + } + + function binb2hex (binarray) { + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i++) { + str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + + hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); + } + return str; + } + + s = Utf8Encode(s); + return binb2hex(core_sha256(str2binb(s), s.length * chrsz)); +} + +export default function (password) { + return { + digest: SHA256(password), + algorithm: 'sha-256' + } +} diff --git a/src/Mutation/index.js b/src/Mutation/index.js new file mode 100644 index 0000000..a3a3c72 --- /dev/null +++ b/src/Mutation/index.js @@ -0,0 +1,29 @@ +import loginWithPassword from './loginWithPassword' +import logout from './logout' +import changePassword from './changePassword' +import createUser from './createUser' +import verifyEmail from './verifyEmail' +import resendVerificationEmail from './resendVerificationEmail' +import forgotPassword from './forgotPassword' +import resetPassword from './resetPassword' +import oauth from './oauth' +import hasService from './oauth/hasService' + +export default function (options) { + const resolvers = { + logout, + verifyEmail, + resendVerificationEmail, + ...oauth(options) + } + + if (hasService(options, 'password')) { + resolvers.loginWithPassword = loginWithPassword + resolvers.changePassword = changePassword + resolvers.createUser = createUser + resolvers.forgotPassword = forgotPassword + resolvers.resetPassword = resetPassword + } + + return {Mutation: resolvers} +} diff --git a/src/Mutation/loginWithPassword.js b/src/Mutation/loginWithPassword.js new file mode 100644 index 0000000..b8a6fb6 --- /dev/null +++ b/src/Mutation/loginWithPassword.js @@ -0,0 +1,34 @@ +import callMethod from '../callMethod' +import hashPassword from './hashPassword' +import getUserLoginMethod from './oauth/getUserLoginMethod' +import {Meteor} from 'meteor/meteor' + +export default async function (root, {username, email, password, plainPassword}, context) { + if (!password && !plainPassword) { + throw new Error('Password is required') + } + if (!password) { + password = hashPassword(plainPassword) + } + + const methodArguments = { + user: email ? {email} : {username}, + password: password + } + try { + return callMethod(context, 'login', methodArguments) + } catch (error) { + if (error.reason === 'User has no password set') { + const method = getUserLoginMethod(email || username) + if (method === 'no-password') { + throw new Meteor.Error('no-password', 'User has no password set, go to forgot password') + } else if (method) { + throw new Error(`User is registered with ${method}.`) + } else { + throw new Error('User has no login methods') + } + } else { + throw error + } + } +} diff --git a/src/Mutation/logout.js b/src/Mutation/logout.js new file mode 100644 index 0000000..d78175c --- /dev/null +++ b/src/Mutation/logout.js @@ -0,0 +1,12 @@ +import {Accounts} from 'meteor/accounts-base' +import getConnection from '../getConnection' + +export default async function (root, {token}, context) { + if (token && context.userId) { + const hashedToken = Accounts._hashLoginToken(token) + Accounts.destroyToken(context.userId, hashedToken) + } + const connection = getConnection() + Accounts._successfulLogout(connection, context.userId) + return { success: true } +} diff --git a/src/Mutation/oauth/getUserLoginMethod.js b/src/Mutation/oauth/getUserLoginMethod.js new file mode 100644 index 0000000..780d489 --- /dev/null +++ b/src/Mutation/oauth/getUserLoginMethod.js @@ -0,0 +1,18 @@ +import {Accounts} from 'meteor/accounts-base' + +export default function (email) { + if (!email) return 'unknown' + const {services} = email.indexOf('@') !== -1 ? Accounts.findUserByEmail(email) : Accounts.findUserByUsername(email) + const list = [] + for (const key in services) { + if (key === 'email') continue + if (key === 'resume') continue + if (key === 'password' && !services.password.bcrypt) { + list.push('no-password') + } else { + list.push(key) + } + } + const allowedServices = [...Accounts.oauth.serviceNames(), 'password'] + return list.filter(service => allowedServices.indexOf(service) !== -1).join(', ') +} diff --git a/src/Mutation/oauth/hasService.js b/src/Mutation/oauth/hasService.js new file mode 100644 index 0000000..b92b7ba --- /dev/null +++ b/src/Mutation/oauth/hasService.js @@ -0,0 +1,19 @@ +export default function (options, service) { + if (service === 'facebook') { + return options.loginWithFacebook + } + + if (service === 'google') { + return options.loginWithGoogle + } + + if (service === 'password') { + return options.loginWithPassword + } + + if (service === 'linkedin') { + return options.loginWithLinkedIn + } + + return false +} diff --git a/src/Mutation/oauth/index.js b/src/Mutation/oauth/index.js new file mode 100644 index 0000000..d0f90a9 --- /dev/null +++ b/src/Mutation/oauth/index.js @@ -0,0 +1,38 @@ +import loginWithFacebook from './loginWithFacebook' +import loginWithGoogle from './loginWithGoogle' +import loginWithLinkedIn from './loginWithLinkedIn' +import hasService from './hasService' +import {Accounts} from 'meteor/accounts-base' + +export default function (options) { + const oauth = {} + + if (hasService(options, 'facebook')) { + oauth.loginWithFacebook = loginWithFacebook + try { + Accounts.oauth.registerService('facebook') + } catch (error) { + // dont log this error + } + } + + if (hasService(options, 'google')) { + oauth.loginWithGoogle = loginWithGoogle + try { + Accounts.oauth.registerService('google') + } catch (error) { + // dont log this error + } + } + + if (hasService(options, 'linkedin')) { + oauth.loginWithLinkedIn = loginWithLinkedIn + try { + Accounts.oauth.registerService('linkedin') + } catch (error) { + // dont log this error + } + } + + return oauth +} diff --git a/src/Mutation/oauth/loginWithFacebook.js b/src/Mutation/oauth/loginWithFacebook.js new file mode 100644 index 0000000..0450194 --- /dev/null +++ b/src/Mutation/oauth/loginWithFacebook.js @@ -0,0 +1,33 @@ +import resolver from './resolver' +import {HTTP} from 'meteor/http' + +const handleAuthFromAccessToken = function ({accessToken}) { + const identity = getIdentity(accessToken) + + const serviceData = { + ...identity, + accessToken + } + + return { + serviceName: 'facebook', + serviceData, + options: {profile: {name: identity.name}} + } +} + +const getIdentity = function (accessToken) { + const fields = ['id', 'email', 'name', 'first_name', 'last_name', 'link', 'gender', 'locale', 'age_range'] + try { + return HTTP.get('https://graph.facebook.com/v2.8/me', { + params: { + access_token: accessToken, + fields: fields.join(',') + } + }).data + } catch (err) { + throw new Error('Failed to fetch identity from Google. ' + err.message) + } +} + +export default resolver(handleAuthFromAccessToken) diff --git a/src/Mutation/oauth/loginWithGoogle.js b/src/Mutation/oauth/loginWithGoogle.js new file mode 100644 index 0000000..caa16ac --- /dev/null +++ b/src/Mutation/oauth/loginWithGoogle.js @@ -0,0 +1,37 @@ +import resolver from './resolver' +import {HTTP} from 'meteor/http' + +const handleAuthFromAccessToken = function ({accessToken}) { + const scopes = getScopes(accessToken) + const identity = getIdentity(accessToken) + + const serviceData = { + ...identity, + accessToken, + scopes + } + + return { + serviceName: 'google', + serviceData, + options: {profile: {name: identity.name}} + } +} + +const getIdentity = function (accessToken) { + try { + return HTTP.get('https://www.googleapis.com/oauth2/v1/userinfo', {params: {access_token: accessToken}}).data + } catch (err) { + throw new Error('Failed to fetch identity from Google. ' + err.message) + } +} + +const getScopes = function (accessToken) { + try { + return HTTP.get('https://www.googleapis.com/oauth2/v1/tokeninfo', {params: {access_token: accessToken}}).data.scope.split(' ') + } catch (err) { + throw new Error('Failed to fetch tokeninfo from Google. ' + err.message) + } +} + +export default resolver(handleAuthFromAccessToken) diff --git a/src/Mutation/oauth/loginWithLinkedIn.js b/src/Mutation/oauth/loginWithLinkedIn.js new file mode 100644 index 0000000..d0ed64e --- /dev/null +++ b/src/Mutation/oauth/loginWithLinkedIn.js @@ -0,0 +1,56 @@ +import resolver from './resolver' +import {HTTP} from 'meteor/http' +import {ServiceConfiguration} from 'meteor/service-configuration' + +const handleAuthFromAccessToken = function ({code, redirectUri}) { + // works with anything also... + const accessToken = getAccessToken(code, redirectUri) + const identity = getIdentity(accessToken) + + const serviceData = { + ...identity, + accessToken + } + + return { + serviceName: 'linkedin', + serviceData, + options: {profile: {name: `${identity.firstName} ${identity.lastName}`}} + } +} + +const getTokens = function () { + const result = ServiceConfiguration.configurations.findOne({service: 'linkedin'}) + return { + client_id: result.clientId, + client_secret: result.secret + } +} + +const getAccessToken = function (code, redirectUri) { + const response = HTTP.post('https://www.linkedin.com/oauth/v2/accessToken', { + params: { + grant_type: 'authorization_code', + code, + redirect_uri: redirectUri, + ...getTokens() + } + }).data + + return response.access_token +} + +const getIdentity = function (accessToken) { + try { + return HTTP.get('https://www.linkedin.com/v1/people/~:(id,email-address,first-name,last-name,headline)', { + params: { + oauth2_access_token: accessToken, + format: 'json' + } + }).data + } catch (err) { + throw new Error('Failed to fetch identity from LinkedIn. ' + err.message) + } +} + +export default resolver(handleAuthFromAccessToken) diff --git a/src/Mutation/oauth/resolver.js b/src/Mutation/oauth/resolver.js new file mode 100644 index 0000000..86b181d --- /dev/null +++ b/src/Mutation/oauth/resolver.js @@ -0,0 +1,35 @@ +import callMethod from '../../callMethod' +import getUserLoginMethod from './getUserLoginMethod' +import {Random} from 'meteor/random' +import {OAuth} from 'meteor/oauth' +import {Meteor} from 'meteor/meteor' + +export default function (handleAuthFromAccessToken) { + return async function (root, params, context) { + const oauthResult = handleAuthFromAccessToken(params) + // Why any token works? :/ + const credentialToken = Random.secret() + const credentialSecret = Random.secret() + + OAuth._storePendingCredential(credentialToken, oauthResult, credentialSecret) + + const oauth = {credentialToken, credentialSecret} + try { + return callMethod(context, 'login', {oauth}) + } catch (error) { + if (error.reason === 'Email already exists.') { + const email = oauthResult.serviceData.email || oauthResult.serviceData.emailAddress + const method = getUserLoginMethod(email) + if (method === 'no-password') { + throw new Meteor.Error('no-password', 'User has no password set, go to forgot password') + } else if (method) { + throw new Error(`User is registered with ${method}.`) + } else { + throw new Error('User has no login methods') + } + } else { + throw error + } + } + } +} diff --git a/src/Mutation/resendVerificationEmail.js b/src/Mutation/resendVerificationEmail.js new file mode 100644 index 0000000..2396916 --- /dev/null +++ b/src/Mutation/resendVerificationEmail.js @@ -0,0 +1,8 @@ +import {Accounts} from 'meteor/accounts-base' + +export default async function (root, {email}, {userId}) { + Accounts.sendVerificationEmail(userId, email) + return { + success: true + } +} diff --git a/src/Mutation/resetPassword.js b/src/Mutation/resetPassword.js new file mode 100644 index 0000000..ac0b054 --- /dev/null +++ b/src/Mutation/resetPassword.js @@ -0,0 +1,5 @@ +import callMethod from '../callMethod' + +export default async function (root, {token, newPassword}, context) { + return callMethod(context, 'resetPassword', token, newPassword) +} diff --git a/src/Mutation/verifyEmail.js b/src/Mutation/verifyEmail.js new file mode 100644 index 0000000..b4ef870 --- /dev/null +++ b/src/Mutation/verifyEmail.js @@ -0,0 +1,5 @@ +import callMethod from '../callMethod' + +export default async function (root, {token}, context) { + return callMethod(context, 'verifyEmail', token) +} diff --git a/src/Mutations.js b/src/Mutations.js new file mode 100644 index 0000000..e757fea --- /dev/null +++ b/src/Mutations.js @@ -0,0 +1,63 @@ +import hasService from './Mutation/oauth/hasService' + +export default function (options) { + const mutations = [] + + if (hasService(options, 'password')) { + mutations.push(` + type Mutation { + # Log the user in with a password. + loginWithPassword (username: String, email: String, password: HashedPassword, plainPassword: String): LoginMethodResponse + + # Create a new user. + createUser (username: String, email: String, password: HashedPassword, plainPassword: String, profile: CreateUserProfileInput): LoginMethodResponse + + # Change the current user's password. Must be logged in. + changePassword (oldPassword: HashedPassword!, newPassword: HashedPassword!): SuccessResponse + + # Request a forgot password email. + forgotPassword (email: String!): SuccessResponse + + # Reset the password for a user using a token received in email. Logs the user in afterwards. + resetPassword (newPassword: HashedPassword!, token: String!): LoginMethodResponse + }`) + } + + mutations.push(` + type Mutation { + # Log the user out. + logout (token: String!): SuccessResponse + + # Marks the user's email address as verified. Logs the user in afterwards. + verifyEmail (token: String!): LoginMethodResponse + + # Send an email with a link the user can use verify their email address. + resendVerificationEmail (email: String): SuccessResponse + }`) + + if (hasService(options, 'facebook')) { + mutations.push(` + type Mutation { + # Login the user with a facebook access token + loginWithFacebook (accessToken: String!): LoginMethodResponse + }`) + } + + if (hasService(options, 'google')) { + mutations.push(` + type Mutation { + # Login the user with a facebook access token + loginWithGoogle (accessToken: String!, tokenId: String): LoginMethodResponse + }`) + } + + if (hasService(options, 'linkedin')) { + mutations.push(` + type Mutation { + # Login the user with a facebook access token + loginWithLinkedIn (code: String!, redirectUri: String!): LoginMethodResponse + }`) + } + + return mutations +} diff --git a/src/callMethod.js b/src/callMethod.js new file mode 100644 index 0000000..37f602f --- /dev/null +++ b/src/callMethod.js @@ -0,0 +1,22 @@ +import {Meteor} from 'meteor/meteor' +import getConnection from './getConnection' + +export default function (passedContext, name, ...args) { + const handler = Meteor.default_server.method_handlers[name] + if (!handler) { + throw new Meteor.Error(404, `Method '${name}' not found`) + } + + const connection = getConnection() + const context = { + connection, + setUserId (userId) { + /** + * This will not make any changes if you don\'t pass setUserId function in context + */ + }, + ...passedContext + } + + return handler.call(context, ...args) +} diff --git a/src/getConnection.js b/src/getConnection.js new file mode 100644 index 0000000..5fd6a6c --- /dev/null +++ b/src/getConnection.js @@ -0,0 +1,10 @@ +import {Random} from 'meteor/random' + +export default function () { + return { + id: Random.id(), + close () { + // nothing to close here + } + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..9bb1eaf --- /dev/null +++ b/src/index.js @@ -0,0 +1,29 @@ +import SchemaTypes from './Auth'; +import SchemaMutations from './Mutations'; +import Mutation from './Mutation'; +import LoginMethodResponse from './LoginMethodResponse'; +import callMethod from './callMethod'; + +const initAccounts = function(givenOptions) { + const defaultOptions = { + CreateUserProfileInput: 'name: String', + loginWithFacebook: false, + loginWithGoogle: false, + loginWithLinkedIn: false, + loginWithPassword: true, + }; + const options = { + ...defaultOptions, + ...givenOptions, + }; + + const typeDefs = [SchemaTypes(options), ...SchemaMutations(options)]; + const resolvers = { ...Mutation(options), ...LoginMethodResponse(options) }; + + return { + typeDefs, + resolvers, + }; +}; + +export { callMethod, initAccounts };