├── .gitignore ├── node ├── .gitignore ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src │ ├── app.ts │ ├── controllers │ │ └── base.ts │ ├── server.ts │ ├── types │ │ ├── express-flash.d.ts │ │ ├── fbgraph.d.ts │ │ └── nylas.d.ts │ └── util │ │ ├── logger.ts │ │ └── secrets.ts ├── test │ └── app.test.ts ├── tsconfig.json └── views │ ├── base.mustache │ └── partials │ ├── after_authorized.mustache │ └── before_authorized.mustache ├── python ├── .vscode │ └── settings.json ├── Pipfile ├── Pipfile.lock ├── Procfile ├── README.md ├── app │ ├── __init__.py │ ├── config.py │ └── templates │ │ ├── after_authorized.html │ │ ├── base.html │ │ └── before_authorized.html ├── config.json └── requirements.txt ├── readme.md └── simple-example.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # next.js build output 79 | .next 80 | 81 | # nuxt.js build output 82 | .nuxt 83 | 84 | # gatsby files 85 | .cache/ 86 | public 87 | 88 | # vuepress build output 89 | .vuepress/dist 90 | 91 | # Serverless directories 92 | .serverless/ 93 | 94 | # FuseBox cache 95 | .fusebox/ 96 | 97 | # DynamoDB Local files 98 | .dynamodb/ 99 | 100 | 101 | # Byte-compiled / optimized / DLL files 102 | __pycache__/ 103 | *.py[cod] 104 | *$py.class 105 | 106 | # C extensions 107 | *.so 108 | 109 | # Distribution / packaging 110 | .Python 111 | build/ 112 | develop-eggs/ 113 | dist/ 114 | downloads/ 115 | eggs/ 116 | .eggs/ 117 | lib/ 118 | lib64/ 119 | parts/ 120 | sdist/ 121 | var/ 122 | wheels/ 123 | pip-wheel-metadata/ 124 | share/python-wheels/ 125 | *.egg-info/ 126 | .installed.cfg 127 | *.egg 128 | MANIFEST 129 | 130 | # PyInstaller 131 | # Usually these files are written by a python script from a template 132 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 133 | *.manifest 134 | *.spec 135 | 136 | # Installer logs 137 | pip-log.txt 138 | pip-delete-this-directory.txt 139 | 140 | # Unit test / coverage reports 141 | htmlcov/ 142 | .tox/ 143 | .nox/ 144 | .coverage 145 | .coverage.* 146 | .cache 147 | nosetests.xml 148 | coverage.xml 149 | *.cover 150 | *.py,cover 151 | .hypothesis/ 152 | .pytest_cache/ 153 | 154 | # Translations 155 | *.mo 156 | *.pot 157 | 158 | # Django stuff: 159 | *.log 160 | local_settings.py 161 | db.sqlite3 162 | db.sqlite3-journal 163 | 164 | # Flask stuff: 165 | instance/ 166 | .webassets-cache 167 | 168 | # Scrapy stuff: 169 | .scrapy 170 | 171 | # Sphinx documentation 172 | docs/_build/ 173 | 174 | # PyBuilder 175 | target/ 176 | 177 | # Jupyter Notebook 178 | .ipynb_checkpoints 179 | 180 | # IPython 181 | profile_default/ 182 | ipython_config.py 183 | 184 | # pyenv 185 | .python-version 186 | 187 | # pipenv 188 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 189 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 190 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 191 | # install all needed dependencies. 192 | #Pipfile.lock 193 | 194 | # celery beat schedule file 195 | celerybeat-schedule 196 | 197 | # SageMath parsed files 198 | *.sage.py 199 | 200 | # Environments 201 | .env 202 | .venv 203 | env/ 204 | venv/ 205 | ENV/ 206 | env.bak/ 207 | venv.bak/ 208 | 209 | # Spyder project settings 210 | .spyderproject 211 | .spyproject 212 | 213 | # Rope project settings 214 | .ropeproject 215 | 216 | # mkdocs documentation 217 | /site 218 | 219 | # mypy 220 | .mypy_cache/ 221 | .dmypy.json 222 | dmypy.json 223 | 224 | # Pyre type checker 225 | .pyre/ 226 | .DS_Store 227 | -------------------------------------------------------------------------------- /node/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /node/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 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 | -------------------------------------------------------------------------------- /node/README.md: -------------------------------------------------------------------------------- 1 | # Example: Hosted OAuth + Scheduling 2 | 3 | This is an example project that implements login via [Nylas Hosted OAuth flow](https://docs.nylas.com/reference#oauth) and demonstrates both instant and smart integration types with the Nylas Scheduler. 4 | 5 | This example uses the [Express](https://expressjs.com/) web framework for NodeJS and is written in TypeScript. In order to successfully run this example and go through the Nylas OAuth flow, you need to follow the steps below. 6 | 7 | 8 | ## Get an Application Client ID & Client Secret from Nylas 9 | 10 | To do this, login (or create) your Nylas developer account using the [Nylas Dashboard](https://dashboard.nylas.com/) account. You should see your `client_id` and `client_secret` on the dashboard. 11 | 12 | ## Set Up HTTPS 13 | 14 | The OAuth protocol requires that all communication occur via the secure HTTPS connections, rather than insecure HTTP connections. You can deploy the application to a serivce like Heroku to test it, or configure HTTPS on your computer. Perhaps the simplest is to use [ngrok](https://ngrok.com), a tool that lets you create a secure tunnel from the ngrok website to your computer. Install it from the website, and then run the following command: 15 | 16 | ``` 17 | ngrok http 5000 18 | ``` 19 | 20 | Notice that ngrok will show you two "forwarding" URLs, which may look something like `http://ed90abe7.ngrok.io` and `https://ed90abe7.ngrok.io`. (The hash subdomain will be different for you.) You'll be using the second URL, which starts with `https`. 21 | 22 | ## Set the Nylas Callback URL 23 | 24 | Once you have a HTTPS URL that points to your computer, you'll need to tell Nylas about it. On the [Nylas Dashboard](https://dashboard.nylas.com) click on the Application Dropdown Menu on the left, then "View all Applications". From there, select "Edit" for the app you'd like to use and select the "Application Callbacks" tab. Paste your HTTPS URL into the text field, and add `/login_callback` after it. For example, if your HTTPS URL is `https://ad172180.ngrok.io`, then you would put `https://ad172180.ngrok.io/login_callback` into the text field in the "Application Callbacks" tab. 25 | 26 | Then click the "Add Callback" button to save. 27 | 28 | ## Install the Dependencies 29 | 30 | This project depends on a few third-party Node modules, like `express` and `requests`. These dependencies are listed in the `package.json` file in this directory. 31 | 32 | ``` 33 | npm install 34 | ``` 35 | 36 | ## Run the Example 37 | 38 | Finally, run the example project like this, specifying the redirect URL you configured along with your Nylas client ID and secret as environment variables: 39 | 40 | ``` 41 | REDIRECT_URI=https://ad172180.ngrok.io/login_callback NYLAS_OAUTH_CLIENT_ID=XXX NYLAS_OAUTH_CLIENT_SECRET=XXX npm start 42 | ``` 43 | 44 | Once the server is running, visit the ngrok URL in your browser to test it out! 45 | 46 | 47 | ## Troubleshooting 48 | 49 | * For OAuth to succeed, you need to visit the Ngrok URL in your browser, not localhost. 50 | * For OAuth to succeed, you need to pass the Ngrok redirect URL with the "/login_callback" path to the app as an environment variable. 51 | -------------------------------------------------------------------------------- /node/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | 'ts-jest': { 4 | tsConfig: 'tsconfig.json' 5 | } 6 | }, 7 | moduleFileExtensions: [ 8 | 'ts', 9 | 'js' 10 | ], 11 | transform: { 12 | '^.+\\.(ts|tsx)$': 'ts-jest' 13 | }, 14 | testMatch: [ 15 | '**/test/**/*.test.(ts|js)' 16 | ], 17 | testEnvironment: 'node' 18 | }; 19 | -------------------------------------------------------------------------------- /node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nylas-scheduling-example-nodejs", 3 | "version": "0.1.0", 4 | "description": "A starting point for Node.js express apps with TypeScript", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/Nylas/Nylas-Scheduling-Beta" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "scripts": { 12 | "start": "npm run build && npm run serve", 13 | "build": "tsc", 14 | "serve": "node dist/server.js", 15 | "test": "jest --forceExit --coverage --verbose" 16 | }, 17 | "dependencies": { 18 | "async": "^3.0.1", 19 | "bcrypt-nodejs": "^0.0.3", 20 | "bluebird": "^3.5.5", 21 | "body-parser": "^1.19.0", 22 | "compression": "^1.7.4", 23 | "consolidate": "^0.15.1", 24 | "dotenv": "^8.0.0", 25 | "errorhandler": "^1.5.1", 26 | "express": "^4.17.1", 27 | "express-flash": "0.0.2", 28 | "express-session": "^1.16.2", 29 | "express-validator": "^6.0.1", 30 | "lodash": "^4.17.19", 31 | "lusca": "^1.6.1", 32 | "mustache": "^3.0.1", 33 | "nodemailer": "^6.2.1", 34 | "nylas": "^4.6.1", 35 | "request": "^2.88.0", 36 | "request-promise": "^4.2.4", 37 | "winston": "^2.4.2" 38 | }, 39 | "devDependencies": { 40 | "@types/async": "^3.0.0", 41 | "@types/bcrypt-nodejs": "^0.0.30", 42 | "@types/bluebird": "^3.5.27", 43 | "@types/body-parser": "^1.17.0", 44 | "@types/compression": "^0.0.36", 45 | "@types/consolidate": "^0.14.0", 46 | "@types/dotenv": "^6.1.1", 47 | "@types/errorhandler": "^0.0.32", 48 | "@types/express": "^4.17.0", 49 | "@types/express-session": "^1.15.13", 50 | "@types/jest": "^24.0.15", 51 | "@types/jquery": "^3.3.29", 52 | "@types/lodash": "^4.14.134", 53 | "@types/lusca": "^1.6.0", 54 | "@types/morgan": "^1.7.35", 55 | "@types/node": "^12.0.10", 56 | "@types/nodemailer": "^6.2.0", 57 | "@types/request": "^2.48.1", 58 | "@types/request-promise": "^4.1.44", 59 | "@types/shelljs": "^0.8.5", 60 | "@types/supertest": "^2.0.7", 61 | "@types/winston": "^2.3.9", 62 | "chai": "^4.2.0", 63 | "concurrently": "^4.1.0", 64 | "jest": "^24.8.0", 65 | "shelljs": "^0.8.3", 66 | "supertest": "^4.0.2", 67 | "ts-jest": "^24.0.2", 68 | "ts-node": "^8.3.0", 69 | "typescript": "^3.5.3" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /node/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import flash from "express-flash"; 3 | import session from "express-session"; 4 | import bodyParser from "body-parser"; 5 | import compression from "compression"; // compresses requests 6 | import consolidate from "consolidate"; 7 | import lusca from "lusca"; 8 | import path from "path"; 9 | import Nylas from "nylas"; 10 | 11 | // Controllers (route handlers) 12 | import router from "./controllers/base"; 13 | 14 | const NylasClientID = process.env["NYLAS_OAUTH_CLIENT_ID"]; 15 | const NylasClientSecret = process.env["NYLAS_OAUTH_CLIENT_SECRET"]; 16 | 17 | if (!NylasClientID || !NylasClientSecret) { 18 | console.warn(` 19 | To run this example, pass the NYLAS_OAUTH_CLIENT_ID and NYLAS_OAUTH_CLIENT_SECRET 20 | environment variables when launching the service. eg:\n 21 | REDIRECT_URI=https://ad172180.ngrok.io/login_callback NYLAS_OAUTH_CLIENT_ID=XXX NYLAS_OAUTH_CLIENT_SECRET=XXX npm start 22 | `); 23 | process.exit(1); 24 | } 25 | 26 | // Configure Nylas 27 | Nylas.config({ 28 | appId: NylasClientID, 29 | appSecret: NylasClientSecret 30 | }); 31 | 32 | // Create Express server 33 | const app = express(); 34 | 35 | // assign the mustache enging to .mustache files 36 | app.engine("mustache", consolidate.mustache); 37 | 38 | // Express configuration 39 | app.set("port", process.env["PORT"] || 5000); 40 | app.set("views", path.join(__dirname, "../views")); 41 | app.set("view engine", "mustache"); 42 | app.use(compression()); 43 | app.use(bodyParser.json()); 44 | app.use(bodyParser.urlencoded({ extended: true })); 45 | app.use( 46 | session({ 47 | resave: true, 48 | saveUninitialized: true, 49 | secret: process.env["SECRET_KEY"] || "test-secret", 50 | cookie: { maxAge: 60000 } 51 | }) 52 | ); 53 | app.use(flash()); 54 | app.use(lusca.xframe("SAMEORIGIN")); 55 | app.use(lusca.xssProtection(true)); 56 | 57 | app.use( 58 | express.static(path.join(__dirname, "public"), { maxAge: 31557600000 }) 59 | ); 60 | 61 | app.use(router); 62 | 63 | export default app; 64 | -------------------------------------------------------------------------------- /node/src/controllers/base.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import request from "request-promise"; 3 | import Nylas from "nylas"; 4 | 5 | const redirectURI = process.env.REDIRECT_URI || 6 | "https://nylas-customer-example-nodejs.herokuapp.com/login_callback"; 7 | 8 | const router = express.Router(); 9 | 10 | router.get("/", async (req: Request, res: Response) => { 11 | const accessToken = req.session["access_token"]; 12 | if (!accessToken) { 13 | res.render("base", { 14 | login_href: Nylas.urlForAuthentication({ 15 | redirectURI, 16 | scopes: ["calendar"] 17 | }), 18 | insecure_override: req.protocol !== "https", 19 | partials: { authorization_partial: "partials/before_authorized" } 20 | }); 21 | return; 22 | } 23 | 24 | const nylas = Nylas.with(accessToken); 25 | const account = await nylas.account.get(); 26 | 27 | // If the user has already connected to Nylas via OAuth, 28 | // `nylas.authorized` will be true. Otherwise, it will be false. 29 | const pages = await request.get({ 30 | uri: "https://schedule.api.nylas.com/manage/pages", 31 | headers: { Authorization: `Bearer ${accessToken}` }, 32 | json: true 33 | }); 34 | 35 | res.render("base", { 36 | pages, 37 | account, 38 | accessToken, 39 | partials: { authorization_partial: "partials/after_authorized" } 40 | }); 41 | }); 42 | 43 | router.post("/", async (req: Request, res: Response) => { 44 | const accessToken = req.session["access_token"]; 45 | if (!accessToken) { 46 | res.redirect("/"); 47 | return; 48 | } 49 | 50 | const newPage = await request.post({ 51 | uri: "https://schedule.api.nylas.com/manage/pages", 52 | json: true, 53 | body: { 54 | name: req.body["name"], 55 | slug: req.body["slug"], 56 | access_tokens: [accessToken], 57 | config: { 58 | // You can provide as few or as many page configuration options as you like. 59 | // Check out the Scheduling Page documentation for a full list of settings. 60 | event: { 61 | title: req.body["event_title"] 62 | } 63 | } 64 | } 65 | }); 66 | 67 | res.redirect("/"); 68 | }); 69 | 70 | router.get("/login_callback", async (req: Request, res: Response) => { 71 | if (req.query.error) { 72 | res.status(400).send(`Login error: ${req.query["error"]}`); 73 | return; 74 | } 75 | 76 | const token = await Nylas.exchangeCodeForToken(req.query.code); 77 | 78 | // save the token to the current session, save it to the user model, etc. 79 | req.session["access_token"] = token; 80 | 81 | res.redirect("/"); 82 | }); 83 | 84 | export default router; 85 | -------------------------------------------------------------------------------- /node/src/server.ts: -------------------------------------------------------------------------------- 1 | import errorHandler from "errorhandler"; 2 | 3 | import app from "./app"; 4 | 5 | /** 6 | * Error Handler. Provides full stack - remove for production 7 | */ 8 | app.use(errorHandler()); 9 | 10 | /** 11 | * Start Express server. 12 | */ 13 | const server = app.listen(app.get("port"), () => { 14 | console.log( 15 | " App is running at http://localhost:%d in %s mode", 16 | app.get("port"), 17 | app.get("env") 18 | ); 19 | console.log(" Press CTRL-C to stop\n"); 20 | }); 21 | 22 | export default server; 23 | -------------------------------------------------------------------------------- /node/src/types/express-flash.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /// 3 | 4 | // Add RequestValidation Interface on to Express's Request Interface. 5 | declare namespace Express { 6 | interface Request extends Flash {} 7 | } 8 | 9 | interface Flash { 10 | flash(type: string, message: any): void; 11 | } 12 | 13 | declare module "express-flash"; 14 | 15 | -------------------------------------------------------------------------------- /node/src/types/fbgraph.d.ts: -------------------------------------------------------------------------------- 1 | /** Declaration file generated by dts-gen */ 2 | 3 | export const version: string; 4 | 5 | export function authorize(params: any, callback: any): any; 6 | 7 | export function batch(reqs: any, additionalData: any, callback: any): any; 8 | 9 | export function del(url: any, postData: any, callback: any): any; 10 | 11 | export function extendAccessToken(params: any, callback: any): any; 12 | 13 | export function fql(query: any, params: any, callback: any): any; 14 | 15 | export function get(url: any, params?: any, callback?: any): any; 16 | 17 | export function getAccessToken(): any; 18 | 19 | export function getAppSecret(): any; 20 | 21 | export function getGraphUrl(): any; 22 | 23 | export function getOauthUrl(params: any, opts: any): any; 24 | 25 | export function getOptions(): any; 26 | 27 | export function post(url: any, postData: any, callback: any): any; 28 | 29 | export function search(options: any, callback: any): any; 30 | 31 | export function setAccessToken(token: any): any; 32 | 33 | export function setAppSecret(token: any): any; 34 | 35 | export function setGraphUrl(url: any): any; 36 | 37 | export function setOptions(options: any): any; 38 | 39 | export function setVersion(version: any): any; 40 | 41 | /** 42 | * Fairly incomplete. I only added some commonly used fields. 43 | */ 44 | export type FacebookUser = { 45 | id: string, 46 | name: string, 47 | email: string, 48 | first_name: string, 49 | last_name: string, 50 | gender: string, 51 | link: string, 52 | locale: string, 53 | timezone: number 54 | }; 55 | 56 | -------------------------------------------------------------------------------- /node/src/types/nylas.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'nylas'; -------------------------------------------------------------------------------- /node/src/util/logger.ts: -------------------------------------------------------------------------------- 1 | import { Logger, LoggerOptions, transports } from "winston"; 2 | 3 | const options: LoggerOptions = { 4 | transports: [ 5 | new transports.Console({ 6 | level: process.env.NODE_ENV === "production" ? "error" : "debug" 7 | }), 8 | new transports.File({ filename: "debug.log", level: "debug" }) 9 | ] 10 | }; 11 | 12 | const logger = new Logger(options); 13 | 14 | if (process.env.NODE_ENV !== "production") { 15 | logger.debug("Logging initialized at debug level"); 16 | } 17 | 18 | export default logger; 19 | -------------------------------------------------------------------------------- /node/src/util/secrets.ts: -------------------------------------------------------------------------------- 1 | import logger from "./logger"; 2 | import dotenv from "dotenv"; 3 | import fs from "fs"; 4 | 5 | if (fs.existsSync(".env")) { 6 | logger.debug("Using .env file to supply config environment variables"); 7 | dotenv.config({ path: ".env" }); 8 | } else { 9 | logger.debug("Using .env.example file to supply config environment variables"); 10 | dotenv.config({ path: ".env.example" }); // you can delete this after you create your own .env file! 11 | } 12 | export const ENVIRONMENT = process.env.NODE_ENV; 13 | const prod = ENVIRONMENT === "production"; // Anything else is treated as 'dev' 14 | 15 | export const SESSION_SECRET = process.env["SESSION_SECRET"]; 16 | 17 | if (!SESSION_SECRET) { 18 | logger.error("No client secret. Set SESSION_SECRET environment variable."); 19 | process.exit(1); 20 | } 21 | -------------------------------------------------------------------------------- /node/test/app.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../src/app"; 3 | 4 | describe("GET /random-url", () => { 5 | it("should return 404", (done) => { 6 | request(app).get("/reset") 7 | .expect(404, done); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es2017", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "sourceMap": true, 11 | "outDir": "dist", 12 | "baseUrl": ".", 13 | "paths": { 14 | "*": ["node_modules/*", "src/types/*"] 15 | } 16 | }, 17 | "include": ["src/**/*"] 18 | } 19 | -------------------------------------------------------------------------------- /node/views/base.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | Nylas Scheduling Demo 11 | 12 | 13 | 14 |
15 | {{#flash}} 16 | {{#messages}} 17 | 18 | {{/messages}} 19 | {{/flash}} 20 | 21 |

Nylas Scheduling Demo

22 | {{> authorization_partial}} 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /node/views/partials/after_authorized.mustache: -------------------------------------------------------------------------------- 1 |

2 | Signed in as {{ account.name }} ({{account.emailAddress}}). 3 |

4 |

5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 27 | 28 |
29 |
30 | 31 |

Basic Integration

32 |

33 | The easiest way to integrate scheduling pages into your app is to use the 34 | instant schedule integration type and pass the user's Nylas access_token. 35 | When provided a Nylas access_token, the scheduling widget shows all pages that 36 | use that token. 37 |

38 | 39 | 61 | 62 |
63 | 64 |

Server-side Integration

65 |

66 | This approach allows you to restrict the ability of users to create arbitrary scheduling pages 67 | and also allows for scheduling pages that use more than one Nylas API token. It does not require 68 | putting the Nylas API token in the client-side JavaScript. 69 |

70 | This flow is split into two parts: 71 |

    72 |
  1. 73 | Create a scheduling page via an API call to schedule.api.nylas.com. 74 |
  2. 75 |
  3. 76 | Show scheduling pages to the user by retrieving the pages that include the user's 77 | Nylas API token on the fly. If only certain users should have permission to view or edit pages, 78 | you might want to filter the list that is returned using data in your application. 79 |
  4. 80 |
  5. 81 | Present the built-in configuration modal for individual pages using their edit tokens. 82 | (Optional! You could also build your own editing interface or not allow users to customize the pages.) 83 |
  6. 84 |
85 |

86 |

87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | {{#pages}} 96 | 97 | 101 | 104 | 105 | {{/pages}} 106 |
Your Scheduling Pages
98 | {{name}}
99 | https://schedule.nylas.com/{{ slug }} 100 |
102 | 103 |
107 | 120 |

121 | Create a Scheduling Page 122 |
123 | 124 | 125 | 126 | 127 |
128 |

129 | -------------------------------------------------------------------------------- /node/views/partials/before_authorized.mustache: -------------------------------------------------------------------------------- 1 | {{protocol}} 2 | {{#insecure_override}} 3 | 15 | {{/insecure_override}} 16 | 17 |

Thanks for giving Nylas a try! This demo app implements Nylas authentication and allows you to create and 18 | manage scheduling pages. To get started, click this button to connect to Nylas and login to your account.

19 | Log In 20 | 21 | -------------------------------------------------------------------------------- /python/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/Users/bengotow/.local/share/virtualenvs/customer-example-18suMFtB/bin/python" 3 | } -------------------------------------------------------------------------------- /python/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | nylas = ">=4.7.0" 10 | gunicorn = ">=19.9.0" 11 | requests = "*" 12 | Flask = ">=0.11" 13 | Flask-Dance = ">=0.11.1" 14 | flask-talisman = "*" 15 | werkzeug = "==0.16.0" 16 | 17 | [requires] 18 | python_version = "3.7" 19 | -------------------------------------------------------------------------------- /python/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "06e1712522e02452f4a172ece404abbaf938881def158ca24dbb7ac81f939ca8" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "bump2version": { 20 | "hashes": [ 21 | "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410", 22 | "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6" 23 | ], 24 | "markers": "python_version >= '3.5'", 25 | "version": "==1.0.1" 26 | }, 27 | "bumpversion": { 28 | "hashes": [ 29 | "sha256:4ba55e4080d373f80177b4dabef146c07ce73c7d1377aabf9d3c3ae1f94584a6", 30 | "sha256:4eb3267a38194d09f048a2179980bb4803701969bff2c85fa8f6d1ce050be15e" 31 | ], 32 | "version": "==0.6.0" 33 | }, 34 | "certifi": { 35 | "hashes": [ 36 | "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", 37 | "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" 38 | ], 39 | "version": "==2020.6.20" 40 | }, 41 | "cffi": { 42 | "hashes": [ 43 | "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", 44 | "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", 45 | "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", 46 | "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", 47 | "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", 48 | "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", 49 | "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", 50 | "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", 51 | "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", 52 | "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", 53 | "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", 54 | "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", 55 | "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", 56 | "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", 57 | "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", 58 | "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", 59 | "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", 60 | "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", 61 | "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", 62 | "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", 63 | "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", 64 | "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", 65 | "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", 66 | "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", 67 | "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", 68 | "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", 69 | "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", 70 | "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", 71 | "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", 72 | "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", 73 | "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", 74 | "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", 75 | "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", 76 | "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", 77 | "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", 78 | "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" 79 | ], 80 | "version": "==1.14.3" 81 | }, 82 | "chardet": { 83 | "hashes": [ 84 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 85 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 86 | ], 87 | "version": "==3.0.4" 88 | }, 89 | "click": { 90 | "hashes": [ 91 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 92 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 93 | ], 94 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 95 | "version": "==7.1.2" 96 | }, 97 | "cryptography": { 98 | "hashes": [ 99 | "sha256:07ca431b788249af92764e3be9a488aa1d39a0bc3be313d826bbec690417e538", 100 | "sha256:13b88a0bd044b4eae1ef40e265d006e34dbcde0c2f1e15eb9896501b2d8f6c6f", 101 | "sha256:32434673d8505b42c0de4de86da8c1620651abd24afe91ae0335597683ed1b77", 102 | "sha256:3cd75a683b15576cfc822c7c5742b3276e50b21a06672dc3a800a2d5da4ecd1b", 103 | "sha256:4e7268a0ca14536fecfdf2b00297d4e407da904718658c1ff1961c713f90fd33", 104 | "sha256:545a8550782dda68f8cdc75a6e3bf252017aa8f75f19f5a9ca940772fc0cb56e", 105 | "sha256:55d0b896631412b6f0c7de56e12eb3e261ac347fbaa5d5e705291a9016e5f8cb", 106 | "sha256:5849d59358547bf789ee7e0d7a9036b2d29e9a4ddf1ce5e06bb45634f995c53e", 107 | "sha256:6dc59630ecce8c1f558277ceb212c751d6730bd12c80ea96b4ac65637c4f55e7", 108 | "sha256:7117319b44ed1842c617d0a452383a5a052ec6aa726dfbaffa8b94c910444297", 109 | "sha256:75e8e6684cf0034f6bf2a97095cb95f81537b12b36a8fedf06e73050bb171c2d", 110 | "sha256:7b8d9d8d3a9bd240f453342981f765346c87ade811519f98664519696f8e6ab7", 111 | "sha256:a035a10686532b0587d58a606004aa20ad895c60c4d029afa245802347fab57b", 112 | "sha256:a4e27ed0b2504195f855b52052eadcc9795c59909c9d84314c5408687f933fc7", 113 | "sha256:a733671100cd26d816eed39507e585c156e4498293a907029969234e5e634bc4", 114 | "sha256:a75f306a16d9f9afebfbedc41c8c2351d8e61e818ba6b4c40815e2b5740bb6b8", 115 | "sha256:bd717aa029217b8ef94a7d21632a3bb5a4e7218a4513d2521c2a2fd63011e98b", 116 | "sha256:d25cecbac20713a7c3bc544372d42d8eafa89799f492a43b79e1dfd650484851", 117 | "sha256:d26a2557d8f9122f9bf445fc7034242f4375bd4e95ecda007667540270965b13", 118 | "sha256:d3545829ab42a66b84a9aaabf216a4dce7f16dbc76eb69be5c302ed6b8f4a29b", 119 | "sha256:d3d5e10be0cf2a12214ddee45c6bd203dab435e3d83b4560c03066eda600bfe3", 120 | "sha256:efe15aca4f64f3a7ea0c09c87826490e50ed166ce67368a68f315ea0807a20df" 121 | ], 122 | "version": "==3.2.1" 123 | }, 124 | "flask": { 125 | "hashes": [ 126 | "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", 127 | "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" 128 | ], 129 | "index": "pypi", 130 | "version": "==1.1.2" 131 | }, 132 | "flask-dance": { 133 | "hashes": [ 134 | "sha256:6fc8b53994de627981b7c1073144604279f1f71003a20aa26b2defdc7d3d8772", 135 | "sha256:b4603d19f49ea820d05b133ecfc16e664cd5fe8dba6dbf4ae40bfe91503ae33a", 136 | "sha256:d240f2509db7a9e95482dc4ba79ad1ffc631ec993da99bdfe9988cfe5b816734" 137 | ], 138 | "index": "pypi", 139 | "version": "==3.1.0" 140 | }, 141 | "flask-talisman": { 142 | "hashes": [ 143 | "sha256:468131464a249274ed226efc21b372518f442487e58918ccab8357eaa638fd1f", 144 | "sha256:eaa754f4b771dfbe473843391d69643b79e3a38c865790011ac5e4179c68e3ec" 145 | ], 146 | "index": "pypi", 147 | "version": "==0.7.0" 148 | }, 149 | "gunicorn": { 150 | "hashes": [ 151 | "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", 152 | "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" 153 | ], 154 | "index": "pypi", 155 | "version": "==20.0.4" 156 | }, 157 | "idna": { 158 | "hashes": [ 159 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 160 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 161 | ], 162 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 163 | "version": "==2.10" 164 | }, 165 | "itsdangerous": { 166 | "hashes": [ 167 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 168 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 169 | ], 170 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 171 | "version": "==1.1.0" 172 | }, 173 | "jinja2": { 174 | "hashes": [ 175 | "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", 176 | "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" 177 | ], 178 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 179 | "version": "==2.11.2" 180 | }, 181 | "markupsafe": { 182 | "hashes": [ 183 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 184 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 185 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 186 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 187 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 188 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 189 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 190 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 191 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 192 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 193 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 194 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 195 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 196 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 197 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 198 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 199 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 200 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 201 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 202 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 203 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 204 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 205 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 206 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 207 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 208 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 209 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 210 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 211 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 212 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 213 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 214 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 215 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" 216 | ], 217 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 218 | "version": "==1.1.1" 219 | }, 220 | "nylas": { 221 | "hashes": [ 222 | "sha256:64b480881b7cf78571330463560374f6f1b368f3b7596cec89a2fe076e0a3640", 223 | "sha256:76e4991d9f80c497a9af9167782171e2981deb189eb38caedfd1187d0cdddb8c", 224 | "sha256:95b3a958cce85595f0ebb96e424fb6e1f30de046ddffa940ca00163de6696eba" 225 | ], 226 | "index": "pypi", 227 | "version": "==4.12.1" 228 | }, 229 | "oauthlib": { 230 | "hashes": [ 231 | "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", 232 | "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" 233 | ], 234 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 235 | "version": "==3.1.0" 236 | }, 237 | "pycparser": { 238 | "hashes": [ 239 | "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", 240 | "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" 241 | ], 242 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 243 | "version": "==2.20" 244 | }, 245 | "pyopenssl": { 246 | "hashes": [ 247 | "sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504", 248 | "sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507" 249 | ], 250 | "version": "==19.1.0" 251 | }, 252 | "requests": { 253 | "extras": [ 254 | "security" 255 | ], 256 | "hashes": [ 257 | "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", 258 | "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" 259 | ], 260 | "index": "pypi", 261 | "version": "==2.24.0" 262 | }, 263 | "requests-oauthlib": { 264 | "hashes": [ 265 | "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", 266 | "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", 267 | "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" 268 | ], 269 | "version": "==1.3.0" 270 | }, 271 | "six": { 272 | "hashes": [ 273 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 274 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 275 | ], 276 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 277 | "version": "==1.15.0" 278 | }, 279 | "urllib3": { 280 | "hashes": [ 281 | "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", 282 | "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" 283 | ], 284 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 285 | "version": "==1.25.11" 286 | }, 287 | "urlobject": { 288 | "hashes": [ 289 | "sha256:47b2e20e6ab9c8366b2f4a3566b6ff4053025dad311c4bb71279bbcfa2430caa" 290 | ], 291 | "version": "==2.4.3" 292 | }, 293 | "werkzeug": { 294 | "hashes": [ 295 | "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", 296 | "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4" 297 | ], 298 | "index": "pypi", 299 | "version": "==0.16.0" 300 | } 301 | }, 302 | "develop": {} 303 | } 304 | -------------------------------------------------------------------------------- /python/Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:app -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # Example: Hosted OAuth + Scheduling 2 | 3 | This is an example project that implements login via [Nylas Hosted OAuth flow](https://docs.nylas.com/reference#oauth) and demonstrates both instant and smart integration types with the Nylas Scheduler. 4 | 5 | Try it out now! Visit [http://nylas-customer-example.herokuapp.com/](http://nylas-customer-example.herokuapp.com/). 6 | 7 | This example uses the [Flask](http://flask.pocoo.org/) web framework for Python. In order to successfully run this example and go through the Nylas OAuth flow, you need to follow the steps below. 8 | 9 | 10 | ## Get an Application Client ID & Client Secret from Nylas 11 | 12 | To do this, login (or create) your Nylas developer account using the [Nylas Dashboard](https://dashboard.nylas.com/) account. You should see your `client_id` and `client_secret` on the dashboard. 13 | 14 | ## Update the `config.json` File 15 | 16 | Open the `config.json` file in this directory, and replace the example `client_id` and `client_secret` with the real values that you got from the Nylas Developer dashboard. You'll also need to replace the example secret key with any random string of letters and numbers: a keyboard mash will do. 17 | 18 | ## Set Up HTTPS 19 | 20 | The OAuth protocol requires that all communication occur via the secure HTTPS connections, rather than insecure HTTP connections. There are several ways to set up HTTPS on your computer, but perhaps the simplest is to use 21 | [ngrok](https://ngrok.com), a tool that lets you create a secure tunnel from the ngrok website to your computer. Install it from the website, and then run the following command: 22 | 23 | ``` 24 | ngrok http 5000 25 | ``` 26 | 27 | Notice that ngrok will show you two "forwarding" URLs, which may look something like `http://ed90abe7.ngrok.io` and `https://ed90abe7.ngrok.io`. (The hash subdomain will be different for you.) You'll be using the second URL, which starts with `https`. 28 | 29 | Alternatively, you can set the `OAUTHLIB_INSECURE_TRANSPORT` environment variable in your shell, to disable the HTTPS check. That way, you'll be able to use `localhost` to refer to your app, instead of an ngrok URL. However, be aware that you won't be able to do this when you deploy your app to production, so it's usually a better idea to set up HTTPS properly. 30 | 31 | ## Set the Nylas Callback URL 32 | 33 | Once you have a HTTPS URL that points to your computer, you'll need to tell Nylas about it. On the [Nylas Dashboard](https://dashboard.nylas.com) click on the Application Dropdown Menu on the left, then "View all Applications". From there, select "Edit" for the app you'd like to use and select the "Application Callbacks" tab. Paste your HTTPS URL into the text field, and add `/login/nylas/authorized` after it. For example, if your HTTPS URL is `https://ad172180.ngrok.io`, then you would put `https://ad172180.ngrok.io/login/nylas/authorized` into the text field in the "Application Callbacks" tab. 34 | 35 | Then click the "Add Callback" button to save. 36 | 37 | ## Install the Dependencies 38 | 39 | This project depends on a few third-party Python modules, like Flask. These dependencies are listed in the `requirements.txt` file in this directory. To get started, install [pipenv](https://pypi.org/project/pipenv/) and run the command below to setup your python virtualenv for the example app: 40 | 41 | ``` 42 | pipenv install 43 | ``` 44 | 45 | ## Run the Example 46 | 47 | Finally, run the example project like this: 48 | 49 | ``` 50 | pipenv run flask run 51 | ``` 52 | 53 | Once the server is running, visit the ngrok URL in your browser to test it out! 54 | -------------------------------------------------------------------------------- /python/app/__init__.py: -------------------------------------------------------------------------------- 1 | # Imports from the Python standard library 2 | import os 3 | import sys 4 | import textwrap 5 | import requests 6 | from json import dumps 7 | from flask import Flask, session, request, render_template, redirect, url_for, flash 8 | from flask_talisman import Talisman 9 | from werkzeug.contrib.fixers import ProxyFix 10 | from nylas import APIClient 11 | 12 | # This example uses Flask, a micro web framework written in Python. 13 | # For more information, check out the documentation: http://flask.pocoo.org 14 | # Create a Flask app, and load the configuration file. 15 | app = Flask(__name__) 16 | app_settings = os.getenv("APP_SETTINGS", "app.config.DevelopmentConfig") 17 | app.config.from_object(app_settings) 18 | 19 | # force SSL, enable HTTP Strict Transport Security 20 | csp = {"default-src": "'self' 'unsafe-inline' *.nylas.com maxcdn.bootstrapcdn.com" } 21 | Talisman(app, content_security_policy=csp) 22 | 23 | # Check for dummy configuration values. 24 | # If you are building your own application based on this example, 25 | # you can remove this check from your code. 26 | cfg_needs_replacing = [ 27 | key 28 | for key, value in app.config.items() 29 | if isinstance(value, str) and value.startswith("replace me") 30 | ] 31 | if cfg_needs_replacing: 32 | message = textwrap.dedent( 33 | """ 34 | This example will only work if you replace the fake configuration 35 | values in `config.json` with real configuration values. 36 | The following config values need to be replaced: 37 | {keys} 38 | Consult the README.md file in this directory for more information. 39 | """ 40 | ).format(keys=", ".join(cfg_needs_replacing)) 41 | print(message, file=sys.stderr) 42 | sys.exit(1) 43 | 44 | # Teach Flask how to find out that it's behind an ngrok proxy 45 | app.wsgi_app = ProxyFix(app.wsgi_app) 46 | 47 | redirect_url = "https://nylas-customer-example.herokuapp.com/login_callback" 48 | client = APIClient(app.config["NYLAS_OAUTH_CLIENT_ID"], 49 | app.config["NYLAS_OAUTH_CLIENT_SECRET"]) 50 | 51 | 52 | # Define what Flask should do when someone visits the root URL of this website. 53 | @app.route("/", methods=['GET']) 54 | def index(): 55 | # If the user has already connected to Nylas via OAuth, 56 | # `nylas.authorized` will be True. Otherwise, it will be False. 57 | if session.get('access_token') is not None: 58 | # If we've gotten to this point, then the user has already connected 59 | # to Nylas via OAuth. Let's set up the SDK client with the OAuth token: 60 | client.access_token = session['access_token'] 61 | 62 | # We'll use the Nylas client to fetch information from Nylas 63 | # about the current user, and pass that to the template. 64 | try: 65 | account = client.account 66 | except requests.HTTPError: 67 | # The user's token may be invalid - log them out. 68 | return redirect("/logout") 69 | 70 | # Server-side scheduling page integration: fetch the list of pages that 71 | # include the current user's API token 72 | response = requests.get('https://schedule.api.nylas.com/manage/pages', 73 | headers=dict(Authorization="Bearer " + session['access_token']), 74 | ) 75 | pages = response.json() 76 | return render_template("after_authorized.html", account=account, pages=pages) 77 | 78 | # OAuth requires HTTPS. The template will display a handy warning, 79 | # unless we've overridden the check. 80 | login_callback_var = client.authentication_url(redirect_url) 81 | 82 | return render_template( 83 | "before_authorized.html", login_callback_var=login_callback_var 84 | ) 85 | 86 | @app.route("/", methods=['POST']) 87 | def index_create_page(): 88 | if session.get('access_token') is None: 89 | return redirect(url_for('index')) 90 | 91 | json = { 92 | "name": request.form["name"], 93 | "slug": request.form["slug"], 94 | "config": { 95 | # You can provide as few or as many page configuration options as you like. 96 | # Check out the Scheduling Page documentation for a full list of settings. 97 | "event": { 98 | "title": request.form["event_title"], 99 | }, 100 | }, 101 | "access_tokens": [session['access_token']] 102 | } 103 | 104 | response = requests.post('https://schedule.api.nylas.com/manage/pages', json=json) 105 | if response.status_code != 201: 106 | data = response.json() 107 | print(dumps(data)) 108 | flash(data['message']) 109 | return redirect(url_for('index')) 110 | 111 | @app.route("/login_callback") 112 | def login_callback(): 113 | if 'error' in request.args: 114 | return "Login error: {0}".format(request.args['error']) 115 | 116 | # Exchange the authorization code for an access token 117 | code = request.args.get('code') 118 | session['access_token'] = client.token_for_code(code) 119 | print(session['access_token']) 120 | return redirect("/") 121 | 122 | 123 | @app.route("/logout", methods=['GET', 'POST']) 124 | def logout(): 125 | del session['access_token'] 126 | return redirect("/") 127 | 128 | def ngrok_url(): 129 | """ 130 | If ngrok is running, it exposes an API on port 4040. We can use that 131 | to figure out what URL it has assigned, and suggest that to the user. 132 | https://ngrok.com/docs#list-tunnels 133 | """ 134 | try: 135 | ngrok_resp = requests.get("http://localhost:4040/api/tunnels") 136 | except requests.ConnectionError: 137 | # I guess ngrok isn't running. 138 | return None 139 | ngrok_data = ngrok_resp.json() 140 | secure_urls = [ 141 | tunnel["public_url"] 142 | for tunnel in ngrok_data["tunnels"] 143 | if tunnel["proto"] == "https" 144 | ] 145 | return secure_urls[0] 146 | 147 | 148 | # When this file is executed, run the Flask web server. 149 | if __name__ == "__main__": 150 | url = ngrok_url() 151 | if url: 152 | print(" * Visit {url} to view this Nylas example".format(url=url)) 153 | 154 | app.run() 155 | -------------------------------------------------------------------------------- /python/app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | base_dir = os.path.abspath(os.path.dirname(__file__)) 4 | 5 | 6 | class BaseConfig: 7 | """ 8 | Base application configuration 9 | """ 10 | 11 | DEBUG = False 12 | SECRET_KEY = os.getenv("SECRET_KEY", "houdsfhoisd") 13 | NYLAS_OAUTH_CLIENT_ID = os.getenv("NYLAS_OAUTH_CLIENT_ID", "") 14 | NYLAS_OAUTH_CLIENT_SECRET = os.getenv("NYLAS_OAUTH_CLIENT_SECRET", "") 15 | 16 | 17 | class DevelopmentConfig(BaseConfig): 18 | """ 19 | Development application configuration 20 | """ 21 | 22 | DEBUG = True 23 | 24 | 25 | class TestingConfig(BaseConfig): 26 | """ 27 | Testing application configuration 28 | """ 29 | 30 | DEBUG = True 31 | TESTING = True 32 | 33 | 34 | class ProductionConfig(BaseConfig): 35 | """ 36 | Production application configuration 37 | """ 38 | 39 | DEBUG = False 40 | -------------------------------------------------------------------------------- /python/app/templates/after_authorized.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block body %} 3 |

4 | Signed in as {{ account.name }} ({{account.email_address}}). 5 |

6 |

7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 29 | 30 |
31 |
32 | 33 |

Basic Integration

34 |

35 | The easiest way to integrate scheduling pages into your app is to use the built-in management 36 | UI and pass the user's Nylas API token. When provided a Nylas API token, the scheduling widget 37 | shows all pages that use the token. 38 |

39 | 40 | 62 | 63 |
64 | 65 |

Server-side Integration

66 |

67 | This approach allows you to restrict the ability of users to create arbitrary scheduling pages 68 | and also allows for scheduling pages that use more than one Nylas API token. It does not require 69 | putting the Nylas API token in the client-side JavaScript. 70 |

71 | This flow is split into two parts: 72 |

    73 |
  1. 74 | Create a scheduling page via an API call to schedule.api.nylas.com. 75 |
  2. 76 |
  3. 77 | Show scheduling pages to the user by retrieving the pages that include the user's 78 | Nylas API token on the fly. If only certain users should have permission to view or edit pages, 79 | you might want to filter the list that is returned using data in your application. 80 |
  4. 81 |
  5. 82 | Present the built-in configuration modal for individual pages using their edit tokens. 83 | (Optional! You could also build your own editing interface or not allow users to customize the pages.) 84 |
  6. 85 |
86 |

87 |

88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | {% for page in pages %} 96 | 97 | 101 | 104 | 105 | {% endfor %} 106 |
Your Scheduling Pages
98 | {{page.name}}
99 | https://schedule.nylas.com/{{ page.slug }} 100 |
102 | 103 |
107 | 120 |

121 | Create a Scheduling Page 122 |
123 | 124 | 125 | 126 | 127 |
128 |

129 | 130 | {% endblock %} 131 | -------------------------------------------------------------------------------- /python/app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | Nylas Scheduling Demo 11 | 12 | 13 | 14 |
15 | {% with messages = get_flashed_messages() %} 16 | {% if messages %} 17 | {% for message in messages %} 18 | 19 | {% endfor %} 20 | {% endif %} 21 | {% endwith %} 22 | 23 |

Nylas Scheduling Demo

24 | {% block body %}{% endblock %} 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /python/app/templates/before_authorized.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block body %} {% if not insecure_override %} 2 | 14 | {% endif %} 15 | 16 |

Thanks for giving Nylas a try! This demo app implements Nylas authentication and allows you to create and 17 | manage scheduling pages. To get started, click this button to connect to Nylas and login to your account.

18 | Log In 19 | 20 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /python/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "SECRET_KEY": "any-random-string", 3 | "NYLAS_OAUTH_CLIENT_ID": "your-client-id", 4 | "NYLAS_OAUTH_CLIENT_SECRET": "your-client-secret" 5 | } 6 | -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=1.0.2 2 | nylas>=4.7.0 3 | gunicorn>=19.9.0 4 | werkzeug==0.16.0 5 | requests 6 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Nylas Scheduler Sample Code 🎯 4 | 5 | - **[Quick Start Simple Example](https://github.com/nylas/scheduler-examples/tree/master/simple-example.html)**: A simple html file that is the quickest way to 6 | test out the schedule editor and create your own scheduling page. Showcases the instant integration type. You'll need to get your own access token through the [Nylas Dashboard](https://dashboard.nylas.com) to run this example. 7 | 8 | - **[NodeJS Hosted Auth + Scheduling](https://github.com/nylas/scheduler-examples/tree/master/node)**: A NodeJS / TypeScript example that showcases both instant and smart integration types. Also demonstrates how to theme the schedule editor to match your application. 9 | 10 | - **[Python Hosted Auth + Scheduling](https://github.com/nylas/scheduler-examples/tree/master/python)**: A Python example that showcases both instant and smart integration types. Also demonstrates how to theme the schedule editor to match your application. 11 | -------------------------------------------------------------------------------- /simple-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Nylas Scheduler Simple Example 7 | 8 | 9 | 10 | 11 | 12 | 13 | 39 | 40 | 41 | 42 | 43 | --------------------------------------------------------------------------------