From 4d082165aa4b2f6dfad7ea66d609a0c3c12fbe71 Mon Sep 17 00:00:00 2001 From: Jessie Rushing Date: Fri, 15 Mar 2019 16:31:59 -0700 Subject: [PATCH 1/3] update tokenUserNotFound docs --- docs/source/api/server.md | 54 +++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/docs/source/api/server.md b/docs/source/api/server.md index 8ead459f9..fd154cdf8 100644 --- a/docs/source/api/server.md +++ b/docs/source/api/server.md @@ -229,28 +229,30 @@ For more information, see the [Apollo Docs](https://github.com/apollographql/gra ### tokenUserNotFound ```js -tokenUserNotFound: async ({jwt, token}) => { - let profile = await someExternalService(token); - if (!profile) { - return null; - } - let user = await UserModel.findOneAndUpdate({ - id: profile.id - }, { - id: profile.id, - username: profile.username, - lowercaseUsername: profile.username.toLowerCase(), - roles: [], - profiles: [] - }, { - setDefaultsOnInsert: true, - new: true, - upsert: true - }); +const Users = require('services/users'); - return user; -} +tokenUserNotFound: async ({ jwt }) => { + + const user = await Users.upsertExternalUser( + null, jwt.sub, jwt.iss, jwt.username) + + const email = jwt.email.toLowerCase(); + + //upsertExternalUser will also create a profile with provider:jwt.iss, id:jwt.sub + //add a "local" profile to persist email on the user + user.profiles.push({ + provider: "local", + id: email, + }); + user.created_at = new Date(jwt.memberSince*1000); + + await user.save(); + + return user; + + + } ``` The `tokenUserNotFound` hook allows auth integrations to hook into the event @@ -258,8 +260,16 @@ when a valid token is provided but a user can't be found in the database that matches the provided id. The function is async, and should return the user object that was created in the -database, or null if the user wasn't found. The `jwt` parameter of the object -is the unpacked token, while `token` is the original jwt token string. +database. The `jwt` parameter of the object is the unpacked token, which has already been validated. + +More details about the `upsertExternalUser` method can be found at: +https://github.com/coralproject/talk/blob/32962aa1e84c09d87141440a1f04cbd1659b3336/services/users.js/#L592 + +In this example, a unix timestamp was included on the jwt with a date that is being persisted on the user. This is optional, but can be used to handle any extra claims you want to include on the jwt. You can use the metadata object on the user to store custom values. + +Having trouble implementing your own `tokenUserNotFound` hook? +Submit a Support ticket ([support@coralproject.net](mailto:support@coralproject.net)) + ### tags From c875a8d9edc368e9d558e19e9568a079821107f4 Mon Sep 17 00:00:00 2001 From: Jessie Rushing Date: Mon, 18 Mar 2019 17:04:09 -0700 Subject: [PATCH 2/3] added note about subdomain whitelisting --- docs/source/03-06-product-guide-configuring-talk.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/source/03-06-product-guide-configuring-talk.md b/docs/source/03-06-product-guide-configuring-talk.md index 2176c7454..c38e59aed 100644 --- a/docs/source/03-06-product-guide-configuring-talk.md +++ b/docs/source/03-06-product-guide-configuring-talk.md @@ -88,8 +88,9 @@ Comments with suspect words will display until a moderator takes action on them. #### Permitted Domains -A list of domains where your Talk instance is allowed to be embedded. Typical -use is `localhost`, `staging.yourdomain.com`, `yourdomain.com`, etc. +A list of domains where your Talk instance is allowed to be embedded. List each domain and subdomain in use +separately, for example: `localhost`, `staging.yourdomain.com`, `yourdomain.com`, `www.yourdomain.com`, etc. + #### Embed Script From 12c4cfbbf6237eb56b46dc4a0da5c8671a65a80b Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 19 Mar 2019 16:22:59 -0600 Subject: [PATCH 3/3] feat: improved server api docs --- docs/source/api/server.md | 403 ++++++++++++++++++++++---------------- 1 file changed, 235 insertions(+), 168 deletions(-) diff --git a/docs/source/api/server.md b/docs/source/api/server.md index fd154cdf8..e9628b23d 100644 --- a/docs/source/api/server.md +++ b/docs/source/api/server.md @@ -56,16 +56,32 @@ Thanks to [gql-merge](https://www.npmjs.com/package/gql-merge) the contents of definitions. `enum`'s will be appended to, types will be appended, and new types will be added. +This can be included as a plugin via: + +```js +const fs = require("fs"); +const path = require("path"); + +module.exports = { + typeDefs: fs.readFileSync(path.join(__dirname, "typeDefs.graphql"), "utf8") +}; +``` + +Assuming the above GraphQL schema is located in the plugin folder at +`typeDefs.graphql`. + ### context ```js -{ - Slack: (context) => ({ - notify: (message) => { - // return a promise after we're done sending notifications. - } - }) -} +module.exports = { + context: { + Slack: context => ({ + notify: message => { + // return a promise after we're done sending notifications. + }, + }); + }, +}; ``` Any property provided here will be added to the context parameter available @@ -83,41 +99,50 @@ A special context hook, `Sort` will allow plugin authors to provide new methods to sort data. An example is as follows: ```js -{ - Sort: () => ({ - Comments: { // <-- (1) - likes: { // <-- (2) - startCursor(ctx, nodes, {cursor}) { // <-- (3) - return cursor != null ? cursor : 0; - }, - endCursor(ctx, nodes, {cursor}) { // <-- (4) - return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null; - }, - sort(ctx, query, {cursor, sort}) { // <-- (5) - if (cursor) { - query = query.skip(cursor); - } +module.exports = { + context: { + Sort: () => ({ + Comments: { + // <-- (1) + likes: { + // <-- (2) + startCursor(ctx, nodes, { cursor }) { + // <-- (3) + return cursor != null ? cursor : 0; + }, + endCursor(ctx, nodes, { cursor }) { + // <-- (4) + return nodes.length + ? (cursor != null ? cursor : 0) + nodes.length + : null; + }, + sort(ctx, query, { cursor, sort }) { + // <-- (5) + if (cursor) { + query = query.skip(cursor); + } - return query.sort({ - 'action_counts.like': sort === 'DESC' ? -1 : 1, - created_at: sort === 'DESC' ? -1 : 1, - }); - }, - }, - }, - }), -} + return query.sort({ + "action_counts.like": sort === "DESC" ? -1 : 1, + created_at: sort === "DESC" ? -1 : 1 + }); + } + } + } + }) + } +}; ``` This has a bunch of special features: 1. `Comments` is the name of the type being sorted, this is pluralized and - capitalized. + capitalized. 2. `likes` is the `sortBy` field in lowercase. 3. `startCursor` will retrieve the start cursor based on the current set of - nodes and the current cursor. + nodes and the current cursor. 4. `endCursor` will retrieve the end cursor based on the current set of nodes - and the current cursor. + and the current cursor. 5. `sort` will mutate the `query` to apply the sort operations. All the `startCursor`, `endCursor`, and `sort` functions must be provided in @@ -126,11 +151,13 @@ order for the sorting to apply properly. ### loaders ```js -(context) => ({ - People: { - load: () => db.people.find({user: context.user}) - } -}) +module.exports = { + context: context => ({ + People: { + load: () => db.people.find({ user: context.user }) + } + }) +}; ``` Loaders should be provided as a function which returns a map which is used in @@ -139,13 +166,15 @@ the resolvers function. These must return a promise or a value. ### mutators ```js -(context) => ({ - People: { - create: (name) => { - return db.people.insert({user: context.user, name}); +module.exports = { + context: context => ({ + People: { + create: name => { + return db.people.insert({ user: context.user, name }); + } } - } -}) + }) +}; ``` Mutators should be provided as a function which returns a map which is used in @@ -154,27 +183,41 @@ the resolvers function. These must return a promise or a value. ### resolvers ```js -{ - Person: { - name(obj, args, context) { - return obj.name; +module.exports = { + resolvers: { + Person: { + name(obj, args, context) { + return obj.name; + }, + colour(obj, args, context) { + // Bill likes the colour red, everyone else likes blue. + return obj.name === "bill" ? "RED" : "BLUE"; + } }, - colour(obj, args, context) { - // Bill likes the colour red, everyone else likes blue. - return obj.name === 'bill' ? 'RED' : 'BLUE'; - } - }, - RootQuery: { - people(obj, args, {loaders: {People}}) { - return People.load(); - } - }, - RootMutation: { - createPerson(obj, {name}, {mutators: {People}}) { - return People.create(name); + RootQuery: { + people( + obj, + args, + { + loaders: { People } + } + ) { + return People.load(); + } + }, + RootMutation: { + createPerson( + obj, + { name }, + { + mutators: { People } + } + ) { + return People.create(name); + } } } -} +}; ``` Should return a resolver map as described in the @@ -185,40 +228,44 @@ This will merge with the existing resolvers in core and from previous plugins. ### hooks ```js -{ - RootMutation: { - createPerson: { - post: async (obj, args, {plugins: {Slack}}, info, person) { - if (!person) { +module.exports = { + hooks: { + RootMutation: { + createPerson: { + post: async (obj, args, {plugins: {Slack}}, info, person) { + if (!person) { + return person; + } + + await Slack.notify(`A new person just was created with name ${person.name}`); + return person; } - - await Slack.notify(`A new person just was created with name ${person.name}`); - - return person; } } } -} +}; ``` Hooks here are pretty special, for each resolver field, you can specify a pre/post hook that will execute pre and post field resolution. If your post function accepts four parameters, then it can modify the field -result. It is *required* that the function resolves a promise (or returns) with +result. It is _required_ that the function resolves a promise (or returns) with the modified value or simply the original if you didn't modify it. ### setupFunctions ```js -setupFunctions: { - leader: (options, args) => ({ - leader: { - filter: (person) => person.place === 1 - }, - }), -} +module.exports = { + setupFunctions: { + leader: (options, args) => ({ + leader: { + filter: person => person.place === 1 + } + }) + } +}; ``` Setup functions allow you to create filters that control which pubsub.publish() events @@ -229,30 +276,32 @@ For more information, see the [Apollo Docs](https://github.com/apollographql/gra ### tokenUserNotFound ```js +const Users = require("services/users"); -const Users = require('services/users'); +module.exports = { + tokenUserNotFound: async ({ jwt }) => { + const user = await Users.upsertExternalUser( + null, + jwt.sub, + jwt.iss, + jwt.username + ); -tokenUserNotFound: async ({ jwt }) => { - - const user = await Users.upsertExternalUser( - null, jwt.sub, jwt.iss, jwt.username) + const email = jwt.email.toLowerCase(); - const email = jwt.email.toLowerCase(); + //upsertExternalUser will also create a profile with provider:jwt.iss, id:jwt.sub + //add a "local" profile to persist email on the user + user.profiles.push({ + provider: "local", + id: email + }); + user.created_at = new Date(jwt.memberSince * 1000); - //upsertExternalUser will also create a profile with provider:jwt.iss, id:jwt.sub - //add a "local" profile to persist email on the user - user.profiles.push({ - provider: "local", - id: email, - }); - user.created_at = new Date(jwt.memberSince*1000); - - await user.save(); - - return user; - - - } + await user.save(); + + return user; + } +}; ``` The `tokenUserNotFound` hook allows auth integrations to hook into the event @@ -265,11 +314,10 @@ database. The `jwt` parameter of the object is the unpacked token, which has alr More details about the `upsertExternalUser` method can be found at: https://github.com/coralproject/talk/blob/32962aa1e84c09d87141440a1f04cbd1659b3336/services/users.js/#L592 -In this example, a unix timestamp was included on the jwt with a date that is being persisted on the user. This is optional, but can be used to handle any extra claims you want to include on the jwt. You can use the metadata object on the user to store custom values. +In this example, a unix timestamp was included on the jwt with a date that is being persisted on the user. This is optional, but can be used to handle any extra claims you want to include on the jwt. You can use the metadata object on the user to store custom values. -Having trouble implementing your own `tokenUserNotFound` hook? +Having trouble implementing your own `tokenUserNotFound` hook? Submit a Support ticket ([support@coralproject.net](mailto:support@coralproject.net)) - ### tags @@ -278,18 +326,20 @@ or enabled by code). Below is an example pulled from the core off topic plugin on how to create a hook for the `OFF_TOPIC` name: ```js -[ - { - name: 'OFF_TOPIC', - permissions: { - public: true, - self: true, - roles: [] - }, - models: ['COMMENTS'], - created_at: new Date() - } -] +module.exports = { + tags: [ + { + name: "OFF_TOPIC", + permissions: { + public: true, + self: true, + roles: [] + }, + models: ["COMMENTS"], + created_at: new Date() + } + ] +}; ``` You can refer to `models/schema/tag.js` for the available schema to match when @@ -298,11 +348,13 @@ creating models to enable/disable specific features. ### router ```js -(router) => { - router.get('/api/v1/people', (req, res) => { - res.json({people: [{name: 'Bob'}]}); - }); -} +module.exports = { + router: router => { + router.get("/api/v1/people", (req, res) => { + res.json({ people: [{ name: "Bob" }] }); + }); + } +}; ``` The Router hook allows you to create a function that accepts the base express @@ -312,56 +364,73 @@ action needed by external applications. ### passport ```js -const FacebookStrategy = require('passport-facebook').Strategy; -const UsersService = require('services/users'); -const {ValidateUserLogin, HandleAuthPopupCallback} = require('services/passport'); +const FacebookStrategy = require("passport-facebook").Strategy; +const UsersService = require("services/users"); +const { + ValidateUserLogin, + HandleAuthPopupCallback +} = require("services/passport"); module.exports = { - passport(passport) { - passport.use(new FacebookStrategy({ - clientID: process.env.TALK_FACEBOOK_APP_ID, - clientSecret: process.env.TALK_FACEBOOK_APP_SECRET, - callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`, - passReqToCallback: true, - profileFields: ['id', 'displayName', 'picture.type(large)'] - }, async (req, accessToken, refreshToken, profile, done) => { + passport: passport => { + passport.use( + new FacebookStrategy( + { + clientID: process.env.TALK_FACEBOOK_APP_ID, + clientSecret: process.env.TALK_FACEBOOK_APP_SECRET, + callbackURL: `${ + process.env.TALK_ROOT_URL + }/api/v1/auth/facebook/callback`, + passReqToCallback: true, + profileFields: ["id", "displayName", "picture.type(large)"] + }, + async (req, accessToken, refreshToken, profile, done) => { + let user; + try { + const { id, provider, displayName } = profile; + user = await UsersService.upsertSocialUser( + req.context, + id, + provider, + displayName + ); + } catch (err) { + return done(err); + } - let user; - try { - const { id, provider, displayName } = profile; - user = await UsersService.upsertSocialUser( - req.context, - id, - provider, - displayName - ); - } catch (err) { - return done(err); - } - - return ValidateUserLogin(profile, user, done); - })); + return ValidateUserLogin(profile, user, done); + } + ) + ); }, - router(router) { - + router: router => { // Note that we have to import the passport instance here, it is // instantiated after all the strategies have been mounted. - const {passport} = require('services/passport'); + const { passport } = require("services/passport"); /** * Facebook auth endpoint, this will redirect the user immediately to facebook * for authorization. */ - router.get('/facebook', passport.authenticate('facebook', {display: 'popup', authType: 'rerequest', scope: ['public_profile']})); + router.get( + "/facebook", + passport.authenticate("facebook", { + display: "popup", + authType: "rerequest", + scope: ["public_profile"] + }) + ); /** * Facebook callback endpoint, this will send the user a html page designed to * send back the user credentials upon successful login. */ - router.get('/facebook/callback', (req, res, next) => { - + router.get("/facebook/callback", (req, res, next) => { // Perform the facebook login flow and pass the data back through the opener. - passport.authenticate('facebook', HandleAuthPopupCallback(req, res, next))(req, res, next); + passport.authenticate( + "facebook", + HandleAuthPopupCallback(req, res, next) + )(req, res, next); }); } }; @@ -370,10 +439,10 @@ module.exports = { ### translations ```js -const path = require('path'); +const path = require("path"); module.exports = { - translations: path.join(__dirname, 'translations.yml'), + translations: path.join(__dirname, "translations.yml") }; ``` @@ -387,11 +456,11 @@ en: Which overrides the copy for the `embedlink.copy` template. You can also provide other languages as well by using the correct language -prefix. +prefix. -When creating a plugin using this `translations` hook to override copy +When creating a plugin using this `translations` hook to override copy from another plugin, be sure to list it after the plugin it's overriding -in the `plugins.json` file. +in the `plugins.json` file. ### websockets @@ -402,12 +471,12 @@ module.exports = { // Do something with the connection params or connection, like // logging it out, or incrementing a metric. }, - onDisconnect: (connection) => { + onDisconnect: connection => { // Do something with the connection params or connection, like // logging it out, or decrementing a metric. - }, - }, -} + } + } +}; ``` This `websockets` hook can be used to attach methods to the @@ -421,12 +490,13 @@ connections. module.exports = { schemaLevelResolveFunction: (root, args, ctx, info) => { // The GraphQL Operation Name. Example: CoralEmbedStream_Embed - const name = info.operation.name !== null ? info.operation.name.value : null; + const name = + info.operation.name !== null ? info.operation.name.value : null; // Maybe increment a metric based on the operation name... // You must _always_ return the root. return root; - }, + } }; ``` @@ -441,9 +511,7 @@ Contents of `plugins.json`: ```json { - "server": [ - "people" - ] + "server": ["people"] } ``` @@ -537,5 +605,4 @@ module.exports = { } } }; - ```