mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-25 12:49:26 -05:00
Autoban on repeated FC + admin FC search Closes #2024
This commit is contained in:
6
app/features/admin/admin-schemas.ts
Normal file
6
app/features/admin/admin-schemas.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { friendCode } from "~/utils/zod";
|
||||
|
||||
export const adminActionSearchParamsSchema = z.object({
|
||||
friendCode,
|
||||
});
|
||||
@@ -1,10 +1,17 @@
|
||||
import type { LoaderFunction } from "@remix-run/node";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { redirect } from "@remix-run/node";
|
||||
import { getUserId, isImpersonating } from "~/features/auth/core/user.server";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { isMod } from "~/permissions";
|
||||
import { parseSafeSearchParams } from "~/utils/remix.server";
|
||||
import { adminActionSearchParamsSchema } from "../admin-schemas";
|
||||
|
||||
export const loader: LoaderFunction = async ({ request }) => {
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const user = await getUserId(request);
|
||||
const parsedSearchParams = parseSafeSearchParams({
|
||||
request,
|
||||
schema: adminActionSearchParamsSchema,
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === "production" && !isMod(user)) {
|
||||
throw redirect("/");
|
||||
@@ -12,5 +19,10 @@ export const loader: LoaderFunction = async ({ request }) => {
|
||||
|
||||
return {
|
||||
isImpersonating: await isImpersonating(request),
|
||||
friendCodeSearchUsers: parsedSearchParams.success
|
||||
? await UserRepository.findByFriendCode(
|
||||
parsedSearchParams.data.friendCode,
|
||||
)
|
||||
: [],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
import type { MetaFunction } from "@remix-run/node";
|
||||
import {
|
||||
Form,
|
||||
Link,
|
||||
useFetcher,
|
||||
useLoaderData,
|
||||
useNavigation,
|
||||
useSearchParams,
|
||||
} from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { Button } from "~/components/Button";
|
||||
import { Catcher } from "~/components/Catcher";
|
||||
import { Input } from "~/components/Input";
|
||||
import { Main } from "~/components/Main";
|
||||
import { NewTabs } from "~/components/NewTabs";
|
||||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import { UserSearch } from "~/components/UserSearch";
|
||||
import { SearchIcon } from "~/components/icons/Search";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { FRIEND_CODE_REGEXP_PATTERN } from "~/features/sendouq/q-constants";
|
||||
import { isAdmin, isMod } from "~/permissions";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import { SEED_URL, STOP_IMPERSONATING_URL, impersonateUrl } from "~/utils/urls";
|
||||
import {
|
||||
SEED_URL,
|
||||
STOP_IMPERSONATING_URL,
|
||||
impersonateUrl,
|
||||
userPage,
|
||||
} from "~/utils/urls";
|
||||
|
||||
import { action } from "../actions/admin.server";
|
||||
import { loader } from "../loaders/admin.server";
|
||||
@@ -30,10 +40,78 @@ export const meta: MetaFunction = (args) => {
|
||||
};
|
||||
|
||||
export default function AdminPage() {
|
||||
return (
|
||||
<Main>
|
||||
<NewTabs
|
||||
tabs={[
|
||||
{
|
||||
label: "Actions",
|
||||
},
|
||||
{
|
||||
label: "Friend code look-up",
|
||||
},
|
||||
]}
|
||||
content={[
|
||||
{
|
||||
key: "actions",
|
||||
element: <AdminActions />,
|
||||
},
|
||||
{
|
||||
key: "friend-code-look-up",
|
||||
element: <FriendCodeLookUp />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
|
||||
function FriendCodeLookUp() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [friendCode, setFriendCode] = React.useState(
|
||||
searchParams.get("friendCode") ?? "",
|
||||
);
|
||||
const fetcher = useFetcher();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="stack md horizontal justify-center">
|
||||
<Input
|
||||
placeholder="1234-5678-9101"
|
||||
name="friendCode"
|
||||
value={friendCode}
|
||||
onChange={(e) => setFriendCode(e.target.value)}
|
||||
/>
|
||||
<SubmitButton
|
||||
state={fetcher.state}
|
||||
icon={<SearchIcon />}
|
||||
onClick={() => setSearchParams({ friendCode })}
|
||||
>
|
||||
Search
|
||||
</SubmitButton>
|
||||
</div>
|
||||
<div className="stack lg">
|
||||
{data.friendCodeSearchUsers?.map((user) => (
|
||||
<Link
|
||||
key={user.id}
|
||||
to={userPage(user)}
|
||||
className="stack horizontal sm text-main-forced items-center"
|
||||
>
|
||||
<Avatar user={user} size="sm" />
|
||||
{user.username}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminActions() {
|
||||
const user = useUser();
|
||||
|
||||
return (
|
||||
<Main className="stack lg">
|
||||
<div className="stack lg">
|
||||
{process.env.NODE_ENV !== "production" && <Seed />}
|
||||
|
||||
{isMod(user) ? <LinkPlayer /> : null}
|
||||
@@ -51,7 +129,7 @@ export default function AdminPage() {
|
||||
{isMod(user) ? <UnbanUser /> : null}
|
||||
{isAdmin(user) ? <RefreshPlusTiers /> : null}
|
||||
{isAdmin(user) ? <CleanUp /> : null}
|
||||
</Main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { ActionFunction } from "@remix-run/node";
|
||||
import { redirect } from "@remix-run/node";
|
||||
import { sql } from "~/db/sql";
|
||||
import * as AdminRepository from "~/features/admin/AdminRepository.server";
|
||||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
import { refreshBannedCache } from "~/features/ban/core/banned.server";
|
||||
import { currentSeason } from "~/features/mmr/season";
|
||||
import * as QRepository from "~/features/sendouq/QRepository.server";
|
||||
import { giveTrust } from "~/features/tournament/queries/giveTrust.server";
|
||||
@@ -9,7 +11,11 @@ import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { errorToastIfFalsy, parseRequestPayload } from "~/utils/remix.server";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import { SENDOUQ_LOOKING_PAGE, SENDOUQ_PREPARING_PAGE } from "~/utils/urls";
|
||||
import {
|
||||
SENDOUQ_LOOKING_PAGE,
|
||||
SENDOUQ_PREPARING_PAGE,
|
||||
SUSPENDED_PAGE,
|
||||
} from "~/utils/urls";
|
||||
import { FULL_GROUP_SIZE, JOIN_CODE_SEARCH_PARAM_KEY } from "../q-constants";
|
||||
import { frontPageSchema } from "../q-schemas.server";
|
||||
import { userCanJoinQueueAt } from "../q-utils";
|
||||
@@ -88,12 +94,29 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
"Friend code already set",
|
||||
);
|
||||
|
||||
const isTakenFriendCode = (
|
||||
await UserRepository.allCurrentFriendCodes()
|
||||
).has(data.friendCode);
|
||||
|
||||
await UserRepository.insertFriendCode({
|
||||
userId: user.id,
|
||||
friendCode: data.friendCode,
|
||||
submitterUserId: user.id,
|
||||
});
|
||||
|
||||
if (isTakenFriendCode) {
|
||||
await AdminRepository.banUser({
|
||||
userId: user.id,
|
||||
banned: 1,
|
||||
bannedReason:
|
||||
"[automatic ban] This friend code is already in use by some other account. Please contact staff on our Discord helpdesk for resolution including merging accounts.",
|
||||
});
|
||||
|
||||
refreshBannedCache();
|
||||
|
||||
throw redirect(SUSPENDED_PAGE);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -253,6 +253,15 @@ export function findByCustomUrl(customUrl: string) {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
export function findByFriendCode(friendCode: string) {
|
||||
return db
|
||||
.selectFrom("UserFriendCode")
|
||||
.innerJoin("User", "User.id", "UserFriendCode.userId")
|
||||
.select([...COMMON_USER_FIELDS])
|
||||
.where("UserFriendCode.friendCode", "=", friendCode)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export function findBannedStatusByUserId(userId: number) {
|
||||
return db
|
||||
.selectFrom("User")
|
||||
@@ -553,6 +562,36 @@ export async function currentFriendCodeByUserId(userId: number) {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
let cachedFriendCodes: Set<string> | null = null;
|
||||
|
||||
export async function allCurrentFriendCodes() {
|
||||
if (cachedFriendCodes) {
|
||||
return cachedFriendCodes;
|
||||
}
|
||||
|
||||
const allFriendCodes = await db
|
||||
.selectFrom("UserFriendCode")
|
||||
.select(["UserFriendCode.friendCode", "UserFriendCode.userId"])
|
||||
.orderBy("UserFriendCode.createdAt desc")
|
||||
.execute();
|
||||
|
||||
const seenUserIds = new Set<number>();
|
||||
const friendCodes = new Set<string>();
|
||||
|
||||
for (const row of allFriendCodes) {
|
||||
if (seenUserIds.has(row.userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seenUserIds.add(row.userId);
|
||||
friendCodes.add(row.friendCode);
|
||||
}
|
||||
|
||||
cachedFriendCodes = friendCodes;
|
||||
|
||||
return friendCodes;
|
||||
}
|
||||
|
||||
export async function inGameNameByUserId(userId: number) {
|
||||
return (
|
||||
await db
|
||||
@@ -564,6 +603,8 @@ export async function inGameNameByUserId(userId: number) {
|
||||
}
|
||||
|
||||
export function insertFriendCode(args: TablesInsertable["UserFriendCode"]) {
|
||||
cachedFriendCodes?.add(args.friendCode);
|
||||
|
||||
return db.insertInto("UserFriendCode").values(args).execute();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user