├── .DS_Store ├── .dockerignore ├── .env.example ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── configs └── redis.js ├── constants └── index.js ├── docker-compose.yml ├── index.js ├── middlewares └── tokenCheck.js ├── package-lock.json ├── package.json ├── routes └── api │ ├── meetings.js │ ├── users.js │ └── webinars.js └── utils ├── errorHandler.js └── token.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zoom/server-to-server-oauth-starter-api/0af0aed5d4e400ae54dbe90a511598505ae51fab/.DS_Store -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile 4 | docker-compose* 5 | .git 6 | .gitignore 7 | .env 8 | .dockerignore 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ZOOM_ACCOUNT_ID= 2 | ZOOM_CLIENT_ID= 3 | ZOOM_CLIENT_SECRET= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true 6 | }, 7 | "extends": "airbnb-base", 8 | "overrides": [ 9 | ], 10 | "parserOptions": { 11 | "ecmaVersion": "latest" 12 | }, 13 | "rules": { 14 | "camelcase": "off", 15 | "no-console": "off" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | npm-debug.log* -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine as base 2 | 3 | WORKDIR /app 4 | COPY package*.json / 5 | EXPOSE 8080 6 | 7 | FROM base as production 8 | ENV NODE_ENV=production 9 | RUN npm ci 10 | COPY . / 11 | CMD ["node", "index.js"] 12 | 13 | FROM base as dev 14 | ENV NODE_ENV=development 15 | RUN npm install -g nodemon && npm install 16 | COPY . / 17 | CMD ["nodemon", "index.js"] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Zoom Video Communications, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # S2S OAuth Starter App 2 | 3 | This boilerplate app creates a functional starting point for building internal applications using Zoom's REST APIs without needing to handle authentication yourself. Add routes or update these starting points with authentication and token refresh already provided by the server. 4 | 5 | Built with [Express](https://expressjs.com/) for the server, [Redis](https://redis.io/) for token storage, and [Axios](https://axios-http.com/docs/intro) for requests, this app is designed as a starting point. To update or add new Zoom API endpoints, add [Express routes](http://expressjs.com/en/5x/api.html#router) in /routes/api (and import them in index.js). 6 | 7 | Note: Zoom server-to-server tokens are scoped during the app creation workflow. Your app's permissions will reflect what you register when setting up the app. 8 | 9 | ## Getting started 10 | 11 | 1. **Create a server-to-server OAuth app** Before cloning, set up your app and collect your credentials. For questions on this, reference the docs on creating a server-to-server app. Make sure you activate the app. Follow our [set up documentation](https://developers.zoom.us/docs/internal-apps/) or[ this video](https://www.youtube.com/watch?v=OkBE7CHVzho) for a more complete walk through. 12 | 2. **Add scopes to your app.** In your app's Scopes tab, add the following scopes: `meeting:write:admin`, `recording:write:admin`, `report:read:admin`, `user:write:admin`, `webinar:write:admin` . _Note: If you add additional API routes to this starter, you may need to add the corresponding scopes._ 13 | 3. **Install and run [Docker Desktop](https://www.docker.com/products/docker-desktop/).** 14 | 4. **Clone this repo** -- `git clone git@github.com:zoom/server-to-server-oauth-starter-api.git`. 15 | 5. **Add environment variables**. Add a **.env** file to the top level of the repository -- `touch .env`. Fill in the following values from your app. The project includes an example in .env.example 16 | 17 | ``` 18 | ZOOM_ACCOUNT_ID= 19 | ZOOM_CLIENT_ID= 20 | ZOOM_CLIENT_SECRET= 21 | ``` 22 | 23 | ## Usage 24 | 25 | To run the project in development mode (with hot reloading): 26 | 27 | `docker-compose up dev` 28 | 29 | To run the project in production mode: 30 | 31 | `docker-compose up prod` 32 | 33 | The app will now be running in a docker container available to test at http://localhost:8080/api/... 34 | 35 | Sending a request to your server's routes, you'll now be able to make requests to Zoom APIs. To test, open up a terminal or a tool like Postman and send a GET request to http://localhost:8080/api/users. If everything's set up, this will return a list of all the users on your account. 36 | 37 | Your server now provides the following API Routes: 38 | 39 | #### Users 40 | 41 | - **GET** /api/users --> _list users_ 42 | - **POST** /api/users/add --> _create users_ 43 | - **GET** /api/users/:userId --> _get a user_ 44 | - **GET** /api/users/:userId/settings --> _get user settings_ 45 | - **PATCH** /api/users/:userId/settings --> _update user settings_ 46 | - **PATCH** /api/users/:userId --> _update a user_ 47 | - **DELETE** /api/users/:userId --> _delete a user_ 48 | - **GET** /api/users/:userId/meetings --> _list meetings_ 49 | - **GET** /api/users/:userId/webinars --> _list webinars_ 50 | - **GET** /api/users/:userId/recordings --> _list all recordings_ 51 | 52 | #### Meetings 53 | 54 | - **GET** /api/meetings/:meetingId --> _get a meeting_ 55 | - **POST** /api/meetings/:userId --> _create a meeting_ 56 | - **PATCH** /api/meetings/:meetingId --> _update a meeting_ 57 | - **DELETE** /api/meetings/:meetingId --> _delete a meeting_ 58 | - **GET** /api/meetings/:meetingId/report/participants --> _get meeting participant reports_ 59 | - **DELETE** /api/meetings/:meetingId/recordings --> _delete meeting recordings_ 60 | 61 | #### Webinars 62 | 63 | - **GET** /api/webinars/:webinarId --> _get a webinar_ 64 | - **POST** /api/webinars/:userId --> _create a webinar_ 65 | - **DELETE** /api/webinars/:webinarId --> _delete a webinar_ 66 | - **PATCH** /api/webinars/:webinarId --> _update a webinar_ 67 | - **GET** /api/webinars/:webinarId/registrants --> _list webinar registrants_ 68 | - **PUT** /api/webinars/:webinarId/registrants/status --> _update registrant's status_ 69 | - **GET** /api/webinars/:webinarId/report/participants --> _get webinar participant reports_ 70 | - **POST** /api/webinars/:webinarId/registrants --> _add a webinar registrant_ 71 | 72 | To stop your container, run the following: 73 | 74 | `docker stop ` or `docker-compose down` 75 | 76 | ## Adding new API routes 77 | 78 | As a starting point, this app predefines API routes in `/routes/api` for Meetings, Webinars, and Users, and Reports. Add new routes or update existing ones with the Zoom APIs you are looking to use. 79 | 80 | If you wanted to add endpoints for Dashboards, for example, create a new route by adding `routes/api/dashboards.js` using `routes/api/meetings.js` as a template: 81 | 82 | ```js 83 | // create a new file: 84 | // routes/api/dashboards.js 85 | const express = require("express"); 86 | const axios = require("axios"); 87 | const qs = require("query-string"); 88 | 89 | const errorHandler = require("../../utils/errorHandler"); 90 | const { ZOOM_API_BASE_URL } = require("../../constants"); 91 | 92 | const router = express.Router(); 93 | 94 | router.get("/metrics/meetings", async (req, res) => { 95 | const { headerConfig, params } = req; 96 | 97 | try { 98 | const request = await axios.get( 99 | `${ZOOM_API_BASE_URL}/metrics/meetings`, 100 | headerConfig 101 | ); 102 | return res.json(request.data); 103 | } catch (err) { 104 | return errorHandler(err, res, `Error fetching list of meetings`); 105 | } 106 | }); 107 | 108 | module.exports = router; 109 | ``` 110 | 111 | In index.js, import the new routes: 112 | 113 | ```js 114 | /** 115 | * Add API Routes w/ tokenCheck middleware 116 | */ 117 | app.use("/api/dashboards", tokenCheck, require("./routes/api/dashboards")); 118 | ``` 119 | 120 | ## Need help? 121 | 122 | For help using this app or any of Zoom's APIs, head to our [Developer Forum](https://devforum.zoom.us/c/api-and-webhooks). 123 | 124 | ### Documentation 125 | 126 | - [API Reference](https://developers.zoom.us/docs/api/) 127 | - [Create a Server-to-Server OAuth App](https://developers.zoom.us/docs/internal-apps/create/) 128 | - [How to create and use a Server to Server OAuth App (Video)](https://www.youtube.com/watch?v=OkBE7CHVzho) 129 | - [Server-to-Server OAuth](https://developers.zoom.us/docs/internal-apps/s2s-oauth/) 130 | -------------------------------------------------------------------------------- /configs/redis.js: -------------------------------------------------------------------------------- 1 | const { createClient } = require('redis'); 2 | 3 | // Socket required for node redis <-> docker-compose connection 4 | const Redis = createClient({ socket: { host: 'redis', port: 6379 } }); 5 | 6 | module.exports = Redis; 7 | -------------------------------------------------------------------------------- /constants/index.js: -------------------------------------------------------------------------------- 1 | const ZOOM_OAUTH_ENDPOINT = 'https://zoom.us/oauth/token'; 2 | const ZOOM_API_BASE_URL = 'https://api.zoom.us/v2'; 3 | 4 | module.exports = { 5 | ZOOM_OAUTH_ENDPOINT, 6 | ZOOM_API_BASE_URL, 7 | }; 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | redis: 5 | image: redis 6 | ports: 7 | - '6379:6379' 8 | 9 | dev: 10 | build: 11 | context: ./ 12 | target: dev 13 | ports: 14 | - '8080:8080' 15 | volumes: 16 | - .:/app 17 | - /app/node_modules 18 | command: npm run dev 19 | environment: 20 | NODE_ENV: development 21 | DEBUG: nodejs-docker-express:* 22 | depends_on: 23 | - redis 24 | restart: always 25 | 26 | prod: 27 | build: 28 | context: ./ 29 | target: production 30 | volumes: 31 | - .:/app 32 | - /app/node_modules 33 | ports: 34 | - '8080:8080' 35 | command: npm run start 36 | environment: 37 | NODE_ENV: production 38 | depends_on: 39 | - redis 40 | restart: always 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * dotenv gives us access to private variables held in a .env file 3 | * never expose this .env file publicly 4 | */ 5 | require('dotenv').config(); 6 | 7 | const express = require('express'); 8 | const cors = require('cors'); 9 | const cookieParser = require('cookie-parser'); 10 | const { debug } = require('node:console'); 11 | 12 | const redis = require('./configs/redis'); 13 | const { tokenCheck } = require('./middlewares/tokenCheck'); 14 | 15 | const app = express(); 16 | 17 | /** 18 | * Default connection to redis - port 6379 19 | * See https://github.com/redis/node-redis/blob/master/docs/client-configuration.md for additional config objects 20 | */ 21 | (async () => { 22 | await redis.connect(); 23 | })(); 24 | 25 | redis.on('connect', (err) => { 26 | if (err) { 27 | console.log('Could not establish connection with redis'); 28 | } else { 29 | console.log('Connected to redis successfully'); 30 | } 31 | }); 32 | 33 | app.use(cookieParser()); 34 | 35 | // Add Global Middlewares 36 | app.use([ 37 | cors(), 38 | express.json(), 39 | express.urlencoded({ extended: false }), 40 | ]); 41 | 42 | app.options('*', cors()); 43 | 44 | /** 45 | * Add API Routes w/ tokenCheck middleware 46 | */ 47 | app.use('/api/users', tokenCheck, require('./routes/api/users')); 48 | app.use('/api/meetings', tokenCheck, require('./routes/api/meetings')); 49 | app.use('/api/webinars', tokenCheck, require('./routes/api/webinars')); 50 | 51 | /** 52 | * API Route Breakdown: 53 | * 54 | * __Users__ 55 | * GET /api/users --> list users - 56 | * POST /api/users/add --> create users - 57 | * GET /api/users/:userId --> get a user - 58 | * GET /api/users/:userId/settings --> get user settings - 59 | * PATCH /api/users/:userId/settings --> update user settings - 60 | * PATCH /api/users/:userId --> update a user - 61 | * DELETE /api/users/:userId --> delete a user - 62 | * GET /api/users/:userId/meetings --> list meetings - 63 | * GET /api/users/:userId/webinars --> list webinars - 64 | * GET /api/users/:userId/recordings --> list all recordings - 65 | * 66 | * __Webinars__ 67 | * GET /api/webinars/:webinarId --> get a webinar - 68 | * POST /api/webinars/:userId --> create a webinar - 69 | * DELETE /api/webinars/:webinarId --> delete a webinar 70 | * PATCH /api/webinars/:webinarId --> update a webinar - 71 | * GET /api/webinars/:webinarId/registrants --> list webinar registrants - 72 | * PUT /api/webinars/:webinarId/registrants/status --> update registrant's status - 73 | * GET /api/webinars/:webinarId/report/participants --> get webinar participant reports - 74 | * POST /api/webinars/:webinarId/registrants --> add a webinar registrant - 75 | * 76 | * __Meetings__ 77 | * GET /api/meetings/:meetingId --> get a meeting - 78 | * POST /api/meetings/:userId -> create a meeting - 79 | * PATCH /api/meetings/:meetingId --> update a meeting - 80 | * DELETE /api/meetings/:meetingId --> delete a meeting - 81 | * GET /api/meetings/:meetingId/report/participants --> get meeting participant reports - 82 | * DELETE /api/meetings/:meetingId/recordings --> delete meeting recordings - 83 | */ 84 | 85 | const PORT = process.env.PORT || 8080; 86 | 87 | const server = app.listen(PORT, () => console.log(`Listening on port ${[PORT]}!`)); 88 | 89 | /** 90 | * Graceful shutdown, removes access_token from redis 91 | */ 92 | const cleanup = async () => { 93 | debug('\nClosing HTTP server'); 94 | await redis.del('access_token'); 95 | server.close(() => { 96 | debug('\nHTTP server closed'); 97 | redis.quit(() => process.exit()); 98 | }); 99 | }; 100 | 101 | process.on('SIGTERM', cleanup); 102 | process.on('SIGINT', cleanup); 103 | -------------------------------------------------------------------------------- /middlewares/tokenCheck.js: -------------------------------------------------------------------------------- 1 | const redis = require('../configs/redis'); 2 | const { getToken, setToken } = require('../utils/token'); 3 | 4 | /** 5 | * Middleware that checks if a valid (not expired) token exists in redis 6 | * If invalid or expired, generate a new token, set in redis, and append to http request 7 | */ 8 | const tokenCheck = async (req, res, next) => { 9 | const redis_token = await redis.get('access_token'); 10 | 11 | let token = redis_token; 12 | 13 | /** 14 | * Redis returns: 15 | * -2 if the key does not exist 16 | * -1 if the key exists but has no associated expire 17 | */ 18 | if (!redis_token || ['-1', '-2'].includes(redis_token)) { 19 | const { access_token, expires_in, error } = await getToken(); 20 | 21 | if (error) { 22 | const { response, message } = error; 23 | return res.status(response?.status || 401).json({ message: `Authentication Unsuccessful: ${message}` }); 24 | } 25 | 26 | setToken({ access_token, expires_in }); 27 | 28 | token = access_token; 29 | } 30 | 31 | req.headerConfig = { 32 | headers: { 33 | Authorization: `Bearer ${token}`, 34 | }, 35 | }; 36 | return next(); 37 | }; 38 | 39 | module.exports = { 40 | tokenCheck, 41 | }; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Zoom S2S API Boilerplate", 3 | "version": "1.0.0", 4 | "description": "Boilerplate express api server with Zoom server-to-server authentication implemented", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "dev": "nodemon index.js" 9 | }, 10 | "author": "Brandon Abajelo", 11 | "license": "ISC", 12 | "dependencies": { 13 | "axios": "^1.6.7", 14 | "cookie-parser": "^1.4.6", 15 | "cors": "^2.8.5", 16 | "dotenv": "^16.0.2", 17 | "express": "^4.18.1", 18 | "query-string": "^7.1.1", 19 | "redis": "^4.3.1" 20 | }, 21 | "devDependencies": { 22 | "eslint": "^8.23.0", 23 | "eslint-config-airbnb-base": "^15.0.0", 24 | "eslint-plugin-import": "^2.26.0", 25 | "nodemon": "^3.0.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /routes/api/meetings.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const axios = require('axios'); 3 | const qs = require('query-string'); 4 | 5 | const errorHandler = require('../../utils/errorHandler'); 6 | const { ZOOM_API_BASE_URL } = require('../../constants'); 7 | 8 | const router = express.Router(); 9 | 10 | /** 11 | * Get a meeting 12 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meeting 13 | */ 14 | router.get('/:meetingId', async (req, res) => { 15 | const { headerConfig, params } = req; 16 | const { meetingId } = params; 17 | 18 | try { 19 | const request = await axios.get(`${ZOOM_API_BASE_URL}/meetings/${meetingId}`, headerConfig); 20 | return res.json(request.data); 21 | } catch (err) { 22 | return errorHandler(err, res, `Error fetching meeting: ${meetingId}`); 23 | } 24 | }); 25 | 26 | /** 27 | * Create a meeting 28 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meetingCreate 29 | */ 30 | router.post('/:userId', async (req, res) => { 31 | const { headerConfig, params, body } = req; 32 | const { userId } = params; 33 | 34 | try { 35 | const request = await axios.post(`${ZOOM_API_BASE_URL}/users/${userId}/meetings`, body, headerConfig); 36 | return res.json(request.data); 37 | } catch (err) { 38 | return errorHandler(err, res, `Error creating meeting for user: ${userId}`); 39 | } 40 | }); 41 | 42 | /** 43 | * Update a meeting 44 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meetingUpdate 45 | */ 46 | router.patch('/:meetingId', async (req, res) => { 47 | const { headerConfig, params, body } = req; 48 | const { meetingId } = params; 49 | 50 | try { 51 | const request = await axios.patch(`${ZOOM_API_BASE_URL}/meetings/${meetingId}`, body, headerConfig); 52 | return res.json(request.data); 53 | } catch (err) { 54 | return errorHandler(err, res, `Error updating meeting: ${meetingId}`); 55 | } 56 | }); 57 | 58 | /** 59 | * Delete a meeting 60 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meetingDelete 61 | */ 62 | router.delete('/:meetingId', async (req, res) => { 63 | const { headerConfig, params } = req; 64 | const { meetingId } = params; 65 | 66 | try { 67 | const request = await axios.delete(`${ZOOM_API_BASE_URL}/meetings/${meetingId}`, headerConfig); 68 | return res.json(request.data); 69 | } catch (err) { 70 | return errorHandler(err, res, `Error deleting meeting: ${meetingId}`); 71 | } 72 | }); 73 | 74 | /** 75 | * Get meeting participant reports 76 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/reportMeetingParticipants 77 | */ 78 | router.get('/:meetingId/report/participants', async (req, res) => { 79 | const { headerConfig, params, query } = req; 80 | const { meetingId } = params; 81 | const { next_page_token } = query; 82 | 83 | try { 84 | const request = await axios.get(`${ZOOM_API_BASE_URL}/report/meetings/${meetingId}/participants?${qs.stringify({ 85 | next_page_token, 86 | })}`, headerConfig); 87 | return res.json(request.data); 88 | } catch (err) { 89 | return errorHandler(err, res, `Error fetching participants for meeting: ${meetingId}`); 90 | } 91 | }); 92 | 93 | /** 94 | * Delete meeting recordings 95 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/recordingDelete 96 | */ 97 | router.delete('/:meetingId/recordings', async (req, res) => { 98 | const { headerConfig, params, query } = req; 99 | const { meetingId } = params; 100 | const { action } = query; 101 | 102 | try { 103 | const request = await axios.delete(`${ZOOM_API_BASE_URL}/meetings/${meetingId}/recordings?${qs.stringify({ action })}`, headerConfig); 104 | return res.json(request.data); 105 | } catch (err) { 106 | return errorHandler(err, res, `Error deleting recordings for meeting: ${meetingId}`); 107 | } 108 | }); 109 | 110 | module.exports = router; 111 | -------------------------------------------------------------------------------- /routes/api/users.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const axios = require('axios'); 3 | const qs = require('query-string'); 4 | 5 | const errorHandler = require('../../utils/errorHandler'); 6 | const { ZOOM_API_BASE_URL } = require('../../constants'); 7 | 8 | const router = express.Router(); 9 | 10 | /** 11 | * List users 12 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/users 13 | */ 14 | router.get('/', async (req, res) => { 15 | const { headerConfig } = req; 16 | const { status, next_page_token } = req.query; 17 | 18 | try { 19 | const request = await axios.get(`${ZOOM_API_BASE_URL}/users?${qs.stringify({ status, next_page_token })}`, headerConfig); 20 | return res.json(request.data); 21 | } catch (err) { 22 | return errorHandler(err, res, 'Error fetching users'); 23 | } 24 | }); 25 | 26 | /** 27 | * Create users 28 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/userCreate 29 | */ 30 | router.post('/add', async (req, res) => { 31 | const { headerConfig, body } = req; 32 | 33 | try { 34 | const request = await axios.post(`${ZOOM_API_BASE_URL}/users`, body, headerConfig); 35 | return res.json(request.data); 36 | } catch (err) { 37 | return errorHandler(err, res, 'Error creating user'); 38 | } 39 | }); 40 | 41 | /** 42 | * Get a user 43 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/user 44 | */ 45 | router.get('/:userId', async (req, res) => { 46 | const { headerConfig, params, query } = req; 47 | const { userId } = params; 48 | const { status } = query; 49 | 50 | try { 51 | const request = await axios.get(`${ZOOM_API_BASE_URL}/users/${userId}?${qs.stringify({ status })}`, headerConfig); 52 | return res.json(request.data); 53 | } catch (err) { 54 | return errorHandler(err, res, `Error fetching user: ${userId}`); 55 | } 56 | }); 57 | 58 | /** 59 | * Get user settings 60 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/userSettings 61 | */ 62 | router.get('/:userId/settings', async (req, res) => { 63 | const { headerConfig, params } = req; 64 | const { userId } = params; 65 | 66 | try { 67 | const request = await axios.get(`${ZOOM_API_BASE_URL}/users/${userId}/settings`, headerConfig); 68 | return res.json(request.data); 69 | } catch (err) { 70 | return errorHandler(err, res, `Error fetching settings for user: ${userId}`); 71 | } 72 | }); 73 | 74 | /** 75 | * Update user settings 76 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/userSettingsUpdate 77 | */ 78 | router.patch('/:userId/settings', async (req, res) => { 79 | const { headerConfig, params, body } = req; 80 | const { userId } = params; 81 | 82 | try { 83 | const request = await axios.patch(`${ZOOM_API_BASE_URL}/users/${userId}/settings`, body, headerConfig); 84 | return res.json(request.data); 85 | } catch (err) { 86 | return errorHandler(err, res, `Error updating settings for user: ${userId}`); 87 | } 88 | }); 89 | 90 | /** 91 | * Update a user 92 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/userUpdate 93 | */ 94 | router.patch('/:userId', async (req, res) => { 95 | const { headerConfig, params, body } = req; 96 | const { userId } = params; 97 | 98 | try { 99 | const request = await axios.patch(`${ZOOM_API_BASE_URL}/users/${userId}`, body, headerConfig); 100 | return res.json(request.data); 101 | } catch (err) { 102 | return errorHandler(err, res, `Error updating user: ${userId}`); 103 | } 104 | }); 105 | 106 | /** 107 | * Delete a user 108 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/userDelete 109 | */ 110 | router.delete('/:userId', async (req, res) => { 111 | const { headerConfig, params, query } = req; 112 | const { userId } = params; 113 | const { action } = query; 114 | 115 | try { 116 | const request = await axios.delete(`${ZOOM_API_BASE_URL}/users/${userId}?${qs.stringify({ action })}`, headerConfig); 117 | return res.json(request.data); 118 | } catch (err) { 119 | return errorHandler(err, res, `Error deleting user: ${userId}`); 120 | } 121 | }); 122 | 123 | /** 124 | * List meetings 125 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meetings 126 | */ 127 | router.get('/:userId/meetings', async (req, res) => { 128 | const { headerConfig, params, query } = req; 129 | const { userId } = params; 130 | const { next_page_token } = query; 131 | 132 | try { 133 | const request = await axios.get(`${ZOOM_API_BASE_URL}/users/${userId}/meetings?${qs.stringify({ next_page_token })}`, headerConfig); 134 | return res.json(request.data); 135 | } catch (err) { 136 | return errorHandler(err, res, `Error fetching meetings for user: ${userId}`); 137 | } 138 | }); 139 | 140 | /** 141 | * List webinars 142 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/webinars 143 | */ 144 | router.get('/:userId/webinars', async (req, res) => { 145 | const { headerConfig, params, query } = req; 146 | const { userId } = params; 147 | const { next_page_token } = query; 148 | 149 | try { 150 | const request = await axios.get(`${ZOOM_API_BASE_URL}/users/${userId}/webinars?${qs.stringify({ next_page_token })}`, headerConfig); 151 | return res.json(request.data); 152 | } catch (err) { 153 | return errorHandler(err, res, `Error fetching webinars for user: ${userId}`); 154 | } 155 | }); 156 | 157 | /** 158 | * List all recordings 159 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/recordingsList 160 | */ 161 | router.get('/:userId/recordings', async (req, res) => { 162 | const { headerConfig, params, query } = req; 163 | const { userId } = params; 164 | const { from, to, next_page_token } = query; 165 | 166 | try { 167 | const request = await axios.get(`${ZOOM_API_BASE_URL}/users/${userId}/recordings?${qs.stringify({ 168 | from, 169 | to, 170 | next_page_token, 171 | })}`, headerConfig); 172 | return res.json(request.data); 173 | } catch (err) { 174 | return errorHandler(err, res, `Error fetching recordings for user: ${userId}`); 175 | } 176 | }); 177 | 178 | module.exports = router; 179 | -------------------------------------------------------------------------------- /routes/api/webinars.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const axios = require('axios'); 3 | const qs = require('query-string'); 4 | 5 | const errorHandler = require('../../utils/errorHandler'); 6 | const { ZOOM_API_BASE_URL } = require('../../constants'); 7 | 8 | const router = express.Router(); 9 | 10 | /** 11 | * Get a webinar 12 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/webinar 13 | */ 14 | router.get('/:webinarId', async (req, res) => { 15 | const { headerConfig, params } = req; 16 | const { webinarId } = params; 17 | 18 | try { 19 | const request = await axios.get(`${ZOOM_API_BASE_URL}/webinars/${webinarId}`, headerConfig); 20 | return res.json(request.data); 21 | } catch (err) { 22 | return errorHandler(err, res, `Error fetching webinar: ${webinarId}`); 23 | } 24 | }); 25 | 26 | /** 27 | * Create a webinar 28 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/webinarCreate 29 | */ 30 | router.post('/:userId', async (req, res) => { 31 | const { headerConfig, body, params } = req; 32 | const { userId } = params; 33 | 34 | try { 35 | const request = await axios.post(`${ZOOM_API_BASE_URL}/users/${userId}/webinars`, body, headerConfig); 36 | return res.json(request.data); 37 | } catch (err) { 38 | return errorHandler(err, res, `Error creating webinar for user: ${userId}`); 39 | } 40 | }); 41 | 42 | /** 43 | * Delete a webinar 44 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/webinarDelete 45 | */ 46 | router.delete('/:webinarId', async (req, res) => { 47 | const { headerConfig, params } = req; 48 | const { webinarId } = params; 49 | 50 | try { 51 | const request = await axios.delete(`${ZOOM_API_BASE_URL}/webinars/${webinarId}`, headerConfig); 52 | return res.json(request.data); 53 | } catch (err) { 54 | return errorHandler(err, res, `Error deleting webinar: ${webinarId}`); 55 | } 56 | }); 57 | 58 | /** 59 | * Update a webinar 60 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/webinarUpdate 61 | */ 62 | router.patch('/:webinarId', async (req, res) => { 63 | const { headerConfig, params, body } = req; 64 | const { webinarId } = params; 65 | 66 | try { 67 | const request = await axios.patch(`${ZOOM_API_BASE_URL}/webinars/${webinarId}`, body, headerConfig); 68 | return res.json(request.data); 69 | } catch (err) { 70 | return errorHandler(err, res, `Error updating webinar: ${webinarId}`); 71 | } 72 | }); 73 | 74 | /** 75 | * List webinar registrants 76 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrants 77 | */ 78 | router.get('/:webinarId/registrants', async (req, res) => { 79 | const { headerConfig, params, query } = req; 80 | const { webinarId } = params; 81 | const { status, next_page_token } = query; 82 | 83 | try { 84 | const request = await axios.get(`${ZOOM_API_BASE_URL}/webinars/${webinarId}/registrants?${qs.stringify({ 85 | status, 86 | next_page_token, 87 | })}`, headerConfig); 88 | return res.json(request.data); 89 | } catch (err) { 90 | return errorHandler(err, res, `Error fetching registrants for webinar: ${webinarId}`); 91 | } 92 | }); 93 | 94 | /** 95 | * Update registrant's status 96 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantStatus 97 | */ 98 | router.put('/:webinarId/registrants/status', async (req, res) => { 99 | const { headerConfig, params, body } = req; 100 | const { webinarId } = params; 101 | 102 | try { 103 | const request = await axios.put(`${ZOOM_API_BASE_URL}/webinars/${webinarId}/registrants/status`, body, headerConfig); 104 | return res.json(request.data); 105 | } catch (err) { 106 | return errorHandler(err, res, 'Error updating webinar registrant status'); 107 | } 108 | }); 109 | 110 | /** 111 | * Get webinar participant reports 112 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/reportWebinarParticipants 113 | */ 114 | router.get('/:webinarId/report/participants', async (req, res) => { 115 | const { headerConfig, params, query } = req; 116 | const { webinarId } = params; 117 | const { next_page_token } = query; 118 | 119 | try { 120 | const request = await axios.get(`${ZOOM_API_BASE_URL}/report/webinars/${webinarId}/participants?${qs.stringify({ 121 | next_page_token, 122 | })}`, headerConfig); 123 | return res.json(request.data); 124 | } catch (err) { 125 | return errorHandler(err, res, `Error fetching webinar participants for webinar: ${webinarId}`); 126 | } 127 | }); 128 | 129 | /** 130 | * Add a webinar registrant 131 | * https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/webinarRegistrantCreate 132 | */ 133 | router.post('/:webinarId/registrants', async (req, res) => { 134 | const { headerConfig, params, body } = req; 135 | const { webinarId } = params; 136 | 137 | try { 138 | const request = await axios.post(`${ZOOM_API_BASE_URL}/webinars/${webinarId}/registrants`, body, headerConfig); 139 | return res.json(request.data); 140 | } catch (err) { 141 | return errorHandler(err, res, `Error creating registrant for webinar: ${webinarId}`); 142 | } 143 | }); 144 | 145 | module.exports = router; 146 | -------------------------------------------------------------------------------- /utils/errorHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {*} error object 3 | * @param {*} res http response 4 | * @param {*} customMessage error message provided by route 5 | * @returns error status with message 6 | */ 7 | const errorHandler = (error, res, customMessage = 'Error') => { 8 | if (!res) return null; 9 | const { status, data } = error?.response || {}; 10 | return res.status(status ?? 500).json({ message: data?.message || customMessage }); 11 | }; 12 | 13 | module.exports = errorHandler; 14 | -------------------------------------------------------------------------------- /utils/token.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const qs = require('query-string'); 3 | 4 | const { ZOOM_OAUTH_ENDPOINT } = require('../constants'); 5 | const redis = require('../configs/redis'); 6 | 7 | /** 8 | * Retrieve token from Zoom API 9 | * 10 | * @returns {Object} { access_token, expires_in, error } 11 | */ 12 | const getToken = async () => { 13 | try { 14 | const { ZOOM_ACCOUNT_ID, ZOOM_CLIENT_ID, ZOOM_CLIENT_SECRET } = process.env; 15 | 16 | const request = await axios.post( 17 | ZOOM_OAUTH_ENDPOINT, 18 | qs.stringify({ grant_type: 'account_credentials', account_id: ZOOM_ACCOUNT_ID }), 19 | { 20 | headers: { 21 | Authorization: `Basic ${Buffer.from(`${ZOOM_CLIENT_ID}:${ZOOM_CLIENT_SECRET}`).toString('base64')}`, 22 | }, 23 | }, 24 | ); 25 | 26 | const { access_token, expires_in } = await request.data; 27 | 28 | return { access_token, expires_in, error: null }; 29 | } catch (error) { 30 | return { access_token: null, expires_in: null, error }; 31 | } 32 | }; 33 | 34 | /** 35 | * Set zoom access token with expiration in redis 36 | * 37 | * @param {Object} auth_object 38 | * @param {String} access_token 39 | * @param {int} expires_in 40 | */ 41 | const setToken = async ({ access_token, expires_in }) => { 42 | await redis.set('access_token', access_token); 43 | await redis.expire('access_token', expires_in); 44 | }; 45 | 46 | module.exports = { 47 | getToken, 48 | setToken, 49 | }; 50 | --------------------------------------------------------------------------------