Update stuff

This commit is contained in:
Jonathan Cremin 2018-06-02 15:50:39 +00:00
parent 0254e42b9c
commit 553ba9db9a
40 changed files with 7343 additions and 717 deletions

View file

@ -1,5 +1,5 @@
import Router from 'koa-router';
import stats from 'koa-statsd';
import stats from '../lib/koa-statsd';
import cors from 'kcors';
import StatsD from 'statsy';
import auth from './lib/auth';
@ -14,9 +14,9 @@ const router = new Router();
const statsdOpts = { prefix: 'hostr-api', host: process.env.STATSD_HOST };
router.use(stats(statsdOpts));
const statsd = new StatsD(statsdOpts);
router.use(function* statsMiddleware(next) {
this.statsd = statsd;
yield next;
router.use(async (ctx, next) => {
ctx.statsd = statsd;
await next();
});
router.use(cors({
@ -24,21 +24,22 @@ router.use(cors({
credentials: true,
}));
router.use('*', function* authMiddleware(next) {
router.use(async (ctx, next) => {
try {
yield next;
if (this.response.status === 404 && !this.response.body) {
this.throw(404);
await next();
if (ctx.response.status === 404 && !ctx.response.body) {
ctx.throw(404);
}
} catch (err) {
if (err.status === 401) {
this.statsd.incr('auth.failure', 1);
this.set('WWW-Authenticate', 'Basic');
this.status = 401;
this.body = err.message;
ctx.statsd.incr('auth.failure', 1);
ctx.set('WWW-Authenticate', 'Basic');
ctx.status = 401;
ctx.body = err.message;
} else if (err.status === 404) {
this.status = 404;
this.body = {
ctx.status = 404;
ctx.body = {
error: {
message: 'File not found',
code: 604,
@ -47,19 +48,20 @@ router.use('*', function* authMiddleware(next) {
} else {
if (!err.status) {
debug(err);
if (this.raven) {
this.raven.captureError(err);
if (ctx.raven) {
ctx.raven.captureError(err);
}
throw err;
} else {
this.status = err.status;
this.body = err.message;
ctx.status = err.status;
ctx.body = err.message;
}
}
}
this.type = 'application/json';
ctx.type = 'application/json';
});
router.delete('/file/:id', auth, file.del);
router.get('/user', auth, user.get);
router.get('/user/token', auth, user.token);
router.get('/token', auth, user.token);
@ -70,12 +72,11 @@ router.delete('/user/pro', auth, pro.cancel);
router.get('/file', auth, file.list);
router.post('/file', auth, file.post);
router.get('/file/:id', file.get);
router.delete('/file/:id', auth, file.del);
router.delete('/file/:id', auth, file.del);
// Hack, if no route matches here, router does not dispatch at all
router.get('/(.*)', function* errorMiddleware() {
this.throw(404);
router.get('/(.*)', async (ctx) => {
ctx.throw(404);
});
export const ws = new Router();

View file

@ -6,27 +6,27 @@ const debug = debugname('hostr-api:auth');
const badLoginMsg = '{"error": {"message": "Incorrect login details.", "code": 607}}';
export default function* (next) {
export default async (ctx, next) => {
let user = false;
const remoteIp = this.req.headers['x-forwarded-for'] || this.req.connection.remoteAddress;
const login = yield models.login.create({
const remoteIp = ctx.req.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress;
const login = await models.login.create({
ip: remoteIp,
successful: false,
});
if (this.req.headers.authorization && this.req.headers.authorization[0] === ':') {
if (ctx.req.headers.authorization && ctx.req.headers.authorization[0] === ':') {
debug('Logging in with token');
const userToken = yield this.redis.get(this.req.headers.authorization.substr(1));
this.assert(userToken, 401, '{"error": {"message": "Invalid token.", "code": 606}}');
const userToken = await ctx.redis.get(ctx.req.headers.authorization.substr(1));
ctx.assert(userToken, 401, '{"error": {"message": "Invalid token.", "code": 606}}');
debug('Token found');
user = yield models.user.findById(userToken);
user = await models.user.findById(userToken);
if (!user) {
login.save();
return;
}
} else {
const authUser = auth(this);
this.assert(authUser, 401, badLoginMsg);
const count = yield models.login.count({
const authUser = auth(ctx);
ctx.assert(authUser, 401, badLoginMsg);
const count = await models.login.count({
where: {
ip: remoteIp,
successful: false,
@ -36,38 +36,38 @@ export default function* (next) {
},
});
this.assert(count < 25, 401,
ctx.assert(count < 25, 401,
'{"error": {"message": "Too many incorrect logins.", "code": 608}}');
user = yield models.user.findOne({
user = await models.user.findOne({
where: {
email: authUser.name,
activated: true,
},
});
if (!user || !(yield passwords.match(authUser.pass, user.password))) {
if (!user || !(await passwords.match(authUser.pass, user.password))) {
login.save();
this.throw(401, badLoginMsg);
ctx.throw(401, badLoginMsg);
return;
}
}
debug('Checking user');
this.assert(user, 401, badLoginMsg);
ctx.assert(user, 401, badLoginMsg);
debug('Checking user is activated');
debug(user.activated);
this.assert(user.activated === true, 401,
ctx.assert(user.activated === true, 401,
'{"error": {"message": "Account has not been activated.", "code": 603}}');
login.successful = true;
yield login.save();
await login.save();
const uploadedTotal = yield models.file.count({
const uploadedTotal = await models.file.count({
where: {
userId: user.id,
},
});
const uploadedToday = yield models.file.count({
const uploadedToday = await models.file.count({
where: {
userId: user.id,
createdAt: {
@ -85,9 +85,9 @@ export default function* (next) {
plan: user.plan,
uploads_today: uploadedToday,
};
this.response.set('Daily-Uploads-Remaining',
ctx.response.set('Daily-Uploads-Remaining',
user.type === 'Pro' ? 'unlimited' : 15 - uploadedToday);
this.user = normalisedUser;
debug('Authenticated user: ', this.user.email);
yield next;
ctx.user = normalisedUser;
debug('Authenticated user: ', ctx.user.email);
await next();
}

View file

@ -6,107 +6,100 @@ import Uploader from '../../lib/uploader';
const redisUrl = process.env.REDIS_URL;
export function* post(next) {
if (!this.request.is('multipart/*')) {
yield next;
export async function post(ctx, next) {
if (!ctx.request.is('multipart/*')) {
await next();
return;
}
const uploader = new Uploader(this);
const uploader = new Uploader(ctx);
yield uploader.checkLimit();
yield uploader.accept();
await uploader.checkLimit();
uploader.acceptedEvent();
await uploader.accept();
await uploader.processImage();
await uploader.finalise();
yield uploader.receive();
yield uploader.promise;
uploader.processingEvent();
yield uploader.processImage();
yield uploader.finalise();
this.status = 201;
this.body = formatFile(uploader.file);
ctx.status = 201;
ctx.body = formatFile(uploader.file);
uploader.completeEvent();
uploader.malwareScan();
}
export function* list() {
export async function list(ctx) {
let limit = 20;
if (this.request.query.perpage === '0') {
if (ctx.request.query.perpage === '0') {
limit = 1000;
} else if (this.request.query.perpage > 0) {
limit = parseInt(this.request.query.perpage / 1, 10);
} else if (ctx.request.query.perpage > 0) {
limit = parseInt(ctx.request.query.perpage / 1, 10);
}
let offset = 0;
if (this.request.query.page) {
offset = parseInt(this.request.query.page - 1, 10) * limit;
if (ctx.request.query.page) {
offset = parseInt(ctx.request.query.page - 1, 10) * limit;
}
const files = yield models.file.findAll({
const files = await models.file.findAll({
where: {
userId: this.user.id,
userId: ctx.user.id,
processed: true,
},
order: '"createdAt" DESC',
order: [
['createdAt', 'DESC'],
],
offset,
limit,
});
this.statsd.incr('file.list', 1);
this.body = files.map(formatFile);
ctx.statsd.incr('file.list', 1);
ctx.body = files.map(formatFile);
}
export function* get() {
const file = yield models.file.findOne({
export async function get(ctx) {
const file = await models.file.findOne({
where: {
id: this.params.id,
id: ctx.params.id,
},
});
this.assert(file, 404, '{"error": {"message": "File not found", "code": 604}}');
const user = yield file.getUser();
this.assert(user && !user.banned, 404, '{"error": {"message": "File not found", "code": 604}}');
this.statsd.incr('file.get', 1);
this.body = formatFile(file);
ctx.assert(file, 404, '{"error": {"message": "File not found", "code": 604}}');
const user = await file.getUser();
ctx.assert(user && !user.banned, 404, '{"error": {"message": "File not found", "code": 604}}');
ctx.statsd.incr('file.get', 1);
ctx.body = formatFile(file);
}
export function* del() {
const file = yield models.file.findOne({
export async function del(ctx) {
const file = await models.file.findOne({
where: {
id: this.params.id,
userId: this.user.id,
id: ctx.params.id,
userId: ctx.user.id,
},
});
this.assert(file, 401, '{"error": {"message": "File not found", "code": 604}}');
yield file.destroy();
const event = { type: 'file-deleted', data: { id: this.params.id } };
yield this.redis.publish(`/file/${this.params.id}`, JSON.stringify(event));
yield this.redis.publish(`/user/${this.user.id}`, JSON.stringify(event));
this.statsd.incr('file.delete', 1);
this.status = 204;
this.body = '';
ctx.assert(file, 401, '{"error": {"message": "File not found", "code": 604}}');
await file.destroy();
const event = { type: 'file-deleted', data: { id: ctx.params.id } };
await ctx.redis.publish(`/file/${ctx.params.id}`, JSON.stringify(event));
await ctx.redis.publish(`/user/${ctx.user.id}`, JSON.stringify(event));
ctx.statsd.incr('file.delete', 1);
ctx.status = 204;
ctx.body = '';
}
export function* events() {
export async function events(ctx) {
const pubsub = redis.createClient(redisUrl);
pubsub.on('ready', () => {
pubsub.subscribe(this.path);
pubsub.subscribe(ctx.path);
});
pubsub.on('message', (channel, message) => {
this.websocket.send(message);
ctx.websocket.send(message);
});
this.websocket.on('close', () => {
ctx.websocket.on('close', () => {
pubsub.quit();
});
}

View file

@ -11,29 +11,29 @@ import models from '../../models';
const from = process.env.EMAIL_FROM;
const fromname = process.env.EMAIL_NAME;
export function* create() {
const stripeToken = this.request.body.stripeToken;
export async function create(ctx) {
const stripeToken = ctx.request.body.stripeToken;
const ip = this.request.headers['x-forwarded-for'] || this.req.connection.remoteAddress;
const ip = ctx.request.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress;
const createCustomer = {
card: stripeToken.id,
plan: 'usd_monthly',
email: this.user.email,
email: ctx.user.email,
};
const customer = yield stripe.customers.create(createCustomer);
const customer = await stripe.customers.create(createCustomer);
this.assert(customer.subscription.status === 'active', 400, '{"status": "error"}');
ctx.assert(customer.subscription.status === 'active', 400, '{"status": "error"}');
delete customer.subscriptions;
const user = yield models.user.findById(this.user.id);
const user = await models.user.findById(ctx.user.id);
user.plan = 'Pro';
yield user.save();
await user.save();
const transaction = yield models.transaction.create({
userId: this.user.id,
const transaction = await models.transaction.create({
userId: ctx.user.id,
amount: customer.subscription.plan.amount,
description: customer.subscription.plan.name,
data: customer,
@ -41,12 +41,12 @@ export function* create() {
ip,
});
yield transaction.save();
await transaction.save();
this.user.plan = 'Pro';
this.body = { status: 'active' };
ctx.user.plan = 'Pro';
ctx.body = { status: 'active' };
const html = yield render('email/inlined/pro');
const html = await render('email/inlined/pro');
const text = `Hey, thanks for upgrading to Hostr Pro!
You've signed up for Hostr Pro Monthly at $6/Month.
@ -55,7 +55,7 @@ export function* create() {
`;
const mail = new sendgrid.Email({
to: this.user.email,
to: ctx.user.email,
subject: 'Hostr Pro',
from,
fromname,
@ -66,20 +66,20 @@ export function* create() {
sendgrid.send(mail);
}
export function* cancel() {
const user = yield models.user.findById(this.user.id);
const transactions = yield user.getTransactions();
export async function cancel(ctx) {
const user = await models.user.findById(ctx.user.id);
const transactions = await user.getTransactions();
const transaction = transactions[0];
yield stripe.customers.cancelSubscription(
await stripe.customers.cancelSubscription(
transaction.data.id,
transaction.data.subscription.id,
{ at_period_end: false }
);
user.plan = 'Free';
yield user.save();
await user.save();
this.user.plan = 'Free';
this.body = { status: 'inactive' };
ctx.user.plan = 'Free';
ctx.body = { status: 'inactive' };
}

View file

@ -9,24 +9,24 @@ const debug = debugname('hostr-api:user');
const redisUrl = process.env.REDIS_URL;
export function* get() {
this.body = this.user;
export async function get(ctx) {
ctx.body = ctx.user;
}
export function* token() {
export async function token(ctx) {
const token = uuid.v4(); // eslint-disable-line no-shadow
yield this.redis.set(token, this.user.id, 'EX', 86400);
this.body = { token };
await ctx.redis.set(token, ctx.user.id, 'EX', 86400);
ctx.body = { token };
}
export function* transaction() {
const transactions = yield models.transaction.findAll({
export async function transaction(ctx) {
const transactions = await models.transaction.findAll({
where: {
userId: this.user.id,
userId: ctx.user.id,
},
});
this.body = transactions.map((item) => {
ctx.body = transactions.map((item) => {
return {
id: item.id,
amount: item.amount / 100,
@ -37,57 +37,57 @@ export function* transaction() {
});
}
export function* settings() {
this.assert(this.request.body, 400,
export async function settings(ctx) {
ctx.assert(ctx.request.body, 400,
'{"error": {"message": "Current Password required to update account.", "code": 612}}');
this.assert(this.request.body.current_password, 400,
ctx.assert(ctx.request.body.current_password, 400,
'{"error": {"message": "Current Password required to update account.", "code": 612}}');
const user = yield models.user.findById(this.user.id);
this.assert(yield passwords.match(this.request.body.current_password, user.password), 400,
const user = await models.user.findById(ctx.user.id);
ctx.assert(await passwords.match(ctx.request.body.current_password, user.password), 400,
'{"error": {"message": "Incorrect password", "code": 606}}');
if (this.request.body.email && this.request.body.email !== user.email) {
user.email = this.request.body.email;
if (ctx.request.body.email && ctx.request.body.email !== user.email) {
user.email = ctx.request.body.email;
}
if (this.request.body.new_password) {
this.assert(this.request.body.new_password.length >= 7, 400,
if (ctx.request.body.new_password) {
ctx.assert(ctx.request.body.new_password.length >= 7, 400,
'{"error": {"message": "Password must be 7 or more characters long.", "code": 606}}');
user.password = yield passwords.hash(this.request.body.new_password);
user.password = await passwords.hash(ctx.request.body.new_password);
}
yield user.save();
this.body = {};
await user.save();
ctx.body = {};
}
export function* events() {
export async function events(ctx) {
const pubsub = redis.createClient(redisUrl);
pubsub.on('message', (channel, message) => {
this.websocket.send(message);
ctx.websocket.send(message);
});
pubsub.on('ready', () => {
this.websocket.on('message', co.wrap(function* wsMessage(message) {
ctx.websocket.on('message', co.wrap(async (message) => {
let json;
try {
json = JSON.parse(message);
} catch (err) {
debug('Invalid JSON for socket auth');
this.websocket.send('Invalid authentication message. Bad JSON?');
this.raven.captureError(err);
ctx.websocket.send('Invalid authentication message. Bad JSON?');
ctx.raven.captureError(err);
}
try {
const reply = yield this.redis.get(json.authorization);
const reply = await ctx.redis.get(json.authorization);
if (reply) {
pubsub.subscribe(`/user/${reply}`);
this.websocket.send('{"status":"active"}');
ctx.websocket.send('{"status":"active"}');
debug('Subscribed to: /user/%s', reply);
} else {
this.websocket.send('Invalid authentication token.');
ctx.websocket.send('Invalid authentication token.');
}
} catch (err) {
debug(err);
this.raven.captureError(err);
ctx.raven.captureError(err);
}
}.bind(this)));
}));
});
this.websocket.on('close', () => {
ctx.websocket.on('close', () => {
debug('Socket closed');
pubsub.quit();
});