sendou.ink/app/features/associations/actions/associations.server.ts
2025-12-29 19:21:11 +02:00

134 lines
3.4 KiB
TypeScript

import type { ActionFunctionArgs } from "react-router";
import { ASSOCIATION } from "~/features/associations/associations-constants";
import { associationsPageActionSchema } from "~/features/associations/associations-schemas";
import { requireUser } from "~/features/auth/core/user.server";
import { requirePermission } from "~/modules/permissions/guards.server";
import {
badRequestIfFalsy,
errorToastIfFalsy,
parseRequestPayload,
successToast,
} from "~/utils/remix.server";
import { assertUnreachable } from "~/utils/types";
import * as AssociationRepository from "../AssociationRepository.server";
export const action = async ({ request }: ActionFunctionArgs) => {
const user = await requireUser(request);
const data = await parseRequestPayload({
request,
schema: associationsPageActionSchema,
});
switch (data._action) {
case "REMOVE_MEMBER": {
await validateHasManagePermissions({
user,
associationId: data.associationId,
});
errorToastIfFalsy(
data.userId !== user.id,
"Cannot remove yourself from the association",
);
await AssociationRepository.removeMember({
userId: data.userId,
associationId: data.associationId,
});
break;
}
case "DELETE_ASSOCIATION": {
await validateHasManagePermissions({
user,
associationId: data.associationId,
});
await AssociationRepository.del(data.associationId);
break;
}
case "REFRESH_INVITE_CODE": {
await validateHasManagePermissions({
user,
associationId: data.associationId,
});
await AssociationRepository.refreshInviteCode(data.associationId);
return successToast("Invite code reset");
}
case "JOIN_ASSOCIATION": {
const associationToJoin = badRequestIfFalsy(
await AssociationRepository.findByInviteCode(data.inviteCode, {
withMembers: true,
}),
);
errorToastIfFalsy(
associationToJoin.members?.every((member) => member.id !== user.id),
"You are already a member of this association",
);
errorToastIfFalsy(
associationToJoin.members!.length <
ASSOCIATION.MAX_ASSOCIATION_MEMBER_COUNT,
"Association is full",
);
const maxAssociationCount = user.roles.includes("SUPPORTER")
? ASSOCIATION.MAX_COUNT_SUPPORTER
: ASSOCIATION.MAX_COUNT_REGULAR_USER;
errorToastIfFalsy(
(await AssociationRepository.findByMemberUserId(user.id)).actual
.length < maxAssociationCount,
`Regular users can only be a member of ${maxAssociationCount} associations (supporters ${ASSOCIATION.MAX_COUNT_SUPPORTER})`,
);
await AssociationRepository.addMember({
userId: user.id,
associationId: associationToJoin.id,
});
break;
}
case "LEAVE_ASSOCIATION": {
const association = badRequestIfFalsy(
await AssociationRepository.findById(data.associationId, {
withMembers: true,
}),
);
errorToastIfFalsy(
!association.permissions.MANAGE.includes(user.id),
"You cannot leave an association you manage",
);
await AssociationRepository.removeMember({
userId: user.id,
associationId: data.associationId,
});
return successToast("Left association");
}
default: {
assertUnreachable(data);
}
}
return null;
};
async function validateHasManagePermissions({
user,
associationId,
}: {
user: { id: number };
associationId: number;
}) {
const association = badRequestIfFalsy(
await AssociationRepository.findById(associationId, { withMembers: true }),
);
requirePermission(association, "MANAGE", user);
}