From 1aebb0a7a85fce84cbb163baee225d59e804c46c Mon Sep 17 00:00:00 2001 From: pan261 Date: Mon, 7 Feb 2022 12:41:57 -0500 Subject: [PATCH 1/5] 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"); + }); +}; From 4b2d28550540d26f4bd67c67177ab7a69ab3ac1d Mon Sep 17 00:00:00 2001 From: pan261 Date: Mon, 7 Feb 2022 13:12:00 -0500 Subject: [PATCH 2/5] converted uploader to forgeSDK --- src/downloader.ts | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/src/downloader.ts b/src/downloader.ts index 97ea851..586a1f7 100644 --- a/src/downloader.ts +++ b/src/downloader.ts @@ -6,6 +6,7 @@ const fs = require("fs").promises; import * as config from "./config"; const ObjectsApi = new ForgeSDK.ObjectsApi(); +const ItemsApi = new ForgeSDK.ItemsApi(); /** * Kevin Pan | pan261@purdue.edu | Last Modified: 12/2/2021 @@ -34,27 +35,13 @@ exports.download = async (projectID: string, itemID: string, fileName: string, d }; 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]; + return ItemsApi.getItem(projectID, itemID, config.authClient, credentials) + .then((res: ForgeSDK.ApiResponse) => { + let data = res.body.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 - ); - } + }).catch((err: ForgeSDK.ApiError) => { + console.log("unable to get storage location for " + itemID + ": " + err.statusCode); }); }; From fc3cebabb4b36ba569ab345977bb0bc6d691dfe2 Mon Sep 17 00:00:00 2001 From: pan261 Date: Tue, 8 Feb 2022 16:31:46 -0500 Subject: [PATCH 3/5] renaming files --- src/forge-delete.ts | 86 ++++++++++++++++++++++++ src/{downloader.ts => forge-download.ts} | 9 ++- src/{uploadfile.ts => forge-upload.ts} | 26 +++---- src/{webhooks.ts => forge-webhooks.ts} | 5 +- src/localhook.ts | 2 +- src/server.ts | 22 ++---- 6 files changed, 114 insertions(+), 36 deletions(-) create mode 100644 src/forge-delete.ts rename src/{downloader.ts => forge-download.ts} (87%) rename src/{uploadfile.ts => forge-upload.ts} (94%) rename src/{webhooks.ts => forge-webhooks.ts} (98%) diff --git a/src/forge-delete.ts b/src/forge-delete.ts new file mode 100644 index 0000000..e7b7c66 --- /dev/null +++ b/src/forge-delete.ts @@ -0,0 +1,86 @@ +import * as ForgeSDK from "forge-apis"; +import * as axios from "axios"; + +const jsonVersion: ForgeSDK.JsonApiVersionJsonapi = {version: "1.0"} +const versionsAPI = new ForgeSDK.VersionsApi(); + +/** + * Kevin Pan | pan261@purdue.edu | Last Modified: 2/7/2021 + * + * Deletes a file using the versions API + * @param {string} projectID + * @param {string} itemID + * @param {ForgeSDK.AuthClient} credentials + */ + +exports.delete = (projectID: string, itemID: string, fileName: string, credentials: ForgeSDK.AuthToken) => { + const data:ForgeSDK.CreateVersionData = { + type: "versions", + attributes: { + name: fileName, + extension: { + type: "versions:autodesk.core:Deleted", + version: "1.0", + schema: { + href: "" + } + } + }, + relationships: { + item: { + data: { + type: "items", + id: itemID + } + } + } + } + + const body: ForgeSDK.CreateVersion = { + jsonapi: jsonVersion, + data: data + } + + axios.default({ + method: 'POST', + url: `https://developer.api.autodesk.com/data/v1/projects/${projectID}/versions`, + headers: { + "Content-Type": "application/vnd.api+json", + Authorization: `Bearer ${credentials.access_token}`, + }, + data: { + jsonapi: { + version: "1.0" + }, + data: { + type: "versions", + attributes: { + extension: { + type: "versions:autodesk.core:Deleted", + version: "1.0", + } + }, + relationships: { + item: { + data: { + type: "items", + id: itemID + } + } + } + } + } + }).then(res => { + console.log("Successfully deleted item with id: " + itemID); + }).catch(err => { + console.log("Error deleting item with id: " + itemID); + }); + + // versionsAPI.postVersion(projectID, body, config.authClient, credentials) + // .then((res: ForgeSDK.ApiResponse) => { + // console.log("Successfully deleted item with " + itemID); + // }).catch((err: ForgeSDK.ApiError) => { + // console.log("Error deleting file: " + itemID + ", Error code: " + err.statusCode); + // console.log(JSON.stringify(err)); + // }); +} \ No newline at end of file diff --git a/src/downloader.ts b/src/forge-download.ts similarity index 87% rename from src/downloader.ts rename to src/forge-download.ts index 586a1f7..204a55a 100644 --- a/src/downloader.ts +++ b/src/forge-download.ts @@ -1,32 +1,31 @@ -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(); const ItemsApi = new ForgeSDK.ItemsApi(); /** - * Kevin Pan | pan261@purdue.edu | Last Modified: 12/2/2021 + * Kevin Pan | pan261@purdue.edu | Last Modified: 2/7/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 {ForgeSDK.AuthClient} credentials */ exports.download = async (projectID: string, itemID: string, fileName: string, destination: string, credentials: ForgeSDK.AuthToken) => { const storageLocation = await getStorageLocation(projectID, itemID, credentials); console.log("Downloading file " + fileName); - ObjectsApi.getObject("wip.dm.prod", storageLocation, {}, config.authClient, credentials) + return 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); + return itemID; }) .catch((err: ForgeSDK.ApiError) => { console.log("error while processing " + storageLocation); diff --git a/src/uploadfile.ts b/src/forge-upload.ts similarity index 94% rename from src/uploadfile.ts rename to src/forge-upload.ts index 24940af..77f9771 100644 --- a/src/uploadfile.ts +++ b/src/forge-upload.ts @@ -17,7 +17,7 @@ function getCreateStorageDataObject(fileName: string, folderID: string): ForgeSD relationships: { target: { data: { - type: "folders", + type: "folders", id: folderID } } @@ -69,7 +69,7 @@ function getCreateItemIncludedObject(fileName: string, objectID: string): ForgeS href: "" } } - }, + }, relationships: { storage: { data: { @@ -116,7 +116,7 @@ function getCreateVersionData(fileName: string, itemID: string):ForgeSDK.CreateV * @param folderID Fusion ID of folder * @param objectID fusion ID of uploaded bucket object * @param credentials Forge Authtoken - * Calls: + * Calls: * @function getCreateItemDataObject * @function getCreateItemIncludedObject */ @@ -136,7 +136,7 @@ function createItem(fileName: string, folderID: string, objectID: string, creden } return true; - }, + }, (err: ForgeSDK.ApiError) => { console.log("API ERROR CODE: ", err.statusCode, "\nMESSAGE: ", err.statusMessage, "\nBODY: ", err.statusBody); return false; @@ -164,12 +164,12 @@ function uploadFileObject(fileName: string, folderName: string, objectID: string const objectName:string = objIDTokens[1].trim(); objectsAPI.uploadObject( - bucketKey, + bucketKey, objectName, - fileBuffer.byteLength, - fileBuffer, - {contentDisposition: fileName}, - config.authClient, + fileBuffer.byteLength, + fileBuffer, + {contentDisposition: fileName}, + config.authClient, credentials).then( (resp: ForgeSDK.ApiResponse) => { if (resp.statusCode != 200) { @@ -188,7 +188,7 @@ function uploadFileObject(fileName: string, folderName: string, objectID: string /** * Wrapper for: @function ProjectsApi.postStorage() - * Extracts necessary information from arguments. + * Extracts necessary information from arguments. * Requests bucket storage for file name in specified project and folder. * On success: Uploads file object to bucket * On fail: Returns @@ -209,7 +209,7 @@ export function uploadFile(fileName: string, folderName: string, credentials: Fo jsonapi: jsonVersion, data: getCreateStorageDataObject(fileName, folderID) } - + projectsAPI.postStorage(config.projectID, body, config.authClient, credentials).then( (resp: ForgeSDK.ApiResponse) => { if (resp.statusCode != 201) { @@ -236,12 +236,12 @@ export function uploadFile(fileName: string, folderName: string, credentials: Fo export function newVersion(fileName: string, folderName: string, credentials: ForgeSDK.AuthToken) { - const folderID: string = config.folderMap[folderName].fusionID; const body: ForgeSDK.CreateVersion = { jsonapi: jsonVersion, data: getCreateVersionData(fileName, "") } - versionsAPI.postVersion(folderID, body, config.authClient, credentials).then( + + versionsAPI.postVersion(config.projectID, body, config.authClient, credentials).then( (resp: ForgeSDK.ApiResponse) => { if (resp.statusCode != 201) { console.log("Allocate Storage Error: ", resp.statusCode); diff --git a/src/webhooks.ts b/src/forge-webhooks.ts similarity index 98% rename from src/webhooks.ts rename to src/forge-webhooks.ts index 1e7fd85..545d3ea 100644 --- a/src/webhooks.ts +++ b/src/forge-webhooks.ts @@ -3,7 +3,7 @@ import * as ForgeSDK from "forge-apis"; import * as config from "./config"; /** - * Kevin Pan | pan261@purdue.edu | Last Modified: 1/24/2022 + * Kevin Pan | pan261@purdue.edu | Last Modified: 2/7/2022 * * Functions to manage hook lifecycle * setup() checks if hooks are valid and active, if not it will reactivated @@ -12,7 +12,7 @@ import * as config from "./config"; * * - 'dm.version.added' (file added) * - 'dm.version.modified' (file modified) - * - dm.version.deleted (file deleted) + * - 'dm.version.deleted' (file deleted) * */ @@ -131,6 +131,7 @@ const deleteHook = (event: string, hookID: string, credentials: ForgeSDK.AuthTok exports.setupToken = (credentials: ForgeSDK.AuthToken) => { console.log("Checking webhook token"); + axios.default({ method: "POST", url: "https://developer.api.autodesk.com/webhooks/v1/tokens", diff --git a/src/localhook.ts b/src/localhook.ts index d18bce7..e9b7855 100644 --- a/src/localhook.ts +++ b/src/localhook.ts @@ -1,6 +1,6 @@ import * as chokidar from 'chokidar'; import { AuthToken } from 'forge-apis'; -import * as uploader from './uploadfile'; +import * as uploader from './forge-upload'; const polltime: number = 100; diff --git a/src/server.ts b/src/server.ts index 709cdeb..b3a244c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -7,8 +7,9 @@ import * as db from "./db"; import * as ForgeSDK from "forge-apis"; import { Request, Response, ErrorRequestHandler } from "express"; -const webhooks = require("./webhooks"); -const downloader = require("./downloader"); +const webhooks = require("./forge-webhooks"); +const downloader = require("./forge-download"); +const deleter = require("./forge-delete"); const localhook = require("./localhook"); /** @@ -145,7 +146,7 @@ const createServer = () => { } }); - app.post("/hook", function (req: any, res: Response) { + app.post("/hook", async 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"); @@ -159,18 +160,9 @@ const createServer = () => { 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 - ); + const destination = __dirname + "/file_share" + config.folderIDtoLocal[body.payload.parentFolderUrn]; + await downloader.download(config.projectID, itemID, fileName, destination, credentials); + await deleter.delete(config.projectID, itemID, fileName, credentials); } }); From ff3d98e3e2dcadc75e51c7422d0af35f0c2ad376 Mon Sep 17 00:00:00 2001 From: pan261 Date: Tue, 8 Feb 2022 22:43:53 -0500 Subject: [PATCH 4/5] running ngrok in docker --- docker-compose.yml | 9 ++++++++ package.json | 2 +- src/forge-webhooks.ts | 18 +++++++-------- src/{ => old}/ngrok_script.js | 0 src/server.ts | 41 ++++++++++++++++++++++++++++++----- 5 files changed, 55 insertions(+), 15 deletions(-) rename src/{ => old}/ngrok_script.js (100%) diff --git a/docker-compose.yml b/docker-compose.yml index cf03a0e..42b5ff5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,14 @@ version: "3.8" services: + ngrok: + image: wernight/ngrok:latest + ports: + - 4040:4040 + environment: + NGROK_PROTOCOL: http + NGROK_PORT: server:3000 + NGROK_AUTH: ${NGROK_AUTH} # access through `psql -h localhost -p 5432 -d filesync -U user`, password is 'pass' # to be able to build after the initializing the db, you may need to run `sudo chown -R $USER db_data/` @@ -19,6 +27,7 @@ services: dockerfile: Dockerfile depends_on: - database + - ngrok env_file: ./.env volumes: - ./src/build:/ffs/ diff --git a/package.json b/package.json index b2779b7..66b41bf 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "start:ngrok": "ngrok http 3000", "test": "echo \"Error: no test specified\" && exit 1", "build": "npm run open:page && npm run build:docker", - "build:docker": "node src/ngrok_script.js && tsc --build && docker-compose up --build --remove-orphans", + "build:docker": "tsc --build && docker-compose up --build --remove-orphans", "open:page": "opener http://localhost:3000/auth" }, "repository": { diff --git a/src/forge-webhooks.ts b/src/forge-webhooks.ts index 545d3ea..245616d 100644 --- a/src/forge-webhooks.ts +++ b/src/forge-webhooks.ts @@ -21,8 +21,8 @@ const fileShareID = "urn:adsk.wipprod:fs.folder:co.T0n0mYQeS16K0lq1VuuYVQ"; /** * Function to manage hook lifecycle */ -exports.setupHooks = (credentials: ForgeSDK.AuthToken) => { - console.log("checking hooks"); +exports.setupHooks = (callbackUrl: string, credentials: ForgeSDK.AuthToken) => { + console.log("Checking hooks"); axios.default({ method: "GET", url: "https://developer.api.autodesk.com/webhooks/v1/hooks", @@ -36,7 +36,7 @@ exports.setupHooks = (credentials: ForgeSDK.AuthToken) => { for (var index in data) { const hook = data[index]; - if (hook.callbackUrl != config.hookCallbackURL) { + if (hook.callbackUrl != callbackUrl) { // hooks with invalid callbacks will be deleted, not added to current list of hooks deleteHook(hook.event, hook.hookId, credentials); } else { @@ -50,15 +50,15 @@ exports.setupHooks = (credentials: ForgeSDK.AuthToken) => { // 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); + createHook("dm.version.added", callbackUrl, credentials); } if (!hookEvents.includes("dm.version.modified")) { - createHook("dm.version.modified", credentials); + createHook("dm.version.modified", callbackUrl, credentials); } if (!hookEvents.includes("dm.version.deleted")) { - createHook("dm.version.deleted", credentials); + createHook("dm.version.deleted", callbackUrl, credentials); } }) .catch((err: axios.AxiosError) => { @@ -66,7 +66,7 @@ exports.setupHooks = (credentials: ForgeSDK.AuthToken) => { }); }; -const createHook = (event: string, credentials: ForgeSDK.AuthToken) => { +const createHook = (event: string, callbackUrl: string, credentials: ForgeSDK.AuthToken) => { axios.default({ method: "POST", url: `https://developer.api.autodesk.com/webhooks/v1/systems/data/events/${event}/hooks`, @@ -76,14 +76,14 @@ const createHook = (event: string, credentials: ForgeSDK.AuthToken) => { }, data: { autoReactivateHook: "true", - callbackUrl: config.hookCallbackURL, + callbackUrl: callbackUrl, scope: { folder: fileShareID, }, }, }) .then((res: axios.AxiosResponse) => { - console.log("sucessfully created hook for " + event); + console.log("Sucessfully created hook for " + event); }) .catch((err: axios.AxiosError) => { err.response && console.log("error creating hook for " + event + ": " + err.response.status); diff --git a/src/ngrok_script.js b/src/old/ngrok_script.js similarity index 100% rename from src/ngrok_script.js rename to src/old/ngrok_script.js diff --git a/src/server.ts b/src/server.ts index b3a244c..0788e38 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,11 +1,13 @@ import express = require("express"); import bodyParser = require("body-parser"); +import * as axios from 'axios'; import * as crypto from "crypto"; import * as config from "./config"; import * as db from "./db"; import * as ForgeSDK from "forge-apis"; import { Request, Response, ErrorRequestHandler } from "express"; +import { resolve } from "path/posix"; const webhooks = require("./forge-webhooks"); const downloader = require("./forge-download"); @@ -60,6 +62,28 @@ app.use( }) ); +const getWebhookCallbackURL = () => { + return axios.default({ + method: "GET", + url: "http://ngrok:4040/api/tunnels", + }) + .then((res: axios.AxiosResponse) => { + let data = res.data.tunnels; + let url: string = ""; + data.forEach((entry: any) => { + if (entry.name === "command_line (http)") { + url = entry.public_url + "/hook" + console.log("Webhook callback url is:", url); + } + }); + + return url; + }) + .catch((err: axios.AxiosError) => { + console.log("ngrok server error:", err.code, err.message); + }); +} + /** * Creates server with three endpoints: * 1. /auth @@ -71,6 +95,7 @@ app.use( * @returns express server object */ const createServer = () => { + const PORT = config.port || 3000; if (config.forgeClientId == null || config.forgeClientSecret == null) { @@ -100,9 +125,12 @@ const createServer = () => { "\x1b[0m/callback reached, token generated" ); }) - .then(() => { + .then(async () => { + // get webhook callback url from ngrok + let webhookUrl = await getWebhookCallbackURL(); + // run the setup function for webhooks - webhooks.setupHooks(credentials); + webhooks.setupHooks(webhookUrl, credentials); // register secret token webhooks.setupToken(credentials); @@ -162,7 +190,7 @@ const createServer = () => { const fileName = body.payload.name; const destination = __dirname + "/file_share" + config.folderIDtoLocal[body.payload.parentFolderUrn]; await downloader.download(config.projectID, itemID, fileName, destination, credentials); - await deleter.delete(config.projectID, itemID, fileName, credentials); + // await deleter.delete(config.projectID, itemID, fileName, credentials); } }); @@ -197,16 +225,19 @@ const createServer = () => { const refresh = () => { config.authClient .refreshToken(credentials, config.scopes) - .then((creds: ForgeSDK.AuthToken) => { + .then(async (creds: ForgeSDK.AuthToken) => { credentials = creds; console.log( "\x1b[92mserver.js::refresh:", "\x1b[0mnew token generated from refresh token:" ); console.log(credentials); + + // check callback URL + let webhookUrl = await getWebhookCallbackURL(); // check on webhooks - webhooks.setupHooks(credentials); + webhooks.setupHooks(webhookUrl, credentials); // refresh localhook credentials localhook.setCredentials(credentials); From 3e45d9058b9bc54d777ef749ab387aa0cd242b34 Mon Sep 17 00:00:00 2001 From: pan261 Date: Fri, 11 Feb 2022 18:18:56 -0500 Subject: [PATCH 5/5] update webhook token --- src/forge-delete.ts | 3 +++ src/forge-webhooks.ts | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/forge-delete.ts b/src/forge-delete.ts index e7b7c66..f4f56cf 100644 --- a/src/forge-delete.ts +++ b/src/forge-delete.ts @@ -1,3 +1,6 @@ +// NOT CURRENTLY USED, ENDPOINTS AND FUNCTIONS DO NOT WORK +//////////////////////////////// + import * as ForgeSDK from "forge-apis"; import * as axios from "axios"; diff --git a/src/forge-webhooks.ts b/src/forge-webhooks.ts index 245616d..c281e99 100644 --- a/src/forge-webhooks.ts +++ b/src/forge-webhooks.ts @@ -149,7 +149,8 @@ exports.setupToken = (credentials: ForgeSDK.AuthToken) => { .catch((err: axios.AxiosError) => { if (err.response) { if (err.response.status == 400) { - console.log("Webhook token already exists"); + console.log("Webhook token already exists, updating"); + updateToken(credentials); } else { console.log("Error creating a new webhook token"); }