sendou.ink/app/features/img-upload/actions/upload.server.ts
Kalle 8f156fb917
Team editors in addition to the owner (#2077)
* Initial

* Handle owner leaving

* Remove old team queries

* Progress

* Retire old toggle

* e2e tests

* Divide loaders/actions of team pages
2025-02-04 10:56:33 +02:00

130 lines
3.9 KiB
TypeScript

import type { ActionFunctionArgs, UploadHandler } from "@remix-run/node";
import {
unstable_composeUploadHandlers as composeUploadHandlers,
unstable_createMemoryUploadHandler as createMemoryUploadHandler,
unstable_parseMultipartFormData as parseMultipartFormData,
redirect,
} from "@remix-run/node";
import { z } from "zod";
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 { dateToDatabaseTimestamp } from "~/utils/dates";
import invariant from "~/utils/invariant";
import {
badRequestIfFalsy,
parseSearchParams,
unauthorizedIfFalsy,
validate,
} from "~/utils/remix.server";
import { teamPage, tournamentOrganizationPage } from "~/utils/urls";
import { addNewImage } from "../queries/addNewImage";
import { countUnvalidatedImg } from "../queries/countUnvalidatedImg.server";
import { s3UploadHandler } from "../s3.server";
import { MAX_UNVALIDATED_IMG_COUNT } from "../upload-constants";
import { requestToImgType } from "../upload-utils";
export const action = async ({ request }: ActionFunctionArgs) => {
const user = await requireUser(request);
const validatedType = requestToImgType(request);
validate(validatedType, "Invalid image type");
const team =
validatedType === "team-pfp" || validatedType === "team-banner"
? await validatedTeam({ user, request })
: undefined;
const organization =
validatedType === "org-pfp"
? await validatedOrg({ user, request })
: undefined;
// TODO: graceful error handling when uploading many images
validate(
countUnvalidatedImg(user.id) < MAX_UNVALIDATED_IMG_COUNT,
"Too many unvalidated images",
);
const uploadHandler: UploadHandler = composeUploadHandlers(
s3UploadHandler(),
createMemoryUploadHandler(),
);
const formData = await parseMultipartFormData(request, uploadHandler);
const imgSrc = formData.get("img") as string | null;
invariant(imgSrc);
const urlParts = imgSrc.split("/");
const fileName = urlParts[urlParts.length - 1];
invariant(fileName);
const shouldAutoValidate =
Boolean(user.patronTier) || validatedType === "org-pfp";
addNewImage({
submitterUserId: user.id,
teamId: team?.id,
organizationId: organization?.id,
type: validatedType,
url: fileName,
validatedAt: shouldAutoValidate
? dateToDatabaseTimestamp(new Date())
: null,
});
if (shouldAutoValidate) {
if (team) {
throw redirect(teamPage(team?.customUrl));
}
if (organization) {
throw redirect(
tournamentOrganizationPage({ organizationSlug: organization.slug }),
);
}
}
return null;
};
async function validatedTeam({
user,
request,
}: { user: { id: number }; request: Request }) {
const { team: teamCustomUrl } = parseSearchParams({
request,
schema: z.object({ team: z.string() }),
});
const team = await TeamRepository.findByCustomUrl(teamCustomUrl);
validate(team, "You must be on a team to upload images");
validate(
team.members.some((member) => member.id === user.id && member.isOwner),
"You must be on the team to upload images",
);
const detailedTeam = await TeamRepository.findByCustomUrl(team.customUrl);
validate(
detailedTeam && isTeamManager({ team: detailedTeam, user }),
"You must be the team manager to upload images",
);
return team;
}
async function validatedOrg({
user,
request,
}: { user: { id: number }; request: Request }) {
const { slug } = parseSearchParams({
request,
schema: z.object({ slug: z.string() }),
});
const organization = badRequestIfFalsy(
await TournamentOrganizationRepository.findBySlug(slug),
);
unauthorizedIfFalsy(canEditTournamentOrganization({ user, organization }));
return organization;
}