Skip to content

Commit 427bcda

Browse files
committed
feat: enforce jwt authentication for moderation endpoints
1 parent 3143d0a commit 427bcda

File tree

8 files changed

+122
-32
lines changed

8 files changed

+122
-32
lines changed

packages/api-main/src/index.ts

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

66
import * as GetRequests from './gets/index';
77
import * as PostRequests from './posts/index';
8+
import { verifyJWT } from './shared/jwt';
89
import { useConfig } from './config';
910

1011
const config = useConfig();
@@ -56,6 +57,23 @@ function startWriteOnlyServer() {
5657
body: Posts.ModBanBody,
5758
});
5859

60+
// Protected route group
61+
app.group('/mod', group =>
62+
group
63+
.onBeforeHandle(verifyJWT)
64+
.post('/post-remove', ({ body, store }) => PostRequests.ModRemovePost(body, store), {
65+
body: Posts.ModRemovePostBody,
66+
})
67+
.post('/post-restore', ({ body, store }) => PostRequests.ModRestorePost(body, store), {
68+
body: Posts.ModRemovePostBody,
69+
})
70+
.post('/ban', ({ body, store }) => PostRequests.ModBan(body, store), {
71+
body: Posts.ModBanBody,
72+
})
73+
.post('/unban', ({ body, store }) => PostRequests.ModUnban(body, store), {
74+
body: Posts.ModBanBody,
75+
}),
76+
);
5977
app.listen(config.WRITE_ONLY_PORT ?? 3001);
6078
console.log(`[API Write Only] Running on ${config.WRITE_ONLY_PORT ?? 3001}`);
6179
}

packages/api-main/src/posts/mod.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const statementAuditRemovePost = getDatabase()
1515
})
1616
.prepare('stmnt_audit_remove_post');
1717

18-
export async function ModRemovePost(body: typeof Posts.ModRemovePostBody.static) {
18+
export async function ModRemovePost(body: typeof Posts.ModRemovePostBody.static, store) {
1919
try {
2020
const [post] = await getDatabase().select().from(FeedTable).where(eq(FeedTable.hash, body.post_hash)).limit(1);
2121
if (!post) {
@@ -25,7 +25,7 @@ export async function ModRemovePost(body: typeof Posts.ModRemovePostBody.static)
2525
const [mod] = await getDatabase()
2626
.select()
2727
.from(ModeratorTable)
28-
.where(eq(ModeratorTable.address, body.mod_address))
28+
.where(eq(ModeratorTable.address, store.userAddress))
2929
.limit(1);
3030
if (!mod) {
3131
return { status: 404, error: 'moderator not found' };
@@ -70,7 +70,7 @@ const statementAuditRestorePost = getDatabase()
7070
})
7171
.prepare('stmnt_audit_restore_post');
7272

73-
export async function ModRestorePost(body: typeof Posts.ModRemovePostBody.static) {
73+
export async function ModRestorePost(body: typeof Posts.ModRemovePostBody.static, store) {
7474
try {
7575
const [post] = await getDatabase().select().from(FeedTable).where(eq(FeedTable.hash, body.post_hash)).limit(1);
7676
if (!post) {
@@ -84,7 +84,7 @@ export async function ModRestorePost(body: typeof Posts.ModRemovePostBody.static
8484
const [mod] = await getDatabase()
8585
.select()
8686
.from(ModeratorTable)
87-
.where(eq(ModeratorTable.address, body.mod_address))
87+
.where(eq(ModeratorTable.address, store.userAddress))
8888
.limit(1);
8989
if (!mod) {
9090
return { status: 404, error: 'moderator not found' };
@@ -138,12 +138,12 @@ const statementAuditBanUser = getDatabase()
138138
})
139139
.prepare('stmnt_audit_ban_user');
140140

141-
export async function ModBan(body: typeof Posts.ModBanBody.static) {
141+
export async function ModBan(body: typeof Posts.ModBanBody.static, store) {
142142
try {
143143
const [mod] = await getDatabase()
144144
.select()
145145
.from(ModeratorTable)
146-
.where(eq(ModeratorTable.address, body.mod_address))
146+
.where(eq(ModeratorTable.address, store.userAddress))
147147
.limit(1);
148148
if (!mod) {
149149
return { status: 404, error: 'moderator not found' };
@@ -188,12 +188,12 @@ const statementAuditUnbanUser = getDatabase()
188188
})
189189
.prepare('stmnt_audit_unban_user');
190190

191-
export async function ModUnban(body: typeof Posts.ModBanBody.static) {
191+
export async function ModUnban(body: typeof Posts.ModBanBody.static, store) {
192192
try {
193193
const [mod] = await getDatabase()
194194
.select()
195195
.from(ModeratorTable)
196-
.where(eq(ModeratorTable.address, body.mod_address))
196+
.where(eq(ModeratorTable.address, store.userAddress))
197197
.limit(1);
198198
if (!mod) {
199199
return { status: 404, error: 'moderator not found' };
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import jwt from 'jsonwebtoken';
2+
3+
import { secretKey } from './useUserAuth';
4+
5+
export const verifyJWT = async ({ request, store }) => {
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+
// token data is on the form Login,id,date,publicKey,nonce
19+
// so to obtain the user address we need to split on the comma
20+
// and take the 4th element
21+
const userAddress = tokenData.data.split(',')[3];
22+
store.userAddress = userAddress;
23+
};

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: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -406,18 +406,41 @@ describe('v1', { sequential: true }, () => {
406406

407407
describe('v1 - mod', { sequential: true }, () => {
408408
const addressUserA = getAtomOneAddress();
409-
const addressModerator = getAtomOneAddress();
409+
let addressModerator = getAtomOneAddress();
410410
const genericPostMessage
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+
addressModerator = walletA.publicKey;
425+
const body: typeof Posts.AuthCreateBody.static = {
426+
address: walletA.publicKey,
427+
};
428+
429+
const response = (await post(`auth-create`, body, 'READ')) as { status: 200; id: number; message: string };
430+
assert.isOk(response?.status === 200, 'response was not okay');
431+
432+
const signData = await signADR36Document(walletA.mnemonic, response.message);
433+
const verifyBody: typeof Posts.AuthBody.static = {
434+
id: response.id,
435+
...signData.signature,
436+
};
437+
438+
const responseVerify = (await post(`auth`, verifyBody, 'READ')) as { status: 200; bearer: string };
439+
assert.isOk(responseVerify?.status === 200, 'response was not verified and confirmed okay');
440+
assert.isOk(responseVerify.bearer.length >= 1, 'bearer was not passed back');
441+
bearerToken = responseVerify.bearer;
442+
});
443+
421444
it('POST - /post', async () => {
422445
const body: typeof Posts.PostBody.static = {
423446
from: addressUserA,
@@ -431,6 +454,18 @@ describe('v1 - mod', { sequential: true }, () => {
431454
assert.isOk(response?.status === 200, 'response was not okay');
432455
});
433456

457+
it('POST - /mod/post-remove without autorization', async () => {
458+
const body: typeof Posts.ModRemovePostBody.static = {
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}`,
@@ -439,14 +474,13 @@ describe('v1 - mod', { sequential: true }, () => {
439474
assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type');
440475

441476
const body: typeof Posts.ModRemovePostBody.static = {
442-
mod_address: addressModerator,
443477
hash: getRandomHash(),
444478
timestamp: '2025-04-16T19:46:42Z',
445479
post_hash: response.rows[0].hash,
446480
reason: 'spam',
447481
};
448482

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

452486
const postsResponse = await get<{
@@ -481,14 +515,13 @@ describe('v1 - mod', { sequential: true }, () => {
481515
assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type');
482516

483517
const body: typeof Posts.ModRemovePostBody.static = {
484-
mod_address: addressModerator,
485518
hash: getRandomHash(),
486519
timestamp: '2025-04-16T19:46:42Z',
487520
post_hash: response.rows[0].hash,
488521
reason: 'spam',
489522
};
490523

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

494527
const postsResponse = await get<{
@@ -510,14 +543,13 @@ describe('v1 - mod', { sequential: true }, () => {
510543

511544
it('POST - /mod/post-restore', async () => {
512545
const body: typeof Posts.ModRemovePostBody.static = {
513-
mod_address: addressModerator,
514546
hash: getRandomHash(),
515547
timestamp: '2025-04-16T19:46:42Z',
516548
post_hash: postHash,
517549
reason: 'spam',
518550
};
519551

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

523555
const postsResponse = await get<{
@@ -546,12 +578,11 @@ describe('v1 - mod', { sequential: true }, () => {
546578
post_hash: postHash,
547579
};
548580

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

552584
// MOD tries to restore post
553585
const bodymod: typeof Posts.ModRemovePostBody.static = {
554-
mod_address: addressModerator,
555586
hash: getRandomHash(),
556587
timestamp: '2025-04-16T19:46:42Z',
557588
post_hash: postHash,
@@ -578,14 +609,13 @@ describe('v1 - mod', { sequential: true }, () => {
578609
it('POST - /mod/ban user banned deletes posts', async () => {
579610
// moderator bans user
580611
const body: typeof Posts.ModBanBody.static = {
581-
mod_address: addressModerator,
582612
hash: getRandomHash(),
583613
timestamp: '2025-04-16T19:46:42Z',
584614
user_address: addressUserA,
585615
reason: 'user too political',
586616
};
587617

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

591621
// post from user should be all hidden
@@ -617,7 +647,7 @@ describe('v1 - mod', { sequential: true }, () => {
617647
timestamp: '2025-04-16T19:46:42Z',
618648
};
619649

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

623653
// Even new post should be hidden
@@ -642,14 +672,13 @@ describe('v1 - mod', { sequential: true }, () => {
642672

643673
it('POST - unban restore all posts but user deleted ones', async () => {
644674
const body: typeof Posts.ModBanBody.static = {
645-
mod_address: addressModerator,
646675
hash: getRandomHash(),
647676
timestamp: '2025-04-16T19:46:42Z',
648677
user_address: addressUserA,
649678
reason: 'user too political',
650679
};
651680

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

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

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

688717
// Even new post should be hidden

packages/lib-api-types/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"name": "@atomone/dither-api-types",
33
"version": "1.0.0",
44
"description": "",
5-
"main": "dist/src/index.js",
6-
"types": "dist/src/index.d.ts",
5+
"main": "src/index.js",
6+
"types": "src/index.d.ts",
77
"scripts": {
88
"build": "tsc"
99
},

packages/lib-api-types/src/posts/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,27 @@ export const LikeBody = t.Object({
5252
timestamp: t.String(),
5353
});
5454

55-
export type ModRemovePostBody = { hash: string; mod_address: string; post_hash: string; reason: string; timestamp: string };
55+
export type ModRemovePostBody = {
56+
hash: string;
57+
post_hash: string;
58+
reason: string;
59+
timestamp: string;
60+
};
5661
export const ModRemovePostBody = t.Object({
5762
hash: t.String(),
58-
mod_address: t.String(),
5963
post_hash: t.String(),
6064
reason: t.String(),
6165
timestamp: t.String(),
6266
});
6367

64-
export type ModBanBody = { hash: string; mod_address: string; user_address: string; reason: string; timestamp: string };
68+
export type ModBanBody = {
69+
hash: string;
70+
user_address: string;
71+
reason: string;
72+
timestamp: string;
73+
};
6574
export const ModBanBody = t.Object({
6675
hash: t.String(),
67-
mod_address: t.String(),
6876
user_address: t.String(),
6977
reason: t.String(),
7078
timestamp: t.String(),

0 commit comments

Comments
 (0)