Merge branch 'postgres'
This commit is contained in:
commit
305dd77f43
36 changed files with 1195 additions and 312 deletions
|
@ -1,8 +1,13 @@
|
||||||
image: node:6
|
image: node:6
|
||||||
services:
|
services:
|
||||||
- mongo
|
- postgres
|
||||||
- redis
|
- redis
|
||||||
|
|
||||||
|
variables:
|
||||||
|
POSTGRES_DB: hostr_test
|
||||||
|
POSTGRES_USER: hostr_test
|
||||||
|
POSTGRES_PASSWORD: ""
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
|
|
||||||
|
@ -16,7 +21,7 @@ test:
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- docker
|
||||||
variables:
|
variables:
|
||||||
MONGO_URL: "mongodb://mongo:27017/hostr"
|
DATABASE_URL: "postgres://hostr_test@postgres:5432/hostr_test"
|
||||||
REDIS_URL: "redis://redis:6379"
|
REDIS_URL: "redis://redis:6379"
|
||||||
DEBUG: "hostr*"
|
DEBUG: "hostr*"
|
||||||
|
|
||||||
|
@ -25,3 +30,4 @@ cache:
|
||||||
untracked: true
|
untracked: true
|
||||||
paths:
|
paths:
|
||||||
- node_modules
|
- node_modules
|
||||||
|
- web/public/jspm_packages
|
||||||
|
|
|
@ -5,6 +5,7 @@ import StatsD from 'statsy';
|
||||||
import auth from './lib/auth';
|
import auth from './lib/auth';
|
||||||
import * as user from './routes/user';
|
import * as user from './routes/user';
|
||||||
import * as file from './routes/file';
|
import * as file from './routes/file';
|
||||||
|
import * as pro from './routes/pro';
|
||||||
import debugname from 'debug';
|
import debugname from 'debug';
|
||||||
const debug = debugname('hostr-api');
|
const debug = debugname('hostr-api');
|
||||||
|
|
||||||
|
@ -64,10 +65,11 @@ router.get('/user/token', auth, user.token);
|
||||||
router.get('/token', auth, user.token);
|
router.get('/token', auth, user.token);
|
||||||
router.get('/user/transaction', auth, user.transaction);
|
router.get('/user/transaction', auth, user.transaction);
|
||||||
router.post('/user/settings', auth, user.settings);
|
router.post('/user/settings', auth, user.settings);
|
||||||
|
router.post('/user/pro', auth, pro.create);
|
||||||
|
router.delete('/user/pro', auth, pro.cancel);
|
||||||
router.get('/file', auth, file.list);
|
router.get('/file', auth, file.list);
|
||||||
router.post('/file', auth, file.post);
|
router.post('/file', auth, file.post);
|
||||||
router.get('/file/:id', file.get);
|
router.get('/file/:id', file.get);
|
||||||
router.put('/file/:id', auth, file.put);
|
|
||||||
router.delete('/file/:id', auth, file.del);
|
router.delete('/file/:id', auth, file.del);
|
||||||
router.delete('/file/:id', auth, file.del);
|
router.delete('/file/:id', auth, file.del);
|
||||||
|
|
||||||
|
|
|
@ -1,63 +1,88 @@
|
||||||
import passwords from 'passwords';
|
import passwords from 'passwords';
|
||||||
import auth from 'basic-auth';
|
import auth from 'basic-auth';
|
||||||
|
import models from '../../models';
|
||||||
import debugname from 'debug';
|
import debugname from 'debug';
|
||||||
const debug = debugname('hostr-api:auth');
|
const debug = debugname('hostr-api:auth');
|
||||||
|
|
||||||
const badLoginMsg = '{"error": {"message": "Incorrect login details.", "code": 607}}';
|
const badLoginMsg = '{"error": {"message": "Incorrect login details.", "code": 607}}';
|
||||||
|
|
||||||
export default function* (next) {
|
export default function* (next) {
|
||||||
const Users = this.db.Users;
|
|
||||||
const Files = this.db.Files;
|
|
||||||
const Logins = this.db.Logins;
|
|
||||||
let user = false;
|
let user = false;
|
||||||
|
const remoteIp = this.req.headers['x-real-ip'] || this.req.connection.remoteAddress;
|
||||||
|
const login = yield models.login.create({
|
||||||
|
ip: remoteIp,
|
||||||
|
successful: false,
|
||||||
|
});
|
||||||
if (this.req.headers.authorization && this.req.headers.authorization[0] === ':') {
|
if (this.req.headers.authorization && this.req.headers.authorization[0] === ':') {
|
||||||
debug('Logging in with token');
|
debug('Logging in with token');
|
||||||
const userToken = yield this.redis.get(this.req.headers.authorization.substr(1));
|
const userToken = yield this.redis.get(this.req.headers.authorization.substr(1));
|
||||||
this.assert(userToken, 401, '{"error": {"message": "Invalid token.", "code": 606}}');
|
this.assert(userToken, 401, '{"error": {"message": "Invalid token.", "code": 606}}');
|
||||||
debug('Token found');
|
debug('Token found');
|
||||||
user = yield Users.findOne({ _id: this.db.objectId(userToken) });
|
user = yield models.user.findById(userToken);
|
||||||
|
if (!user) {
|
||||||
|
login.save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const authUser = auth(this);
|
const authUser = auth(this);
|
||||||
this.assert(authUser, 401, badLoginMsg);
|
this.assert(authUser, 401, badLoginMsg);
|
||||||
const remoteIp = this.req.headers['x-real-ip'] || this.req.connection.remoteAddress;
|
const count = yield models.login.count({
|
||||||
const count = yield Logins.count({
|
where: {
|
||||||
ip: remoteIp,
|
ip: remoteIp,
|
||||||
successful: false,
|
successful: false,
|
||||||
at: { $gt: Math.ceil(Date.now() / 1000) - 600 },
|
createdAt: {
|
||||||
|
$gt: new Date(Date.now() - 600000),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.assert(count < 25, 401,
|
this.assert(count < 25, 401,
|
||||||
'{"error": {"message": "Too many incorrect logins.", "code": 608}}');
|
'{"error": {"message": "Too many incorrect logins.", "code": 608}}');
|
||||||
|
|
||||||
yield Logins.insertOne({ ip: remoteIp, at: Math.ceil(Date.now() / 1000), successful: null });
|
user = yield models.user.findOne({
|
||||||
user = yield Users.findOne({
|
where: {
|
||||||
email: authUser.name,
|
email: authUser.name,
|
||||||
banned: { $exists: false },
|
activated: true,
|
||||||
status: { $ne: 'deleted' },
|
},
|
||||||
});
|
});
|
||||||
this.assert(user, 401, badLoginMsg);
|
|
||||||
const authenticated = yield passwords.match(authUser.pass, user.salted_password);
|
if (!user || !(yield passwords.match(authUser.pass, user.password))) {
|
||||||
this.assert(authenticated, 401, badLoginMsg);
|
login.save();
|
||||||
|
this.throw(401, badLoginMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
debug('Checking user');
|
debug('Checking user');
|
||||||
this.assert(user, 401, badLoginMsg);
|
this.assert(user, 401, badLoginMsg);
|
||||||
debug('Checking user is activated');
|
debug('Checking user is activated');
|
||||||
this.assert(!user.activationCode, 401,
|
debug(user.activated);
|
||||||
|
this.assert(user.activated === true, 401,
|
||||||
'{"error": {"message": "Account has not been activated.", "code": 603}}');
|
'{"error": {"message": "Account has not been activated.", "code": 603}}');
|
||||||
|
|
||||||
const uploadedTotal = yield Files.count({ owner: user._id, status: { $ne: 'deleted' } });
|
login.successful = true;
|
||||||
const uploadedToday = yield Files.count({
|
yield login.save();
|
||||||
owner: user._id,
|
|
||||||
time_added: { $gt: Math.ceil(Date.now() / 1000) - 86400 },
|
const uploadedTotal = yield models.file.count({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const uploadedToday = yield models.file.count({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
createdAt: {
|
||||||
|
$gt: Date.now() - 86400000,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalisedUser = {
|
const normalisedUser = {
|
||||||
id: user._id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
daily_upload_allowance: user.type === 'Pro' ? 'unlimited' : 15,
|
daily_upload_allowance: user.plan === 'Pro' ? 'unlimited' : 15,
|
||||||
file_count: uploadedTotal,
|
file_count: uploadedTotal,
|
||||||
max_filesize: user.type === 'Pro' ? 524288000 : 20971520,
|
max_filesize: user.plan === 'Pro' ? 524288000 : 20971520,
|
||||||
plan: user.type || 'Free',
|
plan: user.plan,
|
||||||
uploads_today: uploadedToday,
|
uploads_today: uploadedToday,
|
||||||
};
|
};
|
||||||
this.response.set('Daily-Uploads-Remaining',
|
this.response.set('Daily-Uploads-Remaining',
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import redis from 'redis';
|
import redis from 'redis';
|
||||||
|
|
||||||
|
import models from '../../models';
|
||||||
import { formatFile } from '../../lib/format';
|
import { formatFile } from '../../lib/format';
|
||||||
import Uploader from '../../lib/uploader';
|
import Uploader from '../../lib/uploader';
|
||||||
|
|
||||||
|
@ -20,7 +21,6 @@ export function* post(next) {
|
||||||
|
|
||||||
uploader.receive();
|
uploader.receive();
|
||||||
|
|
||||||
yield uploader.save();
|
|
||||||
yield uploader.promise;
|
yield uploader.promise;
|
||||||
|
|
||||||
uploader.processingEvent();
|
uploader.processingEvent();
|
||||||
|
@ -30,7 +30,7 @@ export function* post(next) {
|
||||||
yield uploader.finalise();
|
yield uploader.finalise();
|
||||||
|
|
||||||
this.status = 201;
|
this.status = 201;
|
||||||
this.body = uploader.toJSON();
|
this.body = formatFile(uploader.file);
|
||||||
|
|
||||||
uploader.completeEvent();
|
uploader.completeEvent();
|
||||||
uploader.malwareScan();
|
uploader.malwareScan();
|
||||||
|
@ -38,67 +38,56 @@ export function* post(next) {
|
||||||
|
|
||||||
|
|
||||||
export function* list() {
|
export function* list() {
|
||||||
const Files = this.db.Files;
|
|
||||||
|
|
||||||
let status = 'active';
|
|
||||||
if (this.request.query.trashed) {
|
|
||||||
status = 'trashed';
|
|
||||||
} else if (this.request.query.all) {
|
|
||||||
status = { $in: ['active', 'trashed'] };
|
|
||||||
}
|
|
||||||
|
|
||||||
let limit = 20;
|
let limit = 20;
|
||||||
if (this.request.query.perpage === '0') {
|
if (this.request.query.perpage === '0') {
|
||||||
limit = false;
|
limit = 1000;
|
||||||
} else if (this.request.query.perpage > 0) {
|
} else if (this.request.query.perpage > 0) {
|
||||||
limit = parseInt(this.request.query.perpage / 1, 10);
|
limit = parseInt(this.request.query.perpage / 1, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
let skip = 0;
|
let offset = 0;
|
||||||
if (this.request.query.page) {
|
if (this.request.query.page) {
|
||||||
skip = parseInt(this.request.query.page - 1, 10) * limit;
|
offset = parseInt(this.request.query.page - 1, 10) * limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryOptions = {
|
const files = yield models.file.findAll({
|
||||||
limit, skip, sort: [['time_added', 'desc']],
|
where: {
|
||||||
hint: {
|
userId: this.user.id,
|
||||||
owner: 1, status: 1, time_added: -1,
|
processed: true,
|
||||||
},
|
},
|
||||||
};
|
order: '"createdAt" DESC',
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
|
||||||
const userFiles = yield Files.find({
|
|
||||||
owner: this.user.id, status }, queryOptions).toArray();
|
|
||||||
this.statsd.incr('file.list', 1);
|
this.statsd.incr('file.list', 1);
|
||||||
this.body = userFiles.map(formatFile);
|
this.body = files.map(formatFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function* get() {
|
export function* get() {
|
||||||
const Files = this.db.Files;
|
const file = yield models.file.findOne({
|
||||||
const Users = this.db.Users;
|
where: {
|
||||||
const file = yield Files.findOne({ _id: this.params.id,
|
id: this.params.id,
|
||||||
status: { $in: ['active', 'uploading'] } });
|
},
|
||||||
|
});
|
||||||
this.assert(file, 404, '{"error": {"message": "File not found", "code": 604}}');
|
this.assert(file, 404, '{"error": {"message": "File not found", "code": 604}}');
|
||||||
const user = yield Users.findOne({ _id: file.owner });
|
const user = yield file.getUser();
|
||||||
this.assert(user && !user.banned, 404, '{"error": {"message": "File not found", "code": 604}}');
|
this.assert(user && !user.banned, 404, '{"error": {"message": "File not found", "code": 604}}');
|
||||||
this.statsd.incr('file.get', 1);
|
this.statsd.incr('file.get', 1);
|
||||||
this.body = formatFile(file);
|
this.body = formatFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function* put() {
|
|
||||||
if (this.request.body.trashed) {
|
|
||||||
const Files = this.db.Files;
|
|
||||||
const status = this.request.body.trashed ? 'trashed' : 'active';
|
|
||||||
yield Files.updateOne({ _id: this.params.id, owner: this.user.id },
|
|
||||||
{ $set: { status } }, { w: 1 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function* del() {
|
export function* del() {
|
||||||
yield this.db.Files.updateOne({ _id: this.params.id, owner: this.db.objectId(this.user.id) },
|
const file = yield models.file.findOne({
|
||||||
{ $set: { status: 'deleted' } }, { w: 1 });
|
where: {
|
||||||
|
id: this.params.id,
|
||||||
|
userId: this.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 } };
|
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(`/file/${this.params.id}`, JSON.stringify(event));
|
||||||
yield this.redis.publish(`/user/${this.user.id}`, JSON.stringify(event));
|
yield this.redis.publish(`/user/${this.user.id}`, JSON.stringify(event));
|
||||||
|
|
|
@ -6,18 +6,20 @@ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
||||||
import sendgridInit from 'sendgrid';
|
import sendgridInit from 'sendgrid';
|
||||||
const sendgrid = sendgridInit(process.env.SENDGRID_KEY);
|
const sendgrid = sendgridInit(process.env.SENDGRID_KEY);
|
||||||
|
|
||||||
|
import models from '../../models';
|
||||||
|
|
||||||
const from = process.env.EMAIL_FROM;
|
const from = process.env.EMAIL_FROM;
|
||||||
const fromname = process.env.EMAIL_NAME;
|
const fromname = process.env.EMAIL_NAME;
|
||||||
|
|
||||||
export function* create() {
|
export function* create() {
|
||||||
const Users = this.db.Users;
|
|
||||||
const Transactions = this.db.Transactions;
|
|
||||||
const stripeToken = this.request.body.stripeToken;
|
const stripeToken = this.request.body.stripeToken;
|
||||||
|
|
||||||
|
const ip = this.request.headers['x-real-ip'] || this.req.connection.remoteAddress;
|
||||||
|
|
||||||
const createCustomer = {
|
const createCustomer = {
|
||||||
card: stripeToken.id,
|
card: stripeToken.id,
|
||||||
plan: 'usd_monthly',
|
plan: 'usd_monthly',
|
||||||
email: this.session.email,
|
email: this.user.email,
|
||||||
};
|
};
|
||||||
|
|
||||||
const customer = yield stripe.customers.create(createCustomer);
|
const customer = yield stripe.customers.create(createCustomer);
|
||||||
|
@ -26,19 +28,22 @@ export function* create() {
|
||||||
|
|
||||||
delete customer.subscriptions;
|
delete customer.subscriptions;
|
||||||
|
|
||||||
yield Users.updateOne({ _id: this.session.user.id },
|
const user = yield models.user.findById(this.user.id);
|
||||||
{ $set: { stripe_customer: customer, type: 'Pro' } });
|
user.plan = 'Pro';
|
||||||
|
yield user.save();
|
||||||
|
|
||||||
const transaction = {
|
const transaction = yield models.transaction.create({
|
||||||
user_id: this.session.user.id,
|
userId: this.user.id,
|
||||||
amount: customer.subscription.plan.amount,
|
amount: customer.subscription.plan.amount,
|
||||||
desc: customer.subscription.plan.name,
|
description: customer.subscription.plan.name,
|
||||||
date: new Date(customer.subscription.plan.created * 1000),
|
data: customer,
|
||||||
};
|
type: 'direct',
|
||||||
|
ip,
|
||||||
|
});
|
||||||
|
|
||||||
yield Transactions.insertOne(transaction);
|
yield transaction.save();
|
||||||
|
|
||||||
this.session.user.plan = 'Pro';
|
this.user.plan = 'Pro';
|
||||||
this.body = { status: 'active' };
|
this.body = { status: 'active' };
|
||||||
|
|
||||||
const html = yield render('email/inlined/pro');
|
const html = yield render('email/inlined/pro');
|
||||||
|
@ -50,7 +55,7 @@ export function* create() {
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const mail = new sendgrid.Email({
|
const mail = new sendgrid.Email({
|
||||||
to: this.session.user.email,
|
to: this.user.email,
|
||||||
subject: 'Hostr Pro',
|
subject: 'Hostr Pro',
|
||||||
from,
|
from,
|
||||||
fromname,
|
fromname,
|
||||||
|
@ -62,19 +67,19 @@ export function* create() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* cancel() {
|
export function* cancel() {
|
||||||
this.assertCSRF();
|
const user = yield models.user.findById(this.user.id);
|
||||||
const Users = this.db.Users;
|
const transactions = yield user.getTransactions();
|
||||||
const user = yield Users.findOne({ _id: this.session.user.id });
|
const transaction = transactions[0];
|
||||||
|
|
||||||
const confirmation = yield stripe.customers.cancelSubscription(
|
yield stripe.customers.cancelSubscription(
|
||||||
user.stripe_customer.id,
|
transaction.data.id,
|
||||||
user.stripe_customer.subscription.id,
|
transaction.data.subscription.id,
|
||||||
{ at_period_end: true }
|
{ at_period_end: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
yield Users.updateOne({ _id: this.session.user.id },
|
user.plan = 'Free';
|
||||||
{ $set: { 'stripe_customer.subscription': confirmation, type: 'Free' } });
|
yield user.save();
|
||||||
|
|
||||||
this.session.user.plan = 'Pro';
|
this.user.plan = 'Free';
|
||||||
this.body = { status: 'inactive' };
|
this.body = { status: 'inactive' };
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@ import uuid from 'node-uuid';
|
||||||
import redis from 'redis';
|
import redis from 'redis';
|
||||||
import co from 'co';
|
import co from 'co';
|
||||||
import passwords from 'passwords';
|
import passwords from 'passwords';
|
||||||
|
import models from '../../models';
|
||||||
|
|
||||||
import debugname from 'debug';
|
import debugname from 'debug';
|
||||||
const debug = debugname('hostr-api:user');
|
const debug = debugname('hostr-api:user');
|
||||||
|
@ -19,17 +20,19 @@ export function* token() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* transaction() {
|
export function* transaction() {
|
||||||
const Transactions = this.db.Transactions;
|
const transactions = yield models.transaction.findAll({
|
||||||
const transactions = yield Transactions.find({ user_id: this.user.id }).toArray();
|
where: {
|
||||||
|
userId: this.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.body = transactions.map((transaction) => { // eslint-disable-line no-shadow
|
this.body = transactions.map((item) => {
|
||||||
const type = transaction.paypal ? 'paypal' : 'direct';
|
|
||||||
return {
|
return {
|
||||||
id: transaction._id,
|
id: item.id,
|
||||||
amount: transaction.paypal ? transaction.amount : transaction.amount / 100,
|
amount: item.amount / 100,
|
||||||
date: transaction.date,
|
date: item.date,
|
||||||
description: transaction.desc,
|
description: item.description,
|
||||||
type,
|
type: 'direct',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -39,23 +42,18 @@ export function* settings() {
|
||||||
'{"error": {"message": "Current Password required to update account.", "code": 612}}');
|
'{"error": {"message": "Current Password required to update account.", "code": 612}}');
|
||||||
this.assert(this.request.body.current_password, 400,
|
this.assert(this.request.body.current_password, 400,
|
||||||
'{"error": {"message": "Current Password required to update account.", "code": 612}}');
|
'{"error": {"message": "Current Password required to update account.", "code": 612}}');
|
||||||
const Users = this.db.Users;
|
const user = yield models.user.findById(this.user.id);
|
||||||
const user = yield Users.findOne({ _id: this.user.id });
|
this.assert(yield passwords.match(this.request.body.current_password, user.password), 400,
|
||||||
this.assert(yield passwords.match(this.request.body.current_password, user.salted_password), 400,
|
|
||||||
'{"error": {"message": "Incorrect password", "code": 606}}');
|
'{"error": {"message": "Incorrect password", "code": 606}}');
|
||||||
const data = {};
|
|
||||||
if (this.request.body.email && this.request.body.email !== user.email) {
|
if (this.request.body.email && this.request.body.email !== user.email) {
|
||||||
data.email = this.request.body.email;
|
user.email = this.request.body.email;
|
||||||
if (!user.activated_email) {
|
|
||||||
data.activated_email = user.email; // eslint-disable-line camelcase
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (this.request.body.new_password) {
|
if (this.request.body.new_password) {
|
||||||
this.assert(this.request.body.new_password.length >= 7, 400,
|
this.assert(this.request.body.new_password.length >= 7, 400,
|
||||||
'{"error": {"message": "Password must be 7 or more characters long.", "code": 606}}');
|
'{"error": {"message": "Password must be 7 or more characters long.", "code": 606}}');
|
||||||
data.salted_password = yield passwords.hash(this.request.body.new_password);
|
user.password = yield passwords.hash(this.request.body.new_password);
|
||||||
}
|
}
|
||||||
Users.updateOne({ _id: user._id }, { $set: data });
|
yield user.save();
|
||||||
this.body = {};
|
this.body = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
271
api/views/email/style.css
Normal file
271
api/views/email/style.css
Normal file
|
@ -0,0 +1,271 @@
|
||||||
|
/* -------------------------------------
|
||||||
|
GLOBAL
|
||||||
|
A very basic CSS reset
|
||||||
|
------------------------------------- */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Let's make sure all tables have defaults */
|
||||||
|
table td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
BODY & CONTAINER
|
||||||
|
------------------------------------- */
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-wrap {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: block !important;
|
||||||
|
max-width: 600px !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
/* makes it centered */
|
||||||
|
clear: both !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER, FOOTER, MAIN
|
||||||
|
------------------------------------- */
|
||||||
|
.main {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e9e9e9;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrap {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-block {
|
||||||
|
padding: 0 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
width: 100%;
|
||||||
|
clear: both;
|
||||||
|
color: #999;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.footer a {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.footer p, .footer a, .footer unsubscribe, .footer td {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
TYPOGRAPHY
|
||||||
|
------------------------------------- */
|
||||||
|
h1, h2, h3 {
|
||||||
|
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
||||||
|
color: #000;
|
||||||
|
margin: 40px 0 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
p, ul, ol {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
p li, ul li, ol li {
|
||||||
|
margin-left: 5px;
|
||||||
|
list-style-position: inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
LINKS & BUTTONS
|
||||||
|
------------------------------------- */
|
||||||
|
a {
|
||||||
|
color: #456470;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #FFF;
|
||||||
|
background-color: #456470;
|
||||||
|
border: solid #456470;
|
||||||
|
/* seems to be a bug stopping this from being applied above */
|
||||||
|
border-color: #456470;
|
||||||
|
border-width: 10px 20px;
|
||||||
|
line-height: 2;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
OTHER STYLES THAT MIGHT BE USEFUL
|
||||||
|
------------------------------------- */
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aligncenter {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alignright {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alignleft {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
ALERTS
|
||||||
|
Change the class depending on warning email, good email or bad email
|
||||||
|
------------------------------------- */
|
||||||
|
.alert {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 3px 3px 0 0;
|
||||||
|
}
|
||||||
|
.alert a {
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.alert.alert-warning {
|
||||||
|
background: #ff9f00;
|
||||||
|
}
|
||||||
|
.alert.alert-bad {
|
||||||
|
background: #d0021b;
|
||||||
|
}
|
||||||
|
.alert.alert-good {
|
||||||
|
background: #68b90f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
INVOICE
|
||||||
|
Styles for the billing table
|
||||||
|
------------------------------------- */
|
||||||
|
.invoice {
|
||||||
|
margin: 40px auto;
|
||||||
|
text-align: left;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
.invoice td {
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
.invoice .invoice-items {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.invoice .invoice-items td {
|
||||||
|
border-top: #eee 1px solid;
|
||||||
|
}
|
||||||
|
.invoice .invoice-items .total td {
|
||||||
|
border-top: 2px solid #333;
|
||||||
|
border-bottom: 2px solid #333;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 640px) {
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
font-weight: 600 !important;
|
||||||
|
margin: 20px 0 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 22px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content, .content-wrapper {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
2
app.js
2
app.js
|
@ -8,7 +8,6 @@ import bodyparser from 'koa-bodyparser';
|
||||||
import websockify from 'koa-websocket';
|
import websockify from 'koa-websocket';
|
||||||
import helmet from 'koa-helmet';
|
import helmet from 'koa-helmet';
|
||||||
import raven from 'raven';
|
import raven from 'raven';
|
||||||
import mongo from './lib/mongo';
|
|
||||||
import * as redis from './lib/redis';
|
import * as redis from './lib/redis';
|
||||||
import api, { ws } from './api/app';
|
import api, { ws } from './api/app';
|
||||||
import web from './web/app';
|
import web from './web/app';
|
||||||
|
@ -50,7 +49,6 @@ app.use(function* errorMiddleware(next) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(mongo());
|
|
||||||
app.use(redis.middleware());
|
app.use(redis.middleware());
|
||||||
app.use(logger());
|
app.use(logger());
|
||||||
app.use(compress());
|
app.use(compress());
|
||||||
|
|
|
@ -22,26 +22,26 @@ export function formatSize(size) {
|
||||||
|
|
||||||
export function formatFile(file) {
|
export function formatFile(file) {
|
||||||
const formattedFile = {
|
const formattedFile = {
|
||||||
added: moment.unix(file.time_added).format(),
|
added: moment.unix(file.createdAt / 1000).format(),
|
||||||
readableAdded: formatDate(file.time_added),
|
readableAdded: formatDate(file.createdAt / 1000),
|
||||||
downloads: file.downloads !== undefined ? file.downloads : 0,
|
downloads: file.downloads !== undefined ? file.downloads : 0,
|
||||||
href: `${baseURL}/${file._id}`,
|
href: `${baseURL}/${file.id}`,
|
||||||
id: file._id,
|
id: file.id,
|
||||||
name: file.file_name,
|
name: file.name,
|
||||||
size: file.file_size,
|
size: file.size,
|
||||||
readableSize: formatSize(file.file_size),
|
readableSize: formatSize(file.size),
|
||||||
type: sniff(file.file_name),
|
type: sniff(file.name),
|
||||||
trashed: (file.status === 'trashed'),
|
trashed: (file.status === 'trashed'),
|
||||||
status: file.status,
|
status: file.processed === true ? 'active' : 'uploading',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (file.width) {
|
if (file.width) {
|
||||||
formattedFile.height = file.height;
|
formattedFile.height = file.height;
|
||||||
formattedFile.width = file.width;
|
formattedFile.width = file.width;
|
||||||
const ext = (file.file_name.split('.').pop().toLowerCase() === 'psd' ? '.png' : '');
|
const ext = (file.name.split('.').pop().toLowerCase() === 'psd' ? '.png' : '');
|
||||||
formattedFile.direct = {
|
formattedFile.direct = {
|
||||||
'150x': `${baseURL}/file/150/${file._id}/${file.file_name}${ext}`,
|
'150x': `${baseURL}/file/150/${file.id}/${file.name}${ext}`,
|
||||||
'970x': `${baseURL}/file/970/${file._id}/${file.file_name}${ext}`,
|
'970x': `${baseURL}/file/970/${file.id}/${file.name}${ext}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return formattedFile;
|
return formattedFile;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import models from '../models';
|
||||||
|
|
||||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
|
||||||
function randomID() {
|
function randomID() {
|
||||||
|
@ -12,7 +14,7 @@ function* checkId(Files, fileId, attempts) {
|
||||||
if (attempts > 10) {
|
if (attempts > 10) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const file = yield Files.findOne({ _id: fileId });
|
const file = yield models.file.findById(fileId);
|
||||||
if (file === null) {
|
if (file === null) {
|
||||||
return fileId;
|
return fileId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import debugname from 'debug';
|
||||||
const debug = debugname('hostr:mongo');
|
const debug = debugname('hostr:mongo');
|
||||||
|
|
||||||
/* eslint no-param-reassign: ["error", { "props": false }] */
|
/* eslint no-param-reassign: ["error", { "props": false }] */
|
||||||
const configuredClient = new Promise((resolve, reject) => {
|
export const mongo = new Promise((resolve, reject) => {
|
||||||
debug('Connecting to Mongodb');
|
debug('Connecting to Mongodb');
|
||||||
return MongoClient.connect(process.env.MONGO_URL).then((client) => {
|
return MongoClient.connect(process.env.MONGO_URL).then((client) => {
|
||||||
debug('Successfully connected to Mongodb');
|
debug('Successfully connected to Mongodb');
|
||||||
|
@ -25,10 +25,10 @@ const configuredClient = new Promise((resolve, reject) => {
|
||||||
debug(e);
|
debug(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function mongo() {
|
export default function () {
|
||||||
return function* dbMiddleware(next) {
|
return function* dbMiddleware(next) {
|
||||||
try {
|
try {
|
||||||
this.db = yield configuredClient;
|
this.db = yield mongo;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debug(e);
|
debug(e);
|
||||||
}
|
}
|
||||||
|
|
126
lib/uploader.js
126
lib/uploader.js
|
@ -4,8 +4,9 @@ import crypto from 'crypto';
|
||||||
import fs from 'mz/fs';
|
import fs from 'mz/fs';
|
||||||
import sizeOf from 'image-size';
|
import sizeOf from 'image-size';
|
||||||
|
|
||||||
|
import models from '../models';
|
||||||
|
import createHostrId from './hostr-id';
|
||||||
import { formatFile } from './format';
|
import { formatFile } from './format';
|
||||||
import hostrId from './hostr-id';
|
|
||||||
import resize from './resize';
|
import resize from './resize';
|
||||||
import malware from './malware';
|
import malware from './malware';
|
||||||
import { sniff } from './type';
|
import { sniff } from './type';
|
||||||
|
@ -21,7 +22,6 @@ const supported = ['jpeg', 'jpg', 'png', 'gif'];
|
||||||
export default class Uploader {
|
export default class Uploader {
|
||||||
constructor(context) {
|
constructor(context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.Files = context.db.Files;
|
|
||||||
this.expectedSize = context.request.headers['content-length'];
|
this.expectedSize = context.request.headers['content-length'];
|
||||||
this.tempGuid = context.request.headers['hostr-guid'];
|
this.tempGuid = context.request.headers['hostr-guid'];
|
||||||
this.remoteIp = context.request.headers['x-real-ip'] || context.req.connection.remoteAddress;
|
this.remoteIp = context.request.headers['x-real-ip'] || context.req.connection.remoteAddress;
|
||||||
|
@ -54,13 +54,23 @@ export default class Uploader {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.tempGuid = this.tempGuid;
|
this.tempGuid = this.tempGuid;
|
||||||
this.originalName = this.upload.filename;
|
this.file = yield models.file.create({
|
||||||
this.filename = this.upload.filename.replace(/[^a-zA-Z0-9\.\-_\s]/g, '').replace(/\s+/g, '');
|
id: yield createHostrId(),
|
||||||
this.id = yield hostrId(this.Files);
|
name: this.upload.filename.replace(/[^a-zA-Z0-9\.\-_\s]/g, '').replace(/\s+/g, ''),
|
||||||
|
originalName: this.upload.filename,
|
||||||
|
userId: this.context.user.id,
|
||||||
|
status: 'uploading',
|
||||||
|
type: sniff(this.upload.filename),
|
||||||
|
ip: this.remoteIp,
|
||||||
|
accessedAt: null,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
|
});
|
||||||
|
yield this.file.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
receive() {
|
receive() {
|
||||||
this.path = join(this.id[0], `${this.id}_${this.filename}`);
|
this.path = join(this.file.id[0], `${this.file.id}_${this.file.name}`);
|
||||||
this.localStream = fs.createWriteStream(join(storePath, this.path));
|
this.localStream = fs.createWriteStream(join(storePath, this.path));
|
||||||
|
|
||||||
this.upload.pause();
|
this.upload.pause();
|
||||||
|
@ -78,8 +88,8 @@ export default class Uploader {
|
||||||
this.percentComplete = Math.floor(this.receivedSize * 100 / this.expectedSize);
|
this.percentComplete = Math.floor(this.receivedSize * 100 / this.expectedSize);
|
||||||
if (this.percentComplete > this.lastPercent && this.lastTick < Date.now() - 1000) {
|
if (this.percentComplete > this.lastPercent && this.lastTick < Date.now() - 1000) {
|
||||||
const progressEvent = `{"type": "file-progress", "data":
|
const progressEvent = `{"type": "file-progress", "data":
|
||||||
{"id": "${this.upload.id}", "complete": ${this.percentComplete}}}`;
|
{"id": "${this.file.id}", "complete": ${this.percentComplete}}}`;
|
||||||
this.context.redis.publish(`/file/${this.upload.id}`, progressEvent);
|
this.context.redis.publish(`/file/${this.file.id}`, progressEvent);
|
||||||
this.context.redis.publish(`/user/${this.context.user.id}`, progressEvent);
|
this.context.redis.publish(`/user/${this.context.user.id}`, progressEvent);
|
||||||
this.lastTick = Date.now();
|
this.lastTick = Date.now();
|
||||||
}
|
}
|
||||||
|
@ -89,6 +99,8 @@ export default class Uploader {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.upload.on('end', () => {
|
this.upload.on('end', () => {
|
||||||
|
this.file.size = this.receivedSize;
|
||||||
|
this.file.md5 = this.md5sum.digest('hex');
|
||||||
this.localStream.end();
|
this.localStream.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -96,61 +108,34 @@ export default class Uploader {
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptedEvent() {
|
acceptedEvent() {
|
||||||
const acceptedEvent = `{"type": "file-accepted", "data":
|
const accepted = `{"type": "file-accepted", "data":
|
||||||
{"id": "${this.id}", "guid": "${this.tempGuid}", "href": "${baseURL}/${this.id}"}}`;
|
{"id": "${this.file.id}", "guid": "${this.tempGuid}", "href": "${baseURL}/${this.file.id}"}}`;
|
||||||
this.context.redis.publish(`/user/${this.context.user.id}`, acceptedEvent);
|
this.context.redis.publish(`/user/${this.context.user.id}`, accepted);
|
||||||
this.context.statsd.incr('file.upload.accepted', 1);
|
this.context.statsd.incr('file.upload.accepted', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
processingEvent() {
|
processingEvent() {
|
||||||
const processingEvent = `{"type": "file-progress", "data":
|
const processing = `{"type": "file-progress", "data":
|
||||||
{"id": "${this.id}", "complete": 100}}`;
|
{"id": "${this.file.id}", "complete": 100}}`;
|
||||||
this.context.redis.publish(`/file/${this.id}`, processingEvent);
|
this.context.redis.publish(`/file/${this.file.id}`, processing);
|
||||||
this.context.redis.publish(`/user/${this.context.user.id}`, processingEvent);
|
this.context.redis.publish(`/user/${this.context.user.id}`, processing);
|
||||||
this.context.statsd.incr('file.upload.complete', 1);
|
this.context.statsd.incr('file.upload.complete', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
completeEvent() {
|
completeEvent() {
|
||||||
const completeEvent = `{"type": "file-added", "data": ${JSON.stringify(this.toDBFormat())}}`;
|
const complete = `{"type": "file-added", "data": ${JSON.stringify(formatFile(this.file))}}`;
|
||||||
this.context.redis.publish(`/file/${this.id}`, completeEvent);
|
this.context.redis.publish(`/file/${this.file.id}`, complete);
|
||||||
this.context.redis.publish(`/user/${this.context.user.id}`, completeEvent);
|
this.context.redis.publish(`/user/${this.context.user.id}`, complete);
|
||||||
}
|
|
||||||
|
|
||||||
toDBFormat() {
|
|
||||||
const formatted = {
|
|
||||||
owner: this.context.user.id,
|
|
||||||
ip: this.remoteIp,
|
|
||||||
system_name: this.id,
|
|
||||||
file_name: this.filename,
|
|
||||||
original_name: this.originalName,
|
|
||||||
file_size: this.receivedSize,
|
|
||||||
time_added: Math.ceil(Date.now() / 1000),
|
|
||||||
status: 'active',
|
|
||||||
last_accessed: null,
|
|
||||||
s3: false,
|
|
||||||
type: sniff(this.filename),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.width) {
|
|
||||||
formatted.width = this.width;
|
|
||||||
formatted.height = this.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
save() {
|
|
||||||
return this.Files.insertOne({ _id: this.id, ...this.toDBFormat() });
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
return formatFile({ _id: this.id, ...this.toDBFormat() });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*checkLimit() {
|
*checkLimit() {
|
||||||
const count = yield this.Files.count({
|
const count = yield models.file.count({
|
||||||
owner: this.context.user.id,
|
where: {
|
||||||
time_added: { $gt: Math.ceil(Date.now() / 1000) - 86400 },
|
userId: this.context.user.id,
|
||||||
|
createdAt: {
|
||||||
|
$gt: Date.now() - 86400000,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const userLimit = this.context.user.daily_upload_allowance;
|
const userLimit = this.context.user.daily_upload_allowance;
|
||||||
const underLimit = (count < userLimit || userLimit === 'unlimited');
|
const underLimit = (count < userLimit || userLimit === 'unlimited');
|
||||||
|
@ -167,22 +152,15 @@ export default class Uploader {
|
||||||
}
|
}
|
||||||
|
|
||||||
*finalise() {
|
*finalise() {
|
||||||
const dbFile = this.toDBFormat();
|
this.file.size = this.receivedSize;
|
||||||
dbFile.file_size = this.receivedSize;
|
this.file.status = 'active';
|
||||||
dbFile.status = 'active';
|
this.file.processed = 'true';
|
||||||
dbFile.md5 = this.md5sum.digest('hex');
|
yield this.file.save();
|
||||||
|
|
||||||
if (this.width) {
|
|
||||||
dbFile.width = this.width;
|
|
||||||
dbFile.height = this.height;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
yield this.Files.updateOne({ _id: this.id }, { $set: dbFile });
|
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}`);
|
||||||
resizeImage(upload, type, currentSize, newSize) {
|
|
||||||
return resize(join(storePath, this.path), type, currentSize, newSize).then((image) => {
|
|
||||||
const path = join(this.id[0], String(newSize.width), `${this.id}_${this.filename}`);
|
|
||||||
debug('Writing file');
|
debug('Writing file');
|
||||||
debug(join(storePath, path));
|
debug(join(storePath, path));
|
||||||
return fs.writeFile(join(storePath, path), image).catch(debug);
|
return fs.writeFile(join(storePath, path), image).catch(debug);
|
||||||
|
@ -209,8 +187,8 @@ export default class Uploader {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.width = size.width;
|
this.file.width = size.width;
|
||||||
this.height = size.height;
|
this.file.height = size.height;
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
this.resizeImage(upload, size.type, size, { width: 150, height: 150 }),
|
this.resizeImage(upload, size.type, size, { width: 150, height: 150 }),
|
||||||
|
@ -228,9 +206,15 @@ export default class Uploader {
|
||||||
debug('Malware Scan');
|
debug('Malware Scan');
|
||||||
const result = yield malware(this);
|
const result = yield malware(this);
|
||||||
if (result) {
|
if (result) {
|
||||||
yield this.Files.updateOne({ _id: this.id },
|
this.file.malwarePositives = result.positives;
|
||||||
{ $set: { malware: result.positive, virustotal: result } });
|
this.file.save();
|
||||||
if (result.positive) {
|
const fileMalware = yield models.malware.create({
|
||||||
|
fileId: this.file.id,
|
||||||
|
positives: result.positives,
|
||||||
|
virustotal: result,
|
||||||
|
});
|
||||||
|
fileMalware.save();
|
||||||
|
if (result.positive > 5) {
|
||||||
this.context.statsd.incr('file.malware', 1);
|
this.context.statsd.incr('file.malware', 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
75
migrate/migrate-activations.js
Normal file
75
migrate/migrate-activations.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import co from 'co';
|
||||||
|
|
||||||
|
import models from '../models';
|
||||||
|
import { mongo } from '../lib/mongo';
|
||||||
|
|
||||||
|
import debugname from 'debug';
|
||||||
|
const debug = debugname('hostr:db');
|
||||||
|
let db;
|
||||||
|
|
||||||
|
|
||||||
|
co(function *sync() {
|
||||||
|
debug('Syncing schema');
|
||||||
|
yield models.sequelize.sync();
|
||||||
|
debug('Schema synced');
|
||||||
|
db = yield mongo;
|
||||||
|
const users = yield db.Users.find({}, { sort: [['joined', 'asc']] }).toArray();
|
||||||
|
for (const user of users) {
|
||||||
|
if (user.joined === '0') {
|
||||||
|
const file = yield db.Files.findOne({
|
||||||
|
owner: user._id,
|
||||||
|
}, {
|
||||||
|
limit: 1,
|
||||||
|
sort: [['time_added', 'asc']],
|
||||||
|
});
|
||||||
|
if (file && file.time_added > 0) {
|
||||||
|
user.createdAt = new Date(file.time_added * 1000).getTime();
|
||||||
|
} else {
|
||||||
|
user.createdAt = new Date().getTime();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user.createdAt = new Date(user.joined * 1000).getTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
users.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1));
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
if (!user.email) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = yield models.user.findOne({
|
||||||
|
where: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
debug('User exists, continue');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mongoId = user._id.toString();
|
||||||
|
|
||||||
|
const newUser = yield models.user.create({
|
||||||
|
email: user.email,
|
||||||
|
password: user.salted_password,
|
||||||
|
name: user.first_name ? `${user.first_name} ${user.last_name}` : null,
|
||||||
|
plan: user.type || 'Free',
|
||||||
|
activated: !user.activationCode,
|
||||||
|
banned: !!user.banned,
|
||||||
|
deletedAt: user.status === 'deleted' ? new Date().getTime() : null,
|
||||||
|
createdAt: user.createdAt,
|
||||||
|
updatedAt: user.createdAt,
|
||||||
|
mongoId,
|
||||||
|
});
|
||||||
|
yield newUser.save({ silent: true });
|
||||||
|
}
|
||||||
|
models.sequelize.close();
|
||||||
|
db.close();
|
||||||
|
}).catch((err) => {
|
||||||
|
models.sequelize.close();
|
||||||
|
db.close();
|
||||||
|
debug(err);
|
||||||
|
});
|
83
migrate/migrate-files.js
Normal file
83
migrate/migrate-files.js
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import co from 'co';
|
||||||
|
import validateIp from 'validate-ip';
|
||||||
|
|
||||||
|
import models from '../models';
|
||||||
|
import { mongo } from '../lib/mongo';
|
||||||
|
|
||||||
|
import debugname from 'debug';
|
||||||
|
const debug = debugname('hostr:db');
|
||||||
|
let db;
|
||||||
|
|
||||||
|
|
||||||
|
co(function *sync() {
|
||||||
|
debug('Syncing schema');
|
||||||
|
yield models.sequelize.sync();
|
||||||
|
debug('Schema synced');
|
||||||
|
db = yield mongo;
|
||||||
|
const users = yield models.user.findAll({});
|
||||||
|
const userIds = {};
|
||||||
|
debug('remap');
|
||||||
|
for (const user of users) {
|
||||||
|
userIds[user.mongoId] = user.id;
|
||||||
|
}
|
||||||
|
debug('remap done');
|
||||||
|
let files;
|
||||||
|
try {
|
||||||
|
files = db.Files.find({}, {
|
||||||
|
sort: [['time_added', 'desc']],
|
||||||
|
skip: 0,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
debug(err);
|
||||||
|
}
|
||||||
|
debug('fetched files');
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const file = yield files.next();
|
||||||
|
if (!file) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!file.time_added || !file.file_size) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ip = file.ip ? file.ip.split(',').pop().trim() : null;
|
||||||
|
|
||||||
|
if (typeof ip !== 'string' || !validateIp(ip)) {
|
||||||
|
ip = null;
|
||||||
|
}
|
||||||
|
const processed = file.status !== 'uploading';
|
||||||
|
const accessedAt = file.last_accessed ? new Date(file.last_accessed * 1000) : null;
|
||||||
|
|
||||||
|
const mongoId = file._id.toString();
|
||||||
|
|
||||||
|
yield models.file.upsert({
|
||||||
|
id: file._id.toString(),
|
||||||
|
name: file.file_name,
|
||||||
|
originalName: file.original_name || file.file_name,
|
||||||
|
size: file.file_size,
|
||||||
|
downloads: file.downloads,
|
||||||
|
deletedAt: file.status === 'deleted' ? new Date() : null,
|
||||||
|
createdAt: new Date(file.time_added * 1000),
|
||||||
|
updatedAt: new Date(file.time_added * 1000),
|
||||||
|
accessedAt,
|
||||||
|
processed,
|
||||||
|
type: file.type !== 'file' ? file.type : 'other',
|
||||||
|
width: Number.isInteger(file.width) ? file.width : null,
|
||||||
|
height: Number.isInteger(file.height) ? file.height : null,
|
||||||
|
userId: file.owner !== undefined ? userIds[file.owner] : null,
|
||||||
|
ip,
|
||||||
|
legacyId: file.system_name !== file._id ? file.system_name : null,
|
||||||
|
md5: file.md5,
|
||||||
|
malwarePositives: file.virustotal && file.virustotal.positives > 0 ?
|
||||||
|
file.virustotal.positives : null,
|
||||||
|
mongoId,
|
||||||
|
}, { /* logging: false */ });
|
||||||
|
}
|
||||||
|
|
||||||
|
models.sequelize.close();
|
||||||
|
db.close();
|
||||||
|
}).catch((err) => {
|
||||||
|
models.sequelize.close();
|
||||||
|
db.close();
|
||||||
|
debug(err);
|
||||||
|
});
|
54
migrate/migrate-logins.js
Normal file
54
migrate/migrate-logins.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import co from 'co';
|
||||||
|
import validateIp from 'validate-ip';
|
||||||
|
|
||||||
|
import models from '../models';
|
||||||
|
import { mongo } from '../lib/mongo';
|
||||||
|
|
||||||
|
import debugname from 'debug';
|
||||||
|
const debug = debugname('hostr:db');
|
||||||
|
let db;
|
||||||
|
|
||||||
|
|
||||||
|
co(function *sync() {
|
||||||
|
debug('Syncing schema');
|
||||||
|
yield models.sequelize.sync();
|
||||||
|
debug('Schema synced');
|
||||||
|
db = yield mongo;
|
||||||
|
const users = yield models.user.findAll({});
|
||||||
|
const userIds = {};
|
||||||
|
debug('remap');
|
||||||
|
for (const user of users) {
|
||||||
|
userIds[user._id] = user.id;
|
||||||
|
}
|
||||||
|
debug('remap done');
|
||||||
|
let logins;
|
||||||
|
try {
|
||||||
|
logins = db.Logins.find({}, {
|
||||||
|
skip: 0,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
debug(err);
|
||||||
|
}
|
||||||
|
debug('fetched logins');
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const login = yield logins.next();
|
||||||
|
if (!login) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLogin = yield models.login.create({
|
||||||
|
ip: login.ip,
|
||||||
|
createdAt: login.at * 1000,
|
||||||
|
successful: login.successful,
|
||||||
|
}, { /* logging: false */ });
|
||||||
|
newLogin.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
models.sequelize.close();
|
||||||
|
db.close();
|
||||||
|
}).catch((err) => {
|
||||||
|
models.sequelize.close();
|
||||||
|
db.close();
|
||||||
|
debug(err);
|
||||||
|
});
|
46
migrate/migrate-malwares.js
Normal file
46
migrate/migrate-malwares.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import co from 'co';
|
||||||
|
|
||||||
|
import models from '../models';
|
||||||
|
import { mongo } from '../lib/mongo';
|
||||||
|
|
||||||
|
import debugname from 'debug';
|
||||||
|
const debug = debugname('hostr:db');
|
||||||
|
let db;
|
||||||
|
|
||||||
|
|
||||||
|
co(function *sync() {
|
||||||
|
debug('Syncing schema');
|
||||||
|
yield models.sequelize.sync();
|
||||||
|
debug('Schema synced');
|
||||||
|
db = yield mongo;
|
||||||
|
|
||||||
|
const files = db.Files.find({}, {
|
||||||
|
sort: [['time_added', 'desc']],
|
||||||
|
skip: 0,
|
||||||
|
});
|
||||||
|
debug('fetched files');
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const file = yield files.next();
|
||||||
|
if (!file) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.time_added || !file.file_size || !file.malware) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield models.malware.upsert({
|
||||||
|
fileId: file._id,
|
||||||
|
positives: file.virustotal ? file.virustotal.positives : 100,
|
||||||
|
virustotal: file.virustotal || null,
|
||||||
|
}, { /* logging: false */ });
|
||||||
|
}
|
||||||
|
|
||||||
|
models.sequelize.close();
|
||||||
|
db.close();
|
||||||
|
}).catch((err) => {
|
||||||
|
models.sequelize.close();
|
||||||
|
db.close();
|
||||||
|
debug(err);
|
||||||
|
});
|
77
migrate/migrate-users.js
Normal file
77
migrate/migrate-users.js
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import co from 'co';
|
||||||
|
|
||||||
|
import models from '../models';
|
||||||
|
import { mongo } from '../lib/mongo';
|
||||||
|
|
||||||
|
import debugname from 'debug';
|
||||||
|
const debug = debugname('hostr:db');
|
||||||
|
let db;
|
||||||
|
|
||||||
|
|
||||||
|
co(function *sync() {
|
||||||
|
debug('Syncing schema');
|
||||||
|
yield models.sequelize.sync();
|
||||||
|
debug('Schema synced');
|
||||||
|
db = yield mongo;
|
||||||
|
const users = yield db.Users.find({}, { sort: [['joined', 'asc']] }).toArray();
|
||||||
|
for (const user of users) {
|
||||||
|
if (user.joined === '0') {
|
||||||
|
const file = yield db.Files.findOne({
|
||||||
|
owner: user._id,
|
||||||
|
}, {
|
||||||
|
limit: 1,
|
||||||
|
sort: [['time_added', 'asc']],
|
||||||
|
});
|
||||||
|
if (file && file.time_added > 0) {
|
||||||
|
user.createdAt = new Date(file.time_added * 1000).getTime();
|
||||||
|
} else {
|
||||||
|
user.createdAt = new Date().getTime();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user.createdAt = new Date(user.joined * 1000).getTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
users.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1));
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
if (!user.email) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = yield models.user.findOne({
|
||||||
|
where: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
debug('User exists, continue');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mongoId = user._id.toString();
|
||||||
|
|
||||||
|
const newUser = yield models.user.create({
|
||||||
|
email: user.email,
|
||||||
|
password: user.salted_password,
|
||||||
|
name: user.first_name ? `${user.first_name} ${user.last_name}` : null,
|
||||||
|
plan: user.type || 'Free',
|
||||||
|
activated: !user.activationCode,
|
||||||
|
banned: !!user.banned,
|
||||||
|
deletedAt: user.status === 'deleted' ? new Date().getTime() : null,
|
||||||
|
createdAt: user.createdAt,
|
||||||
|
updatedAt: user.createdAt,
|
||||||
|
mongoId,
|
||||||
|
}, {
|
||||||
|
include: [models.activation],
|
||||||
|
});
|
||||||
|
yield newUser.save({ silent: true });
|
||||||
|
}
|
||||||
|
models.sequelize.close();
|
||||||
|
db.close();
|
||||||
|
}).catch((err) => {
|
||||||
|
models.sequelize.close();
|
||||||
|
db.close();
|
||||||
|
debug(err);
|
||||||
|
});
|
15
models/activation.js
Normal file
15
models/activation.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export default function (sequelize, DataTypes) {
|
||||||
|
const Activation = sequelize.define('activation', {
|
||||||
|
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||||
|
activatedAt: { type: DataTypes.DATE },
|
||||||
|
email: DataTypes.STRING,
|
||||||
|
}, {
|
||||||
|
classMethods: {
|
||||||
|
associate: (models) => {
|
||||||
|
Activation.belongsTo(models.user);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return Activation;
|
||||||
|
}
|
49
models/file.js
Normal file
49
models/file.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
export default function (sequelize, DataTypes) {
|
||||||
|
const File = sequelize.define('file', {
|
||||||
|
id: { type: DataTypes.STRING(12), primaryKey: true }, // eslint-disable-line new-cap
|
||||||
|
name: DataTypes.TEXT,
|
||||||
|
originalName: DataTypes.TEXT,
|
||||||
|
size: DataTypes.BIGINT,
|
||||||
|
downloads: DataTypes.BIGINT,
|
||||||
|
accessedAt: DataTypes.DATE,
|
||||||
|
deletedAt: DataTypes.DATE,
|
||||||
|
processed: DataTypes.BOOLEAN,
|
||||||
|
type: DataTypes.ENUM( // eslint-disable-line new-cap
|
||||||
|
'image',
|
||||||
|
'audio',
|
||||||
|
'video',
|
||||||
|
'archive',
|
||||||
|
'other'
|
||||||
|
),
|
||||||
|
width: DataTypes.INTEGER,
|
||||||
|
height: DataTypes.INTEGER,
|
||||||
|
ip: 'inet',
|
||||||
|
legacyId: DataTypes.STRING(12), // eslint-disable-line new-cap
|
||||||
|
md5: DataTypes.STRING(32), // eslint-disable-line new-cap
|
||||||
|
malwarePositives: DataTypes.INTEGER,
|
||||||
|
mongoId: DataTypes.STRING,
|
||||||
|
}, {
|
||||||
|
paranoid: true,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ['userId'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
classMethods: {
|
||||||
|
accessed: (id) => sequelize.query(`
|
||||||
|
UPDATE files
|
||||||
|
SET "downloads" = downloads + 1, "accessedAt" = NOW()
|
||||||
|
WHERE "id" = :id`,
|
||||||
|
{
|
||||||
|
replacements: { id },
|
||||||
|
type: sequelize.QueryTypes.UPDATE,
|
||||||
|
}),
|
||||||
|
associate: (models) => {
|
||||||
|
File.belongsTo(models.user);
|
||||||
|
File.hasOne(models.malware);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return File;
|
||||||
|
}
|
34
models/index.js
Normal file
34
models/index.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sequelize = new Sequelize(process.env.DATABASE_URL, config);
|
||||||
|
const db = {};
|
||||||
|
|
||||||
|
fs
|
||||||
|
.readdirSync(__dirname)
|
||||||
|
.filter((file) => (file.indexOf('.') !== 0) && (file !== 'index.js'))
|
||||||
|
.forEach((file) => {
|
||||||
|
const model = sequelize.import(path.join(__dirname, file));
|
||||||
|
db[model.name] = model;
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(db).forEach((modelName) => {
|
||||||
|
if ('associate' in db[modelName]) {
|
||||||
|
db[modelName].associate(db);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
db.sequelize = sequelize;
|
||||||
|
db.Sequelize = Sequelize;
|
||||||
|
|
||||||
|
export default db;
|
20
models/login.js
Normal file
20
models/login.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
export default function (sequelize, DataTypes) {
|
||||||
|
const Login = sequelize.define('login', {
|
||||||
|
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||||
|
successful: { type: DataTypes.BOOLEAN },
|
||||||
|
ip: { type: 'inet' },
|
||||||
|
}, {
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ['ip'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
classMethods: {
|
||||||
|
associate: (models) => {
|
||||||
|
Login.belongsTo(models.user);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return Login;
|
||||||
|
}
|
15
models/malware.js
Normal file
15
models/malware.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export default function (sequelize, DataTypes) {
|
||||||
|
const Malware = sequelize.define('malware', {
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return Malware;
|
||||||
|
}
|
13
models/remember.js
Normal file
13
models/remember.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return Remember;
|
||||||
|
}
|
13
models/reset.js
Normal file
13
models/reset.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return Reset;
|
||||||
|
}
|
23
models/transaction.js
Normal file
23
models/transaction.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
export default function (sequelize, DataTypes) {
|
||||||
|
const Transaction = sequelize.define('transaction', {
|
||||||
|
uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4 },
|
||||||
|
amount: DataTypes.DECIMAL,
|
||||||
|
description: DataTypes.STRING,
|
||||||
|
type: DataTypes.ENUM('direct', 'paypal'), // eslint-disable-line new-cap
|
||||||
|
ip: 'inet',
|
||||||
|
data: DataTypes.JSON,
|
||||||
|
}, {
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ['userId'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
classMethods: {
|
||||||
|
associate: (models) => {
|
||||||
|
Transaction.belongsTo(models.user);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return Transaction;
|
||||||
|
}
|
29
models/user.js
Normal file
29
models/user.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
export default function (sequelize, DataTypes) {
|
||||||
|
const User = sequelize.define('user', {
|
||||||
|
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||||
|
email: DataTypes.STRING,
|
||||||
|
password: DataTypes.STRING,
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
plan: DataTypes.ENUM('Free', 'Pro'), // eslint-disable-line new-cap
|
||||||
|
activated: DataTypes.BOOLEAN,
|
||||||
|
banned: DataTypes.BOOLEAN,
|
||||||
|
deletedAt: DataTypes.DATE,
|
||||||
|
mongoId: DataTypes.STRING,
|
||||||
|
}, {
|
||||||
|
paranoid: true,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ['email'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
classMethods: {
|
||||||
|
associate: (models) => {
|
||||||
|
User.hasMany(models.file);
|
||||||
|
User.hasMany(models.transaction);
|
||||||
|
User.hasOne(models.activation);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return User;
|
||||||
|
}
|
|
@ -19,7 +19,7 @@
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"start": "node -r babel-register app.js",
|
"start": "node -r babel-register app.js",
|
||||||
"test": "npm run test-seed && mocha -r babel-register test/**/*.spec.js",
|
"test": "npm run test-seed && mocha -r babel-register test/**/*.spec.js",
|
||||||
"test-seed": "node test/fixtures/mongo-user.js && node test/fixtures/mongo-file.js",
|
"test-seed": "babel-node test/fixtures/user.js",
|
||||||
"watch": "parallelshell \"npm run watch-js\" \"npm run watch-sass\" \"npm run watch-server\"",
|
"watch": "parallelshell \"npm run watch-js\" \"npm run watch-sass\" \"npm run watch-server\"",
|
||||||
"watch-js": "babel -Dw -m system -d web/public/build web/public/src",
|
"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 app.js",
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
"koa-csrf": "^2.5.0",
|
"koa-csrf": "^2.5.0",
|
||||||
"koa-error": "^2.1.0",
|
"koa-error": "^2.1.0",
|
||||||
"koa-favicon": "~1.2.0",
|
"koa-favicon": "~1.2.0",
|
||||||
"koa-generic-session": "^1.10.2",
|
"koa-generic-session": "^1.11.0",
|
||||||
"koa-helmet": "^1.0.0",
|
"koa-helmet": "^1.0.0",
|
||||||
"koa-logger": "~1.3.0",
|
"koa-logger": "~1.3.0",
|
||||||
"koa-redis": "^2.1.1",
|
"koa-redis": "^2.1.1",
|
||||||
|
@ -74,10 +74,13 @@
|
||||||
"raven": "^0.11.0",
|
"raven": "^0.11.0",
|
||||||
"redis": "^2.6.1",
|
"redis": "^2.6.1",
|
||||||
"sendgrid": "^2.0.0",
|
"sendgrid": "^2.0.0",
|
||||||
|
"sequelize": "^3.23.3",
|
||||||
|
"sequelize-classes": "^0.1.12",
|
||||||
"ssh2": "^0.5.0",
|
"ssh2": "^0.5.0",
|
||||||
"statsy": "~0.2.0",
|
"statsy": "~0.2.0",
|
||||||
"stripe": "^4.7.0",
|
"stripe": "^4.7.0",
|
||||||
"swig": "~1.4.2"
|
"swig": "~1.4.2",
|
||||||
|
"validate-ip": "^1.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-eslint": "^6.0.4",
|
"babel-eslint": "^6.0.4",
|
||||||
|
|
34
test/fixtures/user.js
vendored
Normal file
34
test/fixtures/user.js
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import co from 'co';
|
||||||
|
|
||||||
|
import models from '../../models';
|
||||||
|
|
||||||
|
import debugname from 'debug';
|
||||||
|
const debug = debugname('hostr:db');
|
||||||
|
|
||||||
|
function *createUser() {
|
||||||
|
const user = yield models.user.create({
|
||||||
|
'email': 'test@hostr.co',
|
||||||
|
'password': '$pbkdf2-256-1$2$kBhIDRqFwnF/1ms6ZHfME2o2$a48e8c350d26397fcc88bf0a7a2817b1cdcd1ffffe0521a5',
|
||||||
|
'ip': '127.0.0.1',
|
||||||
|
'plan': 'Free',
|
||||||
|
'activated': true,
|
||||||
|
});
|
||||||
|
yield user.save();
|
||||||
|
yield models.sequelize.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
co(function *sync() {
|
||||||
|
debug('Syncing schema');
|
||||||
|
yield models.sequelize.sync();
|
||||||
|
debug('Schema synced');
|
||||||
|
const user = yield models.user.findOne({
|
||||||
|
where: {
|
||||||
|
email: 'test@hostr.co',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (user) {
|
||||||
|
yield user.destroy();
|
||||||
|
}
|
||||||
|
debug('Creating test user');
|
||||||
|
yield createUser();
|
||||||
|
});
|
|
@ -8,7 +8,6 @@ import errors from 'koa-error';
|
||||||
import * as redis from '../lib/redis';
|
import * as redis from '../lib/redis';
|
||||||
import * as index from './routes/index';
|
import * as index from './routes/index';
|
||||||
import * as file from './routes/file';
|
import * as file from './routes/file';
|
||||||
import * as pro from './routes/pro';
|
|
||||||
import * as user from './routes/user';
|
import * as user from './routes/user';
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
@ -67,9 +66,6 @@ router.get('/pricing', index.staticPage);
|
||||||
router.get('/apps', index.staticPage);
|
router.get('/apps', index.staticPage);
|
||||||
router.get('/stats', index.staticPage);
|
router.get('/stats', index.staticPage);
|
||||||
|
|
||||||
router.post('/pro/create', pro.create);
|
|
||||||
router.post('/pro/cancel', pro.cancel);
|
|
||||||
|
|
||||||
router.get('/:id', file.landing);
|
router.get('/:id', file.landing);
|
||||||
router.get('/file/:id/:name', file.get);
|
router.get('/file/:id/:name', file.get);
|
||||||
router.get('/file/:size/:id/:name', file.get);
|
router.get('/file/:size/:id/:name', file.get);
|
||||||
|
|
158
web/lib/auth.js
158
web/lib/auth.js
|
@ -1,8 +1,9 @@
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import { join } from 'path';
|
||||||
import passwords from 'passwords';
|
import passwords from 'passwords';
|
||||||
import uuid from 'node-uuid';
|
import uuid from 'node-uuid';
|
||||||
import views from 'co-views';
|
import views from 'co-views';
|
||||||
import { join } from 'path';
|
import models from '../../models';
|
||||||
const render = views(join(__dirname, '..', 'views'), { default: 'ejs' });
|
const render = views(join(__dirname, '..', 'views'), { default: 'ejs' });
|
||||||
import debugname from 'debug';
|
import debugname from 'debug';
|
||||||
const debug = debugname('hostr-web:auth');
|
const debug = debugname('hostr-web:auth');
|
||||||
|
@ -13,64 +14,67 @@ const from = process.env.EMAIL_FROM;
|
||||||
const fromname = process.env.EMAIL_NAME;
|
const fromname = process.env.EMAIL_NAME;
|
||||||
|
|
||||||
export function* authenticate(email, password) {
|
export function* authenticate(email, password) {
|
||||||
const Users = this.db.Users;
|
|
||||||
const Logins = this.db.Logins;
|
|
||||||
const remoteIp = this.headers['x-real-ip'] || this.ip;
|
const remoteIp = this.headers['x-real-ip'] || this.ip;
|
||||||
|
|
||||||
if (!password || password.length < 6) {
|
if (!password || password.length < 6) {
|
||||||
debug('No password, or password too short');
|
debug('No password, or password too short');
|
||||||
return new Error('Invalid login details');
|
return new Error('Invalid login details');
|
||||||
}
|
}
|
||||||
const count = yield Logins.count({
|
const count = yield models.login.count({
|
||||||
|
where: {
|
||||||
ip: remoteIp,
|
ip: remoteIp,
|
||||||
successful: false,
|
successful: false,
|
||||||
at: { $gt: Math.ceil(Date.now() / 1000) - 600 },
|
createdAt: {
|
||||||
|
$gt: Math.ceil(Date.now()) - 600000,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (count > 25) {
|
if (count > 25) {
|
||||||
debug('Throttling brute force');
|
debug('Throttling brute force');
|
||||||
return new Error('Invalid login details');
|
return new Error('Invalid login details');
|
||||||
}
|
}
|
||||||
const login = { ip: remoteIp, at: Math.ceil(Date.now() / 1000), successful: null };
|
const user = yield models.user.findOne({
|
||||||
yield Logins.save(login);
|
where: {
|
||||||
const user = yield Users.findOne({
|
|
||||||
email: email.toLowerCase(),
|
email: email.toLowerCase(),
|
||||||
banned: { $exists: false }, status: { $ne: 'deleted' },
|
activated: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (user) {
|
debug(user);
|
||||||
const verified = yield passwords.verify(password, user.salted_password);
|
const login = yield models.login.create({
|
||||||
if (verified) {
|
ip: remoteIp,
|
||||||
|
successful: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user && user.password) {
|
||||||
|
if (yield passwords.verify(password, user.password)) {
|
||||||
debug('Password verified');
|
debug('Password verified');
|
||||||
login.successful = true;
|
login.successful = true;
|
||||||
yield Logins.updateOne({ _id: login._id }, login);
|
yield login.save();
|
||||||
|
debug(user);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
debug('Password invalid');
|
debug('Password invalid');
|
||||||
login.successful = false;
|
login.userId = user.id;
|
||||||
yield Logins.updateOne({ _id: login._id }, login);
|
|
||||||
} else {
|
|
||||||
debug('Email invalid');
|
|
||||||
login.successful = false;
|
|
||||||
yield Logins.updateOne({ _id: login._id }, login);
|
|
||||||
}
|
}
|
||||||
return new Error('Invalid login details');
|
yield login.save();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function* setupSession(user) {
|
export function* setupSession(user) {
|
||||||
debug('Setting up session');
|
debug('Setting up session');
|
||||||
const token = uuid.v4();
|
const token = uuid.v4();
|
||||||
yield this.redis.set(token, user._id, 'EX', 604800);
|
yield this.redis.set(token, user.id, 'EX', 604800);
|
||||||
|
|
||||||
const sessionUser = {
|
const sessionUser = {
|
||||||
id: user._id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
dailyUploadAllowance: 15,
|
dailyUploadAllowance: 15,
|
||||||
maxFileSize: 20971520,
|
maxFileSize: 20971520,
|
||||||
joined: user.joined,
|
joined: user.createdAt,
|
||||||
plan: user.type || 'Free',
|
plan: user.plan,
|
||||||
uploadsToday: yield this.db.Files.count({
|
uploadsToday: yield models.file.count({ userId: user.id }),
|
||||||
owner: user._id, time_added: { $gt: Math.ceil(Date.now() / 1000) - 86400 },
|
|
||||||
}),
|
|
||||||
md5: crypto.createHash('md5').update(user.email).digest('hex'),
|
md5: crypto.createHash('md5').update(user.email).digest('hex'),
|
||||||
token,
|
token,
|
||||||
};
|
};
|
||||||
|
@ -82,39 +86,50 @@ export function* setupSession(user) {
|
||||||
|
|
||||||
this.session.user = sessionUser;
|
this.session.user = sessionUser;
|
||||||
if (this.request.body.remember && this.request.body.remember === 'on') {
|
if (this.request.body.remember && this.request.body.remember === 'on') {
|
||||||
const Remember = this.db.Remember;
|
const remember = yield models.remember.create({
|
||||||
const rememberToken = uuid();
|
id: uuid(),
|
||||||
Remember.save({ _id: rememberToken, user_id: user.id, created: new Date().getTime() });
|
userId: user.id,
|
||||||
this.cookies.set('r', rememberToken, { maxAge: 1209600000, httpOnly: true });
|
});
|
||||||
|
this.cookies.set('r', remember.id, { maxAge: 1209600000, httpOnly: true });
|
||||||
}
|
}
|
||||||
debug('Session set up');
|
debug('Session set up');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function* signup(email, password, ip) {
|
export function* signup(email, password, ip) {
|
||||||
const Users = this.db.Users;
|
const existingUser = yield models.user.findOne({
|
||||||
const existingUser = yield Users.findOne({ email, status: { $ne: 'deleted' } });
|
where: {
|
||||||
|
email,
|
||||||
|
activated: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
debug('Email already in use.');
|
debug('Email already in use.');
|
||||||
throw new Error('Email already in use.');
|
throw new Error('Email already in use.');
|
||||||
}
|
}
|
||||||
const cryptedPassword = yield passwords.crypt(password);
|
const cryptedPassword = yield passwords.crypt(password);
|
||||||
const user = {
|
const user = yield models.user.create({
|
||||||
email,
|
email,
|
||||||
salted_password: cryptedPassword,
|
password: cryptedPassword,
|
||||||
joined: Math.round(new Date().getTime() / 1000),
|
ip,
|
||||||
signup_ip: ip,
|
plan: 'Free',
|
||||||
activationCode: uuid(),
|
activation: {
|
||||||
};
|
id: uuid(),
|
||||||
Users.insertOne(user);
|
email,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
include: [models.activation],
|
||||||
|
});
|
||||||
|
|
||||||
|
yield user.save();
|
||||||
|
|
||||||
const html = yield render('email/inlined/activate', {
|
const html = yield render('email/inlined/activate', {
|
||||||
activationUrl: `${process.env.WEB_BASE_URL}/activate/${user.activationCode}`,
|
activationUrl: `${process.env.WEB_BASE_URL}/activate/${user.activation.id}`,
|
||||||
});
|
});
|
||||||
const text = `Thanks for signing up to Hostr!
|
const text = `Thanks for signing up to Hostr!
|
||||||
Please confirm your email address by clicking the link below.
|
Please confirm your email address by clicking the link below.
|
||||||
|
|
||||||
${process.env.WEB_BASE_URL}/activate/${user.activationCode}
|
${process.env.WEB_BASE_URL}/activate/${user.activation.id}
|
||||||
|
|
||||||
— Jonathan Cremin, Hostr Founder
|
— Jonathan Cremin, Hostr Founder
|
||||||
`;
|
`;
|
||||||
|
@ -132,21 +147,21 @@ ${process.env.WEB_BASE_URL}/activate/${user.activationCode}
|
||||||
|
|
||||||
|
|
||||||
export function* sendResetToken(email) {
|
export function* sendResetToken(email) {
|
||||||
const Users = this.db.Users;
|
const user = yield models.user.findOne({
|
||||||
const Reset = this.db.Reset;
|
where: {
|
||||||
const user = yield Users.findOne({ email });
|
email,
|
||||||
|
},
|
||||||
|
});
|
||||||
if (user) {
|
if (user) {
|
||||||
const token = uuid.v4();
|
const reset = yield models.reset.create({
|
||||||
Reset.save({
|
id: uuid.v4(),
|
||||||
_id: user._id,
|
userId: user.id,
|
||||||
created: Math.round(new Date().getTime() / 1000),
|
|
||||||
token,
|
|
||||||
});
|
});
|
||||||
const html = yield render('email/inlined/forgot', {
|
const html = yield render('email/inlined/forgot', {
|
||||||
forgotUrl: `${process.env.WEB_BASE_URL}/forgot/${token}`,
|
forgotUrl: `${process.env.WEB_BASE_URL}/forgot/${reset.id}`,
|
||||||
});
|
});
|
||||||
const text = `It seems you've forgotten your password :(
|
const text = `It seems you've forgotten your password :(
|
||||||
Visit ${process.env.WEB_BASE_URL}/forgot/${token} to set a new one.
|
Visit ${process.env.WEB_BASE_URL}/forgot/${reset.id} to set a new one.
|
||||||
`;
|
`;
|
||||||
const mail = new sendgrid.Email({
|
const mail = new sendgrid.Email({
|
||||||
to: user.email,
|
to: user.email,
|
||||||
|
@ -165,38 +180,43 @@ Visit ${process.env.WEB_BASE_URL}/forgot/${token} to set a new one.
|
||||||
|
|
||||||
|
|
||||||
export function* fromToken(token) {
|
export function* fromToken(token) {
|
||||||
const Users = this.db.Users;
|
const userId = yield this.redis.get(token);
|
||||||
const reply = yield this.redis.get(token);
|
return yield models.user.findById(userId);
|
||||||
return yield Users.findOne({ _id: reply });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function* fromCookie(cookie) {
|
export function* fromCookie(rememberId) {
|
||||||
const Remember = this.db.Remember;
|
const userId = yield models.remember.findById(rememberId);
|
||||||
const Users = this.db.Users;
|
return yield models.user.findById(userId);
|
||||||
const remember = yield Remember.findOne({ _id: cookie });
|
|
||||||
return yield Users.findOne({ _id: remember.user_id });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function* validateResetToken() {
|
export function* validateResetToken(resetId) {
|
||||||
const Reset = this.db.Reset;
|
return yield models.reset.findById(resetId);
|
||||||
return yield Reset.findOne({ token: this.params.token });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function* updatePassword(userId, password) {
|
export function* updatePassword(userId, password) {
|
||||||
const Users = this.db.Users;
|
|
||||||
const cryptedPassword = yield passwords.crypt(password);
|
const cryptedPassword = yield passwords.crypt(password);
|
||||||
yield Users.updateOne({ _id: userId }, { $set: { salted_password: cryptedPassword } });
|
const user = yield models.user.findById(userId);
|
||||||
|
user.password = cryptedPassword;
|
||||||
|
yield user.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function* activateUser(code) {
|
export function* activateUser(code) {
|
||||||
const Users = this.db.Users;
|
debug(code);
|
||||||
const user = yield Users.findOne({ activationCode: code });
|
const activation = yield models.activation.findOne({
|
||||||
if (user) {
|
where: {
|
||||||
Users.updateOne({ _id: user._id }, { $unset: { activationCode: '' } });
|
id: code,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (activation.updatedAt.getTime() === activation.createdAt.getTime()) {
|
||||||
|
activation.activated = true;
|
||||||
|
yield activation.save();
|
||||||
|
const user = yield activation.getUser();
|
||||||
|
user.activated = true;
|
||||||
|
yield user.save();
|
||||||
yield setupSession.call(this, user);
|
yield setupSession.call(this, user);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,7 @@ export class ProController {
|
||||||
$scope.user = UserService.get();
|
$scope.user = UserService.get();
|
||||||
$scope.header = 'full';
|
$scope.header = 'full';
|
||||||
$scope.cancel = () => {
|
$scope.cancel = () => {
|
||||||
$http.post('/pro/cancel').success(() => {
|
$http.delete(window.settings.apiURL + '/user/pro').success(() => {
|
||||||
window.location.reload(true);
|
window.location.reload(true);
|
||||||
}).error((data) => {
|
}).error((data) => {
|
||||||
console.error(new Error(data));
|
console.error(new Error(data));
|
||||||
|
|
|
@ -69,7 +69,7 @@ export function stripeSubscribe($http) {
|
||||||
key: window.settings.stripePublic,
|
key: window.settings.stripePublic,
|
||||||
image: '/images/stripe-128.png',
|
image: '/images/stripe-128.png',
|
||||||
token: (token) => {
|
token: (token) => {
|
||||||
$http.post('/pro/create', {
|
$http.post(window.settings.apiURL + '/user/pro', {
|
||||||
stripeToken: token,
|
stripeToken: token,
|
||||||
})
|
})
|
||||||
.success((data) => {
|
.success((data) => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import mime from 'mime-types';
|
import mime from 'mime-types';
|
||||||
|
import models from '../../models';
|
||||||
import hostrFileStream from '../../lib/hostr-file-stream';
|
import hostrFileStream from '../../lib/hostr-file-stream';
|
||||||
import { formatFile } from '../../lib/format';
|
import { formatFile } from '../../lib/format';
|
||||||
|
|
||||||
|
@ -32,36 +33,37 @@ export function* get() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = yield this.db.Files.findOne({
|
const file = yield models.file.findOne({
|
||||||
_id: this.params.id,
|
where: {
|
||||||
file_name: this.params.name,
|
id: this.params.id,
|
||||||
status: 'active',
|
name: this.params.name,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
this.assert(file, 404);
|
this.assert(file, 404);
|
||||||
|
|
||||||
if (!hotlinkCheck(file, this.headers['user-agent'], this.headers.referer)) {
|
if (!hotlinkCheck(file, this.headers['user-agent'], this.headers.referer)) {
|
||||||
this.redirect(`/${file._id}`);
|
this.redirect(`/${file.id}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file.width && this.request.query.warning !== 'on') {
|
if (!file.width && this.request.query.warning !== 'on') {
|
||||||
this.redirect(`/${file._id}`);
|
this.redirect(`/${file.id}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.malware) {
|
if (file.malware) {
|
||||||
const alert = this.request.query.alert;
|
const alert = this.request.query.alert;
|
||||||
if (!alert || !alert.match(/i want to download malware/i)) {
|
if (!alert || !alert.match(/i want to download malware/i)) {
|
||||||
this.redirect(`/${file._id}`);
|
this.redirect(`/${file.id}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let localPath = join(storePath, file._id[0], `${file._id}_${file.file_name}`);
|
let localPath = join(storePath, file.id[0], `${file.id}_${file.name}`);
|
||||||
let remotePath = join(file._id[0], `${file._id}_${file.file_name}`);
|
let remotePath = join(file.id[0], `${file.id}_${file.name}`);
|
||||||
if (this.params.size > 0) {
|
if (this.params.size > 0) {
|
||||||
localPath = join(storePath, file._id[0], this.params.size, `${file._id}_${file.file_name}`);
|
localPath = join(storePath, file.id[0], this.params.size, `${file.id}_${file.name}`);
|
||||||
remotePath = join(file._id[0], this.params.size, `${file._id}_${file.file_name}`);
|
remotePath = join(file.id[0], this.params.size, `${file.id}_${file.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.malware) {
|
if (file.malware) {
|
||||||
|
@ -73,13 +75,13 @@ export function* get() {
|
||||||
if (this.params.size) {
|
if (this.params.size) {
|
||||||
this.statsd.incr('file.view', 1);
|
this.statsd.incr('file.view', 1);
|
||||||
}
|
}
|
||||||
type = mime.lookup(file.file_name);
|
type = mime.lookup(file.name);
|
||||||
} else {
|
} else {
|
||||||
this.statsd.incr('file.download', 1);
|
this.statsd.incr('file.download', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userAgentCheck(this.headers['user-agent'])) {
|
if (userAgentCheck(this.headers['user-agent'])) {
|
||||||
this.set('Content-Disposition', `attachment; filename=${file.file_name}`);
|
this.set('Content-Disposition', `attachment; filename=${file.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set('Content-type', type);
|
this.set('Content-type', type);
|
||||||
|
@ -87,10 +89,7 @@ export function* get() {
|
||||||
this.set('Cache-control', 'max-age=2592000');
|
this.set('Cache-control', 'max-age=2592000');
|
||||||
|
|
||||||
if (!this.params.size || (this.params.size && this.params.size > 150)) {
|
if (!this.params.size || (this.params.size && this.params.size > 150)) {
|
||||||
this.db.Files.updateOne(
|
models.file.accessed(file.id);
|
||||||
{ _id: file._id },
|
|
||||||
{ $set: { last_accessed: Math.ceil(Date.now() / 1000) }, $inc: { downloads: 1 } },
|
|
||||||
{ w: 0 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.body = yield hostrFileStream(localPath, remotePath);
|
this.body = yield hostrFileStream(localPath, remotePath);
|
||||||
|
@ -101,10 +100,14 @@ export function* resized() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* landing() {
|
export function* landing() {
|
||||||
const file = yield this.db.Files.findOne({ _id: this.params.id, status: 'active' });
|
const file = yield models.file.findOne({
|
||||||
|
where: {
|
||||||
|
id: this.params.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
this.assert(file, 404);
|
this.assert(file, 404);
|
||||||
if (userAgentCheck(this.headers['user-agent'])) {
|
if (userAgentCheck(this.headers['user-agent'])) {
|
||||||
this.params.name = file.file_name;
|
this.params.name = file.name;
|
||||||
yield get.call(this);
|
yield get.call(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
authenticate, setupSession, signup as signupUser, activateUser, sendResetToken,
|
authenticate, setupSession, signup as signupUser, activateUser, sendResetToken,
|
||||||
validateResetToken, updatePassword,
|
validateResetToken, updatePassword,
|
||||||
} from '../lib/auth';
|
} from '../lib/auth';
|
||||||
|
import models from '../../models';
|
||||||
import debugname from 'debug';
|
import debugname from 'debug';
|
||||||
const debug = debugname('hostr-web:user');
|
const debug = debugname('hostr-web:user');
|
||||||
|
|
||||||
|
@ -14,13 +15,14 @@ export function* signin() {
|
||||||
this.statsd.incr('auth.attempt', 1);
|
this.statsd.incr('auth.attempt', 1);
|
||||||
this.assertCSRF(this.request.body);
|
this.assertCSRF(this.request.body);
|
||||||
const user = yield authenticate.call(this, this.request.body.email, this.request.body.password);
|
const user = yield authenticate.call(this, this.request.body.email, this.request.body.password);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
this.statsd.incr('auth.failure', 1);
|
this.statsd.incr('auth.failure', 1);
|
||||||
yield this.render('signin', { error: 'Invalid login details', csrf: this.csrf });
|
yield this.render('signin', { error: 'Invalid login details', csrf: this.csrf });
|
||||||
return;
|
return;
|
||||||
} else if (user.activationCode) {
|
} else if (user.activationCode) {
|
||||||
yield this.render('signin', {
|
yield this.render('signin', {
|
||||||
error: 'Your account hasn\'t been activated yet. Check your for an activation email.',
|
error: 'Your account hasn\'t been activated yet. Check for an activation email.',
|
||||||
csrf: this.csrf,
|
csrf: this.csrf,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -69,8 +71,6 @@ export function* signup() {
|
||||||
|
|
||||||
|
|
||||||
export function* forgot() {
|
export function* forgot() {
|
||||||
const Reset = this.db.Reset;
|
|
||||||
const Users = this.db.Users;
|
|
||||||
const token = this.params.token;
|
const token = this.params.token;
|
||||||
|
|
||||||
if (this.request.body.password) {
|
if (this.request.body.password) {
|
||||||
|
@ -83,16 +83,17 @@ export function* forgot() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.assertCSRF(this.request.body);
|
this.assertCSRF(this.request.body);
|
||||||
const tokenUser = yield validateResetToken.call(this, token);
|
const user = yield validateResetToken(token);
|
||||||
const userId = tokenUser._id;
|
if (user) {
|
||||||
yield updatePassword.call(this, userId, this.request.body.password);
|
yield updatePassword(user.userId, this.request.body.password);
|
||||||
yield Reset.deleteOne({ _id: userId });
|
const reset = yield models.reset.findById(token);
|
||||||
const user = yield Users.findOne({ _id: userId });
|
//reset.destroy();
|
||||||
yield setupSession.call(this, user);
|
yield setupSession.call(this, user);
|
||||||
this.statsd.incr('auth.reset.success', 1);
|
this.statsd.incr('auth.reset.success', 1);
|
||||||
this.redirect('/');
|
this.redirect('/');
|
||||||
|
}
|
||||||
} else if (token) {
|
} else if (token) {
|
||||||
const tokenUser = yield validateResetToken.call(this, token);
|
const tokenUser = yield validateResetToken(token);
|
||||||
if (!tokenUser) {
|
if (!tokenUser) {
|
||||||
this.statsd.incr('auth.reset.fail', 1);
|
this.statsd.incr('auth.reset.fail', 1);
|
||||||
yield this.render('forgot', {
|
yield this.render('forgot', {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue