Add TO permissions via UI

This commit is contained in:
Kalle 2024-08-25 11:29:35 +03:00
parent d753365d5d
commit ba12a61ba9
13 changed files with 85 additions and 23 deletions

View File

@ -102,6 +102,7 @@ const basicSeeds = (variation?: SeedVariation | null) => [
adminUser, adminUser,
makeAdminPatron, makeAdminPatron,
makeAdminVideoAdder, makeAdminVideoAdder,
makeAdminTournamentOrganizer,
nzapUser, nzapUser,
users, users,
fixAdminId, fixAdminId,
@ -251,6 +252,12 @@ function makeAdminVideoAdder() {
sql.prepare(`update "User" set "isVideoAdder" = 1 where id = 1`).run(); 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() { function adminUserWeaponPool() {
for (const [i, weaponSplId] of [200, 1100, 2000, 4000].entries()) { for (const [i, weaponSplId] of [200, 1100, 2000, 4000].entries()) {
sql sql

View File

@ -753,6 +753,7 @@ export interface User {
inGameName: string | null; inGameName: string | null;
isArtist: Generated<number | null>; isArtist: Generated<number | null>;
isVideoAdder: Generated<number | null>; isVideoAdder: Generated<number | null>;
isTournamentOrganizer: Generated<number | null>;
languages: string | null; languages: string | null;
motionSens: number | null; motionSens: number | null;
patronSince: number | null; patronSince: number | null;

View File

@ -76,6 +76,14 @@ export function makeVideoAdderByUserId(userId: number) {
.execute(); .execute();
} }
export function makeTournamentOrganizerByUserId(userId: number) {
return db
.updateTable("User")
.set({ isTournamentOrganizer: 1 })
.where("User.id", "=", userId)
.execute();
}
export async function linkUserAndPlayer({ export async function linkUserAndPlayer({
userId, userId,
playerId, playerId,

View File

@ -68,6 +68,12 @@ export const action = async ({ request }: ActionFunctionArgs) => {
await AdminRepository.makeVideoAdderByUserId(data.user); await AdminRepository.makeVideoAdderByUserId(data.user);
break; break;
} }
case "TOURNAMENT_ORGANIZER": {
validate(isMod(user), "Mod needed", 401);
await AdminRepository.makeTournamentOrganizerByUserId(data.user);
break;
}
case "LINK_PLAYER": { case "LINK_PLAYER": {
validate(isMod(user), "Mod needed", 401); validate(isMod(user), "Mod needed", 401);
@ -155,6 +161,10 @@ export const adminActionSchema = z.union([
_action: _action("VIDEO_ADDER"), _action: _action("VIDEO_ADDER"),
user: z.preprocess(actualNumber, z.number().positive()), user: z.preprocess(actualNumber, z.number().positive()),
}), }),
z.object({
_action: _action("TOURNAMENT_ORGANIZER"),
user: z.preprocess(actualNumber, z.number().positive()),
}),
z.object({ z.object({
_action: _action("ARTIST"), _action: _action("ARTIST"),
user: z.preprocess(actualNumber, z.number().positive()), user: z.preprocess(actualNumber, z.number().positive()),

View File

@ -41,6 +41,7 @@ export default function AdminPage() {
{isMod(user) ? <LinkPlayer /> : null} {isMod(user) ? <LinkPlayer /> : null}
{isMod(user) ? <GiveArtist /> : null} {isMod(user) ? <GiveArtist /> : null}
{isMod(user) ? <GiveVideoAdder /> : null} {isMod(user) ? <GiveVideoAdder /> : null}
{isMod(user) ? <GiveTournamentOrganizer /> : null}
{isMod(user) ? <UpdateFriendCode /> : null} {isMod(user) ? <UpdateFriendCode /> : null}
{process.env.NODE_ENV !== "production" || isAdmin(user) ? ( {process.env.NODE_ENV !== "production" || isAdmin(user) ? (
@ -202,6 +203,31 @@ function GiveVideoAdder() {
); );
} }
function GiveTournamentOrganizer() {
const fetcher = useFetcher();
return (
<fetcher.Form className="stack md" method="post">
<h2>Give tournament organizer</h2>
<div className="stack horizontal md">
<div>
<label>User</label>
<UserSearch inputName="user" />
</div>
</div>
<div className="stack horizontal md">
<SubmitButton
type="submit"
_action="TOURNAMENT_ORGANIZER"
state={fetcher.state}
>
Add as tournament organizer
</SubmitButton>
</div>
</fetcher.Form>
);
}
function UpdateFriendCode() { function UpdateFriendCode() {
const fetcher = useFetcher(); const fetcher = useFetcher();

View File

@ -45,10 +45,7 @@ import {
canAddNewEvent, canAddNewEvent,
regClosesAtDate, regClosesAtDate,
} from "../calendar-utils"; } from "../calendar-utils";
import { import { formValuesToBracketProgression } from "../calendar-utils.server";
canCreateTournament,
formValuesToBracketProgression,
} from "../calendar-utils.server";
export const action: ActionFunction = async ({ request }) => { export const action: ActionFunction = async ({ request }) => {
const user = await requireUser(request); const user = await requireUser(request);
@ -97,7 +94,9 @@ export const action: ActionFunction = async ({ request }) => {
} }
: undefined, : undefined,
autoValidateAvatar: Boolean(user.patronTier), autoValidateAvatar: Boolean(user.patronTier),
toToolsEnabled: canCreateTournament(user) ? Number(data.toToolsEnabled) : 0, toToolsEnabled: user.isTournamentOrganizer
? Number(data.toToolsEnabled)
: 0,
toToolsMode: toToolsMode:
rankedModesShort.find((mode) => mode === data.toToolsMode) ?? null, rankedModesShort.find((mode) => mode === data.toToolsMode) ?? null,
bracketProgression: formValuesToBracketProgression(data), bracketProgression: formValuesToBracketProgression(data),

View File

@ -1,18 +1,9 @@
import type { z } from "zod"; import type { z } from "zod";
import type { TournamentSettings } from "~/db/tables"; import type { TournamentSettings } from "~/db/tables";
import { isAdmin } from "~/permissions";
import { BRACKET_NAMES } from "../tournament/tournament-constants"; import { BRACKET_NAMES } from "../tournament/tournament-constants";
import type { newCalendarEventActionSchema } from "./actions/calendar.new.server"; import type { newCalendarEventActionSchema } from "./actions/calendar.new.server";
import { validateFollowUpBrackets } from "./calendar-utils"; 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( export function formValuesToBracketProgression(
args: z.infer<typeof newCalendarEventActionSchema>, args: z.infer<typeof newCalendarEventActionSchema>,
) { ) {

View File

@ -11,7 +11,6 @@ import { validate } from "~/utils/remix";
import { makeTitle } from "~/utils/strings"; import { makeTitle } from "~/utils/strings";
import { tournamentBracketsPage } from "~/utils/urls"; import { tournamentBracketsPage } from "~/utils/urls";
import { canAddNewEvent } from "../calendar-utils"; import { canAddNewEvent } from "../calendar-utils";
import { canCreateTournament } from "../calendar-utils.server";
export const loader = async ({ request }: LoaderFunctionArgs) => { export const loader = async ({ request }: LoaderFunctionArgs) => {
const t = await i18next.getFixedT(request); const t = await i18next.getFixedT(request);
@ -72,23 +71,20 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
); );
} }
const userCanCreateTournament = canCreateTournament(user);
return json({ return json({
managedBadges: await BadgeRepository.findManagedByUserId(user.id), managedBadges: await BadgeRepository.findManagedByUserId(user.id),
recentEventsWithMapPools: recentEventsWithMapPools:
await CalendarRepository.findRecentMapPoolsByAuthorId(user.id), await CalendarRepository.findRecentMapPoolsByAuthorId(user.id),
eventToEdit: canEditEvent ? eventToEdit : undefined, eventToEdit: canEditEvent ? eventToEdit : undefined,
eventToCopy: eventToCopy:
userCanCreateTournament && !eventToEdit user.isTournamentOrganizer && !eventToEdit
? await eventWithTournament("copyEventId") ? await eventWithTournament("copyEventId")
: undefined, : undefined,
recentTournaments: recentTournaments:
userCanCreateTournament && !eventToEdit user.isTournamentOrganizer && !eventToEdit
? await CalendarRepository.findRecentTournamentsByAuthorId(user.id) ? await CalendarRepository.findRecentTournamentsByAuthorId(user.id)
: undefined, : undefined,
title: makeTitle([canEditEvent ? "Edit" : "New", t("pages.calendar")]), title: makeTitle([canEditEvent ? "Edit" : "New", t("pages.calendar")]),
canCreateTournament: userCanCreateTournament,
organizations: await TournamentOrganizationRepository.findByOrganizerUserId( organizations: await TournamentOrganizationRepository.findByOrganizerUserId(
user.id, user.id,
), ),

View File

@ -23,6 +23,7 @@ import { CrossIcon } from "~/components/icons/Cross";
import { TrashIcon } from "~/components/icons/Trash"; import { TrashIcon } from "~/components/icons/Trash";
import type { Tables } from "~/db/tables"; import type { Tables } from "~/db/tables";
import type { Badge as BadgeType, CalendarEventTag } from "~/db/types"; 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 { MapPool } from "~/features/map-list-generator/core/map-pool";
import { import {
BRACKET_NAMES, BRACKET_NAMES,
@ -133,7 +134,6 @@ function TemplateTournamentForm() {
function EventForm() { function EventForm() {
const fetcher = useFetcher(); const fetcher = useFetcher();
const data = useLoaderData<typeof loader>();
const { t } = useTranslation(); const { t } = useTranslation();
const { eventToEdit, eventToCopy } = useLoaderData<typeof loader>(); const { eventToEdit, eventToCopy } = useLoaderData<typeof loader>();
const baseEvent = useBaseEvent(); const baseEvent = useBaseEvent();
@ -142,6 +142,7 @@ function EventForm() {
); );
const ref = React.useRef<HTMLFormElement>(null); const ref = React.useRef<HTMLFormElement>(null);
const [avatarImg, setAvatarImg] = React.useState<File | null>(null); const [avatarImg, setAvatarImg] = React.useState<File | null>(null);
const user = useUser();
const handleSubmit = () => { const handleSubmit = () => {
const formData = new FormData(ref.current!); const formData = new FormData(ref.current!);
@ -179,12 +180,12 @@ function EventForm() {
value={eventToCopy.tournamentId} value={eventToCopy.tournamentId}
/> />
) : null} ) : null}
{data.canCreateTournament && !eventToEdit && ( {user?.isTournamentOrganizer && !eventToEdit ? (
<TournamentEnabler <TournamentEnabler
checked={isTournament} checked={isTournament}
setChecked={setIsTournament} setChecked={setIsTournament}
/> />
)} ) : null}
<NameInput /> <NameInput />
<DescriptionTextarea supportsMarkdown={isTournament} /> <DescriptionTextarea supportsMarkdown={isTournament} />
<OrganizationSelect /> <OrganizationSelect />

View File

@ -244,6 +244,7 @@ export function findLeanById(id: number) {
...COMMON_USER_FIELDS, ...COMMON_USER_FIELDS,
"User.isArtist", "User.isArtist",
"User.isVideoAdder", "User.isVideoAdder",
"User.isTournamentOrganizer",
"User.patronTier", "User.patronTier",
"User.favoriteBadgeId", "User.favoriteBadgeId",
"User.languages", "User.languages",

View File

@ -101,6 +101,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
patronTier: user.patronTier, patronTier: user.patronTier,
isArtist: user.isArtist, isArtist: user.isArtist,
isVideoAdder: user.isVideoAdder, isVideoAdder: user.isVideoAdder,
isTournamentOrganizer: user.isTournamentOrganizer,
inGameName: user.inGameName, inGameName: user.inGameName,
friendCode: user.friendCode, friendCode: user.friendCode,
languages: user.languages ? user.languages.split(",") : [], languages: user.languages ? user.languages.split(",") : [],

View File

@ -0,0 +1,5 @@
export function up(db) {
db.prepare(
`alter table "User" add column "isTournamentOrganizer" integer default 0`,
).run();
}

16
scripts/add-tos.ts Normal file
View File

@ -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();