atlassian-connect-express
Version:
Library for building Atlassian Add-ons on top of Express
239 lines (209 loc) • 7.38 kB
JavaScript
const EventEmitter = require("events").EventEmitter;
const URI = require("urijs");
const urls = require("url");
const _ = require("lodash");
const fs = require("fs");
const util = require("util");
const jwt = require("atlassian-jwt");
const config = require("./internal/config");
const utils = require("./internal/utils");
const registration = require("./internal/registration");
const defLogger = require("./internal/logger");
const HostRequest = require("./internal/host-request");
const verifyInstallation = require("./middleware/verify-installation");
const authentication = require("./middleware/authentication");
const token = require("./middleware/token");
const store = require("./store");
const DESCRIPTOR_FILENAME = "atlassian-connect.json";
const CONFIG_FILENAME = "config.json";
class Addon extends EventEmitter {
constructor(app, opts, logger, callback) {
super();
process.on("unhandledRejection", err => {
logger.error("Unhandled error:", err.stack || err);
});
const self = this;
self.app = app;
Addon.logger = self.logger = logger;
const configOpts = utils.loadJSON(CONFIG_FILENAME);
self.config = config(configOpts, app.get("env"), opts.config);
Addon.settings = self.settings = store(logger, self.config.store());
self.schema = self.settings.schema; // store-adapter-dependent
self.descriptor = require("./internal/addon-descriptor")(self);
self.key = self.descriptor.key;
self.name = self.descriptor.name;
self._jwt = jwt;
try {
_.extend(self, registration(self));
} catch (e) {
self.logger.info("Auto registration is not avaiable for this add-on.");
}
if (self.app.get("env") === "development") {
// validate current descriptor on start
if (self.config.validateDescriptor()) {
self.validateDescriptor();
}
if (self.config.watch()) {
self.logger.info(`Watching ${DESCRIPTOR_FILENAME} for changes`);
const reregisterWatcher = function () {
self.watcher && self.watcher.close(); // clean up existing watcher if present
self.watcher = fs.watch(
DESCRIPTOR_FILENAME,
{ persistent: false },
event => {
if (event === "change" || event === "rename") {
self.logger.info(
`Re-registering due to ${DESCRIPTOR_FILENAME} change`
);
self.reloadDescriptor();
self.register(true);
if (self.config.validateDescriptor()) {
self.validateDescriptor();
}
}
// re-register after each change, intellij's "safe write" feature can break file watches
reregisterWatcher();
}
);
};
reregisterWatcher();
}
}
// defer configuration of the plugin until the express app has been configured
process.nextTick(() => {
self._configure(callback);
});
}
_configure(callback) {
const self = this;
if (!this.descriptor.baseUrl) {
throw Error(
`The "baseUrl" in ${DESCRIPTOR_FILENAME} must be present and non-empty.`
);
}
const baseUrlInternal = new URI(this.descriptor.baseUrl);
self.config.baseUrl = urls.parse(baseUrlInternal.toString());
const descriptorUrl = baseUrlInternal.clone().segment(DESCRIPTOR_FILENAME);
self.app.get(descriptorUrl.path(), (req, res) => {
res.charset = "UTF-8"; // avoid browsers and https://jsonformatter.curiousconcept.com/ saying "Invalid encoding, expecting UTF-8, UTF-16 or UTF-32."
res.json(self.descriptor);
});
// auto-register routes for each webhook in the descriptor
const modules = self.descriptor.modules;
if (modules && modules.webhooks) {
let webhooks = modules.webhooks;
if (!Array.isArray(webhooks)) {
webhooks = [webhooks];
}
webhooks.forEach(webhook => {
if (!webhook.event) {
self.logger.warn(
`Webhook does not have event property: ${util.inspect(webhook)}`
);
return;
}
if (!webhook.url) {
self.logger.warn(
`Webhook does not have url property: ${util.inspect(webhook)}`
);
return;
}
const webhookUrl = baseUrlInternal.clone().segment(webhook.url);
self.app.post(
// mount path
webhookUrl.path(),
// auth middleware
authentication.authenticateWebhook(self),
// request handler
(req, res) => {
try {
self.emit(webhook.event, webhook.event, req.body, req);
res.status(204).end();
} catch (ex) {
res.status(500).send(_.escape(ex));
}
}
);
});
}
const installUrl = baseUrlInternal.clone().segment("/installed");
self.app.post(
// installed POST handler
installUrl.path(),
// installed middleware (checks that the install event is complete and originates from an authorised host)
verifyInstallation(self),
(req, res) => {
const settings = req.body;
self.settings.set("clientInfo", settings, settings.clientKey).then(
data => {
if (self.app.get("env") !== "production") {
self.logger.info(
`Saved tenant details for ${
settings.clientKey
} to database\n${util.inspect(data)}`
);
}
self.emit("host_settings_saved", settings.clientKey, data);
res.status(204).send();
},
err => {
self.emit("host_settings_not_saved", settings.clientKey, {
err
});
res
.status(500)
.send(
_.escape(
`Could not lookup stored client data for ${settings.clientKey}: ${err}`
)
);
}
);
}
);
if (callback) {
callback();
}
}
// this middleware should be called by the add-on setup (see the template)
middleware() {
return require("./middleware")(this);
}
authenticate(skipQshVerification) {
return authentication.authenticate(this, skipQshVerification);
}
loadClientInfo(clientKey) {
return Addon.settings.get("clientInfo", clientKey);
}
checkValidToken() {
return token(this);
}
reloadDescriptor() {
this.descriptor = require("./internal/addon-descriptor")(this);
}
/**
* addon.httpClient(expressRequest)
* addon.httpClient({clientKey, userAccountId})
* (deprecated) addon.httpClient({clientKey, userKey})
*
* @param reqOrOpts either an expressRequest object or options
* @returns HostClient a httpClient
*/
httpClient(reqOrOpts) {
const ctx = reqOrOpts.context;
if (ctx) {
return ctx.http;
}
if (!reqOrOpts.clientKey) {
throw new Error("Http client options must specify clientKey");
}
return new HostRequest(this, reqOrOpts, reqOrOpts.clientKey);
}
}
module.exports = function (app, opts, logger, callback) {
return new Addon(app, opts || {}, logger || defLogger, callback);
};
module.exports.store = {
register: store.register
};
module.exports.DESCRIPTOR_FILENAME = DESCRIPTOR_FILENAME;