diff --git a/README.md b/README.md index f24c8d7d..f9c87002 100644 --- a/README.md +++ b/README.md @@ -221,15 +221,6 @@ $ node build/scripts/removeArticleReply.js --userId= --articleId= --blockedReason= -``` - -- For more options, run the above script with `--help` or see the file level comments. - ### Replace the media of an article - This command replaces all the variants of a media article's file on GCS with the variants of the new file. diff --git a/src/adm/README.md b/src/adm/README.md index 7cab4adb..b225c564 100644 --- a/src/adm/README.md +++ b/src/adm/README.md @@ -15,3 +15,17 @@ curl -XPOST -H "CF-Access-Client-Id: " -H "CF-Access-Client-Secret: < ``` The response would attach a cookie named `CF_Authorization` that you may use for [subsequent requests](https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/#subsequent-requests). + +## Sending request via Swagger UI + +You can send test requests in this Swagger UI in the browser using your current login session. + +However, since different APIs are managed by different Cloudflare Access Applications, your current +login session may not yet be authorized to the API you want to call. In this case, you may see your +request sent in Swagger UI being redirected to Cloudflare, and is then blocked by the browser. + +To authorize your current login session to an API, try visiting the API path directly. +For example, in order to call `/moderation/blockUser`, you can first [visit `/moderation`](/moderation) directly in your browser. +Cloudflare will do the authorization and redirect you to the 404 page. +By that time your login session cookie should have been updated, and you can then call +`/moderation/blockUser` in `/docs`'s Swagger UI. diff --git a/src/scripts/__fixtures__/blockUser.js b/src/adm/handlers/moderation/__fixtures__/blockUser.js similarity index 100% rename from src/scripts/__fixtures__/blockUser.js rename to src/adm/handlers/moderation/__fixtures__/blockUser.js diff --git a/src/scripts/__tests__/blockUser.js b/src/adm/handlers/moderation/__tests__/blockUser.js similarity index 93% rename from src/scripts/__tests__/blockUser.js rename to src/adm/handlers/moderation/__tests__/blockUser.js index 34a1634e..959319c8 100644 --- a/src/scripts/__tests__/blockUser.js +++ b/src/adm/handlers/moderation/__tests__/blockUser.js @@ -7,10 +7,10 @@ beforeEach(() => loadFixtures(fixtures)); afterEach(() => unloadFixtures(fixtures)); it('fails if userId is not valid', async () => { - expect( + await expect( blockUser({ userId: 'not-exist', blockedReason: 'announcement url' }) ).rejects.toMatchInlineSnapshot( - `[Error: User with ID=not-exist does not exist]` + `[HTTPError: User with ID=not-exist does not exist]` ); }); @@ -29,11 +29,20 @@ async function expectSameAsFixture(fixtureKey, clientGetArgs) { } it('correctly sets the block reason and updates status of their works', async () => { - await blockUser({ + const result = await blockUser({ userId: 'user-to-block', blockedReason: 'announcement url', }); + expect(result).toMatchInlineSnapshot(` + Object { + "updateArticleReplyFeedbacks": 1, + "updatedArticleReplies": 1, + "updatedArticles": 1, + "updatedReplyRequests": 1, + } + `); + const { body: { _source: blockedUser }, } = await client.get({ diff --git a/src/scripts/blockUser.ts b/src/adm/handlers/moderation/blockUser.ts similarity index 88% rename from src/scripts/blockUser.ts rename to src/adm/handlers/moderation/blockUser.ts index 9c1d5516..e8b33c47 100644 --- a/src/scripts/blockUser.ts +++ b/src/adm/handlers/moderation/blockUser.ts @@ -1,11 +1,11 @@ /** * Given userId & block reason, blocks the user and marks all their existing ArticleReply, * ArticleReplyFeedback, ReplyRequest, ArticleCategory, ArticleCategoryFeedback as BLOCKED. + * + * Please announce that the user will be blocked openly with a URL first. */ +import { HTTPError } from 'fets'; -import 'dotenv/config'; -import yargs from 'yargs'; -import { SingleBar } from 'cli-progress'; import client from 'util/client'; import getAllDocs from 'util/getAllDocs'; import { updateArticleReplyStatus } from 'graphql/mutations/UpdateArticleReplyStatus'; @@ -48,7 +48,7 @@ async function writeBlockedReasonToUser(userId: string, blockedReason: string) { 'message' in e && e.message === 'document_missing_exception' ) { - throw new Error(`User with ID=${userId} does not exist`); + throw new HTTPError(400, `User with ID=${userId} does not exist`); } throw e; @@ -137,6 +137,8 @@ async function processReplyRequests(userId: string) { }] article ${articleId}: changed to ${total} reply requests, last requested at ${lastRequestedAt}` ); } + + return updateByQueryResult; } /** @@ -191,8 +193,6 @@ async function processArticleReplies(userId: string) { } console.log('Updating article replies...'); - const bar = new SingleBar({ stopOnComplete: true }); - bar.start(articleRepliesToProcess.length, 0); for (const { articleId, replyId, userId, appId } of articleRepliesToProcess) { await updateArticleReplyStatus({ articleId, @@ -201,9 +201,9 @@ async function processArticleReplies(userId: string) { appId, status: 'BLOCKED', }); - bar.increment(); } - bar.stop(); + + return articleRepliesToProcess.length; } /** @@ -283,6 +283,8 @@ async function processArticleReplyFeedbacks(userId: string) { }] article=${articleId} reply=${replyId}: score changed to +${positiveFeedbackCount}, -${negativeFeedbackCount}` ); } + + return updateByQueryResult; } /** @@ -312,44 +314,36 @@ async function processArticles(userId: string) { }); console.log('Article status update result', updateByQueryResult); + return updateByQueryResult; } +type BlockUserReturnValue = { + updatedArticles: number; + updatedReplyRequests: number; + updatedArticleReplies: number; + updateArticleReplyFeedbacks: number; +}; + async function main({ userId, blockedReason, }: { userId: string; blockedReason: string; -}) { +}): Promise { await writeBlockedReasonToUser(userId, blockedReason); - await processArticles(userId); - await processReplyRequests(userId); - await processArticleReplies(userId); - await processArticleReplyFeedbacks(userId); + const { updated: updatedArticles } = await processArticles(userId); + const { updated: updatedReplyRequests } = await processReplyRequests(userId); + const updatedArticleReplies = await processArticleReplies(userId); + const { updated: updateArticleReplyFeedbacks } = + await processArticleReplyFeedbacks(userId); + + return { + updatedArticles, + updatedReplyRequests, + updatedArticleReplies, + updateArticleReplyFeedbacks, + }; } export default main; - -/* istanbul ignore if */ -if (require.main === module) { - const argv = yargs - .options({ - userId: { - alias: 'u', - description: 'The user ID to block', - type: 'string', - demandOption: true, - }, - blockedReason: { - alias: 'r', - description: 'The URL to the annoucement that blocks this user', - type: 'string', - demandOption: true, - }, - }) - .help('help').argv; - - // yargs is typed as a promise for some reason, make Typescript happy here - // - Promise.resolve(argv).then(main).catch(console.error); -} diff --git a/src/adm/index.ts b/src/adm/index.ts index cb8366e2..d9af6811 100644 --- a/src/adm/index.ts +++ b/src/adm/index.ts @@ -9,6 +9,7 @@ import { Type } from '@sinclair/typebox'; import { useAuditLog, useAuth } from './util'; import pingHandler from './handlers/ping'; +import blockUser from './handlers/moderation/blockUser'; const shouldAuth = process.env.NODE_ENV === 'production'; @@ -30,26 +31,58 @@ const router = createRouter({ ...(shouldAuth ? [useAuth()] : []), // block non-cloudflare requests only in production useAuditLog(), ], -}).route({ - method: 'POST', - path: '/ping', - description: - 'Please use this harmless endpoint to test if your connection with API is wired up correctly.', - schemas: { - request: { - json: Type.Object( - { - echo: Type.String({ - description: 'Text that will be included in response message', - }), - }, - { additionalProperties: false } - ), +}) + .route({ + method: 'POST', + path: '/ping', + description: + 'Please use this harmless endpoint to test if your connection with API is wired up correctly.', + schemas: { + request: { + json: Type.Object( + { + echo: Type.String({ + description: 'Text that will be included in response message', + }), + }, + { additionalProperties: false } + ), + }, }, - responses: { 200: { type: 'string' } }, - }, - handler: async (request) => Response.json(pingHandler(await request.json())), -}); + handler: async (request) => + Response.json(pingHandler(await request.json())), + }) + .route({ + method: 'POST', + path: '/moderation/blockUser', + description: + 'Block the specified user by marking all their content as BLOCKED.', + schemas: { + request: { + json: Type.Object( + { + userId: Type.String({ + description: 'The user ID to block', + }), + blockedReason: Type.String({ + description: 'The URL to the announcement that blocks this user', + }), + }, + { additionalProperties: false } + ), + }, + responses: { + 200: Type.Object({ + updatedArticles: Type.Number(), + updatedReplyRequests: Type.Number(), + updatedArticleReplies: Type.Number(), + updateArticleReplyFeedbacks: Type.Number(), + }), + }, + }, + handler: async (request) => + Response.json(await blockUser(await request.json())), + }); createServer(router).listen(process.env.ADM_PORT, () => { console.log(`[Adm] API is running on port ${process.env.ADM_PORT}`);