sync/src/web/routes/account/data.js

159 lines
4.5 KiB
JavaScript

import { GET, POST, PATCH, DELETE } from '@calzoneman/express-babel-decorators';
import { CSRFError, InvalidRequestError } from '../../../errors';
import Promise from 'bluebird';
const LOGGER = require('@calzoneman/jsli')('AccountDataRoute');
function checkAcceptsJSON(req, res) {
if (!req.accepts('application/json')) {
res.status(406).send('Not Acceptable');
return false;
}
return true;
}
async function authorize(req, res, csrfVerify, verifySessionAsync) {
if (!req.signedCookies || !req.signedCookies.auth) {
res.status(401).json({
error: 'Authorization required'
});
return false;
}
try {
csrfVerify(req);
} catch (error) {
if (error instanceof CSRFError) {
res.status(403).json({
error: 'Invalid CSRF token'
});
} else {
LOGGER.error('CSRF check failed: %s', error.stack);
res.status(503).json({ error: 'Internal error' });
}
return false;
}
try {
const user = await verifySessionAsync(req.signedCookies.auth);
if (user.name !== req.params.user) {
res.status(403).json({
error: 'Session username does not match'
});
return false;
}
} catch (error) {
res.status(403).json({
error: error.message
});
return false;
}
return true;
}
function reportError(req, res, error) {
if (error instanceof InvalidRequestError) {
res.status(400).json({ error: error.message });
} else {
LOGGER.error(
'%s %s: %s',
req.method,
req.originalUrl,
error.stack
);
res.status(503).json({ error: 'Internal error' });
}
}
class AccountDataRoute {
constructor(accountDB, channelDB, csrfVerify, verifySessionAsync) {
this.accountDB = accountDB;
this.channelDB = channelDB;
this.csrfVerify = csrfVerify;
this.verifySessionAsync = verifySessionAsync;
}
@GET('/account/data/:user')
async getAccount(req, res) {
if (!checkAcceptsJSON(req, res)) return;
if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return;
try {
const user = await this.accountDB.getByName(req.params.user);
if (user) {
// Whitelist fields to expose, to avoid accidental
// information leaks when new fields are added.
const result = {
name: user.name,
email: user.email,
profile: user.profile,
time: user.time
};
res.status(200).json({ result });
} else {
res.status(404).json({ result: null });
}
} catch (error) {
reportError(req, res, error);
}
}
@PATCH('/account/data/:user')
async updateAccount(req, res) {
if (!checkAcceptsJSON(req, res)) return;
if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return;
res.status(501).json({ error: 'Not implemented' });
}
@GET('/account/data/:user/channels')
async listChannels(req, res) {
if (!checkAcceptsJSON(req, res)) return;
if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return;
try {
const channels = await this.channelDB.listByOwner(req.params.user).map(
channel => ({
name: channel.name,
owner: channel.owner,
time: channel.time,
last_loaded: channel.last_loaded,
owner_last_seen: channel.owner_last_seen
})
);
res.status(200).json({ result: channels });
} catch (error) {
reportError(req, res, error);
}
}
@POST('/account/data/:user/channels/:name')
async createChannel(req, res) {
if (!checkAcceptsJSON(req, res)) return;
if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return;
res.status(501).json({ error: 'Not implemented' });
}
@DELETE('/account/data/:user/channels/:name')
async deleteChannel(req, res) {
if (!checkAcceptsJSON(req, res)) return;
if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return;
res.status(501).json({ error: 'Not implemented' });
}
}
export { AccountDataRoute };