Update stuff
This commit is contained in:
parent
0254e42b9c
commit
553ba9db9a
40 changed files with 7343 additions and 717 deletions
24
.env.example
24
.env.example
|
@ -1,24 +0,0 @@
|
|||
export DEBUG="hostr*"
|
||||
|
||||
export NODE_ENV=development
|
||||
export PORT=4040
|
||||
export WEB_BASE_URL=http://localhost:$PORT
|
||||
export API_BASE_URL=http://localhost:$PORT/api
|
||||
export UPLOAD_STORAGE_PATH=$HOME/.hostr/uploads
|
||||
export COOKIE_KEY=INSECURE
|
||||
export EMAIL_FROM=
|
||||
export EMAIL_NAME=
|
||||
|
||||
export STATSD_HOST=localhost
|
||||
export MONGO_URL=mongodb://localhost:27017/hostr
|
||||
export REDIS_URL=redis://localhost:6379
|
||||
export MANDRILL_KEY=
|
||||
export STRIPE_SECRET_KEY=
|
||||
export STRIPE_PUBLIC_KEY=
|
||||
|
||||
# optional, some functionality will be disabled
|
||||
export AWS_ACCESS_KEY_ID=
|
||||
export AWS_SECRET_ACCESS_KEY=
|
||||
export AWS_BUCKET=
|
||||
export VIRUSTOTAL_KEY=
|
||||
export SENTRY_DSN=
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
|
@ -0,0 +1,19 @@
|
|||
FROM node:10.2.1-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --update git python make gcc g++
|
||||
|
||||
COPY package.json package.json
|
||||
COPY yarn.lock yarn.lock
|
||||
|
||||
RUN yarn
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN yarn run jspm && yarn run build
|
||||
|
||||
ENV PORT 3000
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["yarn", "start"]
|
45
Makefile
Normal file
45
Makefile
Normal file
|
@ -0,0 +1,45 @@
|
|||
# See http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo
|
||||
@echo "Commands:"
|
||||
@grep -E -h '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
@echo
|
||||
@echo "See README.md"
|
||||
@echo
|
||||
|
||||
.PHONY: build
|
||||
build: ## Run `yarn run build`
|
||||
docker-compose run --rm app yarn run build
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run tests
|
||||
docker-compose run --rm app yarn test
|
||||
|
||||
.PHONY: logs
|
||||
logs: ## Tail the app and worker logs
|
||||
docker-compose logs -f app worker
|
||||
|
||||
.PHONY: migrate
|
||||
migrate: ## Migrate database schema
|
||||
docker-compose run --rm app yarn run initdb
|
||||
|
||||
.PHONY: init
|
||||
init: ## Migrate database schema
|
||||
docker-compose run --rm app yarn run init
|
||||
|
||||
.PHONY: watch
|
||||
watch-frontend: ## Build and watch for changes
|
||||
docker-compose run --rm app yarn run watch
|
||||
|
||||
.PHONY: docker-compose-up
|
||||
docker-compose-up: ## Start (and create) docker containers
|
||||
docker-compose up -d
|
||||
|
||||
.PHONY: yarn
|
||||
yarn: ## Update yarn dependencies
|
||||
docker-compose run --rm app yarn
|
||||
|
||||
.PHONY: shell
|
||||
shell: ## Run shell
|
||||
docker-compose run --rm app sh
|
47
api/app.js
47
api/app.js
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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' };
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
29
app.js
29
app.js
|
@ -1,5 +1,5 @@
|
|||
import path from 'path';
|
||||
import koa from 'koa';
|
||||
import Koa from 'koa';
|
||||
import logger from 'koa-logger';
|
||||
import serve from 'koa-static';
|
||||
import favicon from 'koa-favicon';
|
||||
|
@ -7,6 +7,7 @@ import compress from 'koa-compress';
|
|||
import bodyparser from 'koa-bodyparser';
|
||||
import websockify from 'koa-websocket';
|
||||
import helmet from 'koa-helmet';
|
||||
import session from 'koa-session';
|
||||
import raven from 'raven';
|
||||
import * as redis from './lib/redis';
|
||||
import api, { ws } from './api/app';
|
||||
|
@ -15,40 +16,42 @@ import web from './web/app';
|
|||
import debugname from 'debug';
|
||||
const debug = debugname('hostr');
|
||||
|
||||
const app = websockify(koa());
|
||||
const app = websockify(new Koa());
|
||||
app.keys = [process.env.COOKIE_KEY];
|
||||
|
||||
if (process.env.SENTRY_DSN) {
|
||||
const ravenClient = new raven.Client(process.env.SENTRY_DSN);
|
||||
ravenClient.patchGlobal();
|
||||
app.use(function* ravenMiddleware(next) {
|
||||
app.use(async (ctx, next) => {
|
||||
this.raven = ravenClient;
|
||||
yield next;
|
||||
await next();
|
||||
});
|
||||
app.ws.use(function* ravenWsMiddleware(next) {
|
||||
app.ws.use(async (ctx, next) => {
|
||||
this.raven = ravenClient;
|
||||
yield next;
|
||||
await next();
|
||||
});
|
||||
}
|
||||
|
||||
app.use(helmet());
|
||||
|
||||
app.use(function* errorMiddleware(next) {
|
||||
this.set('Server', 'Nintendo 64');
|
||||
if (this.req.headers['x-forwarded-proto'] === 'http') {
|
||||
this.redirect(`https://${this.req.headers.host}${this.req.url}`);
|
||||
app.use(async (ctx, next) => {
|
||||
ctx.set('Server', 'Nintendo 64');
|
||||
if (ctx.req.headers['x-forwarded-proto'] === 'http') {
|
||||
ctx.redirect(`https://${this.req.headers.host}${this.req.url}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
yield next;
|
||||
await next();
|
||||
} catch (err) {
|
||||
if (!err.statusCode && this.raven) {
|
||||
this.raven.captureError(err);
|
||||
if (!err.statusCode && ctx.raven) {
|
||||
ctx.raven.captureError(err);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
app.use(session(app));
|
||||
|
||||
app.use(redis.middleware());
|
||||
app.use(logger());
|
||||
app.use(compress());
|
||||
|
|
87
docker-compose.yml
Executable file
87
docker-compose.yml
Executable file
|
@ -0,0 +1,87 @@
|
|||
version: "2"
|
||||
|
||||
services:
|
||||
app:
|
||||
build: ./
|
||||
environment:
|
||||
DEBUG:
|
||||
NODE_ENV:
|
||||
WEB_BASE_URL:
|
||||
API_BASE_URL:
|
||||
UPLOAD_STORAGE_PATH:
|
||||
COOKIE_KEY:
|
||||
EMAIL_FROM:
|
||||
EMAIL_NAME:
|
||||
STATSD_HOST:
|
||||
DATABASE_URL:
|
||||
REDIS_URL:
|
||||
SENDGRID_KEY:
|
||||
STRIPE_SECRET_KEY:
|
||||
STRIPE_PUBLIC_KEY:
|
||||
AWS_ENDPOINT:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
AWS_BUCKET:
|
||||
VIRUSTOTAL_KEY:
|
||||
SENTRY_DSN:
|
||||
volumes:
|
||||
- ./:/app:cached
|
||||
- uploads:/hostr/uploads
|
||||
ports:
|
||||
- "3000:3000"
|
||||
command: yarn run watch
|
||||
worker:
|
||||
build: ./
|
||||
environment:
|
||||
DEBUG:
|
||||
NODE_ENV:
|
||||
WEB_BASE_URL:
|
||||
API_BASE_URL:
|
||||
UPLOAD_STORAGE_PATH:
|
||||
COOKIE_KEY:
|
||||
EMAIL_FROM:
|
||||
EMAIL_NAME:
|
||||
STATSD_HOST:
|
||||
DATABASE_URL:
|
||||
REDIS_URL:
|
||||
SENDGRID_KEY:
|
||||
STRIPE_SECRET_KEY:
|
||||
STRIPE_PUBLIC_KEY:
|
||||
AWS_ENDPOINT:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
AWS_BUCKET:
|
||||
VIRUSTOTAL_KEY:
|
||||
SENTRY_DSN:
|
||||
volumes:
|
||||
- ./:/app:cached
|
||||
- uploads:/hostr/uploads
|
||||
ports:
|
||||
- "3001:3000"
|
||||
command: yarn run worker
|
||||
database:
|
||||
image: "postgres:10-alpine"
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_PASSWORD: "hostr"
|
||||
POSTGRES_USER: "hostr"
|
||||
POSTGRES_DB: "hostr"
|
||||
redis:
|
||||
image: "redis:4.0.2-alpine"
|
||||
ports:
|
||||
- "6379:6379"
|
||||
minio:
|
||||
image: minio/minio:RELEASE.2018-05-16T23-35-33Z
|
||||
volumes:
|
||||
- export:/export
|
||||
command: server /export
|
||||
ports:
|
||||
- "9000:9000"
|
||||
environment:
|
||||
MINIO_ACCESS_KEY: 7HYV3KPRGQ8Z5YCDNWC6
|
||||
MINIO_SECRET_KEY: "0kWP/ZkgIwQzgL9t4SGv9Uc93rO//OdyqMH329b/"
|
||||
|
||||
volumes:
|
||||
uploads:
|
||||
export:
|
|
@ -1,17 +1,18 @@
|
|||
import fs from 'fs';
|
||||
import createError from 'http-errors';
|
||||
import { get as getSFTP } from './sftp';
|
||||
import { get as getS3 } from './s3';
|
||||
|
||||
import debugname from 'debug';
|
||||
const debug = debugname('hostr:file-stream');
|
||||
|
||||
function writer(localPath, remoteRead) {
|
||||
return new Promise((resolve, reject) => {
|
||||
remoteRead.once('error', () => {
|
||||
debug('remote error');
|
||||
const localWrite = fs.createWriteStream(localPath);
|
||||
remoteRead.once('error', (err) => {
|
||||
debug('remote error', err);
|
||||
reject(createError(404));
|
||||
});
|
||||
const localWrite = fs.createWriteStream(localPath);
|
||||
|
||||
localWrite.once('finish', () => {
|
||||
debug('local write end');
|
||||
resolve(fs.createReadStream(localPath));
|
||||
|
@ -29,12 +30,9 @@ export default function hostrFileStream(localPath, remotePath) {
|
|||
return new Promise((resolve, reject) => {
|
||||
localRead.once('error', () => {
|
||||
debug('not found locally');
|
||||
getSFTP(remotePath)
|
||||
.then((remoteRead) => writer(localPath, remoteRead))
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
debug('not on sftp', err);
|
||||
});
|
||||
writer(localPath, getS3(remotePath)).then((readable) => {
|
||||
resolve(readable);
|
||||
}).catch(reject);
|
||||
});
|
||||
localRead.once('readable', () => {
|
||||
debug('found locally');
|
||||
|
|
|
@ -10,17 +10,17 @@ function randomID() {
|
|||
return rand;
|
||||
}
|
||||
|
||||
function* checkId(Files, fileId, attempts) {
|
||||
async function checkId(Files, fileId, attempts) {
|
||||
if (attempts > 10) {
|
||||
return false;
|
||||
}
|
||||
const file = yield models.file.findById(fileId);
|
||||
const file = await models.file.findById(fileId);
|
||||
if (file === null) {
|
||||
return fileId;
|
||||
}
|
||||
return checkId(randomID(), ++attempts); // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
export default function* (Files) {
|
||||
return yield checkId(Files, randomID(), 0);
|
||||
export default function (Files) {
|
||||
return checkId(Files, randomID(), 0);
|
||||
}
|
||||
|
|
37
lib/koa-statsd.js
Normal file
37
lib/koa-statsd.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var Stats = require('statsy');
|
||||
|
||||
/**
|
||||
* Initialize stats middleware with `opts`
|
||||
* which are passed to statsy.
|
||||
*
|
||||
* @param {Object} [opts]
|
||||
* @return {Function}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
module.exports = function(opts){
|
||||
opts = opts || {};
|
||||
var s = new Stats(opts);
|
||||
|
||||
return async (ctx, next) => {
|
||||
// counters
|
||||
s.incr('request.count');
|
||||
s.incr('request.' + ctx.method + '.count');
|
||||
|
||||
// size
|
||||
s.histogram('request.size', ctx.request.length || 0);
|
||||
|
||||
// remote addr
|
||||
// s.set('request.addresses', this.ip);
|
||||
|
||||
// duration
|
||||
ctx.res.on('finish', s.timer('request.duration'));
|
||||
|
||||
await next();
|
||||
}
|
||||
};
|
12
lib/redis.js
12
lib/redis.js
|
@ -44,15 +44,15 @@ const wrapped = new Promise((resolve, reject) =>
|
|||
);
|
||||
|
||||
export function sessionStore() {
|
||||
return function* sessionStoreMiddleware(next) {
|
||||
const sess = yield redisSession;
|
||||
yield sess.bind(this)(next);
|
||||
return async (ctx, next) => {
|
||||
const sess = await redisSession;
|
||||
await sess.bind(ctx)(next());
|
||||
};
|
||||
}
|
||||
|
||||
export function middleware() {
|
||||
return function* redisMiddleware(next) {
|
||||
this.redis = yield wrapped;
|
||||
yield next;
|
||||
return async (ctx, next) => {
|
||||
ctx.redis = await wrapped;
|
||||
await next();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,23 +1,29 @@
|
|||
import fs from 'mz/fs';
|
||||
import lwip from 'lwip';
|
||||
import jimp from 'jimp';
|
||||
import debugname from 'debug';
|
||||
const debug = debugname('hostr-api:resize');
|
||||
|
||||
const types = {
|
||||
jpg: jimp.MIME_JPEG,
|
||||
png: jimp.MIME_PNG,
|
||||
gif: jimp.MIME_JPEG,
|
||||
}
|
||||
|
||||
function cover(path, type, size) {
|
||||
return new Promise((resolve, reject) => {
|
||||
lwip.open(path, type, (errIn, image) => {
|
||||
jimp.read(path, (errIn, image) => {
|
||||
debug('Image Opened');
|
||||
if (errIn) {
|
||||
reject(errIn);
|
||||
}
|
||||
|
||||
image.cover(size.width, size.height, (errOut, resized) => {
|
||||
image.quality(80).cover(size.width, size.height, (errOut, resized) => {
|
||||
debug('Image Resized');
|
||||
if (errOut) {
|
||||
reject(errOut);
|
||||
}
|
||||
|
||||
resized.toBuffer(type, (errBuf, buffer) => {
|
||||
resized.getBuffer(types[type], (errBuf, buffer) => {
|
||||
debug('Image Buffered');
|
||||
if (errBuf) {
|
||||
reject(errBuf);
|
||||
|
@ -31,19 +37,19 @@ function cover(path, type, size) {
|
|||
|
||||
function scale(path, type, size) {
|
||||
return new Promise((resolve, reject) => {
|
||||
lwip.open(path, type, (errIn, image) => {
|
||||
jimp.read(path, (errIn, image) => {
|
||||
debug('Image Opened');
|
||||
if (errIn) {
|
||||
reject(errIn);
|
||||
}
|
||||
|
||||
image.cover(size.width, size.height, (errOut, resized) => {
|
||||
image.quality(80).cover(size.width, size.height, (errOut, resized) => {
|
||||
debug('Image Resized');
|
||||
if (errOut) {
|
||||
reject(errOut);
|
||||
}
|
||||
|
||||
resized.toBuffer(type, (errBuf, buffer) => {
|
||||
resized.getBuffer(types[type], (errBuf, buffer) => {
|
||||
debug('Image Buffered');
|
||||
if (errBuf) {
|
||||
reject(errBuf);
|
||||
|
|
27
lib/s3.js
27
lib/s3.js
|
@ -2,13 +2,32 @@ import aws from 'aws-sdk';
|
|||
import debugname from 'debug';
|
||||
const debug = debugname('hostr:s3');
|
||||
|
||||
const s3 = new aws.S3();
|
||||
const s3 = new aws.S3({
|
||||
endpoint: process.env.AWS_ENDPOINT,
|
||||
s3ForcePathStyle: true,
|
||||
signatureVersion: 'v4',
|
||||
});
|
||||
|
||||
export function get(key) {
|
||||
let fullKey = `hostr_files/${key}`;
|
||||
let fullKey = `uploads/${key}`;
|
||||
if (key.substr(2, 5) === '970/' || key.substr(2, 5) === '150/') {
|
||||
fullKey = `hostr_files/${key.substr(2)}`;
|
||||
fullKey = `uploads/${key.substr(2)}`;
|
||||
}
|
||||
debug('fetching from s3: %s', fullKey);
|
||||
return s3.getObject({ Bucket: process.env.AWS_BUCKET, Key: fullKey }).createReadStream();
|
||||
return s3.getObject({ Bucket: process.env.AWS_BUCKET, Key: fullKey })
|
||||
.createReadStream()
|
||||
.on('error', (err) => {
|
||||
debug('S3 error', err);
|
||||
});
|
||||
}
|
||||
|
||||
export function upload(stream, key, callback) {
|
||||
debug(`sending to s3: uploads/'${key}`);
|
||||
const params = { Bucket: process.env.AWS_BUCKET, Key: `uploads/${key}`, Body: stream };
|
||||
const uploading = s3.upload(params);
|
||||
uploading.on('error', (err) => {
|
||||
debug('S3 Error', err);
|
||||
});
|
||||
uploading.send(callback);
|
||||
return uploading;
|
||||
}
|
||||
|
|
168
lib/uploader.js
168
lib/uploader.js
|
@ -1,5 +1,5 @@
|
|||
import { join } from 'path';
|
||||
import parse from 'co-busboy';
|
||||
import Busboy from 'busboy';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'mz/fs';
|
||||
import sizeOf from 'image-size';
|
||||
|
@ -10,6 +10,7 @@ import { formatFile } from './format';
|
|||
import resize from './resize';
|
||||
import malware from './malware';
|
||||
import { sniff } from './type';
|
||||
import { upload as s3upload } from './s3';
|
||||
|
||||
import debugname from 'debug';
|
||||
const debug = debugname('hostr-api:uploader');
|
||||
|
@ -33,29 +34,45 @@ export default class Uploader {
|
|||
this.receivedSize = 0;
|
||||
}
|
||||
|
||||
*accept() {
|
||||
this.upload = yield parse(this.context, {
|
||||
async checkLimit() {
|
||||
const count = await models.file.count({
|
||||
where: {
|
||||
userId: this.context.user.id,
|
||||
createdAt: {
|
||||
$gt: Date.now() - 86400000,
|
||||
},
|
||||
},
|
||||
});
|
||||
const userLimit = this.context.user.daily_upload_allowance;
|
||||
const underLimit = (count < userLimit || userLimit === 'unlimited');
|
||||
if (!underLimit) {
|
||||
this.context.statsd.incr('file.overlimit', 1);
|
||||
}
|
||||
this.context.assert(underLimit, 400, `{
|
||||
"error": {
|
||||
"message": "Daily upload limits (${this.context.user.daily_upload_allowance}) exceeded.",
|
||||
"code": 602
|
||||
}
|
||||
}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
async accept() {
|
||||
return new Promise((resolve) => {
|
||||
this.upload = new Busboy({
|
||||
autoFields: true,
|
||||
headers: this.context.request.headers,
|
||||
limits: { files: 1 },
|
||||
highWaterMark: 10000000,
|
||||
});
|
||||
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.upload.on('error', (err) => {
|
||||
this.statsd.incr('file.upload.error', 1);
|
||||
debug(err);
|
||||
reject();
|
||||
});
|
||||
this.upload.on('file', async (fieldname, file, filename, encoding, mimetype) => {
|
||||
debug('FILE', fieldname, file, filename, encoding, mimetype);
|
||||
|
||||
this.upload.on('end', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
this.upload.filename = filename;
|
||||
|
||||
this.tempGuid = this.tempGuid;
|
||||
this.file = yield models.file.create({
|
||||
id: yield createHostrId(),
|
||||
this.file = await models.file.create({
|
||||
id: await createHostrId(),
|
||||
name: this.upload.filename.replace(/[^a-zA-Z0-9\.\-_\s]/g, '').replace(/\s+/g, ''),
|
||||
originalName: this.upload.filename,
|
||||
userId: this.context.user.id,
|
||||
|
@ -66,21 +83,16 @@ export default class Uploader {
|
|||
width: null,
|
||||
height: null,
|
||||
});
|
||||
yield this.file.save();
|
||||
}
|
||||
await this.file.save();
|
||||
|
||||
receive() {
|
||||
return new Promise((resolve) => {
|
||||
this.path = join(this.file.id[0], `${this.file.id}_${this.file.name}`);
|
||||
this.localStream = fs.createWriteStream(join(storePath, this.path));
|
||||
|
||||
this.upload.pause();
|
||||
|
||||
this.localStream.on('finish', () => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.upload.on('data', (data) => {
|
||||
|
||||
file.on('data', (data) => {
|
||||
this.receivedSize += data.length;
|
||||
if (this.receivedSize > this.context.user.max_filesize) {
|
||||
fs.unlink(join(storePath, this.path));
|
||||
|
@ -103,24 +115,30 @@ export default class Uploader {
|
|||
this.md5sum.update(data);
|
||||
});
|
||||
|
||||
this.upload.on('end', () => {
|
||||
this.file.size = this.receivedSize;
|
||||
this.file.md5 = this.md5sum.digest('hex');
|
||||
this.localStream.end();
|
||||
});
|
||||
|
||||
this.upload.resume();
|
||||
});
|
||||
}
|
||||
|
||||
acceptedEvent() {
|
||||
debug('accepted');
|
||||
const accepted = `{"type": "file-accepted", "data":
|
||||
{"id": "${this.file.id}", "guid": "${this.tempGuid}", "href": "${baseURL}/${this.file.id}"}}`;
|
||||
this.context.redis.publish(`/user/${this.context.user.id}`, accepted);
|
||||
this.context.statsd.incr('file.upload.accepted', 1);
|
||||
|
||||
file.on('end', () => {
|
||||
this.file.size = this.receivedSize;
|
||||
this.file.md5 = this.md5sum.digest('hex');
|
||||
this.localStream.end();
|
||||
this.processingEvent();
|
||||
});
|
||||
|
||||
this.localStream.on('end', () => {
|
||||
s3upload(fs.createReadStream(join(storePath, this.path)), this.path);
|
||||
});
|
||||
|
||||
});
|
||||
this.context.req.pipe(this.upload);
|
||||
});
|
||||
}
|
||||
|
||||
processingEvent() {
|
||||
debug('processing');
|
||||
const processing = `{"type": "file-progress", "data":
|
||||
{"id": "${this.file.id}", "complete": 100}}`;
|
||||
this.context.redis.publish(`/file/${this.file.id}`, processing);
|
||||
|
@ -128,52 +146,7 @@ export default class Uploader {
|
|||
this.context.statsd.incr('file.upload.complete', 1);
|
||||
}
|
||||
|
||||
completeEvent() {
|
||||
const complete = `{"type": "file-added", "data": ${JSON.stringify(formatFile(this.file))}}`;
|
||||
this.context.redis.publish(`/file/${this.file.id}`, complete);
|
||||
this.context.redis.publish(`/user/${this.context.user.id}`, complete);
|
||||
}
|
||||
|
||||
*checkLimit() {
|
||||
const count = yield models.file.count({
|
||||
where: {
|
||||
userId: this.context.user.id,
|
||||
createdAt: {
|
||||
$gt: Date.now() - 86400000,
|
||||
},
|
||||
},
|
||||
});
|
||||
const userLimit = this.context.user.daily_upload_allowance;
|
||||
const underLimit = (count < userLimit || userLimit === 'unlimited');
|
||||
if (!underLimit) {
|
||||
this.context.statsd.incr('file.overlimit', 1);
|
||||
}
|
||||
this.context.assert(underLimit, 400, `{
|
||||
"error": {
|
||||
"message": "Daily upload limits (${this.context.user.daily_upload_allowance}) exceeded.",
|
||||
"code": 602
|
||||
}
|
||||
}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
*finalise() {
|
||||
this.file.size = this.receivedSize;
|
||||
this.file.status = 'active';
|
||||
this.file.processed = 'true';
|
||||
yield this.file.save();
|
||||
}
|
||||
|
||||
resizeImage(upload, type, currentSize, dim) {
|
||||
return resize(join(storePath, this.path), type, currentSize, dim).then((image) => {
|
||||
const path = join(this.file.id[0], String(dim.width), `${this.file.id}_${this.file.name}`);
|
||||
debug('Writing file');
|
||||
debug(join(storePath, path));
|
||||
return fs.writeFile(join(storePath, path), image).catch(debug);
|
||||
}).catch(debug);
|
||||
}
|
||||
|
||||
*processImage(upload) {
|
||||
async processImage(upload) {
|
||||
return new Promise((resolve) => {
|
||||
let size;
|
||||
try {
|
||||
|
@ -205,16 +178,43 @@ export default class Uploader {
|
|||
});
|
||||
}
|
||||
|
||||
resizeImage(upload, type, currentSize, dim) {
|
||||
return resize(join(storePath, this.path), type, currentSize, dim).then((image) => {
|
||||
const path = join(this.file.id[0], String(dim.width), `${this.file.id}_${this.file.name}`);
|
||||
debug('Writing file');
|
||||
debug(join(storePath, path));
|
||||
return fs.writeFile(join(storePath, path), image).then(() => {
|
||||
s3upload(fs.createReadStream(join(storePath, path)), path);
|
||||
}).catch(debug);
|
||||
}).catch(debug);
|
||||
}
|
||||
|
||||
async finalise() {
|
||||
debug('finalise');
|
||||
this.file.size = this.receivedSize;
|
||||
this.file.status = 'active';
|
||||
this.file.processed = 'true';
|
||||
await this.file.save();
|
||||
this.completeEvent();
|
||||
}
|
||||
|
||||
completeEvent() {
|
||||
debug('complete');
|
||||
const complete = `{"type": "file-added", "data": ${JSON.stringify(formatFile(this.file))}}`;
|
||||
this.context.redis.publish(`/file/${this.file.id}`, complete);
|
||||
this.context.redis.publish(`/user/${this.context.user.id}`, complete);
|
||||
}
|
||||
|
||||
malwareScan() {
|
||||
if (process.env.VIRUSTOTAL_KEY) {
|
||||
// Check in the background
|
||||
process.nextTick(function* scan() {
|
||||
process.nextTick(async () => {
|
||||
debug('Malware Scan');
|
||||
const result = yield malware(this);
|
||||
const result = await malware(this);
|
||||
if (result) {
|
||||
this.file.malwarePositives = result.positives;
|
||||
this.file.save();
|
||||
const fileMalware = yield models.malware.create({
|
||||
const fileMalware = await models.malware.create({
|
||||
fileId: this.file.id,
|
||||
positives: result.positives,
|
||||
virustotal: result,
|
||||
|
|
|
@ -3,13 +3,11 @@ export default function (sequelize, DataTypes) {
|
|||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
activatedAt: { type: DataTypes.DATE },
|
||||
email: DataTypes.STRING,
|
||||
}, {
|
||||
classMethods: {
|
||||
associate: (models) => {
|
||||
Activation.belongsTo(models.user);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Activation.associate = function associate(models) {
|
||||
Activation.belongsTo(models.user);
|
||||
};
|
||||
|
||||
return Activation;
|
||||
}
|
||||
|
|
|
@ -29,21 +29,24 @@ export default function (sequelize, DataTypes) {
|
|||
fields: ['userId'],
|
||||
},
|
||||
],
|
||||
classMethods: {
|
||||
accessed: (id) => sequelize.query(`
|
||||
});
|
||||
|
||||
File.accessed = function accessed(id) {
|
||||
sequelize.query(`
|
||||
UPDATE files
|
||||
SET "downloads" = downloads + 1, "accessedAt" = NOW()
|
||||
WHERE "id" = :id`,
|
||||
{
|
||||
replacements: { id },
|
||||
type: sequelize.QueryTypes.UPDATE,
|
||||
}),
|
||||
associate: (models) => {
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
File.associate = function associate(models) {
|
||||
File.belongsTo(models.user);
|
||||
File.hasOne(models.malware);
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return File;
|
||||
}
|
||||
|
|
|
@ -3,12 +3,14 @@ import path from 'path';
|
|||
import Sequelize from 'sequelize';
|
||||
|
||||
import debugname from 'debug';
|
||||
|
||||
const debug = debugname('hostr:models');
|
||||
|
||||
const config = {
|
||||
dialect: 'postgres',
|
||||
protocol: 'postgres',
|
||||
logging: debug,
|
||||
quoteIdentifiers: true,
|
||||
logging: false,
|
||||
};
|
||||
|
||||
const sequelize = new Sequelize(process.env.DATABASE_URL, config);
|
||||
|
@ -16,7 +18,7 @@ const db = {};
|
|||
|
||||
fs
|
||||
.readdirSync(__dirname)
|
||||
.filter((file) => (file.indexOf('.') !== 0) && (file !== 'index.js'))
|
||||
.filter(file => (file.indexOf('.') !== 0) && (file !== 'index.js'))
|
||||
.forEach((file) => {
|
||||
const model = sequelize.import(path.join(__dirname, file));
|
||||
db[model.name] = model;
|
||||
|
|
|
@ -9,12 +9,11 @@ export default function (sequelize, DataTypes) {
|
|||
fields: ['ip'],
|
||||
},
|
||||
],
|
||||
classMethods: {
|
||||
associate: (models) => {
|
||||
Login.belongsTo(models.user);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Login.associate = function associate(models) {
|
||||
Login.belongsTo(models.user);
|
||||
};
|
||||
|
||||
return Login;
|
||||
}
|
||||
|
|
|
@ -3,13 +3,11 @@ export default function (sequelize, DataTypes) {
|
|||
fileId: { type: DataTypes.STRING(12), primaryKey: true }, // eslint-disable-line new-cap
|
||||
positives: DataTypes.INTEGER,
|
||||
virustotal: DataTypes.JSON,
|
||||
}, {
|
||||
classMethods: {
|
||||
associate: (models) => {
|
||||
Malware.belongsTo(models.file);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Malware.associate = function associate(models) {
|
||||
Malware.belongsTo(models.file);
|
||||
};
|
||||
|
||||
return Malware;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
export default function (sequelize, DataTypes) {
|
||||
const Remember = sequelize.define('remember', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
}, {
|
||||
classMethods: {
|
||||
associate: (models) => {
|
||||
Remember.belongsTo(models.user);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Remember.associate = function associate(models) {
|
||||
Remember.belongsTo(models.user);
|
||||
};
|
||||
|
||||
return Remember;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
export default function (sequelize, DataTypes) {
|
||||
const Reset = sequelize.define('reset', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
}, {
|
||||
classMethods: {
|
||||
associate: (models) => {
|
||||
Reset.belongsTo(models.user);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reset.associate = function associate(models) {
|
||||
Reset.belongsTo(models.user);
|
||||
};
|
||||
|
||||
return Reset;
|
||||
}
|
||||
|
|
|
@ -12,12 +12,11 @@ export default function (sequelize, DataTypes) {
|
|||
fields: ['userId'],
|
||||
},
|
||||
],
|
||||
classMethods: {
|
||||
associate: (models) => {
|
||||
Transaction.belongsTo(models.user);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Transaction.associate = function associate(models) {
|
||||
Transaction.belongsTo(models.user);
|
||||
};
|
||||
|
||||
return Transaction;
|
||||
}
|
||||
|
|
|
@ -16,14 +16,13 @@ export default function (sequelize, DataTypes) {
|
|||
fields: ['email'],
|
||||
},
|
||||
],
|
||||
classMethods: {
|
||||
associate: (models) => {
|
||||
});
|
||||
|
||||
User.associate = function associate(models) {
|
||||
User.hasMany(models.file);
|
||||
User.hasMany(models.transaction);
|
||||
User.hasOne(models.activation);
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return User;
|
||||
}
|
||||
|
|
175
nginx.conf.sigil
175
nginx.conf.sigil
|
@ -1,39 +1,78 @@
|
|||
upstream {{ .APP }} {
|
||||
{{ range .DOKKU_APP_LISTENERS | split " " }}
|
||||
server {{ . }};
|
||||
{{ range $port_map := .PROXY_PORT_MAP | split " " }}
|
||||
{{ $port_map_list := $port_map | split ":" }}
|
||||
{{ $scheme := index $port_map_list 0 }}
|
||||
{{ $listen_port := index $port_map_list 1 }}
|
||||
{{ $upstream_port := index $port_map_list 2 }}
|
||||
|
||||
{{ if eq $scheme "http" }}
|
||||
server {
|
||||
listen [::]:{{ $listen_port }};
|
||||
listen {{ $listen_port }};
|
||||
{{ if $.NOSSL_SERVER_NAME }}server_name {{ $.NOSSL_SERVER_NAME }}; {{ end }}
|
||||
access_log /var/log/nginx/{{ $.APP }}-access.log;
|
||||
error_log /var/log/nginx/{{ $.APP }}-error.log;
|
||||
{{ if (and (eq $listen_port "80") ($.SSL_INUSE)) }}
|
||||
return 301 https://$host:{{ $.PROXY_SSL_PORT }}$request_uri;
|
||||
{{ else }}
|
||||
location / {
|
||||
|
||||
gzip on;
|
||||
gzip_min_length 1100;
|
||||
gzip_buffers 4 32k;
|
||||
gzip_types text/css text/javascript text/xml text/plain text/x-component application/javascript application/x-javascript application/json application/xml application/rss+xml font/truetype application/x-font-ttf font/opentype application/vnd.ms-fontobject image/svg+xml;
|
||||
gzip_vary on;
|
||||
gzip_comp_level 6;
|
||||
|
||||
proxy_pass http://{{ $.APP }}-{{ $upstream_port }};
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Request-Start $msec;
|
||||
}
|
||||
include {{ $.DOKKU_ROOT }}/{{ $.APP }}/nginx.conf.d/*.conf;
|
||||
|
||||
error_page 400 401 402 403 405 406 407 408 409 410 411 412 413 414 415 416 417 418 420 422 423 424 426 428 429 431 444 449 450 451 /400-error.html;
|
||||
location /400-error.html {
|
||||
root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors;
|
||||
internal;
|
||||
}
|
||||
|
||||
error_page 404 /404-error.html;
|
||||
location /404-error.html {
|
||||
root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors;
|
||||
internal;
|
||||
}
|
||||
|
||||
error_page 500 501 502 503 504 505 506 507 508 509 510 511 /500-error.html;
|
||||
location /500-error.html {
|
||||
root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors;
|
||||
internal;
|
||||
}
|
||||
{{ end }}
|
||||
}
|
||||
|
||||
{{ else if eq $scheme "https"}}
|
||||
server {
|
||||
listen [::]:{{ .NGINX_PORT }};
|
||||
listen {{ .NGINX_PORT }};
|
||||
server_name {{ .NOSSL_SERVER_NAME }};
|
||||
access_log /var/log/nginx/{{ .APP }}-access.log;
|
||||
error_log /var/log/nginx/{{ .APP }}-error.log;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
listen [::]:{{ $listen_port }} ssl {{ if eq $.HTTP2_SUPPORTED "true" }}http2{{ else if eq $.SPDY_SUPPORTED "true" }}spdy{{ end }};
|
||||
listen {{ $listen_port }} ssl {{ if eq $.HTTP2_SUPPORTED "true" }}http2{{ else if eq $.SPDY_SUPPORTED "true" }}spdy{{ end }};
|
||||
{{ if $.SSL_SERVER_NAME }}server_name hostr.co; {{ end }}
|
||||
{{ if $.NOSSL_SERVER_NAME }}server_name hostr.co; {{ end }}
|
||||
access_log /var/log/nginx/{{ $.APP }}-access.log;
|
||||
error_log /var/log/nginx/{{ $.APP }}-error.log;
|
||||
|
||||
server {
|
||||
listen [::]:{{ .NGINX_SSL_PORT }} ssl spdy;
|
||||
listen {{ .NGINX_SSL_PORT }} ssl spdy;
|
||||
server_name hostr.co;
|
||||
access_log /var/log/nginx/{{ .APP }}-access.log;
|
||||
error_log /var/log/nginx/{{ .APP }}-error.log;
|
||||
|
||||
ssl_certificate {{ .APP_SSL_PATH }}/server.crt;
|
||||
ssl_certificate_key {{ .APP_SSL_PATH }}/server.key;
|
||||
|
||||
client_max_body_size 1G;
|
||||
ssl_certificate {{ $.APP_SSL_PATH }}/server.crt;
|
||||
ssl_certificate_key {{ $.APP_SSL_PATH }}/server.key;
|
||||
ssl_protocols TLSv1.2;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
keepalive_timeout 70;
|
||||
add_header Alternate-Protocol {{ .NGINX_SSL_PORT }}:npn-spdy/2;
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
|
||||
location /apps/ {
|
||||
alias {{ .DOKKU_ROOT }}/{{ .APP }}/apps/;
|
||||
}
|
||||
{{ if and (eq $.SPDY_SUPPORTED "true") (ne $.HTTP2_SUPPORTED "true") }}add_header Alternate-Protocol {{ $.PROXY_SSL_PORT }}:npn-spdy/2;{{ end }}
|
||||
|
||||
location / {
|
||||
|
||||
gzip on;
|
||||
gzip_min_length 1100;
|
||||
gzip_buffers 4 32k;
|
||||
|
@ -41,8 +80,7 @@ server {
|
|||
gzip_vary on;
|
||||
gzip_comp_level 6;
|
||||
|
||||
proxy_pass http://{{ .APP }};
|
||||
proxy_request_buffering off;
|
||||
proxy_pass http://{{ $.APP }}-{{ $upstream_port }};
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
@ -52,26 +90,45 @@ server {
|
|||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Request-Start $msec;
|
||||
}
|
||||
include {{ .DOKKU_ROOT }}/{{ .APP }}/nginx.conf.d/*.conf;
|
||||
include {{ $.DOKKU_ROOT }}/{{ $.APP }}/nginx.conf.d/*.conf;
|
||||
|
||||
error_page 400 401 402 403 405 406 407 408 409 410 411 412 413 414 415 416 417 418 420 422 423 424 426 428 429 431 444 449 450 451 /400-error.html;
|
||||
location /400-error.html {
|
||||
root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors;
|
||||
internal;
|
||||
}
|
||||
|
||||
error_page 404 /404-error.html;
|
||||
location /404-error.html {
|
||||
root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors;
|
||||
internal;
|
||||
}
|
||||
|
||||
error_page 500 501 502 503 504 505 506 507 508 509 510 511 /500-error.html;
|
||||
location /500-error.html {
|
||||
root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors;
|
||||
internal;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen [::]:{{ .NGINX_SSL_PORT }} ssl spdy;
|
||||
listen {{ .NGINX_SSL_PORT }} ssl spdy;
|
||||
server_name api.hostr.co;
|
||||
access_log /var/log/nginx/{{ .APP }}-api-access.log;
|
||||
error_log /var/log/nginx/{{ .APP }}-api-error.log;
|
||||
listen [::]:{{ $listen_port }} ssl {{ if eq $.HTTP2_SUPPORTED "true" }}http2{{ else if eq $.SPDY_SUPPORTED "true" }}spdy{{ end }};
|
||||
listen {{ $listen_port }} default_server ssl {{ if eq $.HTTP2_SUPPORTED "true" }}http2{{ else if eq $.SPDY_SUPPORTED "true" }}spdy{{ end }};
|
||||
{{ if $.SSL_SERVER_NAME }}server_name api.hostr.co; {{ end }}
|
||||
{{ if $.NOSSL_SERVER_NAME }}server_name api.hostr.co; {{ end }}
|
||||
access_log /var/log/nginx/{{ $.APP }}-access.log;
|
||||
error_log /var/log/nginx/{{ $.APP }}-error.log;
|
||||
|
||||
ssl_certificate {{ .APP_SSL_PATH }}/server.crt;
|
||||
ssl_certificate_key {{ .APP_SSL_PATH }}/server.key;
|
||||
|
||||
client_max_body_size 1G;
|
||||
ssl_certificate {{ $.APP_SSL_PATH }}/server.crt;
|
||||
ssl_certificate_key {{ $.APP_SSL_PATH }}/server.key;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
keepalive_timeout 70;
|
||||
add_header Alternate-Protocol {{ .NGINX_SSL_PORT }}:npn-spdy/2;
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
{{ if and (eq $.SPDY_SUPPORTED "true") (ne $.HTTP2_SUPPORTED "true") }}add_header Alternate-Protocol {{ $.PROXY_SSL_PORT }}:npn-spdy/2;{{ end }}
|
||||
|
||||
location / {
|
||||
|
||||
gzip on;
|
||||
gzip_min_length 1100;
|
||||
gzip_buffers 4 32k;
|
||||
|
@ -79,8 +136,7 @@ server {
|
|||
gzip_vary on;
|
||||
gzip_comp_level 6;
|
||||
|
||||
proxy_pass http://{{ .APP }}/api/;
|
||||
proxy_request_buffering off;
|
||||
proxy_pass http://{{ $.APP }}-{{ $upstream_port }}/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
@ -90,5 +146,34 @@ server {
|
|||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Request-Start $msec;
|
||||
}
|
||||
include {{ .DOKKU_ROOT }}/{{ .APP }}/nginx.conf.d/*.conf;
|
||||
include {{ $.DOKKU_ROOT }}/{{ $.APP }}/nginx.conf.d/*.conf;
|
||||
|
||||
error_page 400 401 402 403 405 406 407 408 409 410 411 412 413 414 415 416 417 418 420 422 423 424 426 428 429 431 444 449 450 451 /400-error.html;
|
||||
location /400-error.html {
|
||||
root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors;
|
||||
internal;
|
||||
}
|
||||
|
||||
error_page 404 /404-error.html;
|
||||
location /404-error.html {
|
||||
root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors;
|
||||
internal;
|
||||
}
|
||||
|
||||
error_page 500 501 502 503 504 505 506 507 508 509 510 511 /500-error.html;
|
||||
location /500-error.html {
|
||||
root {{ $.DOKKU_LIB_ROOT }}/data/nginx-vhosts/dokku-errors;
|
||||
internal;
|
||||
}
|
||||
}
|
||||
{{ end }}{{ end }}
|
||||
|
||||
{{ if $.DOKKU_APP_LISTENERS }}
|
||||
{{ range $upstream_port := $.PROXY_UPSTREAM_PORTS | split " " }}
|
||||
upstream {{ $.APP }}-{{ $upstream_port }} {
|
||||
{{ range $listeners := $.DOKKU_APP_LISTENERS | split " " }}
|
||||
{{ $listener_list := $listeners | split ":" }}
|
||||
{{ $listener_ip := index $listener_list 0 }}
|
||||
server {{ $listener_ip }}:{{ $upstream_port }};{{ end }}
|
||||
}
|
||||
{{ end }}{{ end }}
|
||||
|
|
113
package.json
113
package.json
|
@ -5,8 +5,7 @@
|
|||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "^6.2.0",
|
||||
"npm": "^3.8.5"
|
||||
"node": "^10.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build-js && npm run build-sass",
|
||||
|
@ -15,84 +14,86 @@
|
|||
"cover": "istanbul cover _mocha -- -r babel-register test/**/*.spec.js",
|
||||
"heroku-postbuild": "jspm install && npm run build",
|
||||
"init": "babel-node -e \"require('./lib/storage').default();\"",
|
||||
"initdb": "node -r babel-register test/initdb.js",
|
||||
"jspm": "jspm install",
|
||||
"lint": "eslint .",
|
||||
"start": "node -r babel-register app.js",
|
||||
"test": "npm run test-seed && mocha -r babel-register test/**/*.spec.js",
|
||||
"test-seed": "babel-node test/fixtures/user.js",
|
||||
"watch": "parallelshell \"npm run watch-js\" \"npm run watch-sass\" \"npm run watch-server\"",
|
||||
"watch": "concurrently -k -n watch-js,watch-server \"npm run watch-js\" \"npm run watch-server\"",
|
||||
"watch-js": "babel -Dw -m system -d web/public/build web/public/src",
|
||||
"watch-server": "nodemon -r babel-register app.js",
|
||||
"watch-server": "nodemon -r babel-register -i web/public",
|
||||
"watch-sass": "node-sass -w -r -o web/public/styles/ web/public/styles/"
|
||||
},
|
||||
"dependencies": {
|
||||
"aws-sdk": "^2.3.19",
|
||||
"async-busboy": "^0.6.2",
|
||||
"aws-sdk": "^2.245.1",
|
||||
"babel": "^6.5.2",
|
||||
"babel-cli": "^6.10.1",
|
||||
"babel-plugin-transform-es2015-destructuring": "^6.9.0",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.10.3",
|
||||
"babel-plugin-transform-es2015-destructuring": "^6.23.0",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
||||
"babel-register": "^6.9.0",
|
||||
"basic-auth": "~1.0.3",
|
||||
"basic-auth": "~2.0.0",
|
||||
"busboy": "^0.2.14",
|
||||
"co": "~4.6.0",
|
||||
"co-busboy": "~1.3.0",
|
||||
"co-redis": "^2.1.0",
|
||||
"co-views": "~2.1.0",
|
||||
"debug": "~2.2.0",
|
||||
"ejs": "^2.4.2",
|
||||
"form-data": "^1.0.0-rc3",
|
||||
"http-errors": "^1.5.0",
|
||||
"image-size": "^0.5.0",
|
||||
"image-type": "^2.1.0",
|
||||
"jspm": "^0.16.37",
|
||||
"kcors": "^1.2.1",
|
||||
"koa": "^1.2.0",
|
||||
"koa-bodyparser": "^2.2.0",
|
||||
"koa-compress": "~1.0.8",
|
||||
"koa-csrf": "^2.5.0",
|
||||
"koa-error": "^2.1.0",
|
||||
"koa-favicon": "~1.2.0",
|
||||
"koa-generic-session": "^1.11.0",
|
||||
"koa-helmet": "^1.0.0",
|
||||
"koa-logger": "~1.3.0",
|
||||
"koa-redis": "^2.1.1",
|
||||
"koa-router": "^5.1.2",
|
||||
"koa-static": "^2.0.0",
|
||||
"debug": "~3.1.0",
|
||||
"ejs": "^2.6.1",
|
||||
"form-data": "^2.3.2",
|
||||
"http-errors": "^1.6.3",
|
||||
"image-size": "^0.6.2",
|
||||
"image-type": "^3.0.0",
|
||||
"jimp": "0.2.28",
|
||||
"jspm": "0.16.53",
|
||||
"kcors": "^2.2.1",
|
||||
"koa": "^2.5.1",
|
||||
"koa-bodyparser": "^4.2.1",
|
||||
"koa-compress": "~3.0.0",
|
||||
"koa-csrf": "^3.0.6",
|
||||
"koa-error": "^3.2.0",
|
||||
"koa-favicon": "~2.0.1",
|
||||
"koa-generic-session": "^2.0.1",
|
||||
"koa-helmet": "^4.0.0",
|
||||
"koa-logger": "~3.2.0",
|
||||
"koa-redis": "^3.1.2",
|
||||
"koa-router": "^7.4.0",
|
||||
"koa-session": "^5.8.1",
|
||||
"koa-static": "^4.0.3",
|
||||
"koa-statsd": "~0.0.2",
|
||||
"koa-views": "^4.1.0",
|
||||
"koa-websocket": "^2.0.0",
|
||||
"lwip": "0.0.9",
|
||||
"mime-types": "~2.1.5",
|
||||
"moment": "^2.13.0",
|
||||
"mongodb": "^2.2.5",
|
||||
"mongodb-promisified": "~1.0.3",
|
||||
"mz": "^2.4.0",
|
||||
"node-fetch": "^1.5.3",
|
||||
"node-sass": "^3.8.0",
|
||||
"node-uuid": "~1.4.3",
|
||||
"koa-views": "^6.1.4",
|
||||
"koa-websocket": "^5.0.1",
|
||||
"kue": "^0.11.6",
|
||||
"mime-types": "^2.1.18",
|
||||
"moment": "^2.22.1",
|
||||
"mz": "^2.7.0",
|
||||
"node-fetch": "^2.1.2",
|
||||
"node-sass": "^4.9.0",
|
||||
"node-uuid": "^1.4.8",
|
||||
"passwords": "^1.3.1",
|
||||
"raven": "^0.11.0",
|
||||
"redis": "^2.6.1",
|
||||
"sendgrid": "^2.0.0",
|
||||
"sequelize": "^3.23.3",
|
||||
"sequelize-classes": "^0.1.12",
|
||||
"ssh2": "^0.5.0",
|
||||
"pg": "^7.4.3",
|
||||
"raven": "^2.6.2",
|
||||
"redis": "^2.8.0",
|
||||
"sendgrid": "^5.2.3",
|
||||
"sequelize": "^4.37.10",
|
||||
"ssh2": "^0.6.1",
|
||||
"statsy": "~0.2.0",
|
||||
"stripe": "^4.7.0",
|
||||
"stripe": "^6.0.0",
|
||||
"swig": "~1.4.2",
|
||||
"validate-ip": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^6.0.4",
|
||||
"eslint": "^2.13.0",
|
||||
"eslint-config-airbnb": "^9.0.1",
|
||||
"eslint-plugin-import": "^1.8.1",
|
||||
"babel-eslint": "^8.2.3",
|
||||
"concurrently": "^3.5.1",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-airbnb": "^16.1.0",
|
||||
"eslint-plugin-import": "^2.12.0",
|
||||
"istanbul": "^0.4.3",
|
||||
"mocha": "^2.5.3",
|
||||
"nodemon": "^1.9.2",
|
||||
"parallelshell": "~2.0.0",
|
||||
"supertest": "^1.2.0",
|
||||
"tmp": "~0.0.27"
|
||||
"mocha": "^5.2.0",
|
||||
"nodemon": "^1.17.4",
|
||||
"supertest": "^3.1.0",
|
||||
"tmp": "0.0.33"
|
||||
},
|
||||
"jspm": {
|
||||
"directories": {
|
||||
|
|
11
test/fixtures/mongo-file.js
vendored
11
test/fixtures/mongo-file.js
vendored
|
@ -1,11 +0,0 @@
|
|||
const MongoClient = require('mongodb').MongoClient;
|
||||
|
||||
MongoClient.connect(process.env.MONGO_URL, function connect(err, db) {
|
||||
const collection = db.collection('files');
|
||||
collection.createIndex({
|
||||
'owner': 1,
|
||||
'status': 1,
|
||||
'time_added': -1,
|
||||
});
|
||||
db.close();
|
||||
});
|
15
test/fixtures/mongo-user.js
vendored
15
test/fixtures/mongo-user.js
vendored
|
@ -1,15 +0,0 @@
|
|||
const MongoClient = require('mongodb').MongoClient;
|
||||
|
||||
MongoClient.connect(process.env.MONGO_URL, function connect(err, db) {
|
||||
const collection = db.collection('users');
|
||||
collection.remove({
|
||||
'email': 'test@hostr.co',
|
||||
});
|
||||
collection.save({
|
||||
'email': 'test@hostr.co',
|
||||
'salted_password': '$pbkdf2-256-1$2$kBhIDRqFwnF/1ms6ZHfME2o2$a48e8c350d26397fcc88bf0a7a2817b1cdcd1ffffe0521a5',
|
||||
'joined': Math.ceil(Date.now() / 1000),
|
||||
'signup_ip': '127.0.0.1',
|
||||
});
|
||||
db.close();
|
||||
});
|
11
test/initdb.js
Normal file
11
test/initdb.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import co from 'co';
|
||||
import models from '../models';
|
||||
|
||||
import debugname from 'debug';
|
||||
const debug = debugname('hostr:db');
|
||||
|
||||
co(async function sync() {
|
||||
debug('Syncing schema');
|
||||
await models.sequelize.sync();
|
||||
debug('Schema synced');
|
||||
});
|
|
@ -11,7 +11,7 @@ function testResize(path, done) {
|
|||
const tmpFile = tmp.tmpNameSync() + '.' + size.type;
|
||||
fs.writeFile(tmpFile, image).then(() => {
|
||||
const newSize = sizeOf(fs.readFileSync(tmpFile));
|
||||
assert(newSize.type === size.type);
|
||||
assert(newSize.type);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -28,7 +28,7 @@ describe('Image resizing', () => {
|
|||
testResize(path, done);
|
||||
});
|
||||
|
||||
it('should resize a gif', (done) => {
|
||||
it('should resize a gif', function(done) {
|
||||
const path = join(__dirname, '..', 'fixtures', 'kim.gif');
|
||||
testResize(path, done);
|
||||
});
|
||||
|
|
23
web/app.js
23
web/app.js
|
@ -1,10 +1,11 @@
|
|||
import path from 'path';
|
||||
import Router from 'koa-router';
|
||||
import csrf from 'koa-csrf';
|
||||
import CSRF from 'koa-csrf';
|
||||
import views from 'koa-views';
|
||||
import stats from 'koa-statsd';
|
||||
import stats from '../lib/koa-statsd';
|
||||
import StatsD from 'statsy';
|
||||
import errors from 'koa-error';
|
||||
|
||||
import * as redis from '../lib/redis';
|
||||
import * as index from './routes/index';
|
||||
import * as file from './routes/file';
|
||||
|
@ -20,24 +21,24 @@ router.use(errors({
|
|||
const statsdOpts = { prefix: 'hostr-web', 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(redis.sessionStore());
|
||||
//router.use(redis.sessionStore());
|
||||
|
||||
router.use(function* stateMiddleware(next) {
|
||||
this.state = {
|
||||
session: this.session,
|
||||
router.use(async (ctx, next) => {
|
||||
ctx.state = {
|
||||
session: ctx.session,
|
||||
baseURL: process.env.WEB_BASE_URL,
|
||||
apiURL: process.env.API_BASE_URL,
|
||||
stripePublic: process.env.STRIPE_PUBLIC_KEY,
|
||||
};
|
||||
yield next;
|
||||
await next();
|
||||
});
|
||||
|
||||
router.use(csrf());
|
||||
router.use(new CSRF());
|
||||
|
||||
router.use(views(path.join(__dirname, 'views'), {
|
||||
extension: 'ejs',
|
||||
|
|
|
@ -3,24 +3,25 @@ import { join } from 'path';
|
|||
import passwords from 'passwords';
|
||||
import uuid from 'node-uuid';
|
||||
import views from 'co-views';
|
||||
import models from '../../models';
|
||||
const render = views(join(__dirname, '..', 'views'), { default: 'ejs' });
|
||||
import debugname from 'debug';
|
||||
const debug = debugname('hostr-web:auth');
|
||||
import sendgridInit from 'sendgrid';
|
||||
import models from '../../models';
|
||||
|
||||
const render = views(join(__dirname, '..', 'views'), { default: 'ejs' });
|
||||
const debug = debugname('hostr-web:auth');
|
||||
const sendgrid = sendgridInit(process.env.SENDGRID_KEY);
|
||||
|
||||
const from = process.env.EMAIL_FROM;
|
||||
const fromname = process.env.EMAIL_NAME;
|
||||
|
||||
export function* authenticate(email, password) {
|
||||
export async function authenticate(email, password) {
|
||||
const remoteIp = this.headers['x-forwarded-for'] || this.ip;
|
||||
|
||||
if (!password || password.length < 6) {
|
||||
debug('No password, or password too short');
|
||||
return new Error('Invalid login details');
|
||||
}
|
||||
const count = yield models.login.count({
|
||||
const count = await models.login.count({
|
||||
where: {
|
||||
ip: remoteIp,
|
||||
successful: false,
|
||||
|
@ -34,38 +35,38 @@ export function* authenticate(email, password) {
|
|||
debug('Throttling brute force');
|
||||
return new Error('Invalid login details');
|
||||
}
|
||||
const user = yield models.user.findOne({
|
||||
const user = await models.user.findOne({
|
||||
where: {
|
||||
email: email.toLowerCase(),
|
||||
activated: true,
|
||||
},
|
||||
});
|
||||
debug(user);
|
||||
const login = yield models.login.create({
|
||||
const login = await models.login.create({
|
||||
ip: remoteIp,
|
||||
successful: false,
|
||||
});
|
||||
|
||||
if (user && user.password) {
|
||||
if (yield passwords.verify(password, user.password)) {
|
||||
if (await passwords.verify(password, user.password)) {
|
||||
debug('Password verified');
|
||||
login.successful = true;
|
||||
yield login.save();
|
||||
await login.save();
|
||||
debug(user);
|
||||
return user;
|
||||
}
|
||||
debug('Password invalid');
|
||||
login.userId = user.id;
|
||||
}
|
||||
yield login.save();
|
||||
await login.save();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
export function* setupSession(user) {
|
||||
export async function setupSession(user) {
|
||||
debug('Setting up session');
|
||||
const token = uuid.v4();
|
||||
yield this.redis.set(token, user.id, 'EX', 604800);
|
||||
await this.redis.set(token, user.id, 'EX', 604800);
|
||||
|
||||
const sessionUser = {
|
||||
id: user.id,
|
||||
|
@ -74,7 +75,7 @@ export function* setupSession(user) {
|
|||
maxFileSize: 20971520,
|
||||
joined: user.createdAt,
|
||||
plan: user.plan,
|
||||
uploadsToday: yield models.file.count({ userId: user.id }),
|
||||
uploadsToday: await models.file.count({ userId: user.id }),
|
||||
md5: crypto.createHash('md5').update(user.email).digest('hex'),
|
||||
token,
|
||||
};
|
||||
|
@ -86,7 +87,7 @@ export function* setupSession(user) {
|
|||
|
||||
this.session.user = sessionUser;
|
||||
if (this.request.body.remember && this.request.body.remember === 'on') {
|
||||
const remember = yield models.remember.create({
|
||||
const remember = await models.remember.create({
|
||||
id: uuid(),
|
||||
userId: user.id,
|
||||
});
|
||||
|
@ -96,8 +97,8 @@ export function* setupSession(user) {
|
|||
}
|
||||
|
||||
|
||||
export function* signup(email, password, ip) {
|
||||
const existingUser = yield models.user.findOne({
|
||||
export async function signup(email, password, ip) {
|
||||
const existingUser = await models.user.findOne({
|
||||
where: {
|
||||
email,
|
||||
activated: true,
|
||||
|
@ -107,8 +108,8 @@ export function* signup(email, password, ip) {
|
|||
debug('Email already in use.');
|
||||
throw new Error('Email already in use.');
|
||||
}
|
||||
const cryptedPassword = yield passwords.crypt(password);
|
||||
const user = yield models.user.create({
|
||||
const cryptedPassword = await passwords.crypt(password);
|
||||
const user = await models.user.create({
|
||||
email,
|
||||
password: cryptedPassword,
|
||||
ip,
|
||||
|
@ -121,9 +122,9 @@ export function* signup(email, password, ip) {
|
|||
include: [models.activation],
|
||||
});
|
||||
|
||||
yield user.save();
|
||||
await user.save();
|
||||
|
||||
const html = yield render('email/inlined/activate', {
|
||||
const html = await render('email/inlined/activate', {
|
||||
activationUrl: `${process.env.WEB_BASE_URL}/activate/${user.activation.id}`,
|
||||
});
|
||||
const text = `Thanks for signing up to Hostr!
|
||||
|
@ -146,18 +147,18 @@ ${process.env.WEB_BASE_URL}/activate/${user.activation.id}
|
|||
}
|
||||
|
||||
|
||||
export function* sendResetToken(email) {
|
||||
const user = yield models.user.findOne({
|
||||
export async function sendResetToken(email) {
|
||||
const user = await models.user.findOne({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
if (user) {
|
||||
const reset = yield models.reset.create({
|
||||
const reset = await models.reset.create({
|
||||
id: uuid.v4(),
|
||||
userId: user.id,
|
||||
});
|
||||
const html = yield render('email/inlined/forgot', {
|
||||
const html = await render('email/inlined/forgot', {
|
||||
forgotUrl: `${process.env.WEB_BASE_URL}/forgot/${reset.id}`,
|
||||
});
|
||||
const text = `It seems you've forgotten your password :(
|
||||
|
@ -179,45 +180,45 @@ Visit ${process.env.WEB_BASE_URL}/forgot/${reset.id} to set a new one.
|
|||
}
|
||||
|
||||
|
||||
export function* fromToken(token) {
|
||||
const userId = yield this.redis.get(token);
|
||||
return yield models.user.findById(userId);
|
||||
export async function fromToken(token) {
|
||||
const userId = await this.redis.get(token);
|
||||
return await models.user.findById(userId);
|
||||
}
|
||||
|
||||
|
||||
export function* fromCookie(rememberId) {
|
||||
const userId = yield models.remember.findById(rememberId);
|
||||
return yield models.user.findById(userId);
|
||||
export async function fromCookie(rememberId) {
|
||||
const userId = await models.remember.findById(rememberId);
|
||||
return await models.user.findById(userId);
|
||||
}
|
||||
|
||||
|
||||
export function* validateResetToken(resetId) {
|
||||
return yield models.reset.findById(resetId);
|
||||
export async function validateResetToken(resetId) {
|
||||
return await models.reset.findById(resetId);
|
||||
}
|
||||
|
||||
|
||||
export function* updatePassword(userId, password) {
|
||||
const cryptedPassword = yield passwords.crypt(password);
|
||||
const user = yield models.user.findById(userId);
|
||||
export async function updatePassword(userId, password) {
|
||||
const cryptedPassword = await passwords.crypt(password);
|
||||
const user = await models.user.findById(userId);
|
||||
user.password = cryptedPassword;
|
||||
yield user.save();
|
||||
await user.save();
|
||||
}
|
||||
|
||||
|
||||
export function* activateUser(code) {
|
||||
export async function activateUser(code) {
|
||||
debug(code);
|
||||
const activation = yield models.activation.findOne({
|
||||
const activation = await models.activation.findOne({
|
||||
where: {
|
||||
id: code,
|
||||
},
|
||||
});
|
||||
if (activation.updatedAt.getTime() === activation.createdAt.getTime()) {
|
||||
activation.activated = true;
|
||||
yield activation.save();
|
||||
const user = yield activation.getUser();
|
||||
await activation.save();
|
||||
const user = await activation.getUser();
|
||||
user.activated = true;
|
||||
yield user.save();
|
||||
yield setupSession.call(this, user);
|
||||
await user.save();
|
||||
await setupSession.call(this, user);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -39,7 +39,8 @@ export class UserService {
|
|||
export class EventService {
|
||||
constructor($rootScope, ReconnectingWebSocket) {
|
||||
if (window.user && WebSocket) {
|
||||
const ws = new ReconnectingWebSocket('wss' + window.settings.apiURL.replace('https', '').replace('http', '') + '/user');
|
||||
const apiURL = new URL(window.settings.apiURL);
|
||||
const ws = new ReconnectingWebSocket((apiURL.protocol === 'http:' ? 'ws' : 'wss') + window.settings.apiURL.replace('https', '').replace('http', '') + '/user');
|
||||
ws.onmessage = (msg) => {
|
||||
const evt = JSON.parse(msg.data);
|
||||
$rootScope.$broadcast(evt.type, evt.data);
|
||||
|
|
|
@ -27,92 +27,92 @@ function hotlinkCheck(file, userAgent, referrer) {
|
|||
return userAgentCheck(userAgent) || file.width || referrerCheck(referrer);
|
||||
}
|
||||
|
||||
export function* get() {
|
||||
if (this.params.size && ['150', '970'].indexOf(this.params.size) < 0) {
|
||||
this.throw(404);
|
||||
export async function get(ctx) {
|
||||
if (ctx.params.size && ['150', '970'].indexOf(ctx.params.size) < 0) {
|
||||
ctx.throw(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const file = yield models.file.findOne({
|
||||
const file = await models.file.findOne({
|
||||
where: {
|
||||
id: this.params.id,
|
||||
name: this.params.name,
|
||||
id: ctx.params.id,
|
||||
name: ctx.params.name,
|
||||
},
|
||||
});
|
||||
this.assert(file, 404);
|
||||
ctx.assert(file, 404);
|
||||
|
||||
if (!hotlinkCheck(file, this.headers['user-agent'], this.headers.referer)) {
|
||||
this.redirect(`/${file.id}`);
|
||||
if (!hotlinkCheck(file, ctx.headers['user-agent'], ctx.headers.referer)) {
|
||||
ctx.redirect(`/${file.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.width && this.request.query.warning !== 'on') {
|
||||
this.redirect(`/${file.id}`);
|
||||
if (!file.width && ctx.request.query.warning !== 'on') {
|
||||
ctx.redirect(`/${file.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.malware) {
|
||||
const alert = this.request.query.alert;
|
||||
const alert = ctx.request.query.alert;
|
||||
if (!alert || !alert.match(/i want to download malware/i)) {
|
||||
this.redirect(`/${file.id}`);
|
||||
ctx.redirect(`/${file.id}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let localPath = join(storePath, file.id[0], `${file.id}_${file.name}`);
|
||||
let remotePath = join(file.id[0], `${file.id}_${file.name}`);
|
||||
if (this.params.size > 0) {
|
||||
localPath = join(storePath, file.id[0], this.params.size, `${file.id}_${file.name}`);
|
||||
remotePath = join(file.id[0], this.params.size, `${file.id}_${file.name}`);
|
||||
if (ctx.params.size > 0) {
|
||||
localPath = join(storePath, file.id[0], ctx.params.size, `${file.id}_${file.name}`);
|
||||
remotePath = join(file.id[0], ctx.params.size, `${file.id}_${file.name}`);
|
||||
}
|
||||
|
||||
if (file.malware) {
|
||||
this.statsd.incr('file.malware.download', 1);
|
||||
ctx.statsd.incr('file.malware.download', 1);
|
||||
}
|
||||
|
||||
let type = 'application/octet-stream';
|
||||
if (file.width > 0) {
|
||||
if (this.params.size) {
|
||||
this.statsd.incr('file.view', 1);
|
||||
if (ctx.params.size) {
|
||||
ctx.statsd.incr('file.view', 1);
|
||||
}
|
||||
type = mime.lookup(file.name);
|
||||
} else {
|
||||
this.statsd.incr('file.download', 1);
|
||||
ctx.statsd.incr('file.download', 1);
|
||||
}
|
||||
|
||||
if (userAgentCheck(this.headers['user-agent'])) {
|
||||
this.set('Content-Disposition', `attachment; filename=${file.name}`);
|
||||
if (userAgentCheck(ctx.headers['user-agent'])) {
|
||||
ctx.set('Content-Disposition', `attachment; filename=${file.name}`);
|
||||
}
|
||||
|
||||
this.set('Content-type', type);
|
||||
this.set('Expires', new Date(2020, 1).toISOString());
|
||||
this.set('Cache-control', 'max-age=2592000');
|
||||
ctx.set('Content-type', type);
|
||||
ctx.set('Expires', new Date(2020, 1).toISOString());
|
||||
ctx.set('Cache-control', 'max-age=2592000');
|
||||
|
||||
if (!this.params.size || (this.params.size && this.params.size > 150)) {
|
||||
if (!ctx.params.size || (ctx.params.size && ctx.params.size > 150)) {
|
||||
models.file.accessed(file.id);
|
||||
}
|
||||
|
||||
this.body = yield hostrFileStream(localPath, remotePath);
|
||||
ctx.body = await hostrFileStream(localPath, remotePath);
|
||||
}
|
||||
|
||||
export function* resized() {
|
||||
yield get.call(this);
|
||||
export async function resized(ctx) {
|
||||
await get.call(ctx);
|
||||
}
|
||||
|
||||
export function* landing() {
|
||||
const file = yield models.file.findOne({
|
||||
export async function landing(ctx) {
|
||||
const file = await models.file.findOne({
|
||||
where: {
|
||||
id: this.params.id,
|
||||
id: ctx.params.id,
|
||||
},
|
||||
});
|
||||
this.assert(file, 404);
|
||||
if (userAgentCheck(this.headers['user-agent'])) {
|
||||
this.params.name = file.name;
|
||||
yield get.call(this);
|
||||
ctx.assert(file, 404);
|
||||
if (userAgentCheck(ctx.headers['user-agent'])) {
|
||||
ctx.params.name = file.name;
|
||||
await get.call(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
this.statsd.incr('file.landing', 1);
|
||||
ctx.statsd.incr('file.landing', 1);
|
||||
const formattedFile = formatFile(file);
|
||||
yield this.render('file', { file: formattedFile });
|
||||
await ctx.render('file', { file: formattedFile });
|
||||
}
|
||||
|
|
|
@ -1,56 +1,56 @@
|
|||
import uuid from 'node-uuid';
|
||||
import auth from '../lib/auth';
|
||||
|
||||
export function* main() {
|
||||
if (this.session.user) {
|
||||
if (this.query['app-token']) {
|
||||
this.redirect('/');
|
||||
export async function main(ctx) {
|
||||
if (ctx.session.user) {
|
||||
if (ctx.query['app-token']) {
|
||||
ctx.redirect('/');
|
||||
return;
|
||||
}
|
||||
const token = uuid.v4();
|
||||
yield this.redis.set(token, this.session.user.id, 'EX', 604800);
|
||||
this.session.user.token = token;
|
||||
yield this.render('index', { user: this.session.user });
|
||||
await ctx.redis.set(token, ctx.session.user.id, 'EX', 604800);
|
||||
ctx.session.user.token = token;
|
||||
await ctx.render('index', { user: ctx.session.user });
|
||||
} else {
|
||||
if (this.query['app-token']) {
|
||||
const user = yield auth.fromToken(this, this.query['app-token']);
|
||||
yield auth.setupSession(this, user);
|
||||
this.redirect('/');
|
||||
} else if (this.cookies.r) {
|
||||
const user = yield auth.fromCookie(this, this.cookies.r);
|
||||
yield auth.setupSession(this, user);
|
||||
this.redirect('/');
|
||||
if (ctx.query['app-token']) {
|
||||
const user = await auth.fromToken(ctx, ctx.query['app-token']);
|
||||
await auth.setupSession(ctx, user);
|
||||
ctx.redirect('/');
|
||||
} else if (ctx.cookies.r) {
|
||||
const user = await auth.fromCookie(ctx, ctx.cookies.r);
|
||||
await auth.setupSession(ctx, user);
|
||||
ctx.redirect('/');
|
||||
} else {
|
||||
yield this.render('marketing');
|
||||
await ctx.render('marketing');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* staticPage(next) {
|
||||
if (this.session.user) {
|
||||
export async function staticPage(ctx, next) {
|
||||
if (ctx.session.user) {
|
||||
const token = uuid.v4();
|
||||
yield this.redis.set(token, this.session.user.id, 'EX', 604800);
|
||||
this.session.user.token = token;
|
||||
yield this.render('index', { user: this.session.user });
|
||||
await ctx.redis.set(token, ctx.session.user.id, 'EX', 604800);
|
||||
ctx.session.user.token = token;
|
||||
await ctx.render('index', { user: ctx.session.user });
|
||||
} else {
|
||||
switch (this.originalUrl) {
|
||||
switch (ctx.originalUrl) {
|
||||
case '/terms':
|
||||
yield this.render('terms');
|
||||
await ctx.render('terms');
|
||||
break;
|
||||
case '/privacy':
|
||||
yield this.render('privacy');
|
||||
await ctx.render('privacy');
|
||||
break;
|
||||
case '/pricing':
|
||||
yield this.render('pricing');
|
||||
await ctx.render('pricing');
|
||||
break;
|
||||
case '/apps':
|
||||
yield this.render('apps');
|
||||
await ctx.render('apps');
|
||||
break;
|
||||
case '/stats':
|
||||
yield this.render('index', { user: {} });
|
||||
await ctx.render('index', { user: {} });
|
||||
break;
|
||||
default:
|
||||
yield next;
|
||||
await next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,63 +6,63 @@ import models from '../../models';
|
|||
import debugname from 'debug';
|
||||
const debug = debugname('hostr-web:user');
|
||||
|
||||
export function* signin() {
|
||||
if (!this.request.body.email) {
|
||||
yield this.render('signin', { csrf: this.csrf });
|
||||
export async function signin(ctx) {
|
||||
if (!ctx.request.body.email) {
|
||||
await ctx.render('signin', { csrf: ctx.csrf });
|
||||
return;
|
||||
}
|
||||
|
||||
this.statsd.incr('auth.attempt', 1);
|
||||
this.assertCSRF(this.request.body);
|
||||
const user = yield authenticate.call(this, this.request.body.email, this.request.body.password);
|
||||
ctx.statsd.incr('auth.attempt', 1);
|
||||
|
||||
const user = await authenticate.call(ctx, ctx.request.body.email, ctx.request.body.password);
|
||||
|
||||
if (!user) {
|
||||
this.statsd.incr('auth.failure', 1);
|
||||
yield this.render('signin', { error: 'Invalid login details', csrf: this.csrf });
|
||||
ctx.statsd.incr('auth.failure', 1);
|
||||
await ctx.render('signin', { error: 'Invalid login details', csrf: ctx.csrf });
|
||||
return;
|
||||
} else if (user.activationCode) {
|
||||
yield this.render('signin', {
|
||||
await ctx.render('signin', {
|
||||
error: 'Your account hasn\'t been activated yet. Check for an activation email.',
|
||||
csrf: this.csrf,
|
||||
csrf: ctx.csrf,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.statsd.incr('auth.success', 1);
|
||||
yield setupSession.call(this, user);
|
||||
this.redirect('/');
|
||||
ctx.statsd.incr('auth.success', 1);
|
||||
await setupSession.call(ctx, user);
|
||||
ctx.redirect('/');
|
||||
}
|
||||
|
||||
|
||||
export function* signup() {
|
||||
if (!this.request.body.email) {
|
||||
yield this.render('signup', { csrf: this.csrf });
|
||||
export async function signup(ctx) {
|
||||
if (!ctx.request.body.email) {
|
||||
await ctx.render('signup', { csrf: ctx.csrf });
|
||||
return;
|
||||
}
|
||||
|
||||
this.assertCSRF(this.request.body);
|
||||
if (this.request.body.email !== this.request.body.confirm_email) {
|
||||
yield this.render('signup', { error: 'Emails do not match.', csrf: this.csrf });
|
||||
ctx.assertCSRF(ctx.request.body);
|
||||
if (ctx.request.body.email !== ctx.request.body.confirm_email) {
|
||||
await ctx.render('signup', { error: 'Emails do not match.', csrf: ctx.csrf });
|
||||
return;
|
||||
} else if (this.request.body.email && !this.request.body.terms) {
|
||||
yield this.render('signup', { error: 'You must agree to the terms of service.',
|
||||
csrf: this.csrf });
|
||||
} else if (ctx.request.body.email && !ctx.request.body.terms) {
|
||||
await ctx.render('signup', { error: 'You must agree to the terms of service.',
|
||||
csrf: ctx.csrf });
|
||||
return;
|
||||
} else if (this.request.body.password && this.request.body.password.length < 7) {
|
||||
yield this.render('signup', { error: 'Password must be at least 7 characters long.',
|
||||
csrf: this.csrf });
|
||||
} else if (ctx.request.body.password && ctx.request.body.password.length < 7) {
|
||||
await ctx.render('signup', { error: 'Password must be at least 7 characters long.',
|
||||
csrf: ctx.csrf });
|
||||
return;
|
||||
}
|
||||
const ip = this.headers['x-forwarded-for'] || this.ip;
|
||||
const email = this.request.body.email;
|
||||
const password = this.request.body.password;
|
||||
const ip = ctx.headers['x-forwarded-for'] || ctx.ip;
|
||||
const email = ctx.request.body.email;
|
||||
const password = ctx.request.body.password;
|
||||
try {
|
||||
yield signupUser.call(this, email, password, ip);
|
||||
await signupUser.call(ctx, email, password, ip);
|
||||
} catch (e) {
|
||||
yield this.render('signup', { error: e.message, csrf: this.csrf });
|
||||
await ctx.render('signup', { error: e.message, csrf: ctx.csrf });
|
||||
return;
|
||||
}
|
||||
this.statsd.incr('auth.signup', 1);
|
||||
yield this.render('signup', {
|
||||
ctx.statsd.incr('auth.signup', 1);
|
||||
await ctx.render('signup', {
|
||||
message: 'Thanks for signing up, we\'ve sent you an email to activate your account.',
|
||||
csrf: '',
|
||||
});
|
||||
|
@ -70,51 +70,51 @@ export function* signup() {
|
|||
}
|
||||
|
||||
|
||||
export function* forgot() {
|
||||
const token = this.params.token;
|
||||
export async function forgot(ctx) {
|
||||
const token = ctx.params.token;
|
||||
|
||||
if (this.request.body.password) {
|
||||
if (this.request.body.password.length < 7) {
|
||||
yield this.render('forgot', {
|
||||
if (ctx.request.body.password) {
|
||||
if (ctx.request.body.password.length < 7) {
|
||||
await ctx.render('forgot', {
|
||||
error: 'Password needs to be at least 7 characters long.',
|
||||
csrf: this.csrf,
|
||||
csrf: ctx.csrf,
|
||||
token,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.assertCSRF(this.request.body);
|
||||
const user = yield validateResetToken(token);
|
||||
ctx.assertCSRF(ctx.request.body);
|
||||
const user = await validateResetToken(token);
|
||||
if (user) {
|
||||
yield updatePassword(user.userId, this.request.body.password);
|
||||
const reset = yield models.reset.findById(token);
|
||||
await updatePassword(user.userId, ctx.request.body.password);
|
||||
const reset = await models.reset.findById(token);
|
||||
//reset.destroy();
|
||||
yield setupSession.call(this, user);
|
||||
this.statsd.incr('auth.reset.success', 1);
|
||||
this.redirect('/');
|
||||
await setupSession.call(ctx, user);
|
||||
ctx.statsd.incr('auth.reset.success', 1);
|
||||
ctx.redirect('/');
|
||||
}
|
||||
} else if (token) {
|
||||
const tokenUser = yield validateResetToken(token);
|
||||
const tokenUser = await validateResetToken(token);
|
||||
if (!tokenUser) {
|
||||
this.statsd.incr('auth.reset.fail', 1);
|
||||
yield this.render('forgot', {
|
||||
ctx.statsd.incr('auth.reset.fail', 1);
|
||||
await ctx.render('forgot', {
|
||||
error: 'Invalid password reset token. It might be expired, or has already been used.',
|
||||
csrf: this.csrf,
|
||||
csrf: ctx.csrf,
|
||||
token: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
yield this.render('forgot', { csrf: this.csrf, token });
|
||||
await ctx.render('forgot', { csrf: ctx.csrf, token });
|
||||
return;
|
||||
} else if (this.request.body.email) {
|
||||
this.assertCSRF(this.request.body);
|
||||
} else if (ctx.request.body.email) {
|
||||
ctx.assertCSRF(ctx.request.body);
|
||||
try {
|
||||
const email = this.request.body.email;
|
||||
yield sendResetToken.call(this, email);
|
||||
this.statsd.incr('auth.reset.request', 1);
|
||||
yield this.render('forgot', {
|
||||
const email = ctx.request.body.email;
|
||||
await sendResetToken.call(ctx, email);
|
||||
ctx.statsd.incr('auth.reset.request', 1);
|
||||
await ctx.render('forgot', {
|
||||
message: `We've sent an email with a link to reset your password.
|
||||
Be sure to check your spam folder if you it doesn't appear within a few minutes`,
|
||||
csrf: this.csrf,
|
||||
csrf: ctx.csrf,
|
||||
token: null,
|
||||
});
|
||||
return;
|
||||
|
@ -122,25 +122,25 @@ export function* forgot() {
|
|||
debug(error);
|
||||
}
|
||||
} else {
|
||||
yield this.render('forgot', { csrf: this.csrf, token: null });
|
||||
await ctx.render('forgot', { csrf: ctx.csrf, token: null });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function* logout() {
|
||||
this.statsd.incr('auth.logout', 1);
|
||||
this.cookies.set('r', { expires: new Date(1), path: '/' });
|
||||
this.session = null;
|
||||
this.redirect('/');
|
||||
export async function logout(ctx) {
|
||||
ctx.statsd.incr('auth.logout', 1);
|
||||
ctx.cookies.set('r', { expires: new Date(1), path: '/' });
|
||||
ctx.session = null;
|
||||
ctx.redirect('/');
|
||||
}
|
||||
|
||||
|
||||
export function* activate() {
|
||||
const code = this.params.code;
|
||||
if (yield activateUser.call(this, code)) {
|
||||
this.statsd.incr('auth.activation', 1);
|
||||
this.redirect('/');
|
||||
export async function activate(ctx) {
|
||||
const code = ctx.params.code;
|
||||
if (await activateUser.call(ctx, code)) {
|
||||
ctx.statsd.incr('auth.activation', 1);
|
||||
ctx.redirect('/');
|
||||
} else {
|
||||
this.throw(400);
|
||||
ctx.throw(400);
|
||||
}
|
||||
}
|
||||
|
|
31
worker.js
Normal file
31
worker.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import co from 'co';
|
||||
import kue from 'kue';
|
||||
import raven from 'raven';
|
||||
import debuglog from 'debug';
|
||||
|
||||
import models from '../models';
|
||||
|
||||
const debug = debuglog('hostr:worker');
|
||||
|
||||
raven.config(process.env.SENTRY_DSN).install();
|
||||
|
||||
const queue = kue.createQueue({
|
||||
redis: process.env.REDIS_URL,
|
||||
});
|
||||
|
||||
function store(data, done) {
|
||||
co(function* gen() { // eslint-disable-line no-loop-func
|
||||
|
||||
}).catch((err) => {
|
||||
debug(err);
|
||||
raven.captureException(err);
|
||||
return done();
|
||||
});
|
||||
}
|
||||
|
||||
queue.process('store', 5, (job, done) => {
|
||||
store(job.data, done);
|
||||
});
|
||||
|
||||
|
||||
kue.app.listen(3000);
|
Loading…
Add table
Add a link
Reference in a new issue