├── .env.sample ├── .eslintrc.js ├── .github └── CODE_OF_CONDUCT.md ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── config └── default.json ├── data └── .gitkeep ├── index.js ├── lib ├── message.js ├── users.js └── util.js ├── package-lock.json ├── package.json ├── public └── app.css ├── routers ├── api.js └── web.js └── views ├── association.pug ├── home.pug ├── includes ├── footer.pug ├── header.pug ├── loginForm.pug └── logoutForm.pug ├── layout.pug └── register.pug /.env.sample: -------------------------------------------------------------------------------- 1 | SLACK_VERIFICATION_TOKEN='REPLACE_ME' 2 | SLACK_BOT_TOKEN='REPLACE_ME' 3 | SESSION_SECRET='REPLACE_ME' 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "airbnb-base", 3 | rules: { 4 | 'comma-dangle': ['error', { 5 | arrays: 'always-multiline', 6 | objects: 'always-multiline', 7 | imports: 'always-multiline', 8 | exports: 'always-multiline', 9 | functions: 'never', 10 | }], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Introduction 4 | 5 | Diversity and inclusion make our community strong. We encourage participation from the most varied and diverse backgrounds possible and want to be very clear about where we stand. 6 | 7 | Our goal is to maintain a safe, helpful and friendly community for everyone, regardless of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other defining characteristic. 8 | 9 | This code and related procedures also apply to unacceptable behavior occurring outside the scope of community activities, in all community venues (online and in-person) as well as in all one-on-one communications, and anywhere such behavior has the potential to adversely affect the safety and well-being of community members. 10 | 11 | For more information on our code of conduct, please visit [https://slackhq.github.io/code-of-conduct](https://slackhq.github.io/code-of-conduct) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | 3 | /data/* 4 | !/data/.gitkeep 5 | /config/* 6 | !/config/default.json 7 | 8 | *.log 9 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/boron 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Slack Technologies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Account Binding Template 2 | 3 | A Sample Slack app that shows how a user account on Slack can be bound to an account on another system. 4 | 5 | ![account-binding](https://user-images.githubusercontent.com/700173/27056630-b57cd40c-4f7d-11e7-98f1-7e723f472192.gif) 6 | 7 | ## Setup 8 | 9 | #### Create a Slack app 10 | 11 | 1. Create an app at api.slack.com/apps 12 | 1. Click on `Bot Users` 13 | 1. Add a bot user and make sure it displays as always online 14 | 1. Install the app and copy the `xoxb-` token 15 | 16 | #### Run locally or [![Remix on Glitch](https://cdn.glitch.com/2703baf2-b643-4da7-ab91-7ee2a2d00b5b%2Fremix-button.svg)](https://glitch.com/edit/#!/remix/slack-account-binding-blueprint) 17 | 1. Get the code 18 | * Either clone this repo and run `npm install` 19 | * Or visit https://glitch.com/edit/#!/remix/slack-account-binding-blueprint 20 | 1. Set the following environment variables to `.env` (see `.env.sample`): 21 | * `SLACK_BOT_TOKEN`: Your app's `xoxb-` token (available on the Install App page) 22 | * `SLACK_VERIFICATION_TOKEN`: Your app's Verification Token (available on the Basic Information page) 23 | * `SESSION_SECRET`: A randomly generated secret for your session storage 24 | 1. If you're running the app locally: 25 | 1. Start the app (`npm start`) 26 | 1. In another windown, start ngrok on the same port as your webserver (`ngrok http $PORT`) 27 | 28 | #### Add Slash Commands 29 | 1. Go back to the app settings and click on Slash Commands 30 | 1. Add the following Slash Commands: 31 | * Command: /read-message 32 | * Request URL: ngrok or Glitch URL + api/slack/command 33 | * Description: Read secret message 34 | * Command: /write-message 35 | * Request URL: ngrok or Glitch URL + api/slack/command 36 | * Description: Write secret message 37 | * Usage Hint: [message] 38 | 1. Reinstall the app by navigating to the Install App page 39 | 40 | #### In Slack 41 | 42 | 1. In any channel, run /read-message 43 | 1. You should see a DM from the bot asking you to link your accounts 44 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | // "server" value must adopt the interface of a URL Object 3 | // (https://nodejs.org/dist/latest-v6.x/docs/api/url.html#url_url_strings_and_url_objects) 4 | "server": { 5 | "protocol": "http", 6 | "hostname": "localhost", 7 | "port": 3000 8 | }, 9 | "session": { 10 | // one day 11 | "maxAge": 86400000, 12 | // HTTPS-only cookies: set to `true` or the number of proxies to trust 13 | "secure": false 14 | }, 15 | "routes": { 16 | "associationPath": "/link/slack" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slack-samples/javascript-account-binding/8e38891fe8f66db044b1cfad9cc24d03b83fed3b/data/.gitkeep -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const http = require('http'); 3 | const config = require('config'); 4 | const express = require('express'); 5 | const level = require('level'); 6 | const sublevel = require('level-sublevel'); 7 | 8 | const usersFactory = require('./lib/users'); 9 | const messageFactory = require('./lib/message'); 10 | const webRouterFactory = require('./routers/web'); 11 | const apiRouterFactory = require('./routers/api'); 12 | 13 | const db = sublevel(level('./data/db')); 14 | 15 | const users = usersFactory(db.sublevel('users')); 16 | // message represents a shared resource that users must be authenticated/authorized to access 17 | const message = messageFactory(db.sublevel('message'), users); 18 | 19 | const webRouter = webRouterFactory(db, users, message); 20 | const apiRouter = apiRouterFactory(users, message); 21 | 22 | const app = express(); 23 | 24 | app.set('trust proxy', config.has('session.secure') ? config.get('session.secure') : false); 25 | app.set('view engine', 'pug'); 26 | 27 | app.use('/api', apiRouter); 28 | app.use('/', webRouter); 29 | 30 | const server = http.createServer(app); 31 | const port = config.has('server.internalPort') ? config.get('server.internalPort') : config.get('server.port'); 32 | 33 | // eslint-disable-next-line no-console 34 | server.listen(port, () => { console.log(`server listening on port ${port}`); }); 35 | -------------------------------------------------------------------------------- /lib/message.js: -------------------------------------------------------------------------------- 1 | const messageKey = 'MESSAGE'; 2 | const initialValue = 'Hello World'; 3 | const selfCredential = Symbol('self'); 4 | 5 | module.exports = (db, users) => { 6 | // Basic authorization system. Your app might choose ACLs or some more sophisticated mechanism. 7 | function isUser(credential) { 8 | return users.findById(credential.id).then(() => true).catch(() => false); 9 | } 10 | 11 | function authorizeSelfOrUser(credential) { 12 | let authorizationPromise; 13 | if (credential === selfCredential) { 14 | authorizationPromise = Promise.resolve(true); 15 | } else { 16 | authorizationPromise = isUser(credential); 17 | } 18 | return authorizationPromise; 19 | } 20 | 21 | return { 22 | initialize() { 23 | return this.setMessage(initialValue, selfCredential); 24 | }, 25 | 26 | getMessage(credential = {}) { 27 | return authorizeSelfOrUser(credential) 28 | .then((isAuthorized) => { 29 | if (isAuthorized) { 30 | return new Promise((resolve, reject) => { 31 | db.get(messageKey, (error, message) => { 32 | if (error) { 33 | if (error.notFound) { 34 | resolve(this.initialize()); 35 | } else { 36 | reject(error); 37 | } 38 | } else { 39 | resolve(message); 40 | } 41 | }); 42 | }); 43 | } 44 | throw new Error('Not Authorized'); 45 | }); 46 | }, 47 | 48 | setMessage(newMessage, credential = {}) { 49 | return authorizeSelfOrUser(credential) 50 | .then(isAuthorized => new Promise((resolve, reject) => { 51 | if (isAuthorized) { 52 | db.put(messageKey, newMessage, (error) => { 53 | if (error) { 54 | reject(error); 55 | } else { 56 | resolve(newMessage); 57 | } 58 | }); 59 | } else { 60 | reject(new Error('Not Authorized')); 61 | } 62 | })); 63 | }, 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /lib/users.js: -------------------------------------------------------------------------------- 1 | const url = require('url'); 2 | const config = require('config'); 3 | const uuid = require('uuid'); 4 | const randomstring = require('randomstring'); 5 | const bcrypt = require('bcrypt'); 6 | const SlackWebClient = require('@slack/client').WebClient; 7 | 8 | // The shape of a user object: 9 | // { 10 | // id: '', 11 | // username: '', 12 | // passwordHash: '', 13 | // slack: { 14 | // id: '', 15 | // dmChannelId: '', 16 | // }, 17 | // } 18 | 19 | const slack = new SlackWebClient(process.env.SLACK_BOT_TOKEN); 20 | 21 | function dataStoreOperation(store, op, ...params) { 22 | return new Promise((resolve, reject) => { 23 | params.push({ valueEncoding: 'json' }); 24 | params.push((error, result) => { 25 | if (error) return reject(error); 26 | return resolve(result); 27 | }); 28 | store[op](...params); 29 | }); 30 | } 31 | 32 | module.exports = (db) => { 33 | const userStore = db.sublevel('userlist'); 34 | const linkStore = db.sublevel('associationlinklist'); 35 | 36 | function searchUsers(predicate) { 37 | return new Promise((resolve, reject) => { 38 | let foundItem; 39 | userStore.createValueStream({ valueEncoding: 'json' }) 40 | .on('data', function onData(item) { 41 | if (!foundItem && predicate(item)) { 42 | foundItem = item; 43 | this.destroy(); 44 | resolve(foundItem); 45 | } 46 | }) 47 | .on('close', () => { 48 | if (!foundItem) { 49 | const notFoundError = new Error('User not found'); 50 | notFoundError.code = 'EUSERNOTFOUND'; 51 | reject(notFoundError); 52 | } 53 | }) 54 | .on('error', reject); 55 | }); 56 | } 57 | 58 | return { 59 | findById(id) { 60 | return dataStoreOperation(userStore, 'get', id) 61 | .catch((error) => { 62 | if (error.notFound) { 63 | const notFoundError = new Error('User not found'); 64 | notFoundError.code = 'EUSERNOTFOUND'; 65 | throw notFoundError; 66 | } 67 | throw error; 68 | }); 69 | }, 70 | 71 | setById(id, user) { 72 | return dataStoreOperation(userStore, 'put', id, user).then(() => user); 73 | }, 74 | 75 | findByUsername(username) { 76 | return searchUsers(user => user.username === username); 77 | }, 78 | 79 | findBySlackId(slackId) { 80 | return searchUsers(user => user.slack && user.slack.id === slackId); 81 | }, 82 | 83 | checkPassword(userId, password) { 84 | return this.findById(userId) 85 | .then(user => new Promise((resolve, reject) => { 86 | bcrypt.compare(password, user.passwordHash, (hashError, res) => { 87 | if (hashError) { 88 | reject(hashError); 89 | } else { 90 | resolve(!!res); 91 | } 92 | }); 93 | })); 94 | }, 95 | 96 | register({ username, password }) { 97 | // Validations 98 | // NOTE: more validations would be necessary for a production app, such as password length 99 | // and/or complexity 100 | if (!username) { 101 | return Promise.reject(new Error('A username is required')); 102 | } 103 | if (!password) { 104 | return Promise.reject(new Error('A password is required')); 105 | } 106 | 107 | return this.findByUsername(username) 108 | .then(() => Promise.reject(new Error('The username is not available'))) 109 | .catch((findError) => { 110 | if (findError.code !== 'EUSERNOTFOUND') { 111 | throw findError; 112 | } 113 | return new Promise((resolve, reject) => { 114 | bcrypt.hash(password, 10, (hashError, passwordHash) => { 115 | if (hashError) { 116 | reject(hashError); 117 | } 118 | const user = { 119 | id: uuid(), 120 | username, 121 | passwordHash, 122 | }; 123 | resolve(this.setById(user.id, user)); 124 | }); 125 | }); 126 | }); 127 | }, 128 | 129 | beginSlackAssociation(slackUserId) { 130 | const associationLink = { 131 | ref: randomstring.generate(), 132 | slackUserId, 133 | }; 134 | return slack.im.open(slackUserId) 135 | .then((r) => { 136 | associationLink.dmChannelId = r.channel.id; 137 | const authUrl = config.get('server'); 138 | authUrl.pathname = config.get('routes.associationPath'); 139 | authUrl.query = { ref: associationLink.ref }; 140 | const slackMessage = slack.chat.postMessage(associationLink.dmChannelId, 141 | 'Hello, new friend! I think it\'s time we introduce ourselves. I\'m a bot that helps you access your internal protected resources.', 142 | { 143 | attachments: [ 144 | { 145 | text: `<${url.format(authUrl)}|Click here> to introduce yourself to me by authenticating.`, 146 | }, 147 | ], 148 | } 149 | ); 150 | const saveLink = dataStoreOperation(linkStore, 'put', associationLink.ref, associationLink); 151 | return Promise.all([slackMessage, saveLink]); 152 | }) 153 | .then(() => associationLink.ref); 154 | }, 155 | 156 | completeSlackAssociation(userId, associationRef) { 157 | return dataStoreOperation(linkStore, 'get', associationRef) 158 | .catch((error) => { 159 | if (error.notFound) { 160 | throw new Error('The user association link was not valid.'); 161 | } else { 162 | throw error; 163 | } 164 | }) 165 | .then(associationLink => Promise.all([ 166 | this.findById(userId) 167 | .then(user => this.setById(userId, Object.assign(user, { 168 | slack: { id: associationLink.slackUserId, dmChannelId: associationLink.dmChannelId }, 169 | }))), 170 | slack.chat.postMessage(associationLink.dmChannelId, 'Well, it\'s nice to meet you! Thanks for completing authentication.'), 171 | ])) 172 | .then(() => dataStoreOperation(linkStore, 'del', associationRef)); 173 | }, 174 | }; 175 | }; 176 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const pkg = require('../package.json'); 3 | 4 | exports.packageIdentifier = () => `${pkg.name.replace('/', ':')}/${pkg.version} ${os.platform()}/${os.release()} node/${process.version.replace('v', '')}`; 5 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@slack/template-account-binding", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/helper-validator-identifier": { 8 | "version": "7.12.11", 9 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", 10 | "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" 11 | }, 12 | "@babel/parser": { 13 | "version": "7.13.9", 14 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.9.tgz", 15 | "integrity": "sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw==" 16 | }, 17 | "@babel/types": { 18 | "version": "7.13.0", 19 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.0.tgz", 20 | "integrity": "sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==", 21 | "requires": { 22 | "@babel/helper-validator-identifier": "^7.12.11", 23 | "lodash": "^4.17.19", 24 | "to-fast-properties": "^2.0.0" 25 | }, 26 | "dependencies": { 27 | "lodash": { 28 | "version": "4.17.21", 29 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 30 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 31 | } 32 | } 33 | }, 34 | "@slack/client": { 35 | "version": "3.10.0", 36 | "resolved": "https://registry.npmjs.org/@slack/client/-/client-3.10.0.tgz", 37 | "integrity": "sha1-3WodPiG77DTwSYD2UGa7J27eQPw=" 38 | }, 39 | "assert-never": { 40 | "version": "1.2.1", 41 | "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz", 42 | "integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==" 43 | }, 44 | "axios": { 45 | "version": "0.16.2", 46 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz", 47 | "integrity": "sha1-uk+S8XFn37q0CYN4VFS5rBScPG0=" 48 | }, 49 | "babel-walk": { 50 | "version": "3.0.0-canary-5", 51 | "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", 52 | "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", 53 | "requires": { 54 | "@babel/types": "^7.9.6" 55 | } 56 | }, 57 | "bcrypt": { 58 | "version": "1.0.2", 59 | "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-1.0.2.tgz", 60 | "integrity": "sha1-0F/F0iMXPg4o7DgcDwDMJf+vJzY=" 61 | }, 62 | "body-parser": { 63 | "version": "1.17.2", 64 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.17.2.tgz", 65 | "integrity": "sha1-+IkqvI+eYn1Crtr7yma/WrmRBO4=", 66 | "dependencies": { 67 | "debug": { 68 | "version": "2.6.7", 69 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", 70 | "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=" 71 | } 72 | } 73 | }, 74 | "character-parser": { 75 | "version": "2.2.0", 76 | "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", 77 | "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=" 78 | }, 79 | "config": { 80 | "version": "1.26.1", 81 | "resolved": "https://registry.npmjs.org/config/-/config-1.26.1.tgz", 82 | "integrity": "sha1-9kfOMsNF6AunOo6qeppLTlspDKE=" 83 | }, 84 | "connect-flash": { 85 | "version": "0.1.1", 86 | "resolved": "https://registry.npmjs.org/connect-flash/-/connect-flash-0.1.1.tgz", 87 | "integrity": "sha1-2GMPJtlaf4UfmVax6MxnMvO2qjA=" 88 | }, 89 | "csurf": { 90 | "version": "1.9.0", 91 | "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.9.0.tgz", 92 | "integrity": "sha1-SdLGkl/87Ht95VlZfBU/pTM2QTM=", 93 | "dependencies": { 94 | "http-errors": { 95 | "version": "1.5.1", 96 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.1.tgz", 97 | "integrity": "sha1-eIwNLB3iyBuebowBhDtrl+uSB1A=" 98 | }, 99 | "setprototypeof": { 100 | "version": "1.0.2", 101 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.2.tgz", 102 | "integrity": "sha1-gaVSFB7BBLiOic44MQOtXGZWTQg=" 103 | } 104 | } 105 | }, 106 | "doctypes": { 107 | "version": "1.1.0", 108 | "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", 109 | "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" 110 | }, 111 | "dotenv": { 112 | "version": "4.0.0", 113 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-4.0.0.tgz", 114 | "integrity": "sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0=" 115 | }, 116 | "eslint": { 117 | "version": "3.19.0", 118 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", 119 | "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", 120 | "dev": true 121 | }, 122 | "eslint-config-airbnb-base": { 123 | "version": "11.2.0", 124 | "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-11.2.0.tgz", 125 | "integrity": "sha1-GancRIGib3CQRUXsBAEWh2AY+FM=", 126 | "dev": true 127 | }, 128 | "eslint-plugin-import": { 129 | "version": "2.3.0", 130 | "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.3.0.tgz", 131 | "integrity": "sha1-N8gB4K2g4pbL3yDD85OstbUq82s=", 132 | "dev": true, 133 | "dependencies": { 134 | "doctrine": { 135 | "version": "1.5.0", 136 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", 137 | "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=" 138 | } 139 | } 140 | }, 141 | "express": { 142 | "version": "4.15.3", 143 | "resolved": "https://registry.npmjs.org/express/-/express-4.15.3.tgz", 144 | "integrity": "sha1-urZdDwOqgMNYQIly/HAPkWlEtmI=", 145 | "dependencies": { 146 | "debug": { 147 | "version": "2.6.7", 148 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", 149 | "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=" 150 | } 151 | } 152 | }, 153 | "express-session": { 154 | "version": "1.15.3", 155 | "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.15.3.tgz", 156 | "integrity": "sha1-21RfBDWnsbIorgLagZf2UUFzXGc=", 157 | "dependencies": { 158 | "debug": { 159 | "version": "2.6.7", 160 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", 161 | "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=" 162 | } 163 | } 164 | }, 165 | "is-core-module": { 166 | "version": "2.2.0", 167 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", 168 | "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", 169 | "requires": { 170 | "has": "^1.0.3" 171 | }, 172 | "dependencies": { 173 | "function-bind": { 174 | "version": "1.1.1", 175 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 176 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 177 | }, 178 | "has": { 179 | "version": "1.0.3", 180 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 181 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 182 | "requires": { 183 | "function-bind": "^1.1.1" 184 | } 185 | } 186 | } 187 | }, 188 | "js-stringify": { 189 | "version": "1.0.2", 190 | "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", 191 | "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" 192 | }, 193 | "jstransformer": { 194 | "version": "1.0.0", 195 | "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", 196 | "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=" 197 | }, 198 | "level": { 199 | "version": "1.7.0", 200 | "resolved": "https://registry.npmjs.org/level/-/level-1.7.0.tgz", 201 | "integrity": "sha1-Q0ZKOounOy895WokKSgFFG2iE6E=" 202 | }, 203 | "level-session-store": { 204 | "version": "2.0.1", 205 | "resolved": "https://registry.npmjs.org/level-session-store/-/level-session-store-2.0.1.tgz", 206 | "integrity": "sha1-/LXfYe0q8jVg9+H6JZf1TEmZVEE=" 207 | }, 208 | "level-sublevel": { 209 | "version": "6.6.1", 210 | "resolved": "https://registry.npmjs.org/level-sublevel/-/level-sublevel-6.6.1.tgz", 211 | "integrity": "sha1-+ad/dSGrcKj46S7VbyGjx4hqRIU=", 212 | "dependencies": { 213 | "abstract-leveldown": { 214 | "version": "0.12.4", 215 | "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-0.12.4.tgz", 216 | "integrity": "sha1-KeGOYy5g5OIh1YECR4UqY9ey5BA=", 217 | "dependencies": { 218 | "xtend": { 219 | "version": "3.0.0", 220 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", 221 | "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=" 222 | } 223 | } 224 | }, 225 | "bl": { 226 | "version": "0.8.2", 227 | "resolved": "https://registry.npmjs.org/bl/-/bl-0.8.2.tgz", 228 | "integrity": "sha1-yba8oI0bwuoA/Ir7Txpf0eHGbk4=" 229 | }, 230 | "deferred-leveldown": { 231 | "version": "0.2.0", 232 | "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-0.2.0.tgz", 233 | "integrity": "sha1-LO8fER4cV4cNi7uK8mUOWHzS9bQ=" 234 | }, 235 | "isarray": { 236 | "version": "0.0.1", 237 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 238 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" 239 | }, 240 | "levelup": { 241 | "version": "0.19.1", 242 | "resolved": "https://registry.npmjs.org/levelup/-/levelup-0.19.1.tgz", 243 | "integrity": "sha1-86anIFJyxLXzXkEv8ASgOgrt9Qs=", 244 | "dependencies": { 245 | "xtend": { 246 | "version": "3.0.0", 247 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", 248 | "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=" 249 | } 250 | } 251 | }, 252 | "prr": { 253 | "version": "0.0.0", 254 | "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", 255 | "integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=" 256 | }, 257 | "readable-stream": { 258 | "version": "1.0.34", 259 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", 260 | "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=" 261 | }, 262 | "semver": { 263 | "version": "5.1.1", 264 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.1.tgz", 265 | "integrity": "sha1-oykqNz5vPgeY2gsgZBuanFvEfhk=" 266 | }, 267 | "string_decoder": { 268 | "version": "0.10.31", 269 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 270 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" 271 | } 272 | } 273 | }, 274 | "object-assign": { 275 | "version": "4.1.1", 276 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 277 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 278 | }, 279 | "param-case": { 280 | "version": "2.1.1", 281 | "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", 282 | "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=" 283 | }, 284 | "passport": { 285 | "version": "0.3.2", 286 | "resolved": "https://registry.npmjs.org/passport/-/passport-0.3.2.tgz", 287 | "integrity": "sha1-ndAJ+RXo/glbASSgG4+C2gdRAQI=" 288 | }, 289 | "passport-local": { 290 | "version": "1.0.0", 291 | "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", 292 | "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=" 293 | }, 294 | "pug": { 295 | "version": "3.0.1", 296 | "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.1.tgz", 297 | "integrity": "sha512-9v1o2yXMfSKJy2PykKyWUhpgx9Pf9D/UlPgIs2pTTxR6DQZ0oivy4I9f8PlWXRY4sjIhDU4TMJ7hQmYnNJc2bw==", 298 | "requires": { 299 | "pug-code-gen": "^3.0.2", 300 | "pug-filters": "^4.0.0", 301 | "pug-lexer": "^5.0.0", 302 | "pug-linker": "^4.0.0", 303 | "pug-load": "^3.0.0", 304 | "pug-parser": "^6.0.0", 305 | "pug-runtime": "^3.0.0", 306 | "pug-strip-comments": "^2.0.0" 307 | }, 308 | "dependencies": { 309 | "acorn": { 310 | "version": "7.4.1", 311 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", 312 | "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" 313 | }, 314 | "constantinople": { 315 | "version": "4.0.1", 316 | "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", 317 | "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", 318 | "requires": { 319 | "@babel/parser": "^7.6.0", 320 | "@babel/types": "^7.6.1" 321 | } 322 | }, 323 | "is-expression": { 324 | "version": "4.0.0", 325 | "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", 326 | "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", 327 | "requires": { 328 | "acorn": "^7.1.1", 329 | "object-assign": "^4.1.1" 330 | } 331 | }, 332 | "path-parse": { 333 | "version": "1.0.6", 334 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 335 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" 336 | }, 337 | "pug-attrs": { 338 | "version": "3.0.0", 339 | "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", 340 | "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", 341 | "requires": { 342 | "constantinople": "^4.0.1", 343 | "js-stringify": "^1.0.2", 344 | "pug-runtime": "^3.0.0" 345 | } 346 | }, 347 | "pug-code-gen": { 348 | "version": "3.0.2", 349 | "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.2.tgz", 350 | "integrity": "sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==", 351 | "requires": { 352 | "constantinople": "^4.0.1", 353 | "doctypes": "^1.1.0", 354 | "js-stringify": "^1.0.2", 355 | "pug-attrs": "^3.0.0", 356 | "pug-error": "^2.0.0", 357 | "pug-runtime": "^3.0.0", 358 | "void-elements": "^3.1.0", 359 | "with": "^7.0.0" 360 | } 361 | }, 362 | "pug-error": { 363 | "version": "2.0.0", 364 | "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.0.0.tgz", 365 | "integrity": "sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==" 366 | }, 367 | "pug-filters": { 368 | "version": "4.0.0", 369 | "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", 370 | "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", 371 | "requires": { 372 | "constantinople": "^4.0.1", 373 | "jstransformer": "1.0.0", 374 | "pug-error": "^2.0.0", 375 | "pug-walk": "^2.0.0", 376 | "resolve": "^1.15.1" 377 | } 378 | }, 379 | "pug-lexer": { 380 | "version": "5.0.1", 381 | "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", 382 | "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", 383 | "requires": { 384 | "character-parser": "^2.2.0", 385 | "is-expression": "^4.0.0", 386 | "pug-error": "^2.0.0" 387 | } 388 | }, 389 | "pug-linker": { 390 | "version": "4.0.0", 391 | "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", 392 | "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", 393 | "requires": { 394 | "pug-error": "^2.0.0", 395 | "pug-walk": "^2.0.0" 396 | } 397 | }, 398 | "pug-load": { 399 | "version": "3.0.0", 400 | "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", 401 | "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", 402 | "requires": { 403 | "object-assign": "^4.1.1", 404 | "pug-walk": "^2.0.0" 405 | } 406 | }, 407 | "pug-parser": { 408 | "version": "6.0.0", 409 | "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", 410 | "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", 411 | "requires": { 412 | "pug-error": "^2.0.0", 413 | "token-stream": "1.0.0" 414 | } 415 | }, 416 | "pug-runtime": { 417 | "version": "3.0.1", 418 | "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", 419 | "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==" 420 | }, 421 | "pug-strip-comments": { 422 | "version": "2.0.0", 423 | "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", 424 | "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", 425 | "requires": { 426 | "pug-error": "^2.0.0" 427 | } 428 | }, 429 | "pug-walk": { 430 | "version": "2.0.0", 431 | "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", 432 | "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" 433 | }, 434 | "resolve": { 435 | "version": "1.20.0", 436 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", 437 | "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", 438 | "requires": { 439 | "is-core-module": "^2.2.0", 440 | "path-parse": "^1.0.6" 441 | } 442 | }, 443 | "token-stream": { 444 | "version": "1.0.0", 445 | "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", 446 | "integrity": "sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=" 447 | }, 448 | "void-elements": { 449 | "version": "3.1.0", 450 | "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", 451 | "integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=" 452 | }, 453 | "with": { 454 | "version": "7.0.2", 455 | "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", 456 | "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", 457 | "requires": { 458 | "@babel/parser": "^7.9.6", 459 | "@babel/types": "^7.9.6", 460 | "assert-never": "^1.2.1", 461 | "babel-walk": "3.0.0-canary-5" 462 | } 463 | } 464 | } 465 | }, 466 | "randomstring": { 467 | "version": "1.1.5", 468 | "resolved": "https://registry.npmjs.org/randomstring/-/randomstring-1.1.5.tgz", 469 | "integrity": "sha1-bfBij3XL1ZMpMNn+OrTpVqGFGMM=" 470 | }, 471 | "to-fast-properties": { 472 | "version": "2.0.0", 473 | "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", 474 | "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" 475 | }, 476 | "uuid": { 477 | "version": "3.0.1", 478 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", 479 | "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=" 480 | } 481 | } 482 | } 483 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@slack/template-account-binding", 3 | "version": "0.1.0", 4 | "description": "Template application for account binding between an internal service and Slack (node).", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "start": "node index.js", 9 | "test": "npm run lint" 10 | }, 11 | "author": "Ankur Oberoi ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@slack/client": "^3.10.0", 15 | "axios": "^0.16.1", 16 | "bcrypt": "^1.0.2", 17 | "body-parser": "^1.17.1", 18 | "config": "^1.25.1", 19 | "connect-flash": "^0.1.1", 20 | "csurf": "^1.9.0", 21 | "dotenv": "^4.0.0", 22 | "express": "^4.15.2", 23 | "express-session": "^1.15.2", 24 | "level": "^1.6.0", 25 | "level-session-store": "^2.0.1", 26 | "level-sublevel": "^6.6.1", 27 | "param-case": "^2.1.1", 28 | "passport": "^0.3.2", 29 | "passport-local": "^1.0.0", 30 | "pug": "^3.0.1", 31 | "randomstring": "^1.1.5", 32 | "uuid": "^3.0.1" 33 | }, 34 | "devDependencies": { 35 | "eslint": "^3.19.0", 36 | "eslint-config-airbnb-base": "^11.1.3", 37 | "eslint-plugin-import": "^2.2.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/app.css: -------------------------------------------------------------------------------- 1 | .page-register .user-info, 2 | .page-link-slack .user-info { 3 | display: none; 4 | } 5 | -------------------------------------------------------------------------------- /routers/api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const axios = require('axios'); 4 | 5 | const util = require('../lib/util'); 6 | 7 | const slackVerificationToken = process.env.SLACK_VERIFICATION_TOKEN; 8 | const httpClient = axios.create({ 9 | headers: { 'User-Agent': util.packageIdentifier() }, 10 | }); 11 | 12 | function verifySlack(req, res, next) { 13 | // Assumes this application is is not distributed and can only be installed on one team. 14 | // If this assumption does not hold true, then we would modify this code as well as 15 | // the data model to store individual team IDs, verification tokens, and access tokens. 16 | if (req.body.token === slackVerificationToken) { 17 | next(); 18 | } else { 19 | next(new Error('Could not verify the request originated from Slack.')); 20 | } 21 | } 22 | 23 | module.exports = (users, message) => { 24 | const commands = { 25 | '/read-message': ({ user }) => message.getMessage(user).then(m => `The message is: ${m}`), 26 | '/write-message': ({ user, text }) => message.setMessage(text, user).then(m => `The message has been set: ${m}`), 27 | }; 28 | 29 | const api = express.Router(); 30 | 31 | api.use(bodyParser.urlencoded({ extended: false })); 32 | 33 | api.post('/slack/command', verifySlack, (req, res) => { 34 | // Respond to Slack immediately 35 | // There's no reason to wait, we will handle error cases asynchronously. 36 | // This value will ensure that the command is made visible to the entire channel. 37 | res.json({ response_type: 'in_channel' }); 38 | 39 | // Authenticate the Slack user 40 | // An assumption is being made: all commands require authentication 41 | users.findBySlackId(req.body.user_id) 42 | .then((user) => { 43 | // Execution of command 44 | const command = commands[req.body.command]; 45 | if (!command) { 46 | throw new Error(`Cannot understand the command: \`${req.body.command}\``); 47 | } 48 | return command.call(undefined, { 49 | user, 50 | text: req.body.text, 51 | }); 52 | }) 53 | .catch((error) => { 54 | // A helpful message for commands that will not complete because of failed user auth 55 | if (error.code === 'EUSERNOTFOUND') { 56 | // Start user association 57 | return users.beginSlackAssociation(req.body.user_id) 58 | .then(() => `Sorry <@${req.body.user_id}>, you cannot run \`${req.body.command}\` until after you authenticate. I can help you, just check my DM for the next step, and then you can try the command again.`); 59 | } 60 | // For all other errors, the in-channel response will be the error's message 61 | return error.message; 62 | }) 63 | .then(response => httpClient.post(req.body.response_url, { 64 | response_type: 'in_channel', 65 | text: response, 66 | })); 67 | }); 68 | 69 | return api; 70 | }; 71 | -------------------------------------------------------------------------------- /routers/web.js: -------------------------------------------------------------------------------- 1 | const qs = require('querystring'); 2 | const config = require('config'); 3 | const express = require('express'); 4 | const bodyParser = require('body-parser'); 5 | const session = require('express-session'); 6 | const LevelStore = require('level-session-store')(session); 7 | const flash = require('connect-flash'); 8 | const csurf = require('csurf'); 9 | const paramCase = require('param-case'); 10 | const passport = require('passport'); 11 | const LocalStrategy = require('passport-local').Strategy; 12 | 13 | function notAuthenticated(req, res, next) { 14 | if (req.user) { 15 | res.redirect('/'); 16 | } else { 17 | next(); 18 | } 19 | } 20 | 21 | module.exports = (db, users, message) => { 22 | passport.use(new LocalStrategy((username, password, done) => { 23 | users.findByUsername(username) 24 | .then(user => users.checkPassword(user.id, password) 25 | .then((isCorrect) => { 26 | if (!isCorrect) { 27 | done(null, false, { message: 'Credentials are invalid.' }); 28 | } else { 29 | done(null, user); 30 | } 31 | })) 32 | .catch((error) => { 33 | if (error.code === 'EUSERNOTFOUND') { 34 | done(null, false, { message: 'Credentials are invalid.' }); 35 | } else { 36 | done(error); 37 | } 38 | }); 39 | })); 40 | 41 | passport.serializeUser((user, done) => done(null, user.id)); 42 | passport.deserializeUser((id, done) => { 43 | users.findById(id) 44 | .then(user => done(null, user)) 45 | .catch((error) => { 46 | if (error.code === 'EUSERNOTFOUND') { 47 | done(null, false); 48 | } else { 49 | done(error); 50 | } 51 | }); 52 | }); 53 | 54 | const web = express.Router(); 55 | web.use(express.static('public')); 56 | web.use(session({ 57 | maxAge: config.get('session.maxAge'), 58 | resave: false, 59 | store: new LevelStore(db), 60 | secret: process.env.SESSION_SECRET, 61 | secure: !!(config.has('session.secure') && config.get('session.secure')), 62 | saveUninitialized: false, 63 | })); 64 | web.use(bodyParser.urlencoded({ extended: false })); 65 | web.use(csurf()); 66 | web.use(flash()); 67 | web.use(passport.initialize()); 68 | web.use(passport.session()); 69 | web.use((req, res, next) => { 70 | res.locals.pageName = paramCase(req.path); 71 | next(); 72 | }); 73 | 74 | web.get('/', (req, res) => { 75 | let myMessage; 76 | 77 | function renderHome() { 78 | res.render('home', { 79 | title: 'Home', 80 | user: req.user, 81 | message: myMessage, 82 | loginError: req.flash('login-error'), 83 | csrfToken: req.csrfToken(), 84 | }); 85 | } 86 | 87 | message.getMessage(req.user) 88 | .then((m) => { 89 | myMessage = m; 90 | renderHome(); 91 | }) 92 | .catch(() => renderHome()); 93 | }); 94 | 95 | web.get('/register', notAuthenticated, (req, res) => { 96 | res.render('register', { 97 | title: 'Registration', 98 | registrationError: req.flash('registration-error'), 99 | registerFormExtraParams: { 100 | redirectUrl: req.query.ref ? config.get('routes.associationPath') + '?ref=' + req.query.ref : '/' , 101 | }, 102 | csrfToken: req.csrfToken(), 103 | }); 104 | }); 105 | 106 | web.post('/login', (req, res, next) => { 107 | passport.authenticate('local', { 108 | successRedirect: req.body.redirectUrl || '/', 109 | failureRedirect: req.get('Referrer') || '/', 110 | failureFlash: { type: 'login-error' }, 111 | })(req, res, next); 112 | }); 113 | 114 | web.post('/logout', (req, res) => { 115 | req.logout(); 116 | res.redirect('/'); 117 | }); 118 | 119 | web.post('/register', (req, res) => { 120 | if (!req.body.password || !req.body.confirmPassword || 121 | req.body.password !== req.body.confirmPassword) { 122 | req.flash('registration-error', 'Password and confirm password fields must match.'); 123 | res.redirect('/register'); 124 | return; 125 | } 126 | users.register({ 127 | username: req.body.username, 128 | password: req.body.password, 129 | }) 130 | .then((user) => { 131 | req.login(user, (loginError) => { 132 | if (loginError) { 133 | throw new Error('User was created but could not be logged in.'); 134 | } 135 | res.redirect(req.body.redirectUrl || '/'); 136 | }); 137 | }) 138 | .catch((error) => { 139 | req.flash('registration-error', `An error occurred: ${error.message}`); 140 | res.redirect('/register'); 141 | }); 142 | }); 143 | 144 | web.get(config.get('routes.associationPath'), (req, res) => { 145 | res.locals.title = 'Slack User Association'; 146 | if (!req.user) { 147 | res.render('association', { 148 | mainMessage: 'You must login before user association can be completed.', 149 | renderLoginForm: true, 150 | nonce: req.query.ref, 151 | loginFormExtraParams: { 152 | redirectUrl: req.originalUrl, 153 | }, 154 | loginError: req.flash('login-error'), 155 | csrfToken: req.csrfToken(), 156 | }); 157 | } else { 158 | if (req.user.slack) { 159 | res.render('association', { 160 | mainMessage: 'Your user account is already associated with a Slack user.', 161 | }); 162 | } else if (req.query.ref) { 163 | users.completeSlackAssociation(req.user.id, req.query.ref) 164 | .then(() => { 165 | res.render('association', { 166 | mainMessage: 'Your user account has successfully been associated with your Slack user.', 167 | redirectUrl: '/', 168 | }); 169 | }) 170 | .catch((error) => { 171 | res.render('association', { 172 | mainMessage: `An error occurred: ${error.message}`, 173 | }); 174 | }); 175 | } else { 176 | // You might want to supply an alternative user association flow with Sign In With Slack 177 | res.render('association', { 178 | mainMessage: 'You must begin the user association process before visiting this page.', 179 | }); 180 | } 181 | } 182 | }); 183 | 184 | return web; 185 | }; 186 | -------------------------------------------------------------------------------- /views/association.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | h2= title 5 | 6 | if mainMessage 7 | p= mainMessage 8 | 9 | if redirectUrl 10 | p You will be redirected in a few seconds... 11 | script. 12 | window.setTimeout(function () { window.location.replace('!{redirectUrl}'); }, 6000); 13 | 14 | if renderLoginForm 15 | include includes/loginForm 16 | -------------------------------------------------------------------------------- /views/home.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | h2= title 5 | 6 | p This is some placeholder text. 7 | 8 | if message 9 | p Current Message: #{message} 10 | -------------------------------------------------------------------------------- /views/includes/footer.pug: -------------------------------------------------------------------------------- 1 | footer#footer 2 | block footer 3 | p Slack Team Based Authentication Template 4 | p: a(href='https://github.com/slackapi/template-account-binding') GitHub 5 | p: a(href='https://api.slack.com') Slack API Documentation 6 | -------------------------------------------------------------------------------- /views/includes/header.pug: -------------------------------------------------------------------------------- 1 | header#header 2 | block header 3 | .user-info 4 | if user 5 | include logoutForm 6 | else 7 | include loginForm 8 | -------------------------------------------------------------------------------- /views/includes/loginForm.pug: -------------------------------------------------------------------------------- 1 | if loginError 2 | .error-msg 3 | p= loginError 4 | form.login(action='/login', method='post') 5 | div 6 | label(for='username') Username 7 | input(type='text', id='username', name='username') 8 | div 9 | label(for='password') Password 10 | input(type='password', id='password', name='password') 11 | div 12 | input(type='hidden', name='_csrf', value=csrfToken) 13 | if loginFormExtraParams 14 | each value, key in loginFormExtraParams 15 | div 16 | input(type='hidden', name=key, value=value) 17 | 18 | div 19 | button(type='submit') Login 20 | 21 | if nonce 22 | a(href='/register?ref=' + nonce) Register 23 | else 24 | a(href='/register') Register 25 | -------------------------------------------------------------------------------- /views/includes/logoutForm.pug: -------------------------------------------------------------------------------- 1 | form.logout(action='/logout', method='post') 2 | div 3 | input(type='hidden', name='_csrf', value=csrfToken) 4 | div 5 | button(type='submit') Logout 6 | -------------------------------------------------------------------------------- /views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | meta(charset='utf-8') 5 | meta(http-equiv='x-ua-compatible', content='ie=edge') 6 | meta(name='description', content='Slack Team Based Authentication Template') 7 | meta(name='viewport', content='width=device-width, initial-scale=1') 8 | title Slack Team Based Authentication Template - #{title} 9 | link(rel='stylesheet', href='/app.css') 10 | body(class=pageName ? `page-${pageName}` : '') 11 | #container 12 | include includes/header.pug 13 | block content 14 | include includes/footer.pug 15 | -------------------------------------------------------------------------------- /views/register.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | h2= title 5 | 6 | if registrationError 7 | .error-msg 8 | p= registrationError 9 | 10 | form.login(action='/register', method='post') 11 | p User Registration 12 | div 13 | label(for='username') Username 14 | input(type='text', id='username', name='username') 15 | div 16 | label(for='password') Password 17 | input(type='password', id='password', name='password') 18 | div 19 | label(for='confirm_password') Confirm Password 20 | input(type='password', id='confirm_password', name='confirmPassword') 21 | div 22 | input(type='hidden', name='_csrf', value=csrfToken) 23 | 24 | if registerFormExtraParams 25 | each value, key in registerFormExtraParams 26 | div 27 | input(type='hidden', name=key, value=value) 28 | div 29 | button(type='submit') Register 30 | --------------------------------------------------------------------------------