├── iisnode.yaml ├── public ├── images │ ├── bg.jpg │ └── field.png └── css │ └── style.css ├── screenshots ├── oauth1.gif ├── oauth2.gif ├── oauth3.gif ├── oauth4.gif ├── join-page.jpg └── legacy-token.gif ├── locales ├── zh-CN.json ├── zh-TW.json ├── ko.json ├── ja.json ├── de.json ├── cs.json ├── en.json ├── pl.json ├── fr.json ├── pt.json ├── tr.json ├── es.json ├── ru.json ├── it.json ├── nl.json └── pt-BR.json ├── Dockerfile ├── package.json ├── views ├── error.jade ├── result.jade └── index.jade ├── .gitignore ├── app.json ├── manifest.yml ├── LICENSE.md ├── config.js ├── app.js ├── bin └── www ├── azuredeploy.json ├── routes └── index.js └── README.md /iisnode.yaml: -------------------------------------------------------------------------------- 1 | loggingEnabled: true 2 | logDirectory: iisnode 3 | -------------------------------------------------------------------------------- /public/images/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/slack-invite-automation/master/public/images/bg.jpg -------------------------------------------------------------------------------- /public/images/field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/slack-invite-automation/master/public/images/field.png -------------------------------------------------------------------------------- /screenshots/oauth1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/slack-invite-automation/master/screenshots/oauth1.gif -------------------------------------------------------------------------------- /screenshots/oauth2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/slack-invite-automation/master/screenshots/oauth2.gif -------------------------------------------------------------------------------- /screenshots/oauth3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/slack-invite-automation/master/screenshots/oauth3.gif -------------------------------------------------------------------------------- /screenshots/oauth4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/slack-invite-automation/master/screenshots/oauth4.gif -------------------------------------------------------------------------------- /screenshots/join-page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/slack-invite-automation/master/screenshots/join-page.jpg -------------------------------------------------------------------------------- /screenshots/legacy-token.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/slack-invite-automation/master/screenshots/legacy-token.gif -------------------------------------------------------------------------------- /locales/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "以下请输入您的电子邮箱加入 %s 的 Slack!", 3 | "ENTER_EMAIL": "输入您的电子邮箱地址", 4 | "ENTER_TOKEN": "输入提供予您的邀请令牌", 5 | "TITLE": "加入 %s 的 Slack 社群!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/zh-TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "以下請輸入您的電子郵箱加入 %s 的 Slack!", 3 | "ENTER_EMAIL": "輸入您的電子郵箱地址", 4 | "ENTER_TOKEN": "輸入提供予您的邀請令牌", 5 | "TITLE": "加入 %s 的 Slack 社群!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Slack의 %s에 가입하기 위한 이메일 주소를 입력하세요!", 3 | "ENTER_EMAIL": "이메일 주소를 입력하세요", 4 | "ENTER_TOKEN": "초대 토큰을 입력하세요", 5 | "TITLE": "Slack의 %s 커뮤니티에 참여하세요!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Slackで %s に参加するためにメールアドレスを入力してください!", 3 | "ENTER_EMAIL": "メールアドレスを入力してください", 4 | "ENTER_TOKEN": "あなたが取得した招待用トークンを入力してください。", 5 | "TITLE": "Slackで %s コミュニティに参加しましょう!" 6 | } 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:onbuild 2 | MAINTAINER Benjamin Jorand 3 | 4 | EXPOSE 3000 5 | 6 | COPY . /slack-invite-automation 7 | WORKDIR /slack-invite-automation 8 | RUN npm install 9 | CMD ./bin/www 10 | -------------------------------------------------------------------------------- /locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Gib deine E-Mail Adresse an, um %s auf Slack beizutreten!", 3 | "ENTER_EMAIL": "Deine E-Mail Adresse", 4 | "ENTER_TOKEN": "Gib deinen Zugangscode an", 5 | "TITLE": "Tritt %s auf Slack bei!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Zadej e-mail a staň se členem skupiny %s na Slacku!", 3 | "ENTER_EMAIL": "Zadej svůj e-mail", 4 | "ENTER_TOKEN": "Zadej přihlašovací kód z pozvánky", 5 | "TITLE": "Staň se členem skupiny %s na Slacku!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Enter your email below to join %s on Slack!", 3 | "ENTER_EMAIL": "Enter Your Email Address", 4 | "ENTER_TOKEN": "Enter the invite token you were given", 5 | "TITLE": "Join the %s community on Slack!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Podaj swój email pod spodem aby dołączyć do %s na Slack!", 3 | "ENTER_EMAIL": "Podaj swój adres email", 4 | "ENTER_TOKEN": "Podaj swój kod zaproszenia", 5 | "TITLE": "Dołącz do społeczności %s na Slack!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Entrez votre adresse mail pour rejoindre le groupe %s sur Slack!", 3 | "ENTER_EMAIL": "Entrez votre adresse mail", 4 | "ENTER_TOKEN": "Entrez votre token d'invitation", 5 | "TITLE": "Rejoignez le groupe %s sur Slack!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Introduza o seu email para participar no Slack de %s!", 3 | "ENTER_EMAIL": "Insira o seu endereço de e-mail", 4 | "ENTER_TOKEN": "Digite o convite que lhe foi entregue", 5 | "TITLE": "Junte-se à comunidade Slack de %s!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Slack'te %s topluluğuna katılmak için e-mailinizi giriniz.", 3 | "ENTER_EMAIL": "E-mail adresinizi giriniz.", 4 | "ENTER_TOKEN": "Size verilen davet token'ınızı giriniz.", 5 | "TITLE": "Slack'te %s topluluğuna katılın" 6 | } 7 | -------------------------------------------------------------------------------- /locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "¡Ingresa tu dirección de email debajo para unirte a %s en Slack!", 3 | "ENTER_EMAIL": "Ingresa tu dirección de email", 4 | "ENTER_TOKEN": "Ingresa el código de invitación", 5 | "TITLE": "¡Únete a la comunidad de %s en Slack!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Напишите свой E-Mail чтобы присоединиться к %s в Slack!", 3 | "ENTER_EMAIL": "Введите e-mail на него будет отправлено приглашение", 4 | "ENTER_TOKEN": "Введите ключ-приглашение", 5 | "TITLE": "Присоиденяйтесь к %s в Slack!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Inseirisci qui sotto la tua email per unirti a %s su Slack!", 3 | "ENTER_EMAIL": "Inserisci il tuo indirizzo e-mail", 4 | "ENTER_TOKEN": "Inserisci il token d'invito che ti è stato fornito", 5 | "TITLE": "Unisciti a %s su Slack!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Voer uw email adres in om deel te nemen aan %s op Slack!", 3 | "ENTER_EMAIL": "Voer uw email adres in", 4 | "ENTER_TOKEN": "Voer het token in dat u kreeg bij uw uitnodiging", 5 | "TITLE": "Neem deel aan de %s gemeenschap op Slack!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Digite seu email abaixo para se juntar a %s no Slack!", 3 | "ENTER_EMAIL": "Insira seu endereço de e-mail", 4 | "ENTER_TOKEN": "Digite seu Token de convite que foi dado a você", 5 | "TITLE": "Junte-se a comunidade %s no Slack!" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slack-invite-automation", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "^1.18.0", 10 | "cookie-parser": "^1.4.0", 11 | "debug": "^3.1.0", 12 | "dotenv": "^4.0.0", 13 | "express": "^4.13.0", 14 | "i18n": "^0.8.3", 15 | "jade": "^1.11.0", 16 | "morgan": "^1.6.0", 17 | "request": "^2.62.0", 18 | "serve-favicon": "^2.3.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /views/error.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(http-equiv="Content-Type", content="text/html; charset=utf-8") 5 | meta(name="viewport", content="width=device-width, initial-scale=1") 6 | title #{__('TITLE', community)} 7 | link(href="css/style.css", rel="stylesheet", type="text/css") 8 | link(href="//fonts.googleapis.com/css?family=Lato:300,400,700,900,700italic|Open+Sans:700italic,400,600,300,700,800", rel="stylesheet", type="text/css") 9 | body 10 | #wrapper 11 | .main 12 | .header 13 | h1= message 14 | h2= error.status 15 | pre #{error.stack} 16 | -------------------------------------------------------------------------------- /views/result.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(http-equiv="Content-Type", content="text/html; charset=utf-8") 5 | meta(name="viewport", content="width=device-width, initial-scale=1") 6 | title #{__('TITLE', community)} 7 | link(href="css/style.css", rel="stylesheet", type="text/css") 8 | link(href="//fonts.googleapis.com/css?family=Lato:300,400,700,900,700italic|Open+Sans:700italic,400,600,300,700,800", rel="stylesheet", type="text/css") 9 | body 10 | #wrapper 11 | .main 12 | .header 13 | h1 14 | strong #{community} 15 | h2(class="#{isFailed?'error':''}") !{message} 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | 30 | .env 31 | .env.* 32 | .idea/ 33 | .envrc 34 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Slack Invite Automation", 3 | "description": "A tiny web application to invite a user into your slack team.", 4 | "repository": "https://github.com/outsideris/slack-invite-automation", 5 | "keywords": ["node", "slack", "invite"], 6 | "env": { 7 | "COMMUNITY_NAME": { 8 | "description": "Your team name", 9 | "value": "YOUR TEAM NAME" 10 | }, 11 | "SLACK_URL": { 12 | "description": "Your slack url(ex: socketio.slack.com)", 13 | "value": "YOUR-TEAM.slack.com" 14 | }, 15 | "SLACK_TOKEN": { 16 | "description": "access token of slack, You can generate it in https://api.slack.com/web#auth", 17 | "value": "" 18 | }, 19 | "INVITE_TOKEN": { 20 | "description": "Shared invite token used to get access. Leave blank if you don't want users to have to provide a token.", 21 | "value": "", 22 | "required": false 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | applications: 2 | - name: slack-invite-automation 3 | host: slack-invite-automation 4 | command: bin/www 5 | instances: 1 6 | memory: 128M 7 | disk_quota: 128M 8 | env: 9 | # your community or team name to display on join page 10 | COMMUNITY_NAME: SocketIO 11 | 12 | # your slack team url (ex: socketio.slack.com) 13 | SLACK_URL: socketio.slack.com 14 | 15 | # access token of slack 16 | # You can generate it in https://api.slack.com/web#auth 17 | # You should generate the token in admin user, not owner. 18 | # If you generate the token in owner user, missing_scope error will occur. 19 | # 20 | # You can test your token via curl: 21 | # curl -X POST 'https://YOUR-SLACK-TEAM.slack.com/api/users.admin.invite' \ 22 | # --data 'email=EMAIL&token=TOKEN&set_active=true' \ 23 | # --compressed 24 | SLACK_TOKEN: 25 | 26 | # an optional security measure - if it is set, then that token will be required to get invited. 27 | # INVITE_TOKEN: 28 | 29 | LOCALE: en 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 "Outsider" Jeonghoon Byun 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // your community or team name to display on join page. 3 | community: process.env.COMMUNITY_NAME || 'YOUR-TEAM-NAME', 4 | // your slack team url (ex: socketio.slack.com) 5 | slackUrl: process.env.SLACK_URL || 'YOUR-TEAM.slack.com', 6 | // access token of slack 7 | // You can generate it in https://api.slack.com/web#auth 8 | // You should generate the token in admin user, not owner. 9 | // If you generate the token in owner user, missing_scope error will be occurred. 10 | // 11 | // You can test your token via curl: 12 | // curl -X POST 'https://YOUR-SLACK-TEAM.slack.com/api/users.admin.invite' \ 13 | // --data 'email=EMAIL&token=TOKEN&set_active=true' \ 14 | // --compressed 15 | slacktoken: process.env.SLACK_TOKEN || 'YOUR-ACCESS-TOKEN', 16 | // an optional security measure - if it is set, then that token will be required to get invited. 17 | inviteToken: process.env.INVITE_TOKEN || null, 18 | // an optional security measure - if both are set, then recaptcha will be used. 19 | recaptchaSiteKey: process.env.RECAPTCHA_SITE || null, 20 | recaptchaSecretKey: process.env.RECAPTCHA_SECRET || null, 21 | // default locale 22 | locale: process.env.LOCALE || "en", 23 | }; 24 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(http-equiv="Content-Type", content="text/html; charset=utf-8") 5 | meta(name="viewport", content="width=device-width, initial-scale=1") 6 | title #{__('TITLE', community)} 7 | link(href="css/style.css", rel="stylesheet", type="text/css") 8 | link(href="//fonts.googleapis.com/css?family=Lato:300,400,700,900,700italic|Open+Sans:700italic,400,600,300,700,800", rel="stylesheet", type="text/css") 9 | body 10 | #wrapper 11 | .main 12 | .header 13 | h1 14 | strong #{community} 15 | h2 #{__('HEADER', community)} 16 | .content 17 | .information 18 | form(method="POST", action="/invite")#join-form.form 19 | input(type="email", name="email", autofocus, placeholder="#{__('ENTER_EMAIL')}")#slack-email.field 20 | if tokenRequired 21 | input(type="text", name="token", placeholder="#{__('ENTER_TOKEN')}")#slack-token.field 22 | input(type="submit", value="Join").submit 23 | if !!recaptchaSiteKey 24 | div(class="g-recaptcha", data-sitekey="#{recaptchaSiteKey}") 25 | if !!recaptchaSiteKey 26 | script(src='https://www.google.com/recaptcha/api.js') 27 | script. 28 | var tokenRequired = #{tokenRequired}; 29 | var form = document.getElementById('join-form'); 30 | var email = document.getElementById('slack-email'); 31 | var token = document.getElementById('slack-token'); 32 | form.addEventListener('submit', function(evt) { 33 | if (!email.value) { 34 | evt.preventDefault(); 35 | } 36 | if (tokenRequired && !token.value) { 37 | evt.preventDefault(); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const favicon = require('serve-favicon'); 4 | const logger = require('morgan'); 5 | const cookieParser = require('cookie-parser'); 6 | const bodyParser = require('body-parser'); 7 | const i18n = require("i18n"); 8 | 9 | const config = require('./config'); 10 | 11 | const routes = require('./routes/index'); 12 | 13 | const app = express(); 14 | 15 | i18n.configure({ 16 | defaultLocale: "en", 17 | directory: __dirname + '/locales', 18 | autoReload: true 19 | }); 20 | 21 | i18n.setLocale(config.locale); 22 | 23 | // default: using 'accept-language' header to guess language settings 24 | app.use(i18n.init); 25 | 26 | // view engine setup 27 | app.set('views', path.join(__dirname, 'views')); 28 | app.set('view engine', 'jade'); 29 | 30 | // uncomment after placing your favicon in /public 31 | //app.use(favicon(__dirname + '/public/favicon.ico')); 32 | app.use(logger('dev')); 33 | app.use(bodyParser.json()); 34 | app.use(bodyParser.urlencoded({ extended: false })); 35 | app.use(cookieParser()); 36 | app.use(express.static(path.join(__dirname, 'public'))); 37 | 38 | app.use('/', routes); 39 | 40 | // catch 404 and forward to error handler 41 | app.use(function(req, res, next) { 42 | const err = new Error('Not Found'); 43 | err.status = 404; 44 | next(err); 45 | }); 46 | 47 | // error handlers 48 | 49 | // development error handler 50 | // will print stacktrace 51 | if (app.get('env') === 'development') { 52 | app.use(function(err, req, res, next) { 53 | res.status(err.status || 500); 54 | res.render('error', { 55 | message: err.message, 56 | error: err 57 | }); 58 | }); 59 | } 60 | 61 | // production error handler 62 | // no stacktraces leaked to user 63 | app.use(function(err, req, res, next) { 64 | res.status(err.status || 500); 65 | res.render('error', { 66 | message: err.message, 67 | error: {} 68 | }); 69 | }); 70 | 71 | 72 | module.exports = app; 73 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // support for .env file to get loaded in to environment variables. 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | const envFile = path.join(__dirname, '../.env'); 7 | try { 8 | fs.accessSync(envFile, fs.F_OK); 9 | require('dotenv').config({path: envFile}); 10 | } catch (e) { 11 | // no env file 12 | } 13 | 14 | /** 15 | * Module dependencies. 16 | */ 17 | const app = require('../app'); 18 | const debug = require('debug')('slack-invite-automation:server'); 19 | const http = require('http'); 20 | 21 | /** 22 | * Normalize a port into a number, string, or false. 23 | * @param {int} val 24 | */ 25 | function normalizePort(val) { 26 | const port = parseInt(val, 10); 27 | if (isNaN(port)) { 28 | return val; 29 | } else if (port >= 0) { 30 | return port; 31 | } 32 | 33 | return false; 34 | } 35 | 36 | 37 | /** 38 | * Get port from environment and store in Express. 39 | */ 40 | 41 | const port = normalizePort(process.env.PORT || '3000'); 42 | app.set('port', port); 43 | 44 | /** 45 | * Create HTTP server. 46 | */ 47 | 48 | const server = http.createServer(app); 49 | 50 | /** 51 | * Listen on provided port, on all network interfaces. 52 | */ 53 | 54 | server.listen(port); 55 | server.on('error', onError); 56 | server.on('listening', onListening); 57 | 58 | /** 59 | * Event listener for HTTP server "error" event. 60 | */ 61 | 62 | function onError(error) { 63 | if (error.syscall !== 'listen') { 64 | throw error; 65 | } 66 | 67 | // handle specific listen errors with friendly messages 68 | switch (error.code) { 69 | case 'EACCES': 70 | console.error('Port ' + port + ' requires elevated privileges'); 71 | process.exit(1); 72 | break; 73 | case 'EADDRINUSE': 74 | console.error('Port ' + port + ' is already in use'); 75 | process.exit(1); 76 | break; 77 | default: 78 | throw error; 79 | } 80 | } 81 | 82 | /** 83 | * Event listener for HTTP server "listening" event. 84 | */ 85 | 86 | function onListening() { 87 | debug('Listening on port ' + server.address().port); 88 | } 89 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | /* Reset */ 2 | 3 | body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,dfn,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;} 4 | 5 | 6 | html,body{ 7 | margin:0; 8 | padding:0; 9 | font-family: 'Open Sans', sans-serif; 10 | background: #fff url(../images/bg.jpg) center top no-repeat; 11 | background-size: cover; 12 | height: 100%; 13 | width: 100%; 14 | } 15 | 16 | 17 | #wrapper{ 18 | max-width: 100%; 19 | width: 940px; 20 | margin: 0 auto; 21 | } 22 | 23 | .main{ 24 | max-width: 100%; 25 | width: 940px; 26 | float: left; 27 | } 28 | 29 | .header, .content, .bottom, .sep2{ 30 | width: 100%; 31 | float: left; 32 | } 33 | 34 | 35 | 36 | .header{ 37 | margin-top: 160px; 38 | margin-bottom: 40px; 39 | } 40 | 41 | 42 | h1{ 43 | font-family: 'Open Sans', sans-serif; 44 | color: #fff; 45 | text-align: center; 46 | font-size: 50px; 47 | font-weight: 300; 48 | margin: 0 0 10px 0; 49 | } 50 | 51 | 52 | h1 strong{ 53 | font-size: 38px; 54 | font-weight: 700; 55 | } 56 | 57 | h2{ 58 | text-align: center; 59 | font-weight: 300; 60 | font-size: 20px; 61 | color: #ffffff; 62 | 63 | } 64 | 65 | h2 strong{ 66 | font-weight: 700; 67 | font-style: italic; 68 | } 69 | 70 | 71 | .information{ 72 | width: 480px; 73 | padding-top: 35px; 74 | margin: 0 auto; 75 | } 76 | 77 | h3{ 78 | font-size: 28px; 79 | font-weight: 600; 80 | color: #ffffff; 81 | } 82 | 83 | .information p{ 84 | font-size: 16px; 85 | color: #ffffff; 86 | display: block; 87 | } 88 | 89 | .form{ 90 | position: relative; 91 | width: 478px; 92 | margin-top: 20px; 93 | } 94 | 95 | .field{ 96 | background: url(../images/field.png) repeat; 97 | width: 448px; 98 | -webkit-border-radius: 30px;-moz-border-radius: 30px;border-radius: 30px; 99 | border: none; 100 | font-style: italic; 101 | font-family: 'Lato', sans-serif; 102 | font-size: 16px; 103 | color: #ffffff; 104 | padding: 15px; 105 | margin-bottom: 15px; 106 | outline: none; 107 | } 108 | .field:focus { 109 | border: 1px solid #ffffff; 110 | padding: 14px; 111 | } 112 | @media only screen and (max-width: 480px) { 113 | .information{ 114 | width: 100%; 115 | } 116 | .form{ 117 | width: 90%; 118 | margin-left: 5%; 119 | margin-right: 5%; 120 | } 121 | 122 | .field { 123 | width: 90%; 124 | } 125 | } 126 | 127 | .submit{ 128 | position: absolute; 129 | right: 15px; 130 | top: 13px; 131 | padding: 3px 10px; 132 | -webkit-border-radius: 30px;-moz-border-radius: 30px;border-radius: 30px; 133 | background: #ffffff; 134 | border: none; 135 | font-family: 'Lato', sans-serif; 136 | font-size: 14px; 137 | font-weight: bold; 138 | cursor: pointer; 139 | } 140 | 141 | 142 | .submit:hover{ 143 | background: #eeeeee; 144 | } 145 | 146 | .error { 147 | color: #FE7070; 148 | font-weight: 700; 149 | font-size: 25px; 150 | } 151 | 152 | a { 153 | color: #92BCF2; 154 | font-weight: 700; 155 | } 156 | 157 | a:visited, 158 | a:hover { 159 | color: #7EB6FF; 160 | } 161 | 162 | .g-recaptcha, .g-recaptcha > div { 163 | margin: auto; 164 | } 165 | -------------------------------------------------------------------------------- /azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "siteName": { 6 | "type": "string" 7 | }, 8 | "hostingPlanName": { 9 | "type": "string" 10 | }, 11 | "siteLocation": { 12 | "type": "string", 13 | "default": "West US" 14 | }, 15 | "sku": { 16 | "type": "string", 17 | "allowedValues": [ 18 | "Free", 19 | "Shared", 20 | "Basic", 21 | "Standard" 22 | ], 23 | "defaultValue": "Free" 24 | }, 25 | "workerSize": { 26 | "type": "string", 27 | "allowedValues": [ 28 | "0", 29 | "1", 30 | "2" 31 | ], 32 | "defaultValue": "0" 33 | }, 34 | "Slack: Slack team name": { 35 | "type": "string" 36 | }, 37 | "Slack: Slack team url": { 38 | "type": "string" 39 | }, 40 | "Slack: Slack token": { 41 | "type": "string" 42 | }, 43 | "Slack: Required invite token": { 44 | "type": "string" 45 | } 46 | }, 47 | "variables": {}, 48 | "resources": [ 49 | { 50 | "apiVersion": "2014-06-01", 51 | "name": "[parameters('hostingPlanName')]", 52 | "type": "Microsoft.Web/serverFarms", 53 | "location": "[parameters('siteLocation')]", 54 | "properties": { 55 | "name": "[parameters('hostingPlanName')]", 56 | "sku": "[parameters('sku')]", 57 | "workerSize": "[parameters('workerSize')]", 58 | "numberOfWorkers": 1 59 | } 60 | }, 61 | { 62 | "type": "Microsoft.Web/sites", 63 | "name": "[parameters('siteName')]", 64 | "apiVersion": "2015-08-01", 65 | "location": "[parameters('siteLocation')]", 66 | "tags": { 67 | "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "empty" 68 | }, 69 | "dependsOn": [ 70 | "[concat('Microsoft.Web/serverFarms/', parameters('hostingPlanName'))]" 71 | ], 72 | "properties": { 73 | "name": "[parameters('siteName')]", 74 | "serverFarm": "[parameters('hostingPlanName')]" 75 | }, 76 | "resources": [{ 77 | "apiVersion": "2014-04-01", 78 | "type": "config", 79 | "name": "web", 80 | "dependsOn": [ 81 | "[concat('Microsoft.Web/Sites/', parameters('siteName'))]" 82 | ], 83 | "properties": { 84 | "appSettings": [{ 85 | "name": "COMMUNITY_NAME", 86 | "value": "[parameters('Slack: Slack team name')]" 87 | }, { 88 | "name": "SLACK_URL", 89 | "value": "[parameters('Slack: Slack team url')]" 90 | }, { 91 | "name": "SLACK_TOKEN", 92 | "value": "[parameters('Slack: Slack token')]" 93 | }, { 94 | "name": "INVITE_TOKEN", 95 | "value": "[parameters('Slack: Required invite token')]" 96 | }] 97 | } 98 | }] 99 | } 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const request = require('request'); 4 | const config = require('../config'); 5 | 6 | router.get('/', function(req, res) { 7 | res.setLocale(config.locale); 8 | res.render('index', { community: config.community, 9 | tokenRequired: !!config.inviteToken, 10 | recaptchaSiteKey: config.recaptchaSiteKey }); 11 | }); 12 | 13 | router.post('/invite', function(req, res) { 14 | if (req.body.email && (!config.inviteToken || (!!config.inviteToken && req.body.token === config.inviteToken))) { 15 | function doInvite() { 16 | request.post({ 17 | url: 'https://'+ config.slackUrl + '/api/users.admin.invite', 18 | form: { 19 | email: req.body.email, 20 | token: config.slacktoken, 21 | set_active: true 22 | } 23 | }, function(err, httpResponse, body) { 24 | // body looks like: 25 | // {"ok":true} 26 | // or 27 | // {"ok":false,"error":"already_invited"} 28 | if (err) { return res.send('Error:' + err); } 29 | body = JSON.parse(body); 30 | if (body.ok) { 31 | res.render('result', { 32 | community: config.community, 33 | message: 'Success! Check “'+ req.body.email +'” for an invite from Slack.' 34 | }); 35 | } else { 36 | const error = body.error; 37 | if (error === 'already_invited' || error === 'already_in_team') { 38 | res.render('result', { 39 | community: config.community, 40 | message: 'Success! You were already invited.
' + 41 | 'Visit '+ config.community +'' 42 | }); 43 | return; 44 | } else if (error === 'invalid_email') { 45 | error = 'The email you entered is an invalid email.'; 46 | } else if (error === 'invalid_auth') { 47 | error = 'Something has gone wrong. Please contact a system administrator.'; 48 | } 49 | 50 | res.render('result', { 51 | community: config.community, 52 | message: 'Failed! ' + error, 53 | isFailed: true 54 | }); 55 | } 56 | }); 57 | } 58 | if (!!config.recaptchaSiteKey && !!config.recaptchaSecretKey) { 59 | request.post({ 60 | url: 'https://www.google.com/recaptcha/api/siteverify', 61 | form: { 62 | response: req.body['g-recaptcha-response'], 63 | secret: config.recaptchaSecretKey 64 | } 65 | }, function(err, httpResponse, body) { 66 | if (typeof body === "string") { 67 | body = JSON.parse(body); 68 | } 69 | 70 | if (body.success) { 71 | doInvite(); 72 | } else { 73 | error = 'Invalid captcha.'; 74 | res.render('result', { 75 | community: config.community, 76 | message: 'Failed! ' + error, 77 | isFailed: true 78 | }); 79 | } 80 | }); 81 | } else { 82 | doInvite(); 83 | } 84 | } else { 85 | const errMsg = []; 86 | if (!req.body.email) { 87 | errMsg.push('your email is required'); 88 | } 89 | 90 | if (!!config.inviteToken) { 91 | if (!req.body.token) { 92 | errMsg.push('valid token is required'); 93 | } 94 | 95 | if (req.body.token && req.body.token !== config.inviteToken) { 96 | errMsg.push('the token you entered is wrong'); 97 | } 98 | } 99 | 100 | res.render('result', { 101 | community: config.community, 102 | message: 'Failed! ' + errMsg.join(' and ') + '.', 103 | isFailed: true 104 | }); 105 | } 106 | }); 107 | 108 | module.exports = router; 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Slack Invite Automation 2 | ------------ 3 | 4 | A tiny web application to invite a user into your Slack team. 5 | 6 | Inspired by 7 | [How I hacked Slack into a community platform with Typeform](https://levels.io/slack-typeform-auto-invite-sign-ups/) 8 | and Socket.io's Slack page. 9 | 10 | This project supports Heroku, Azure and Cloud Foundry. 11 | 12 | [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 13 | [![Deploy to Azure](http://azuredeploy.net/deploybutton.png)](https://azuredeploy.net/) 14 | 15 | ## Settings 16 | 17 | You can set variables for your own purpose in `config.js` or environment variables. 18 | 19 | ### `config.js` 20 | 21 | Fill out `config.js` as your infomation. 22 | 23 | * `community`: your community or team name to display on join page. 24 | * `slackUrl` : your slack team url (ex.: socketio.slack.com) 25 | * `slacktoken` : Your access token for Slack. (see [Issue token](#issue-token)) 26 | * `inviteToken`: An optional security measure - if it is set, then that token will be required to get invited. 27 | * `recaptchaSiteKey`: An optional security measure - if it is set, and `recaptchaSecretKey` is set, then a captcha will be required to get invited. 28 | * `recaptchaSecretKey`: An optional security measure - if it is set, and `recaptchaSiteKey` is set, then a captcha will be required to get invited. 29 | * `locale`: Application language (currently `cs`, `de`, `en`, `es`, `fr`, `it`, `ja`, `ko`, `nl`, `pl`, `pt`, `pt-BR`, `tr`, `zh-CN` and `zh-TW` available). 30 | 31 | ### Environment Variables 32 | You can set environment variables directly or in `.env` file. 33 | If you want to use a `.env` file, create a file in the root called `.env` with the following key/value pairs. 34 | (`.env` files are added to the `.gitignore`.) 35 | 36 | - `COMMUNITY_NAME` : Your community or team name to display on join page. 37 | - `SLACK_URL` : Your Slack team url (ex.: socketio.slack.com) 38 | - `SLACK_TOKEN` : Your access token for Slack. (see [Issue token](#issue-token)) 39 | - `INVITE_TOKEN`: An optional security measure - if it is set, then that token will be required to get invited. 40 | - `RECAPTCHA_SITE`: An optional security measure - used to enable reCAPTCHA. 41 | - `RECAPTCHA_SECRET`: An optional security measure - used to enable reCAPTCHA. 42 | - `LOCALE`: Application language (currently `cs`, `de`, `en`, `es`, `fr`, `it`, `ja`, `ko`, `nl`, `pl`, `pt`, `pt-BR`, `tr`, `zh-CN` and `zh-TW` available). 43 | 44 | **Sample** 45 | 46 | ``` 47 | COMMUNITY_NAME=socketio 48 | SLACK_URL=socketio.slack.com 49 | SLACK_TOKEN=ffsdf-5411524512154-16875416847864648976-45641654654654654-444334f43b34566f 50 | INVITE_TOKEN=abcdefg 51 | LOCALE=en 52 | ``` 53 | 54 | You can test your token via curl: 55 | 56 | ```shell 57 | curl -X POST 'https://YOUR-SLACK-TEAM.slack.com/api/users.admin.invite' \ 58 | --data 'email=EMAIL&token=TOKEN&set_active=true' \ 59 | --compressed 60 | ``` 61 | 62 | ### Heroku / Azure 63 | 64 | Add the application settings that are defined in the environment variables above. 65 | 66 | ## Run 67 | [Node.js](http://nodejs.org/) is required. 68 | 69 | ```shell 70 | $ git clone https://github.com/outsideris/slack-invite-automation.git 71 | $ cd slack-invite-automation 72 | $ npm install 73 | $ npm start 74 | ``` 75 | 76 | You can access on your web browser. 77 | 78 | ![](https://raw.github.com/outsideris/slack-invite-automation/master/screenshots/join-page.jpg) 79 | 80 | ## Run with Docker 81 | 82 | It's easy to run this service if you have installed Docker on your system. 83 | 84 | ```shell 85 | $ git clone https://github.com/outsideris/slack-invite-automation.git 86 | $ cd slack-invite-automation 87 | $ docker build -t slack-invite-automation . 88 | $ docker run -it --rm -e COMMUNITY_NAME="YOUR-TEAM-NAME" -e SLACK_URL="YOUR-TEAM.slack.com" -e SLACK_TOKEN="YOUR-ACCESS-TOKEN" -p 3000:3000 slack-invite-automation 89 | ``` 90 | 91 | ## Issue token 92 | **You should generate the token in admin user, not owner.** If you generate the token in owner user, a `missing_scope` error may occur. 93 | 94 | There are two ways to issue the access token. 95 | 96 | ### Legacy tokens 97 | 1. Visit . 98 | 1. Click `Create token`. 99 | 100 | ![](https://raw.github.com/outsideris/slack-invite-automation/master/screenshots/legacy-token.gif) 101 | 102 | ### OAuth tokens 103 | 1. Visit and Create New App. 104 | 105 | ![](https://raw.github.com/outsideris/slack-invite-automation/master/screenshots/oauth1.gif) 106 | 107 | 1. Click "Permissions". 108 | 109 | ![](https://raw.github.com/outsideris/slack-invite-automation/master/screenshots/oauth2.gif) 110 | 111 | 1. In "OAuth & Permissions" page, select `admin` scope under "Permission Scopes" menu and save changes. 112 | 113 | ![](https://raw.github.com/outsideris/slack-invite-automation/master/screenshots/oauth3.gif) 114 | 115 | 1. Click "Install App to Team". 116 | 117 | ![](https://raw.github.com/outsideris/slack-invite-automation/master/screenshots/oauth4.gif) 118 | 119 | 1. Visit in your browser and authorize it. 120 | * It authorizes the `client` permission. Otherwise, you can see `{"ok":false,"error":"missing_scope","needed":"client","provided":"admin"}` error. 121 | * Your `CLIENT_ID` could be found in "Basic Information" menu of your app page that you just install. 122 | * Your `TEAM_ID` could be found in 123 | --------------------------------------------------------------------------------