Fix linting
This commit is contained in:
parent
553ba9db9a
commit
bb5189c9ed
35 changed files with 157 additions and 866 deletions
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"extends": "airbnb/base",
|
"extends": "airbnb/base",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 6,
|
"ecmaVersion": 2017,
|
||||||
"sourceType": "module",
|
"sourceType": "module",
|
||||||
"ecmaFeatures": {
|
"ecmaFeatures": {
|
||||||
"experimentalObjectRestSpread": true
|
"experimentalObjectRestSpread": true
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"node": true,
|
"node": true,
|
||||||
|
@ -14,5 +14,6 @@
|
||||||
"rules": {
|
"rules": {
|
||||||
"quotes": [2, "single"],
|
"quotes": [2, "single"],
|
||||||
"no-underscore-dangle": [0],
|
"no-underscore-dangle": [0],
|
||||||
|
"no-plusplus": ["error", { "allowForLoopAfterthoughts": true }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
14
.vscode/launch.json
vendored
Normal file
14
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Program",
|
||||||
|
"program": "${workspaceFolder}/app.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
10
api/app.js
10
api/app.js
|
@ -1,12 +1,14 @@
|
||||||
import Router from 'koa-router';
|
import Router from 'koa-router';
|
||||||
import stats from '../lib/koa-statsd';
|
|
||||||
import cors from 'kcors';
|
import cors from 'kcors';
|
||||||
import StatsD from 'statsy';
|
import StatsD from 'statsy';
|
||||||
|
import debugname from 'debug';
|
||||||
|
|
||||||
|
import stats from '../lib/koa-statsd';
|
||||||
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 * as pro from './routes/pro';
|
||||||
import debugname from 'debug';
|
|
||||||
const debug = debugname('hostr-api');
|
const debug = debugname('hostr-api');
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
@ -45,8 +47,7 @@ router.use(async (ctx, next) => {
|
||||||
code: 604,
|
code: 604,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else if (!err.status) {
|
||||||
if (!err.status) {
|
|
||||||
debug(err);
|
debug(err);
|
||||||
if (ctx.raven) {
|
if (ctx.raven) {
|
||||||
ctx.raven.captureError(err);
|
ctx.raven.captureError(err);
|
||||||
|
@ -57,7 +58,6 @@ router.use(async (ctx, next) => {
|
||||||
ctx.body = err.message;
|
ctx.body = err.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
ctx.type = 'application/json';
|
ctx.type = 'application/json';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
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';
|
||||||
|
|
||||||
|
import models from '../../models';
|
||||||
|
|
||||||
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}}';
|
||||||
|
@ -36,8 +38,10 @@ export default async (ctx, next) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.assert(count < 25, 401,
|
ctx.assert(
|
||||||
'{"error": {"message": "Too many incorrect logins.", "code": 608}}');
|
count < 25, 401,
|
||||||
|
'{"error": {"message": "Too many incorrect logins.", "code": 608}}',
|
||||||
|
);
|
||||||
|
|
||||||
user = await models.user.findOne({
|
user = await models.user.findOne({
|
||||||
where: {
|
where: {
|
||||||
|
@ -56,8 +60,10 @@ export default async (ctx, next) => {
|
||||||
ctx.assert(user, 401, badLoginMsg);
|
ctx.assert(user, 401, badLoginMsg);
|
||||||
debug('Checking user is activated');
|
debug('Checking user is activated');
|
||||||
debug(user.activated);
|
debug(user.activated);
|
||||||
ctx.assert(user.activated === true, 401,
|
ctx.assert(
|
||||||
'{"error": {"message": "Account has not been activated.", "code": 603}}');
|
user.activated === true, 401,
|
||||||
|
'{"error": {"message": "Account has not been activated.", "code": 603}}',
|
||||||
|
);
|
||||||
|
|
||||||
login.successful = true;
|
login.successful = true;
|
||||||
await login.save();
|
await login.save();
|
||||||
|
@ -85,9 +91,11 @@ export default async (ctx, next) => {
|
||||||
plan: user.plan,
|
plan: user.plan,
|
||||||
uploads_today: uploadedToday,
|
uploads_today: uploadedToday,
|
||||||
};
|
};
|
||||||
ctx.response.set('Daily-Uploads-Remaining',
|
ctx.response.set(
|
||||||
user.type === 'Pro' ? 'unlimited' : 15 - uploadedToday);
|
'Daily-Uploads-Remaining',
|
||||||
|
user.type === 'Pro' ? 'unlimited' : 15 - uploadedToday,
|
||||||
|
);
|
||||||
ctx.user = normalisedUser;
|
ctx.user = normalisedUser;
|
||||||
debug('Authenticated user: ', ctx.user.email);
|
debug('Authenticated user: ', ctx.user.email);
|
||||||
await next();
|
await next();
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import views from 'co-views';
|
import views from 'co-views';
|
||||||
const render = views(path.join(__dirname, '/../views'), { default: 'ejs' });
|
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
|
||||||
import sendgridInit from 'sendgrid';
|
import sendgridInit from 'sendgrid';
|
||||||
const sendgrid = sendgridInit(process.env.SENDGRID_KEY);
|
|
||||||
|
|
||||||
import models from '../../models';
|
import models from '../../models';
|
||||||
|
|
||||||
|
const render = views(path.join(__dirname, '/../views'), { default: 'ejs' });
|
||||||
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
||||||
|
const sendgrid = sendgridInit(process.env.SENDGRID_KEY);
|
||||||
|
|
||||||
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 async function create(ctx) {
|
export async function create(ctx) {
|
||||||
const stripeToken = ctx.request.body.stripeToken;
|
const { stripeToken } = ctx.request.body;
|
||||||
|
|
||||||
const ip = ctx.request.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress;
|
const ip = ctx.request.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress;
|
||||||
|
|
||||||
|
@ -74,7 +75,7 @@ export async function cancel(ctx) {
|
||||||
await stripe.customers.cancelSubscription(
|
await stripe.customers.cancelSubscription(
|
||||||
transaction.data.id,
|
transaction.data.id,
|
||||||
transaction.data.subscription.id,
|
transaction.data.subscription.id,
|
||||||
{ at_period_end: false }
|
{ at_period_end: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
user.plan = 'Free';
|
user.plan = 'Free';
|
||||||
|
|
|
@ -2,9 +2,10 @@ 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 debugname from 'debug';
|
||||||
|
|
||||||
import models from '../../models';
|
import models from '../../models';
|
||||||
|
|
||||||
import debugname from 'debug';
|
|
||||||
const debug = debugname('hostr-api:user');
|
const debug = debugname('hostr-api:user');
|
||||||
|
|
||||||
const redisUrl = process.env.REDIS_URL;
|
const redisUrl = process.env.REDIS_URL;
|
||||||
|
@ -26,31 +27,37 @@ export async function transaction(ctx) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.body = transactions.map((item) => {
|
ctx.body = transactions.map(item => ({
|
||||||
return {
|
|
||||||
id: item.id,
|
id: item.id,
|
||||||
amount: item.amount / 100,
|
amount: item.amount / 100,
|
||||||
date: item.date,
|
date: item.date,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
};
|
}));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function settings(ctx) {
|
export async function settings(ctx) {
|
||||||
ctx.assert(ctx.request.body, 400,
|
ctx.assert(
|
||||||
'{"error": {"message": "Current Password required to update account.", "code": 612}}');
|
ctx.request.body, 400,
|
||||||
ctx.assert(ctx.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}}');
|
);
|
||||||
|
ctx.assert(
|
||||||
|
ctx.request.body.current_password, 400,
|
||||||
|
'{"error": {"message": "Current Password required to update account.", "code": 612}}',
|
||||||
|
);
|
||||||
const user = await models.user.findById(ctx.user.id);
|
const user = await models.user.findById(ctx.user.id);
|
||||||
ctx.assert(await passwords.match(ctx.request.body.current_password, user.password), 400,
|
ctx.assert(
|
||||||
'{"error": {"message": "Incorrect password", "code": 606}}');
|
await passwords.match(ctx.request.body.current_password, user.password), 400,
|
||||||
|
'{"error": {"message": "Incorrect password", "code": 606}}',
|
||||||
|
);
|
||||||
if (ctx.request.body.email && ctx.request.body.email !== user.email) {
|
if (ctx.request.body.email && ctx.request.body.email !== user.email) {
|
||||||
user.email = ctx.request.body.email;
|
user.email = ctx.request.body.email;
|
||||||
}
|
}
|
||||||
if (ctx.request.body.new_password) {
|
if (ctx.request.body.new_password) {
|
||||||
ctx.assert(ctx.request.body.new_password.length >= 7, 400,
|
ctx.assert(
|
||||||
'{"error": {"message": "Password must be 7 or more characters long.", "code": 606}}');
|
ctx.request.body.new_password.length >= 7, 400,
|
||||||
|
'{"error": {"message": "Password must be 7 or more characters long.", "code": 606}}',
|
||||||
|
);
|
||||||
user.password = await passwords.hash(ctx.request.body.new_password);
|
user.password = await passwords.hash(ctx.request.body.new_password);
|
||||||
}
|
}
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
2
app.js
2
app.js
|
@ -9,11 +9,11 @@ import websockify from 'koa-websocket';
|
||||||
import helmet from 'koa-helmet';
|
import helmet from 'koa-helmet';
|
||||||
import session from 'koa-session';
|
import session from 'koa-session';
|
||||||
import raven from 'raven';
|
import raven from 'raven';
|
||||||
|
import debugname from 'debug';
|
||||||
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';
|
||||||
|
|
||||||
import debugname from 'debug';
|
|
||||||
const debug = debugname('hostr');
|
const debug = debugname('hostr');
|
||||||
|
|
||||||
const app = websockify(new Koa());
|
const app = websockify(new Koa());
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { sniff } from './type';
|
import sniff from './sniff';
|
||||||
|
|
||||||
const baseURL = process.env.WEB_BASE_URL;
|
const baseURL = process.env.WEB_BASE_URL;
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import createError from 'http-errors';
|
import createError from 'http-errors';
|
||||||
|
import debugname from 'debug';
|
||||||
import { get as getS3 } from './s3';
|
import { get as getS3 } from './s3';
|
||||||
|
|
||||||
import debugname from 'debug';
|
|
||||||
const debug = debugname('hostr:file-stream');
|
const debug = debugname('hostr:file-stream');
|
||||||
|
|
||||||
function writer(localPath, remoteRead) {
|
function writer(localPath, remoteRead) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
|
||||||
function randomID() {
|
function randomID() {
|
||||||
let rand = '';
|
let rand = '';
|
||||||
for (let i = 0; i < 12; i++) {
|
for (let i = 0; i < 12; i += 1) {
|
||||||
rand += chars.charAt(Math.floor((Math.random() * chars.length)));
|
rand += chars.charAt(Math.floor((Math.random() * chars.length)));
|
||||||
}
|
}
|
||||||
return rand;
|
return rand;
|
||||||
|
@ -18,7 +18,7 @@ async function checkId(Files, fileId, attempts) {
|
||||||
if (file === null) {
|
if (file === null) {
|
||||||
return fileId;
|
return fileId;
|
||||||
}
|
}
|
||||||
return checkId(randomID(), ++attempts); // eslint-disable-line no-param-reassign
|
return checkId(Files, randomID(), attempts + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (Files) {
|
export default function (Files) {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* Module dependencies.
|
* Module dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var Stats = require('statsy');
|
const Stats = require('statsy');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize stats middleware with `opts`
|
* Initialize stats middleware with `opts`
|
||||||
|
@ -14,14 +14,13 @@ var Stats = require('statsy');
|
||||||
* @api public
|
* @api public
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = function(opts){
|
export default function (opts) {
|
||||||
opts = opts || {};
|
const s = new Stats(opts || {});
|
||||||
var s = new Stats(opts);
|
|
||||||
|
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
// counters
|
// counters
|
||||||
s.incr('request.count');
|
s.incr('request.count');
|
||||||
s.incr('request.' + ctx.method + '.count');
|
s.incr(`request.${ctx.method}.count`);
|
||||||
|
|
||||||
// size
|
// size
|
||||||
s.histogram('request.size', ctx.request.length || 0);
|
s.histogram('request.size', ctx.request.length || 0);
|
||||||
|
@ -33,5 +32,5 @@ module.exports = function(opts){
|
||||||
ctx.res.on('finish', s.timer('request.duration'));
|
ctx.res.on('finish', s.timer('request.duration'));
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
}
|
};
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import virustotal from './virustotal';
|
import getFileReport from './virustotal';
|
||||||
|
|
||||||
const extensions = [
|
const extensions = [
|
||||||
'EXE',
|
'EXE',
|
||||||
|
@ -65,13 +65,13 @@ function getExtension(filename) {
|
||||||
return (i < 0) ? '' : filename.substr(i + 1);
|
return (i < 0) ? '' : filename.substr(i + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function* (file) {
|
export default async (file) => {
|
||||||
if (extensions.indexOf(getExtension(file.file_name.toUpperCase())) < 0) {
|
if (extensions.indexOf(getExtension(file.name.toUpperCase())) < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const result = yield virustotal.getFileReport(file.md5);
|
const result = await getFileReport(file.md5);
|
||||||
return {
|
return {
|
||||||
positive: result.positives >= 5,
|
positive: result.positives >= 5,
|
||||||
result,
|
result,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
37
lib/mongo.js
37
lib/mongo.js
|
@ -1,37 +0,0 @@
|
||||||
import mongodb from 'mongodb-promisified';
|
|
||||||
const MongoClient = mongodb().MongoClient;
|
|
||||||
import debugname from 'debug';
|
|
||||||
const debug = debugname('hostr:mongo');
|
|
||||||
|
|
||||||
/* eslint no-param-reassign: ["error", { "props": false }] */
|
|
||||||
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');
|
|
||||||
client.Users = client.collection('users');
|
|
||||||
client.Files = client.collection('files');
|
|
||||||
client.Transactions = client.collection('transactions');
|
|
||||||
client.Logins = client.collection('logins');
|
|
||||||
client.Remember = client.collection('remember');
|
|
||||||
client.Reset = client.collection('reset');
|
|
||||||
client.Remember.ensureIndex({ created: 1 }, { expireAfterSeconds: 2592000 });
|
|
||||||
client.Files.ensureIndex({ owner: 1, status: 1, time_added: -1 });
|
|
||||||
client.ObjectId = client.objectId = mongodb().ObjectId;
|
|
||||||
return resolve(client);
|
|
||||||
}).catch((e) => {
|
|
||||||
reject(e);
|
|
||||||
});
|
|
||||||
}).catch((e) => {
|
|
||||||
debug(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function () {
|
|
||||||
return function* dbMiddleware(next) {
|
|
||||||
try {
|
|
||||||
this.db = yield mongo;
|
|
||||||
} catch (e) {
|
|
||||||
debug(e);
|
|
||||||
}
|
|
||||||
yield next;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -3,6 +3,7 @@ import coRedis from 'co-redis';
|
||||||
import koaRedis from 'koa-redis';
|
import koaRedis from 'koa-redis';
|
||||||
import session from 'koa-generic-session';
|
import session from 'koa-generic-session';
|
||||||
import debugname from 'debug';
|
import debugname from 'debug';
|
||||||
|
|
||||||
const debug = debugname('hostr:redis');
|
const debug = debugname('hostr:redis');
|
||||||
|
|
||||||
const connection = new Promise((resolve, reject) => {
|
const connection = new Promise((resolve, reject) => {
|
||||||
|
@ -25,8 +26,7 @@ const redisSession = new Promise((resolve, reject) =>
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
debug('koa-redis error: ', err);
|
debug('koa-redis error: ', err);
|
||||||
reject(err);
|
reject(err);
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
const wrapped = new Promise((resolve, reject) =>
|
const wrapped = new Promise((resolve, reject) =>
|
||||||
connection.then((client) => {
|
connection.then((client) => {
|
||||||
|
@ -40,8 +40,7 @@ const wrapped = new Promise((resolve, reject) =>
|
||||||
debug('co-redis error: ', err);
|
debug('co-redis error: ', err);
|
||||||
reject(err);
|
reject(err);
|
||||||
throw err;
|
throw err;
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
export function sessionStore() {
|
export function sessionStore() {
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import fs from 'mz/fs';
|
import fs from 'mz/fs';
|
||||||
import jimp from 'jimp';
|
import jimp from 'jimp';
|
||||||
import debugname from 'debug';
|
import debugname from 'debug';
|
||||||
|
|
||||||
const debug = debugname('hostr-api:resize');
|
const debug = debugname('hostr-api:resize');
|
||||||
|
|
||||||
const types = {
|
const types = {
|
||||||
jpg: jimp.MIME_JPEG,
|
jpg: jimp.MIME_JPEG,
|
||||||
png: jimp.MIME_PNG,
|
png: jimp.MIME_PNG,
|
||||||
gif: jimp.MIME_JPEG,
|
gif: jimp.MIME_JPEG,
|
||||||
}
|
};
|
||||||
|
|
||||||
function cover(path, type, size) {
|
function cover(path, type, size) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import aws from 'aws-sdk';
|
import aws from 'aws-sdk';
|
||||||
import debugname from 'debug';
|
import debugname from 'debug';
|
||||||
|
|
||||||
const debug = debugname('hostr:s3');
|
const debug = debugname('hostr:s3');
|
||||||
|
|
||||||
const s3 = new aws.S3({
|
const s3 = new aws.S3({
|
||||||
|
|
53
lib/sftp.js
53
lib/sftp.js
|
@ -1,53 +0,0 @@
|
||||||
import { dirname, join } from 'path';
|
|
||||||
import StatsD from 'statsy';
|
|
||||||
import Client from './ssh2-sftp-client';
|
|
||||||
import debugname from 'debug';
|
|
||||||
const debug = debugname('hostr:sftp');
|
|
||||||
|
|
||||||
const statsdOpts = { prefix: 'hostr-api', host: process.env.STATSD_HOST };
|
|
||||||
const statsd = new StatsD(statsdOpts);
|
|
||||||
|
|
||||||
export function get(remotePath) {
|
|
||||||
debug('fetching', join('hostr', 'uploads', remotePath));
|
|
||||||
const sftp = new Client();
|
|
||||||
return sftp.connect({
|
|
||||||
host: process.env.SFTP_HOST,
|
|
||||||
port: process.env.SFTP_PORT,
|
|
||||||
username: process.env.SFTP_USERNAME,
|
|
||||||
password: process.env.SFTP_PASSWORD,
|
|
||||||
})
|
|
||||||
.then(() => sftp.get(join('hostr', 'uploads', remotePath), { encoding: null }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendFile(localPath, remotePath) {
|
|
||||||
const sftp = new Client();
|
|
||||||
return sftp.connect({
|
|
||||||
host: process.env.SFTP_HOST,
|
|
||||||
port: process.env.SFTP_PORT,
|
|
||||||
username: process.env.SFTP_USERNAME,
|
|
||||||
password: process.env.SFTP_PASSWORD,
|
|
||||||
})
|
|
||||||
.then(() => sftp.put(localPath, remotePath, true))
|
|
||||||
.catch((err) => {
|
|
||||||
if (err.message === 'No such file') {
|
|
||||||
debug('Creating directory');
|
|
||||||
return sftp.mkdir(dirname(remotePath), true)
|
|
||||||
.then(() => sftp.put(localPath, remotePath, true));
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function *upload(localPath, remotePath) {
|
|
||||||
let done = false;
|
|
||||||
for (let retries = 0; retries < 5; retries++) {
|
|
||||||
try {
|
|
||||||
done = yield sendFile(localPath, remotePath);
|
|
||||||
break;
|
|
||||||
} catch (err) {
|
|
||||||
statsd.incr('file.upload.retry', 1);
|
|
||||||
debug('retry');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return done;
|
|
||||||
}
|
|
|
@ -26,9 +26,9 @@ const extensions = {
|
||||||
rar: 'archive',
|
rar: 'archive',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function sniff(filename) {
|
export default (filename) => {
|
||||||
if (extensions[filename.split('.').pop().toLowerCase()]) {
|
if (extensions[filename.split('.').pop().toLowerCase()]) {
|
||||||
return extensions[filename.split('.').pop().toLowerCase()];
|
return extensions[filename.split('.').pop().toLowerCase()];
|
||||||
}
|
}
|
||||||
return 'other';
|
return 'other';
|
||||||
}
|
};
|
|
@ -1,303 +0,0 @@
|
||||||
/**
|
|
||||||
* ssh2 sftp client for node
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
let Client = require('ssh2').Client;
|
|
||||||
|
|
||||||
let SftpClient = function(){
|
|
||||||
this.client = new Client();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a directory listing
|
|
||||||
*
|
|
||||||
* @param {String} path, a string containing the path to a directory
|
|
||||||
* @return {Promise} data, list info
|
|
||||||
*/
|
|
||||||
SftpClient.prototype.list = function(path) {
|
|
||||||
let reg = /-/gi;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let sftp = this.sftp;
|
|
||||||
|
|
||||||
if (sftp) {
|
|
||||||
sftp.readdir(path, (err, list) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// reset file info
|
|
||||||
list.forEach((item, i) => {
|
|
||||||
list[i] = {
|
|
||||||
type: item.longname.substr(0, 1),
|
|
||||||
name: item.filename,
|
|
||||||
size: item.attrs.size,
|
|
||||||
modifyTime: item.attrs.mtime * 1000,
|
|
||||||
accessTime: item.attrs.atime * 1000,
|
|
||||||
rights: {
|
|
||||||
user: item.longname.substr(1, 3).replace(reg, ''),
|
|
||||||
group: item.longname.substr(4,3).replace(reg, ''),
|
|
||||||
other: item.longname.substr(7, 3).replace(reg, '')
|
|
||||||
},
|
|
||||||
owner: item.attrs.uid,
|
|
||||||
group: item.attrs.gid
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resolve(list);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
reject('sftp connect error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get file
|
|
||||||
*
|
|
||||||
* @param {String} path, path
|
|
||||||
* @param {Object} useCompression, config options
|
|
||||||
* @return {Promise} stream, readable stream
|
|
||||||
*/
|
|
||||||
SftpClient.prototype.get = function(path, useCompression) {
|
|
||||||
useCompression = Object.assign({}, {encoding: 'utf8'}, useCompression);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let sftp = this.sftp;
|
|
||||||
|
|
||||||
if (sftp) {
|
|
||||||
try {
|
|
||||||
let stream = sftp.createReadStream(path, useCompression);
|
|
||||||
|
|
||||||
stream.on('error', reject);
|
|
||||||
|
|
||||||
resolve(stream);
|
|
||||||
} catch(err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reject('sftp connect error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create file
|
|
||||||
*
|
|
||||||
* @param {String|Buffer|stream} input
|
|
||||||
* @param {String} remotePath,
|
|
||||||
* @param {Object} useCompression [description]
|
|
||||||
* @return {[type]} [description]
|
|
||||||
*/
|
|
||||||
SftpClient.prototype.put = function(input, remotePath, useCompression) {
|
|
||||||
useCompression = Object.assign({}, {encoding: 'utf8'}, useCompression);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let sftp = this.sftp;
|
|
||||||
|
|
||||||
if (sftp) {
|
|
||||||
if (typeof input === 'string') {
|
|
||||||
sftp.fastPut(input, remotePath, useCompression, (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let stream = sftp.createWriteStream(remotePath, useCompression);
|
|
||||||
let data;
|
|
||||||
|
|
||||||
stream.on('error', reject);
|
|
||||||
stream.on('close', resolve);
|
|
||||||
|
|
||||||
if (input instanceof Buffer) {
|
|
||||||
data = stream.end(input);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
data = input.pipe(stream);
|
|
||||||
} else {
|
|
||||||
reject('sftp connect error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
SftpClient.prototype.mkdir = function(path, recursive) {
|
|
||||||
recursive = recursive || false;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let sftp = this.sftp;
|
|
||||||
|
|
||||||
if (sftp) {
|
|
||||||
if (!recursive) {
|
|
||||||
sftp.mkdir(path, (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tokens = path.split(/\//g);
|
|
||||||
let p = '';
|
|
||||||
|
|
||||||
let mkdir = () => {
|
|
||||||
let token = tokens.shift();
|
|
||||||
|
|
||||||
if (!token && !tokens.length) {
|
|
||||||
resolve();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
token += '/';
|
|
||||||
p = p + token;
|
|
||||||
sftp.mkdir(p, (err) => {
|
|
||||||
if (err && err.code !== 4) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
mkdir();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return mkdir();
|
|
||||||
} else {
|
|
||||||
reject('sftp connect error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
SftpClient.prototype.rmdir = function(path, recursive) {
|
|
||||||
recursive = recursive || false;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let sftp = this.sftp;
|
|
||||||
|
|
||||||
if (sftp) {
|
|
||||||
if (!recursive) {
|
|
||||||
return sftp.rmdir(path, (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let rmdir = (p) => {
|
|
||||||
return this.list(p).then((list) => {
|
|
||||||
if (list.length > 0) {
|
|
||||||
let promises = [];
|
|
||||||
|
|
||||||
list.forEach((item) => {
|
|
||||||
let name = item.name;
|
|
||||||
let promise;
|
|
||||||
var subPath;
|
|
||||||
|
|
||||||
if (name[0] === '/') {
|
|
||||||
subPath = name;
|
|
||||||
} else {
|
|
||||||
if (p[p.length - 1] === '/') {
|
|
||||||
subPath = p + name;
|
|
||||||
} else {
|
|
||||||
subPath = p + '/' + name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.type === 'd') {
|
|
||||||
if (name !== '.' || name !== '..') {
|
|
||||||
promise = rmdir(subPath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
promise = this.delete(subPath);
|
|
||||||
}
|
|
||||||
promises.push(promise);
|
|
||||||
});
|
|
||||||
if (promises.length) {
|
|
||||||
return Promise.all(promises).then(() => {
|
|
||||||
return rmdir(p);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
return sftp.rmdir(p, (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return rmdir(path).then(() => {resolve()})
|
|
||||||
.catch((err) => {reject(err)});
|
|
||||||
} else {
|
|
||||||
reject('sftp connect error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
SftpClient.prototype.delete = function(path) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let sftp = this.sftp;
|
|
||||||
|
|
||||||
if (sftp) {
|
|
||||||
sftp.unlink(path, (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
reject('sftp connect error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
SftpClient.prototype.rename = function(srcPath, remotePath) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let sftp = this.sftp;
|
|
||||||
|
|
||||||
if (sftp) {
|
|
||||||
sftp.rename(srcPath, remotePath, (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
reject('sftp connect error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
SftpClient.prototype.connect = function(config) {
|
|
||||||
var c = this.client;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.client.on('ready', () => {
|
|
||||||
|
|
||||||
this.client.sftp((err, sftp) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
this.sftp = sftp;
|
|
||||||
resolve(sftp);
|
|
||||||
});
|
|
||||||
}).on('error', (err) => {
|
|
||||||
reject(err);
|
|
||||||
}).connect(config);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
SftpClient.prototype.end = function() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.client.end();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = SftpClient;
|
|
|
@ -3,16 +3,16 @@ import Busboy from 'busboy';
|
||||||
import crypto from 'crypto';
|
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 debugname from 'debug';
|
||||||
|
|
||||||
import models from '../models';
|
import models from '../models';
|
||||||
import createHostrId from './hostr-id';
|
import createHostrId from './hostr-id';
|
||||||
import { formatFile } from './format';
|
import { formatFile } from './format';
|
||||||
import resize from './resize';
|
import resize from './resize';
|
||||||
import malware from './malware';
|
import malware from './malware';
|
||||||
import { sniff } from './type';
|
import sniff from './sniff';
|
||||||
import { upload as s3upload } from './s3';
|
import { upload as s3upload } from './s3';
|
||||||
|
|
||||||
import debugname from 'debug';
|
|
||||||
const debug = debugname('hostr-api:uploader');
|
const debug = debugname('hostr-api:uploader');
|
||||||
|
|
||||||
const storePath = process.env.UPLOAD_STORAGE_PATH;
|
const storePath = process.env.UPLOAD_STORAGE_PATH;
|
||||||
|
@ -66,14 +66,12 @@ export default class Uploader {
|
||||||
highWaterMark: 10000000,
|
highWaterMark: 10000000,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.upload.on('file', async (fieldname, file, filename, encoding, mimetype) => {
|
this.upload.on('file', async (fieldname, file, filename) => {
|
||||||
debug('FILE', fieldname, file, filename, encoding, mimetype);
|
|
||||||
|
|
||||||
this.upload.filename = filename;
|
this.upload.filename = filename;
|
||||||
|
|
||||||
this.file = await models.file.create({
|
this.file = await models.file.create({
|
||||||
id: await createHostrId(),
|
id: await createHostrId(),
|
||||||
name: this.upload.filename.replace(/[^a-zA-Z0-9\.\-_\s]/g, '').replace(/\s+/g, ''),
|
name: this.upload.filename.replace(/[^a-zA-Z0-9\.\-_\s]/g, '').replace(/\s+/g, ''), // eslint-disable-line no-useless-escape
|
||||||
originalName: this.upload.filename,
|
originalName: this.upload.filename,
|
||||||
userId: this.context.user.id,
|
userId: this.context.user.id,
|
||||||
status: 'uploading',
|
status: 'uploading',
|
||||||
|
@ -102,7 +100,7 @@ export default class Uploader {
|
||||||
|
|
||||||
this.localStream.write(data);
|
this.localStream.write(data);
|
||||||
|
|
||||||
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.file.id}", "complete": ${this.percentComplete}}}`;
|
{"id": "${this.file.id}", "complete": ${this.percentComplete}}}`;
|
||||||
|
@ -131,7 +129,6 @@ export default class Uploader {
|
||||||
this.localStream.on('end', () => {
|
this.localStream.on('end', () => {
|
||||||
s3upload(fs.createReadStream(join(storePath, this.path)), this.path);
|
s3upload(fs.createReadStream(join(storePath, this.path)), this.path);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
this.context.req.pipe(this.upload);
|
this.context.req.pipe(this.upload);
|
||||||
});
|
});
|
||||||
|
@ -210,7 +207,7 @@ export default class Uploader {
|
||||||
// Check in the background
|
// Check in the background
|
||||||
process.nextTick(async () => {
|
process.nextTick(async () => {
|
||||||
debug('Malware Scan');
|
debug('Malware Scan');
|
||||||
const result = await malware(this);
|
const result = await malware(this.file);
|
||||||
if (result) {
|
if (result) {
|
||||||
this.file.malwarePositives = result.positives;
|
this.file.malwarePositives = result.positives;
|
||||||
this.file.save();
|
this.file.save();
|
||||||
|
|
|
@ -3,9 +3,9 @@ import FormData from 'form-data';
|
||||||
|
|
||||||
const apiRoot = 'https://www.virustotal.com/vtapi/v2';
|
const apiRoot = 'https://www.virustotal.com/vtapi/v2';
|
||||||
|
|
||||||
export function* getFileReport(resource, apiKey = process.env.VIRUSTOTAL_KEY) {
|
export default async (resource, apiKey = process.env.VIRUSTOTAL_KEY) => {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append('apikey', apiKey);
|
form.append('apikey', apiKey);
|
||||||
form.append('resource', resource);
|
form.append('resource', resource);
|
||||||
return yield fetch(`${apiRoot}/file/report`, { method: 'POST' });
|
return fetch(`${apiRoot}/file/report`, { method: 'POST' });
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
|
@ -1,83 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
|
@ -1,54 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
|
@ -1,46 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
|
@ -1,77 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
|
@ -13,7 +13,7 @@ export default function (sequelize, DataTypes) {
|
||||||
'audio',
|
'audio',
|
||||||
'video',
|
'video',
|
||||||
'archive',
|
'archive',
|
||||||
'other'
|
'other',
|
||||||
),
|
),
|
||||||
width: DataTypes.INTEGER,
|
width: DataTypes.INTEGER,
|
||||||
height: DataTypes.INTEGER,
|
height: DataTypes.INTEGER,
|
||||||
|
@ -32,14 +32,15 @@ export default function (sequelize, DataTypes) {
|
||||||
});
|
});
|
||||||
|
|
||||||
File.accessed = function accessed(id) {
|
File.accessed = function accessed(id) {
|
||||||
sequelize.query(`
|
sequelize.query(
|
||||||
|
`
|
||||||
UPDATE files
|
UPDATE files
|
||||||
SET "downloads" = downloads + 1, "accessedAt" = NOW()
|
SET "downloads" = downloads + 1, "accessedAt" = NOW()
|
||||||
WHERE "id" = :id`,
|
WHERE "id" = :id`,
|
||||||
{
|
{
|
||||||
replacements: { id },
|
replacements: { id },
|
||||||
type: sequelize.QueryTypes.UPDATE,
|
type: sequelize.QueryTypes.UPDATE,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,6 @@ import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import Sequelize from 'sequelize';
|
import Sequelize from 'sequelize';
|
||||||
|
|
||||||
import debugname from 'debug';
|
|
||||||
|
|
||||||
const debug = debugname('hostr:models');
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
dialect: 'postgres',
|
dialect: 'postgres',
|
||||||
protocol: 'postgres',
|
protocol: 'postgres',
|
||||||
|
|
17
web/app.js
17
web/app.js
|
@ -2,11 +2,10 @@ import path from 'path';
|
||||||
import Router from 'koa-router';
|
import Router from 'koa-router';
|
||||||
import CSRF from 'koa-csrf';
|
import CSRF from 'koa-csrf';
|
||||||
import views from 'koa-views';
|
import views from 'koa-views';
|
||||||
import stats from '../lib/koa-statsd';
|
|
||||||
import StatsD from 'statsy';
|
import StatsD from 'statsy';
|
||||||
import errors from 'koa-error';
|
import errors from 'koa-error';
|
||||||
|
|
||||||
import * as redis from '../lib/redis';
|
import stats from '../lib/koa-statsd';
|
||||||
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 user from './routes/user';
|
import * as user from './routes/user';
|
||||||
|
@ -26,8 +25,6 @@ router.use(async (ctx, next) => {
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
//router.use(redis.sessionStore());
|
|
||||||
|
|
||||||
router.use(async (ctx, next) => {
|
router.use(async (ctx, next) => {
|
||||||
ctx.state = {
|
ctx.state = {
|
||||||
session: ctx.session,
|
session: ctx.session,
|
||||||
|
@ -71,15 +68,15 @@ 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);
|
||||||
router.get('/files/:id/:name', file.get);
|
router.get('/files/:id/:name', file.get);
|
||||||
router.get('/download/:id/:name', function* downloadRedirect(id) {
|
router.get('/download/:id/:name', async (ctx, id) => {
|
||||||
this.redirect(`/${id}`);
|
ctx.redirect(`/${id}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/updaters/mac', function* macUpdater() {
|
router.get('/updaters/mac', async (ctx) => {
|
||||||
this.redirect('/updaters/mac.xml');
|
ctx.redirect('/updaters/mac.xml');
|
||||||
});
|
});
|
||||||
router.get('/updaters/mac/changelog', function* macChangelog() {
|
router.get('/updaters/mac/changelog', async (ctx) => {
|
||||||
yield this.render('mac-update-changelog');
|
await ctx.render('mac-update-changelog');
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -41,7 +41,7 @@ export async function authenticate(email, password) {
|
||||||
activated: true,
|
activated: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
debug(user);
|
|
||||||
const login = await models.login.create({
|
const login = await models.login.create({
|
||||||
ip: remoteIp,
|
ip: remoteIp,
|
||||||
successful: false,
|
successful: false,
|
||||||
|
@ -52,7 +52,6 @@ export async function authenticate(email, password) {
|
||||||
debug('Password verified');
|
debug('Password verified');
|
||||||
login.successful = true;
|
login.successful = true;
|
||||||
await login.save();
|
await login.save();
|
||||||
debug(user);
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
debug('Password invalid');
|
debug('Password invalid');
|
||||||
|
@ -182,18 +181,18 @@ Visit ${process.env.WEB_BASE_URL}/forgot/${reset.id} to set a new one.
|
||||||
|
|
||||||
export async function fromToken(token) {
|
export async function fromToken(token) {
|
||||||
const userId = await this.redis.get(token);
|
const userId = await this.redis.get(token);
|
||||||
return await models.user.findById(userId);
|
return models.user.findById(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function fromCookie(rememberId) {
|
export async function fromCookie(rememberId) {
|
||||||
const userId = await models.remember.findById(rememberId);
|
const userId = await models.remember.findById(rememberId);
|
||||||
return await models.user.findById(userId);
|
return models.user.findById(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function validateResetToken(resetId) {
|
export async function validateResetToken(resetId) {
|
||||||
return await models.reset.findById(resetId);
|
return models.reset.findById(resetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -206,7 +205,6 @@ export async function updatePassword(userId, password) {
|
||||||
|
|
||||||
|
|
||||||
export async function activateUser(code) {
|
export async function activateUser(code) {
|
||||||
debug(code);
|
|
||||||
const activation = await models.activation.findOne({
|
const activation = await models.activation.findOne({
|
||||||
where: {
|
where: {
|
||||||
id: code,
|
id: code,
|
||||||
|
|
|
@ -20,7 +20,7 @@ function userAgentCheck(userAgent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function referrerCheck(referrer) {
|
function referrerCheck(referrer) {
|
||||||
return referrer && referrerRegexes.some((regex) => referrer.match(regex));
|
return referrer && referrerRegexes.some(regex => referrer.match(regex));
|
||||||
}
|
}
|
||||||
|
|
||||||
function hotlinkCheck(file, userAgent, referrer) {
|
function hotlinkCheck(file, userAgent, referrer) {
|
||||||
|
@ -52,7 +52,7 @@ export async function get(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.malware) {
|
if (file.malware) {
|
||||||
const alert = ctx.request.query.alert;
|
const { alert } = ctx.request.query;
|
||||||
if (!alert || !alert.match(/i want to download malware/i)) {
|
if (!alert || !alert.match(/i want to download malware/i)) {
|
||||||
ctx.redirect(`/${file.id}`);
|
ctx.redirect(`/${file.id}`);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import uuid from 'node-uuid';
|
import uuid from 'node-uuid';
|
||||||
import auth from '../lib/auth';
|
import { fromToken, fromCookie, setupSession } from '../lib/auth';
|
||||||
|
|
||||||
export async function main(ctx) {
|
export async function main(ctx) {
|
||||||
if (ctx.session.user) {
|
if (ctx.session.user) {
|
||||||
|
@ -11,19 +11,17 @@ export async function main(ctx) {
|
||||||
await ctx.redis.set(token, ctx.session.user.id, 'EX', 604800);
|
await ctx.redis.set(token, ctx.session.user.id, 'EX', 604800);
|
||||||
ctx.session.user.token = token;
|
ctx.session.user.token = token;
|
||||||
await ctx.render('index', { user: ctx.session.user });
|
await ctx.render('index', { user: ctx.session.user });
|
||||||
} else {
|
} else if (ctx.query['app-token']) {
|
||||||
if (ctx.query['app-token']) {
|
const user = await fromToken(ctx, ctx.query['app-token']);
|
||||||
const user = await auth.fromToken(ctx, ctx.query['app-token']);
|
await setupSession(ctx, user);
|
||||||
await auth.setupSession(ctx, user);
|
|
||||||
ctx.redirect('/');
|
ctx.redirect('/');
|
||||||
} else if (ctx.cookies.r) {
|
} else if (ctx.cookies.r) {
|
||||||
const user = await auth.fromCookie(ctx, ctx.cookies.r);
|
const user = await fromCookie(ctx, ctx.cookies.r);
|
||||||
await auth.setupSession(ctx, user);
|
await setupSession(ctx, user);
|
||||||
ctx.redirect('/');
|
ctx.redirect('/');
|
||||||
} else {
|
} else {
|
||||||
await ctx.render('marketing');
|
await ctx.render('marketing');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function staticPage(ctx, next) {
|
export async function staticPage(ctx, next) {
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
import debugname from 'debug';
|
||||||
import {
|
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 models from '../../models';
|
||||||
import debugname from 'debug';
|
|
||||||
const debug = debugname('hostr-web:user');
|
const debug = debugname('hostr-web:user');
|
||||||
|
|
||||||
export async function signin(ctx) {
|
export async function signin(ctx) {
|
||||||
|
@ -44,17 +46,20 @@ export async function signup(ctx) {
|
||||||
await ctx.render('signup', { error: 'Emails do not match.', csrf: ctx.csrf });
|
await ctx.render('signup', { error: 'Emails do not match.', csrf: ctx.csrf });
|
||||||
return;
|
return;
|
||||||
} else if (ctx.request.body.email && !ctx.request.body.terms) {
|
} else if (ctx.request.body.email && !ctx.request.body.terms) {
|
||||||
await ctx.render('signup', { error: 'You must agree to the terms of service.',
|
await ctx.render('signup', {
|
||||||
csrf: ctx.csrf });
|
error: 'You must agree to the terms of service.',
|
||||||
|
csrf: ctx.csrf,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
} else if (ctx.request.body.password && ctx.request.body.password.length < 7) {
|
} else if (ctx.request.body.password && ctx.request.body.password.length < 7) {
|
||||||
await ctx.render('signup', { error: 'Password must be at least 7 characters long.',
|
await ctx.render('signup', {
|
||||||
csrf: ctx.csrf });
|
error: 'Password must be at least 7 characters long.',
|
||||||
|
csrf: ctx.csrf,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ip = ctx.headers['x-forwarded-for'] || ctx.ip;
|
const ip = ctx.headers['x-forwarded-for'] || ctx.ip;
|
||||||
const email = ctx.request.body.email;
|
const { email, password } = ctx.request.body;
|
||||||
const password = ctx.request.body.password;
|
|
||||||
try {
|
try {
|
||||||
await signupUser.call(ctx, email, password, ip);
|
await signupUser.call(ctx, email, password, ip);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -66,12 +71,11 @@ export async function signup(ctx) {
|
||||||
message: 'Thanks for signing up, we\'ve sent you an email to activate your account.',
|
message: 'Thanks for signing up, we\'ve sent you an email to activate your account.',
|
||||||
csrf: '',
|
csrf: '',
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function forgot(ctx) {
|
export async function forgot(ctx) {
|
||||||
const token = ctx.params.token;
|
const { token } = ctx.params;
|
||||||
|
|
||||||
if (ctx.request.body.password) {
|
if (ctx.request.body.password) {
|
||||||
if (ctx.request.body.password.length < 7) {
|
if (ctx.request.body.password.length < 7) {
|
||||||
|
@ -87,7 +91,7 @@ export async function forgot(ctx) {
|
||||||
if (user) {
|
if (user) {
|
||||||
await updatePassword(user.userId, ctx.request.body.password);
|
await updatePassword(user.userId, ctx.request.body.password);
|
||||||
const reset = await models.reset.findById(token);
|
const reset = await models.reset.findById(token);
|
||||||
//reset.destroy();
|
reset.destroy();
|
||||||
await setupSession.call(ctx, user);
|
await setupSession.call(ctx, user);
|
||||||
ctx.statsd.incr('auth.reset.success', 1);
|
ctx.statsd.incr('auth.reset.success', 1);
|
||||||
ctx.redirect('/');
|
ctx.redirect('/');
|
||||||
|
@ -104,11 +108,10 @@ export async function forgot(ctx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await ctx.render('forgot', { csrf: ctx.csrf, token });
|
await ctx.render('forgot', { csrf: ctx.csrf, token });
|
||||||
return;
|
|
||||||
} else if (ctx.request.body.email) {
|
} else if (ctx.request.body.email) {
|
||||||
ctx.assertCSRF(ctx.request.body);
|
ctx.assertCSRF(ctx.request.body);
|
||||||
try {
|
try {
|
||||||
const email = ctx.request.body.email;
|
const { email } = ctx.request.body;
|
||||||
await sendResetToken.call(ctx, email);
|
await sendResetToken.call(ctx, email);
|
||||||
ctx.statsd.incr('auth.reset.request', 1);
|
ctx.statsd.incr('auth.reset.request', 1);
|
||||||
await ctx.render('forgot', {
|
await ctx.render('forgot', {
|
||||||
|
@ -136,7 +139,7 @@ export async function logout(ctx) {
|
||||||
|
|
||||||
|
|
||||||
export async function activate(ctx) {
|
export async function activate(ctx) {
|
||||||
const code = ctx.params.code;
|
const { code } = ctx.params;
|
||||||
if (await activateUser.call(ctx, code)) {
|
if (await activateUser.call(ctx, code)) {
|
||||||
ctx.statsd.incr('auth.activation', 1);
|
ctx.statsd.incr('auth.activation', 1);
|
||||||
ctx.redirect('/');
|
ctx.redirect('/');
|
||||||
|
|
|
@ -3,8 +3,6 @@ import kue from 'kue';
|
||||||
import raven from 'raven';
|
import raven from 'raven';
|
||||||
import debuglog from 'debug';
|
import debuglog from 'debug';
|
||||||
|
|
||||||
import models from '../models';
|
|
||||||
|
|
||||||
const debug = debuglog('hostr:worker');
|
const debug = debuglog('hostr:worker');
|
||||||
|
|
||||||
raven.config(process.env.SENTRY_DSN).install();
|
raven.config(process.env.SENTRY_DSN).install();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue