Skip to content

Commit e230c39

Browse files
committed
feat: enforce jwt authentication for moderation endpoints
1 parent f097a6d commit e230c39

File tree

5 files changed

+94
-23
lines changed

5 files changed

+94
-23
lines changed

packages/api-main/src/index.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Elysia } from 'elysia';
44

55
import * as GetRequests from './gets/index';
66
import * as PostRequests from './posts/index';
7+
import { verifyJWT } from './shared/jwt';
78
import { useConfig } from './config';
89

910
const config = useConfig();
@@ -42,19 +43,24 @@ function startWriteOnlyServer() {
4243
app.post('/dislike', ({ body }) => PostRequests.Dislike(body), { body: PostRequests.DislikeBody });
4344
app.post('/flag', ({ body }) => PostRequests.Flag(body), { body: PostRequests.FlagBody });
4445
app.post('/post-remove', ({ body }) => PostRequests.PostRemove(body), { body: PostRequests.PostRemoveBody });
45-
app.post('/mod/post-remove', ({ body }) => PostRequests.ModRemovePost(body), {
46-
body: PostRequests.ModRemovePostBody,
47-
});
48-
app.post('/mod/post-restore', ({ body }) => PostRequests.ModRestorePost(body), {
49-
body: PostRequests.ModRemovePostBody,
50-
});
51-
app.post('/mod/ban', ({ body }) => PostRequests.ModBan(body), {
52-
body: PostRequests.ModBanBody,
53-
});
54-
app.post('/mod/unban', ({ body }) => PostRequests.ModUnban(body), {
55-
body: PostRequests.ModBanBody,
56-
});
5746

47+
// Protected route group
48+
app.group('/mod', group =>
49+
group
50+
.onBeforeHandle(verifyJWT)
51+
.post('/post-remove', ({ body }) => PostRequests.ModRemovePost(body), {
52+
body: PostRequests.ModRemovePostBody,
53+
})
54+
.post('/post-restore', ({ body }) => PostRequests.ModRestorePost(body), {
55+
body: PostRequests.ModRemovePostBody,
56+
})
57+
.post('/ban', ({ body }) => PostRequests.ModBan(body), {
58+
body: PostRequests.ModBanBody,
59+
})
60+
.post('/unban', ({ body }) => PostRequests.ModUnban(body), {
61+
body: PostRequests.ModBanBody,
62+
}),
63+
);
5864
app.listen(config.WRITE_ONLY_PORT ?? 3001);
5965
console.log(`[API Write Only] Running on ${config.WRITE_ONLY_PORT ?? 3001}`);
6066
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import jwt from 'jsonwebtoken';
2+
3+
import { secretKey } from './useUserAuth';
4+
5+
export const verifyJWT = async ({ request }) => {
6+
const authHeader = request.headers.get('authorization');
7+
8+
if (!authHeader?.startsWith('Bearer ')) {
9+
return { status: 401, error: 'Unauthorized: No token' };
10+
}
11+
12+
const token = authHeader.split(' ')[1];
13+
const tokenData = await jwt.verify(token, secretKey);
14+
15+
if (!tokenData) {
16+
return { status: 401, error: 'Unauthorized: Invalid token' };
17+
}
18+
};

packages/api-main/src/shared/useUserAuth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import jwt from 'jsonwebtoken';
77

88
const expirationTime = 60_000 * 5;
99
const requests: { [publicKey: string]: string } = {};
10-
const secretKey = 'temp-key-need-to-config-this';
10+
export const secretKey = 'temp-key-need-to-config-this';
1111

1212
let id = 0;
1313

packages/api-main/tests/shared.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,22 @@ export async function get<T>(endpoint: string, port: 'WRITE' | 'READ' = 'READ')
2020
return jsonData as T;
2121
}
2222

23-
export async function post<T = { status: number }>(endpoint: string, body: object, port: 'WRITE' | 'READ' = 'WRITE') {
23+
export async function post<T = { status: number }>(
24+
endpoint: string,
25+
body: object,
26+
port: 'WRITE' | 'READ' = 'WRITE',
27+
token?: string,
28+
): Promise<T | null> {
29+
const headers: Record<string, string> = {
30+
'Content-Type': 'application/json',
31+
};
32+
33+
if (token) {
34+
headers['authorization'] = `Bearer ${token}`;
35+
}
2436
const response = await fetch(`http://localhost:${port === 'WRITE' ? 3001 : 3000}/v1/${endpoint}`, {
2537
method: 'POST',
26-
headers: { 'Content-Type': 'application/json' },
38+
headers,
2739
body: JSON.stringify({ ...body }),
2840
}).catch((err) => {
2941
console.error(err);

packages/api-main/tests/v1.test.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -411,13 +411,35 @@ describe('v1 - mod', { sequential: true }, () => {
411411
= 'hello world, this is a really intereresting post $@!($)@!()@!$21,4214,12,42142,14,12,421,';
412412
const postHash = getRandomHash();
413413
const secondPostHash = getRandomHash();
414+
let bearerToken: string;
414415

415416
it('EMPTY ALL TABLES', async () => {
416417
for (const tableName of tables) {
417418
await getDatabase().execute(sql`TRUNCATE TABLE ${sql.raw(tableName)};`);
418419
}
419420
});
420421

422+
it('POST mod obtain bearer token', async () => {
423+
const walletA = await createWallet();
424+
const body: typeof Posts.AuthCreateBody.static = {
425+
address: walletA.publicKey,
426+
};
427+
428+
const response = (await post(`auth-create`, body, 'READ')) as { status: 200; id: number; message: string };
429+
assert.isOk(response?.status === 200, 'response was not okay');
430+
431+
const signData = await signADR36Document(walletA.mnemonic, response.message);
432+
const verifyBody: typeof Posts.AuthBody.static = {
433+
id: response.id,
434+
...signData.signature,
435+
};
436+
437+
const responseVerify = (await post(`auth`, verifyBody, 'READ')) as { status: 200; bearer: string };
438+
assert.isOk(responseVerify?.status === 200, 'response was not verified and confirmed okay');
439+
assert.isOk(responseVerify.bearer.length >= 1, 'bearer was not passed back');
440+
bearerToken = responseVerify.bearer;
441+
});
442+
421443
it('POST - /post', async () => {
422444
const body: typeof Posts.PostBody.static = {
423445
from: addressUserA,
@@ -431,6 +453,19 @@ describe('v1 - mod', { sequential: true }, () => {
431453
assert.isOk(response?.status === 200, 'response was not okay');
432454
});
433455

456+
it('POST - /mod/post-remove without autorization', async () => {
457+
const body: typeof Posts.ModRemovePostBody.static = {
458+
mod_address: addressModerator,
459+
hash: getRandomHash(),
460+
timestamp: '2025-04-16T19:46:42Z',
461+
post_hash: postHash,
462+
reason: 'spam',
463+
};
464+
465+
const replyResponse = await post(`mod/post-remove`, body);
466+
assert.isOk(replyResponse?.status === 401, `expected unauthorized, got ${JSON.stringify(replyResponse)}`);
467+
});
468+
434469
it('POST - /mod/post-remove moderator does not exists', async () => {
435470
const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>(
436471
`posts?address=${addressUserA}`,
@@ -446,7 +481,7 @@ describe('v1 - mod', { sequential: true }, () => {
446481
reason: 'spam',
447482
};
448483

449-
const replyResponse = await post(`mod/post-remove`, body);
484+
const replyResponse = await post(`mod/post-remove`, body, 'WRITE', bearerToken);
450485
assert.isOk(replyResponse?.status === 404, `expected moderator was not found`);
451486

452487
const postsResponse = await get<{
@@ -488,7 +523,7 @@ describe('v1 - mod', { sequential: true }, () => {
488523
reason: 'spam',
489524
};
490525

491-
const replyResponse = await post(`mod/post-remove`, body);
526+
const replyResponse = await post(`mod/post-remove`, body, 'WRITE', bearerToken);
492527
assert.isOk(replyResponse?.status === 200, `response was not okay, got ${JSON.stringify(replyResponse)}`);
493528

494529
const postsResponse = await get<{
@@ -517,7 +552,7 @@ describe('v1 - mod', { sequential: true }, () => {
517552
reason: 'spam',
518553
};
519554

520-
const replyResponse = await post(`mod/post-restore`, body);
555+
const replyResponse = await post(`mod/post-restore`, body, 'WRITE', bearerToken);
521556
assert.isOk(replyResponse?.status === 200, `response was not okay, got ${JSON.stringify(replyResponse)}`);
522557

523558
const postsResponse = await get<{
@@ -546,7 +581,7 @@ describe('v1 - mod', { sequential: true }, () => {
546581
post_hash: postHash,
547582
};
548583

549-
const userRemoveResponse = await post(`post-remove`, body);
584+
const userRemoveResponse = await post(`post-remove`, body, 'WRITE', bearerToken);
550585
assert.isOk(userRemoveResponse?.status === 200, 'response was not okay');
551586

552587
// MOD tries to restore post
@@ -585,7 +620,7 @@ describe('v1 - mod', { sequential: true }, () => {
585620
reason: 'user too political',
586621
};
587622

588-
const userBanResponse = await post(`mod/ban`, body);
623+
const userBanResponse = await post(`mod/ban`, body, 'WRITE', bearerToken);
589624
assert.isOk(userBanResponse?.status === 200, `response was not okay ${JSON.stringify(userBanResponse)}`);
590625

591626
// post from user should be all hidden
@@ -617,7 +652,7 @@ describe('v1 - mod', { sequential: true }, () => {
617652
timestamp: '2025-04-16T19:46:42Z',
618653
};
619654

620-
const response = await post(`post`, body);
655+
const response = await post(`post`, body, 'WRITE', bearerToken);
621656
assert.isOk(response?.status === 200, 'response was not okay');
622657

623658
// Even new post should be hidden
@@ -649,7 +684,7 @@ describe('v1 - mod', { sequential: true }, () => {
649684
reason: 'user too political',
650685
};
651686

652-
const userBanResponse = await post(`mod/unban`, body);
687+
const userBanResponse = await post(`mod/unban`, body, 'WRITE', bearerToken);
653688
assert.isOk(userBanResponse?.status === 200, `response was not okay ${JSON.stringify(userBanResponse)}`);
654689

655690
// Totally user should have 2 post as one was deleted by itself (including the one posted while banned)
@@ -682,7 +717,7 @@ describe('v1 - mod', { sequential: true }, () => {
682717
timestamp: '2025-04-16T19:46:42Z',
683718
};
684719

685-
const response = await post(`post`, body);
720+
const response = await post(`post`, body, 'WRITE', bearerToken);
686721
assert.isOk(response?.status === 200, 'response was not okay');
687722

688723
// Even new post should be hidden

0 commit comments

Comments
 (0)