Skip to content

Commit

Permalink
Merge pull request #352 from cofacts/admin-api-moderation
Browse files Browse the repository at this point in the history
Move blockUser to under admin API
  • Loading branch information
MrOrz authored Dec 1, 2024
2 parents ec2b269 + 5970626 commit 225b088
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 68 deletions.
9 changes: 0 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,15 +221,6 @@ $ node build/scripts/removeArticleReply.js --userId=<userId> --articleId=<articl

- For more options, run the above script with `--help` or see the file level comments.

### Block a user
- Please announce that the user will be blocked openly with a URL first.
- To block a user, execute the following:
```
$ node build/scripts/blockUser.js --userId=<userId> --blockedReason=<Announcement URL>
```

- 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.

Expand Down
14 changes: 14 additions & 0 deletions src/adm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,17 @@ curl -XPOST -H "CF-Access-Client-Id: <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.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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]`
);
});

Expand All @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -137,6 +137,8 @@ async function processReplyRequests(userId: string) {
}] article ${articleId}: changed to ${total} reply requests, last requested at ${lastRequestedAt}`
);
}

return updateByQueryResult;
}

/**
Expand Down Expand Up @@ -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,
Expand All @@ -201,9 +201,9 @@ async function processArticleReplies(userId: string) {
appId,
status: 'BLOCKED',
});
bar.increment();
}
bar.stop();

return articleRepliesToProcess.length;
}

/**
Expand Down Expand Up @@ -283,6 +283,8 @@ async function processArticleReplyFeedbacks(userId: string) {
}] article=${articleId} reply=${replyId}: score changed to +${positiveFeedbackCount}, -${negativeFeedbackCount}`
);
}

return updateByQueryResult;
}

/**
Expand Down Expand Up @@ -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<BlockUserReturnValue> {
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);
}
71 changes: 52 additions & 19 deletions src/adm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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}`);
Expand Down

0 comments on commit 225b088

Please sign in to comment.