Create
cancel
Showing results for 
Search instead for 
Did you mean: 
Sign up Log in

atlassian-connect-express: explore the source code of example app

Hello!

We created a simple Bitbucket Cloud app using atlassian-connect-express in the previous article.

In this article we will explore the source code of the app to understand how everything works.

You can find a video for this article here.

Package.json

We have a nodejs project that is why we need to pay attention to the package.json file which provides configuration for our project.

{
    "name": "my-app",
    "version": "0.0.1",
    "private": true,
    "scripts": {
        "start": "node -r esm app.js",
        "lint": "eslint app.js routes"
    },
    "dependencies": {
        "atlassian-connect-express": "^6.6.0",
        "body-parser": "^1.19.0",
        "compression": "^1.7.4",
        "cookie-parser": "^1.4.5",
        "errorhandler": "^1.5.1",
        "esm": "^3.2.25",
        "express": "^4.17.1",
        "express-hbs": "^2.3.5",
        "morgan": "^1.10.0",
        "sequelize": "^6.6.2",
        "sqlite3": "^5.0.2",
        "static-expiry": ">=0.0.11"
    },
    "devDependencies": {
        "eslint": "^7.24.0"
    }
}

First we define the name, the version, dependencies which are needed for the project and scripts, which we can call. Here are the scripts:

"scripts": {
        "start": "node -r esm app.js",
        "lint": "eslint app.js routes"
    },

We can see our start command which we use to start our app and the lint command to analyse our project with eslint. Let’s change the lint command to

"lint": "eslint app.js routes" --fix

It will let us fix the errors by eslint automatically. Not let’s execute the command in the terminal:

npm run lint

I have two errors:

/Users/alexm/IdeaProjects/bb-test-app/app.js
  24:8   error  'os' is defined but never used      no-unused-vars
  33:45  error  'config' is defined but never used  no-unused-vars

✖ 2 problems (2 errors, 0 warnings)

I can remove the os and config variables from app.js and run the npm run lint command again. This time I have no errors.

config.json

This file contains our configuration parameters for the app:

{
    "product": "bitbucket",
    "development": {
        "port": 3000,
        "errorTemplate": true,
        "store": {
            "adapter": "sequelize",
            "dialect": "sqlite3",
            "type": "memory"
        },
        "localBaseUrl": "https://81ee1cf7e1d9.ngrok.io"
    },
    "production": {
        "port": "$PORT",
        "errorTemplate": true,
        "localBaseUrl": "https://your-subdomain.herokuapp.com",
        "store": {
            "url": "$DATABASE_URL"
        },
        "whitelist": [
            "bitbucket.org",
            "api.bitbucket.org"
        ]
    }
}

Right now it contains two sections: development and production. It means that you can provide different sets of option when you develop your app and when you move to production.

You make your application choose what section to use by settings the NODE_ENV variable in the environment where you run your app. For example, in my mac environment, I would set the NODE_ENV parameter like this:

export NODE_ENV=production

You do not need to set the NODE_ENV variable for the development section. It will be taken by default.

Development section is very simple and all values for parameters are hardcoded. But production has a couple of variables defined:

"production": {
        "port": "$PORT",
        "errorTemplate": true,
        "localBaseUrl": "https://your-subdomain.herokuapp.com",
        "store": {
            "url": "$DATABASE_URL"
        }
    }

You can see the $PORT and $DATABASE_URL variables. It means that if you set the NODE_ENV variable to production, you also need to define the PORT and DATABASE_URL variables in your environment (in my case I need to use the export PORT and export DATABASE_URL commands)

While you are developing your app, all you have to change in the config.json file is the ngrok url in the localBaseUrl parameter.

atlassian-connect.json

This is our app descriptor. App descriptor provides information to Bitbucket how to communicate with our app. I will not go in details about app descriptors. You can read about app descriptors here.

{
    "key": "matveev-bb-test-app",
    "name": "Example App",
    "description": "An example app for Bitbucket",
    "vendor": {
        "name": "Angry Nerds",
        "url": "https://www.atlassian.com/angrynerds"
    },
    "baseUrl": "{{localBaseUrl}}",
    "authentication": {
        "type": "jwt"
    },
    "lifecycle": {
        "installed": "/installed",
        "uninstalled": "/uninstalled"
    },
    "modules": {
        "webhooks": [
            {
                "event": "*",
                "url": "/webhook"
            }
        ],
        "webItems": [
            {
                "url": "http://example.com?repoPath={repository.full_name}",
                "name": {
                    "value": "Example Web Item"
                },
                "location": "org.bitbucket.repository.navigation",
                "key": "example-web-item",
                "params": {
                    "auiIcon": "aui-iconfont-link"
                }
            }
        ],
        "repoPages": [
            {
                "url": "/connect-example?repoPath={repository.full_name}",
                "name": {
                    "value": "Example Page"
                },
                "location": "org.bitbucket.repository.navigation",
                "key": "example-repo-page",
                "params": {
                    "auiIcon": "aui-iconfont-doc"
                }
            }
        ],
        "webPanels": [
            {
                "url": "/connect-example?repoPath={repository.full_name}",
                "name": {
                    "value": "Example Web Panel"
                },
                "location": "org.bitbucket.repository.overview.informationPanel",
                "key": "example-web-panel"
            }
        ]
    },
    "scopes": ["account", "repository"],
    "contexts": ["account"]
}

First we define the key, name, description and vendor information. Then we define the baseUrl of our application. As you can see it equals to the localBaseUrl parameter from config.json file which we discussed earlier.

Then we define what endpoints should be called when our app is installed and uninstalled in Bitbucket:

"lifecycle": {
        "installed": "/installed",
        "uninstalled": "/uninstalled"
    },

The installed endpoint defined by atlassian-connect-express. We also added the installed endpoint which sets the correct url for Bitbucket Api. The uninstalled endpoint we defined ourselves in our app.

Then you can see modules which are defined for our app. You can read more about modules here.

app.js

This is the entry point for our app. I will not discuss every line in this file. I will just mark important lines

const viewsDir = __dirname + '/views';
app.engine('hbs', hbs.express4({partialsDir: viewsDir}));
app.set('view engine', 'hbs');
app.set('views', viewsDir);

These lines tell us that our app will use Handlebars which is a template engine for html output. We can see that all templates are in the views folder:

Screenshot 2021-05-01 at 11.33.31.png

The unauthorized.hbs template is used by atlassian-connect-express to provide errors if something wrong happened during calling our app module.

Other templates we will discuss later.

routes(app, addon);

In this line we define our routes, which are placed in the routes/index.js file:

import util from 'util';

export default function routes(app, addon) {
    //healthcheck route used by micros to ensure the addon is running.
    app.get('/healthcheck', function(req, res) {
        res.send(200);
    });

    // Root route. This route will redirect to the add-on descriptor: `atlassian-connect.json`.
    app.get('/', function (req, res) {
        res.format({
            // If the request content-type is text-html, it will decide which to serve up
            'text/html': function () {
                res.redirect('/atlassian-connect.json');
            },
            // This logic is here to make sure that the `atlassian-connect.json` is always
            // served up when requested by the host
            'application/json': function () {
                res.redirect('/atlassian-connect.json');
            }
        });
    });

    // This route will be targeted by iframes rendered by Bitbucket. It renders a simple template
    // with two pieces of data:
    //
    //   1. the repository path (passed in the query string via a context parameter)
    //   2. the user who installed the add-on's display name (retrieved from Bitbucket via REST)

    app.get('/connect-example', addon.authenticate(), function (req, res) {

        // the call to addon.authenticate() above verifies the JWT token provided by Bitbucket
        // in the iframe URL

        var httpClient = addon.httpClient(req);

        httpClient.get('/2.0/user/', function (err, resp, data) {
            try {
                data = JSON.parse(data);
                res.render('connect-example', {
                    title: 'Atlassian Connect',
                    displayName: data.display_name,
                    repoPath: req.query.repoPath
                });
            } catch (e) {
                console.log(e);
                res.sendStatus(500);
            }
        });
    });

    // This route will handle webhooks from repositories this add-on is installed for.
    // Webhook subscriptions are managed in the `modules.webhooks` section of
    // `atlassian-connect.json`

    app.post('/webhook', addon.authenticate(), function (req, res) {

        // log the webhook payload
        console.log(util.inspect(req.body, {
            colors:true,
            depth:null
        }));
        res.send(204);

    });

    // Add any additional route handlers you need for views or REST resources here...
    app.post('/uninstalled', addon.authenticate(), function (req, res) {
        res.send(200)
    })
}

There are good explanations in comments. That is why I will not give any explanations.

hbs templates

We did not discuss yet the views/layout.hbs and views/connect-example.hbs.

views/layout.hbs is just the root template which should be included in all templates for the pages which we call from webPanel, repoPage, adminPage or configurePage modules:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="ap-local-base-url" content="{{localBaseUrl}}">
  <title>{{title}}</title>
  <link rel="stylesheet" href="//aui-cdn.atlassian.com/aui-adg/5.6.11/css/aui.css" media="all">
  <link rel="stylesheet" href="//aui-cdn.atlassian.com/aui-adg/5.6.11/css/aui-experimental.css" media="all">
  <!--[if IE 9]><link rel="stylesheet" href="//aui-cdn.atlassian.com/aui-adg/5.6.11/css/aui-ie9.css" media="all"><![endif]-->
  <link rel="stylesheet" href="{{furl '/css/addon.css'}}" type="text/css" />
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
  <script src="//aui-cdn.atlassian.com/aui-adg/5.6.11/js/aui-soy.js" type="text/javascript"></script>
  <script src="//aui-cdn.atlassian.com/aui-adg/5.6.11/js/aui.js" type="text/javascript"></script>
  <script src="//aui-cdn.atlassian.com/aui-adg/5.6.11/js/aui-datepicker.js"></script>
  <script src="//aui-cdn.atlassian.com/aui-adg/5.6.11/js/aui-experimental.js"></script>
  <script src="https://bitbucket.org/atlassian-connect/all.js" type="text/javascript"></script>
</head>
<body class="aui-page-hybrid">
  <section id="content" role="main">
    {{{body}}}
  </section>
  <script src="{{furl '/js/addon.js'}}"></script>
</body>
</html>

It contains styles according to Atlassian design guide and adds all.js without which your app will not be able to produce html output in Bitbucket.

Also we define there our custom css and js files which are stored in the public folder.

views/connect-example.hbs uses views/layout.hbs and adds some info related to the module from which it is called.

{{!< layout}}
<table>
  <tr>
      <td>Add-on user (retrieved via server-to-server REST):</td>
      <td>{{displayName}}</td>
  </tr>
  <tr>
      <td>Your name (retrieved via <code>AP.request()</code>):</td>
      <td>
          <span id="displayName"></span>
          <span class="aui-icon aui-icon-wait loading">Please wait</span>
      </td>
  </tr>
  <tr>
      <td>This repository:</td>
      <td>{{repoPath}}</td>
  </tr>
  <tr>
      <td>Page visits:</td>
      <td><strong>No longer supported</strong></td>
  </tr>
</table>

Now what module calls views/connect-example.hbs?

All modules are defined in atlassian-connect.json. We need to search this file. We can find that the repoPage and webPanel calls connect-example rest endpoint.

"repoPages": [
            {
                "url": "/connect-example?repoPath={repository.full_name}",
                "name": {
                    "value": "Example Page"
                },
                "location": "org.bitbucket.repository.navigation",
                "key": "example-repo-page",
                "params": {
                    "auiIcon": "aui-iconfont-doc"
                }
            }
        ],
        "webPanels": [
            {
                "url": "/connect-example?repoPath={repository.full_name}",
                "name": {
                    "value": "Example Web Panel"
                },
                "location": "org.bitbucket.repository.overview.informationPanel",
                "key": "example-web-panel"
            }
        ]
    },

connect-example rest endpoint is defined in routes/index.js file:

app.get('/connect-example', addon.authenticate(), function (req, res) {
        var httpClient = addon.httpClient(req);
        httpClient.get('/2.0/user/', function (err, resp, data) {
            try {
                data = JSON.parse(data);
                res.render('connect-example', {
                    title: 'Atlassian Connect',
                    displayName: data.display_name,
                    repoPath: req.query.repoPath
                });
            } catch (e) {
                console.log(e);
                res.sendStatus(500);
            }
        });
    });

First of all, we can see the addon.authenticate() function. This function checks that the user who called the endpoint has been correctly identified in Bitbucket. This function should be used only for endpoints which are called directly from Bitbucket.

Then we execute the /2.0/user endpoint which will return data not for the user who called the module but for the workspace where our addon is installed. We take the display_name attribute from the returned data.

Then we return the connect-example.hbs.

That is all for the source code. See you next time!

0 comments

Comment

Log in or Sign up to comment
TAGS
AUG Leaders

Atlassian Community Events