mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-24 23:19:39 -05:00
Tournament organizations use new permissions model & e2e test added
This commit is contained in:
parent
c70a9b82ab
commit
9afb5df064
|
|
@ -42,6 +42,7 @@ import * as QRepository from "~/features/sendouq/QRepository.server";
|
|||
import { addMember } from "~/features/sendouq/queries/addMember.server";
|
||||
import { createMatch } from "~/features/sendouq/queries/createMatch.server";
|
||||
import { clearAllTournamentDataCache } from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import * as TournamentOrganizationRepository from "~/features/tournament-organization/TournamentOrganizationRepository.server";
|
||||
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { createVod } from "~/features/vods/queries/createVod.server";
|
||||
|
|
@ -174,6 +175,7 @@ const basicSeeds = (variation?: SeedVariation | null) => [
|
|||
scrimPostRequests,
|
||||
associations,
|
||||
notifications,
|
||||
organization,
|
||||
];
|
||||
|
||||
export async function seed(variation?: SeedVariation | null) {
|
||||
|
|
@ -235,6 +237,7 @@ function wipeDB() {
|
|||
"PlusVote",
|
||||
"TournamentBadgeOwner",
|
||||
"BadgeManager",
|
||||
"TournamentOrganization",
|
||||
];
|
||||
|
||||
for (const table of tablesToDelete) {
|
||||
|
|
@ -2550,3 +2553,34 @@ async function notifications() {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function organization() {
|
||||
await TournamentOrganizationRepository.create({
|
||||
ownerId: ADMIN_ID,
|
||||
name: "sendou.ink",
|
||||
});
|
||||
|
||||
await TournamentOrganizationRepository.update({
|
||||
id: 1,
|
||||
name: "sendou.ink",
|
||||
description: "Sendou.ink official tournaments",
|
||||
socials: [
|
||||
"https://bsky.app/profile/sendou.ink",
|
||||
"https://twitch.tv/sendou",
|
||||
],
|
||||
members: [
|
||||
{
|
||||
userId: ADMIN_ID,
|
||||
role: "ADMIN",
|
||||
roleDisplayName: null,
|
||||
},
|
||||
{
|
||||
userId: NZAP_TEST_ID,
|
||||
role: "MEMBER",
|
||||
roleDisplayName: null,
|
||||
},
|
||||
],
|
||||
series: [],
|
||||
badges: [],
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,14 +10,13 @@ import { requireUser } from "~/features/auth/core/user.server";
|
|||
import * as TeamRepository from "~/features/team/TeamRepository.server";
|
||||
import { isTeamManager } from "~/features/team/team-utils";
|
||||
import * as TournamentOrganizationRepository from "~/features/tournament-organization/TournamentOrganizationRepository.server";
|
||||
import { canEditTournamentOrganization } from "~/features/tournament-organization/tournament-organization-utils";
|
||||
import { requirePermission } from "~/modules/permissions/guards.server";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import invariant from "~/utils/invariant";
|
||||
import {
|
||||
badRequestIfFalsy,
|
||||
errorToastIfFalsy,
|
||||
parseSearchParams,
|
||||
unauthorizedIfFalsy,
|
||||
} from "~/utils/remix.server";
|
||||
import { teamPage, tournamentOrganizationPage } from "~/utils/urls";
|
||||
import { addNewImage } from "../queries/addNewImage";
|
||||
|
|
@ -38,7 +37,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||
: undefined;
|
||||
const organization =
|
||||
validatedType === "org-pfp"
|
||||
? await validatedOrg({ user, request })
|
||||
? await requireEditableOrganization({ user, request })
|
||||
: undefined;
|
||||
|
||||
errorToastIfFalsy(
|
||||
|
|
@ -105,7 +104,7 @@ async function validatedTeam({
|
|||
return team;
|
||||
}
|
||||
|
||||
async function validatedOrg({
|
||||
async function requireEditableOrganization({
|
||||
user,
|
||||
request,
|
||||
}: { user: { id: number }; request: Request }) {
|
||||
|
|
@ -117,7 +116,7 @@ async function validatedOrg({
|
|||
await TournamentOrganizationRepository.findBySlug(slug),
|
||||
);
|
||||
|
||||
unauthorizedIfFalsy(canEditTournamentOrganization({ user, organization }));
|
||||
requirePermission(organization, "EDIT", user);
|
||||
|
||||
return organization;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ export function create(args: CreateArgs) {
|
|||
});
|
||||
}
|
||||
|
||||
export function findBySlug(slug: string) {
|
||||
return db
|
||||
export async function findBySlug(slug: string) {
|
||||
const organization = await db
|
||||
.selectFrom("TournamentOrganization")
|
||||
.leftJoin(
|
||||
"UserSubmittedImage",
|
||||
|
|
@ -95,6 +95,17 @@ export function findBySlug(slug: string) {
|
|||
])
|
||||
.where("TournamentOrganization.slug", "=", slug)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!organization) return null;
|
||||
|
||||
return {
|
||||
...organization,
|
||||
permissions: {
|
||||
EDIT: organization.members
|
||||
.filter((member) => member.role === "ADMIN")
|
||||
.map((member) => member.id),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function findByOrganizerUserId(userId: number) {
|
||||
|
|
|
|||
|
|
@ -2,16 +2,12 @@ import { type ActionFunctionArgs, redirect } from "@remix-run/node";
|
|||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server";
|
||||
import i18next from "~/modules/i18n/i18next.server";
|
||||
import { requirePermission } from "~/modules/permissions/guards.server";
|
||||
import { valueArrayToDBFormat } from "~/utils/form";
|
||||
import {
|
||||
actionError,
|
||||
parseRequestPayload,
|
||||
unauthorizedIfFalsy,
|
||||
} from "~/utils/remix.server";
|
||||
import { actionError, parseRequestPayload } from "~/utils/remix.server";
|
||||
import { tournamentOrganizationPage } from "~/utils/urls";
|
||||
import * as TournamentOrganizationRepository from "../TournamentOrganizationRepository.server";
|
||||
import { organizationEditSchema } from "../tournament-organization-schemas";
|
||||
import { canEditTournamentOrganization } from "../tournament-organization-utils";
|
||||
import { organizationFromParams } from "../tournament-organization-utils.server";
|
||||
|
||||
export const action = async ({ request, params }: ActionFunctionArgs) => {
|
||||
|
|
@ -24,7 +20,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
|||
|
||||
const organization = await organizationFromParams(params);
|
||||
|
||||
unauthorizedIfFalsy(canEditTournamentOrganization({ organization, user }));
|
||||
requirePermission(organization, "EDIT", user);
|
||||
|
||||
if (
|
||||
!data.members.some(
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import type { LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
import * as BadgeRepository from "~/features/badges/BadgeRepository.server";
|
||||
import { unauthorizedIfFalsy } from "~/utils/remix.server";
|
||||
import { canEditTournamentOrganization } from "../tournament-organization-utils";
|
||||
import { requirePermission } from "~/modules/permissions/guards.server";
|
||||
import { organizationFromParams } from "../tournament-organization-utils.server";
|
||||
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
const user = await requireUser(request);
|
||||
const organization = await organizationFromParams(params);
|
||||
|
||||
unauthorizedIfFalsy(canEditTournamentOrganization({ organization, user }));
|
||||
requirePermission(organization, "EDIT", user);
|
||||
|
||||
const badgeOptions = async () => {
|
||||
const result = await BadgeRepository.findByManagersList(
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ import { NewTabs } from "~/components/NewTabs";
|
|||
import { Pagination } from "~/components/Pagination";
|
||||
import { Placement } from "~/components/Placement";
|
||||
import { EditIcon } from "~/components/icons/Edit";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { BadgeDisplay } from "~/features/badges/components/BadgeDisplay";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useHasPermission } from "~/modules/permissions/hooks";
|
||||
import { databaseTimestampNow, databaseTimestampToDate } from "~/utils/dates";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
|
|
@ -27,7 +27,6 @@ import {
|
|||
import { EventCalendar } from "../components/EventCalendar";
|
||||
import { SocialLinksList } from "../components/SocialLinksList";
|
||||
import { TOURNAMENT_SERIES_EVENTS_PER_PAGE } from "../tournament-organization-constants";
|
||||
import { canEditTournamentOrganization } from "../tournament-organization-utils";
|
||||
|
||||
import { loader } from "../loaders/org.$slug.server";
|
||||
export { loader };
|
||||
|
|
@ -100,8 +99,8 @@ export default function TournamentOrganizationPage() {
|
|||
|
||||
function LogoHeader() {
|
||||
const { t } = useTranslation(["common"]);
|
||||
const user = useUser();
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const canEditOrganization = useHasPermission(data.organization, "EDIT");
|
||||
|
||||
return (
|
||||
<div className="stack horizontal md">
|
||||
|
|
@ -115,16 +114,14 @@ function LogoHeader() {
|
|||
/>
|
||||
<div className="stack sm">
|
||||
<div className="text-xl font-bold">{data.organization.name}</div>
|
||||
{canEditTournamentOrganization({
|
||||
user,
|
||||
organization: data.organization,
|
||||
}) ? (
|
||||
{canEditOrganization ? (
|
||||
<div className="stack items-start">
|
||||
<LinkButton
|
||||
to={tournamentOrganizationEditPage(data.organization.slug)}
|
||||
icon={<EditIcon />}
|
||||
size="tiny"
|
||||
variant="outlined"
|
||||
testId="edit-org-button"
|
||||
>
|
||||
{t("common:actions.edit")}
|
||||
</LinkButton>
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
import { isAdmin } from "~/modules/permissions/utils";
|
||||
import type { UnwrappedNonNullable } from "~/utils/types";
|
||||
import type * as TournamentOrganizationRepository from "./TournamentOrganizationRepository.server";
|
||||
|
||||
export function canEditTournamentOrganization({
|
||||
user,
|
||||
organization,
|
||||
}: {
|
||||
user?: { id: number };
|
||||
organization: Pick<
|
||||
UnwrappedNonNullable<typeof TournamentOrganizationRepository.findBySlug>,
|
||||
"members"
|
||||
>;
|
||||
}) {
|
||||
if (isAdmin(user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return organization.members.some(
|
||||
(member) => member.id === user?.id && member.role === "ADMIN",
|
||||
);
|
||||
}
|
||||
52
e2e/org.spec.ts
Normal file
52
e2e/org.spec.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import test, { expect } from "@playwright/test";
|
||||
import { NZAP_TEST_ID } from "~/db/seed/constants";
|
||||
import { ADMIN_ID } from "~/features/admin/admin-constants";
|
||||
import {
|
||||
impersonate,
|
||||
isNotVisible,
|
||||
navigate,
|
||||
seed,
|
||||
submit,
|
||||
} from "~/utils/playwright";
|
||||
import { tournamentOrganizationPage } from "~/utils/urls";
|
||||
|
||||
const url = tournamentOrganizationPage({
|
||||
organizationSlug: "sendouink",
|
||||
});
|
||||
|
||||
test.describe("Tournament Organization", () => {
|
||||
test("user can be promoted to admin gaining org controls", async ({
|
||||
page,
|
||||
}) => {
|
||||
await seed(page);
|
||||
|
||||
const editButtonLocator = page.getByTestId("edit-org-button");
|
||||
|
||||
// 1. As a regular user, verify edit controls are not visible
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await navigate({
|
||||
page,
|
||||
url,
|
||||
});
|
||||
await isNotVisible(editButtonLocator);
|
||||
|
||||
// 2. As admin, promote user to admin
|
||||
await impersonate(page, ADMIN_ID);
|
||||
await navigate({ page, url });
|
||||
await editButtonLocator.click();
|
||||
// Add member as admin
|
||||
await page.getByLabel("Role").first().selectOption("ADMIN");
|
||||
await submit(page);
|
||||
|
||||
// 3. As the promoted user, verify edit controls are visible and page can be accessed
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await navigate({
|
||||
page,
|
||||
url,
|
||||
});
|
||||
await editButtonLocator.click();
|
||||
await expect(
|
||||
page.getByText("Editing tournament organization"),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user