From d2551d2706f9df627150df129e8c39de1fedd2eb Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Mon, 21 Apr 2025 23:51:30 +0300 Subject: [PATCH] Global roles refactor (#2212) * Initial * isMod etc. * canPerformAdminActions * isAdmin * isSupporter * admin override * Lohi * Badge manage with new permissions style * Refactor badge loading logic * Move funcs * Delete permissions.ts * DRY --- app/components/Main.tsx | 6 +- app/components/layout/index.tsx | 4 +- app/constants.ts | 4 +- app/features/admin/actions/admin.server.ts | 34 ++--- app/features/admin/loaders/admin.server.ts | 16 +- app/features/admin/routes/admin.tsx | 30 ++-- app/features/api-private/routes/patrons.ts | 11 +- app/features/api-private/routes/users.ts | 2 +- app/features/art/routes/art.new.tsx | 6 +- .../actions/associations.new.server.ts | 3 +- .../actions/associations.server.ts | 3 +- app/features/auth/core/routes.server.ts | 16 +- app/features/badges/BadgeRepository.server.ts | 111 ++++++++------ .../badges/actions/badges.$id.edit.server.ts | 38 ++--- .../badges/loaders/badges.$id.server.ts | 17 ++- .../badges/routes/badges.$id.edit.tsx | 35 +++-- app/features/badges/routes/badges.$id.tsx | 36 ++--- app/features/badges/routes/badges.tsx | 2 +- .../calendar.$id.report-winners.server.ts | 2 +- .../calendar/actions/calendar.$id.server.ts | 2 +- .../calendar/actions/calendar.new.server.ts | 6 +- app/features/calendar/calendar-utils.ts | 56 ++++++- .../calendar.$id.report-winners.server.ts | 2 +- .../calendar/loaders/calendar.new.server.ts | 9 +- app/features/calendar/routes/calendar.$id.tsx | 10 +- app/features/calendar/routes/calendar.new.tsx | 10 +- .../img-upload/actions/upload.admin.server.ts | 11 +- .../img-upload/loaders/upload.admin.server.ts | 10 +- app/features/lfg/actions/lfg.server.ts | 3 +- app/features/lfg/components/LFGPost.tsx | 8 +- app/features/lfg/routes/lfg.new.tsx | 5 +- ...uggestions.comment.$tier.$userId.server.ts | 2 +- .../actions/plus.suggestions.new.server.ts | 2 +- .../actions/plus.suggestions.server.ts | 2 +- .../plus-suggestions-utils.ts} | 142 +----------------- ...plus.suggestions.comment.$tier.$userId.tsx | 2 +- .../routes/plus.suggestions.new.tsx | 6 +- .../routes/plus.suggestions.tsx | 10 +- .../loaders/plus.voting.results.server.ts | 7 +- app/features/plus-voting/routes/plus.list.ts | 2 +- .../scrims/loaders/scrims.$id.server.ts | 3 +- .../actions/q.match.$id.server.ts | 3 +- .../loaders/q.match.$id.server.ts | 8 +- .../sendouq-match/routes/q.match.$id.tsx | 12 +- .../sendouq/loaders/q.looking.server.ts | 4 +- app/features/sendouq/routes/q.tsx | 6 +- .../team/actions/t.$customUrl.edit.server.ts | 7 +- .../actions/t.$customUrl.roster.server.ts | 7 +- app/features/team/actions/t.server.ts | 3 +- .../team/loaders/t.$customUrl.edit.server.ts | 7 +- .../loaders/t.$customUrl.roster.server.ts | 8 +- app/features/team/routes/t.$customUrl.tsx | 9 +- app/features/team/routes/t.tsx | 6 +- .../actions/to.$id.matches.$mid.server.ts | 21 ++- .../core/Tournament.server.ts | 2 +- .../tournament-bracket/core/Tournament.ts | 2 +- .../tournament-organization-utils.ts | 2 +- .../tournament/loaders/to.$id.server.ts | 3 +- .../user-page/UserRepository.server.ts | 20 +-- .../loaders/u.$identifier.index.server.ts | 7 +- .../user-page/routes/short.$customUrl.ts | 4 +- .../user-page/routes/u.$identifier.edit.tsx | 11 +- app/features/vods/routes/vods.new.tsx | 6 +- app/features/vods/vods-utils.ts | 2 +- app/modules/patreon/updater.ts | 4 +- app/modules/permissions/guards.server.ts | 15 +- app/modules/permissions/hooks.ts | 19 ++- app/modules/permissions/mapper.server.ts | 57 +++++++ app/modules/permissions/types.ts | 12 ++ app/modules/permissions/utils.ts | 15 ++ app/root.tsx | 9 +- app/utils/remix.server.ts | 7 + app/utils/users.test.ts | 8 +- app/utils/users.ts | 24 +-- 74 files changed, 525 insertions(+), 491 deletions(-) rename app/{permissions.ts => features/plus-suggestions/plus-suggestions-utils.ts} (57%) create mode 100644 app/modules/permissions/mapper.server.ts create mode 100644 app/modules/permissions/utils.ts diff --git a/app/components/Main.tsx b/app/components/Main.tsx index d38e60388..70acd8ad6 100644 --- a/app/components/Main.tsx +++ b/app/components/Main.tsx @@ -1,7 +1,7 @@ import { isRouteErrorResponse, useRouteError } from "@remix-run/react"; import clsx from "clsx"; import type * as React from "react"; -import { useUser } from "~/features/auth/core/user"; +import { useHasRole } from "~/modules/permissions/hooks"; export const Main = ({ children, @@ -19,10 +19,10 @@ export const Main = ({ style?: React.CSSProperties; }) => { const error = useRouteError(); - const user = useUser(); + const isMinorSupporter = useHasRole("MINOR_SUPPORT"); const showLeaderboard = import.meta.env.VITE_PLAYWIRE_PUBLISHER_ID && - !user?.patronTier && + !isMinorSupporter && !isRouteErrorResponse(error); return ( diff --git a/app/components/layout/index.tsx b/app/components/layout/index.tsx index e70b913fc..a7ec84b35 100644 --- a/app/components/layout/index.tsx +++ b/app/components/layout/index.tsx @@ -48,7 +48,7 @@ export const Layout = React.memo(function Layout({ const showLeaderboard = import.meta.env.VITE_PLAYWIRE_PUBLISHER_ID && - !data?.user?.patronTier && + !data?.user?.roles.includes("MINOR_SUPPORT") && !location.pathname.includes("plans"); return (
@@ -81,7 +81,7 @@ export const Layout = React.memo(function Layout({ setNavDialogOpen(true)} /> diff --git a/app/constants.ts b/app/constants.ts index e1338634a..f54948365 100644 --- a/app/constants.ts +++ b/app/constants.ts @@ -58,8 +58,8 @@ export const ADMIN_DISCORD_ID = "79237403620945920"; export const ADMIN_ID = process.env.NODE_ENV === "test" ? 1 : 274; // Panda Scep -export const MOD_IDS = [11329, 9719]; -export const MOD_DISCORD_IDS = ["138757634500067328", "184478601171828737"]; +export const STAFF_IDS = [11329, 9719]; +export const STAFF_DISCORD_IDS = ["138757634500067328", "184478601171828737"]; export const LOHI_TOKEN_HEADER_NAME = "Lohi-Token"; export const SKALOP_TOKEN_HEADER_NAME = "Skalop-Token"; diff --git a/app/features/admin/actions/admin.server.ts b/app/features/admin/actions/admin.server.ts index 1aae50689..d0b456605 100644 --- a/app/features/admin/actions/admin.server.ts +++ b/app/features/admin/actions/admin.server.ts @@ -2,16 +2,12 @@ import type { ActionFunctionArgs } from "@remix-run/node"; import { z } from "zod"; import * as AdminRepository from "~/features/admin/AdminRepository.server"; import { makeArtist } from "~/features/art/queries/makeArtist.server"; -import { requireUserId } from "~/features/auth/core/user.server"; +import { requireUser } from "~/features/auth/core/user.server"; import { refreshBannedCache } from "~/features/ban/core/banned.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; -import { isAdmin, isMod } from "~/permissions"; +import { requireRole } from "~/modules/permissions/guards.server"; import { logger } from "~/utils/logger"; -import { - errorToastIfFalsy, - parseRequestPayload, - successToast, -} from "~/utils/remix.server"; +import { parseRequestPayload, successToast } from "~/utils/remix.server"; import { assertUnreachable } from "~/utils/types"; import { _action, actualNumber, friendCode } from "~/utils/zod"; import { plusTiersFromVotingAndLeaderboard } from "../core/plus-tier.server"; @@ -21,12 +17,12 @@ export const action = async ({ request }: ActionFunctionArgs) => { request, schema: adminActionSchema, }); - const user = await requireUserId(request); + const user = await requireUser(request); let message: string; switch (data._action) { case "MIGRATE": { - errorToastIfFalsy(isMod(user), "Admin needed"); + requireRole(user, "STAFF"); await AdminRepository.migrate({ oldUserId: data["old-user"], @@ -37,7 +33,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { break; } case "REFRESH": { - errorToastIfFalsy(isAdmin(user), "Admin needed"); + requireRole(user, "ADMIN"); await AdminRepository.replacePlusTiers( await plusTiersFromVotingAndLeaderboard(), @@ -47,7 +43,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { break; } case "FORCE_PATRON": { - errorToastIfFalsy(isAdmin(user), "Admin needed"); + requireRole(user, "ADMIN"); await AdminRepository.forcePatron({ id: data.user, @@ -60,7 +56,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { break; } case "CLEAN_UP": { - errorToastIfFalsy(isAdmin(user), "Admin needed"); + requireRole(user, "ADMIN"); // on purpose sync AdminRepository.cleanUp(); @@ -69,7 +65,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { break; } case "ARTIST": { - errorToastIfFalsy(isMod(user), "Mod needed"); + requireRole(user, "STAFF"); makeArtist(data.user); @@ -77,7 +73,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { break; } case "VIDEO_ADDER": { - errorToastIfFalsy(isMod(user), "Mod needed"); + requireRole(user, "STAFF"); await AdminRepository.makeVideoAdderByUserId(data.user); @@ -85,7 +81,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { break; } case "TOURNAMENT_ORGANIZER": { - errorToastIfFalsy(isMod(user), "Mod needed"); + requireRole(user, "STAFF"); await AdminRepository.makeTournamentOrganizerByUserId(data.user); @@ -93,7 +89,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { break; } case "LINK_PLAYER": { - errorToastIfFalsy(isMod(user), "Mod needed"); + requireRole(user, "STAFF"); await AdminRepository.linkUserAndPlayer({ userId: data.user, @@ -104,7 +100,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { break; } case "BAN_USER": { - errorToastIfFalsy(isMod(user), "Mod needed"); + requireRole(user, "STAFF"); await AdminRepository.banUser({ bannedReason: data.reason ?? null, @@ -127,7 +123,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { break; } case "UNBAN_USER": { - errorToastIfFalsy(isMod(user), "Mod needed"); + requireRole(user, "STAFF"); await AdminRepository.unbanUser(data.user); @@ -142,7 +138,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { break; } case "UPDATE_FRIEND_CODE": { - errorToastIfFalsy(isMod(user), "Mod needed"); + requireRole(user, "STAFF"); await UserRepository.insertFriendCode({ friendCode: data.friendCode, diff --git a/app/features/admin/loaders/admin.server.ts b/app/features/admin/loaders/admin.server.ts index 20c98e32e..b9067d3e7 100644 --- a/app/features/admin/loaders/admin.server.ts +++ b/app/features/admin/loaders/admin.server.ts @@ -1,22 +1,22 @@ import type { LoaderFunctionArgs } from "@remix-run/node"; -import { redirect } from "@remix-run/node"; -import { getUserId, isImpersonating } from "~/features/auth/core/user.server"; +import { isImpersonating, requireUser } from "~/features/auth/core/user.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; -import { isMod } from "~/permissions"; +import { requireRole } from "~/modules/permissions/guards.server"; import { parseSafeSearchParams } from "~/utils/remix.server"; import { adminActionSearchParamsSchema } from "../admin-schemas"; export const loader = async ({ request }: LoaderFunctionArgs) => { - const user = await getUserId(request); + // allow unauthorized access in development mode to access impersonation controls + if (process.env.NODE_ENV === "production") { + const user = await requireUser(request); + requireRole(user, "STAFF"); + } + const parsedSearchParams = parseSafeSearchParams({ request, schema: adminActionSearchParamsSchema, }); - if (process.env.NODE_ENV === "production" && !isMod(user)) { - throw redirect("/"); - } - return { isImpersonating: await isImpersonating(request), friendCodeSearchUsers: parsedSearchParams.success diff --git a/app/features/admin/routes/admin.tsx b/app/features/admin/routes/admin.tsx index 07c903ca5..eb7f325a4 100644 --- a/app/features/admin/routes/admin.tsx +++ b/app/features/admin/routes/admin.tsx @@ -17,9 +17,8 @@ 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 { useHasRole } from "~/modules/permissions/hooks"; import { metaTags } from "~/utils/remix"; import { SEED_URL, @@ -108,27 +107,28 @@ function FriendCodeLookUp() { } function AdminActions() { - const user = useUser(); + const isStaff = useHasRole("STAFF"); + const isAdmin = useHasRole("ADMIN"); return (
{process.env.NODE_ENV !== "production" && } - {isMod(user) ? : null} - {isMod(user) ? : null} - {isMod(user) ? : null} - {isMod(user) ? : null} - {isMod(user) ? : null} + {isStaff ? : null} + {isStaff ? : null} + {isStaff ? : null} + {isStaff ? : null} + {isStaff ? : null} - {process.env.NODE_ENV !== "production" || isAdmin(user) ? ( + {process.env.NODE_ENV !== "production" || isAdmin ? ( ) : null} - {isMod(user) ? : null} - {isAdmin(user) ? : null} - {isMod(user) ? : null} - {isMod(user) ? : null} - {isAdmin(user) ? : null} - {isAdmin(user) ? : null} + {isStaff ? : null} + {isAdmin ? : null} + {isStaff ? : null} + {isStaff ? : null} + {isAdmin ? : null} + {isAdmin ? : null}
); } diff --git a/app/features/api-private/routes/patrons.ts b/app/features/api-private/routes/patrons.ts index 757ff4672..1d9d299a4 100644 --- a/app/features/api-private/routes/patrons.ts +++ b/app/features/api-private/routes/patrons.ts @@ -1,14 +1,13 @@ import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/node"; -import { getUserId } from "~/features/auth/core/user.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; import { updatePatreonData } from "~/modules/patreon"; -import { canAccessLohiEndpoint, canPerformAdminActions } from "~/permissions"; -import { unauthorizedIfFalsy } from "~/utils/remix.server"; +import { + canAccessLohiEndpoint, + unauthorizedIfFalsy, +} from "~/utils/remix.server"; export const action: ActionFunction = async ({ request }) => { - const user = await getUserId(request); - - if (!canPerformAdminActions(user) && !canAccessLohiEndpoint(request)) { + if (!canAccessLohiEndpoint(request)) { throw new Response("Not authorized", { status: 403 }); } diff --git a/app/features/api-private/routes/users.ts b/app/features/api-private/routes/users.ts index 5d2416a62..0d3fccc53 100644 --- a/app/features/api-private/routes/users.ts +++ b/app/features/api-private/routes/users.ts @@ -1,6 +1,6 @@ import type { ActionFunction } from "@remix-run/node"; import * as UserRepository from "~/features/user-page/UserRepository.server"; -import { canAccessLohiEndpoint } from "~/permissions"; +import { canAccessLohiEndpoint } from "~/utils/remix.server"; export const action: ActionFunction = async ({ request }) => { if (!canAccessLohiEndpoint(request)) { diff --git a/app/features/art/routes/art.new.tsx b/app/features/art/routes/art.new.tsx index c8f0e6a9a..ba136ffde 100644 --- a/app/features/art/routes/art.new.tsx +++ b/app/features/art/routes/art.new.tsx @@ -14,7 +14,7 @@ import { Main } from "~/components/Main"; import { UserSearch } from "~/components/UserSearch"; import { SendouSwitch } from "~/components/elements/Switch"; import { CrossIcon } from "~/components/icons/Cross"; -import { useUser } from "~/features/auth/core/user"; +import { useHasRole } from "~/modules/permissions/hooks"; import invariant from "~/utils/invariant"; import type { SendouRouteHandle } from "~/utils/remix.server"; import { @@ -52,7 +52,7 @@ export default function NewArtPage() { const { t } = useTranslation(["common", "art"]); const ref = React.useRef(null); const fetcher = useFetcher(); - const user = useUser(); + const isArtist = useHasRole("ARTIST"); const handleSubmit = () => { const formData = new FormData(ref.current!); @@ -72,7 +72,7 @@ export default function NewArtPage() { return !img && !data.art; }; - if (!user || !user.isArtist) { + if (!isArtist) { return (
{t("art:gainPerms")} diff --git a/app/features/associations/actions/associations.new.server.ts b/app/features/associations/actions/associations.new.server.ts index 81798aa48..c18743d7e 100644 --- a/app/features/associations/actions/associations.new.server.ts +++ b/app/features/associations/actions/associations.new.server.ts @@ -4,7 +4,6 @@ import { createNewAssociationSchema } from "~/features/associations/associations import { requireUser } from "~/features/auth/core/user.server"; import { actionError, parseRequestPayload } from "~/utils/remix.server"; import { associationsPage } from "~/utils/urls"; -import { isAtLeastFiveDollarTierPatreon } from "~/utils/users"; import * as AssociationRepository from "../AssociationRepository.server"; export const action = async ({ request }: ActionFunctionArgs) => { @@ -17,7 +16,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { const associationCount = ( await AssociationRepository.findByMemberUserId(user.id) ).actual; - const maxAssociationCount = isAtLeastFiveDollarTierPatreon(user) + const maxAssociationCount = user.roles.includes("SUPPORTER") ? ASSOCIATION.MAX_COUNT_SUPPORTER : ASSOCIATION.MAX_COUNT_REGULAR_USER; diff --git a/app/features/associations/actions/associations.server.ts b/app/features/associations/actions/associations.server.ts index e69432020..15d5827db 100644 --- a/app/features/associations/actions/associations.server.ts +++ b/app/features/associations/actions/associations.server.ts @@ -10,7 +10,6 @@ import { successToast, } from "~/utils/remix.server"; import { assertUnreachable } from "~/utils/types"; -import { isAtLeastFiveDollarTierPatreon } from "~/utils/users"; import * as AssociationRepository from "../AssociationRepository.server"; export const action = async ({ request }: ActionFunctionArgs) => { @@ -75,7 +74,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { "Association is full", ); - const maxAssociationCount = isAtLeastFiveDollarTierPatreon(user) + const maxAssociationCount = user.roles.includes("SUPPORTER") ? ASSOCIATION.MAX_COUNT_SUPPORTER : ASSOCIATION.MAX_COUNT_REGULAR_USER; diff --git a/app/features/auth/core/routes.server.ts b/app/features/auth/core/routes.server.ts index 91303ac5d..51fe7d38c 100644 --- a/app/features/auth/core/routes.server.ts +++ b/app/features/auth/core/routes.server.ts @@ -3,9 +3,13 @@ import { redirect } from "@remix-run/node"; import { isbot } from "isbot"; import { z } from "zod"; import * as UserRepository from "~/features/user-page/UserRepository.server"; -import { canAccessLohiEndpoint, canPerformAdminActions } from "~/permissions"; +import { requireRole } from "~/modules/permissions/guards.server"; import { logger } from "~/utils/logger"; -import { errorToastIfFalsy, parseSearchParams } from "~/utils/remix.server"; +import { + canAccessLohiEndpoint, + errorToastIfFalsy, + parseSearchParams, +} from "~/utils/remix.server"; import { ADMIN_PAGE, authErrorUrl } from "~/utils/urls"; import { createLogInLink } from "../queries/createLogInLink.server"; import { deleteLogInLinkByCode } from "../queries/deleteLogInLinkByCode.server"; @@ -16,7 +20,7 @@ import { authenticator, } from "./authenticator.server"; import { authSessionStorage } from "./session.server"; -import { getUserId } from "./user.server"; +import { getUserId, requireUser } from "./user.server"; export const callbackLoader: LoaderFunction = async ({ request }) => { const url = new URL(request.url); @@ -70,9 +74,9 @@ export const logInAction: ActionFunction = async ({ request }) => { }; export const impersonateAction: ActionFunction = async ({ request }) => { - const user = await getUserId(request); - if (!canPerformAdminActions(user)) { - throw new Response(null, { status: 403 }); + if (process.env.NODE_ENV === "production") { + const user = await requireUser(request); + requireRole(user, "ADMIN"); } const session = await authSessionStorage.getSession( diff --git a/app/features/badges/BadgeRepository.server.ts b/app/features/badges/BadgeRepository.server.ts index 0a352e23f..d648b9ceb 100644 --- a/app/features/badges/BadgeRepository.server.ts +++ b/app/features/badges/BadgeRepository.server.ts @@ -1,7 +1,53 @@ +import type { ExpressionBuilder } from "kysely"; import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite"; import { db } from "~/db/sql"; +import type { DB } from "~/db/tables"; import { COMMON_USER_FIELDS } from "~/utils/kysely.server"; -import type { Unwrapped } from "~/utils/types"; + +const addPermissions = ( + row: T, +) => ({ + ...row, + permissions: { + MANAGE: row.managers.map((m) => m.userId), + }, +}); + +const withAuthor = (eb: ExpressionBuilder) => { + return jsonObjectFrom( + eb + .selectFrom("User") + .select(COMMON_USER_FIELDS) + .whereRef("User.id", "=", "Badge.authorId"), + ).as("author"); +}; + +const withManagers = (eb: ExpressionBuilder) => { + return jsonArrayFrom( + eb + .selectFrom("BadgeManager") + .innerJoin("User", "BadgeManager.userId", "User.id") + .select(["userId", ...COMMON_USER_FIELDS]) + .whereRef("BadgeManager.badgeId", "=", "Badge.id"), + ).as("managers"); +}; + +const withOwners = (eb: ExpressionBuilder) => { + return jsonArrayFrom( + eb + .selectFrom("BadgeOwner") + .innerJoin("User", "BadgeOwner.userId", "User.id") + .select(({ fn }) => [ + fn.count("BadgeOwner.badgeId").as("count"), + "User.id", + "User.discordId", + "User.username", + ]) + .whereRef("BadgeOwner.badgeId", "=", "Badge.id") + .groupBy("User.id") + .orderBy("count", "desc"), + ).as("owners"); +}; export async function all() { const rows = await db @@ -11,33 +57,34 @@ export async function all() { "displayName", "code", "hue", - jsonArrayFrom( - eb - .selectFrom("BadgeManager") - .whereRef("BadgeManager.badgeId", "=", "Badge.id") - .select(["userId"]), - ).as("managers"), - jsonObjectFrom( - eb - .selectFrom("User") - .select(COMMON_USER_FIELDS) - .whereRef("User.id", "=", "Badge.authorId"), - ).as("author"), + withManagers(eb), + withAuthor(eb), ]) .execute(); - return rows.map((row) => ({ - ...row, - managers: row.managers.map((m) => m.userId), - })); + return rows.map(addPermissions); } export async function findById(badgeId: number) { - return db + const row = await db .selectFrom("Badge") - .select(["Badge.displayName"]) + .select((eb) => [ + "Badge.id", + "Badge.displayName", + "Badge.code", + "Badge.hue", + withAuthor(eb), + withManagers(eb), + withOwners(eb), + ]) .where("id", "=", badgeId) .executeTakeFirst(); + + if (!row) { + return null; + } + + return addPermissions(row); } export async function findByOwnerId({ @@ -97,32 +144,6 @@ export function findManagedByUserId(userId: number) { .execute(); } -export function findManagersByBadgeId(badgeId: number) { - return db - .selectFrom("BadgeManager") - .innerJoin("User", "BadgeManager.userId", "User.id") - .select(COMMON_USER_FIELDS) - .where("BadgeManager.badgeId", "=", badgeId) - .execute(); -} - -export type FindOwnersByBadgeIdItem = Unwrapped; -export function findOwnersByBadgeId(badgeId: number) { - return db - .selectFrom("BadgeOwner") - .innerJoin("User", "BadgeOwner.userId", "User.id") - .select(({ fn }) => [ - fn.count("BadgeOwner.badgeId").as("count"), - "User.id", - "User.discordId", - "User.username", - ]) - .where("BadgeOwner.badgeId", "=", badgeId) - .groupBy("User.id") - .orderBy("count", "desc") - .execute(); -} - export function replaceManagers({ badgeId, managerIds, diff --git a/app/features/badges/actions/badges.$id.edit.server.ts b/app/features/badges/actions/badges.$id.edit.server.ts index b9afd1a01..52bc9f56c 100644 --- a/app/features/badges/actions/badges.$id.edit.server.ts +++ b/app/features/badges/actions/badges.$id.edit.server.ts @@ -1,15 +1,14 @@ import type { ActionFunction } from "@remix-run/node"; import { redirect } from "@remix-run/node"; import { z } from "zod"; -import { requireUserId } from "~/features/auth/core/user.server"; +import { requireUser } from "~/features/auth/core/user.server"; import { notify } from "~/features/notifications/core/notify.server"; -import { canEditBadgeManagers, canEditBadgeOwners } from "~/permissions"; -import { diff } from "~/utils/arrays"; import { - errorToastIfFalsy, - notFoundIfFalsy, - parseRequestPayload, -} from "~/utils/remix.server"; + requirePermission, + requireRole, +} from "~/modules/permissions/guards.server"; +import { diff } from "~/utils/arrays"; +import { notFoundIfFalsy, parseRequestPayload } from "~/utils/remix.server"; import { assertUnreachable } from "~/utils/types"; import { badgePage } from "~/utils/urls"; import { actualNumber } from "~/utils/zod"; @@ -22,18 +21,15 @@ export const action: ActionFunction = async ({ request, params }) => { schema: editBadgeActionSchema, }); const badgeId = z.preprocess(actualNumber, z.number()).parse(params.id); - const user = await requireUserId(request); + const user = await requireUser(request); const badge = notFoundIfFalsy(await BadgeRepository.findById(badgeId)); switch (data._action) { case "MANAGERS": { - errorToastIfFalsy( - canEditBadgeManagers(user), - "No permissions to edit managers", - ); + requireRole(user, "STAFF"); - const oldManagers = await BadgeRepository.findManagersByBadgeId(badgeId); + const oldManagers = badge.managers; await BadgeRepository.replaceManagers({ badgeId, @@ -42,7 +38,7 @@ export const action: ActionFunction = async ({ request, params }) => { const newManagers = data.managerIds.filter( (newManagerId) => - !oldManagers.some((oldManager) => oldManager.id === newManagerId), + !oldManagers.some((oldManager) => oldManager.userId === newManagerId), ); notify({ @@ -58,17 +54,11 @@ export const action: ActionFunction = async ({ request, params }) => { break; } case "OWNERS": { - errorToastIfFalsy( - canEditBadgeOwners({ - user, - managers: await BadgeRepository.findManagersByBadgeId(badgeId), - }), - "No permissions to edit owners", - ); + requirePermission(badge, "MANAGE", user); - const oldOwners: number[] = ( - await BadgeRepository.findOwnersByBadgeId(badgeId) - ).flatMap((owner) => new Array(owner.count).fill(owner.id)); + const oldOwners: number[] = badge.owners.flatMap((owner) => + new Array(owner.count).fill(owner.id), + ); await BadgeRepository.replaceOwners({ badgeId, ownerIds: data.ownerIds }); diff --git a/app/features/badges/loaders/badges.$id.server.ts b/app/features/badges/loaders/badges.$id.server.ts index ed988e843..42cb0f941 100644 --- a/app/features/badges/loaders/badges.$id.server.ts +++ b/app/features/badges/loaders/badges.$id.server.ts @@ -1,15 +1,18 @@ -import type { LoaderFunctionArgs, SerializeFrom } from "@remix-run/node"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import type { SerializeFrom } from "~/utils/remix"; +import { notFoundIfFalsy, parseParams } from "~/utils/remix.server"; +import { idObject } from "~/utils/zod"; import * as BadgeRepository from "../BadgeRepository.server"; export type BadgeDetailsLoaderData = SerializeFrom; export const loader = async ({ params }: LoaderFunctionArgs) => { - const badgeId = Number(params.id); - if (Number.isNaN(badgeId)) { - throw new Response(null, { status: 404 }); - } + const { id } = parseParams({ + params, + schema: idObject, + }); + const badge = notFoundIfFalsy(await BadgeRepository.findById(id)); return { - owners: await BadgeRepository.findOwnersByBadgeId(badgeId), - managers: await BadgeRepository.findManagersByBadgeId(badgeId), + badge, }; }; diff --git a/app/features/badges/routes/badges.$id.edit.tsx b/app/features/badges/routes/badges.$id.edit.tsx index 1735693ba..5ef0d96a5 100644 --- a/app/features/badges/routes/badges.$id.edit.tsx +++ b/app/features/badges/routes/badges.$id.edit.tsx @@ -6,10 +6,8 @@ import { Label } from "~/components/Label"; import { UserSearch } from "~/components/UserSearch"; import { TrashIcon } from "~/components/icons/Trash"; import type { Tables } from "~/db/tables"; -import { useUser } from "~/features/auth/core/user"; -import { canEditBadgeManagers, canEditBadgeOwners } from "~/permissions"; +import { useHasPermission, useHasRole } from "~/modules/permissions/hooks"; import { atOrError } from "~/utils/arrays"; -import type * as BadgeRepository from "../BadgeRepository.server"; import type { BadgeDetailsLoaderData } from "../loaders/badges.$id.server"; import type { BadgeDetailsContext } from "./badges.$id"; @@ -17,17 +15,18 @@ import { action } from "../actions/badges.$id.edit.server"; export { action }; export default function EditBadgePage() { - const user = useUser(); + const isStaff = useHasRole("STAFF"); const matches = useMatches(); const data = atOrError(matches, -2).data as BadgeDetailsLoaderData; - const { badgeName } = useOutletContext(); + const { badge } = useOutletContext(); + const canManageBadge = useHasPermission(badge, "MANAGE"); return (

- Editing winners of {badgeName} + Editing winners of {badge.displayName}

- {canEditBadgeManagers(user) ? : null} - {canEditBadgeOwners({ user, managers: data.managers }) ? ( - - ) : null} + {isStaff ? : null} + {canManageBadge ? : null}
); } function Managers({ data }: { data: BadgeDetailsLoaderData }) { - const [managers, setManagers] = React.useState(data.managers); + const [managers, setManagers] = React.useState< + Array<{ id: number; username: string }> + >(data.badge.managers); const amountOfChanges = managers - .filter((m) => !data.managers.some((om) => om.id === m.id)) + .filter((m) => !data.badge.managers.some((om) => om.id === m.id)) // maps to id to keep typescript happy .map((m) => m.id) // needed so we can also list amount of removed managers .concat( - data.managers + data.badge.managers .filter((om) => !managers.some((m) => m.id === om.id)) .map((m) => m.id), ).length; const userIdsToOmitFromCombobox = React.useMemo(() => { - return new Set(data.managers.map((m) => m.id)); + return new Set(data.badge.managers.map((m) => m.id)); }, [data]); return ( @@ -130,9 +129,9 @@ function Managers({ data }: { data: BadgeDetailsLoaderData }) { } function Owners({ data }: { data: BadgeDetailsLoaderData }) { - const [owners, setOwners] = React.useState(data.owners); + const [owners, setOwners] = React.useState(data.badge.owners); - const ownerDifferences = getOwnerDifferences(owners, data.owners); + const ownerDifferences = getOwnerDifferences(owners, data.badge.owners); const userInputKey = owners.map((o) => `${o.id}-${o.count}`).join("-"); @@ -230,8 +229,8 @@ function Owners({ data }: { data: BadgeDetailsLoaderData }) { } function getOwnerDifferences( - newOwners: BadgeRepository.FindOwnersByBadgeIdItem[], - oldOwners: BadgeRepository.FindOwnersByBadgeIdItem[], + newOwners: BadgeDetailsLoaderData["badge"]["owners"], + oldOwners: BadgeDetailsLoaderData["badge"]["owners"], ) { const result: Array<{ id: Tables["User"]["id"]; diff --git a/app/features/badges/routes/badges.$id.tsx b/app/features/badges/routes/badges.$id.tsx index f4855d40a..b260e06a0 100644 --- a/app/features/badges/routes/badges.$id.tsx +++ b/app/features/badges/routes/badges.$id.tsx @@ -1,44 +1,37 @@ -import { Outlet, useLoaderData, useMatches, useParams } from "@remix-run/react"; +import { Outlet, useLoaderData } from "@remix-run/react"; import clsx from "clsx"; import { useTranslation } from "react-i18next"; import { Badge } from "~/components/Badge"; import { LinkButton } from "~/components/Button"; -import { Redirect } from "~/components/Redirect"; -import { useUser } from "~/features/auth/core/user"; -import { canEditBadgeOwners, isMod } from "~/permissions"; -import { BADGES_PAGE } from "~/utils/urls"; +import { useHasPermission, useHasRole } from "~/modules/permissions/hooks"; +import type { SerializeFrom } from "~/utils/remix"; import { badgeExplanationText } from "../badges-utils"; -import type { BadgesLoaderData } from "../loaders/badges.server"; import { loader } from "../loaders/badges.$id.server"; export { loader }; export interface BadgeDetailsContext { - badgeName: string; + badge: SerializeFrom["badge"]; } export default function BadgeDetailsPage() { - const user = useUser(); - const [, parentRoute] = useMatches(); - const { badges } = parentRoute.data as BadgesLoaderData; - const params = useParams(); + const isStaff = useHasRole("STAFF"); const data = useLoaderData(); const { t } = useTranslation("badges"); - const badge = badges.find((b) => b.id === Number(params.id)); - if (!badge) return ; + const canManageBadge = useHasPermission(data.badge, "MANAGE"); - const context: BadgeDetailsContext = { badgeName: badge.displayName }; + const context: BadgeDetailsContext = { badge: data.badge }; const badgeMaker = () => { - if (badge.author?.username) return badge.author?.username; + if (data.badge.author?.username) return data.badge.author?.username; if ( [ "XP3500 (Splatoon 3)", "XP4000 (Splatoon 3)", "XP4500 (Splatoon 3)", "XP5000 (Splatoon 3)", - ].includes(badge.displayName) + ].includes(data.badge.displayName) ) { return "Dreamy"; } @@ -49,14 +42,15 @@ export default function BadgeDetailsPage() { return (
- +
- {badgeExplanationText(t, badge)} + {badgeExplanationText(t, data.badge)}
{t("managedBy", { - users: data.managers.map((m) => m.username).join(", ") || "???", + users: + data.badge.managers.map((m) => m.username).join(", ") || "???", })}{" "} ( {t("madeBy", { @@ -65,14 +59,14 @@ export default function BadgeDetailsPage() { )
- {isMod(user) || canEditBadgeOwners({ user, managers: data.managers }) ? ( + {isStaff || canManageBadge ? ( Edit ) : null}
    - {data.owners.map((owner) => ( + {data.badge.owners.map((owner) => (
  • { const user = await requireUserId(request); diff --git a/app/features/calendar/actions/calendar.$id.server.ts b/app/features/calendar/actions/calendar.$id.server.ts index 993837012..4228c5026 100644 --- a/app/features/calendar/actions/calendar.$id.server.ts +++ b/app/features/calendar/actions/calendar.$id.server.ts @@ -8,11 +8,11 @@ import { clearTournamentDataCache, tournamentManagerData, } from "~/features/tournament-bracket/core/Tournament.server"; -import { canDeleteCalendarEvent } from "~/permissions"; import { databaseTimestampToDate } from "~/utils/dates"; import { errorToastIfFalsy, notFoundIfFalsy } from "~/utils/remix.server"; import { CALENDAR_PAGE } from "~/utils/urls"; import { actualNumber, id } from "~/utils/zod"; +import { canDeleteCalendarEvent } from "../calendar-utils"; export const action: ActionFunction = async ({ params, request }) => { const user = await requireUserId(request); diff --git a/app/features/calendar/actions/calendar.new.server.ts b/app/features/calendar/actions/calendar.new.server.ts index 340972c54..15fee59a3 100644 --- a/app/features/calendar/actions/calendar.new.server.ts +++ b/app/features/calendar/actions/calendar.new.server.ts @@ -17,7 +17,7 @@ import { } from "~/features/tournament-bracket/core/Tournament.server"; import { TOURNAMENT } from "~/features/tournament/tournament-constants"; import { rankedModesShort } from "~/modules/in-game-lists/modes"; -import { canEditCalendarEvent } from "~/permissions"; +import { requireRole } from "~/modules/permissions/guards.server"; import { databaseTimestampToDate, dateToDatabaseTimestamp, @@ -41,10 +41,10 @@ import { toArray, } from "~/utils/zod"; import { CALENDAR_EVENT, REG_CLOSES_AT_OPTIONS } from "../calendar-constants"; +import { canEditCalendarEvent } from "../calendar-utils"; import { calendarEventMaxDate, calendarEventMinDate, - canAddNewEvent, regClosesAtDate, } from "../calendar-utils"; @@ -61,7 +61,7 @@ export const action: ActionFunction = async ({ request }) => { parseAsync: true, }); - errorToastIfFalsy(canAddNewEvent(user), "Not authorized"); + requireRole(user, "CALENDAR_EVENT_ADDER"); const startTimes = data.date.map((date) => dateToDatabaseTimestamp(date)); const commonArgs = { diff --git a/app/features/calendar/calendar-utils.ts b/app/features/calendar/calendar-utils.ts index 88224c4ab..8488cf8ed 100644 --- a/app/features/calendar/calendar-utils.ts +++ b/app/features/calendar/calendar-utils.ts @@ -1,11 +1,11 @@ +import type { Tables } from "~/db/tables"; +import { isAdmin } from "~/modules/permissions/utils"; +import { allTruthy } from "~/utils/arrays"; +import { databaseTimestampToDate } from "~/utils/dates"; import { logger } from "~/utils/logger"; import { assertUnreachable } from "~/utils/types"; -import { userDiscordIdIsAged } from "~/utils/users"; import type { RegClosesAtOption } from "./calendar-constants"; -export const canAddNewEvent = (user: { discordId: string }) => - userDiscordIdIsAged(user); - export const calendarEventMinDate = () => new Date(Date.UTC(2015, 4, 28)); export const calendarEventMaxDate = () => { const result = new Date(); @@ -144,3 +144,51 @@ export function closeByWeeks(args: { week: number; year: number }) { }; }); } + +interface CanEditCalendarEventArgs { + user?: Pick; + event: Pick; +} +export function canEditCalendarEvent({ + user, + event, +}: CanEditCalendarEventArgs) { + if (isAdmin(user)) return true; + + return user?.id === event.authorId; +} + +export function canDeleteCalendarEvent({ + user, + event, + startTime, +}: CanEditCalendarEventArgs & { startTime: Date }) { + if (isAdmin(user)) return true; + + return user?.id === event.authorId && startTime > new Date(); +} + +interface CanReportCalendarEventWinnersArgs { + user?: Pick; + event: Pick; + startTimes: number[]; +} +export function canReportCalendarEventWinners({ + user, + event, + startTimes, +}: CanReportCalendarEventWinnersArgs) { + return allTruthy([ + canEditCalendarEvent({ user, event }), + eventStartedInThePast(startTimes), + ]); +} + +function eventStartedInThePast( + startTimes: CanReportCalendarEventWinnersArgs["startTimes"], +) { + return startTimes.every( + (startTime) => + databaseTimestampToDate(startTime).getTime() < new Date().getTime(), + ); +} diff --git a/app/features/calendar/loaders/calendar.$id.report-winners.server.ts b/app/features/calendar/loaders/calendar.$id.report-winners.server.ts index 4c394d628..89cb9f83b 100644 --- a/app/features/calendar/loaders/calendar.$id.report-winners.server.ts +++ b/app/features/calendar/loaders/calendar.$id.report-winners.server.ts @@ -1,9 +1,9 @@ import type { LoaderFunctionArgs } from "@remix-run/node"; import { requireUserId } from "~/features/auth/core/user.server"; import * as CalendarRepository from "~/features/calendar/CalendarRepository.server"; -import { canReportCalendarEventWinners } from "~/permissions"; import { notFoundIfFalsy, unauthorizedIfFalsy } from "~/utils/remix.server"; import { reportWinnersParamsSchema } from "../calendar-schemas"; +import { canReportCalendarEventWinners } from "../calendar-utils"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const parsedParams = reportWinnersParamsSchema.parse(params); diff --git a/app/features/calendar/loaders/calendar.new.server.ts b/app/features/calendar/loaders/calendar.new.server.ts index dd9fdf63f..907c10553 100644 --- a/app/features/calendar/loaders/calendar.new.server.ts +++ b/app/features/calendar/loaders/calendar.new.server.ts @@ -5,16 +5,15 @@ import * as BadgeRepository from "~/features/badges/BadgeRepository.server"; import * as CalendarRepository from "~/features/calendar/CalendarRepository.server"; import { tournamentData } from "~/features/tournament-bracket/core/Tournament.server"; import * as TournamentOrganizationRepository from "~/features/tournament-organization/TournamentOrganizationRepository.server"; -import { canEditCalendarEvent } from "~/permissions"; -import { unauthorizedIfFalsy } from "~/utils/remix.server"; +import { requireRole } from "~/modules/permissions/guards.server"; import { tournamentBracketsPage } from "~/utils/urls"; -import { canAddNewEvent } from "../calendar-utils"; +import { canEditCalendarEvent } from "../calendar-utils"; export const loader = async ({ request }: LoaderFunctionArgs) => { const user = await requireUser(request); - const url = new URL(request.url); + requireRole(user, "CALENDAR_EVENT_ADDER"); - unauthorizedIfFalsy(canAddNewEvent(user)); + const url = new URL(request.url); const eventWithTournament = async (key: string) => { const eventId = Number(url.searchParams.get(key)); diff --git a/app/features/calendar/routes/calendar.$id.tsx b/app/features/calendar/routes/calendar.$id.tsx index 7ade818f7..b93390614 100644 --- a/app/features/calendar/routes/calendar.$id.tsx +++ b/app/features/calendar/routes/calendar.$id.tsx @@ -16,11 +16,6 @@ import { Table } from "~/components/Table"; import { useUser } from "~/features/auth/core/user"; import { MapPool } from "~/features/map-list-generator/core/map-pool"; import { useIsMounted } from "~/hooks/useIsMounted"; -import { - canDeleteCalendarEvent, - canEditCalendarEvent, - canReportCalendarEventWinners, -} from "~/permissions"; import { databaseTimestampToDate } from "~/utils/dates"; import type { SendouRouteHandle } from "~/utils/remix.server"; import { @@ -34,6 +29,11 @@ import { userPage, } from "~/utils/urls"; import { metaTags } from "../../../utils/remix"; +import { + canDeleteCalendarEvent, + canEditCalendarEvent, + canReportCalendarEventWinners, +} from "../calendar-utils"; import { Tags } from "../components/Tags"; import { action } from "../actions/calendar.$id.server"; diff --git a/app/features/calendar/routes/calendar.new.tsx b/app/features/calendar/routes/calendar.new.tsx index e7f082cba..ada25ea26 100644 --- a/app/features/calendar/routes/calendar.new.tsx +++ b/app/features/calendar/routes/calendar.new.tsx @@ -20,7 +20,6 @@ import { SubmitButton } from "~/components/SubmitButton"; import { CrossIcon } from "~/components/icons/Cross"; import { TrashIcon } from "~/components/icons/Trash"; import type { CalendarEventTag, Tables } from "~/db/tables"; -import { useUser } from "~/features/auth/core/user"; import { MapPool } from "~/features/map-list-generator/core/map-pool"; import * as Progression from "~/features/tournament-bracket/core/Progression"; import { useIsMounted } from "~/hooks/useIsMounted"; @@ -46,11 +45,11 @@ import { datesToRegClosesAt, regClosesAtToDisplayName, } from "../calendar-utils"; -import { canAddNewEvent } from "../calendar-utils"; import { BracketProgressionSelector } from "../components/BracketProgressionSelector"; import { Tags } from "../components/Tags"; import "~/styles/calendar-new.css"; import { SendouSwitch } from "~/components/elements/Switch"; +import { useHasRole } from "~/modules/permissions/hooks"; import { metaTags } from "~/utils/remix"; import { action } from "../actions/calendar.new.server"; @@ -80,10 +79,11 @@ const useBaseEvent = () => { export default function CalendarNewEventPage() { const baseEvent = useBaseEvent(); - const user = useUser(); + const isCalendarEventAdder = useHasRole("CALENDAR_EVENT_ADDER"); + const isTournamentAdder = useHasRole("TOURNAMENT_ADDER"); const data = useLoaderData(); - if (!user || !canAddNewEvent(user)) { + if (!isCalendarEventAdder) { return (
    @@ -93,7 +93,7 @@ export default function CalendarNewEventPage() { ); } - if (data.isAddingTournament && !user.isTournamentOrganizer) { + if (data.isAddingTournament && !isTournamentAdder) { return (
    diff --git a/app/features/img-upload/actions/upload.admin.server.ts b/app/features/img-upload/actions/upload.admin.server.ts index 78174c11c..d7a9c3f43 100644 --- a/app/features/img-upload/actions/upload.admin.server.ts +++ b/app/features/img-upload/actions/upload.admin.server.ts @@ -1,10 +1,9 @@ import type { ActionFunction } from "@remix-run/node"; -import { requireUserId } from "~/features/auth/core/user.server"; +import { requireUser } from "~/features/auth/core/user.server"; import { clearTournamentDataCache } from "~/features/tournament-bracket/core/Tournament.server"; -import { isMod } from "~/permissions"; +import { requireRole } from "~/modules/permissions/guards.server"; import { badRequestIfFalsy, - errorToastIfFalsy, parseRequestPayload, successToast, } from "~/utils/remix.server"; @@ -14,14 +13,14 @@ import { validateImage } from "../queries/validateImage"; import { validateImageSchema } from "../upload-schemas.server"; export const action: ActionFunction = async ({ request }) => { - const user = await requireUserId(request); + const user = await requireUser(request); + requireRole(user, "STAFF"); + const data = await parseRequestPayload({ schema: validateImageSchema, request, }); - errorToastIfFalsy(isMod(user), "Only admins can validate images"); - switch (data._action) { case "VALIDATE": { for (const imageId of data.imageIds) { diff --git a/app/features/img-upload/loaders/upload.admin.server.ts b/app/features/img-upload/loaders/upload.admin.server.ts index 075acebb1..5feec6189 100644 --- a/app/features/img-upload/loaders/upload.admin.server.ts +++ b/app/features/img-upload/loaders/upload.admin.server.ts @@ -1,14 +1,12 @@ import type { LoaderFunctionArgs } from "@remix-run/node"; -import { requireUserId } from "~/features/auth/core/user.server"; -import { isMod } from "~/permissions"; -import { notFoundIfFalsy } from "~/utils/remix.server"; +import { requireUser } from "~/features/auth/core/user.server"; +import { requireRole } from "~/modules/permissions/guards.server"; import { countAllUnvalidatedImg } from "../queries/countAllUnvalidatedImg.server"; import { unvalidatedImages } from "../queries/unvalidatedImages"; export const loader = async ({ request }: LoaderFunctionArgs) => { - const user = await requireUserId(request); - - notFoundIfFalsy(isMod(user)); + const user = await requireUser(request); + requireRole(user, "STAFF"); return { images: unvalidatedImages(), diff --git a/app/features/lfg/actions/lfg.server.ts b/app/features/lfg/actions/lfg.server.ts index 5fd28abd2..5c9756410 100644 --- a/app/features/lfg/actions/lfg.server.ts +++ b/app/features/lfg/actions/lfg.server.ts @@ -1,7 +1,6 @@ import type { ActionFunctionArgs } from "@remix-run/node"; import { z } from "zod"; import { requireUser } from "~/features/auth/core/user.server"; -import { isAdmin } from "~/permissions"; import { errorToastIfFalsy, parseRequestPayload } from "~/utils/remix.server"; import { _action, id } from "~/utils/zod"; import * as LFGRepository from "../LFGRepository.server"; @@ -17,7 +16,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { const post = posts.find((post) => post.id === data.id); errorToastIfFalsy(post, "Post not found"); errorToastIfFalsy( - isAdmin(user) || post.author.id === user.id, + post.author.id === user.id || user.roles.includes("ADMIN"), "Not your own post", ); diff --git a/app/features/lfg/components/LFGPost.tsx b/app/features/lfg/components/LFGPost.tsx index f4fd3c5f2..6bc6bf234 100644 --- a/app/features/lfg/components/LFGPost.tsx +++ b/app/features/lfg/components/LFGPost.tsx @@ -15,7 +15,7 @@ import { useUser } from "~/features/auth/core/user"; import { currentOrPreviousSeason } from "~/features/mmr/season"; import type { TieredSkill } from "~/features/mmr/tiered.server"; import { useIsMounted } from "~/hooks/useIsMounted"; -import { isAdmin } from "~/permissions"; +import { useHasRole } from "~/modules/permissions/hooks"; import { databaseTimestampToDate } from "~/utils/dates"; import { lfgNewPostPage, @@ -49,6 +49,7 @@ export function LFGPost({ const USER_POST_EXPANDABLE_CRITERIA = 300; function UserLFGPost({ post, tiersMap }: { post: Post; tiersMap: TiersMap }) { const user = useUser(); + const isAdmin = useHasRole("ADMIN"); const [isExpanded, setIsExpanded] = React.useState(false); return ( @@ -75,7 +76,7 @@ function UserLFGPost({ post, tiersMap }: { post: Post; tiersMap: TiersMap }) {
    - {isAdmin(user) || post.author.id === user?.id ? ( + {post.author.id === user?.id || isAdmin ? ( ) : null}
    @@ -99,6 +100,7 @@ function TeamLFGPost({ }) { const isMounted = useIsMounted(); const user = useUser(); + const isAdmin = useHasRole("ADMIN"); const [isExpanded, setIsExpanded] = React.useState(false); return ( @@ -130,7 +132,7 @@ function TeamLFGPost({
    - {isAdmin(user) || post.author.id === user?.id ? ( + {post.author.id === user?.id || isAdmin ? ( ) : null}
    diff --git a/app/features/lfg/routes/lfg.new.tsx b/app/features/lfg/routes/lfg.new.tsx index 0ba6a47fc..3011efd90 100644 --- a/app/features/lfg/routes/lfg.new.tsx +++ b/app/features/lfg/routes/lfg.new.tsx @@ -10,6 +10,7 @@ import { SubmitButton } from "~/components/SubmitButton"; import { ArrowLeftIcon } from "~/components/icons/ArrowLeft"; import type { Tables } from "~/db/tables"; import { useUser } from "~/features/auth/core/user"; +import { useHasRole } from "~/modules/permissions/hooks"; import type { SendouRouteHandle } from "~/utils/remix.server"; import { LFG_PAGE, @@ -33,7 +34,7 @@ export const handle: SendouRouteHandle = { }; export default function LFGNewPostPage() { - const user = useUser(); + const isPlusServerMember = useHasRole("PLUS_SERVER_MEMBER"); const data = useLoaderData(); const fetcher = useFetcher(); const { t } = useTranslation(["common", "lfg"]); @@ -67,7 +68,7 @@ export default function LFGNewPostPage() { />