More changes for db migration
This commit is contained in:
parent
de0284e48a
commit
889dc02945
33 changed files with 740 additions and 100 deletions
|
@ -30,3 +30,4 @@ cache:
|
|||
untracked: true
|
||||
paths:
|
||||
- node_modules
|
||||
- web/public/jspm_packages
|
||||
|
|
|
@ -5,6 +5,7 @@ import StatsD from 'statsy';
|
|||
import auth from './lib/auth';
|
||||
import * as user from './routes/user';
|
||||
import * as file from './routes/file';
|
||||
import * as pro from './routes/pro';
|
||||
import debugname from 'debug';
|
||||
const debug = debugname('hostr-api');
|
||||
|
||||
|
@ -64,10 +65,11 @@ router.get('/user/token', auth, user.token);
|
|||
router.get('/token', auth, user.token);
|
||||
router.get('/user/transaction', auth, user.transaction);
|
||||
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.post('/file', auth, file.post);
|
||||
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);
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ export default function* (next) {
|
|||
ip: remoteIp,
|
||||
successful: false,
|
||||
createdAt: {
|
||||
$gt: new Date(Math.ceil(Date.now()) - 600000),
|
||||
$gt: new Date(Date.now() - 600000),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -71,7 +71,7 @@ export default function* (next) {
|
|||
where: {
|
||||
userId: user.id,
|
||||
createdAt: {
|
||||
$gt: Math.ceil(Date.now() / 1000) - 86400,
|
||||
$gt: Date.now() - 86400000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -54,7 +54,7 @@ export function* list() {
|
|||
const files = yield models.file.findAll({
|
||||
where: {
|
||||
userId: this.user.id,
|
||||
status: 'active',
|
||||
processed: true,
|
||||
},
|
||||
order: '"createdAt" DESC',
|
||||
offset,
|
||||
|
@ -70,9 +70,6 @@ export function* get() {
|
|||
const file = yield models.file.findOne({
|
||||
where: {
|
||||
id: this.params.id,
|
||||
status: {
|
||||
$in: ['active', 'uploading'],
|
||||
},
|
||||
},
|
||||
});
|
||||
this.assert(file, 404, '{"error": {"message": "File not found", "code": 604}}');
|
||||
|
@ -83,20 +80,6 @@ export function* get() {
|
|||
}
|
||||
|
||||
|
||||
export function* put() {
|
||||
if (this.request.body.trashed) {
|
||||
const file = yield models.file.findOne({
|
||||
where: {
|
||||
id: this.params.id,
|
||||
userId: this.user.id,
|
||||
},
|
||||
});
|
||||
file.status = this.request.body.trashed ? 'trashed' : 'active';
|
||||
yield file.save();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function* del() {
|
||||
const file = yield models.file.findOne({
|
||||
where: {
|
||||
|
@ -105,8 +88,7 @@ export function* del() {
|
|||
},
|
||||
});
|
||||
this.assert(file, 401, '{"error": {"message": "File not found", "code": 604}}');
|
||||
file.status = 'deleted';
|
||||
yield file.save();
|
||||
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));
|
||||
|
|
|
@ -6,18 +6,20 @@ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
|||
import sendgridInit from 'sendgrid';
|
||||
const sendgrid = sendgridInit(process.env.SENDGRID_KEY);
|
||||
|
||||
import models from '../../models';
|
||||
|
||||
const from = process.env.EMAIL_FROM;
|
||||
const fromname = process.env.EMAIL_NAME;
|
||||
|
||||
export function* create() {
|
||||
const Users = this.db.Users;
|
||||
const Transactions = this.db.Transactions;
|
||||
const stripeToken = this.request.body.stripeToken;
|
||||
|
||||
const ip = this.request.headers['x-real-ip'] || this.req.connection.remoteAddress;
|
||||
|
||||
const createCustomer = {
|
||||
card: stripeToken.id,
|
||||
plan: 'usd_monthly',
|
||||
email: this.session.email,
|
||||
email: this.user.email,
|
||||
};
|
||||
|
||||
const customer = yield stripe.customers.create(createCustomer);
|
||||
|
@ -26,19 +28,22 @@ export function* create() {
|
|||
|
||||
delete customer.subscriptions;
|
||||
|
||||
yield Users.updateOne({ _id: this.session.user.id },
|
||||
{ $set: { stripe_customer: customer, type: 'Pro' } });
|
||||
const user = yield models.user.findById(this.user.id);
|
||||
user.plan = 'Pro';
|
||||
yield user.save();
|
||||
|
||||
const transaction = {
|
||||
user_id: this.session.user.id,
|
||||
const transaction = yield models.transaction.create({
|
||||
userId: this.user.id,
|
||||
amount: customer.subscription.plan.amount,
|
||||
desc: customer.subscription.plan.name,
|
||||
date: new Date(customer.subscription.plan.created * 1000),
|
||||
};
|
||||
description: customer.subscription.plan.name,
|
||||
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' };
|
||||
|
||||
const html = yield render('email/inlined/pro');
|
||||
|
@ -50,7 +55,7 @@ export function* create() {
|
|||
`;
|
||||
|
||||
const mail = new sendgrid.Email({
|
||||
to: this.session.user.email,
|
||||
to: this.user.email,
|
||||
subject: 'Hostr Pro',
|
||||
from,
|
||||
fromname,
|
||||
|
@ -62,19 +67,19 @@ export function* create() {
|
|||
}
|
||||
|
||||
export function* cancel() {
|
||||
this.assertCSRF();
|
||||
const Users = this.db.Users;
|
||||
const user = yield Users.findOne({ _id: this.session.user.id });
|
||||
const user = yield models.user.findById(this.user.id);
|
||||
const transactions = yield user.getTransactions();
|
||||
const transaction = transactions[0];
|
||||
|
||||
const confirmation = yield stripe.customers.cancelSubscription(
|
||||
user.stripe_customer.id,
|
||||
user.stripe_customer.subscription.id,
|
||||
{ at_period_end: true }
|
||||
yield stripe.customers.cancelSubscription(
|
||||
transaction.data.id,
|
||||
transaction.data.subscription.id,
|
||||
{ at_period_end: false }
|
||||
);
|
||||
|
||||
yield Users.updateOne({ _id: this.session.user.id },
|
||||
{ $set: { 'stripe_customer.subscription': confirmation, type: 'Free' } });
|
||||
user.plan = 'Free';
|
||||
yield user.save();
|
||||
|
||||
this.session.user.plan = 'Pro';
|
||||
this.user.plan = 'Free';
|
||||
this.body = { status: 'inactive' };
|
||||
}
|
|
@ -20,7 +20,11 @@ export function* token() {
|
|||
}
|
||||
|
||||
export function* transaction() {
|
||||
const transactions = yield models.transaction.findAll({ userId: this.user.id });
|
||||
const transactions = yield models.transaction.findAll({
|
||||
where: {
|
||||
userId: this.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
this.body = transactions.map((item) => {
|
||||
return {
|
||||
|
|
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;
|
||||
}
|
||||
}
|
4
app.js
4
app.js
|
@ -8,13 +8,10 @@ import bodyparser from 'koa-bodyparser';
|
|||
import websockify from 'koa-websocket';
|
||||
import helmet from 'koa-helmet';
|
||||
import raven from 'raven';
|
||||
import mongo from './lib/mongo';
|
||||
import * as redis from './lib/redis';
|
||||
import api, { ws } from './api/app';
|
||||
import web from './web/app';
|
||||
|
||||
import models from './models';
|
||||
|
||||
import debugname from 'debug';
|
||||
const debug = debugname('hostr');
|
||||
|
||||
|
@ -52,7 +49,6 @@ app.use(function* errorMiddleware(next) {
|
|||
}
|
||||
});
|
||||
|
||||
app.use(mongo());
|
||||
app.use(redis.middleware());
|
||||
app.use(logger());
|
||||
app.use(compress());
|
||||
|
|
|
@ -22,8 +22,8 @@ export function formatSize(size) {
|
|||
|
||||
export function formatFile(file) {
|
||||
const formattedFile = {
|
||||
added: moment.unix(file.createdAt).format(),
|
||||
readableAdded: formatDate(file.createdAt),
|
||||
added: moment.unix(file.createdAt / 1000).format(),
|
||||
readableAdded: formatDate(file.createdAt / 1000),
|
||||
downloads: file.downloads !== undefined ? file.downloads : 0,
|
||||
href: `${baseURL}/${file.id}`,
|
||||
id: file.id,
|
||||
|
@ -32,7 +32,7 @@ export function formatFile(file) {
|
|||
readableSize: formatSize(file.size),
|
||||
type: sniff(file.name),
|
||||
trashed: (file.status === 'trashed'),
|
||||
status: file.status,
|
||||
status: file.processed === true ? 'active' : 'uploading',
|
||||
};
|
||||
|
||||
if (file.width) {
|
||||
|
|
|
@ -71,7 +71,7 @@ export default function* (file) {
|
|||
}
|
||||
const result = yield virustotal.getFileReport(file.md5);
|
||||
return {
|
||||
positives: result.positives,
|
||||
positive: result.positives >= 5,
|
||||
result,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import debugname from 'debug';
|
|||
const debug = debugname('hostr:mongo');
|
||||
|
||||
/* eslint no-param-reassign: ["error", { "props": false }] */
|
||||
const configuredClient = new Promise((resolve, reject) => {
|
||||
export const mongo = new Promise((resolve, reject) => {
|
||||
debug('Connecting to Mongodb');
|
||||
return MongoClient.connect(process.env.MONGO_URL).then((client) => {
|
||||
debug('Successfully connected to Mongodb');
|
||||
|
@ -25,10 +25,10 @@ const configuredClient = new Promise((resolve, reject) => {
|
|||
debug(e);
|
||||
});
|
||||
|
||||
export default function mongo() {
|
||||
export default function () {
|
||||
return function* dbMiddleware(next) {
|
||||
try {
|
||||
this.db = yield configuredClient;
|
||||
this.db = yield mongo;
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
|
|
|
@ -55,17 +55,17 @@ function scale(path, type, size) {
|
|||
});
|
||||
}
|
||||
|
||||
export default function resize(path, type, currentSize, dim) {
|
||||
export default function resize(path, type, currentSize, newSize) {
|
||||
debug('Resizing');
|
||||
const ratio = 970 / currentSize.width;
|
||||
debug(dim.width, ratio);
|
||||
if (dim.width <= 150) {
|
||||
debug(newSize.width, ratio);
|
||||
if (newSize.width <= 150) {
|
||||
debug('Cover');
|
||||
return cover(path, type, dim);
|
||||
} else if (dim.width >= 970 && ratio < 1) {
|
||||
return cover(path, type, newSize);
|
||||
} else if (newSize.width >= 970 && ratio < 1) {
|
||||
debug('Scale');
|
||||
dim.height = currentSize.height * ratio; // eslint-disable-line no-param-reassign
|
||||
return scale(path, type, dim);
|
||||
newSize.height = currentSize.height * ratio; // eslint-disable-line no-param-reassign
|
||||
return scale(path, type, newSize);
|
||||
}
|
||||
debug('Copy');
|
||||
return fs.readFile(path);
|
||||
|
|
|
@ -135,12 +135,13 @@ export default class Uploader {
|
|||
|
||||
*checkLimit() {
|
||||
const count = yield models.file.count({
|
||||
where: {
|
||||
userId: this.context.user.id,
|
||||
createdAt: {
|
||||
$gt: Math.ceil(Date.now() / 1000) - 86400,
|
||||
$gt: Date.now() - 86400000,
|
||||
},
|
||||
},
|
||||
});
|
||||
debug(count);
|
||||
const userLimit = this.context.user.daily_upload_allowance;
|
||||
const underLimit = (count < userLimit || userLimit === 'unlimited');
|
||||
if (!underLimit) {
|
||||
|
@ -158,6 +159,7 @@ export default class Uploader {
|
|||
*finalise() {
|
||||
this.file.size = this.receivedSize;
|
||||
this.file.status = 'active';
|
||||
this.file.processed = 'true';
|
||||
yield this.file.save();
|
||||
}
|
||||
|
||||
|
|
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 oldId = 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,
|
||||
oldId,
|
||||
});
|
||||
yield newUser.save({ silent: true });
|
||||
}
|
||||
models.sequelize.close();
|
||||
db.close();
|
||||
}).catch((err) => {
|
||||
models.sequelize.close();
|
||||
db.close();
|
||||
debug(err);
|
||||
});
|
80
migrate/migrate-files.js
Normal file
80
migrate/migrate-files.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
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 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;
|
||||
|
||||
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,
|
||||
}, { /* 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 oldId = 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,
|
||||
oldId,
|
||||
}, {
|
||||
include: [models.activation],
|
||||
});
|
||||
yield newUser.save({ silent: true });
|
||||
}
|
||||
models.sequelize.close();
|
||||
db.close();
|
||||
}).catch((err) => {
|
||||
models.sequelize.close();
|
||||
db.close();
|
||||
debug(err);
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
export default function (sequelize, DataTypes) {
|
||||
const Activation = sequelize.define('activation', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
activated: { type: DataTypes.BOOLEAN, defaultValue: false, primaryKey: true },
|
||||
activatedAt: { type: DataTypes.DATE },
|
||||
email: DataTypes.STRING,
|
||||
}, {
|
||||
classMethods: {
|
||||
|
|
|
@ -6,7 +6,8 @@ export default function (sequelize, DataTypes) {
|
|||
size: DataTypes.BIGINT,
|
||||
downloads: DataTypes.BIGINT,
|
||||
accessedAt: DataTypes.DATE,
|
||||
status: DataTypes.ENUM('active', 'uploading', 'deleted'), // eslint-disable-line new-cap
|
||||
deletedAt: DataTypes.DATE,
|
||||
processed: DataTypes.BOOLEAN,
|
||||
type: DataTypes.ENUM( // eslint-disable-line new-cap
|
||||
'image',
|
||||
'audio',
|
||||
|
@ -21,6 +22,7 @@ export default function (sequelize, DataTypes) {
|
|||
md5: DataTypes.STRING(32), // eslint-disable-line new-cap
|
||||
malwarePositives: DataTypes.INTEGER,
|
||||
}, {
|
||||
paranoid: true,
|
||||
indexes: [
|
||||
{
|
||||
fields: ['userId'],
|
||||
|
@ -37,6 +39,7 @@ export default function (sequelize, DataTypes) {
|
|||
}),
|
||||
associate: (models) => {
|
||||
File.belongsTo(models.user);
|
||||
File.hasOne(models.malware);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ export default function (sequelize, DataTypes) {
|
|||
description: DataTypes.STRING,
|
||||
type: DataTypes.ENUM('direct', 'paypal'), // eslint-disable-line new-cap
|
||||
ip: 'inet',
|
||||
data: DataTypes.JSON,
|
||||
}, {
|
||||
indexes: [
|
||||
{
|
||||
|
|
|
@ -3,12 +3,14 @@ export default function (sequelize, DataTypes) {
|
|||
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
|
||||
ip: 'inet',
|
||||
activated: DataTypes.BOOLEAN,
|
||||
banned: DataTypes.BOOLEAN,
|
||||
deleted: DataTypes.BOOLEAN,
|
||||
deletedAt: DataTypes.DATE,
|
||||
oldId: DataTypes.STRING,
|
||||
}, {
|
||||
paranoid: true,
|
||||
indexes: [
|
||||
{
|
||||
fields: ['email'],
|
||||
|
@ -17,6 +19,7 @@ export default function (sequelize, DataTypes) {
|
|||
classMethods: {
|
||||
associate: (models) => {
|
||||
User.hasMany(models.file);
|
||||
User.hasMany(models.transaction);
|
||||
User.hasOne(models.activation);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
"koa-csrf": "^2.5.0",
|
||||
"koa-error": "^2.1.0",
|
||||
"koa-favicon": "~1.2.0",
|
||||
"koa-generic-session": "^1.10.2",
|
||||
"koa-generic-session": "^1.11.0",
|
||||
"koa-helmet": "^1.0.0",
|
||||
"koa-logger": "~1.3.0",
|
||||
"koa-redis": "^2.1.1",
|
||||
|
@ -78,7 +78,8 @@
|
|||
"ssh2": "^0.5.0",
|
||||
"statsy": "~0.2.0",
|
||||
"stripe": "^4.7.0",
|
||||
"swig": "~1.4.2"
|
||||
"swig": "~1.4.2",
|
||||
"validate-ip": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^6.0.4",
|
||||
|
|
11
test/fixtures/mongo-file.js
vendored
Normal file
11
test/fixtures/mongo-file.js
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
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
Normal file
15
test/fixtures/mongo-user.js
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
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();
|
||||
});
|
|
@ -8,7 +8,6 @@ import errors from 'koa-error';
|
|||
import * as redis from '../lib/redis';
|
||||
import * as index from './routes/index';
|
||||
import * as file from './routes/file';
|
||||
import * as pro from './routes/pro';
|
||||
import * as user from './routes/user';
|
||||
|
||||
const router = new Router();
|
||||
|
@ -67,9 +66,6 @@ router.get('/pricing', index.staticPage);
|
|||
router.get('/apps', 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('/file/:id/:name', file.get);
|
||||
router.get('/file/:size/:id/:name', file.get);
|
||||
|
|
|
@ -25,7 +25,7 @@ export function* authenticate(email, password) {
|
|||
ip: remoteIp,
|
||||
successful: false,
|
||||
createdAt: {
|
||||
$gt: Math.ceil(Date.now() / 1000) - 600,
|
||||
$gt: Math.ceil(Date.now()) - 600000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -35,26 +35,30 @@ export function* authenticate(email, password) {
|
|||
return new Error('Invalid login details');
|
||||
}
|
||||
const user = yield models.user.findOne({
|
||||
where: {
|
||||
email: email.toLowerCase(),
|
||||
activated: 'true',
|
||||
activated: true,
|
||||
},
|
||||
});
|
||||
debug(user);
|
||||
const login = yield models.login.create({
|
||||
ip: remoteIp,
|
||||
successful: false,
|
||||
});
|
||||
|
||||
if (user) {
|
||||
if (user && user.password) {
|
||||
if (yield passwords.verify(password, user.password)) {
|
||||
debug('Password verified');
|
||||
login.successful = true;
|
||||
yield login.save();
|
||||
debug(user);
|
||||
return user;
|
||||
}
|
||||
debug('Password invalid');
|
||||
login.userId = user.id;
|
||||
}
|
||||
yield login.save();
|
||||
return new Error('Invalid login details');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
@ -93,7 +97,12 @@ export function* setupSession(user) {
|
|||
|
||||
|
||||
export function* signup(email, password, ip) {
|
||||
const existingUser = yield models.user.findOne({ where: { email, activated: true } });
|
||||
const existingUser = yield models.user.findOne({
|
||||
where: {
|
||||
email,
|
||||
activated: true,
|
||||
},
|
||||
});
|
||||
if (existingUser) {
|
||||
debug('Email already in use.');
|
||||
throw new Error('Email already in use.');
|
||||
|
@ -102,8 +111,8 @@ export function* signup(email, password, ip) {
|
|||
const user = yield models.user.create({
|
||||
email,
|
||||
password: cryptedPassword,
|
||||
created: Math.round(new Date().getTime() / 1000),
|
||||
ip,
|
||||
plan: 'Free',
|
||||
activation: {
|
||||
id: uuid(),
|
||||
email,
|
||||
|
@ -138,7 +147,11 @@ ${process.env.WEB_BASE_URL}/activate/${user.activation.id}
|
|||
|
||||
|
||||
export function* sendResetToken(email) {
|
||||
const user = yield models.user.findOne({ email });
|
||||
const user = yield models.user.findOne({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
if (user) {
|
||||
const reset = yield models.reset.create({
|
||||
id: uuid.v4(),
|
||||
|
@ -168,18 +181,18 @@ 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);
|
||||
return yield models.user.findById(userId);
|
||||
}
|
||||
|
||||
|
||||
export function* fromCookie(rememberId) {
|
||||
const userId = yield models.remember.findById(rememberId);
|
||||
return yield models.user.findbyId(userId);
|
||||
return yield models.user.findById(userId);
|
||||
}
|
||||
|
||||
|
||||
export function* validateResetToken(resetId) {
|
||||
return yield models.reset.findbyId(resetId);
|
||||
return yield models.reset.findById(resetId);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ export class ProController {
|
|||
$scope.user = UserService.get();
|
||||
$scope.header = 'full';
|
||||
$scope.cancel = () => {
|
||||
$http.post('/pro/cancel').success(() => {
|
||||
$http.delete(window.settings.apiURL + '/user/pro').success(() => {
|
||||
window.location.reload(true);
|
||||
}).error((data) => {
|
||||
console.error(new Error(data));
|
||||
|
|
|
@ -69,7 +69,7 @@ export function stripeSubscribe($http) {
|
|||
key: window.settings.stripePublic,
|
||||
image: '/images/stripe-128.png',
|
||||
token: (token) => {
|
||||
$http.post('/pro/create', {
|
||||
$http.post(window.settings.apiURL + '/user/pro', {
|
||||
stripeToken: token,
|
||||
})
|
||||
.success((data) => {
|
||||
|
|
|
@ -37,7 +37,6 @@ export function* get() {
|
|||
where: {
|
||||
id: this.params.id,
|
||||
name: this.params.name,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
this.assert(file, 404);
|
||||
|
@ -104,7 +103,6 @@ export function* landing() {
|
|||
const file = yield models.file.findOne({
|
||||
where: {
|
||||
id: this.params.id,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
this.assert(file, 404);
|
||||
|
|
|
@ -15,13 +15,14 @@ export function* signin() {
|
|||
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);
|
||||
|
||||
if (!user) {
|
||||
this.statsd.incr('auth.failure', 1);
|
||||
yield this.render('signin', { error: 'Invalid login details', csrf: this.csrf });
|
||||
return;
|
||||
} else if (user.activationCode) {
|
||||
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,
|
||||
});
|
||||
return;
|
||||
|
@ -83,11 +84,14 @@ export function* forgot() {
|
|||
}
|
||||
this.assertCSRF(this.request.body);
|
||||
const user = yield validateResetToken(token);
|
||||
yield updatePassword(user.id, this.request.body.password);
|
||||
yield models.reset.deleteById(token);
|
||||
yield setupSession(this, user);
|
||||
if (user) {
|
||||
yield updatePassword(user.userId, this.request.body.password);
|
||||
const reset = yield models.reset.findById(token);
|
||||
//reset.destroy();
|
||||
yield setupSession.call(this, user);
|
||||
this.statsd.incr('auth.reset.success', 1);
|
||||
this.redirect('/');
|
||||
}
|
||||
} else if (token) {
|
||||
const tokenUser = yield validateResetToken(token);
|
||||
if (!tokenUser) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue