Apply Javascript styleguide
This commit is contained in:
parent
752ce964c8
commit
6e0f351093
30 changed files with 364 additions and 375 deletions
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"extends": "airbnb/base",
|
||||
"ecmaFeatures": {
|
||||
"modules": true,
|
||||
"jsx": true
|
||||
|
@ -9,6 +10,6 @@
|
|||
},
|
||||
"rules": {
|
||||
"quotes": [2, "single"],
|
||||
"no-underscore-dangle": [0]
|
||||
"no-underscore-dangle": [0],
|
||||
}
|
||||
}
|
||||
|
|
18
api/app.js
18
api/app.js
|
@ -10,20 +10,20 @@ const debug = debugname('hostr-api');
|
|||
|
||||
const router = new Router();
|
||||
|
||||
let statsdOpts = {prefix: 'hostr-api', host: process.env.STATSD_HOST || 'localhost'};
|
||||
const statsdOpts = {prefix: 'hostr-api', host: process.env.STATSD_HOST || 'localhost'};
|
||||
router.use(stats(statsdOpts));
|
||||
let statsd = new StatsD(statsdOpts);
|
||||
router.use(function*(next) {
|
||||
const statsd = new StatsD(statsdOpts);
|
||||
router.use(function* statsMiddleware(next) {
|
||||
this.statsd = statsd;
|
||||
yield next;
|
||||
});
|
||||
|
||||
router.use(cors({
|
||||
origin: '*',
|
||||
credentials: true
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
router.use('/*',function* (next) {
|
||||
router.use('/*', function* authMiddleware(next) {
|
||||
try {
|
||||
yield next;
|
||||
if (this.response.status === 404 && !this.response.body) {
|
||||
|
@ -35,13 +35,13 @@ router.use('/*',function* (next) {
|
|||
this.set('WWW-Authenticate', 'Basic');
|
||||
this.status = 401;
|
||||
this.body = err.message;
|
||||
} else if(err.status === 404) {
|
||||
} else if (err.status === 404) {
|
||||
this.status = 404;
|
||||
this.body = {
|
||||
error: {
|
||||
message: 'File not found',
|
||||
code: 604
|
||||
}
|
||||
code: 604,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
if (!err.status) {
|
||||
|
@ -70,7 +70,7 @@ router.delete('/file/:id', auth, file.del);
|
|||
router.delete('/file/:id', auth, file.del);
|
||||
|
||||
// Hack, if no route matches here, router does not dispatch at all
|
||||
router.get('/(.*)', function* () {
|
||||
router.get('/(.*)', function* errorMiddleware() {
|
||||
this.throw(404);
|
||||
});
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ const debug = debugname('hostr-api:auth');
|
|||
|
||||
const badLoginMsg = '{"error": {"message": "Incorrect login details.", "code": 607}}';
|
||||
|
||||
module.exports = function* (next) {
|
||||
export default function* (next) {
|
||||
const Users = this.db.Users;
|
||||
const Files = this.db.Files;
|
||||
const Logins = this.db.Logins;
|
||||
|
@ -20,7 +20,6 @@ module.exports = function* (next) {
|
|||
debug('Token found');
|
||||
user = yield Users.findOne({'_id': objectID(userToken)});
|
||||
} else {
|
||||
|
||||
const authUser = auth(this);
|
||||
this.assert(authUser, 401, badLoginMsg);
|
||||
const remoteIp = this.req.headers['x-real-ip'] || this.req.connection.remoteAddress;
|
||||
|
@ -39,7 +38,7 @@ module.exports = function* (next) {
|
|||
this.assert(!user.activationCode, 401, '{"error": {"message": "Account has not been activated.", "code": 603}}');
|
||||
|
||||
const uploadedTotal = yield Files.count({owner: user._id, status: {'$ne': 'deleted'}});
|
||||
const uploadedToday = yield Files.count({owner: user._id, 'time_added': {'$gt': Math.ceil(Date.now()/1000)-86400}});
|
||||
const uploadedToday = yield Files.count({owner: user._id, 'time_added': {'$gt': Math.ceil(Date.now() / 1000) - 86400}});
|
||||
|
||||
const normalisedUser = {
|
||||
'id': user._id,
|
||||
|
@ -48,10 +47,10 @@ module.exports = function* (next) {
|
|||
'file_count': uploadedTotal,
|
||||
'max_filesize': user.type === 'Pro' ? 524288000 : 20971520,
|
||||
'plan': user.type || 'Free',
|
||||
'uploads_today': uploadedToday
|
||||
'uploads_today': uploadedToday,
|
||||
};
|
||||
this.response.set('Daily-Uploads-Remaining', user.type === 'Pro' ? 'unlimited' : 15 - uploadedToday);
|
||||
this.user = normalisedUser;
|
||||
debug('Authenticated user: ' + this.user.email);
|
||||
yield next;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ export function* post(next) {
|
|||
const fileId = yield hostrId(Files);
|
||||
|
||||
// Fire an event to let the frontend map the GUID it sent to the real ID. Allows immediate linking to the file
|
||||
let acceptedEvent = `{"type": "file-accepted", "data": {"id": "${fileId}", "guid": "${tempGuid}", "href": "${fileHost}/${fileId}"}}`;
|
||||
const acceptedEvent = `{"type": "file-accepted", "data": {"id": "${fileId}", "guid": "${tempGuid}", "href": "${fileHost}/${fileId}"}}`;
|
||||
this.redis.publish('/user/' + this.user.id, acceptedEvent);
|
||||
this.statsd.incr('file.upload.accepted', 1);
|
||||
|
||||
|
@ -82,20 +82,20 @@ export function* post(next) {
|
|||
upload.pipe(s3Upload(key));
|
||||
|
||||
const thumbsPromises = [
|
||||
new Promise((resolve, reject) => {
|
||||
new Promise((resolve) => {
|
||||
const small = gm(upload).resize(150, 150, '>').stream();
|
||||
small.pipe(fs.createWriteStream(path.join(storePath, fileId[0], '150', fileId + '_' + upload.filename)));
|
||||
small.pipe(s3Upload(path.join('150', fileId + '_' + upload.filename))).on('finish', resolve);
|
||||
}),
|
||||
new Promise((resolve, reject) => {
|
||||
new Promise((resolve) => {
|
||||
const medium = gm(upload).resize(970, '>').stream();
|
||||
medium.pipe(fs.createWriteStream(path.join(storePath, fileId[0], '970', fileId + '_' + upload.filename)));
|
||||
medium.pipe(s3Upload(path.join('970', fileId + '_' + upload.filename))).on('finish', resolve);
|
||||
})
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
let dimensionsPromise = new Promise((resolve, reject) => {
|
||||
const dimensionsPromise = new Promise((resolve, reject) => {
|
||||
gm(upload).size((err, size) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
|
@ -142,7 +142,7 @@ export function* post(next) {
|
|||
status: 'active',
|
||||
'last_accessed': null,
|
||||
s3: false,
|
||||
type: sniff(upload.filename)
|
||||
type: sniff(upload.filename),
|
||||
};
|
||||
|
||||
yield Files.insertOne(dbFile);
|
||||
|
@ -175,7 +175,7 @@ export function* post(next) {
|
|||
|
||||
if (process.env.VIRUSTOTAL) {
|
||||
// Check in the background
|
||||
process.nextTick(function*() {
|
||||
process.nextTick(function* malwareScan() {
|
||||
debug('Malware Scan');
|
||||
const { positive, result } = yield malware(dbFile);
|
||||
if (positive) {
|
||||
|
@ -202,20 +202,20 @@ export function* list() {
|
|||
let limit = 20;
|
||||
if (this.request.query.perpage === '0') {
|
||||
limit = false;
|
||||
} else if(this.request.query.perpage > 0) {
|
||||
limit = parseInt(this.request.query.perpage / 1);
|
||||
} else if (this.request.query.perpage > 0) {
|
||||
limit = parseInt(this.request.query.perpage / 1, 10);
|
||||
}
|
||||
|
||||
let skip = 0;
|
||||
if (this.request.query.page) {
|
||||
skip = parseInt(this.request.query.page - 1) * limit;
|
||||
skip = parseInt(this.request.query.page - 1, 10) * limit;
|
||||
}
|
||||
|
||||
const queryOptions = {
|
||||
limit: limit, skip: skip, sort: [['time_added', 'desc']],
|
||||
hint: {
|
||||
owner: 1, status: 1, 'time_added': -1
|
||||
}
|
||||
owner: 1, status: 1, 'time_added': -1,
|
||||
},
|
||||
};
|
||||
|
||||
const userFiles = yield Files.find({owner: this.user.id, status: status}, queryOptions).toArray();
|
||||
|
@ -266,7 +266,7 @@ export function* events() {
|
|||
pubsub.on('message', (channel, message) => {
|
||||
this.websocket.send(message);
|
||||
});
|
||||
this.websocket.on('close', function() {
|
||||
this.websocket.on('close', () => {
|
||||
pubsub.quit();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,28 +8,28 @@ const debug = debugname('hostr-api:user');
|
|||
|
||||
const redisUrl = process.env.REDIS_URL || process.env.REDISTOGO_URL || 'redis://localhost:6379';
|
||||
|
||||
export function* get (){
|
||||
export function* get() {
|
||||
this.body = this.user;
|
||||
}
|
||||
|
||||
export function* token(){
|
||||
export function* token() {
|
||||
const token = uuid.v4(); // eslint-disable-line no-shadow
|
||||
yield this.redis.set(token, this.user.id, 'EX', 86400);
|
||||
this.body = {token: token};
|
||||
}
|
||||
|
||||
export function* transaction(){
|
||||
export function* transaction() {
|
||||
const Transactions = this.db.Transactions;
|
||||
const transactions = yield Transactions.find({'user_id': this.user.id}).toArray();
|
||||
|
||||
this.body = transactions.map(function(transaction) { // eslint-disable-line no-shadow
|
||||
this.body = transactions.map((transaction) => { // eslint-disable-line no-shadow
|
||||
const type = transaction.paypal ? 'paypal' : 'direct';
|
||||
return {
|
||||
id: transaction._id,
|
||||
amount: transaction.paypal ? transaction.amount : transaction.amount / 100,
|
||||
date: transaction.date,
|
||||
description: transaction.desc,
|
||||
type: type
|
||||
type: type,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -61,9 +61,9 @@ export function* events() {
|
|||
this.websocket.send(message);
|
||||
});
|
||||
pubsub.on('ready', () => {
|
||||
this.websocket.on('message', co.wrap(function* (message) {
|
||||
this.websocket.on('message', co.wrap(function* wsMessage(message) {
|
||||
let json;
|
||||
try{
|
||||
try {
|
||||
json = JSON.parse(message);
|
||||
} catch(err) {
|
||||
debug('Invalid JSON for socket auth');
|
||||
|
|
14
app.js
14
app.js
|
@ -1,9 +1,6 @@
|
|||
import path from 'path';
|
||||
import koa from 'koa';
|
||||
import mount from 'koa-mount';
|
||||
import route from 'koa-route';
|
||||
import logger from 'koa-logger';
|
||||
import Router from 'koa-router';
|
||||
import serve from 'koa-static';
|
||||
import favicon from 'koa-favicon';
|
||||
import compress from 'koa-compress';
|
||||
|
@ -13,7 +10,6 @@ import helmet from 'koa-helmet';
|
|||
import raven from 'raven';
|
||||
import mongo from './lib/mongo';
|
||||
import * as redis from './lib/redis';
|
||||
import co from 'co';
|
||||
import api from './api/app';
|
||||
import { ws } from './api/app';
|
||||
import web from './web/app';
|
||||
|
@ -27,11 +23,11 @@ app.keys = [process.env.KEYS || 'INSECURE'];
|
|||
if (process.env.SENTRY_DSN) {
|
||||
const ravenClient = new raven.Client(process.env.SENTRY_DSN);
|
||||
ravenClient.patchGlobal();
|
||||
app.use(function* (next) {
|
||||
app.use(function* ravenMiddleware(next) {
|
||||
this.raven = ravenClient;
|
||||
yield next;
|
||||
});
|
||||
app.ws.use(function* (next) {
|
||||
app.ws.use(function* ravenWsMiddleware(next) {
|
||||
this.raven = ravenClient;
|
||||
yield next;
|
||||
});
|
||||
|
@ -39,9 +35,9 @@ if (process.env.SENTRY_DSN) {
|
|||
|
||||
app.use(helmet());
|
||||
|
||||
app.use(function* (next){
|
||||
app.use(function* errorMiddleware(next) {
|
||||
this.set('Server', 'Nintendo 64');
|
||||
if(this.req.headers['x-forwarded-proto'] === 'http'){
|
||||
if (this.req.headers['x-forwarded-proto'] === 'http') {
|
||||
return this.redirect('https://' + this.req.headers.host + this.req.url);
|
||||
}
|
||||
try {
|
||||
|
@ -70,7 +66,7 @@ app.ws.use(redis.middleware());
|
|||
app.ws.use(ws.prefix('/api').routes());
|
||||
|
||||
if (!module.parent) {
|
||||
app.listen(process.env.PORT || 4040, function() {
|
||||
app.listen(process.env.PORT || 4040, function listen() {
|
||||
debug('Koa HTTP server listening on port ' + (process.env.PORT || 4040));
|
||||
});
|
||||
}
|
||||
|
|
2
init.js
2
init.js
|
@ -1,2 +0,0 @@
|
|||
import { init as storageInit } from './lib/storage';
|
||||
storageInit();
|
|
@ -9,20 +9,16 @@ export function formatDate(timestamp) {
|
|||
|
||||
export function formatSize(size) {
|
||||
if (size >= 1073741824) {
|
||||
size = Math.round((size / 1073741824) * 10) / 10 + 'GB';
|
||||
} else {
|
||||
if (size >= 1048576) {
|
||||
size = Math.round((size / 1048576) * 10) / 10 + 'MB';
|
||||
} else {
|
||||
if (size >= 1024) {
|
||||
size = Math.round((size / 1024) * 10) / 10 + 'KB';
|
||||
} else {
|
||||
size = Math.round(size) + 'B';
|
||||
}
|
||||
}
|
||||
return Math.round((size / 1073741824) * 10) / 10 + 'GB';
|
||||
}
|
||||
return size;
|
||||
};
|
||||
if (size >= 1048576) {
|
||||
return Math.round((size / 1048576) * 10) / 10 + 'MB';
|
||||
}
|
||||
if (size >= 1024) {
|
||||
return Math.round((size / 1024) * 10) / 10 + 'KB';
|
||||
}
|
||||
return Math.round(size) + 'B';
|
||||
}
|
||||
|
||||
export function formatFile(file) {
|
||||
const formattedFile = {
|
||||
|
@ -36,7 +32,7 @@ export function formatFile(file) {
|
|||
readableSize: formatSize(file.file_size),
|
||||
type: sniff(file.file_name),
|
||||
trashed: (file.status === 'trashed'),
|
||||
status: file.status
|
||||
status: file.status,
|
||||
};
|
||||
|
||||
if (file.width) {
|
||||
|
@ -45,7 +41,7 @@ export function formatFile(file) {
|
|||
const ext = (file.file_name.split('.').pop().toLowerCase() === 'psd' ? '.png' : '');
|
||||
formattedFile.direct = {
|
||||
'150x': fileHost + '/file/150/' + file._id + '/' + file.file_name + ext, // eslint-disable-line no-underscore-dangle
|
||||
'970x': fileHost + '/file/970/' + file._id + '/' + file.file_name + ext // eslint-disable-line no-underscore-dangle
|
||||
'970x': fileHost + '/file/970/' + file._id + '/' + file.file_name + ext, // eslint-disable-line no-underscore-dangle
|
||||
};
|
||||
}
|
||||
return formattedFile;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import createError from 'http-errors';
|
||||
import { get as getFile } from './s3';
|
||||
|
||||
|
|
|
@ -13,14 +13,12 @@ function* checkId(Files, fileId, attempts) {
|
|||
return false;
|
||||
}
|
||||
const file = yield Files.findOne({'_id': fileId});
|
||||
if(file === null) {
|
||||
if (file === null) {
|
||||
return fileId;
|
||||
} else {
|
||||
return checkId(randomID(), attempts++);
|
||||
}
|
||||
return checkId(randomID(), ++attempts); // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
export default function* (Files) {
|
||||
let attempts = 0;
|
||||
return yield checkId(Files, randomID(), attempts);
|
||||
return yield checkId(Files, randomID(), 0);
|
||||
}
|
||||
|
|
|
@ -6,33 +6,29 @@ const extensions = ['EXE', 'PIF', 'APPLICATION', 'GADGET', 'MSI', 'MSP', 'COM',
|
|||
'JAR', 'BAT', 'CMD', 'VB', 'VBS', 'VBE', 'JS', 'JSE', 'WS', 'WSF', 'WSC', 'WSH', 'PS1', 'PS1XML', 'PS2',
|
||||
'PS2XML', 'PSC1', 'PSC2', 'MSH', 'MSH1', 'MSH2', 'MSHXML', 'MSH1XML', 'MSH2XML', 'SCF', 'LNK', 'INF', 'REG',
|
||||
'PDF', 'DOC', 'XLS', 'PPT', 'DOCM', 'DOTM', 'XLSM', 'XLTM', 'XLAM', 'PPTM', 'POTM', 'PPAM', 'PPSM', 'SLDM',
|
||||
'RAR', 'TAR', 'ZIP', 'GZ'
|
||||
'RAR', 'TAR', 'ZIP', 'GZ',
|
||||
];
|
||||
|
||||
function getExtension(filename) {
|
||||
const i = filename.lastIndexOf('.');
|
||||
return (i < 0) ? '' : filename.substr(i + 1);
|
||||
};
|
||||
}
|
||||
|
||||
export default function (file) {
|
||||
const deferred = {};
|
||||
deferred.promise = new Promise(function(resolve, reject) {
|
||||
deferred.resolve = resolve;
|
||||
deferred.reject = reject;
|
||||
export default function(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (extensions.indexOf(getExtension(file.file_name.toUpperCase())) >= 0) {
|
||||
virustotal.getFileReport(file.md5, (err, res) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
if (res.scans) {
|
||||
resolve({positive: res.positives >= 5, result: res});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
if (extensions.indexOf(getExtension(file.file_name.toUpperCase())) >= 0) {
|
||||
virustotal.getFileReport(file.md5, function (err, res) {
|
||||
if (err) {
|
||||
return deferred.reject(err);
|
||||
}
|
||||
if (res.scans) {
|
||||
deferred.resolve({positive: res.positives >= 5, result: res});
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
return deferred.promise;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ const debug = debugname('hostr:mongo');
|
|||
|
||||
const uristring = process.env.MONGO_URL || process.env.MONGOLAB_URI || 'mongodb://localhost:27017/hostr';
|
||||
|
||||
let configuredClient = new Promise(function (resolve, reject) {
|
||||
const configuredClient = new Promise((resolve, reject) => {
|
||||
debug('Connecting to Mongodb');
|
||||
return MongoClient.connect(uristring).then((client) => {
|
||||
debug('Successfully connected to Mongodb');
|
||||
|
@ -20,19 +20,19 @@ let configuredClient = new Promise(function (resolve, reject) {
|
|||
client.ObjectId = mongodb().ObjectId;
|
||||
return resolve(client);
|
||||
}).catch((e) => {
|
||||
reject(e)
|
||||
reject(e);
|
||||
});
|
||||
}).catch((e) => {
|
||||
debug(e);
|
||||
});
|
||||
|
||||
export default function() {
|
||||
return function* (next) {
|
||||
return function* dbMiddleware(next) {
|
||||
try {
|
||||
this.db = yield configuredClient;
|
||||
} catch (e) {
|
||||
debug(e);
|
||||
}
|
||||
yield next;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
29
lib/redis.js
29
lib/redis.js
|
@ -9,17 +9,20 @@ const redisUrl = process.env.REDIS_URL || process.env.REDISTOGO_URL || 'redis://
|
|||
|
||||
const connection = new Promise((resolve, reject) => {
|
||||
debug('Connecting to Redis');
|
||||
resolve(redis.connect(redisUrl));
|
||||
const client = redis.connect(redisUrl);
|
||||
client.on('error', reject);
|
||||
resolve(client);
|
||||
}).catch((err) => {
|
||||
debug('Connection error: ' + err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
const redisSession = new Promise((resolve, reject) => {
|
||||
return connection.then((client) => {
|
||||
client = koaRedis({client: client});
|
||||
const sessionClient = koaRedis({client: client});
|
||||
resolve(session({
|
||||
key: 'hid',
|
||||
store: client
|
||||
store: sessionClient,
|
||||
}));
|
||||
}).catch((err) => {
|
||||
debug('koa-redis error: ' + err);
|
||||
|
@ -29,31 +32,29 @@ const redisSession = new Promise((resolve, reject) => {
|
|||
|
||||
const wrapped = new Promise((resolve, reject) => {
|
||||
return connection.then((client) => {
|
||||
client = coRedis(client);
|
||||
client.on('error', (err) => {
|
||||
debug('Client error: ' + err);
|
||||
reject(err);
|
||||
});
|
||||
client.on('ready', () => {
|
||||
const asyncClient = coRedis(client);
|
||||
asyncClient.on('error', reject);
|
||||
asyncClient.on('ready', () => {
|
||||
debug('Successfully connected to Redis');
|
||||
resolve(client);
|
||||
resolve(asyncClient);
|
||||
});
|
||||
}).catch((err) => {
|
||||
debug('co-redis error: ' + err);
|
||||
reject(err);
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
|
||||
export function sessionStore() {
|
||||
return function* (next) {
|
||||
return function* sessionStoreMiddleware(next) {
|
||||
const sess = yield redisSession;
|
||||
yield sess.bind(this)(next);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function middleware() {
|
||||
return function* (next) {
|
||||
return function* redisMiddleware(next) {
|
||||
this.redis = yield wrapped;
|
||||
yield next;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export function get(key) {
|
|||
return s3.getObject({Bucket: bucket, Key: 'hostr_files/' + key}).createReadStream();
|
||||
}
|
||||
|
||||
export function upload(key, body) {
|
||||
export function upload(key) {
|
||||
debug('Uploading file: %s', 'hostr_files/' + key);
|
||||
return s3Stream.upload({Bucket: bucket, Key: 'hostr_files/' + key});
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
|
||||
function range(start, stop) {
|
||||
var result = [];
|
||||
for (var idx = start.charCodeAt(0), end = stop.charCodeAt(0); idx <= end; ++idx){
|
||||
const result = [];
|
||||
for (let idx = start.charCodeAt(0), end = stop.charCodeAt(0); idx <= end; ++idx) {
|
||||
result.push(String.fromCharCode(idx));
|
||||
}
|
||||
return result;
|
||||
|
|
|
@ -23,7 +23,7 @@ const extensions = {
|
|||
'gz': 'archive',
|
||||
'tgz': 'archive',
|
||||
'bz2': 'archive',
|
||||
'rar': 'archive'
|
||||
'rar': 'archive',
|
||||
};
|
||||
|
||||
export function sniff(filename) {
|
||||
|
|
|
@ -41,13 +41,10 @@
|
|||
"koa-compress": "~1.0.8",
|
||||
"koa-csrf": "~2.3.0",
|
||||
"koa-favicon": "~1.2.0",
|
||||
"koa-file-server": "~2.3.1",
|
||||
"koa-generic-session": "~1.9.0",
|
||||
"koa-helmet": "^0.2.0",
|
||||
"koa-logger": "~1.3.0",
|
||||
"koa-mount": "~1.3.0",
|
||||
"koa-redis": "~1.0.1",
|
||||
"koa-route": "~2.4.2",
|
||||
"koa-router": "^5.1.2",
|
||||
"koa-static": "^1.4.9",
|
||||
"koa-statsd": "~0.0.2",
|
||||
|
@ -70,7 +67,9 @@
|
|||
"virustotal.js": "~0.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^4.0.10",
|
||||
"eslint": "~1.2.1",
|
||||
"eslint-config-airbnb": "0.0.8",
|
||||
"istanbul": "~0.3.18",
|
||||
"mocha": "~2.2.5",
|
||||
"nodemon": "~1.4.1",
|
||||
|
|
22
web/app.js
22
web/app.js
|
@ -4,7 +4,6 @@ import csrf from 'koa-csrf';
|
|||
import views from 'koa-views';
|
||||
import stats from 'koa-statsd';
|
||||
import * as redis from '../lib/redis';
|
||||
import co from 'co';
|
||||
import StatsD from 'statsy';
|
||||
// waiting for PR to be merged, can remove swig dependency when done
|
||||
import errors from '../lib/koa-error';
|
||||
|
@ -13,29 +12,26 @@ import * as file from './routes/file';
|
|||
import * as pro from './routes/pro';
|
||||
import * as user from './routes/user';
|
||||
|
||||
import debugname from 'debug';
|
||||
const debug = debugname('hostr-web');
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.use(errors({template: path.join(__dirname, 'public', 'error.html')}));
|
||||
|
||||
let statsdOpts = {prefix: 'hostr-web', host: process.env.STATSD_HOST || 'localhost'};
|
||||
const statsdOpts = {prefix: 'hostr-web', host: process.env.STATSD_HOST || 'localhost'};
|
||||
router.use(stats(statsdOpts));
|
||||
let statsd = new StatsD(statsdOpts);
|
||||
router.use(function* (next) {
|
||||
const statsd = new StatsD(statsdOpts);
|
||||
router.use(function* statsMiddleware(next) {
|
||||
this.statsd = statsd;
|
||||
yield next;
|
||||
});
|
||||
|
||||
router.use(redis.sessionStore());
|
||||
|
||||
router.use(function* (next) {
|
||||
router.use(function* stateMiddleware(next) {
|
||||
this.state = {
|
||||
session: this.session,
|
||||
apiURL: process.env.API_URL,
|
||||
baseURL: process.env.BASE_URL,
|
||||
stripePublic: process.env.STRIPE_PUBLIC_KEY
|
||||
stripePublic: process.env.STRIPE_PUBLIC_KEY,
|
||||
};
|
||||
yield next;
|
||||
});
|
||||
|
@ -43,7 +39,7 @@ router.use(function* (next) {
|
|||
router.use(csrf());
|
||||
|
||||
router.use(views('views', {
|
||||
default: 'ejs'
|
||||
default: 'ejs',
|
||||
}));
|
||||
|
||||
router.get('/', index.main);
|
||||
|
@ -76,14 +72,14 @@ router.get('/:id', file.landing);
|
|||
router.get('/file/:id/:name', file.get);
|
||||
router.get('/file/:size/:id/:name', file.get);
|
||||
router.get('/files/:id/:name', file.get);
|
||||
router.get('/download/:id/:name', function* (id) {
|
||||
router.get('/download/:id/:name', function* downloadRedirect(id) {
|
||||
this.redirect('/' + id);
|
||||
});
|
||||
|
||||
router.get('/updaters/mac', function* () {
|
||||
router.get('/updaters/mac', function* macUpdater() {
|
||||
this.redirect('/updaters/mac.xml');
|
||||
});
|
||||
router.get('/updaters/mac/changelog', function* () {
|
||||
router.get('/updaters/mac/changelog', function* macChangelog() {
|
||||
yield this.render('mac-update-changelog');
|
||||
});
|
||||
|
||||
|
|
|
@ -7,14 +7,13 @@ import debugname from 'debug';
|
|||
const debug = debugname('hostr-web:auth');
|
||||
import { Mandrill } from 'mandrill-api/mandrill';
|
||||
const mandrill = new Mandrill(process.env.MANDRILL_KEY);
|
||||
import mongo from '../../lib/mongo';
|
||||
|
||||
export function* authenticate(email, password) {
|
||||
const Users = this.db.Users;
|
||||
const Logins = this.db.Logins;
|
||||
const remoteIp = this.headers['x-real-ip'] || this.ip;
|
||||
|
||||
if (!password || password.length < 6){
|
||||
if (!password || password.length < 6) {
|
||||
debug('No password, or password too short');
|
||||
return new Error('Invalid login details');
|
||||
}
|
||||
|
@ -33,11 +32,10 @@ export function* authenticate(email, password) {
|
|||
login.successful = true;
|
||||
yield Logins.updateOne({_id: login._id}, login);
|
||||
return user;
|
||||
} else {
|
||||
debug('Password invalid');
|
||||
login.successful = false;
|
||||
yield Logins.updateOne({_id: login._id}, login);
|
||||
}
|
||||
debug('Password invalid');
|
||||
login.successful = false;
|
||||
yield Logins.updateOne({_id: login._id}, login);
|
||||
} else {
|
||||
debug('Email invalid');
|
||||
login.successful = false;
|
||||
|
@ -58,9 +56,9 @@ export function* setupSession(user) {
|
|||
'maxFileSize': 20971520,
|
||||
'joined': user.joined,
|
||||
'plan': user.type || 'Free',
|
||||
'uploadsToday': yield this.db.Files.count({owner: user._id, 'time_added': {'$gt': Math.ceil(Date.now()/1000)-86400}}),
|
||||
'uploadsToday': yield this.db.Files.count({owner: user._id, 'time_added': {'$gt': Math.ceil(Date.now() / 1000) - 86400}}),
|
||||
'token': token,
|
||||
'md5': crypto.createHash('md5').update(user.email).digest('hex')
|
||||
'md5': crypto.createHash('md5').update(user.email).digest('hex'),
|
||||
};
|
||||
|
||||
if (sessionUser.plan === 'Pro') {
|
||||
|
@ -71,7 +69,7 @@ export function* setupSession(user) {
|
|||
this.session.user = sessionUser;
|
||||
if (this.request.body.remember && this.request.body.remember === 'on') {
|
||||
const Remember = this.db.Remember;
|
||||
var rememberToken = uuid();
|
||||
const rememberToken = uuid();
|
||||
Remember.save({_id: rememberToken, 'user_id': user.id, created: new Date().getTime()});
|
||||
this.cookies.set('r', rememberToken, { maxAge: 1209600000, httpOnly: true});
|
||||
}
|
||||
|
@ -87,12 +85,12 @@ export function* signup(email, password, ip) {
|
|||
throw new Error('Email already in use.');
|
||||
}
|
||||
const cryptedPassword = yield passwords.crypt(password);
|
||||
var user = {
|
||||
const user = {
|
||||
email: email,
|
||||
'salted_password': cryptedPassword,
|
||||
joined: Math.round(new Date().getTime() / 1000),
|
||||
'signup_ip': ip,
|
||||
activationCode: uuid()
|
||||
activationCode: uuid(),
|
||||
};
|
||||
Users.insertOne(user);
|
||||
|
||||
|
@ -112,11 +110,11 @@ ${process.env.BASE_URL + '/activate/' + user.activationCode}
|
|||
'from_name': 'Jonathan from Hostr',
|
||||
to: [{
|
||||
email: user.email,
|
||||
type: 'to'
|
||||
type: 'to',
|
||||
}],
|
||||
'tags': [
|
||||
'user-activation'
|
||||
]
|
||||
'user-activation',
|
||||
],
|
||||
}});
|
||||
}
|
||||
|
||||
|
@ -126,11 +124,11 @@ export function* sendResetToken(email) {
|
|||
const Reset = this.db.Reset;
|
||||
const user = yield Users.findOne({email: email});
|
||||
if (user) {
|
||||
var token = uuid.v4();
|
||||
const token = uuid.v4();
|
||||
Reset.save({
|
||||
'_id': user._id,
|
||||
'token': token,
|
||||
'created': Math.round(new Date().getTime() / 1000)
|
||||
'created': Math.round(new Date().getTime() / 1000),
|
||||
});
|
||||
const html = yield render('email/inlined/forgot', {forgotUrl: process.env.BASE_URL + '/forgot/' + token});
|
||||
const text = `It seems you've forgotten your password :(
|
||||
|
@ -144,11 +142,11 @@ Visit ${process.env.BASE_URL + '/forgot/' + token} to set a new one.
|
|||
'from_name': 'Jonathan from Hostr',
|
||||
to: [{
|
||||
email: user.email,
|
||||
type: 'to'
|
||||
type: 'to',
|
||||
}],
|
||||
'tags': [
|
||||
'password-reset'
|
||||
]
|
||||
'password-reset',
|
||||
],
|
||||
}});
|
||||
} else {
|
||||
throw new Error('There was an error looking up your email address.');
|
||||
|
|
3
web/public/.eslintignore
Normal file
3
web/public/.eslintignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
jspm_packages/
|
||||
config.js
|
||||
lazy-src.js
|
|
@ -1,11 +1,11 @@
|
|||
import angular from 'angular';
|
||||
import ngRoute from 'angular/route';
|
||||
import ngResource from 'angular/resource';
|
||||
import ReconnectingWebSocket from 'angular-reconnecting-websocket';
|
||||
import ngDimensions from 'angular-strap/dist/modules/dimensions';
|
||||
import ngStrapCore from 'angular-strap/dist/modules/compiler';
|
||||
import ngTooltip from 'angular-strap/dist/modules/tooltip';
|
||||
import ngTooltipTemplate from 'angular-strap/dist/modules/tooltip.tpl';
|
||||
import 'angular/route';
|
||||
import 'angular/resource';
|
||||
import 'angular-reconnecting-websocket';
|
||||
import 'angular-strap/dist/modules/dimensions';
|
||||
import 'angular-strap/dist/modules/compiler';
|
||||
import 'angular-strap/dist/modules/tooltip';
|
||||
import 'angular-strap/dist/modules/tooltip.tpl';
|
||||
|
||||
import { FilesController, FileController, AccountController, ProController, BillingController } from './app/controllers';
|
||||
import { appHeader, appFooter, menuDropdown, searchShortcut, stripeSubscribe } from './app/directives';
|
||||
|
@ -15,12 +15,12 @@ import { fileSize, direct } from './app/filters';
|
|||
import { FileService, UserService, EventService, TransactionService, SettingService } from './app/services';
|
||||
|
||||
// Declare app level module which depends on filters, and services
|
||||
var app = angular.module('hostr', [
|
||||
const app = angular.module('hostr', [
|
||||
'ngRoute',
|
||||
'ngResource',
|
||||
'reconnectingWebSocket',
|
||||
'mgcrea.ngStrap.core',
|
||||
'mgcrea.ngStrap.tooltip'
|
||||
'mgcrea.ngStrap.tooltip',
|
||||
]);
|
||||
|
||||
app.factory('FileService', ['$resource', '$cacheFactory', FileService.factory]);
|
||||
|
@ -40,21 +40,20 @@ app.directive('lazySrc', ['$window', '$document', lazySrc]);
|
|||
app.directive('searchShortcut', ['$document', searchShortcut]);
|
||||
app.directive('stripeSubscribe', ['$http', stripeSubscribe]);
|
||||
|
||||
app.config(['$routeProvider', '$locationProvider', '$httpProvider', '$tooltipProvider', function($routeProvider, $locationProvider, $httpProvider, $tooltipProvider) {
|
||||
|
||||
app.config(['$routeProvider', '$locationProvider', '$httpProvider', ($routeProvider, $locationProvider, $httpProvider) => {
|
||||
if (typeof window.user !== 'undefined') {
|
||||
$httpProvider.defaults.headers.common.Authorization = ':' + window.user.token;
|
||||
}
|
||||
$locationProvider.html5Mode(true);
|
||||
|
||||
$httpProvider.interceptors.push(['$q', function($q) {
|
||||
$httpProvider.interceptors.push(['$q', ($q) => {
|
||||
return {
|
||||
responseError: function(rejection) {
|
||||
responseError: (rejection) => {
|
||||
if (rejection.status === 401) {
|
||||
window.location = '/logout';
|
||||
}
|
||||
return $q.reject(rejection);
|
||||
}
|
||||
},
|
||||
};
|
||||
}]);
|
||||
|
||||
|
@ -63,66 +62,65 @@ app.config(['$routeProvider', '$locationProvider', '$httpProvider', '$tooltipPro
|
|||
controller: FilesController,
|
||||
title: ' - Files',
|
||||
resolve: {
|
||||
files: ['FileService', function(Files) {
|
||||
files: ['FileService', (Files) => {
|
||||
return Files.query();
|
||||
}]
|
||||
}
|
||||
}],
|
||||
},
|
||||
})
|
||||
.when('/apps', {
|
||||
templateUrl: '/build/partials/apps.html',
|
||||
title: ' - Apps for Mac and Windows'
|
||||
title: ' - Apps for Mac and Windows',
|
||||
})
|
||||
.when('/pro', {
|
||||
templateUrl: '/build/partials/pro.html',
|
||||
controller: ProController,
|
||||
title: ' - Pro'
|
||||
title: ' - Pro',
|
||||
})
|
||||
.when('/account', {
|
||||
templateUrl: '/build/partials/account.html',
|
||||
controller: AccountController,
|
||||
title: ' - Account'
|
||||
title: ' - Account',
|
||||
})
|
||||
.when('/billing', {
|
||||
templateUrl: '/build/partials/billing.html',
|
||||
controller: BillingController,
|
||||
title: ' - Billing'
|
||||
title: ' - Billing',
|
||||
})
|
||||
.when('/terms', {
|
||||
templateUrl: '/build/partials/terms.html',
|
||||
title: ' - Terms of Service'
|
||||
title: ' - Terms of Service',
|
||||
})
|
||||
.when('/privacy', {
|
||||
templateUrl: '/build/partials/privacy.html',
|
||||
title: ' - Privacy Policy'
|
||||
title: ' - Privacy Policy',
|
||||
})
|
||||
.when('/:id', {
|
||||
templateUrl: '/build/partials/file.html',
|
||||
controller: FileController,
|
||||
resolve: {
|
||||
file: ['$route', 'FileService', function($route, Files) {
|
||||
file: ['$route', 'FileService', ($route, Files) => {
|
||||
return Files.get({id: $route.current.params.id});
|
||||
}]
|
||||
}
|
||||
}],
|
||||
},
|
||||
});
|
||||
}]);
|
||||
|
||||
app.run(['$location', '$rootScope', function($location, $rootScope) {
|
||||
|
||||
$rootScope.$on('$routeChangeStart', function(e, curr) {
|
||||
if (curr.$$route && curr.$$route.resolve) {
|
||||
app.run(['$location', '$rootScope', ($location, $rootScope) => {
|
||||
$rootScope.$on('$routeChangeStart', (e, curr) => {
|
||||
if (curr.$$route && curr.$$route.resolve) {
|
||||
// Show a loading message until promises are resolved
|
||||
$rootScope.loadingView = true;
|
||||
}
|
||||
});
|
||||
$rootScope.$on('$routeChangeSuccess', function (event, current) {
|
||||
$rootScope.loadingView = true;
|
||||
}
|
||||
});
|
||||
$rootScope.$on('$routeChangeSuccess', (event, current) => {
|
||||
$rootScope.navError = false;
|
||||
$rootScope.pageTitle = current.$$route.title;
|
||||
});
|
||||
$rootScope.$on('$routeChangeError', function () {
|
||||
$rootScope.$on('$routeChangeError', () => {
|
||||
$rootScope.loadingView = false;
|
||||
$rootScope.navError = true;
|
||||
});
|
||||
$rootScope.$on('$locationChangeStart', function(event, newUrl) {
|
||||
$rootScope.$on('$locationChangeStart', (event, newUrl) => {
|
||||
if (window.ga) {
|
||||
window.ga('send', 'pageview', newUrl);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
export class FilesController {
|
||||
constructor($scope, UserService, EventService, files) {
|
||||
$scope.$root.user = UserService.get();
|
||||
files.$promise.then(function() {
|
||||
files.$promise.then(() => {
|
||||
$scope.$root.loadingView = false;
|
||||
});
|
||||
$scope.header = 'full';
|
||||
if (!$scope.$root.files) {
|
||||
$scope.$root.files = files;
|
||||
}
|
||||
$scope.remove = function(file) {
|
||||
$scope.$root.files.some(function(existingFile, index) {
|
||||
$scope.remove = (file) => {
|
||||
$scope.$root.files.some((existingFile, index) => {
|
||||
if (file.id === existingFile.id) {
|
||||
file.$remove(function() {
|
||||
file.$remove(() => {
|
||||
$scope.$root.showDropdown = false;
|
||||
$scope.$root.files.splice(index, 1);
|
||||
});
|
||||
|
@ -25,8 +25,8 @@ export class FilesController {
|
|||
FilesController.$inject = ['$scope', 'UserService', 'EventService', 'files'];
|
||||
|
||||
export class FileController {
|
||||
constructor ($scope, $rootScope, $routeParams, ReconnectingWebSocket, file) {
|
||||
file.$promise.then(function() {
|
||||
constructor($scope, $rootScope, $routeParams, ReconnectingWebSocket, file) {
|
||||
file.$promise.then(() => {
|
||||
$scope.$root.loadingView = false;
|
||||
$scope.header = 'small';
|
||||
$scope.file = file;
|
||||
|
@ -34,25 +34,25 @@ export class FileController {
|
|||
$rootScope.pageTitle = ' - ' + file.name;
|
||||
if (file.status === 'uploading') {
|
||||
file.percent = 0;
|
||||
var ws = new ReconnectingWebSocket(window.settings.apiURL.replace(/^http/, 'ws') + '/file/' + file.id);
|
||||
ws.onmessage = function (msg) {
|
||||
var evt = JSON.parse(msg.data);
|
||||
const ws = new ReconnectingWebSocket(window.settings.apiURL.replace(/^http/, 'ws') + '/file/' + file.id);
|
||||
ws.onmessage = (msg) => {
|
||||
const evt = JSON.parse(msg.data);
|
||||
$rootScope.$broadcast(evt.type, evt.data);
|
||||
};
|
||||
ws.onopen = function() {
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify({authorization: window.user.token}));
|
||||
};
|
||||
$rootScope.$on('file-progress', function(evt, data) {
|
||||
$rootScope.$on('file-progress', (evt, data) => {
|
||||
$scope.file.percent = data.complete;
|
||||
});
|
||||
$rootScope.$on('file-added', function(evt, data) {
|
||||
$rootScope.$on('file-added', (evt, data) => {
|
||||
$scope.file = data;
|
||||
});
|
||||
$rootScope.$on('file-accepted', function(evt, data) {
|
||||
$rootScope.$on('file-accepted', (evt, data) => {
|
||||
$scope.file = data;
|
||||
});
|
||||
}
|
||||
}, function() {
|
||||
}, () => {
|
||||
$rootScope.navError = true;
|
||||
$scope.$root.loadingView = false;
|
||||
});
|
||||
|
@ -61,15 +61,15 @@ export class FileController {
|
|||
FileController.$inject = ['$scope', '$rootScope', '$routeParams', 'WebSocket', 'file'];
|
||||
|
||||
export class ProController {
|
||||
constructor ($scope, $http, UserService) {
|
||||
constructor($scope, $http, UserService) {
|
||||
$scope.$root.loadingView = false;
|
||||
$scope.user = UserService.get();
|
||||
$scope.header = 'full';
|
||||
$scope.cancel = function() {
|
||||
$http.post('/pro/cancel').success(function() {
|
||||
$scope.cancel = () => {
|
||||
$http.post('/pro/cancel').success(() => {
|
||||
window.location.reload(true);
|
||||
}).error(function(data) {
|
||||
console.log(new Error(data));
|
||||
}).error((data) => {
|
||||
console.error(new Error(data));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -77,17 +77,17 @@ export class ProController {
|
|||
ProController.$inject = ['$scope', '$http', 'UserService'];
|
||||
|
||||
export class AccountController {
|
||||
constructor ($scope, UserService, SettingService) {
|
||||
constructor($scope, UserService, SettingService) {
|
||||
$scope.$root.loadingView = false;
|
||||
$scope.$root.user = UserService.get();
|
||||
$scope.submit = function(form) {
|
||||
$scope.submit = (form) => {
|
||||
$scope.updated = false;
|
||||
$scope.error = false;
|
||||
SettingService.update(form).then(function() {
|
||||
SettingService.update(form).then(() => {
|
||||
$scope.updated = true;
|
||||
delete $scope.user.new_password;
|
||||
delete $scope.user.current_password;
|
||||
}, function(response) {
|
||||
}, (response) => {
|
||||
$scope.error = response.data.error.message;
|
||||
});
|
||||
};
|
||||
|
@ -96,7 +96,7 @@ export class AccountController {
|
|||
AccountController.$inject = ['$scope', 'UserService', 'SettingService'];
|
||||
|
||||
export class BillingController {
|
||||
constructor ($scope, UserService, TransactionService) {
|
||||
constructor($scope, UserService, TransactionService) {
|
||||
$scope.$root.loadingView = false;
|
||||
$scope.$root.user = UserService.get();
|
||||
$scope.transactions = TransactionService.query();
|
||||
|
|
|
@ -9,7 +9,7 @@ export function appHeader() {
|
|||
scope.userMD5 = window.user.md5;
|
||||
scope.email = window.user.email;
|
||||
scope.pro = (window.user.type === 'Pro');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -23,19 +23,19 @@ export function appFooter() {
|
|||
scope.userMD5 = window.user.md5;
|
||||
scope.email = window.user.email;
|
||||
scope.pro = (window.user.type === 'Pro');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function menuDropdown() {
|
||||
return function($scope, element) {
|
||||
return ($scope, element) => {
|
||||
$scope.$root.overlayClick = function overlayClick() {
|
||||
$scope.$root.showDropdown = false;
|
||||
$('.dropdown').hide();
|
||||
};
|
||||
var activeDropdown = $(element).find('.dropdown');
|
||||
element.on('click', function(e) {
|
||||
const activeDropdown = $(element).find('.dropdown');
|
||||
element.on('click', (e) => {
|
||||
if (activeDropdown.not(':visible').length > 0) {
|
||||
$('.dropdown').hide();
|
||||
$scope.$root.showDropdown = true;
|
||||
|
@ -50,10 +50,10 @@ export function menuDropdown() {
|
|||
}
|
||||
|
||||
|
||||
export function searchShortcut ($document) {
|
||||
return function($scope, element) {
|
||||
$document.bind('keypress', function(event) {
|
||||
if(event.which === 47) {
|
||||
export function searchShortcut($document) {
|
||||
return ($scope, element) => {
|
||||
$document.bind('keypress', (event) => {
|
||||
if (event.which === 47) {
|
||||
if (['INPUT', 'TEXTAREA'].indexOf(document.activeElement.tagName) < 0) {
|
||||
element[0].focus();
|
||||
event.preventDefault();
|
||||
|
@ -68,21 +68,23 @@ export function stripeSubscribe($http) {
|
|||
const handler = window.StripeCheckout.configure({
|
||||
key: window.settings.stripePublic,
|
||||
image: '/images/stripe-128.png',
|
||||
token: function(token) {
|
||||
$http.post('/pro/create', {stripeToken: token})
|
||||
.success(function(data) {
|
||||
token: (token) => {
|
||||
$http.post('/pro/create', {
|
||||
stripeToken: token,
|
||||
})
|
||||
.success((data) => {
|
||||
if (data.status === 'active') {
|
||||
window.user.plan = 'Pro';
|
||||
window.location.reload(true);
|
||||
}
|
||||
})
|
||||
.error(function() {
|
||||
alert('Error upgrading your account');
|
||||
.error(() => {
|
||||
console.error('Error upgrading your account');
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
return function(scope, element) {
|
||||
element.on('click', function() {
|
||||
return (scope, element) => {
|
||||
element.on('click', () => {
|
||||
// Open Checkout with further options
|
||||
handler.open({
|
||||
name: 'Hostr',
|
||||
|
@ -91,7 +93,7 @@ export function stripeSubscribe($http) {
|
|||
amount: 600,
|
||||
currency: 'USD',
|
||||
panelLabel: 'Subscribe {{amount}}',
|
||||
billingAddress: false
|
||||
billingAddress: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -11,56 +11,56 @@ function guid() {
|
|||
}
|
||||
|
||||
export default function dropzone(FileService, $cacheFactory) {
|
||||
var dropOverlay = document.getElementById('filedrop-overlay');
|
||||
var dropzoneEl;
|
||||
var errorTimeout;
|
||||
return function($scope) {
|
||||
$scope.$on('$viewContentLoaded', function() {
|
||||
const dropOverlay = document.getElementById('filedrop-overlay');
|
||||
let dropzoneEl;
|
||||
let errorTimeout;
|
||||
return ($scope) => {
|
||||
$scope.$on('$viewContentLoaded', () => {
|
||||
if (!dropzoneEl) {
|
||||
$scope.$root.uploadingFiles = [];
|
||||
var clickable = [].slice.call(document.querySelectorAll('.choose-file'));
|
||||
const clickable = [].slice.call(document.querySelectorAll('.choose-file'));
|
||||
|
||||
dropzoneEl = new Dropzone(document.body, {
|
||||
url: window.settings.apiURL + '/file',
|
||||
maxFilesize: window.user.maxFileSize / 1024 / 1024,
|
||||
maxThumbnailFilesize: 5,
|
||||
thumbnailWidth: 150,
|
||||
thumbnailHeight: 98,
|
||||
parallelUploads: 1,
|
||||
uploadMultiple: false,
|
||||
clickable: clickable.length ? clickable : false,
|
||||
autoDiscover: false,
|
||||
headers: {'Authorization': ':' + window.user.token},
|
||||
previewsContainer: false
|
||||
url: window.settings.apiURL + '/file',
|
||||
maxFilesize: window.user.maxFileSize / 1024 / 1024,
|
||||
maxThumbnailFilesize: 5,
|
||||
thumbnailWidth: 150,
|
||||
thumbnailHeight: 98,
|
||||
parallelUploads: 1,
|
||||
uploadMultiple: false,
|
||||
clickable: clickable.length ? clickable : false,
|
||||
autoDiscover: false,
|
||||
headers: {'Authorization': ':' + window.user.token},
|
||||
previewsContainer: false,
|
||||
});
|
||||
dropzoneEl.on('thumbnail', function(file, thumbnail){
|
||||
dropzoneEl.on('thumbnail', (file, thumbnail) => {
|
||||
file.thumbnail = thumbnail;
|
||||
$scope.$apply();
|
||||
});
|
||||
dropzoneEl.on('addedfile', function(file){
|
||||
var id = guid();
|
||||
dropzoneEl.on('addedfile', (file) => {
|
||||
const id = guid();
|
||||
file.guid = id;
|
||||
$scope.$root.uploadingFiles.push(file);
|
||||
$scope.$apply();
|
||||
});
|
||||
dropzoneEl.on('sending', function(file, xhr) {
|
||||
dropzoneEl.on('sending', (file, xhr) => {
|
||||
xhr.setRequestHeader('hostr-guid', file.guid);
|
||||
});
|
||||
dropzoneEl.on('uploadprogress', function(file, progress) {
|
||||
dropzoneEl.on('uploadprogress', (file, progress) => {
|
||||
$scope.$root.progress = {
|
||||
name: file.name,
|
||||
percent: progress,
|
||||
status: 'Uploading'
|
||||
status: 'Uploading',
|
||||
};
|
||||
if (progress === 100) {
|
||||
$scope.$root.progress.status = 'Processing';
|
||||
}
|
||||
$scope.$apply();
|
||||
});
|
||||
dropzoneEl.on('complete', function(file){
|
||||
dropzoneEl.on('complete', (file) => {
|
||||
delete $scope.$root.progress;
|
||||
$scope.$apply();
|
||||
$scope.$root.uploadingFiles.some(function(uploadingFile, index) {
|
||||
$scope.$root.uploadingFiles.some((uploadingFile, index) => {
|
||||
if (uploadingFile.guid === file.guid) {
|
||||
$scope.$root.uploadingFiles.splice(index, 1);
|
||||
$scope.$apply();
|
||||
|
@ -69,11 +69,10 @@ export default function dropzone(FileService, $cacheFactory) {
|
|||
return false;
|
||||
});
|
||||
});
|
||||
dropzoneEl.on('error', function(evt, error){
|
||||
dropzoneEl.on('error', (evt, error) => {
|
||||
if (error.error) {
|
||||
$scope.$root.uploadError = 'Error uploading file: ' + evt.name + '. ' + error.error.message;
|
||||
}
|
||||
else if (evt.name) {
|
||||
} else if (evt.name) {
|
||||
$scope.$root.uploadError = 'Error uploading file: ' + evt.name + '. ' + error;
|
||||
} else {
|
||||
if (error[0] !== '<') {
|
||||
|
@ -82,33 +81,33 @@ export default function dropzone(FileService, $cacheFactory) {
|
|||
}
|
||||
$scope.$apply();
|
||||
clearTimeout(errorTimeout);
|
||||
errorTimeout = setTimeout(function() {
|
||||
errorTimeout = setTimeout(() => {
|
||||
$scope.$root.uploadError = '';
|
||||
$scope.$apply();
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
var addFile = function(newFile) {
|
||||
if (!$scope.$root.files.some(function (file) {
|
||||
return file.id === newFile.id;
|
||||
})) {
|
||||
var cache = $cacheFactory.get('files-cache');
|
||||
const addFile = (newFile) => {
|
||||
if (!$scope.$root.files.some((file) => {
|
||||
return file.id === newFile.id;
|
||||
})) {
|
||||
const cache = $cacheFactory.get('files-cache');
|
||||
cache.removeAll();
|
||||
var file = new FileService(newFile);
|
||||
const file = new FileService(newFile);
|
||||
$scope.$root.files.unshift(file);
|
||||
$scope.$root.user.uploads_today++;
|
||||
$scope.$apply();
|
||||
}
|
||||
};
|
||||
|
||||
dropzoneEl.on('success', function(file, response){
|
||||
dropzoneEl.on('success', (file, response) => {
|
||||
addFile(response);
|
||||
});
|
||||
$scope.$on('file-added', function(event, data){
|
||||
$scope.$on('file-added', (event, data) => {
|
||||
addFile(data);
|
||||
});
|
||||
$scope.$on('file-accepted', function(event, data){
|
||||
$scope.$root.uploadingFiles.some(function(file) {
|
||||
$scope.$on('file-accepted', (event, data) => {
|
||||
$scope.$root.uploadingFiles.some((file) => {
|
||||
if (file.guid === data.guid) {
|
||||
file.id = data.id;
|
||||
file.href = data.href;
|
||||
|
@ -117,33 +116,33 @@ export default function dropzone(FileService, $cacheFactory) {
|
|||
}
|
||||
});
|
||||
});
|
||||
$scope.$on('file-deleted', function(evt, data) {
|
||||
$scope.$root.files.forEach(function(file, index) {
|
||||
if(data.id === file.id) {
|
||||
$scope.$on('file-deleted', (evt, data) => {
|
||||
$scope.$root.files.forEach((file, index) => {
|
||||
if (data.id === file.id) {
|
||||
delete $scope.$root.files[index];
|
||||
$scope.$digest();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.body.addEventListener('dragenter', function(){
|
||||
document.body.addEventListener('dragenter', () => {
|
||||
dropOverlay.style.display = 'block';
|
||||
});
|
||||
|
||||
dropOverlay.addEventListener('dragleave', function(event){
|
||||
dropOverlay.addEventListener('dragleave', (event) => {
|
||||
if (event.target.outerText !== 'Drop files to upload' || event.x === 0) {
|
||||
dropOverlay.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
dropOverlay.addEventListener('drop', function(){
|
||||
dropOverlay.addEventListener('drop', () => {
|
||||
dropOverlay.style.display = 'none';
|
||||
});
|
||||
} else {
|
||||
var clicker = [].slice.call(document.querySelectorAll('.choose-file'));
|
||||
const clicker = [].slice.call(document.querySelectorAll('.choose-file'));
|
||||
if (clicker) {
|
||||
clicker.forEach(function(el) {
|
||||
el.addEventListener('click', function() {
|
||||
clicker.forEach((el) => {
|
||||
el.addEventListener('click', () => {
|
||||
return dropzoneEl.hiddenFileInput.click();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,33 +1,29 @@
|
|||
export function fileSize() {
|
||||
return function(input) {
|
||||
return (input) => {
|
||||
if (input >= 1073741824) {
|
||||
input = Math.round((input / 1073741824) * 10) / 10 + 'GB';
|
||||
} else {
|
||||
if (input >= 1048576) {
|
||||
input = Math.round((input / 1048576) * 10) / 10 + 'MB';
|
||||
} else {
|
||||
if (input >= 1024) {
|
||||
input = Math.round((input / 1024) * 10) / 10 + 'KB';
|
||||
} else {
|
||||
input = Math.round(input) + 'B';
|
||||
}
|
||||
}
|
||||
return Math.round((input / 1073741824) * 10) / 10 + 'GB';
|
||||
}
|
||||
return input;
|
||||
if (input >= 1048576) {
|
||||
return Math.round((input / 1048576) * 10) / 10 + 'MB';
|
||||
}
|
||||
if (input >= 1024) {
|
||||
return Math.round((input / 1024) * 10) / 10 + 'KB';
|
||||
}
|
||||
return Math.round(input) + 'B';
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function direct() {
|
||||
return function(file) {
|
||||
if(file.name) {
|
||||
return (file) => {
|
||||
if (file.name) {
|
||||
if (file.direct && file.name.split('.').pop().toLowerCase() === 'psd') {
|
||||
return file.direct['970x'].replace('/970/', '/').slice(0, -4);
|
||||
} else if (file.direct && file.direct['970x']) {
|
||||
return file.direct['970x'].replace('/970/', '/');
|
||||
} else {
|
||||
return file.href.replace('hostr.co/', 'hostr.co/file/') + '/' + file.name;
|
||||
}
|
||||
if (file.direct && file.direct['970x']) {
|
||||
return file.direct['970x'].replace('/970/', '/');
|
||||
}
|
||||
return file.href.replace('hostr.co/', 'hostr.co/file/') + '/' + file.name;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,72 +1,92 @@
|
|||
export class FileService {
|
||||
constructor($resource, $cacheFactory) {
|
||||
var cache = $cacheFactory('files-cache');
|
||||
return $resource(window.settings.apiURL + '/file/:id', {id: '@id'}, {
|
||||
query: {method: 'GET', isArray: true, cache: cache,
|
||||
params: {perpage: 0, all: true}
|
||||
constructor($resource, $cacheFactory) {
|
||||
const cache = $cacheFactory('files-cache');
|
||||
return $resource(window.settings.apiURL + '/file/:id', {
|
||||
id: '@id',
|
||||
}, {
|
||||
query: {
|
||||
method: 'GET',
|
||||
isArray: true,
|
||||
cache: cache,
|
||||
params: {
|
||||
perpage: 0,
|
||||
all: true,
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
method: 'DELETE',
|
||||
isArray: true,
|
||||
cache: cache,
|
||||
},
|
||||
delete: {method: 'DELETE', isArray: true, cache: cache}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static factory($resource, $cacheFactory) {
|
||||
static factory($resource, $cacheFactory) {
|
||||
return new FileService($resource, $cacheFactory);
|
||||
}
|
||||
}
|
||||
|
||||
export class UserService {
|
||||
constructor($resource) {
|
||||
return $resource(window.settings.apiURL + '/user');
|
||||
}
|
||||
constructor($resource) {
|
||||
return $resource(window.settings.apiURL + '/user');
|
||||
}
|
||||
|
||||
static factory($resource) {
|
||||
static factory($resource) {
|
||||
return new UserService($resource);
|
||||
}
|
||||
}
|
||||
|
||||
export class EventService {
|
||||
constructor($rootScope, ReconnectingWebSocket) {
|
||||
if (window.user && WebSocket) {
|
||||
let ws = new ReconnectingWebSocket('wss' + window.settings.apiURL.replace('https', '').replace('http', '') + '/user');
|
||||
ws.onmessage = function (msg) {
|
||||
var evt = JSON.parse(msg.data);
|
||||
constructor($rootScope, ReconnectingWebSocket) {
|
||||
if (window.user && WebSocket) {
|
||||
const ws = new ReconnectingWebSocket('wss' + window.settings.apiURL.replace('https', '').replace('http', '') + '/user');
|
||||
ws.onmessage = (msg) => {
|
||||
const evt = JSON.parse(msg.data);
|
||||
$rootScope.$broadcast(evt.type, evt.data);
|
||||
};
|
||||
ws.onopen = function() {
|
||||
ws.send(JSON.stringify({authorization: window.user.token}));
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify({
|
||||
authorization: window.user.token,
|
||||
}));
|
||||
};
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static factory($rootScope, ReconnectingWebSocket) {
|
||||
return new EventService($rootScope, ReconnectingWebSocket);
|
||||
}
|
||||
static factory($rootScope, ReconnectingWebSocket) {
|
||||
return new EventService($rootScope, ReconnectingWebSocket);
|
||||
}
|
||||
}
|
||||
|
||||
export class TransactionService {
|
||||
constructor ($resource, $cacheFactory) {
|
||||
var cache = $cacheFactory('transaction-cache');
|
||||
return $resource(window.settings.apiURL + '/user/transaction/:id', {id: '@id'}, {
|
||||
query: {method: 'GET', isArray: true, cache: cache}
|
||||
constructor($resource, $cacheFactory) {
|
||||
const cache = $cacheFactory('transaction-cache');
|
||||
return $resource(window.settings.apiURL + '/user/transaction/:id', {
|
||||
id: '@id',
|
||||
}, {
|
||||
query: {
|
||||
method: 'GET',
|
||||
isArray: true,
|
||||
cache: cache,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static factory($resource, $cacheFactory) {
|
||||
static factory($resource, $cacheFactory) {
|
||||
return new TransactionService($resource, $cacheFactory);
|
||||
}
|
||||
}
|
||||
|
||||
export class SettingService {
|
||||
constructor ($http) {
|
||||
var service = {};
|
||||
service.update = function(data) {
|
||||
constructor($http) {
|
||||
const service = {};
|
||||
service.update = (data) => {
|
||||
return $http.post(window.settings.apiURL + '/user/settings', data);
|
||||
};
|
||||
return service;
|
||||
}
|
||||
|
||||
static factory($http) {
|
||||
static factory($http) {
|
||||
return new SettingService($http);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,39 +1,35 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import mime from 'mime-types';
|
||||
import hostrFileStream from '../../lib/hostr-file-stream';
|
||||
import { formatFile } from '../../lib/format';
|
||||
|
||||
import debugname from 'debug';
|
||||
const debug = debugname('hostr-web:file');
|
||||
|
||||
const storePath = process.env.STORE_PATH || path.join(process.env.HOME, '.hostr', 'uploads');
|
||||
|
||||
const userAgentCheck = function(userAgent) {
|
||||
if (!userAgent){
|
||||
function userAgentCheck(userAgent) {
|
||||
if (!userAgent) {
|
||||
return false;
|
||||
}
|
||||
return userAgent.match(/^(wget|curl|vagrant)/i);
|
||||
};
|
||||
}
|
||||
|
||||
const hotlinkCheck = function(file, userAgent, referrer) {
|
||||
return !userAgentCheck(userAgent) && !file.width && (!referrer || !(referrer.match(/^https:\/\/hostr.co/) || referrer.match(/^http:\/\/localhost:4040/)))
|
||||
};
|
||||
function hotlinkCheck(file, userAgent, referrer) {
|
||||
return !userAgentCheck(userAgent) && !file.width && (!referrer || !(referrer.match(/^https:\/\/hostr.co/) || referrer.match(/^http:\/\/localhost:4040/)));
|
||||
}
|
||||
|
||||
export function* get() {
|
||||
const file = yield this.db.Files.findOne({_id: this.params.id, 'file_name': this.params.name, 'status': 'active'});
|
||||
this.assert(file, 404);
|
||||
|
||||
if (hotlinkCheck(file, this.headers['user-agent'], this.headers['referer'])) {
|
||||
if (hotlinkCheck(file, this.headers['user-agent'], this.headers.referer)) {
|
||||
return this.redirect('/' + file._id);
|
||||
}
|
||||
|
||||
if (!file.width && this.request.query.warning != 'on') {
|
||||
if (!file.width && this.request.query.warning !== 'on') {
|
||||
return this.redirect('/' + file._id);
|
||||
}
|
||||
|
||||
if (file.malware) {
|
||||
let alert = this.request.query.alert;
|
||||
const alert = this.request.query.alert;
|
||||
if (!alert || !alert.match(/i want to download malware/i)) {
|
||||
return this.redirect('/' + file._id);
|
||||
}
|
||||
|
@ -70,9 +66,9 @@ export function* get() {
|
|||
|
||||
if (!this.params.size || (this.params.size && this.params.size > 150)) {
|
||||
this.db.Files.updateOne(
|
||||
{_id: file._id},
|
||||
{'$set': {'last_accessed': Math.ceil(Date.now()/1000)}, '$inc': {downloads: 1}},
|
||||
{w:0}
|
||||
{'_id': file._id},
|
||||
{'$set': {'last_accessed': Math.ceil(Date.now() / 1000)}, '$inc': {downloads: 1}},
|
||||
{'w': 0}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -86,7 +82,7 @@ export function* resized() {
|
|||
export function* landing() {
|
||||
const file = yield this.db.Files.findOne({_id: this.params.id, status: 'active'});
|
||||
this.assert(file, 404);
|
||||
if(userAgentCheck(this.headers['user-agent'])) {
|
||||
if (userAgentCheck(this.headers['user-agent'])) {
|
||||
this.params.name = file.file_name;
|
||||
return yield get.call(this);
|
||||
}
|
||||
|
|
|
@ -32,24 +32,24 @@ export function* staticPage(next) {
|
|||
this.session.user.token = token;
|
||||
yield this.render('index', {user: this.session.user});
|
||||
} else {
|
||||
switch(this.originalUrl) {
|
||||
case '/terms':
|
||||
yield this.render('terms');
|
||||
break;
|
||||
case '/privacy':
|
||||
yield this.render('privacy');
|
||||
break;
|
||||
case '/pricing':
|
||||
yield this.render('pricing');
|
||||
break;
|
||||
case '/apps':
|
||||
yield this.render('apps');
|
||||
break;
|
||||
case '/stats':
|
||||
yield this.render('index', {user: {}});
|
||||
break;
|
||||
default:
|
||||
yield next;
|
||||
switch (this.originalUrl) {
|
||||
case '/terms':
|
||||
yield this.render('terms');
|
||||
break;
|
||||
case '/privacy':
|
||||
yield this.render('privacy');
|
||||
break;
|
||||
case '/pricing':
|
||||
yield this.render('pricing');
|
||||
break;
|
||||
case '/apps':
|
||||
yield this.render('apps');
|
||||
break;
|
||||
case '/stats':
|
||||
yield this.render('index', {user: {}});
|
||||
break;
|
||||
default:
|
||||
yield next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ export function* create() {
|
|||
const createCustomer = {
|
||||
card: stripeToken.id,
|
||||
plan: 'usd_monthly',
|
||||
email: this.session.email
|
||||
email: this.session.email,
|
||||
};
|
||||
|
||||
const customer = yield stripe.customers.create(createCustomer);
|
||||
|
@ -32,7 +32,7 @@ export function* create() {
|
|||
'user_id': this.session.user.id,
|
||||
amount: customer.subscription.plan.amount,
|
||||
desc: customer.subscription.plan.name,
|
||||
date: new Date(customer.subscription.plan.created * 1000)
|
||||
date: new Date(customer.subscription.plan.created * 1000),
|
||||
};
|
||||
|
||||
yield Transactions.insertOne(transaction);
|
||||
|
@ -40,8 +40,8 @@ export function* create() {
|
|||
this.session.user.plan = 'Pro';
|
||||
this.body = {status: 'active'};
|
||||
|
||||
let html = yield render('email/inlined/pro');
|
||||
let text = `Hey, thanks for upgrading to Hostr Pro!
|
||||
const html = yield render('email/inlined/pro');
|
||||
const text = `Hey, thanks for upgrading to Hostr Pro!
|
||||
|
||||
You've signed up for Hostr Pro Monthly at $6/Month.
|
||||
|
||||
|
@ -56,11 +56,11 @@ export function* create() {
|
|||
'from_name': fromName,
|
||||
to: [{
|
||||
email: this.session.user.email,
|
||||
type: 'to'
|
||||
type: 'to',
|
||||
}],
|
||||
'tags': [
|
||||
'pro-upgrade'
|
||||
]
|
||||
'pro-upgrade',
|
||||
],
|
||||
}});
|
||||
}
|
||||
|
||||
|
|
|
@ -10,16 +10,15 @@ 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) {
|
||||
if (!user) {
|
||||
this.statsd.incr('auth.failure', 1);
|
||||
return yield this.render('signin', {error: 'Invalid login details', csrf: this.csrf});
|
||||
} else if (user.activationCode) {
|
||||
return yield this.render('signin', {error: 'Your account hasn\'t been activated yet. Check your for an activation email.', csrf: this.csrf});
|
||||
} else {
|
||||
this.statsd.incr('auth.success', 1);
|
||||
yield setupSession.call(this, user);
|
||||
this.redirect('/');
|
||||
}
|
||||
this.statsd.incr('auth.success', 1);
|
||||
yield setupSession.call(this, user);
|
||||
this.redirect('/');
|
||||
}
|
||||
|
||||
|
||||
|
@ -60,7 +59,7 @@ export function* forgot() {
|
|||
}
|
||||
this.assertCSRF(this.request.body);
|
||||
const tokenUser = yield validateResetToken.call(this, token);
|
||||
var userId = tokenUser._id;
|
||||
const userId = tokenUser._id;
|
||||
yield updatePassword.call(this, userId, this.request.body.password);
|
||||
yield Reset.deleteOne({_id: userId});
|
||||
const user = yield Users.findOne({_id: userId});
|
||||
|
@ -72,13 +71,12 @@ export function* forgot() {
|
|||
if (!tokenUser) {
|
||||
this.statsd.incr('auth.reset.fail', 1);
|
||||
return yield this.render('forgot', {error: 'Invalid password reset token. It might be expired, or has already been used.', token: null, csrf: this.csrf});
|
||||
} else {
|
||||
return yield this.render('forgot', {token: token, csrf: this.csrf});
|
||||
}
|
||||
return yield this.render('forgot', {token: token, csrf: this.csrf});
|
||||
} else if (this.request.body.email) {
|
||||
this.assertCSRF(this.request.body);
|
||||
try {
|
||||
var email = this.request.body.email;
|
||||
const email = this.request.body.email;
|
||||
yield sendResetToken.call(this, email);
|
||||
this.statsd.incr('auth.reset.request', 1);
|
||||
return yield this.render('forgot', {message: 'We\'ve sent an email with a link to reset your password. Be sure to check your spam folder if you it doesn\'t appear within a few minutes', token: null, csrf: this.csrf});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue