Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move blockUser to under admin API #352

Merged
merged 9 commits into from
Dec 1, 2024
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.
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
Loading