mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
137 lines
3.7 KiB
TypeScript
137 lines
3.7 KiB
TypeScript
import type { FileUpload } from "@remix-run/form-data-parser";
|
|
import type { ActionFunctionArgs } from "react-router";
|
|
import { redirect } from "react-router";
|
|
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 { requirePermission } from "~/modules/permissions/guards.server";
|
|
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
|
import invariant from "~/utils/invariant";
|
|
import {
|
|
badRequestIfFalsy,
|
|
errorToastIfFalsy,
|
|
parseSearchParams,
|
|
safeParseMultipartFormData,
|
|
} from "~/utils/remix.server";
|
|
import { teamPage, tournamentOrganizationPage } from "~/utils/urls";
|
|
import * as ImageRepository from "../ImageRepository.server";
|
|
import { uploadStreamToS3 } from "../s3.server";
|
|
import { MAX_UNVALIDATED_IMG_COUNT } from "../upload-constants";
|
|
import { requestToImgType } from "../upload-utils";
|
|
|
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
|
const user = requireUser();
|
|
|
|
const validatedType = requestToImgType(request);
|
|
errorToastIfFalsy(validatedType, "Invalid image type");
|
|
|
|
const team =
|
|
validatedType === "team-pfp" || validatedType === "team-banner"
|
|
? await validatedTeam({ user, request })
|
|
: undefined;
|
|
const organization =
|
|
validatedType === "org-pfp"
|
|
? await requireEditableOrganization({ user, request })
|
|
: undefined;
|
|
|
|
errorToastIfFalsy(
|
|
(await ImageRepository.countUnvalidatedBySubmitterUserId(user.id)) <
|
|
MAX_UNVALIDATED_IMG_COUNT,
|
|
"Too many unvalidated images",
|
|
);
|
|
|
|
const uploadHandler = async (fileUpload: FileUpload) => {
|
|
if (fileUpload.fieldName === "img") {
|
|
const [, ending] = fileUpload.name.split(".");
|
|
invariant(ending);
|
|
const newFilename = `img-${Date.now()}.${ending}`;
|
|
|
|
const uploadedFileLocation = await uploadStreamToS3(
|
|
fileUpload.stream(),
|
|
newFilename,
|
|
);
|
|
return uploadedFileLocation;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const formData = await safeParseMultipartFormData(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 =
|
|
user.roles.includes("SUPPORTER") || validatedType === "org-pfp";
|
|
|
|
await ImageRepository.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);
|
|
|
|
errorToastIfFalsy(team, "Team not found");
|
|
errorToastIfFalsy(
|
|
isTeamManager({ team, user }),
|
|
"You must be the team manager to upload images",
|
|
);
|
|
|
|
return team;
|
|
}
|
|
|
|
async function requireEditableOrganization({
|
|
user,
|
|
request,
|
|
}: {
|
|
user: { id: number };
|
|
request: Request;
|
|
}) {
|
|
const { slug } = parseSearchParams({
|
|
request,
|
|
schema: z.object({ slug: z.string() }),
|
|
});
|
|
const organization = badRequestIfFalsy(
|
|
await TournamentOrganizationRepository.findBySlug(slug),
|
|
);
|
|
|
|
requirePermission(organization, "EDIT", user);
|
|
|
|
return organization;
|
|
}
|