From a040f87c20194045d35d1c475c20586e116ee430 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Mon, 16 Dec 2024 11:19:42 +0000 Subject: [PATCH 1/2] Prevent banned users from creating new posts and comments --- packages/frontpage/app/(app)/about/page.tsx | 40 +++++----------- packages/frontpage/app/(app)/layout.tsx | 18 +++---- .../[postAuthor]/[postRkey]/_lib/actions.tsx | 15 +++++- .../[postRkey]/_lib/comment-client.tsx | 34 +++++++------ .../post/new/{_action.ts => _action.tsx} | 13 +++++ .../app/(app)/profile/[user]/page.tsx | 3 +- .../frontpage/app/api/receive_hook/route.ts | 5 ++ .../lib/components/ui/typography.tsx | 48 +++++++++++++++++++ packages/frontpage/lib/data/db/user.ts | 13 +++++ 9 files changed, 134 insertions(+), 55 deletions(-) rename packages/frontpage/app/(app)/post/new/{_action.ts => _action.tsx} (80%) create mode 100644 packages/frontpage/lib/components/ui/typography.tsx diff --git a/packages/frontpage/app/(app)/about/page.tsx b/packages/frontpage/app/(app)/about/page.tsx index b48304c4..8fa28a9f 100644 --- a/packages/frontpage/app/(app)/about/page.tsx +++ b/packages/frontpage/app/(app)/about/page.tsx @@ -1,3 +1,9 @@ +import { + Heading1, + Paragraph, + Heading2, + TextLink, +} from "@/lib/components/ui/typography"; import { Metadata } from "next"; import { ReactNode } from "react"; @@ -18,14 +24,11 @@ export default function CommunityGuidelinesPage() { Frontpage is a decentralised and federated link aggregator that's built on the same protocol as Bluesky. - Community Guidelines - We want Frontpage to be a safe and welcoming place for everyone. And so we ask that you follow these guidelines: -
  1. Don't post hate speech, harassment, or other forms of abuse. @@ -33,40 +36,21 @@ export default function CommunityGuidelinesPage() {
  2. Don't post content that is illegal or harmful.
  3. Don't post adult content*.
- * this is a temporary guideline while we build labeling and content warning features. - Frontpage is moderated by it's core developers, but we also rely on reports from users to help us keep the community safe. Please report any content that violates our guidelines. + Contact + + Email us at{" "} + team@frontpage.fyi + . + ); } - -function Heading1({ children }: { children: ReactNode }) { - return ( -

- {children} -

- ); -} - -function Heading2({ children, id }: { children: ReactNode; id?: string }) { - return ( -

- {children} -

- ); -} - -function Paragraph({ children }: { children: ReactNode }) { - return

{children}

; -} diff --git a/packages/frontpage/app/(app)/layout.tsx b/packages/frontpage/app/(app)/layout.tsx index 232502d9..ec97f32b 100644 --- a/packages/frontpage/app/(app)/layout.tsx +++ b/packages/frontpage/app/(app)/layout.tsx @@ -23,6 +23,7 @@ import { FRONTPAGE_ATPROTO_HANDLE } from "@/lib/constants"; import { cookies } from "next/headers"; import { revalidatePath } from "next/cache"; import { NotificationIndicator } from "./_components/notification-indicator"; +import { TextLink } from "@/lib/components/ui/typography"; export default async function Layout({ children, @@ -55,12 +56,11 @@ export default async function Layout({ @@ -98,12 +98,12 @@ async function LoginOrLogout() { Profile - - - - About - - + + + + About + + {isAdmin().then((isAdmin) => isAdmin ? ( diff --git a/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/_lib/actions.tsx b/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/_lib/actions.tsx index 6e6b3137..0ee1e3d8 100644 --- a/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/_lib/actions.tsx +++ b/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/_lib/actions.tsx @@ -14,14 +14,25 @@ import { createReport } from "@/lib/data/db/report"; import { getVoteForComment } from "@/lib/data/db/vote"; import { ensureUser } from "@/lib/data/user"; import { revalidatePath } from "next/cache"; +import { isBanned } from "@/lib/data/db/user"; +import { TextLink } from "@/lib/components/ui/typography"; export async function createCommentAction( input: { parentRkey?: string; postRkey: string; postAuthorDid: DID }, - _prevState: unknown, formData: FormData, ) { const content = formData.get("comment") as string; const user = await ensureUser(); + if (await isBanned(user.did)) { + return { + error: ( + <> + Your account is currently banned from creating new comments.{" "} + Contact us to appeal. + + ), + }; + } const [post, comment] = await Promise.all([ getPost(input.postAuthorDid, input.postRkey), @@ -34,7 +45,7 @@ export async function createCommentAction( ]); if (!post) { - throw new Error("Post not found"); + return { error: "Failed to create comment. Post not found." }; } if (post.status !== "live") { diff --git a/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/_lib/comment-client.tsx b/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/_lib/comment-client.tsx index e8fd4cac..c24396e7 100644 --- a/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/_lib/comment-client.tsx +++ b/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/_lib/comment-client.tsx @@ -22,13 +22,7 @@ import { reportCommentAction, } from "./actions"; import { ChatBubbleIcon, TrashIcon } from "@radix-ui/react-icons"; -import { - useActionState, - useRef, - useState, - useId, - startTransition, -} from "react"; +import { useRef, useState, useId, startTransition, useTransition } from "react"; import { VoteButton, VoteButtonState, @@ -238,22 +232,32 @@ export function NewComment({ textAreaRef?: React.RefObject; }) { const [input, setInput] = useState(""); - const [_, action, isPending] = useActionState( - createCommentAction.bind(null, { parentRkey, postRkey, postAuthorDid }), - undefined, - ); + const action = createCommentAction.bind(null, { + parentRkey, + postRkey, + postAuthorDid, + }); + const [isPending, startTransition] = useTransition(); const id = useId(); + const { toast } = useToast(); const textAreaId = `${id}-comment`; return (
{ event.preventDefault(); - startTransition(() => { - action(new FormData(event.currentTarget)); + startTransition(async () => { + const result = await action(new FormData(event.currentTarget)); onActionDone?.(); - setInput(""); + if (result?.error) { + toast({ + title: "Failed to create comment", + description: result.error, + type: "foreground", + }); + } else { + setInput(""); + } }); }} aria-busy={isPending} diff --git a/packages/frontpage/app/(app)/post/new/_action.ts b/packages/frontpage/app/(app)/post/new/_action.tsx similarity index 80% rename from packages/frontpage/app/(app)/post/new/_action.ts rename to packages/frontpage/app/(app)/post/new/_action.tsx index 0e1995f4..0c0d08db 100644 --- a/packages/frontpage/app/(app)/post/new/_action.ts +++ b/packages/frontpage/app/(app)/post/new/_action.tsx @@ -1,9 +1,11 @@ "use server"; +import { TextLink } from "@/lib/components/ui/typography"; import { DID } from "@/lib/data/atproto/did"; import { getVerifiedHandle } from "@/lib/data/atproto/identity"; import { createPost } from "@/lib/data/atproto/post"; import { uncached_doesPostExist } from "@/lib/data/db/post"; +import { isBanned } from "@/lib/data/db/user"; import { DataLayerError } from "@/lib/data/error"; import { ensureUser } from "@/lib/data/user"; import { redirect } from "next/navigation"; @@ -26,6 +28,17 @@ export async function newPostAction(_prevState: unknown, formData: FormData) { return { error: "Invalid URL" }; } + if (await isBanned(user.did)) { + return { + error: ( + <> + Your account is currently banned from creating new posts.{" "} + Contact us to appeal. + + ), + }; + } + try { const { rkey } = await createPost({ title, url }); const [handle] = await Promise.all([ diff --git a/packages/frontpage/app/(app)/profile/[user]/page.tsx b/packages/frontpage/app/(app)/profile/[user]/page.tsx index fc16d44d..2add2f95 100644 --- a/packages/frontpage/app/(app)/profile/[user]/page.tsx +++ b/packages/frontpage/app/(app)/profile/[user]/page.tsx @@ -23,6 +23,7 @@ import { ReportDialogDropdownButton } from "../../_components/report-dialog"; import { reportUserAction } from "@/lib/components/user-hover-card"; import { Metadata } from "next"; import { LinkAlternateAtUri } from "@/lib/components/link-alternate-at"; +import { isBanned } from "@/lib/data/db/user"; type Params = { user: string; @@ -33,7 +34,7 @@ export async function generateMetadata(props: { }): Promise { const params = await props.params; const did = await getDidFromHandleOrDid(params.user); - if (!did) { + if (!did || (await isBanned(did))) { notFound(); } const [handle, profile] = await Promise.all([ diff --git a/packages/frontpage/app/api/receive_hook/route.ts b/packages/frontpage/app/api/receive_hook/route.ts index 04bda27c..15c470b2 100644 --- a/packages/frontpage/app/api/receive_hook/route.ts +++ b/packages/frontpage/app/api/receive_hook/route.ts @@ -17,6 +17,7 @@ import { unauthed_createCommentVote, } from "@/lib/data/db/vote"; import { unauthed_createNotification } from "@/lib/data/db/notification"; +import { isBanned } from "@/lib/data/db/user"; export async function POST(request: Request) { const auth = request.headers.get("Authorization"); @@ -31,6 +32,10 @@ export async function POST(request: Request) { } const { ops, repo, seq } = commit.data; + if (await isBanned(repo)) { + throw new Error("[naughty] User is banned"); + } + const service = await getPdsUrl(repo); if (!service) { throw new Error("No AtprotoPersonalDataServer service found"); diff --git a/packages/frontpage/lib/components/ui/typography.tsx b/packages/frontpage/lib/components/ui/typography.tsx new file mode 100644 index 00000000..88f6072a --- /dev/null +++ b/packages/frontpage/lib/components/ui/typography.tsx @@ -0,0 +1,48 @@ +import Link, { LinkProps } from "next/link"; +import { ReactNode } from "react"; + +export function Heading1({ children }: { children: ReactNode }) { + return ( +

+ {children} +

+ ); +} + +export function Heading2({ + children, + id, +}: { + children: ReactNode; + id?: string; +}) { + return ( +

+ {children} +

+ ); +} + +export function Paragraph({ children }: { children: ReactNode }) { + return

{children}

; +} + +export function TextLink({ + href, + children, +}: { + href: LinkProps["href"]; + children: ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/packages/frontpage/lib/data/db/user.ts b/packages/frontpage/lib/data/db/user.ts index f82d6a6a..5eb555d4 100644 --- a/packages/frontpage/lib/data/db/user.ts +++ b/packages/frontpage/lib/data/db/user.ts @@ -2,6 +2,8 @@ import { db } from "@/lib/db"; import { DID } from "../atproto/did"; import * as schema from "@/lib/schema"; import { isAdmin } from "../user"; +import { cache } from "react"; +import { and, eq } from "drizzle-orm"; type ModerateUserInput = { userDid: DID; @@ -35,3 +37,14 @@ export async function moderateUser({ set: { isHidden: hide, labels: label, updatedAt: new Date() }, }); } + +export const isBanned = cache(async (did: DID) => { + const bannedUser = await db.query.LabelledProfile.findFirst({ + where: and( + eq(schema.LabelledProfile.did, did), + eq(schema.LabelledProfile.isHidden, true), + ), + }); + + return Boolean(bannedUser); +}); From e8af35be320068591198ec22259a3e33c37f5858 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Mon, 16 Dec 2024 11:27:12 +0000 Subject: [PATCH 2/2] Dont allow banned users to vote in UI --- .../frontpage/app/(app)/_components/post-card.tsx | 11 +++++++++-- .../post/[postAuthor]/[postRkey]/_lib/actions.tsx | 5 ++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/frontpage/app/(app)/_components/post-card.tsx b/packages/frontpage/app/(app)/_components/post-card.tsx index 1820874d..f65a42fe 100644 --- a/packages/frontpage/app/(app)/_components/post-card.tsx +++ b/packages/frontpage/app/(app)/_components/post-card.tsx @@ -15,6 +15,7 @@ import { revalidatePath } from "next/cache"; import { ReportDialogDropdownButton } from "./report-dialog"; import { DeleteButton } from "./delete-button"; import { ShareDropdownButton } from "./share-button"; +import { isBanned } from "@/lib/data/db/user"; type PostProps = { id: number; @@ -54,7 +55,10 @@ export async function PostCard({ { "use server"; - await ensureUser(); + const user = await ensureUser(); + if (await isBanned(user.did)) { + throw new Error("Author is banned"); + } await createVote({ subjectAuthorDid: author, subjectCid: cid, @@ -64,7 +68,10 @@ export async function PostCard({ }} unvoteAction={async () => { "use server"; - await ensureUser(); + const user = await ensureUser(); + if (await isBanned(user.did)) { + throw new Error("Author is banned"); + } const vote = await getVoteForPost(id); if (!vote) { // TODO: Show error notification diff --git a/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/_lib/actions.tsx b/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/_lib/actions.tsx index 0ee1e3d8..44cf58df 100644 --- a/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/_lib/actions.tsx +++ b/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/_lib/actions.tsx @@ -110,7 +110,10 @@ export async function commentVoteAction(input: { rkey: string; authorDid: DID; }) { - await ensureUser(); + const user = await ensureUser(); + if (await isBanned(user.did)) { + throw new Error("Author is banned"); + } await createVote({ subjectAuthorDid: input.authorDid, subjectCid: input.cid,