"use strict";
const nconf = require('nconf');
// Load configuration from a designated file.
nconf.file({ file: 'config.json' });
// This sample Node.js app shows how you can use Simple OAuth2 to access Mix.api.
// For more information, see the Simple OAuth2 client README for Authorization Code flow:
// https://www.npmjs.com/package/simple-oauth2#authorization-code-flow
const axios = require("axios");
const express = require("express");
const oauth2 = require("simple-oauth2");
const open = require("open");
const uuidv4 = require("uuid/v4");
const grpc = require('grpc');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');
const fs = require('fs');
const google_apis_proto_dir = './';
// Loading user specific configuration from config file
const oauthClientId = nconf.get('client_id');
const oauthClientSecret = nconf.get('client_secret');
const namespace = nconf.get('namespace');
const appId = nconf.get('appId');
const regionName = nconf.get('regionName');
const environmentName = nconf.get('environmentName');
const oauthScope = 'mix-api';
if (!oauthClientId || !oauthClientSecret) {
console.error('Missing client ID or client secret');
process.exit(1);
}
// Configure Authorization and Mix endpoints
// By default, the app assumes that the redirect URL uses port 3000, and that you want
// to use the Prod Mix server and OAuth server. You can override these values using
// environment variables.
const mixServerHost = process.env.MIX_SERVER || 'mix.api.nuance.com';
const authorizationServerHost = process.env.AUTHORIZATION_SERVER || 'auth.crt.nuance.com';
const expressPort = process.env.AUTH_CALLBACK_PORT || 3000;
// The other required constants should not change based on environment
const oauthTenant = 'mix';
const expressBaseUrl = `http://localhost:${expressPort}`;
const loginUrl = `https://${mixServerHost}/v3/bolt/login`;
const getProjectsUrl = `https://${mixServerHost}/v3/api/v3/projects`;
const oauthClientRedirectUri = process.env.AUTH_REDIRECT_URI || `${expressBaseUrl}/callback`;
const authorizationServerBaseUrl = `https://${authorizationServerHost}`;
const authorizationServerTokenPath = '/oauth2/token';
const authorizationServeAuthorizePath = '/oauth2/auth';
const PROTOBUF_PATH = path.join(__dirname, '/appconfig.proto');
// Use grpc-loader to include Google protobufs
// See https://www.npmjs.com/package/@grpc/proto-loader
const protoLoaderOptions = {
enums: String,
includeDirs: [
`${google_apis_proto_dir}`
]
};
const packageDefinition = protoLoader.loadSync(PROTOBUF_PATH, protoLoaderOptions);
const AppConfigsService = grpc.loadPackageDefinition(packageDefinition).mix.api.v3.AppConfigs;
// Create the OAuth2 client configuration, using the client credentials and specifying the URL
// of the Mix authentication service; see https://github.com/lelylan/simple-oauth2/blob/HEAD/API.md
// for the description of each property
const credentials = {
client: {
id: oauthClientId,
secret: oauthClientSecret
},
auth: {
tokenHost: authorizationServerBaseUrl,
tokenPath: authorizationServerTokenPath,
authorizePath: authorizationServeAuthorizePath
}
};
const oauth2_client = oauth2.create(credentials);
const oauth_state = uuidv4(); // Random string that can be used to prevent CSRF attacks
// Get the authorization code: First create the authorization URL, using the client configuration,
// redirect URL, and state, and then appending the tenant to the URL.
// Redirect the browser to the authentication URL
var getAuthorizationCode = (_, res) => {
var authorizationUri = oauth2_client.authorizationCode.authorizeURL({
redirect_uri: oauthClientRedirectUri,
state: oauth_state,
scope: oauthScope
});
// Appends the tenant. For Mix.api, this is always 'mix'
authorizationUri = `${authorizationUri}&tenant=${oauthTenant}`;
res.redirect(authorizationUri);
}
// Get the access token, using the authorization code received as well as the state
var getAccessToken = async (req, err) => {
const { code: authorizationCode, state } = req.query;
// Check the state value to confirm the callback was originated from this client (i.e. oauth_state)
if (state != oauth_state) {
err(`state doesn't match; possible CSRF attack!`)
}
const tokenConfig = {
code: authorizationCode,
redirect_uri: oauthClientRedirectUri,
state: oauth_state,
scope: oauthScope
};
try {
const httpOptions = {};
const token = await oauth2_client.authorizationCode.getToken(
tokenConfig,
httpOptions
);
// Log the access token
console.log(`access token:\n${token.access_token}`);
return token.access_token;
} catch (error) {
console.error("\nError getting access token:\n", error.message);
err(error);
}
}
// Access Mix.api using the provided token
// Get the list of all app configs
function getAppConfigsServiceStub(accessToken) {
function generateMetadata(params, callback) {
var metadata = new grpc.Metadata();
metadata.add('authorization', `Bearer ${accessToken}`);
callback(null, metadata);
}
const call_credentials = grpc.credentials.createFromMetadataGenerator(generateMetadata);
let channel_credentials = grpc.credentials.createSsl();
channel_credentials = grpc.credentials.combineChannelCredentials(channel_credentials, call_credentials);
return new AppConfigsService(`${mixServerHost}:443`, channel_credentials);
}
// Download the first model in the first app config from the zero-based list of app configs returned
var downloadAppConfigModel = async (accessToken, appConfigs) => {
const configNbr = 0;
const modelNbr = 0;
const tag = appConfigs.appConfigs[configNbr].tag;
const modelType = appConfigs.appConfigs[configNbr].models[modelNbr].type;
const language = appConfigs.appConfigs[configNbr].models[modelNbr].language;
const request = {
namespace,
regionName,
environmentName,
appId,
tag,
language,
modelType,
};
console.log(`\nREQUEST=\n${JSON.stringify(request, null, 2)}`);
const stub = getAppConfigsServiceStub(accessToken);
return new Promise((resolve, reject) => {
const modelFilename = path.join(__dirname, `${request.modelType.toLowerCase()}-model.zip`);
var stream = fs.createWriteStream(modelFilename);
const call = stub.DownloadAppConfigArtifacts(request);
let totalNbrBytes = 0;
call.on('data', response => {
totalNbrBytes += response.chunk.byteLength
console.log(`\nRESPONSE=\nGot ${totalNbrBytes} bytes`);
stream.write(response.chunk);
});
call.on('end', () => {
console.log(`\nGot total of ${totalNbrBytes} bytes; saved in ${modelFilename}`)
stream.end();
resolve();
});
call.on('error', e => {
reject(e);
});
call.on('status', status => {
});
});
}
var getAppConfigs = async (accessToken) => {
const request = {
namespace,
regionName,
environmentName,
appId,
};
console.log(`\nREQUEST=\n${JSON.stringify(request, null, 2)}`);
const stub = getAppConfigsServiceStub(accessToken);
return new Promise((resolve, reject) => {
stub.ListAppConfigs(request, (error, response) => {
if (error) {
reject(error);
}
console.log(`\nRESPONSE=\n${JSON.stringify(response, null, 2)}`);
resolve(response);
});
});
}
// Main: Create the local web server to handle callback,
// get the authorization code,
// get the access token,
// then list the app configs and download the artifact
const expressApp = express();
expressApp.get("/", getAuthorizationCode);
expressApp.get("/callback", async (req, res, err) => {
try {
var accessToken = await getAccessToken(req, err);
let appConfigs = await getAppConfigs(accessToken);
await downloadAppConfigModel(accessToken, appConfigs);
return res.status(200).json();
} catch (error) {
err(error);
} finally { // Close application regardless of success or error
setInterval(() => process.exit(0), 2000);
}
});
expressApp.listen(expressPort, () => {
open(expressBaseUrl);
});