Skip to content

Instantly share code, notes, and snippets.

@maxwellb
Last active October 12, 2017 20:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save maxwellb/bb84375e5490a5dfb46c419e36464e67 to your computer and use it in GitHub Desktop.
Save maxwellb/bb84375e5490a5dfb46c419e36464e67 to your computer and use it in GitHub Desktop.
A Linode Manager on Webtask

The included files are two tasks for Webtask (https://webtask.io/edit). The linode-mgmt task is the manager, and linode-profile an example of calling the Linode API from behind a task.

Getting started:

  • Create the two tasks.
  • Set the ACCESS_KEY secret on both tasks to the same value as each other, chosen at random.
  • Create an API Client in your Linode profile (https://cloud.linode.com/profile/clients) that sets the Redirect URI to "https://*.run.webtask.io/linode-mgmt/login", filling in the appropriate domain for your Webtask.
  • Set the CLIENT_ID and CLIENT_SECRET secrets in the linode-mgmt task to the appropriate values for your new API Client.

Limitations:

  • Linode documentation (as of last edit) directs the use of a parameter called "scope" to specify scopes, but in practice "scopes" appears to be the correct parameter name.
  • The cookie used by the tasks is currently the access token itself. More opaque management is an exercise left to the reader.
"use latest";
/*
Copyright (c) 2017 Maxwell Bloch, All rights reserved.
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import express from "express";
import { fromExpress } from "webtask-tools";
import bodyParser from "body-parser";
import request from "request";
import cookieParser from "cookie-parser";
const accessCookie = { name: "Access", options: { httpOnly: true, signed: true, secure: true } };
const DEBUG = false;
const Views = {
"index": (model) => `
<p class="row">
Hello there
</p>
${model.linodes ?
(function(linodes) {
return `
<table class="table row">
<thead>
<tr>
<th>Group</th>
<th>Hypervisor</th>
<th>Label</th>
<th>Status</th>
</tr>
</thead>
<tbody>
${linodes.map(linode => `
<tr>
<td>${linode.group}</td>
<td>${linode.hypervisor}</td>
<td>${linode.label}</td>
<td>${linode.status}</td>
</tr>
`).join(" ")}
</tbody>
</table>`;
})(model.linodes)
: ""}
`,
"error": (model) => `
<div class="alert alert-danger" role="alert">
<p>
<strong>An error has occurred</strong>
</p>
<p>
${model.message}
</p>
</div>
`
}
var app = express();
app.use(function (req, res, next) {
let ctx = req.webtaskContext;
cookieParser(ctx.secrets.ACCESS_KEY)(req, res, next);
});
app.use(function (req, res, next) {
res.setHeader("Cache-Control", "no-cache, must-revalidate");
next();
});
app.use(function decodeAccessCookie(req, res, next) {
let ctx = req.webtaskContext;
if (req.accessToken) {
next(); return;
}
if (req.signedCookies == null
|| req.signedCookies.Access == null
|| req.signedCookies.Access.access == null) {
res.clearCookie(accessCookie.name);
next(); return;
}
req.accessToken = req.signedCookies.Access.access;
next();
});
app.use(bodyParser.json());
app.get("/login", (req,res) => {
var ctx = req.webtaskContext;
var clientId = ctx.secrets.CLIENT_ID;
var clientSecret = ctx.secrets.CLIENT_SECRET;
var code = req.query.code;
DEBUG && console.log(req.query);
request.post({
url: "https://login.linode.com/oauth/token",
form: { client_id: clientId, client_secret: clientSecret, code: code, state: "test" }
}, (err, resp, body) => {
body = JSON.parse(body);
if (err || body.success === false) {
err = err || body.error;
return res.send(View("error", ctx, { message: err }));
}
let accessToken = body.access_token;
if (accessToken == null) {
return res.send(View("error", ctx, { message: "Unable to parse access token" }));
} else {
res.cookie(accessCookie.name, { access: accessToken }, accessCookie.options)
res.redirect("/linode-mgmt/");
}
}
);
});
app.get("/logout", (req, res) => {
res.clearCookie(accessCookie.name);
res.redirect("/linode-mgmt/");
})
app.get("/", (req, res) => {
let ctx = req.webtaskContext;
if (req.accessToken) {
request.get({
url: "https://api.linode.com/v4/linode/instances",
headers: { "Authorization": `token ${req.accessToken}`}
}, (err, resp, body) => {
body = JSON.parse(body);
if (err) {
// err = err || body.error;
return res.send(View("error", ctx, { message: err }));
}
res.send(View("index", ctx, { linodes: body.data }, "Linodes - My Linode Manager"));
});
} else {
res.send(View("index", ctx, {}, "My Linode Manager"));
}
});
function View(name, ctx, model, opts) {
DEBUG && console.log(`View: ${name}`, model);
let title = typeof opts === "string" ? opts : (opts) ? opts.title : "Page";
let clientId = ctx.secrets.CLIENT_ID;
let html = Views[name](model);
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<title>${title}</title>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-main" aria-expanded="false"><span class="sr-only">Toggle navigation</span><span class="glyphicon glyphicon-console"></span></button>
<a class="navbar-brand" href="/linode-mgmt/">Linode Manager</a>
</div>
<div class="collapse navbar-collapse" id="navbar-main">
<div class="navbar-left">
<span data-name="username" style="display: inline-block; height: 50px; padding: 15px;"></span>
</div>
<form id="login" class="navbar-form navbar-right hidden" action="https://login.linode.com/oauth/authorize" method="GET">
<input type="submit" class="btn btn-default" value="Login">
<input type="hidden" name="client_id" value="${clientId}">
<input type="hidden" name="scope" value="linodes:view">
<input type="hidden" name="response_type" value="code">
<input type="hidden" name="state" value="test">
</form>
<div id="logout" class="navbar-form navbar-right hidden">
<a class="btn btn-default" id="logout" href="logout">Logout</a>
</div>
</div>
</div>
</nav>
<div class="container">
<!-- container BEGIN -->
${html}
<!-- container END -->
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script>
$.getJSON("/linode-profile", data => {
$("[data-name=username]").text(data.username);
if (data.username !== undefined) {
$("#logout").removeClass("hidden");
} else {
$("#login").removeClass("hidden");
}
});
</script>
</body>
</html>
`;
}
module.exports = fromExpress(app);
"use latest";
/*
Copyright (c) 2017 Maxwell Bloch, All rights reserved.
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import express from "express";
import { fromExpress } from "webtask-tools";
import cookieParser from "cookie-parser";
const popsicle = require("popsicle");
const resolve = require("popsicle-resolve");
const accessCookie = { name: "Access", options: { httpOnly: true, signed: true, secure: true } };
const DEBUG = true;
var app = express();
app.use(function (req, res, next) {
let ctx = req.webtaskContext;
cookieParser(ctx.secrets.ACCESS_KEY)(req, res, next);
});
app.use(function decodeAccessCookie(req, res, next) {
let ctx = req.webtaskContext;
if (req.accessToken) {
next(); return;
}
if (req.signedCookies == null
|| req.signedCookies.Access == null
|| req.signedCookies.Access.access == null) {
res.clearCookie(accessCookie.name);
next(); return;
}
req.accessToken = req.signedCookies.Access.access;
next();
});
function auth(accessToken) {
return function auth(req, next) {
req.set('Authorization', `token ${accessToken}`);
return next();
}
}
app.get("/", (req, res, next) => {
let ctx = req.webtaskContext;
let accessToken = req.accessToken;
if (accessToken != null) {
popsicle.get("profile")
.use(resolve("https://api.linode.com/v4/"))
.use(auth(accessToken))
.then(resp => {
let profile = JSON.parse(resp.body);
DEBUG && console.log(profile);
res.send({
username: profile.username
});
})
.catch(next);
} else {
res.send({});
next();
}
});
module.exports = fromExpress(app);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment