diff --git a/environment.d.ts b/environment.d.ts new file mode 100644 index 0000000..507be66 --- /dev/null +++ b/environment.d.ts @@ -0,0 +1,14 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + FORGE_CLIENT_ID: string; + FORGE_CLIENT_SECRET: string; + FORGE_CALLBACK_URL: string; + PORT: string; + } + } + } + + // If this file has no import/export statements (i.e. is a script) + // convert it into a module by adding an empty export statement. + export {} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..184e30e --- /dev/null +++ b/src/config.ts @@ -0,0 +1,61 @@ +/** + * Configures constants used in the other parts of this app + */ + +import { AuthClientThreeLegged } 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']; + + +// 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 fileShareProjectID = "a.YnVzaW5lc3M6cHVyZHVlMjY5NiMyMDIxMTExNTQ2Njg0NTI1MQ"; + +export const folderMap = { + gantry: { + fusionID: "urn:adsk.wipprod:fs.folder:co.IxiedfeBTOGg6NSIGQhXxQ", + local: "File Share/Gantry" + }, + lathe: { + fusionID: "urn:adsk.wipprod:fs.folder:co.FDWCeyBGQX2rv8xSNWo5lg", + local: "File Share/Lathe" + }, + mill: { + fusionID: "urn:adsk.wipprod:fs.folder:co.nEihcpHUSW-ZsVzU-__1iw", + local: "File Share/Mill" + }, + waterjet: { + fusionID: "urn:adsk.wipprod:fs.folder:co.vJLXAKGbQQayeljr-nwztQ", + local: "File Share/Waterjet" + } +}; + +export const port = process.env.PORT; +export const forgeClientId = process.env.FORGE_CLIENT_ID; +export const forgeClientSecret = process.env.FORGE_CLIENT_SECRET; +export const forgeCallbackURL = process.env.FORGE_CALLBACK_URL; + +// export = { +// authClient: authClient, +// scopes: scopes, +// projectID: fileShareProjectID, +// folderMap: folderMap, +// port: process.env.PORT, +// forgeClientId: process.env.FORGE_CLIENT_ID, +// forgeClientSecret: process.env.FORGE_CLIENT_SECRET, +// forgeCallbackURL: process.env.FORGE_CALLBACK_URL +// } \ No newline at end of file diff --git a/src/package-lock.json b/src/package-lock.json index e11ebd3..1956c63 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -16,13 +16,16 @@ "forge-apis": "^0.8.6" }, "devDependencies": { + "@types/dotenv": "^8.2.0", "@types/express": "^4.17.13", + "@types/forge-apis": "^0.8.2", "@types/node": "^16.11.11", "eslint": "^7.32.0", "eslint-config-standard": "^16.0.3", "eslint-plugin-import": "^2.25.3", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^5.1.1" + "eslint-plugin-promise": "^5.1.1", + "typescript": "^4.5.2" } }, "node_modules/@babel/code-frame": { @@ -233,6 +236,16 @@ "@types/node": "*" } }, + "node_modules/@types/dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-ylSC9GhfRH7m1EUXBXofhgx4lUWmFeQDINW5oLuS+gxWdfUeW4zJdeVTYVkexEW+e2VUvlZR2kGnGGipAWR7kw==", + "deprecated": "This is a stub types definition. dotenv provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "dotenv": "*" + } + }, "node_modules/@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -256,6 +269,15 @@ "@types/range-parser": "*" } }, + "node_modules/@types/forge-apis": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@types/forge-apis/-/forge-apis-0.8.2.tgz", + "integrity": "sha512-uY/BAv6fVbYmhrDFi0t5qCcuXXiipIwwLfUOt9is1ZhNU4k/YyeGNfTSTzxkvUDLN4iEwX3LCeSQVcZe+DbIkg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -3008,6 +3030,19 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -3308,6 +3343,15 @@ "@types/node": "*" } }, + "@types/dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-ylSC9GhfRH7m1EUXBXofhgx4lUWmFeQDINW5oLuS+gxWdfUeW4zJdeVTYVkexEW+e2VUvlZR2kGnGGipAWR7kw==", + "dev": true, + "requires": { + "dotenv": "*" + } + }, "@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -3331,6 +3375,15 @@ "@types/range-parser": "*" } }, + "@types/forge-apis": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@types/forge-apis/-/forge-apis-0.8.2.tgz", + "integrity": "sha512-uY/BAv6fVbYmhrDFi0t5qCcuXXiipIwwLfUOt9is1ZhNU4k/YyeGNfTSTzxkvUDLN4iEwX3LCeSQVcZe+DbIkg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -5400,6 +5453,12 @@ "mime-types": "~2.1.24" } }, + "typescript": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", + "dev": true + }, "unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", diff --git a/src/package.json b/src/package.json index e7e4718..b5fee7d 100644 --- a/src/package.json +++ b/src/package.json @@ -22,12 +22,15 @@ "forge-apis": "^0.8.6" }, "devDependencies": { + "@types/dotenv": "^8.2.0", "@types/express": "^4.17.13", + "@types/forge-apis": "^0.8.2", "@types/node": "^16.11.11", "eslint": "^7.32.0", "eslint-config-standard": "^16.0.3", "eslint-plugin-import": "^2.25.3", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^5.1.1" + "eslint-plugin-promise": "^5.1.1", + "typescript": "^4.5.2" } } diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..cc423d9 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,132 @@ +import express from 'express'; +import * as config from './config'; + + +const ForgeAuthClient = require('forge-apis').AuthClientThreeLegged; +/** + * Kevin Pan | pan261@purdue.edu | Last Modified: 11/22/2021 + * + * Authentication server used to perform 3-legged auth through browser. + * + * Uses environment variables: + * - FORGE_CLIENT_ID + * - FORGE CLIENT_SECRET + * - FORGE_CALLBACK_URL + * - PORT (defaults to 3000) + * + * These are all configured in config.js + * + */ + +let credentials: any = null; +let refreshTime: any = null; +let intervalID: NodeJS.Timeout; +const app = express(); + +/** + * Creates server with three endpoints: + * 1. /auth + * - navigate to login in to Autodesk and begin auth process + * 2. /callback + * - after consent screen, auth API will redirect headers + * 3. /credentials + * - will return credentials JSON object or null if there isn't one + * @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, res) { + 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(() => { + // 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, res) { + 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'); + } + }); + + // 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(config.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); + }) + .catch(err => { + console.log(err); + }); +}; + +createServer(); \ No newline at end of file