diff --git a/packages/api-main/src/index.ts b/packages/api-main/src/index.ts index a6ec81de..3dd016a0 100644 --- a/packages/api-main/src/index.ts +++ b/packages/api-main/src/index.ts @@ -5,6 +5,7 @@ import { Elysia } from 'elysia'; import * as GetRequests from './gets/index'; import * as PostRequests from './posts/index'; +import { verifyJWT } from './shared/jwt'; import { useConfig } from './config'; const config = useConfig(); @@ -56,6 +57,23 @@ function startWriteOnlyServer() { body: Posts.ModBanBody, }); + // Protected route group + app.group('/mod', group => + group + .onBeforeHandle(verifyJWT) + .post('/post-remove', ({ body, store }) => PostRequests.ModRemovePost(body, store), { + body: Posts.ModRemovePostBody, + }) + .post('/post-restore', ({ body, store }) => PostRequests.ModRestorePost(body, store), { + body: Posts.ModRemovePostBody, + }) + .post('/ban', ({ body, store }) => PostRequests.ModBan(body, store), { + body: Posts.ModBanBody, + }) + .post('/unban', ({ body, store }) => PostRequests.ModUnban(body, store), { + body: Posts.ModBanBody, + }), + ); app.listen(config.WRITE_ONLY_PORT ?? 3001); console.log(`[API Write Only] Running on ${config.WRITE_ONLY_PORT ?? 3001}`); } diff --git a/packages/api-main/src/posts/mod.ts b/packages/api-main/src/posts/mod.ts index c158482b..cd2df82e 100644 --- a/packages/api-main/src/posts/mod.ts +++ b/packages/api-main/src/posts/mod.ts @@ -15,22 +15,22 @@ const statementAuditRemovePost = getDatabase() }) .prepare('stmnt_audit_remove_post'); -export async function ModRemovePost(body: typeof Posts.ModRemovePostBody.static) { +export async function ModRemovePost(body: typeof Posts.ModRemovePostBody.static, store) { try { - const [post] = await getDatabase().select().from(FeedTable).where(eq(FeedTable.hash, body.post_hash)).limit(1); - if (!post) { - return { status: 404, error: 'post not found' }; - } - const [mod] = await getDatabase() .select() .from(ModeratorTable) - .where(eq(ModeratorTable.address, body.mod_address)) + .where(eq(ModeratorTable.address, store.userAddress)) .limit(1); if (!mod) { return { status: 404, error: 'moderator not found' }; } + const [post] = await getDatabase().select().from(FeedTable).where(eq(FeedTable.hash, body.post_hash)).limit(1); + if (!post) { + return { status: 404, error: 'post not found' }; + } + const statement = getDatabase() .update(FeedTable) .set({ @@ -70,8 +70,17 @@ const statementAuditRestorePost = getDatabase() }) .prepare('stmnt_audit_restore_post'); -export async function ModRestorePost(body: typeof Posts.ModRemovePostBody.static) { +export async function ModRestorePost(body: typeof Posts.ModRemovePostBody.static, store) { try { + const [mod] = await getDatabase() + .select() + .from(ModeratorTable) + .where(eq(ModeratorTable.address, store.userAddress)) + .limit(1); + if (!mod) { + return { status: 404, error: 'moderator not found' }; + } + const [post] = await getDatabase().select().from(FeedTable).where(eq(FeedTable.hash, body.post_hash)).limit(1); if (!post) { return { status: 404, error: 'post not found' }; @@ -81,15 +90,6 @@ export async function ModRestorePost(body: typeof Posts.ModRemovePostBody.static return { status: 404, error: 'post not removed' }; } - const [mod] = await getDatabase() - .select() - .from(ModeratorTable) - .where(eq(ModeratorTable.address, body.mod_address)) - .limit(1); - if (!mod) { - return { status: 404, error: 'moderator not found' }; - } - const [postWasRemovedByMod] = await getDatabase() .select() .from(ModeratorTable) @@ -138,12 +138,12 @@ const statementAuditBanUser = getDatabase() }) .prepare('stmnt_audit_ban_user'); -export async function ModBan(body: typeof Posts.ModBanBody.static) { +export async function ModBan(body: typeof Posts.ModBanBody.static, store) { try { const [mod] = await getDatabase() .select() .from(ModeratorTable) - .where(eq(ModeratorTable.address, body.mod_address)) + .where(eq(ModeratorTable.address, store.userAddress)) .limit(1); if (!mod) { return { status: 404, error: 'moderator not found' }; @@ -188,12 +188,12 @@ const statementAuditUnbanUser = getDatabase() }) .prepare('stmnt_audit_unban_user'); -export async function ModUnban(body: typeof Posts.ModBanBody.static) { +export async function ModUnban(body: typeof Posts.ModBanBody.static, store) { try { const [mod] = await getDatabase() .select() .from(ModeratorTable) - .where(eq(ModeratorTable.address, body.mod_address)) + .where(eq(ModeratorTable.address, store.userAddress)) .limit(1); if (!mod) { return { status: 404, error: 'moderator not found' }; diff --git a/packages/api-main/src/shared/jwt.ts b/packages/api-main/src/shared/jwt.ts new file mode 100644 index 00000000..052fbaa3 --- /dev/null +++ b/packages/api-main/src/shared/jwt.ts @@ -0,0 +1,23 @@ +import jwt from 'jsonwebtoken'; + +import { secretKey } from './useUserAuth'; + +export const verifyJWT = async ({ request, store }) => { + const authHeader = request.headers.get('authorization'); + + if (!authHeader?.startsWith('Bearer ')) { + return { status: 401, error: 'Unauthorized: No token' }; + } + + const token = authHeader.split(' ')[1]; + const tokenData = await jwt.verify(token, secretKey); + + if (!tokenData) { + return { status: 401, error: 'Unauthorized: Invalid token' }; + } + // token data is on the form Login,id,date,publicKey,nonce + // so to obtain the user address we need to split on the comma + // and take the 4th element + const userAddress = tokenData.data.split(',')[3]; + store.userAddress = userAddress; +}; diff --git a/packages/api-main/src/shared/useUserAuth.ts b/packages/api-main/src/shared/useUserAuth.ts index a241bf6f..3d4656e4 100644 --- a/packages/api-main/src/shared/useUserAuth.ts +++ b/packages/api-main/src/shared/useUserAuth.ts @@ -7,7 +7,7 @@ import jwt from 'jsonwebtoken'; const expirationTime = 60_000 * 5; const requests: { [publicKey: string]: string } = {}; -const secretKey = 'temp-key-need-to-config-this'; +export const secretKey = 'temp-key-need-to-config-this'; let id = 0; diff --git a/packages/api-main/tests/shared.ts b/packages/api-main/tests/shared.ts index 123199a5..6ce81baa 100644 --- a/packages/api-main/tests/shared.ts +++ b/packages/api-main/tests/shared.ts @@ -20,10 +20,22 @@ export async function get(endpoint: string, port: 'WRITE' | 'READ' = 'READ') return jsonData as T; } -export async function post(endpoint: string, body: object, port: 'WRITE' | 'READ' = 'WRITE') { +export async function post( + endpoint: string, + body: object, + port: 'WRITE' | 'READ' = 'WRITE', + token?: string, +): Promise { + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['authorization'] = `Bearer ${token}`; + } const response = await fetch(`http://localhost:${port === 'WRITE' ? 3001 : 3000}/v1/${endpoint}`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers, body: JSON.stringify({ ...body }), }).catch((err) => { console.error(err); diff --git a/packages/api-main/tests/v1.test.ts b/packages/api-main/tests/v1.test.ts index 90b055a8..cac5e962 100644 --- a/packages/api-main/tests/v1.test.ts +++ b/packages/api-main/tests/v1.test.ts @@ -406,11 +406,12 @@ describe('v1', { sequential: true }, () => { describe('v1 - mod', { sequential: true }, () => { const addressUserA = getAtomOneAddress(); - const addressModerator = getAtomOneAddress(); + let addressModerator = getAtomOneAddress(); const genericPostMessage = 'hello world, this is a really intereresting post $@!($)@!()@!$21,4214,12,42142,14,12,421,'; const postHash = getRandomHash(); const secondPostHash = getRandomHash(); + let bearerToken: string; it('EMPTY ALL TABLES', async () => { for (const tableName of tables) { @@ -418,6 +419,28 @@ describe('v1 - mod', { sequential: true }, () => { } }); + it('POST mod obtain bearer token', async () => { + const walletA = await createWallet(); + addressModerator = walletA.publicKey; + const body: typeof Posts.AuthCreateBody.static = { + address: walletA.publicKey, + }; + + const response = (await post(`auth-create`, body, 'READ')) as { status: 200; id: number; message: string }; + assert.isOk(response?.status === 200, 'response was not okay'); + + const signData = await signADR36Document(walletA.mnemonic, response.message); + const verifyBody: typeof Posts.AuthBody.static = { + id: response.id, + ...signData.signature, + }; + + const responseVerify = (await post(`auth`, verifyBody, 'READ')) as { status: 200; bearer: string }; + assert.isOk(responseVerify?.status === 200, 'response was not verified and confirmed okay'); + assert.isOk(responseVerify.bearer.length >= 1, 'bearer was not passed back'); + bearerToken = responseVerify.bearer; + }); + it('POST - /post', async () => { const body: typeof Posts.PostBody.static = { from: addressUserA, @@ -431,6 +454,18 @@ describe('v1 - mod', { sequential: true }, () => { assert.isOk(response?.status === 200, 'response was not okay'); }); + it('POST - /mod/post-remove without autorization', async () => { + const body: typeof Posts.ModRemovePostBody.static = { + hash: getRandomHash(), + timestamp: '2025-04-16T19:46:42Z', + post_hash: postHash, + reason: 'spam', + }; + + const replyResponse = await post(`mod/post-remove`, body); + assert.isOk(replyResponse?.status === 401, `expected unauthorized, got ${JSON.stringify(replyResponse)}`); + }); + it('POST - /mod/post-remove moderator does not exists', async () => { const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( `posts?address=${addressUserA}`, @@ -439,14 +474,13 @@ describe('v1 - mod', { sequential: true }, () => { assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type'); const body: typeof Posts.ModRemovePostBody.static = { - mod_address: addressModerator, hash: getRandomHash(), timestamp: '2025-04-16T19:46:42Z', post_hash: response.rows[0].hash, reason: 'spam', }; - const replyResponse = await post(`mod/post-remove`, body); + const replyResponse = await post(`mod/post-remove`, body, 'WRITE', bearerToken); assert.isOk(replyResponse?.status === 404, `expected moderator was not found`); const postsResponse = await get<{ @@ -481,14 +515,13 @@ describe('v1 - mod', { sequential: true }, () => { assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type'); const body: typeof Posts.ModRemovePostBody.static = { - mod_address: addressModerator, hash: getRandomHash(), timestamp: '2025-04-16T19:46:42Z', post_hash: response.rows[0].hash, reason: 'spam', }; - const replyResponse = await post(`mod/post-remove`, body); + const replyResponse = await post(`mod/post-remove`, body, 'WRITE', bearerToken); assert.isOk(replyResponse?.status === 200, `response was not okay, got ${JSON.stringify(replyResponse)}`); const postsResponse = await get<{ @@ -510,14 +543,13 @@ describe('v1 - mod', { sequential: true }, () => { it('POST - /mod/post-restore', async () => { const body: typeof Posts.ModRemovePostBody.static = { - mod_address: addressModerator, hash: getRandomHash(), timestamp: '2025-04-16T19:46:42Z', post_hash: postHash, reason: 'spam', }; - const replyResponse = await post(`mod/post-restore`, body); + const replyResponse = await post(`mod/post-restore`, body, 'WRITE', bearerToken); assert.isOk(replyResponse?.status === 200, `response was not okay, got ${JSON.stringify(replyResponse)}`); const postsResponse = await get<{ @@ -546,12 +578,11 @@ describe('v1 - mod', { sequential: true }, () => { post_hash: postHash, }; - const userRemoveResponse = await post(`post-remove`, body); + const userRemoveResponse = await post(`post-remove`, body, 'WRITE', bearerToken); assert.isOk(userRemoveResponse?.status === 200, 'response was not okay'); // MOD tries to restore post const bodymod: typeof Posts.ModRemovePostBody.static = { - mod_address: addressModerator, hash: getRandomHash(), timestamp: '2025-04-16T19:46:42Z', post_hash: postHash, @@ -578,14 +609,13 @@ describe('v1 - mod', { sequential: true }, () => { it('POST - /mod/ban user banned deletes posts', async () => { // moderator bans user const body: typeof Posts.ModBanBody.static = { - mod_address: addressModerator, hash: getRandomHash(), timestamp: '2025-04-16T19:46:42Z', user_address: addressUserA, reason: 'user too political', }; - const userBanResponse = await post(`mod/ban`, body); + const userBanResponse = await post(`mod/ban`, body, 'WRITE', bearerToken); assert.isOk(userBanResponse?.status === 200, `response was not okay ${JSON.stringify(userBanResponse)}`); // post from user should be all hidden @@ -617,7 +647,7 @@ describe('v1 - mod', { sequential: true }, () => { timestamp: '2025-04-16T19:46:42Z', }; - const response = await post(`post`, body); + const response = await post(`post`, body, 'WRITE', bearerToken); assert.isOk(response?.status === 200, 'response was not okay'); // Even new post should be hidden @@ -642,14 +672,13 @@ describe('v1 - mod', { sequential: true }, () => { it('POST - unban restore all posts but user deleted ones', async () => { const body: typeof Posts.ModBanBody.static = { - mod_address: addressModerator, hash: getRandomHash(), timestamp: '2025-04-16T19:46:42Z', user_address: addressUserA, reason: 'user too political', }; - const userBanResponse = await post(`mod/unban`, body); + const userBanResponse = await post(`mod/unban`, body, 'WRITE', bearerToken); assert.isOk(userBanResponse?.status === 200, `response was not okay ${JSON.stringify(userBanResponse)}`); // 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 }, () => { timestamp: '2025-04-16T19:46:42Z', }; - const response = await post(`post`, body); + const response = await post(`post`, body, 'WRITE', bearerToken); assert.isOk(response?.status === 200, 'response was not okay'); // Even new post should be hidden diff --git a/packages/lib-api-types/package.json b/packages/lib-api-types/package.json index f41ce164..ed19b21c 100644 --- a/packages/lib-api-types/package.json +++ b/packages/lib-api-types/package.json @@ -2,8 +2,8 @@ "name": "@atomone/dither-api-types", "version": "1.0.0", "description": "", - "main": "dist/src/index.js", - "types": "dist/src/index.d.ts", + "main": "src/index.js", + "types": "src/index.d.ts", "scripts": { "build": "tsc" }, diff --git a/packages/lib-api-types/src/posts/index.ts b/packages/lib-api-types/src/posts/index.ts index 18d1ccfa..a04a5b71 100644 --- a/packages/lib-api-types/src/posts/index.ts +++ b/packages/lib-api-types/src/posts/index.ts @@ -52,19 +52,27 @@ export const LikeBody = t.Object({ timestamp: t.String(), }); -export type ModRemovePostBody = { hash: string; mod_address: string; post_hash: string; reason: string; timestamp: string }; +export type ModRemovePostBody = { + hash: string; + post_hash: string; + reason: string; + timestamp: string; +}; export const ModRemovePostBody = t.Object({ hash: t.String(), - mod_address: t.String(), post_hash: t.String(), reason: t.String(), timestamp: t.String(), }); -export type ModBanBody = { hash: string; mod_address: string; user_address: string; reason: string; timestamp: string }; +export type ModBanBody = { + hash: string; + user_address: string; + reason: string; + timestamp: string; +}; export const ModBanBody = t.Object({ hash: t.String(), - mod_address: t.String(), user_address: t.String(), reason: t.String(), timestamp: t.String(),