Skip to content

Commit

Permalink
Prevent banned users from creating new posts and comments
Browse files Browse the repository at this point in the history
  • Loading branch information
tom-sherman committed Dec 16, 2024
1 parent 5d362ae commit a040f87
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 55 deletions.
40 changes: 12 additions & 28 deletions packages/frontpage/app/(app)/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import {
Heading1,
Paragraph,
Heading2,
TextLink,
} from "@/lib/components/ui/typography";
import { Metadata } from "next";
import { ReactNode } from "react";

Check failure on line 8 in packages/frontpage/app/(app)/about/page.tsx

View workflow job for this annotation

GitHub Actions / lint

'ReactNode' is defined but never used. Allowed unused vars must match /^_/u

Expand All @@ -18,55 +24,33 @@ export default function CommunityGuidelinesPage() {
Frontpage is a decentralised and federated link aggregator that's
built on the same protocol as Bluesky.
</Paragraph>

<Heading2>Community Guidelines</Heading2>

<Paragraph>
We want Frontpage to be a safe and welcoming place for everyone. And so
we ask that you follow these guidelines:
</Paragraph>

<ol className="my-6 ml-6 list-decimal [&>li]:mt-2">
<li>
Don&apos;t post hate speech, harassment, or other forms of abuse.
</li>
<li>Don&apos;t post content that is illegal or harmful.</li>
<li>Don&apos;t post adult content*.</li>
</ol>

<small className="text-sm font-medium leading-none">
* this is a temporary guideline while we build labeling and content
warning features.
</small>

<Paragraph>
Frontpage is moderated by it&apos;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.
</Paragraph>
<Heading2 id="contact">Contact</Heading2>
<Paragraph>
Email us at{" "}
<TextLink href="mailto:[email protected]">[email protected]</TextLink>
.
</Paragraph>
</>
);
}

function Heading1({ children }: { children: ReactNode }) {
return (
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
{children}
</h1>
);
}

function Heading2({ children, id }: { children: ReactNode; id?: string }) {
return (
<h2
id={id}
className="mt-10 scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0"
>
{children}
</h2>
);
}

function Paragraph({ children }: { children: ReactNode }) {
return <p className="leading-7 [&:not(:first-child)]:mt-6">{children}</p>;
}
18 changes: 9 additions & 9 deletions packages/frontpage/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -55,12 +56,11 @@ export default async function Layout({
<footer className="flex justify-between items-center text-gray-500 dark:text-gray-400">
<p>
Made by{" "}
<a
<TextLink
href={`https://bsky.app/profile/${FRONTPAGE_ATPROTO_HANDLE}`}
className="font-medium text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
@frontpage.fyi <OpenInNewWindowIcon className="inline" />
</a>
</TextLink>
</p>
</footer>
</div>
Expand Down Expand Up @@ -98,12 +98,12 @@ async function LoginOrLogout() {
<Link href={`/profile/${handle}`} className="cursor-pointer">
Profile
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/about" className="cursor-pointer">
About
</Link>
</DropdownMenuItem>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/about" className="cursor-pointer">
About
</Link>
</DropdownMenuItem>
<Suspense fallback={null}>
{isAdmin().then((isAdmin) =>
isAdmin ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.{" "}
<TextLink href="/about#contact">Contact us</TextLink> to appeal.
</>
),
};
}

const [post, comment] = await Promise.all([
getPost(input.postAuthorDid, input.postRkey),
Expand All @@ -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") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -238,22 +232,32 @@ export function NewComment({
textAreaRef?: React.RefObject<HTMLTextAreaElement>;
}) {
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 (
<form
action={action}
onSubmit={(event) => {
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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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.{" "}
<TextLink href="/about#contact">Contact us</TextLink> to appeal.
</>
),
};
}

try {
const { rkey } = await createPost({ title, url });
const [handle] = await Promise.all([
Expand Down
3 changes: 2 additions & 1 deletion packages/frontpage/app/(app)/profile/[user]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,7 +34,7 @@ export async function generateMetadata(props: {
}): Promise<Metadata> {
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([
Expand Down
5 changes: 5 additions & 0 deletions packages/frontpage/app/api/receive_hook/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand Down
48 changes: 48 additions & 0 deletions packages/frontpage/lib/components/ui/typography.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Link, { LinkProps } from "next/link";
import { ReactNode } from "react";

export function Heading1({ children }: { children: ReactNode }) {
return (
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
{children}
</h1>
);
}

export function Heading2({
children,
id,
}: {
children: ReactNode;
id?: string;
}) {
return (
<h2
id={id}
className="mt-10 scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0"
>
{children}
</h2>
);
}

export function Paragraph({ children }: { children: ReactNode }) {
return <p className="leading-7 [&:not(:first-child)]:mt-6">{children}</p>;
}

export function TextLink({
href,
children,
}: {
href: LinkProps["href"];
children: ReactNode;
}) {
return (
<Link
href={href}
className="font-medium text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{children}
</Link>
);
}
13 changes: 13 additions & 0 deletions packages/frontpage/lib/data/db/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
});

0 comments on commit a040f87

Please sign in to comment.