diff --git a/app/db/seed/index.ts b/app/db/seed/index.ts index 1c2c1a69aa..285edf669d 100644 --- a/app/db/seed/index.ts +++ b/app/db/seed/index.ts @@ -102,6 +102,7 @@ const basicSeeds = (variation?: SeedVariation | null) => [ adminUser, makeAdminPatron, makeAdminVideoAdder, + makeAdminTournamentOrganizer, nzapUser, users, fixAdminId, @@ -251,6 +252,12 @@ function makeAdminVideoAdder() { sql.prepare(`update "User" set "isVideoAdder" = 1 where id = 1`).run(); } +function makeAdminTournamentOrganizer() { + sql + .prepare(`update "User" set "isTournamentOrganizer" = 1 where id = 1`) + .run(); +} + function adminUserWeaponPool() { for (const [i, weaponSplId] of [200, 1100, 2000, 4000].entries()) { sql diff --git a/app/db/tables.ts b/app/db/tables.ts index ecbaf21cde..f073f4dc22 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -753,6 +753,7 @@ export interface User { inGameName: string | null; isArtist: Generated; isVideoAdder: Generated; + isTournamentOrganizer: Generated; languages: string | null; motionSens: number | null; patronSince: number | null; diff --git a/app/features/admin/AdminRepository.server.ts b/app/features/admin/AdminRepository.server.ts index e7182a271e..ca4da80913 100644 --- a/app/features/admin/AdminRepository.server.ts +++ b/app/features/admin/AdminRepository.server.ts @@ -76,6 +76,14 @@ export function makeVideoAdderByUserId(userId: number) { .execute(); } +export function makeTournamentOrganizerByUserId(userId: number) { + return db + .updateTable("User") + .set({ isTournamentOrganizer: 1 }) + .where("User.id", "=", userId) + .execute(); +} + export async function linkUserAndPlayer({ userId, playerId, diff --git a/app/features/admin/actions/admin.server.ts b/app/features/admin/actions/admin.server.ts index 58872c6522..d4a9586a6d 100644 --- a/app/features/admin/actions/admin.server.ts +++ b/app/features/admin/actions/admin.server.ts @@ -68,6 +68,12 @@ export const action = async ({ request }: ActionFunctionArgs) => { await AdminRepository.makeVideoAdderByUserId(data.user); break; } + case "TOURNAMENT_ORGANIZER": { + validate(isMod(user), "Mod needed", 401); + + await AdminRepository.makeTournamentOrganizerByUserId(data.user); + break; + } case "LINK_PLAYER": { validate(isMod(user), "Mod needed", 401); @@ -155,6 +161,10 @@ export const adminActionSchema = z.union([ _action: _action("VIDEO_ADDER"), user: z.preprocess(actualNumber, z.number().positive()), }), + z.object({ + _action: _action("TOURNAMENT_ORGANIZER"), + user: z.preprocess(actualNumber, z.number().positive()), + }), z.object({ _action: _action("ARTIST"), user: z.preprocess(actualNumber, z.number().positive()), diff --git a/app/features/admin/routes/admin.tsx b/app/features/admin/routes/admin.tsx index ea684d69ea..152fe9f5fc 100644 --- a/app/features/admin/routes/admin.tsx +++ b/app/features/admin/routes/admin.tsx @@ -41,6 +41,7 @@ export default function AdminPage() { {isMod(user) ? : null} {isMod(user) ? : null} {isMod(user) ? : null} + {isMod(user) ? : null} {isMod(user) ? : null} {process.env.NODE_ENV !== "production" || isAdmin(user) ? ( @@ -202,6 +203,31 @@ function GiveVideoAdder() { ); } +function GiveTournamentOrganizer() { + const fetcher = useFetcher(); + + return ( + +

Give tournament organizer

+
+
+ + +
+
+
+ + Add as tournament organizer + +
+
+ ); +} + function UpdateFriendCode() { const fetcher = useFetcher(); diff --git a/app/features/calendar/actions/calendar.new.server.ts b/app/features/calendar/actions/calendar.new.server.ts index 7799817b15..778ff924aa 100644 --- a/app/features/calendar/actions/calendar.new.server.ts +++ b/app/features/calendar/actions/calendar.new.server.ts @@ -45,10 +45,7 @@ import { canAddNewEvent, regClosesAtDate, } from "../calendar-utils"; -import { - canCreateTournament, - formValuesToBracketProgression, -} from "../calendar-utils.server"; +import { formValuesToBracketProgression } from "../calendar-utils.server"; export const action: ActionFunction = async ({ request }) => { const user = await requireUser(request); @@ -97,7 +94,9 @@ export const action: ActionFunction = async ({ request }) => { } : undefined, autoValidateAvatar: Boolean(user.patronTier), - toToolsEnabled: canCreateTournament(user) ? Number(data.toToolsEnabled) : 0, + toToolsEnabled: user.isTournamentOrganizer + ? Number(data.toToolsEnabled) + : 0, toToolsMode: rankedModesShort.find((mode) => mode === data.toToolsMode) ?? null, bracketProgression: formValuesToBracketProgression(data), diff --git a/app/features/calendar/calendar-utils.server.ts b/app/features/calendar/calendar-utils.server.ts index 79fd5fc598..67cfb457fd 100644 --- a/app/features/calendar/calendar-utils.server.ts +++ b/app/features/calendar/calendar-utils.server.ts @@ -1,18 +1,9 @@ import type { z } from "zod"; import type { TournamentSettings } from "~/db/tables"; -import { isAdmin } from "~/permissions"; import { BRACKET_NAMES } from "../tournament/tournament-constants"; import type { newCalendarEventActionSchema } from "./actions/calendar.new.server"; import { validateFollowUpBrackets } from "./calendar-utils"; -const usersWithTournamentPerms = - process.env.TOURNAMENT_PERMS?.split(",").map(Number) ?? []; -export function canCreateTournament(user?: { id: number }) { - if (!user) return false; - - return isAdmin(user) || usersWithTournamentPerms.includes(user.id); -} - export function formValuesToBracketProgression( args: z.infer, ) { diff --git a/app/features/calendar/loaders/calendar.new.server.ts b/app/features/calendar/loaders/calendar.new.server.ts index 905f5b4871..0752af46b8 100644 --- a/app/features/calendar/loaders/calendar.new.server.ts +++ b/app/features/calendar/loaders/calendar.new.server.ts @@ -11,7 +11,6 @@ import { validate } from "~/utils/remix"; import { makeTitle } from "~/utils/strings"; import { tournamentBracketsPage } from "~/utils/urls"; import { canAddNewEvent } from "../calendar-utils"; -import { canCreateTournament } from "../calendar-utils.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const t = await i18next.getFixedT(request); @@ -72,23 +71,20 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { ); } - const userCanCreateTournament = canCreateTournament(user); - return json({ managedBadges: await BadgeRepository.findManagedByUserId(user.id), recentEventsWithMapPools: await CalendarRepository.findRecentMapPoolsByAuthorId(user.id), eventToEdit: canEditEvent ? eventToEdit : undefined, eventToCopy: - userCanCreateTournament && !eventToEdit + user.isTournamentOrganizer && !eventToEdit ? await eventWithTournament("copyEventId") : undefined, recentTournaments: - userCanCreateTournament && !eventToEdit + user.isTournamentOrganizer && !eventToEdit ? await CalendarRepository.findRecentTournamentsByAuthorId(user.id) : undefined, title: makeTitle([canEditEvent ? "Edit" : "New", t("pages.calendar")]), - canCreateTournament: userCanCreateTournament, organizations: await TournamentOrganizationRepository.findByOrganizerUserId( user.id, ), diff --git a/app/features/calendar/routes/calendar.new.tsx b/app/features/calendar/routes/calendar.new.tsx index a1e5c83c6e..243a412bb1 100644 --- a/app/features/calendar/routes/calendar.new.tsx +++ b/app/features/calendar/routes/calendar.new.tsx @@ -23,6 +23,7 @@ import { CrossIcon } from "~/components/icons/Cross"; import { TrashIcon } from "~/components/icons/Trash"; import type { Tables } from "~/db/tables"; import type { Badge as BadgeType, CalendarEventTag } from "~/db/types"; +import { useUser } from "~/features/auth/core/user"; import { MapPool } from "~/features/map-list-generator/core/map-pool"; import { BRACKET_NAMES, @@ -133,7 +134,6 @@ function TemplateTournamentForm() { function EventForm() { const fetcher = useFetcher(); - const data = useLoaderData(); const { t } = useTranslation(); const { eventToEdit, eventToCopy } = useLoaderData(); const baseEvent = useBaseEvent(); @@ -142,6 +142,7 @@ function EventForm() { ); const ref = React.useRef(null); const [avatarImg, setAvatarImg] = React.useState(null); + const user = useUser(); const handleSubmit = () => { const formData = new FormData(ref.current!); @@ -179,12 +180,12 @@ function EventForm() { value={eventToCopy.tournamentId} /> ) : null} - {data.canCreateTournament && !eventToEdit && ( + {user?.isTournamentOrganizer && !eventToEdit ? ( - )} + ) : null} diff --git a/app/features/user-page/UserRepository.server.ts b/app/features/user-page/UserRepository.server.ts index 20967be406..69b9fda170 100644 --- a/app/features/user-page/UserRepository.server.ts +++ b/app/features/user-page/UserRepository.server.ts @@ -244,6 +244,7 @@ export function findLeanById(id: number) { ...COMMON_USER_FIELDS, "User.isArtist", "User.isVideoAdder", + "User.isTournamentOrganizer", "User.patronTier", "User.favoriteBadgeId", "User.languages", diff --git a/app/root.tsx b/app/root.tsx index 2d9d0721a9..aa85f81fe9 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -101,6 +101,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { patronTier: user.patronTier, isArtist: user.isArtist, isVideoAdder: user.isVideoAdder, + isTournamentOrganizer: user.isTournamentOrganizer, inGameName: user.inGameName, friendCode: user.friendCode, languages: user.languages ? user.languages.split(",") : [], diff --git a/migrations/067-tournament-organizer-perms.js b/migrations/067-tournament-organizer-perms.js new file mode 100644 index 0000000000..f3cfebbc77 --- /dev/null +++ b/migrations/067-tournament-organizer-perms.js @@ -0,0 +1,5 @@ +export function up(db) { + db.prepare( + `alter table "User" add column "isTournamentOrganizer" integer default 0`, + ).run(); +} diff --git a/scripts/add-tos.ts b/scripts/add-tos.ts new file mode 100644 index 0000000000..523c5b2e5d --- /dev/null +++ b/scripts/add-tos.ts @@ -0,0 +1,16 @@ +import "dotenv/config"; +import * as AdminRepository from "~/features/admin/AdminRepository.server"; +import { logger } from "~/utils/logger"; + +async function main() { + const input = process.argv[2]?.trim(); + + const userIds = input.split(",").map((id) => Number(id)); + + for (const userId of userIds) { + await AdminRepository.makeTournamentOrganizerByUserId(userId); + } + logger.info(`Added TOs: ${userIds}`); +} + +main();