Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/api-main/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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}`);
}
Expand Down
42 changes: 21 additions & 21 deletions packages/api-main/src/posts/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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' };
Expand All @@ -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)
Expand Down Expand Up @@ -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' };
Expand Down Expand Up @@ -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' };
Expand Down
23 changes: 23 additions & 0 deletions packages/api-main/src/shared/jwt.ts
Original file line number Diff line number Diff line change
@@ -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;
};
2 changes: 1 addition & 1 deletion packages/api-main/src/shared/useUserAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we put this to env?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, still needs to be put into a vite environment variable, can be ignored for now though.


let id = 0;

Expand Down
16 changes: 14 additions & 2 deletions packages/api-main/tests/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,22 @@ export async function get<T>(endpoint: string, port: 'WRITE' | 'READ' = 'READ')
return jsonData as T;
}

export async function post<T = { status: number }>(endpoint: string, body: object, port: 'WRITE' | 'READ' = 'WRITE') {
export async function post<T = { status: number }>(
endpoint: string,
body: object,
port: 'WRITE' | 'READ' = 'WRITE',
token?: string,
): Promise<T | null> {
const headers: Record<string, string> = {
'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);
Expand Down
59 changes: 44 additions & 15 deletions packages/api-main/tests/v1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,18 +406,41 @@ 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) {
await getDatabase().execute(sql`TRUNCATE TABLE ${sql.raw(tableName)};`);
}
});

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,
Expand All @@ -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}`,
Expand All @@ -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<{
Expand Down Expand Up @@ -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<{
Expand All @@ -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<{
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/lib-api-types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
Loading