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
2 changes: 1 addition & 1 deletion packages/api-main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@
"semi": true,
"printWidth": 120
}
}
}
73 changes: 11 additions & 62 deletions packages/api-main/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,24 @@
import { Gets, Posts } from '@atomone/dither-api-types';
import { cors } from '@elysiajs/cors';
import node from '@elysiajs/node';
import { Elysia, t } from 'elysia';
import { Elysia } from 'elysia';

import * as GetRequests from './gets/index';
import * as PostRequests from './posts/index';
import { authRoutes } from './routes/auth';
import { moderatorRoutes } from './routes/moderator';
import { publicRoutes } from './routes/public';
import { readerRoutes } from './routes/reader';
import { userRoutes } from './routes/user';
import { useConfig } from './config';

const config = useConfig();
const app = new Elysia({ adapter: node(), prefix: '/v1' });

export function start() {
app.use(cors());
app.get('/health', GetRequests.health);
app.get('/dislikes', ({ query }) => GetRequests.Dislikes(query), { query: Gets.DislikesQuery });
app.get('/feed', ({ query }) => GetRequests.Feed(query), { query: Gets.FeedQuery });
app.get('/flags', ({ query }) => GetRequests.Flags(query), { query: Gets.FlagsQuery });
app.get('/is-following', ({ query }) => GetRequests.IsFollowing(query), { query: Gets.IsFollowingQuery });
app.get('/followers', ({ query }) => GetRequests.Followers(query), { query: Gets.FollowersQuery });
app.get('/following', ({ query }) => GetRequests.Following(query), { query: Gets.FollowingQuery });
app.get('/likes', ({ query }) => GetRequests.Likes(query), { query: Gets.LikesQuery });
app.get('/posts', ({ query }) => GetRequests.Posts(query), { query: Gets.PostsQuery });
app.get('/post', ({ query }) => GetRequests.Post(query), { query: Gets.PostQuery });
app.get('/replies', ({ query }) => GetRequests.Replies(query), { query: Gets.RepliesQuery });
app.get('/search', ({ query }) => GetRequests.Search(query), { query: Gets.SearchQuery });
app.get('/user-replies', ({ query }) => GetRequests.UserReplies(query), { query: Gets.UserRepliesQuery });
app.get('/following-posts', ({ query }) => GetRequests.FollowingPosts(query), { query: Gets.PostsQuery });
app.get('/last-block', GetRequests.LastBlock);
app.get('/auth-verify', ({ cookie: { auth } }) => GetRequests.AuthVerify(auth));
app.get('/notifications', ({ query, cookie: { auth } }) => GetRequests.Notifications(query, auth), {
query: Gets.NotificationsQuery,
});
app.get('/notifications-count', ({ query, cookie: { auth } }) => GetRequests.NotificationsCount(query, auth), {
query: Gets.NotificationsCountQuery,
});

app.post('/auth-create', ({ body, request }) => PostRequests.AuthCreate(body, request), { body: Posts.AuthCreateBody });
app.post('/auth', ({ body, cookie: { auth }, request }) => PostRequests.Auth(body, auth, request), { body: t.Object({
id: t.Number(),
pub_key: t.Object({ type: t.String(), value: t.String() }),
signature: t.String(),
json: t.Optional(t.Boolean()),
}) });

app.post('/post', ({ body, headers }) => PostRequests.Post(body, headers), { body: Posts.PostBody });
app.post('/reply', ({ body, headers }) => PostRequests.Reply(body, headers), { body: Posts.ReplyBody });
app.post('/follow', ({ body, headers }) => PostRequests.Follow(body, headers), { body: Posts.FollowBody });
app.post('/unfollow', ({ body, headers }) => PostRequests.Unfollow(body, headers), { body: Posts.UnfollowBody });
app.post('/like', ({ body, headers }) => PostRequests.Like(body, headers), { body: Posts.LikeBody });
app.post('/dislike', ({ body, headers }) => PostRequests.Dislike(body, headers), { body: Posts.DislikeBody });
app.post('/flag', ({ body, headers }) => PostRequests.Flag(body, headers), { body: Posts.FlagBody });
app.post('/post-remove', ({ body, headers }) => PostRequests.PostRemove(body, headers), { body: Posts.PostRemoveBody });
app.post('/update-state', ({ body, headers }) => PostRequests.UpdateState(body, headers), { body: t.Object({ last_block: t.String() }) });
app.post('/logout', ({ cookie: { auth } }) => PostRequests.Logout(auth));

app.post('/notification-read', ({ query, cookie: { auth } }) => GetRequests.ReadNotification(query, auth), {
query: Gets.ReadNotificationQuery,
});

app.post('/mod/post-remove', ({ body, cookie: { auth } }) => PostRequests.ModRemovePost(body, auth), {
body: Posts.ModRemovePostBody,
});
app.post('/mod/post-restore', ({ body, cookie: { auth } }) => PostRequests.ModRestorePost(body, auth), {
body: Posts.ModRemovePostBody,
});
app.post('/mod/ban', ({ body, cookie: { auth } }) => PostRequests.ModBan(body, auth), {
body: Posts.ModBanBody,
});
app.post('/mod/unban', ({ body, cookie: { auth } }) => PostRequests.ModUnban(body, auth), {
body: Posts.ModBanBody,
});
app.use(publicRoutes);
app.use(authRoutes);
app.use(readerRoutes);
app.use(userRoutes);
app.use(moderatorRoutes);

app.listen(config.PORT);
}
Expand Down
10 changes: 10 additions & 0 deletions packages/api-main/src/middleware/readerAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { isReaderAuthorizationValid } from '../utility';

/**
* Middleware to validate reader authorization header
*/
export const readerAuthMiddleware = (context: { headers: Record<string, string | undefined> }) => {
if (!isReaderAuthorizationValid(context.headers)) {
return { status: 401, error: 'Unauthorized to make write request' };
}
};
8 changes: 2 additions & 6 deletions packages/api-main/src/posts/dislike.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useSharedQueries } from '../shared/useSharedQueries';

const sharedQueries = useSharedQueries();
import { notify } from '../shared/notify';
import { isReaderAuthorizationValid, postToDiscord } from '../utility';
import { postToDiscord } from '../utility';

const statement = getDatabase()
.insert(DislikesTable)
Expand All @@ -30,11 +30,7 @@ const statementAddDislikeToPost = getDatabase()
.where(eq(FeedTable.hash, sql.placeholder('post_hash')))
.prepare('stmnt_add_dislike_count_to_post');

export async function Dislike(body: typeof Posts.DislikeBody.static, headers: Record<string, string | undefined>) {
if (!isReaderAuthorizationValid(headers)) {
return { status: 401, error: 'Unauthorized to make write request' };
}

export async function Dislike(body: typeof Posts.DislikeBody.static) {
if (body.post_hash.length !== 64) {
return { status: 400, error: 'Provided post_hash is not valid for dislike' };
}
Expand Down
7 changes: 1 addition & 6 deletions packages/api-main/src/posts/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { useSharedQueries } from '../shared/useSharedQueries';

const sharedQueries = useSharedQueries();
import { notify } from '../shared/notify';
import { isReaderAuthorizationValid } from '../utility';

const statement = getDatabase()
.insert(FlagsTable)
Expand All @@ -30,11 +29,7 @@ const statementAddFlagToPost = getDatabase()
.where(eq(FeedTable.hash, sql.placeholder('post_hash')))
.prepare('stmnt_add_flag_count_to_post');

export async function Flag(body: typeof Posts.FlagBody.static, headers: Record<string, string | undefined>) {
if (!isReaderAuthorizationValid(headers)) {
return { status: 401, error: 'Unauthorized to make write request' };
}

export async function Flag(body: typeof Posts.FlagBody.static) {
if (body.post_hash.length !== 64) {
return { status: 400, error: 'Provided post_hash is not valid for flag' };
}
Expand Down
7 changes: 1 addition & 6 deletions packages/api-main/src/posts/follow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { and, eq, isNotNull, sql } from 'drizzle-orm';
import { getDatabase } from '../../drizzle/db';
import { FollowsTable } from '../../drizzle/schema';
import { notify } from '../shared/notify';
import { isReaderAuthorizationValid } from '../utility';

const statementAddFollower = getDatabase()
.insert(FollowsTable)
Expand All @@ -17,11 +16,7 @@ const statementAddFollower = getDatabase()
.onConflictDoNothing()
.prepare('stmnt_add_follower');

export async function Follow(body: typeof Posts.FollowBody.static, headers: Record<string, string | undefined>) {
if (!isReaderAuthorizationValid(headers)) {
return { status: 401, error: 'Unauthorized to make write request' };
}

export async function Follow(body: typeof Posts.FollowBody.static) {
if (body.hash.length !== 64) {
return { status: 400, error: 'Provided hash is not valid for follow' };
}
Expand Down
8 changes: 2 additions & 6 deletions packages/api-main/src/posts/like.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useSharedQueries } from '../shared/useSharedQueries';

const sharedQueries = useSharedQueries();
import { notify } from '../shared/notify';
import { isReaderAuthorizationValid, postToDiscord } from '../utility';
import { postToDiscord } from '../utility';

const statement = getDatabase()
.insert(LikesTable)
Expand All @@ -30,11 +30,7 @@ const statementAddLikeToPost = getDatabase()
.where(eq(FeedTable.hash, sql.placeholder('post_hash')))
.prepare('stmnt_add_like_count_to_post');

export async function Like(body: typeof Posts.LikeBody.static, headers: Record<string, string | undefined>) {
if (!isReaderAuthorizationValid(headers)) {
return { status: 401, error: 'Unauthorized to make write request' };
}

export async function Like(body: typeof Posts.LikeBody.static) {
if (body.post_hash.length !== 64) {
return { status: 400, error: 'Provided post_hash is not valid for like' };
}
Expand Down
8 changes: 2 additions & 6 deletions packages/api-main/src/posts/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { desc, eq, sql } from 'drizzle-orm';

import { getDatabase } from '../../drizzle/db';
import { AuditTable, FeedTable } from '../../drizzle/schema';
import { isReaderAuthorizationValid, postToDiscord } from '../utility';
import { postToDiscord } from '../utility';

const statement = getDatabase()
.insert(FeedTable)
Expand All @@ -17,11 +17,7 @@ const statement = getDatabase()
.onConflictDoNothing()
.prepare('stmnt_post');

export async function Post(body: typeof Posts.PostBody.static, headers: Record<string, string | undefined>) {
if (!isReaderAuthorizationValid(headers)) {
return { status: 401, error: 'Unauthorized to make write request' };
}

export async function Post(body: typeof Posts.PostBody.static) {
try {
if (body.msg.length >= 512) {
return { status: 400, error: 'message is too long' };
Expand Down
7 changes: 1 addition & 6 deletions packages/api-main/src/posts/postRemove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,8 @@ import { and, eq } from 'drizzle-orm';

import { getDatabase } from '../../drizzle/db';
import { FeedTable } from '../../drizzle/schema';
import { isReaderAuthorizationValid } from '../utility';

export async function PostRemove(body: typeof Posts.PostRemoveBody.static, headers: Record<string, string | undefined>) {
if (!isReaderAuthorizationValid(headers)) {
return { status: 401, error: 'Unauthorized to make write request' };
}

export async function PostRemove(body: typeof Posts.PostRemoveBody.static) {
try {
const selectResults = await getDatabase()
.select()
Expand Down
8 changes: 2 additions & 6 deletions packages/api-main/src/posts/reply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useSharedQueries } from '../shared/useSharedQueries';

const sharedQueries = useSharedQueries();
import { notify } from '../shared/notify';
import { isReaderAuthorizationValid, postToDiscord } from '../utility';
import { postToDiscord } from '../utility';

const statement = getDatabase()
.insert(FeedTable)
Expand All @@ -28,11 +28,7 @@ const statementAddReplyCount = getDatabase()
.where(eq(FeedTable.hash, sql.placeholder('post_hash')))
.prepare('stmnt_add_reply_count');

export async function Reply(body: typeof Posts.ReplyBody.static, headers: Record<string, string | undefined>) {
if (!isReaderAuthorizationValid(headers)) {
return { status: 401, error: 'Unauthorized to make write request' };
}

export async function Reply(body: typeof Posts.ReplyBody.static) {
if (body.post_hash.length !== 64) {
return { status: 400, error: 'Provided post_hash is not valid for reply' };
}
Expand Down
7 changes: 1 addition & 6 deletions packages/api-main/src/posts/unfollow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { and, eq, sql } from 'drizzle-orm';

import { getDatabase } from '../../drizzle/db';
import { FollowsTable } from '../../drizzle/schema';
import { isReaderAuthorizationValid } from '../utility';

const statementRemoveFollowing = getDatabase()
.update(FollowsTable)
Expand All @@ -16,11 +15,7 @@ const statementRemoveFollowing = getDatabase()
)
.prepare('stmnt_remove_follower');

export async function Unfollow(body: typeof Posts.UnfollowBody.static, headers: Record<string, string | undefined>) {
if (!isReaderAuthorizationValid(headers)) {
return { status: 401, error: 'Unauthorized to make write request' };
}

export async function Unfollow(body: typeof Posts.UnfollowBody.static) {
try {
await statementRemoveFollowing.execute({ follower: body.from.toLowerCase(), following: body.address.toLowerCase(), removed_at: new Date(body.timestamp) });
return { status: 200 };
Expand Down
7 changes: 1 addition & 6 deletions packages/api-main/src/posts/updateState.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { getDatabase } from '../../drizzle/db';
import { ReaderState } from '../../drizzle/schema';
import { isReaderAuthorizationValid } from '../utility';

export async function UpdateState(body: { last_block: string }, headers: Record<string, string | undefined>) {
if (!isReaderAuthorizationValid(headers)) {
return { status: 401, error: 'Unauthorized to make write request' };
}

export async function UpdateState(body: { last_block: string }) {
try {
await getDatabase()
.insert(ReaderState)
Expand Down
21 changes: 21 additions & 0 deletions packages/api-main/src/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Posts } from '@atomone/dither-api-types';
import { Elysia, t } from 'elysia';

import * as GetRequests from '../gets/index';
import * as PostRequests from '../posts/index';

/**
* Authentication-related routes
*/
export const authRoutes = new Elysia()
.get('/auth-verify', ({ cookie: { auth } }) => GetRequests.AuthVerify(auth))
.post('/auth-create', ({ body, request }) => PostRequests.AuthCreate(body, request), { body: Posts.AuthCreateBody })
.post('/auth', ({ body, cookie: { auth }, request }) => PostRequests.Auth(body, auth, request), {
body: t.Object({
id: t.Number(),
pub_key: t.Object({ type: t.String(), value: t.String() }),
signature: t.String(),
json: t.Optional(t.Boolean()),
}),
})
.post('/logout', ({ cookie: { auth } }) => PostRequests.Logout(auth));
22 changes: 22 additions & 0 deletions packages/api-main/src/routes/moderator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Posts } from '@atomone/dither-api-types';
import { Elysia } from 'elysia';

import * as PostRequests from '../posts/index';

/**
* Moderator routes that require JWT cookie authentication
* These routes are prefixed with /mod
*/
export const moderatorRoutes = new Elysia({ prefix: '/mod' })
.post('/post-remove', ({ body, cookie: { auth } }) => PostRequests.ModRemovePost(body, auth), {
body: Posts.ModRemovePostBody,
})
.post('/post-restore', ({ body, cookie: { auth } }) => PostRequests.ModRestorePost(body, auth), {
body: Posts.ModRemovePostBody,
})
.post('/ban', ({ body, cookie: { auth } }) => PostRequests.ModBan(body, auth), {
body: Posts.ModBanBody,
})
.post('/unban', ({ body, cookie: { auth } }) => PostRequests.ModUnban(body, auth), {
body: Posts.ModBanBody,
});
24 changes: 24 additions & 0 deletions packages/api-main/src/routes/public.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Gets } from '@atomone/dither-api-types';
import { Elysia } from 'elysia';

import * as GetRequests from '../gets/index';

/**
* Public routes that don't require authentication
*/
export const publicRoutes = new Elysia()
.get('/health', GetRequests.health)
.get('/dislikes', ({ query }) => GetRequests.Dislikes(query), { query: Gets.DislikesQuery })
.get('/feed', ({ query }) => GetRequests.Feed(query), { query: Gets.FeedQuery })
.get('/flags', ({ query }) => GetRequests.Flags(query), { query: Gets.FlagsQuery })
.get('/is-following', ({ query }) => GetRequests.IsFollowing(query), { query: Gets.IsFollowingQuery })
.get('/followers', ({ query }) => GetRequests.Followers(query), { query: Gets.FollowersQuery })
.get('/following', ({ query }) => GetRequests.Following(query), { query: Gets.FollowingQuery })
.get('/likes', ({ query }) => GetRequests.Likes(query), { query: Gets.LikesQuery })
.get('/posts', ({ query }) => GetRequests.Posts(query), { query: Gets.PostsQuery })
.get('/post', ({ query }) => GetRequests.Post(query), { query: Gets.PostQuery })
.get('/replies', ({ query }) => GetRequests.Replies(query), { query: Gets.RepliesQuery })
.get('/search', ({ query }) => GetRequests.Search(query), { query: Gets.SearchQuery })
.get('/user-replies', ({ query }) => GetRequests.UserReplies(query), { query: Gets.UserRepliesQuery })
.get('/following-posts', ({ query }) => GetRequests.FollowingPosts(query), { query: Gets.PostsQuery })
.get('/last-block', GetRequests.LastBlock);
23 changes: 23 additions & 0 deletions packages/api-main/src/routes/reader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Posts } from '@atomone/dither-api-types';
import { Elysia, t } from 'elysia';

import { readerAuthMiddleware } from '../middleware/readerAuth';
import * as PostRequests from '../posts/index';

/**
* Routes that require reader/indexer authorization
* These routes use the Authorization header for authentication
*/
export const readerRoutes = new Elysia()
.onBeforeHandle(readerAuthMiddleware)
.post('/post', ({ body }) => PostRequests.Post(body), { body: Posts.PostBody })
.post('/reply', ({ body }) => PostRequests.Reply(body), { body: Posts.ReplyBody })
.post('/follow', ({ body }) => PostRequests.Follow(body), { body: Posts.FollowBody })
.post('/unfollow', ({ body }) => PostRequests.Unfollow(body), { body: Posts.UnfollowBody })
.post('/like', ({ body }) => PostRequests.Like(body), { body: Posts.LikeBody })
.post('/dislike', ({ body }) => PostRequests.Dislike(body), { body: Posts.DislikeBody })
.post('/flag', ({ body }) => PostRequests.Flag(body), { body: Posts.FlagBody })
.post('/post-remove', ({ body }) => PostRequests.PostRemove(body), { body: Posts.PostRemoveBody })
.post('/update-state', ({ body }) => PostRequests.UpdateState(body), {
body: t.Object({ last_block: t.String() }),
});
18 changes: 18 additions & 0 deletions packages/api-main/src/routes/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Gets } from '@atomone/dither-api-types';
import { Elysia } from 'elysia';

import * as GetRequests from '../gets/index';

/**
* Routes that require user authentication via JWT cookie
*/
export const userRoutes = new Elysia()
.get('/notifications', ({ query, cookie: { auth } }) => GetRequests.Notifications(query, auth), {
query: Gets.NotificationsQuery,
})
.get('/notifications-count', ({ query, cookie: { auth } }) => GetRequests.NotificationsCount(query, auth), {
query: Gets.NotificationsCountQuery,
})
.post('/notification-read', ({ query, cookie: { auth } }) => GetRequests.ReadNotification(query, auth), {
query: Gets.ReadNotificationQuery,
});
Loading