mirror of
https://github.com/wassname/talk.git
synced 2026-08-01 13:00:55 +08:00
added direct download mutation
This commit is contained in:
@@ -10,6 +10,9 @@ const secrets = require('../secrets');
|
||||
// Errors.
|
||||
const errors = require('../errors');
|
||||
|
||||
// URLs.
|
||||
const url = require('../url');
|
||||
|
||||
// Graph.
|
||||
const { getBroker } = require('./subscriptions/broker');
|
||||
const { getPubsub } = require('./subscriptions/pubsub');
|
||||
@@ -58,6 +61,7 @@ const defaultConnectors = {
|
||||
errors,
|
||||
config,
|
||||
secrets,
|
||||
url,
|
||||
models: {
|
||||
Action,
|
||||
Asset,
|
||||
|
||||
@@ -1 +1 @@
|
||||
<p><%= t('email.download.download_link_ready', organizationName, now.toLocaleString()) %> <a href="<%= BASE_URL %>account/download#<%= token %>"><%= t('email.download.download_archive') %></a></p>
|
||||
<p><%= t('email.download.download_link_ready', organizationName, now.toLocaleString()) %> <a href="<%= downloadLandingURL %>"><%= t('email.download.download_archive') %></a></p>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<%= t('email.download.download_link_ready', organizationName, now.toLocaleString()) %>
|
||||
|
||||
<%= BASE_URL %>account/download#<%= token %>
|
||||
<%= downloadLandingURL %>
|
||||
|
||||
@@ -1,17 +1,41 @@
|
||||
const moment = require('moment');
|
||||
const uuid = require('uuid/v4');
|
||||
const { DOWNLOAD_LINK_SUBJECT } = require('./constants');
|
||||
const { ErrNotAuthorized, ErrMaxRateLimit } = require('errors');
|
||||
const { URL } = require('url');
|
||||
|
||||
// generateDownloadLinks will generate a signed set of links for a given user to
|
||||
// download an archive of their data.
|
||||
async function generateDownloadLinks(ctx, userID) {
|
||||
const { connectors: { url: { BASE_URL }, secrets } } = ctx;
|
||||
|
||||
// Generate a token for the download link.
|
||||
const token = await secrets.jwt.sign(
|
||||
{ user: userID },
|
||||
{ jwtid: uuid.v4(), expiresIn: '1d', subject: DOWNLOAD_LINK_SUBJECT }
|
||||
);
|
||||
|
||||
// Generate the url that a user can land on.
|
||||
const downloadLandingURL = new URL('account/download', BASE_URL);
|
||||
downloadLandingURL.hash = token;
|
||||
|
||||
// Generate the url that the API calls to download the actual zip.
|
||||
const downloadFileURL = new URL('api/v1/account/download', BASE_URL);
|
||||
downloadFileURL.searchParams.set('token', token);
|
||||
|
||||
return {
|
||||
downloadLandingURL: downloadLandingURL.href,
|
||||
downloadFileURL: downloadFileURL.href,
|
||||
};
|
||||
}
|
||||
|
||||
async function sendDownloadLink(ctx) {
|
||||
const {
|
||||
user,
|
||||
loaders: { Settings },
|
||||
connectors: { services: { Users, I18n, Limit }, models: { User } },
|
||||
} = ctx;
|
||||
|
||||
async function sendDownloadLink({
|
||||
user,
|
||||
loaders: { Settings },
|
||||
connectors: {
|
||||
errors,
|
||||
secrets,
|
||||
services: { Users, I18n, Limit },
|
||||
models: { User },
|
||||
},
|
||||
}) {
|
||||
// downloadLinkLimiter can be used to limit downloads for the user's data to
|
||||
// once every 7 days.
|
||||
const downloadLinkLimiter = new Limit('profileDataDownloadLimiter', 1, '7d');
|
||||
@@ -20,7 +44,7 @@ async function sendDownloadLink({
|
||||
// 7 days.
|
||||
const attempts = await downloadLinkLimiter.get(user.id);
|
||||
if (attempts && attempts >= 1) {
|
||||
throw errors.ErrMaxRateLimit;
|
||||
throw new ErrMaxRateLimit();
|
||||
}
|
||||
|
||||
// Check if the lastAccountDownload time is within 7 days.
|
||||
@@ -30,7 +54,7 @@ async function sendDownloadLink({
|
||||
.add(7, 'days')
|
||||
.isAfter(moment())
|
||||
) {
|
||||
throw errors.ErrMaxRateLimit;
|
||||
throw new ErrMaxRateLimit();
|
||||
}
|
||||
|
||||
// The account currently does not have a download link, let's record the
|
||||
@@ -38,21 +62,18 @@ async function sendDownloadLink({
|
||||
// now.
|
||||
await downloadLinkLimiter.test(user.id);
|
||||
|
||||
// Generate a token for the download link.
|
||||
const token = await secrets.jwt.sign(
|
||||
{ user: user.id },
|
||||
{ jwtid: uuid.v4(), expiresIn: '1d', subject: DOWNLOAD_LINK_SUBJECT }
|
||||
);
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// Generate the download links.
|
||||
const { downloadLandingURL } = await generateDownloadLinks(ctx, user.id);
|
||||
|
||||
const { organizationName } = await Settings.load('organizationName');
|
||||
|
||||
// Send the download link via the user's attached email account.
|
||||
await Users.sendEmail(user, {
|
||||
template: 'download',
|
||||
locals: {
|
||||
token,
|
||||
downloadLandingURL,
|
||||
organizationName,
|
||||
now,
|
||||
},
|
||||
@@ -66,8 +87,20 @@ async function sendDownloadLink({
|
||||
);
|
||||
}
|
||||
|
||||
// downloadUser will return the download file url that can be used to directly
|
||||
// download the archive.
|
||||
async function downloadUser(ctx, userID) {
|
||||
const { downloadFileURL } = await generateDownloadLinks(ctx, userID);
|
||||
return downloadFileURL;
|
||||
}
|
||||
|
||||
module.exports = ctx => ({
|
||||
User: {
|
||||
requestDownloadLink: () => sendDownloadLink(ctx),
|
||||
download:
|
||||
// Only ADMIN users can execute an account download.
|
||||
ctx.user && ctx.user.role === 'ADMIN'
|
||||
? userID => downloadUser(ctx, userID)
|
||||
: () => Promise.reject(new ErrNotAuthorized()),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,6 +5,9 @@ module.exports = {
|
||||
requestDownloadLink: async (_, args, { mutators: { User } }) => {
|
||||
await User.requestDownloadLink();
|
||||
},
|
||||
downloadUser: async (_, { id }, { mutators: { User } }) => ({
|
||||
archiveURL: await User.download(id),
|
||||
}),
|
||||
},
|
||||
User: {
|
||||
lastAccountDownload: (user, args, { user: currentUser }) => {
|
||||
|
||||
@@ -101,11 +101,21 @@ module.exports = router => {
|
||||
|
||||
// /api/v1/account/download will send back a zipped archive of the users
|
||||
// account.
|
||||
router.post(
|
||||
router.all(
|
||||
'/api/v1/account/download',
|
||||
express.urlencoded({ extended: false }),
|
||||
async (req, res, next) => {
|
||||
const { token = null, check = false } = req.body;
|
||||
let { token = null, check = false } = req.body;
|
||||
|
||||
if (!token) {
|
||||
// If the token wasn't found in the body, then we should check the query
|
||||
// to see if it was passed that way.
|
||||
token = req.query.token;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return res.status(400).end();
|
||||
}
|
||||
|
||||
if (check) {
|
||||
// This request is checking to see if the token is valid.
|
||||
|
||||
@@ -11,9 +11,23 @@ type RequestDownloadLinkResponse implements Response {
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type DownloadUserResponse implements Response {
|
||||
|
||||
# archiveURL is the link that can be used within the next 1 hour to download a
|
||||
# users archive.
|
||||
archiveURL: String
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type RootMutation {
|
||||
|
||||
# requestDownloadLink will request a download link be sent to the primary
|
||||
# users email address.
|
||||
requestDownloadLink: RequestDownloadLinkResponse
|
||||
|
||||
# downloadUser will provide an account download for the indicated User. This
|
||||
# mutation requires the ADMIN role.
|
||||
downloadUser(id: ID!): DownloadUserResponse
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user