From 1aebb0a7a85fce84cbb163baee225d59e804c46c Mon Sep 17 00:00:00 2001 From: pan261 Date: Mon, 7 Feb 2022 12:41:57 -0500 Subject: [PATCH] added types, formatted files --- environment.d.ts | 4 + src/config.ts | 76 +++++----- src/db.ts | 201 +++++++++++++------------ src/downloader.ts | 89 ++++++----- src/ngrok_script.js | 52 ++++--- src/server.ts | 350 ++++++++++++++++++++++++-------------------- src/webhooks.ts | 288 +++++++++++++++++++----------------- 7 files changed, 569 insertions(+), 491 deletions(-) diff --git a/environment.d.ts b/environment.d.ts index e91e6ca..218060a 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -7,6 +7,10 @@ declare global { PORT_SERVER: number; PORT_DB: number; WEBHOOK_TOKEN: string; + POSTGRES_USER: string; + POSTGRES_DB: string; + POSTGRES_PASSWORD: string; + HOOK_CALLBACK_HOSTNAME: string; } } } diff --git a/src/config.ts b/src/config.ts index 91b6855..1871b73 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,54 +2,60 @@ * Configures constants used in the other parts of this app */ -import { AuthClientThreeLegged } from 'forge-apis'; -import * as dotenv from 'dotenv'; -dotenv.config({path: '../.env'}); +import * as ForgeSDK from "forge-apis"; +import * as dotenv from "dotenv"; +dotenv.config({ path: "../.env" }); -export const ForgeAuthClient = AuthClientThreeLegged; - -export const scopes: any = ['bucket:create', 'bucket:read', 'data:read', 'data:create', 'data:write']; +// export const ForgeAuthClient = ForgeSDK.AuthClientThreeLegged; +export const scopes: ForgeSDK.Scope[] = [ + "bucket:create", + "bucket:read", + "data:read", + "data:create", + "data:write", +]; // TODO: add production environment variables -export const authClient = new ForgeAuthClient( - process.env.FORGE_CLIENT_ID, - process.env.FORGE_CLIENT_SECRET, - process.env.FORGE_CALLBACK_URL, - scopes, - true +export const authClient = new ForgeSDK.AuthClientThreeLegged( + process.env.FORGE_CLIENT_ID, + process.env.FORGE_CLIENT_SECRET, + process.env.FORGE_CALLBACK_URL, + scopes, + true ); -export const projectID = "a.YnVzaW5lc3M6cHVyZHVlMjY5NiMyMDIxMTExNTQ2Njg0NTI1MQ"; +export const projectID: string = + "a.YnVzaW5lc3M6cHVyZHVlMjY5NiMyMDIxMTExNTQ2Njg0NTI1MQ"; // The local fields are only for internal/container use, host paths are specified in the environment export const folderMap: any = { - gantry: { - fusionID: "urn:adsk.wipprod:fs.folder:co.IxiedfeBTOGg6NSIGQhXxQ", - local: "/Gantry" - }, - lathe: { - fusionID: "urn:adsk.wipprod:fs.folder:co.FDWCeyBGQX2rv8xSNWo5lg", - local: "/Lathe" - }, - mill: { - fusionID: "urn:adsk.wipprod:fs.folder:co.nEihcpHUSW-ZsVzU-__1iw", - local: "/Mill" - }, - waterjet: { - fusionID: "urn:adsk.wipprod:fs.folder:co.vJLXAKGbQQayeljr-nwztQ", - local: "/Waterjet" - } + gantry: { + fusionID: "urn:adsk.wipprod:fs.folder:co.IxiedfeBTOGg6NSIGQhXxQ", + local: "/Gantry", + }, + lathe: { + fusionID: "urn:adsk.wipprod:fs.folder:co.FDWCeyBGQX2rv8xSNWo5lg", + local: "/Lathe", + }, + mill: { + fusionID: "urn:adsk.wipprod:fs.folder:co.nEihcpHUSW-ZsVzU-__1iw", + local: "/Mill", + }, + waterjet: { + fusionID: "urn:adsk.wipprod:fs.folder:co.vJLXAKGbQQayeljr-nwztQ", + local: "/Waterjet", + }, }; // dictionary for looking up local directory path based on folder ID export const folderIDtoLocal: any = { - "urn:adsk.wipprod:fs.folder:co.IxiedfeBTOGg6NSIGQhXxQ" : "/Gantry", - "urn:adsk.wipprod:fs.folder:co.FDWCeyBGQX2rv8xSNWo5lg": "/Lathe", - "urn:adsk.wipprod:fs.folder:co.nEihcpHUSW-ZsVzU-__1iw": "/Mill", - "urn:adsk.wipprod:fs.folder:co.vJLXAKGbQQayeljr-nwztQ": "/Waterjet" -} + "urn:adsk.wipprod:fs.folder:co.IxiedfeBTOGg6NSIGQhXxQ": "/Gantry", + "urn:adsk.wipprod:fs.folder:co.FDWCeyBGQX2rv8xSNWo5lg": "/Lathe", + "urn:adsk.wipprod:fs.folder:co.nEihcpHUSW-ZsVzU-__1iw": "/Mill", + "urn:adsk.wipprod:fs.folder:co.vJLXAKGbQQayeljr-nwztQ": "/Waterjet", +}; export const port = process.env.PORT_SERVER; export const forgeClientId = process.env.FORGE_CLIENT_ID; @@ -60,4 +66,4 @@ export const webhookToken = process.env.WEBHOOK_TOKEN; export const postgresUser = process.env.POSTGRES_USER; export const postgresPassword = process.env.POSTGRES_PASSWORD; export const postgresPort = process.env.PORT_DB; -export const postgresDB = process.env.POSTGRES_DB; \ No newline at end of file +export const postgresDB = process.env.POSTGRES_DB; diff --git a/src/db.ts b/src/db.ts index 3ba3686..de2386e 100644 --- a/src/db.ts +++ b/src/db.ts @@ -1,108 +1,119 @@ -import { Client, Query } from 'ts-postgres'; -import { createPool } from 'generic-pool'; -import * as config from './config'; +import { Client, Query, DatabaseError } from "ts-postgres"; +import { createPool } from "generic-pool"; +import * as config from "./config"; -const pool = createPool({ - create: async () => { - return new Promise((resolve, reject) => { - - const client = new Client({ - 'host': 'database', // name of docker container, not 'localhost' - 'port': config.postgresPort, - 'user': config.postgresUser, - 'password': config.postgresPassword, - 'database': config.postgresDB - }); - - client.connect() - .then(() => { - console.log("successfully connected to db") - resolve(client); - }).catch((err: any) => { - console.log() - console.log(err) - reject(err); - }); - }); - }, - destroy: async (client: Client) => { - return client.end() - }, - validate: (client: Client) => { - return Promise.resolve(!client.closed); - } -}, { - testOnBorrow: true, - max: 10 -}) +const pool = createPool( + { + create: async () => { + return new Promise((resolve, reject) => { + const client = new Client({ + host: "database", // name of docker container, not 'localhost' + port: config.postgresPort, + user: config.postgresUser, + password: config.postgresPassword, + database: config.postgresDB, + }); + + client + .connect() + .then(() => { + console.log("successfully connected to db"); + resolve(client); + }) + .catch((err: DatabaseError) => { + console.log(err); + reject(err); + }); + }); + }, + destroy: async (client: Client) => { + return client.end(); + }, + validate: (client: Client) => { + return Promise.resolve(!client.closed); + }, + }, + { + testOnBorrow: true, + max: 10, + } +); const executeQuery = (query: Query) => { - console.log('executing query'); - - const res = pool.acquire(); + console.log("executing query"); - return res.then(async (client: Client) => { - console.log('aquired client from pool') - const result = await client.execute(query); - pool.release(client).then(() => { - console.log("client released back into pool") - }); - return result; - }).catch((err: any) => { - console.log(err); - }) -} + const res = pool.acquire(); + + return res + .then(async (client: Client) => { + console.log("aquired client from pool"); + const result = await client.execute(query); + pool.release(client).then(() => { + console.log("client released back into pool"); + }); + return result; + }) + .catch((err: DatabaseError) => { + console.log(err); + }); +}; export const shutdown = () => { - pool.drain() - .then(() => { - return pool.clear(); - }); -} + pool.drain().then(() => { + return pool.clear(); + }); +}; export const insert = (table: string, params: any) => { + let fileName = params.fileName; + let folderName = params.folderName; + let fusionID = null; + let username = null; + let size = null; + let version = null; - let fileName = params.fileName; - let folderName = params.folderName; - let fusionID = null; - let username = null; - let size = null; - let version = null; + if (params.hasOwnProperty("fusionID")) { + fusionID = params.fusionID; + } + if (params.hasOwnProperty("username")) { + username = params.username; + } + if (params.hasOwnProperty("size")) { + size = params.size; + } + if (params.hasOwnProperty("version")) { + version = params.version; + } - if (params.hasOwnProperty('fusionID')) {fusionID = params.fusionID} - if (params.hasOwnProperty('username')) {username = params.username} - if (params.hasOwnProperty('size')) {size = params.size} - if (params.hasOwnProperty('version')) {version = params.version} - - let query: Query; + let query: Query; - console.log("params: ", params); + console.log("params: ", params); - switch(table) { - case 'fusion': - query = new Query ( - "INSERT INTO fusion (fusion_id, folder_name, file_name, username, size, version)" + - "VALUES ($1, $2, $3, $4, $5, $6)", - [fusionID, folderName, fileName, username, size, version] - ); - break; - case 'local': - query = new Query ( - "INSERT INTO local (file_name, folder_name, fusion_id)" + - "VALUES ($1, $2, $3)", - [fileName, folderName, fusionID] - ); - break; - case 'archive': - query = new Query ( - "INSERT INTO archive (fusion_id, folder_name, file_name, username, size, version)" + - "VALUES ($1, $2, $3, $4, $5, $6)", - [fusionID, folderName, fileName, username, size, version] - ); - break; - default: - throw new Error("Invalid table/params"); - } + switch (table) { + case "fusion": + query = new Query( + "INSERT INTO fusion (fusion_id, folder_name, file_name, username, size, version)" + + "VALUES ($1, $2, $3, $4, $5, $6)", + [fusionID, folderName, fileName, username, size, version] + ); + break; + case "local": + query = new Query( + "INSERT INTO local (file_name, folder_name, fusion_id)" + + "VALUES ($1, $2, $3)", + [fileName, folderName, fusionID] + ); + break; + case "archive": + query = new Query( + "INSERT INTO archive (fusion_id, folder_name, file_name, username, size, version)" + + "VALUES ($1, $2, $3, $4, $5, $6)", + [fusionID, folderName, fileName, username, size, version] + ); + break; + default: + throw new Error("Invalid table/params"); + } - return executeQuery(query); -} \ No newline at end of file + return executeQuery(query); +}; diff --git a/src/downloader.ts b/src/downloader.ts index 45e80fb..97ea851 100644 --- a/src/downloader.ts +++ b/src/downloader.ts @@ -1,51 +1,60 @@ -const fs = require('fs').promises; -const axios = require('axios'); -const ForgeSDK = require('forge-apis'); -const config = require('./config'); +import * as axios from "axios"; +import * as ForgeSDK from "forge-apis"; + +const fs = require("fs").promises; +// const axios = require('axios'); +import * as config from "./config"; const ObjectsApi = new ForgeSDK.ObjectsApi(); /** * Kevin Pan | pan261@purdue.edu | Last Modified: 12/2/2021 - * + * * Downloads a file by using the Forge SDK to make a GET request * on the storageLocation and then saves the file as `filename` under the `destination` directory - * @param {string} storageLocation - * @param {string} fileName - * @param {string} destination - * @param {Object} credentials + * @param {string} storageLocation + * @param {string} fileName + * @param {string} destination + * @param {Object} credentials */ -exports.download = async (projectID: string, itemID: string, fileName: string, destination: string, credentials: any) => { - const storageLocation = await getStorageLocation(projectID, itemID, credentials); - - console.log("Downloading file "+ fileName); +exports.download = async (projectID: string, itemID: string, fileName: string, destination: string, credentials: ForgeSDK.AuthToken) => { + const storageLocation = await getStorageLocation(projectID, itemID, credentials); - ObjectsApi.getObject('wip.dm.prod', storageLocation, {}, config.authClient, credentials) - .then((res: any) => { - console.log("Downloaded file " + fileName + " to " + destination); - fs.writeFile(`${destination}/${fileName}`, res.body) - }) - .catch((err: any) => { - console.log("error while processing " + storageLocation); - console.log(err); - }); -} + console.log("Downloading file " + fileName); -const getStorageLocation = (projectID: string, itemID: string, credentials: any) => { - return axios({ - method: 'GET', - url: `https://developer.api.autodesk.com/data/v1/projects/${projectID}/items/${itemID}`, - headers: { - Authorization: `Bearer ${credentials.access_token}` - }, - }).then((res: any) => { - return res.data.included[0]; - }).then((data: any) => { - var storageID = data.relationships.storage.data.id; - storageID = storageID.substring(storageID.indexOf('/') + 1); - return storageID; - }).catch((err: any) => { - console.log("unable to get storage location for " + itemID + ": " + err.response.status); - }) -} + ObjectsApi.getObject("wip.dm.prod", storageLocation, {}, config.authClient, credentials) + .then((res: ForgeSDK.ApiResponse) => { + console.log("Downloaded file " + fileName + " to " + destination); + fs.writeFile(`${destination}/${fileName}`, res.body); + }) + .catch((err: ForgeSDK.ApiError) => { + console.log("error while processing " + storageLocation); + console.log(err); + }); +}; +const getStorageLocation = (projectID: string, itemID: string, credentials: ForgeSDK.AuthToken) => { + return axios.default({ + method: "GET", + url: `https://developer.api.autodesk.com/data/v1/projects/${projectID}/items/${itemID}`, + headers: { + Authorization: `Bearer ${credentials.access_token}`, + }, + }) + .then((res: axios.AxiosResponse) => { + let data = res.data.included[0]; + let storageID = data.relationships.storage.data.id; + storageID = storageID.substring(storageID.indexOf("/") + 1); + return storageID; + }) + .catch((err: axios.AxiosError) => { + if (err.response) { + console.log( + "unable to get storage location for " + + itemID + + ": " + + err.response.status + ); + } + }); +}; diff --git a/src/ngrok_script.js b/src/ngrok_script.js index 2650b50..48a6ea0 100644 --- a/src/ngrok_script.js +++ b/src/ngrok_script.js @@ -1,25 +1,31 @@ -const axios = require('axios'); -const fs = require('fs'); +const axios = require("axios"); +const fs = require("fs"); axios({ - method: 'GET', - url: 'http://127.0.0.1:4040/api/tunnels' -}).then(res => { - return res.data.tunnels; -}).then(data => { - data.forEach((entry) => { - if (entry.name === 'command_line (http)') { - const data = fs.readFileSync(`${__dirname}/../.env`).toString(); - if (data.includes(entry.public_url)) { - console.log("ngrok callback url not changed"); - return; - } - const newData = data.replace(new RegExp(/http:\/\/.+\.ngrok\.io/g), entry.public_url); - fs.writeFileSync(`${__dirname}/../.env`, newData); - console.log("updated ngrok callback url"); - } - }) -}).catch(err => { - console.log(); - throw("ngrok server not started"); -}); \ No newline at end of file + method: "GET", + url: "http://127.0.0.1:4040/api/tunnels", +}) + .then((res) => { + return res.data.tunnels; + }) + .then((data) => { + data.forEach((entry) => { + if (entry.name === "command_line (http)") { + const data = fs.readFileSync(`${__dirname}/../.env`).toString(); + if (data.includes(entry.public_url)) { + console.log("ngrok callback url not changed"); + return; + } + const newData = data.replace( + new RegExp(/http:\/\/.+\.ngrok\.io/g), + entry.public_url + ); + fs.writeFileSync(`${__dirname}/../.env`, newData); + console.log("updated ngrok callback url"); + } + }); + }) + .catch((err) => { + console.log(); + throw "ngrok server not started"; + }); diff --git a/src/server.ts b/src/server.ts index c15bf00..709cdeb 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,52 +1,63 @@ -import express = require('express'); -import bodyParser = require('body-parser'); -import crypto = require('crypto'); -import * as config from './config'; -import * as db from './db'; -import fs = require('fs'); +import express = require("express"); +import bodyParser = require("body-parser"); +import * as crypto from "crypto"; +import * as config from "./config"; +import * as db from "./db"; -const webhooks = require('./webhooks'); -const downloader = require('./downloader'); -const localhook = require('./localhook'); +import * as ForgeSDK from "forge-apis"; +import { Request, Response, ErrorRequestHandler } from "express"; + +const webhooks = require("./webhooks"); +const downloader = require("./downloader"); +const localhook = require("./localhook"); /** * Kevin Pan | pan261@purdue.edu | Last Modified: 1/24/2022 - * + * * Authentication server used to perform 3-legged auth through browser. - * + * * Uses environment variables: * - FORGE_CLIENT_ID * - FORGE CLIENT_SECRET * - FORGE_CALLBACK_URL * - PORT_SERVER (defaults to 3000) - * + * * These are all configured in config.js - * + * */ -let credentials: any = null; -let refreshTime: any = null; +let credentials: ForgeSDK.AuthToken; +let refreshTime: number; let intervalID: NodeJS.Timeout; const app = express(); -const verifySignature = (req: any, res: any, buf: any, encoding: any) => { - console.log("Verifying webhook callback signature"); - const signature = req.header('x-adsk-signature'); - if(!signature) { return; } - - // use utf-8 encoding by default - const body = buf.toString(encoding); - const hmac = crypto.createHmac('sha1', config.webhookToken); - const calcSignature = 'sha1hash=' + hmac.update(body).digest('hex'); - req.signature_match = (calcSignature === signature); -} - -app.use(bodyParser.json({ - inflate: true, - limit: '1024kb', - type: 'application/json', - verify: verifySignature -})); +const verifySignature = ( + req: any, + res: Response, + buf: Buffer, + encoding: crypto.Encoding +) => { + console.log("Verifying webhook callback signature"); + const signature = req.header("x-adsk-signature"); + if (!signature) { + return; + } + + // use utf-8 encoding by default + const body = buf.toString(encoding); + const hmac = crypto.createHmac("sha1", config.webhookToken); + const calcSignature = "sha1hash=" + hmac.update(body).digest("hex"); + req.signature_match = calcSignature === signature; +}; + +app.use( + bodyParser.json({ + inflate: true, + limit: "1024kb", + type: "application/json", + verify: verifySignature, + }) +); /** * Creates server with three endpoints: @@ -59,143 +70,158 @@ app.use(bodyParser.json({ * @returns express server object */ const createServer = () => { - - const PORT = config.port || 3000; - - if (config.forgeClientId == null || config.forgeClientSecret == null) { - console.error('Missing FORGE_CLIENT_ID or FORGE_CLIENT_SECRET env. variables.'); - return; - } - - // Endpoint to begin authentication process - app.get('/auth', function (req: any, res: any) { - console.log( - '\x1b[96mserver.js::createServer:', - '\x1b[0m/auth endpoint called' - ); - res.redirect(config.authClient.generateAuthUrl("")); - }); - - // Endpoint Forge redirects to after consent screen - app.get('/callback', function (req: any, res: any) { - config.authClient.getToken(req.query.code) - .then((creds) => { - credentials = creds; - refreshTime = creds.expires_in - 300; - res.send('Generated token: ' + credentials.access_token); - console.log( - '\x1b[92mserver.js::createServer:', - '\x1b[0m/callback reached, token generated' - ); - }) - .then(() => { - // run the setup function for webhooks - webhooks.setupHooks(credentials); - - // register secret token - webhooks.setupToken(credentials); - - // pass credentials to local hook - localhook.setCredentials(credentials); - - // sets refresh() function to run on an interval every 55 minutes - intervalID = setInterval(() => refresh(), refreshTime * 1000); // 55 seconds to ms - - // sets timeout for 13 days, before clearing refresh interval and clearing credentials - setTimeout(() => { - credentials = null; - clearInterval(intervalID); - console.log( - '\x1b[93mserver.js::createServer:', - '\x1b[0mRefresh token expiring, need to authenticate again' - ); - }, 13 * 24 * 3600 * 1000); // 13 days to ms - }) - .catch((err: any) => { - console.error(err); - res.send(err); - }); - }); - - // Endpoint for internal use, to get credentials from our auth server - app.get('/credentials', function (req: any, res: any) { - if (credentials) { - console.log( - '\x1b[96mserver.js::createServer:', - '\x1b[0m/credentials endpoint called, credentials returned' - ); - res.send(credentials); - } else { - console.log( - '\x1b[93mserver.js::createServer:', - '\x1b[0m/credentials endpoint called, no credentials found' - ); - res.send('Need to authenticate at localhost:3000/auth'); - } - }); + const PORT = config.port || 3000; - app.post('/hook', function (req: any, res: any) { - if(!req.signature_match) { - console.log('Request received from outside webhooks service') - return res.status(403).send('Not called from webhooks service'); + if (config.forgeClientId == null || config.forgeClientSecret == null) { + console.error("Missing FORGE_CLIENT_ID or FORGE_CLIENT_SECRET env. variables."); + return; } - - res.status(204).send(); - let body = req.body; + // Endpoint to begin authentication process + app.get("/auth", function (req: Request, res: Response) { + console.log( + "\x1b[96mserver.js::createServer:", + "\x1b[0m/auth endpoint called" + ); + res.redirect(config.authClient.generateAuthUrl("")); + }); + + // Endpoint Forge redirects to after consent screen + app.get("/callback", function (req: any, res: Response) { + config.authClient + .getToken(req.query.code) + .then((creds: ForgeSDK.AuthToken) => { + credentials = creds; + refreshTime = creds.expires_in - 300; + res.send("Generated token: " + credentials.access_token); + console.log( + "\x1b[92mserver.js::createServer:", + "\x1b[0m/callback reached, token generated" + ); + }) + .then(() => { + // run the setup function for webhooks + webhooks.setupHooks(credentials); + + // register secret token + webhooks.setupToken(credentials); + + // pass credentials to local hook + localhook.setCredentials(credentials); + + // sets refresh() function to run on an interval every 55 minutes + intervalID = setInterval(() => refresh(), refreshTime * 1000); // 55 seconds to ms + + // sets timeout for 13 days, before clearing refresh interval and clearing credentials + setTimeout(() => { + credentials.access_token = ""; + clearInterval(intervalID); + console.log( + "\x1b[93mserver.js::createServer:", + "\x1b[0mRefresh token expiring, need to authenticate again" + ); + }, 13 * 24 * 3600 * 1000); // 13 days to ms + }) + .catch((err: ForgeSDK.ApiError) => { + console.error(err); + res.send(err); + }); + }); - if (credentials && body.hook.event === "dm.version.added") { - const itemID = body.payload.lineageUrn; - //TODO: check if extension is in the name - const fileName = body.payload.name; - const destination = __dirname + '/file_share' + config.folderIDtoLocal[body.payload.parentFolderUrn]; + // Endpoint for internal use, to get credentials from our auth server + app.get("/credentials", function (req: Request, res: Response) { + if (credentials.access_token !== "") { + console.log( + "\x1b[96mserver.js::createServer:", + "\x1b[0m/credentials endpoint called, credentials returned" + ); + res.send(credentials); + } else { + console.log( + "\x1b[93mserver.js::createServer:", + "\x1b[0m/credentials endpoint called, no credentials found" + ); + res.send("Need to authenticate at localhost:3000/auth"); + } + }); - downloader.download(config.projectID, itemID, fileName, destination, credentials); - } - }); + app.post("/hook", function (req: any, res: Response) { + if (!req.signature_match) { + console.log("Request received from outside webhooks service"); + return res.status(403).send("Not called from webhooks service"); + } + + res.status(204).send(); + + let body: Request["body"] = req.body; + + if (credentials && body.hook.event === "dm.version.added") { + const itemID = body.payload.lineageUrn; + //TODO: check if extension is in the name + const fileName = body.payload.name; + const destination = + __dirname + + "/file_share" + + config.folderIDtoLocal[body.payload.parentFolderUrn]; + + downloader.download( + config.projectID, + itemID, + fileName, + destination, + credentials + ); + } + }); - // Will make some server endpoints to test db - app.get('/db/insert', async function (req: any, res: any) { - const result = await db.insert("local", { - fileName: crypto.randomBytes(10).toString('hex'), - folderName: 'Mill', + // Will make some server endpoints to test db + app.get("/db/insert", async function (req: Request, res: Response) { + const result = await db.insert("local", { + fileName: crypto.randomBytes(10).toString("hex"), + folderName: "Mill", + }); + res.send(result); }); - res.send(result); - }); - // Default endpoint - app.use((err: any, req: any, res: any, next: any) => { - console.error(err); - res.status(err.statusCode).json(err); - }); - const server = app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); }); + const errorHandler: ErrorRequestHandler = (err, req, res, next) => { + console.error(err); + res.status(err.statusCode).json(err); + }; - return server; -} + // Default endpoint + app.use(errorHandler); + + const server = app.listen(PORT, () => { + console.log(`Server listening on port ${PORT}`); + }); + + return server; +}; /** * Used internally to refresh tokens automatically */ const refresh = () => { - config.authClient.refreshToken(credentials, config.scopes) - .then(creds => { - credentials = creds; - console.log( - '\x1b[92mserver.js::refresh:', - '\x1b[0mnew token generated from refresh token:' - ); - console.log(credentials); - - // check on webhooks - webhooks.setupHooks(credentials); - - // refresh localhook credentials - localhook.setCredentials(credentials); - }) - .catch(err => { - console.log(err); - }); + config.authClient + .refreshToken(credentials, config.scopes) + .then((creds: ForgeSDK.AuthToken) => { + credentials = creds; + console.log( + "\x1b[92mserver.js::refresh:", + "\x1b[0mnew token generated from refresh token:" + ); + console.log(credentials); + + // check on webhooks + webhooks.setupHooks(credentials); + + // refresh localhook credentials + localhook.setCredentials(credentials); + }) + .catch((err: ForgeSDK.ApiError) => { + console.log(err); + }); }; const server = createServer(); @@ -205,9 +231,11 @@ const server = createServer(); * With docker, ctrl+c doesn't work (apparently it is not a SIGINT), * need to open a new terminal and run `docker-compose stop` */ -process.on('SIGTERM', async () => { - console.log("shutting down node process") - await db.shutdown(); - if (server) {server.close()} - process.exit(0) -}); \ No newline at end of file +process.on("SIGTERM", async () => { + console.log("shutting down node process"); + await db.shutdown(); + if (server) { + server.close(); + } + process.exit(0); +}); diff --git a/src/webhooks.ts b/src/webhooks.ts index a070ae7..1e7fd85 100644 --- a/src/webhooks.ts +++ b/src/webhooks.ts @@ -1,164 +1,178 @@ -const axios = require('axios'); -import * as config from './config'; - +import * as axios from "axios"; +import * as ForgeSDK from "forge-apis"; +import * as config from "./config"; /** * Kevin Pan | pan261@purdue.edu | Last Modified: 1/24/2022 - * + * * Functions to manage hook lifecycle * setup() checks if hooks are valid and active, if not it will reactivated * and/or recreate the specified hook. There is one hook for each of the * following events: - * + * * - 'dm.version.added' (file added) * - 'dm.version.modified' (file modified) * - dm.version.deleted (file deleted) - * + * */ -const fileShareID = "urn:adsk.wipprod:fs.folder:co.T0n0mYQeS16K0lq1VuuYVQ" +const fileShareID = "urn:adsk.wipprod:fs.folder:co.T0n0mYQeS16K0lq1VuuYVQ"; /** * Function to manage hook lifecycle */ -exports.setupHooks = (credentials: any) => { - console.log("checking hooks"); - axios({ - method: 'GET', - url: 'https://developer.api.autodesk.com/webhooks/v1/hooks', - headers: { - Authorization: `Bearer ${credentials.access_token}` - } - }).then((res: any) => { - return res.data.data; - }).then((data: any) => { - var hookEvents = []; - for (var index in data) { - - const hook = data[index]; +exports.setupHooks = (credentials: ForgeSDK.AuthToken) => { + console.log("checking hooks"); + axios.default({ + method: "GET", + url: "https://developer.api.autodesk.com/webhooks/v1/hooks", + headers: { + Authorization: `Bearer ${credentials.access_token}`, + }, + }) + .then((res: axios.AxiosResponse) => { + let data = res.data.data; + var hookEvents = []; + for (var index in data) { + const hook = data[index]; - if (hook.callbackUrl != config.hookCallbackURL) { // hooks with invalid callbacks will be deleted, not added to current list of hooks - deleteHook(hook.event, hook.hookId, credentials); - } else { - hookEvents.push(hook.event); - if (hook.status === 'inactive') { - console.log("reactivating " + hook.event + " hook"); - reactivateHook(hook.event, hook.hookId, credentials); + if (hook.callbackUrl != config.hookCallbackURL) { + // hooks with invalid callbacks will be deleted, not added to current list of hooks + deleteHook(hook.event, hook.hookId, credentials); + } else { + hookEvents.push(hook.event); + if (hook.status === "inactive") { + console.log("reactivating " + hook.event + " hook"); + reactivateHook(hook.event, hook.hookId, credentials); + } + } } - } - } - // these are the events we want, if not in the list, then register the hook - if (!hookEvents.includes('dm.version.added')) { - createHook('dm.version.added', credentials); - } - - if (!hookEvents.includes('dm.version.modified')) { - createHook('dm.version.modified', credentials); - } + // these are the events we want, if not in the list, then register the hook + if (!hookEvents.includes("dm.version.added")) { + createHook("dm.version.added", credentials); + } - if (!hookEvents.includes('dm.version.deleted')) { - createHook('dm.version.deleted', credentials); - } - }).catch((err: any) => { - console.log(err); - }) -} + if (!hookEvents.includes("dm.version.modified")) { + createHook("dm.version.modified", credentials); + } + if (!hookEvents.includes("dm.version.deleted")) { + createHook("dm.version.deleted", credentials); + } + }) + .catch((err: axios.AxiosError) => { + console.log(err); + }); +}; -const createHook = (event: string, credentials: any) => { - axios({ - method: 'POST', - url: `https://developer.api.autodesk.com/webhooks/v1/systems/data/events/${event}/hooks`, - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${credentials.access_token}` - }, - data: { - autoReactivateHook: "true", - callbackUrl: config.hookCallbackURL, - scope: { - folder: fileShareID - } - } - }).then((res: any) => { - console.log("sucessfully created hook for " + event); - }).catch((err: any) => { - console.log("error creating hook for " + event + ": " + err.response.status); - }); -} +const createHook = (event: string, credentials: ForgeSDK.AuthToken) => { + axios.default({ + method: "POST", + url: `https://developer.api.autodesk.com/webhooks/v1/systems/data/events/${event}/hooks`, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${credentials.access_token}`, + }, + data: { + autoReactivateHook: "true", + callbackUrl: config.hookCallbackURL, + scope: { + folder: fileShareID, + }, + }, + }) + .then((res: axios.AxiosResponse) => { + console.log("sucessfully created hook for " + event); + }) + .catch((err: axios.AxiosError) => { + err.response && console.log("error creating hook for " + event + ": " + err.response.status); + }); +}; -const reactivateHook = (event: string, hookID: string, credentials: any) => { - axios({ - method: 'PATCH', - url: `https://developer.api.autodesk.com/webhooks/v1/systems/data/events/${event}/hooks/${hookID}`, - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${credentials.access_token}` - }, - data: { - status: 'active', - autoReactivateHook: true, - } - }).then((res: any) => { - console.log("successfully reactivated " + event + " hook"); - }).catch((err: any) => { - console.log("error reactivating hook for " + event + ": " + err.response.status); - }); -} +const reactivateHook = (event: string, hookID: string, credentials: ForgeSDK.AuthToken) => { + axios.default({ + method: "PATCH", + url: `https://developer.api.autodesk.com/webhooks/v1/systems/data/events/${event}/hooks/${hookID}`, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${credentials.access_token}`, + }, + data: { + status: "active", + autoReactivateHook: true, + }, + }) + .then((res: axios.AxiosResponse) => { + console.log("successfully reactivated " + event + " hook"); + }) + .catch((err: axios.AxiosError) => { + err.response && console.log("error reactivating hook for " + event + ": " + err.response.status); + }); +}; -const deleteHook = (event:string, hookID: string, credentials: any) => { - axios({ - method: 'DELETE', - url: `https://developer.api.autodesk.com/webhooks/v1/systems/data/events/${event}/hooks/${hookID}`, - headers: { - Authorization: `Bearer ${credentials.access_token}` - } - }).then((res: any) => { - console.log("deleted " + event + " hook with bad callback url"); - }).catch((err: any) => { - console.log("error deleting hook for " + event + ": " + err.response.status); - }); -} +const deleteHook = (event: string, hookID: string, credentials: ForgeSDK.AuthToken) => { + axios.default({ + method: "DELETE", + url: `https://developer.api.autodesk.com/webhooks/v1/systems/data/events/${event}/hooks/${hookID}`, + headers: { + Authorization: `Bearer ${credentials.access_token}`, + }, + }) + .then((res: axios.AxiosResponse) => { + console.log("deleted " + event + " hook with bad callback url"); + }) + .catch((err: axios.AxiosError) => { + if (err.response) { + console.log("error deleting hook for " + event + ": " + err.response.status); + } + }); +}; -exports.setupToken = (credentials: any) => { - console.log("Checking webhook token") - axios({ - method: 'POST', - url: 'https://developer.api.autodesk.com/webhooks/v1/tokens', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${credentials.access_token}` - }, - data: { - token: config.webhookToken - } - }).then((res: any) => { - console.log(res.data.detail); - }).catch((err: any) => { - if (err.response.status == 400) { - console.log("Webhook token already exists") - } else { - console.log("Error creating a new webhook token"); - } - }); -} +exports.setupToken = (credentials: ForgeSDK.AuthToken) => { + console.log("Checking webhook token"); + axios.default({ + method: "POST", + url: "https://developer.api.autodesk.com/webhooks/v1/tokens", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${credentials.access_token}`, + }, + data: { + token: config.webhookToken, + }, + }) + .then((res: axios.AxiosResponse) => { + console.log(res.data.detail); + }) + .catch((err: axios.AxiosError) => { + if (err.response) { + if (err.response.status == 400) { + console.log("Webhook token already exists"); + } else { + console.log("Error creating a new webhook token"); + } + } + }); +}; // not sure how to incorporate this into webhook token workflow -const updateToken = (credentials: any) => { - axios({ - method: 'PUT', - url: 'https://developer.api.autodesk.com/webhooks/v1/tokens/@me', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${credentials.access_token}` - }, - data: { - token: config.webhookToken - } - }).then((res: any) => { - console.log("Successfully updated webhook token"); - }).catch((err: any) => { - console.log("Error updating webhook token"); - }); -} \ No newline at end of file +const updateToken = (credentials: ForgeSDK.AuthToken) => { + axios.default({ + method: "PUT", + url: "https://developer.api.autodesk.com/webhooks/v1/tokens/@me", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${credentials.access_token}`, + }, + data: { + token: config.webhookToken, + }, + }) + .then((res: axios.AxiosResponse) => { + console.log("Successfully updated webhook token"); + }) + .catch((err: axios.AxiosError) => { + console.log("Error updating webhook token"); + }); +};