pulled data download into seperate plugin

This commit is contained in:
Wyatt Johnson
2018-04-09 15:52:40 -06:00
parent bb7f1a57f0
commit 5941bfac7c
25 changed files with 17 additions and 701 deletions
@@ -247,17 +247,5 @@ export default {
},
},
}),
DownloadCommentHistory: () => ({
updateQueries: {
CoralEmbedStream_Profile: previousData =>
update(previousData, {
me: {
lastAccountDownload: {
$set: new Date().toISOString(),
},
},
}),
},
}),
},
};
@@ -1,12 +0,0 @@
.button {
margin: 0;
i {
font-size: inherit;
vertical-align: sub;
}
}
.most_recent {
color: #808080;
}
@@ -1,109 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Button } from 'coral-ui';
import { compose, gql } from 'react-apollo';
import { t } from 'plugin-api/beta/client/services';
import styles from './DownloadCommentHistorySection.css';
import { withFragments, withMutation } from 'coral-framework/hocs';
export const readableDuration = durAsHours => {
const durAsDays = Math.ceil(durAsHours / 24);
return durAsHours > 23
? durAsDays > 1
? t('download_request.days', durAsDays)
: t('download_request.day', durAsDays)
: durAsHours > 1
? t('download_request.hours', durAsHours)
: t('download_request.hour', durAsHours);
};
class DownloadCommentHistorySection extends Component {
static propTypes = {
requestDownloadLink: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
};
render() {
const {
root: { me: { lastAccountDownload } },
requestDownloadLink,
} = this.props;
const now = new Date();
const lastAccountDownloadDate =
lastAccountDownload && new Date(lastAccountDownload);
const hoursLeft = lastAccountDownloadDate
? Math.ceil(
24 - (now.getTime() - lastAccountDownloadDate.getTime()) / 3.6e6
)
: 0;
const canRequestDownload = !lastAccountDownloadDate || hoursLeft <= 0;
return (
<section className={'talk-plugin-ignore-user-section'}>
<h3>{t('download_request.section_title')}</h3>
<p>
{t('download_request.you_will_get_a_copy')}{' '}
<b>{t('download_request.download_rate')}</b>.
</p>
{lastAccountDownloadDate && (
<p className={styles.most_recent}>
{t('download_request.most_recent_request')}:{' '}
{lastAccountDownloadDate.toLocaleString()}
</p>
)}
{canRequestDownload ? (
<Button className={styles.button} onClick={requestDownloadLink}>
<i className="material-icons" aria-hidden={true}>
file_download
</i>{' '}
{t('download_request.request')}
</Button>
) : (
<Button className={styles.button} disabled>
<i className="material-icons" aria-hidden={true}>
access_time
</i>{' '}
{t('download_request.rate_limit', readableDuration(hoursLeft))}
</Button>
)}
</section>
);
}
}
const withDownloadCommentHistorySectionFragments = withFragments({
root: gql`
fragment TalkDownloadCommentHistory_DownloadCommentHistorySection_root on RootQuery {
me {
id
lastAccountDownload
}
}
`,
});
const withRequestDownloadLink = withMutation(
gql`
mutation DownloadCommentHistory {
requestDownloadLink {
errors {
translation_key
}
}
}
`,
{
props: ({ mutate }) => ({
requestDownloadLink: () => mutate({ variables: {} }),
}),
}
);
const enhance = compose(
withDownloadCommentHistorySectionFragments,
withRequestDownloadLink
);
export default enhance(DownloadCommentHistorySection);
@@ -1,7 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Slot } from 'coral-framework/components';
import DownloadCommentHistorySection from './DownloadCommentHistorySection';
class Settings extends React.Component {
render() {
@@ -10,7 +9,6 @@ class Settings extends React.Component {
return (
<div>
<Slot fill="profileSettings" passthrough={slotPassthrough} />
<DownloadCommentHistorySection root={root} />
</div>
);
}
@@ -3,8 +3,6 @@ import { compose, gql } from 'react-apollo';
import Settings from '../components/Settings';
import { withFragments } from 'coral-framework/hocs';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import { getDefinitionName } from 'coral-framework/utils';
import DownloadCommentHistorySection from '../components/DownloadCommentHistorySection';
const slots = ['profileSettings'];
@@ -20,9 +18,7 @@ const enhance = compose(
fragment TalkEmbedStream_ProfileSettings_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
...${getDefinitionName(DownloadCommentHistorySection.fragments.root)}
}
${DownloadCommentHistorySection.fragments.root}
`,
})
);
-10
View File
@@ -9,7 +9,6 @@ const {
SET_USER_SUSPENSION_STATUS,
UPDATE_USER_ROLES,
DELETE_USER,
REQUEST_DOWNLOAD_LINK,
} = require('../../perms/constants');
const setUserUsernameStatus = async (ctx, id, status) => {
@@ -144,10 +143,6 @@ const delUser = async (ctx, id) => {
await user.remove();
};
// requestDownloadLink will request the current user's account be available via
// a download link sent to their email address.
const requestDownloadLink = async ({ user }) => Users.sendDownloadLink(user);
module.exports = ctx => {
let mutators = {
User: {
@@ -160,7 +155,6 @@ module.exports = ctx => {
setUsername: () => Promise.reject(errors.ErrNotAuthorized),
stopIgnoringUser: () => Promise.reject(errors.ErrNotAuthorized),
del: () => Promise.reject(errors.ErrNotAuthorized),
requestDownloadLink: () => Promise.reject(errors.ErrNotAuthorized),
},
};
@@ -200,10 +194,6 @@ module.exports = ctx => {
if (ctx.user.can(DELETE_USER)) {
mutators.User.del = id => delUser(ctx, id);
}
if (ctx.user.can(REQUEST_DOWNLOAD_LINK)) {
mutators.User.requestDownloadLink = () => requestDownloadLink(ctx);
}
}
return mutators;
+4
View File
@@ -58,6 +58,10 @@ const Comment = {
},
async url(comment, args, { loaders: { Assets } }) {
const asset = await Assets.getByID.load(comment.asset_id);
if (!asset) {
return null;
}
const assetURL = new URL(asset.url);
assetURL.searchParams.set('commentId', comment.id);
return assetURL.href;
-3
View File
@@ -139,9 +139,6 @@ const RootMutation = {
delUser: async (_, { id }, { mutators: { User } }) => {
await User.del(id);
},
requestDownloadLink: async (_, args, { mutators: { User } }) => {
await User.requestDownloadLink();
},
};
module.exports = RootMutation;
-2
View File
@@ -12,7 +12,6 @@ const {
LIST_OWN_TOKENS,
VIEW_USER_STATUS,
VIEW_USER_EMAIL,
VIEW_LAST_DOWNLOADED_DATE,
} = require('../../perms/constants');
const { property } = require('lodash');
@@ -65,7 +64,6 @@ decorateWithPermissionCheck(
tokens: [LIST_OWN_TOKENS],
profiles: [SEARCH_OTHER_USERS],
comments: [SEARCH_OTHERS_COMMENTS],
lastAccountDownload: [VIEW_LAST_DOWNLOADED_DATE],
},
checkSelfField('id')
);
-14
View File
@@ -238,10 +238,6 @@ type User {
# returns user status
state: UserState
# lastAccountDownload is the date that the user last requested a comment
# download.
lastAccountDownload: Date
}
# UserConnection represents a paginable subset of a user list.
@@ -1439,12 +1435,6 @@ type DelUserResponse implements Response {
errors: [UserError!]
}
type RequestDownloadLinkResponse implements Response {
# An array of errors relating to the mutation that occurred.
errors: [UserError!]
}
# All mutations for the application are defined on this object.
type RootMutation {
@@ -1545,10 +1535,6 @@ type RootMutation {
# delUser will delete the user with the specified id.
delUser(id: ID!): DelUserResponse
# requestDownloadLink will request a download link be sent to the primary
# users email address.
requestDownloadLink: RequestDownloadLinkResponse
}
type UsernameChangedPayload {
-19
View File
@@ -201,10 +201,6 @@ en:
we_received_a_request: "We received a request to reset your password. If you did not request this change, you can ignore this email."
if_you_did: "If you did,"
please_click: "please click here to reset password"
download:
subject: "Your download link is ready" # TODO: replace
download_link_ready: "Your download link is now ready, visit the following to download an archive of your account:" # TODO: replace
download_archive: "Download Archive" # TODO: replace
embedlink:
copy: "Copy to Clipboard"
error:
@@ -483,18 +479,3 @@ en:
admin_sidebar:
view_options: "View Options"
sort_comments: "Sort Comments"
download_request:
section_title: "Download My Comment History"
you_will_get_a_copy: "You will recieve an email with a link to download your comment history. You can make"
download_rate: "one download request every 24 hours"
most_recent_request: "Your most recent request"
request: "Request Comment History"
rate_limit: "You can submit another Comment History request in {0}"
hours: "{0} hours"
days: "{0} days"
hour: "{0} hour"
day: "{0} day"
download_landing:
download_your_account: "Download Your Account"
click_to_download: "Click below to download your account"
confirm: "Download"
-7
View File
@@ -191,13 +191,6 @@ const UserSchema = new Schema(
// Tags are added by the self or by administrators.
tags: [TagLinkSchema],
// lastAccountDownload is the date that the user last downloaded their
// account details.
lastAccountDownload: {
type: Date,
default: null,
},
// Additional metadata stored on the field.
metadata: {
default: {},
-2
View File
@@ -62,7 +62,6 @@
"apollo-server-express": "^1.2.0",
"apollo-utilities": "^1.0.3",
"app-module-path": "^2.2.0",
"archiver": "^2.1.1",
"autoprefixer": "^6.5.2",
"babel-cli": "6.26.0",
"babel-core": "6.26.0",
@@ -93,7 +92,6 @@
"copy-webpack-plugin": "^4.0.0",
"cross-spawn": "^5.1.0",
"css-loader": "^0.28.5",
"csv-stringify": "^2.0.4",
"dataloader": "^1.3.0",
"debug": "3.1.0",
"dialog-polyfill": "^0.4.9",
-1
View File
@@ -19,5 +19,4 @@ module.exports = {
UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS',
UPDATE_SETTINGS: 'UPDATE_SETTINGS',
DELETE_USER: 'DELETE_USER',
REQUEST_DOWNLOAD_LINK: 'REQUEST_DOWNLOAD_LINK',
};
-1
View File
@@ -10,5 +10,4 @@ module.exports = {
LIST_OWN_TOKENS: 'LIST_OWN_TOKENS',
VIEW_USER_ROLE: 'VIEW_USER_ROLE',
VIEW_USER_EMAIL: 'VIEW_USER_EMAIL',
VIEW_LAST_DOWNLOADED_DATE: 'VIEW_LAST_DOWNLOADED_DATE',
};
-1
View File
@@ -13,7 +13,6 @@ module.exports = (user, perm) => {
case types.CREATE_ACTION:
case types.DELETE_ACTION:
case types.EDIT_COMMENT:
case types.REQUEST_DOWNLOAD_LINK:
// Anyone can do these things if they aren't suspended, banned, or blocked
// as they're editing their username.
return !['UNSET', 'REJECTED', 'CHANGED'].includes(
-1
View File
@@ -13,7 +13,6 @@ module.exports = (user, perm) => {
case types.VIEW_PROTECTED_SETTINGS:
case types.VIEW_USER_ROLE:
case types.VIEW_USER_EMAIL:
case types.VIEW_LAST_DOWNLOADED_DATE:
return check(user, ['ADMIN', 'MODERATOR']);
case types.LIST_OWN_TOKENS:
return check(user, ['ADMIN']);
+4 -2
View File
@@ -2,7 +2,8 @@
"server": [
"talk-plugin-auth",
"talk-plugin-featured-comments",
"talk-plugin-respect"
"talk-plugin-respect",
"talk-plugin-profile-data"
],
"client": [
"talk-plugin-auth",
@@ -18,6 +19,7 @@
"talk-plugin-sort-most-respected",
"talk-plugin-sort-newest",
"talk-plugin-sort-oldest",
"talk-plugin-viewing-options"
"talk-plugin-viewing-options",
"talk-plugin-profile-data"
]
}
-4
View File
@@ -9,8 +9,4 @@ router.get('/password/reset', (req, res) => {
res.render('account/password/reset');
});
router.get('/download', (req, res) => {
res.render('account/download');
});
module.exports = router;
-140
View File
@@ -4,10 +4,6 @@ const UsersService = require('../../../services/users');
const mailer = require('../../../services/mailer');
const authorization = require('../../../middleware/authorization');
const errors = require('../../../errors');
const archiver = require('archiver');
const stringify = require('csv-stringify');
const moment = require('moment');
const { pick, get, kebabCase } = require('lodash');
// Return the current logged in user.
router.get('/', authorization.needed(), (req, res, next) => {
@@ -131,140 +127,4 @@ router.put(
}
);
// loadCommentsBatch will load a batch of the comments and write them to the
// stream.
async function loadCommentsBatch(ctx, csv, variables = {}) {
let result = await ctx.graphql(
`
query GetMyComments($cursor: Cursor) {
me {
comments(query: {
limit: 100,
cursor: $cursor
}) {
hasNextPage
endCursor
nodes {
id
created_at
asset {
url
}
body
url
}
}
}
}
`,
variables
);
if (result.errors) {
throw result.errors;
}
for (const comment of get(result, 'data.me.comments.nodes', [])) {
csv.write([
comment.id,
moment(comment.created_at).format('YYYY-MM-DD HH:mm:ss'),
comment.asset.url,
comment.url,
comment.body,
]);
}
return pick(result.data.me.comments, ['hasNextPage', 'endCursor']);
}
// loadComments will load batches of the comments and write them to the csv
// stream. Once the comments have finished writing, it will close the stream.
async function loadComments(ctx, archive, latestContentDate) {
// Create all the csv writers that'll write the data to the archive.
const csv = stringify();
// Add all the streams as files to the archive.
archive.append(csv, { name: 'talk-export/my_comments.csv' });
csv.write(['ID', 'Timestamp', 'Article', 'Link', 'Body']);
// Load the first batch's comments from the latest date that we were provided
// from the token.
let connection = await loadCommentsBatch(ctx, csv, {
cursor: latestContentDate,
});
// As long as there's more comments, keep paginating.
while (connection.hasNextPage) {
connection = await loadCommentsBatch(ctx, csv, {
cursor: connection.endCursor,
});
}
csv.end();
}
// /download will send back a zipped archive of the users account.
router.post(
'/download',
express.urlencoded({ extended: false }),
tokenCheck(UsersService.verifyDownloadToken, new Error('invalid token')),
async (req, res, next) => {
try {
const { token } = req.body;
// Pull the userID and the date that the token was issued out of the
// provided token.
const { user: userID, iat } = await UsersService.verifyDownloadToken(
token
);
// Unpack the date that the token was issued, and use it as a source for the
// earliest comment we should include in the download.
const latestContentDate = new Date(iat * 1000);
// Grab the user that we're generating the export from. We'll use it to
// create a new context.
const user = await UsersService.findById(userID);
// Base a new context off of the new user.
const ctx = req.context.masqueradeAs(user);
// Get the current user's username. We need it for the generated filenames.
const result = await ctx.graphql('{ me { username } }');
if (result.errors) {
throw result.errors;
}
const username = get(result, 'data.me.username');
// Generate the filename of the file that the user will download.
const filename = `talk-${kebabCase(username)}-${kebabCase(
moment(latestContentDate).format('YYYY-MM-DD HH:mm:ss')
)}.zip`;
res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename=${filename}`,
});
// Create the zip archive we'll use to write all the exported files to.
const archive = archiver('zip', {
zlib: { level: 9 },
});
// Pipe this to the response writer directly.
archive.pipe(res);
// Load the comments csv up with the user's comments.
await loadComments(ctx, archive, latestContentDate);
// Mark the end of adding files, no more files can be added after this. Once
// all the stream readers have finished writing, and have closed, the
// archiver will close which will finish the HTTP request.
archive.finalize();
} catch (err) {
return next(err);
}
}
);
module.exports = router;
@@ -1 +0,0 @@
<p><%= t('email.download.download_link_ready') %> <a href="<%= BASE_URL %>account/download#<%= token %>"><%= t('email.download.download_archive') %></a></p>
@@ -1,3 +0,0 @@
<%= t('email.download.download_link_ready') %>
<%= BASE_URL %>account/download#<%= token %>
-59
View File
@@ -14,11 +14,9 @@ const i18n = require('./i18n');
const Wordlist = require('./wordlist');
const DomainList = require('./domain_list');
const Limit = require('./limit');
const moment = require('moment');
const EMAIL_CONFIRM_JWT_SUBJECT = 'email_confirm';
const PASSWORD_RESET_JWT_SUBJECT = 'password_reset';
const DOWNLOAD_LINK_SUBJECT = 'download_link';
// SALT_ROUNDS is the number of rounds that the bcrypt algorithm will run
// through during the salting process.
@@ -31,10 +29,6 @@ const loginRateLimiter = new Limit(
RECAPTCHA_WINDOW
);
// downloadLinkLimiter can be used to limit downloads for the user's data to
// once every 24 hours.
const downloadLinkLimiter = new Limit('downloadLinkLimiter', 1, '1d');
// Users is the interface for the application to interact with the
// User through.
class Users {
@@ -472,59 +466,6 @@ class Users {
});
}
static async sendDownloadLink(user) {
// Check that the user has not already requested a download within the last
// 24 hours.
const attempts = await downloadLinkLimiter.get(user.id);
if (attempts && attempts >= 1) {
throw errors.ErrMaxRateLimit;
}
// Check if the lastAccountDownload time is within 24 hours.
if (
user.lastAccountDownload &&
moment(user.lastAccountDownload)
.add(24, 'hours')
.isAfter(moment())
) {
throw errors.ErrMaxRateLimit;
}
// The account currently does not have a download link, let's record the
// download. This will throw an error if a race ocurred and we should stop
// now.
await downloadLinkLimiter.test(user.id);
// Generate a token for the download link.
const token = await JWT_SECRET.sign(
{ user: user.id },
{ jwtid: uuid.v4(), expiresIn: '1d', subject: DOWNLOAD_LINK_SUBJECT }
);
// Send the download link via the user's attached email account.
await Users.sendEmail(user, {
template: 'download',
locals: {
token,
},
subject: i18n.t('email.download.subject'),
});
// Amend the lastAccountDownload on the user.
await User.update(
{ id: user.id },
{ $set: { lastAccountDownload: new Date() } }
);
}
static async verifyDownloadToken(token) {
const jwt = await Users.verifyToken(token, {
subject: DOWNLOAD_LINK_SUBJECT,
});
return jwt;
}
static async sendEmail(user, options) {
return mailer.send(
merge({}, options, {
-47
View File
@@ -1,47 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<title><%= t('download_landing.download_your_account') %></title>
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
<link rel="stylesheet" href="<%= BASE_PATH %>public/css/admin.css">
<%- include ../partials/head %>
</head>
<body class="confirm-email-page">
<div id="root">
<div class="error-console container"></div>
<form id="download-form" class="container" method="post" action="<%= BASE_PATH %>api/v1/account/download">
<legend class="legend"><%= t('download_landing.click_to_download') %></legend>
<button type="submit"><%= t('download_landing.confirm') %></button>
</form>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script type="text/javascript">
$(function() {
function showError(error) {
try {
let err = JSON.parse(error);
$('.error-console').text(err.message).addClass('active');
} catch (err) {
$('.error-console').text(error).addClass('active');
}
}
var token = location.hash.replace('#', '');
$.ajax({
url: '<%= BASE_PATH %>api/v1/account/download',
contentType: 'application/json',
method: 'POST',
data: JSON.stringify({token: token, check: true})
})
.then(function () {
$('#download-form').append('<input name="token" type="hidden" value="' + token + '"/>').fadeIn();
})
.catch(function (error) {
showError(error.responseText);
});
});
</script>
</body>
</html>
+9 -245
View File
@@ -68,7 +68,7 @@
lodash "^4.2.0"
to-fast-properties "^2.0.0"
"@coralproject/eslint-config-talk@^0.1.0", "@coralproject/eslint-config-talk@^0.1.1":
"@coralproject/eslint-config-talk@^0.1.0":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@coralproject/eslint-config-talk/-/eslint-config-talk-0.1.1.tgz#71991b4937a3ffe657128d7f1170da4b5fb75c9e"
dependencies:
@@ -174,13 +174,6 @@ accepts@^1.3.4, accepts@~1.3.4:
mime-types "~2.1.16"
negotiator "0.6.1"
accepts@~1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2"
dependencies:
mime-types "~2.1.18"
negotiator "0.6.1"
acorn-dynamic-import@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4"
@@ -235,10 +228,6 @@ acorn@^5.3.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102"
acorn@^5.5.0:
version "5.5.3"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9"
addressparser@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/addressparser/-/addressparser-1.0.1.tgz#47afbe1a2a9262191db6838e4fd1d39b40821746"
@@ -1805,13 +1794,6 @@ caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
casual@^1.5.19:
version "1.5.19"
resolved "https://registry.yarnpkg.com/casual/-/casual-1.5.19.tgz#66fac46f7ae463f468f5913eb139f9c41c58bbf2"
dependencies:
mersenne-twister "^1.0.1"
moment "^2.15.2"
center-align@^0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
@@ -2485,10 +2467,6 @@ crc32-stream@^2.0.0:
crc "^3.4.4"
readable-stream "^2.0.0"
crc@3.4.4:
version "3.4.4"
resolved "https://registry.yarnpkg.com/crc/-/crc-3.4.4.tgz#9da1e980e3bd44fc5c93bf5ab3da3378d85e466b"
crc@^3.4.4:
version "3.5.0"
resolved "https://registry.yarnpkg.com/crc/-/crc-3.5.0.tgz#98b8ba7d489665ba3979f59b21381374101a1964"
@@ -2723,9 +2701,9 @@ cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0", "cssom@>= 0.3.2 < 0.4.0":
dependencies:
cssom "0.3.x"
csv-stringify@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-2.0.4.tgz#9ace220df98ffa7ca91314ac77ff8cba0a08c863"
csv-stringify@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-3.0.0.tgz#ed2b4eaae5a3be382309f7864168458970307d7f"
dependencies:
lodash.get "~4.4.2"
@@ -2988,7 +2966,7 @@ dns-prefetch-control@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/dns-prefetch-control/-/dns-prefetch-control-0.1.0.tgz#60ddb457774e178f1f9415f0cabb0e85b0b300b2"
doctrine@^2.0.0, doctrine@^2.1.0:
doctrine@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
dependencies:
@@ -3129,10 +3107,6 @@ ejs@2.5.7, ejs@^2.5.7:
version "2.5.7"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a"
ejs@^2.5.8:
version "2.5.8"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.8.tgz#2ab6954619f225e6193b7ac5f7c39c48fefe4380"
electron-to-chromium@^1.2.7:
version "1.3.26"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.26.tgz#996427294861a74d9c7c82b9260ea301e8c02d66"
@@ -3428,49 +3402,6 @@ eslint-visitor-keys@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
eslint@^4.19.1:
version "4.19.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300"
dependencies:
ajv "^5.3.0"
babel-code-frame "^6.22.0"
chalk "^2.1.0"
concat-stream "^1.6.0"
cross-spawn "^5.1.0"
debug "^3.1.0"
doctrine "^2.1.0"
eslint-scope "^3.7.1"
eslint-visitor-keys "^1.0.0"
espree "^3.5.4"
esquery "^1.0.0"
esutils "^2.0.2"
file-entry-cache "^2.0.0"
functional-red-black-tree "^1.0.1"
glob "^7.1.2"
globals "^11.0.1"
ignore "^3.3.3"
imurmurhash "^0.1.4"
inquirer "^3.0.6"
is-resolvable "^1.0.0"
js-yaml "^3.9.1"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.3.0"
lodash "^4.17.4"
minimatch "^3.0.2"
mkdirp "^0.5.1"
natural-compare "^1.4.0"
optionator "^0.8.2"
path-is-inside "^1.0.2"
pluralize "^7.0.0"
progress "^2.0.0"
regexpp "^1.0.1"
require-uncached "^1.0.3"
semver "^5.3.0"
strip-ansi "^4.0.0"
strip-json-comments "~2.0.1"
table "4.0.2"
text-table "~0.2.0"
eslint@^4.5.0:
version "4.13.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.13.1.tgz#0055e0014464c7eb7878caf549ef2941992b444f"
@@ -3520,13 +3451,6 @@ espree@^3.5.2:
acorn "^5.2.1"
acorn-jsx "^3.0.0"
espree@^3.5.4:
version "3.5.4"
resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
dependencies:
acorn "^5.5.0"
acorn-jsx "^3.0.0"
esprima@3.x.x, esprima@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
@@ -3701,20 +3625,6 @@ exports-loader@^0.6.4:
loader-utils "^1.0.2"
source-map "0.5.x"
express-session@^1.15.6:
version "1.15.6"
resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.15.6.tgz#47b4160c88f42ab70fe8a508e31cbff76757ab0a"
dependencies:
cookie "0.3.1"
cookie-signature "1.0.6"
crc "3.4.4"
debug "2.6.9"
depd "~1.1.1"
on-headers "~1.0.1"
parseurl "~1.3.2"
uid-safe "~2.1.5"
utils-merge "1.0.1"
express-static-gzip@^0.3.1:
version "0.3.2"
resolved "https://registry.yarnpkg.com/express-static-gzip/-/express-static-gzip-0.3.2.tgz#89ede84547a5717de3146315f62dc996c071a88d"
@@ -3756,41 +3666,6 @@ express@4.16.0, express@^4.12.2:
utils-merge "1.0.1"
vary "~1.1.2"
express@^4.16.3:
version "4.16.3"
resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53"
dependencies:
accepts "~1.3.5"
array-flatten "1.1.1"
body-parser "1.18.2"
content-disposition "0.5.2"
content-type "~1.0.4"
cookie "0.3.1"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~1.1.2"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "1.1.1"
fresh "0.5.2"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "~2.3.0"
parseurl "~1.3.2"
path-to-regexp "0.1.7"
proxy-addr "~2.0.3"
qs "6.5.1"
range-parser "~1.2.0"
safe-buffer "5.1.1"
send "0.16.2"
serve-static "1.13.2"
setprototypeof "1.1.0"
statuses "~1.4.0"
type-is "~1.6.16"
utils-merge "1.0.1"
vary "~1.1.2"
extend-shallow@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@@ -3988,18 +3863,6 @@ finalhandler@1.1.0:
statuses "~1.3.1"
unpipe "~1.0.0"
finalhandler@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105"
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
escape-html "~1.0.3"
on-finished "~2.3.0"
parseurl "~1.3.2"
statuses "~1.4.0"
unpipe "~1.0.0"
find-cache-dir@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f"
@@ -5306,10 +5169,6 @@ ipaddr.js@1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0"
ipaddr.js@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.6.0.tgz#e3fa357b773da619f26e95f049d055c72796f86b"
is-absolute-url@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
@@ -7172,10 +7031,6 @@ merge@^1.1.3:
version "1.2.0"
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
mersenne-twister@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mersenne-twister/-/mersenne-twister-1.1.0.tgz#f916618ee43d7179efcf641bec4531eb9670978a"
metascraper-author@^3.9.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/metascraper-author/-/metascraper-author-3.9.2.tgz#ff2020ac428f59a875d655df3b0d4bea171fde19"
@@ -7316,7 +7171,7 @@ miller-rabin@^4.0.0:
version "1.30.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
"mime-db@>= 1.33.0 < 2", mime-db@~1.33.0:
"mime-db@>= 1.33.0 < 2":
version "1.33.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
@@ -7326,12 +7181,6 @@ mime-types@^2.1.10, mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16,
dependencies:
mime-db "~1.30.0"
mime-types@~2.1.18:
version "2.1.18"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
dependencies:
mime-db "~1.33.0"
mime@1.4.1, mime@^1.3.4, mime@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
@@ -7464,10 +7313,6 @@ moment@^2.10.3:
version "2.19.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.1.tgz#56da1a2d1cbf01d38b7e1afc31c10bcfa1929167"
moment@^2.15.2:
version "2.22.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.0.tgz#7921ade01017dd45186e7fee5f424f0b8663a730"
mongodb-core@2.1.17:
version "2.1.17"
resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-2.1.17.tgz#a418b337a14a14990fb510b923dee6a813173df8"
@@ -8425,15 +8270,6 @@ passport-oauth2@1.x.x, passport-oauth2@^1.1.2:
uid2 "0.0.x"
utils-merge "1.x.x"
passport-openidconnect@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/passport-openidconnect/-/passport-openidconnect-0.0.2.tgz#e488f8bdb386c9a9fd39c91d5ab8c880156e8153"
dependencies:
oauth "0.9.x"
passport-strategy "1.x.x"
request "^2.75.0"
webfinger "0.4.x"
passport-strategy@1.x.x, passport-strategy@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
@@ -9158,13 +8994,6 @@ proxy-addr@~2.0.2:
forwarded "~0.1.2"
ipaddr.js "1.5.2"
proxy-addr@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341"
dependencies:
forwarded "~0.1.2"
ipaddr.js "1.6.0"
proxy-agent@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-2.0.0.tgz#57eb5347aa805d74ec681cb25649dba39c933499"
@@ -9415,10 +9244,6 @@ randexp@^0.4.2:
discontinuous-range "1.0.0"
ret "~0.1.10"
random-bytes@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
randomatic@^1.1.3:
version "1.1.7"
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
@@ -9889,10 +9714,6 @@ regexp-clone@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-0.0.1.tgz#a7c2e09891fdbf38fbb10d376fb73003e68ac589"
regexpp@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab"
regexpu-core@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b"
@@ -10043,33 +9864,6 @@ request@2.81.0:
tunnel-agent "^0.6.0"
uuid "^3.0.0"
request@^2.75.0:
version "2.85.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa"
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.6.0"
caseless "~0.12.0"
combined-stream "~1.0.5"
extend "~3.0.1"
forever-agent "~0.6.1"
form-data "~2.3.1"
har-validator "~5.0.3"
hawk "~6.0.2"
http-signature "~1.2.0"
is-typedarray "~1.0.0"
isstream "~0.1.2"
json-stringify-safe "~5.0.1"
mime-types "~2.1.17"
oauth-sign "~0.8.2"
performance-now "^2.1.0"
qs "~6.5.1"
safe-buffer "^5.1.1"
stringstream "~0.0.5"
tough-cookie "~2.3.3"
tunnel-agent "^0.6.0"
uuid "^3.1.0"
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -10284,7 +10078,7 @@ sax@0.5.x:
version "0.5.8"
resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.8.tgz#d472db228eb331c2506b0e8c15524adb939d12c1"
sax@>=0.1.1, sax@^1.1.4, sax@^1.2.1, sax@^1.2.4, sax@~1.2.1:
sax@^1.1.4, sax@^1.2.1, sax@^1.2.4, sax@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@@ -10423,7 +10217,7 @@ serve-static@1.13.0:
parseurl "~1.3.2"
send "0.16.0"
serve-static@1.13.2, serve-static@^1.10.0:
serve-static@^1.10.0:
version "1.13.2"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1"
dependencies:
@@ -10840,10 +10634,6 @@ stealthy-require@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
step@0.0.x:
version "0.0.6"
resolved "https://registry.yarnpkg.com/step/-/step-0.0.6.tgz#143e7849a5d7d3f4a088fe29af94915216eeede2"
stream-browserify@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
@@ -11141,7 +10931,7 @@ symbol-observable@^1.0.2, symbol-observable@^1.0.3, symbol-observable@^1.0.4:
version "3.2.2"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
table@4.0.2, table@^4.0.1:
table@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36"
dependencies:
@@ -11470,13 +11260,6 @@ type-is@~1.6.15:
media-typer "0.3.0"
mime-types "~2.1.15"
type-is@~1.6.16:
version "1.6.16"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
dependencies:
media-typer "0.3.0"
mime-types "~2.1.18"
typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@@ -11551,12 +11334,6 @@ uid-number@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
uid-safe@~2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a"
dependencies:
random-bytes "~1.0.0"
uid2@0.0.x:
version "0.0.3"
resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82"
@@ -11847,13 +11624,6 @@ watchpack@^1.4.0:
chokidar "^1.7.0"
graceful-fs "^4.1.2"
webfinger@0.4.x:
version "0.4.2"
resolved "https://registry.yarnpkg.com/webfinger/-/webfinger-0.4.2.tgz#3477a6d97799461896039fcffc650b73468ee76d"
dependencies:
step "0.0.x"
xml2js "0.1.x"
webidl-conversions@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-2.0.1.tgz#3bf8258f7d318c7443c36f2e169402a1a6703506"
@@ -12090,12 +11860,6 @@ xml-name-validator@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
xml2js@0.1.x:
version "0.1.14"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.1.14.tgz#5274e67f5a64c5f92974cd85139e0332adc6b90c"
dependencies:
sax ">=0.1.1"
xml@^1.0.0, xml@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"