From 0e37a474fa900bc409fdfb9acd820f340608fcf8 Mon Sep 17 00:00:00 2001 From: Tessa Thornton Date: Fri, 23 Aug 2019 16:05:54 -0400 Subject: [PATCH] downloads by user id (#2500) --- .../app/handlers/api/account/download.ts | 42 +++++++++++++++++++ src/core/server/app/router/api/account.ts | 2 + .../server/graph/tenant/mutators/Users.ts | 13 ++++++ .../server/graph/tenant/resolvers/Mutation.ts | 4 ++ .../server/graph/tenant/schema/schema.graphql | 34 +++++++++++++++ .../services/users/download/download.ts | 42 ++++++++++++++++++- src/core/server/services/users/index.ts | 24 ++++++++++- 7 files changed, 158 insertions(+), 3 deletions(-) diff --git a/src/core/server/app/handlers/api/account/download.ts b/src/core/server/app/handlers/api/account/download.ts index 043a96905..366bdd2bf 100644 --- a/src/core/server/app/handlers/api/account/download.ts +++ b/src/core/server/app/handlers/api/account/download.ts @@ -27,6 +27,8 @@ export type DownloadOptions = Pick< "mongo" | "redis" | "signingConfig" | "config" >; +export type AdminDownloadOptions = Pick; + async function sendExport( mongo: Db, tenant: Tenant, @@ -216,6 +218,46 @@ export const downloadHandler = ({ }; }; +export const adminDownloadHandler = ({ + mongo, + signingConfig, +}: AdminDownloadOptions): RequestHandler => { + return async (req: Request, res, next) => { + // Tenant is guaranteed at this point. + const coral = req.coral!; + const tenant = coral.tenant!; + const { token } = req.query; + + const { sub: userID } = decodeJWT(token); + if (!userID) { + return res.sendStatus(400); + } + + try { + const { + token: { iat }, + user, + } = await verifyDownloadTokenString( + mongo, + tenant, + signingConfig, + token, + coral.now + ); + + // Only load comments since this download token was issued. + const latestContentDate = new Date(iat * 1000); + + // Send the export down the response. + await sendExport(mongo, tenant, user, latestContentDate, res); + + return; + } catch (err) { + return next(err); + } + }; +}; + export type DownloadCheckOptions = Pick< AppOptions, "mongo" | "redis" | "signingConfig" | "config" diff --git a/src/core/server/app/router/api/account.ts b/src/core/server/app/router/api/account.ts index 6be6821bf..1b59eb26a 100644 --- a/src/core/server/app/router/api/account.ts +++ b/src/core/server/app/router/api/account.ts @@ -3,6 +3,7 @@ import express from "express"; import { AppOptions } from "coral-server/app"; import { + adminDownloadHandler, confirmCheckHandler, confirmHandler, confirmRequestHandler, @@ -34,6 +35,7 @@ export function createNewAccountRouter( router.put("/invite", jsonMiddleware, inviteHandler(app)); router.get("/downloadcheck", downloadCheckHandler(app)); + router.get("/download", adminDownloadHandler(app)); router.post( "/download", bodyParser.urlencoded({ diff --git a/src/core/server/graph/tenant/mutators/Users.ts b/src/core/server/graph/tenant/mutators/Users.ts index a7c300491..a2c1805d2 100644 --- a/src/core/server/graph/tenant/mutators/Users.ts +++ b/src/core/server/graph/tenant/mutators/Users.ts @@ -11,6 +11,7 @@ import { removeIgnore, removeSuspension, requestCommentsDownload, + requestUserCommentsDownload, setEmail, setPassword, setUsername, @@ -35,6 +36,7 @@ import { GQLRemoveUserIgnoreInput, GQLRemoveUserSuspensionInput, GQLRequestCommentsDownloadInput, + GQLRequestUserCommentsDownloadInput, GQLSetEmailInput, GQLSetPasswordInput, GQLSetUsernameInput, @@ -187,6 +189,17 @@ export const Users = (ctx: TenantContext) => ({ ignore(ctx.mongo, ctx.tenant, ctx.user!, input.userID, ctx.now), removeIgnore: async (input: GQLRemoveUserIgnoreInput) => removeIgnore(ctx.mongo, ctx.tenant, ctx.user!, input.userID), + requestUserCommentsDownload: async ( + input: GQLRequestUserCommentsDownloadInput + ) => + requestUserCommentsDownload( + ctx.mongo, + ctx.tenant, + ctx.config, + ctx.signingConfig!, + input.userID, + ctx.now + ), requestCommentsDownload: async (input: GQLRequestCommentsDownloadInput) => requestCommentsDownload( ctx.mongo, diff --git a/src/core/server/graph/tenant/resolvers/Mutation.ts b/src/core/server/graph/tenant/resolvers/Mutation.ts index fe952618a..143ea9603 100644 --- a/src/core/server/graph/tenant/resolvers/Mutation.ts +++ b/src/core/server/graph/tenant/resolvers/Mutation.ts @@ -189,4 +189,8 @@ export const Mutation: Required> = { user: await ctx.mutators.Users.requestCommentsDownload(input), clientMutationId: input.clientMutationId, }), + requestUserCommentsDownload: async (sourc, { input }, ctx) => ({ + archiveURL: await ctx.mutators.Users.requestUserCommentsDownload(input), + clientMutationId: input.clientMutationId, + }), }; diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 86255fdf5..08b5509d4 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -4579,6 +4579,33 @@ type RequestCommentsDownloadPayload { clientMutationId: String! } +######################### +# requestUserCommentsDownload +######################### + +input RequestUserCommentsDownloadInput { + """ + userID specifies user to download comments for + """ + userID: String! + + """ + clientMutationId is required for Relay support. + """ + clientMutationId: String! +} + +type RequestUserCommentsDownloadPayload { + """ + clientMutationId is required for Relay support. + """ + clientMutationId: String! + + """ + archiveURL is the archive url + """ + archiveURL: String! +} ################## ## Mutation ################## @@ -4862,6 +4889,13 @@ type Mutation { requestCommentsDownload( input: RequestCommentsDownloadInput! ): RequestCommentsDownloadPayload! @auth(permit: [SUSPENDED, BANNED]) + + """ + requestUserCommentsDownload allows a user to request to download their comments. + """ + requestUserCommentsDownload( + input: RequestUserCommentsDownloadInput! + ): RequestUserCommentsDownloadPayload! @auth(roles: [ADMIN]) } ################## diff --git a/src/core/server/services/users/download/download.ts b/src/core/server/services/users/download/download.ts index adc3c27d0..fd2f4f55d 100644 --- a/src/core/server/services/users/download/download.ts +++ b/src/core/server/services/users/download/download.ts @@ -25,7 +25,7 @@ const DownloadTokenSchema = StandardClaimsSchema.keys({ aud: Joi.string().only("download"), }); -export async function generateDownloadLink( +export async function generateDownloadToken( userID: string, tenant: Tenant, config: Config, @@ -46,7 +46,23 @@ export async function generateDownloadLink( aud: "download", }; - const token = await signString(signingConfig, downloadToken); + return await signString(signingConfig, downloadToken); +} + +export async function generateDownloadLink( + userID: string, + tenant: Tenant, + config: Config, + signingConfig: JWTSigningConfig, + now: Date +) { + const token = await generateDownloadToken( + userID, + tenant, + config, + signingConfig, + now + ); return constructTenantURL( config, @@ -55,6 +71,28 @@ export async function generateDownloadLink( ); } +export async function generateAdminDownloadLink( + userID: string, + tenant: Tenant, + config: Config, + signingConfig: JWTSigningConfig, + now: Date +) { + const token = await generateDownloadToken( + userID, + tenant, + config, + signingConfig, + now + ); + + return constructTenantURL( + config, + tenant, + `/api/account/download?token=${token}` + ); +} + export function validateDownloadToken( token: DownloadToken | object ): Error | null { diff --git a/src/core/server/services/users/index.ts b/src/core/server/services/users/index.ts index 3350ce96e..6c4506b71 100644 --- a/src/core/server/services/users/index.ts +++ b/src/core/server/services/users/index.ts @@ -65,7 +65,10 @@ import { sendConfirmationEmail } from "coral-server/services/users/auth"; import { JWTSigningConfig, signPATString } from "coral-server/services/jwt"; -import { generateDownloadLink } from "./download/download"; +import { + generateAdminDownloadLink, + generateDownloadLink, +} from "./download/download"; import { validateEmail, validatePassword, validateUsername } from "./helpers"; export type InsertUser = InsertUserInput; @@ -917,3 +920,22 @@ export async function requestCommentsDownload( return user; } + +export async function requestUserCommentsDownload( + mongo: Db, + tenant: Tenant, + config: Config, + signingConfig: JWTSigningConfig, + userID: string, + now: Date +) { + const downloadUrl = await generateAdminDownloadLink( + userID, + tenant, + config, + signingConfig, + now + ); + + return downloadUrl; +}