src/app.ts (209 lines of code) (raw):
import { format } from '@guardian/image';
import {
AuthenticationStatus,
guardianValidation,
PanDomainAuthentication,
} from '@guardian/pan-domain-node';
import type { AuthenticationResult } from '@guardian/pan-domain-node';
import { json as jsonBodyParser } from 'body-parser';
import cors from 'cors';
import express from 'express';
import type { Express } from 'express';
import { getStage, REGION, SETTINGS_FILE } from './environment';
import { getLoginResponse, getUI } from './uiHtml';
const DEFAULT_WIDTH = 800;
interface SignedImageUrlConfig {
url?: string;
profile?: { width?: number; height?: number; quality?: number };
}
function getPanDomainAuth() {
const SETTINGS_BUCKET = 'pan-domain-auth-settings';
const panda = new PanDomainAuthentication(
'gutoolsAuth-assym',
REGION,
SETTINGS_BUCKET,
SETTINGS_FILE,
guardianValidation,
);
return panda;
}
function getCookieString(req: express.Request) {
const maybeList = req.headers.cookie ?? req.headers.Cookie ?? '';
return Array.isArray(maybeList) ? maybeList.join('') : maybeList;
}
function handleImageSigning(
config: SignedImageUrlConfig | undefined,
getPanda: () => PanDomainAuthentication,
req: express.Request,
res: express.Response,
) {
const url = config?.url;
if (!url || typeof url != 'string' || url.length <= 0) {
res.status(400);
res.send({ error: 'No URL provided' });
return;
}
const salt = process.env.SALT;
if (!salt) {
res.status(500);
res.send({
error: 'Service incorrectly configured. No salt provided',
});
return;
}
const profile = config.profile ?? { width: DEFAULT_WIDTH };
try {
const signedUrl = format(url, salt, profile);
res.send({ signedUrl });
} catch (ex: unknown) {
res.status(500).send({
error: 'Error signing url',
ex: ex,
});
}
}
function withPandaAuth(
getPanda: () => PanDomainAuthentication,
req: express.Request,
res: express.Response,
onSuccess: (result: AuthenticationResult) => unknown,
onFailure: () => unknown,
) {
const panda = getPanda();
panda
.verify(getCookieString(req))
.then((panAuthResult) => {
if (panAuthResult.status === AuthenticationStatus.AUTHORISED) {
onSuccess(panAuthResult);
} else {
onFailure();
}
})
.catch((ex: unknown) => {
res.status(500).send({
error: 'Pan domain auth error',
ex: ex,
});
})
.finally(() => {
panda.stop();
});
}
export function buildApp(
getPanda: () => PanDomainAuthentication = getPanDomainAuth,
): Express {
const app = express();
app.use(
cors({
origin: /\.(dev-)?gutools.co.uk$/,
credentials: true,
}),
);
app.use(jsonBodyParser());
const uiHandler = (req: express.Request, res: express.Response) => {
withPandaAuth(
getPanda,
req,
res,
async () => {
const panAuthResult = await getPanda().verify(
getCookieString(req),
);
res.contentType('html').send(getUI(panAuthResult));
},
() => {
res.status(403).contentType('html').send(getLoginResponse(req));
},
);
};
app.get('/', uiHandler);
app.post(
'/signed-image-url',
(req: express.Request, res: express.Response) =>
withPandaAuth(
getPanda,
req,
res,
() => {
handleImageSigning(
req.body as SignedImageUrlConfig | undefined,
getPanda,
req,
res,
);
},
() => {
res.status(403).send({
error: 'Not authorised by pan-domain login',
});
},
),
);
app.get(
'/signed-image-url',
(req: express.Request, res: express.Response) =>
withPandaAuth(
getPanda,
req,
res,
() => {
const config: SignedImageUrlConfig = {
url: req.query.url as string,
profile: {
width: DEFAULT_WIDTH,
},
};
// The typeof checks below are because of the way express
// handles multiple query parameters of the same name. I
// don't think we need to handle this, so if it's not a
// string, ignore it.
if (
config.profile &&
req.query.width &&
typeof req.query.width === 'string'
) {
config.profile.width = Number.parseInt(req.query.width);
}
if (
config.profile &&
req.query.height &&
req.query.height === 'string'
) {
config.profile.height = Number.parseInt(
req.query.height,
);
}
if (
config.profile &&
req.query.quality &&
req.query.quality === 'string'
) {
config.profile.quality = Number.parseInt(
req.query.quality,
);
}
handleImageSigning(config, getPanda, req, res);
},
() => {
res.status(403).send({
error: 'Not authorised by pan-domain login',
});
},
),
);
app.get('/userdetails', (req: express.Request, res: express.Response) =>
withPandaAuth(
getPanda,
req,
res,
(authResult) => {
res.send(authResult);
},
() => {
res.status(403).send({
error: 'Not authorised by pan-domain login',
});
},
),
);
app.get('/healthcheck', (req: express.Request, res: express.Response) => {
res.status(200).json({ status: 'OK', stage: getStage() });
});
return app;
}