mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-22 23:21:52 -05:00
350 lines
9.3 KiB
TypeScript
350 lines
9.3 KiB
TypeScript
import invariant from "tiny-invariant";
|
|
import type * as PlusSuggestionRepository from "~/features/plus-suggestions/PlusSuggestionRepository.server";
|
|
import type * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
|
|
import { ADMIN_ID, LOHI_TOKEN_HEADER_NAME, MOD_IDS } from "./constants";
|
|
import type {
|
|
CalendarEvent,
|
|
PlusSuggestion,
|
|
User,
|
|
UserWithPlusTier,
|
|
} from "./db/types";
|
|
import type { FindMatchById } from "./features/tournament-bracket/queries/findMatchById.server";
|
|
import { allTruthy } from "./utils/arrays";
|
|
import { databaseTimestampToDate } from "./utils/dates";
|
|
import { isVotingActive } from "./features/plus-voting/core";
|
|
|
|
// TODO: 1) move "root checkers" to one file and utils to one file 2) make utils const for more terseness
|
|
|
|
type IsAdminUser = Pick<User, "id">;
|
|
export function isAdmin(user?: IsAdminUser) {
|
|
return user?.id === ADMIN_ID;
|
|
}
|
|
|
|
export function isMod(user?: IsAdminUser) {
|
|
if (!user) return false;
|
|
|
|
return isAdmin(user) || MOD_IDS.includes(user.id);
|
|
}
|
|
|
|
export function canPerformAdminActions(user?: IsAdminUser) {
|
|
if (["development", "test"].includes(process.env.NODE_ENV)) return true;
|
|
|
|
return isAdmin(user);
|
|
}
|
|
|
|
function adminOverride(user?: IsAdminUser) {
|
|
if (isAdmin(user)) {
|
|
return () => true;
|
|
}
|
|
|
|
return (canPerformActionAsNormalUser: boolean) =>
|
|
canPerformActionAsNormalUser;
|
|
}
|
|
|
|
interface CanAddCommentToSuggestionArgs {
|
|
user?: Pick<UserWithPlusTier, "id" | "plusTier">;
|
|
suggestions: PlusSuggestionRepository.FindAllByMonthItem[];
|
|
suggested: Pick<User, "id">;
|
|
targetPlusTier: NonNullable<UserWithPlusTier["plusTier"]>;
|
|
}
|
|
export function canAddCommentToSuggestionFE(
|
|
args: CanAddCommentToSuggestionArgs,
|
|
) {
|
|
return allTruthy([
|
|
!alreadyCommentedByUser(args),
|
|
isPlusServerMember(args.user),
|
|
args.user?.plusTier && args.targetPlusTier >= args.user?.plusTier,
|
|
]);
|
|
}
|
|
|
|
export function canAddCommentToSuggestionBE({
|
|
user,
|
|
suggestions,
|
|
suggested,
|
|
targetPlusTier,
|
|
}: CanAddCommentToSuggestionArgs) {
|
|
return allTruthy([
|
|
canAddCommentToSuggestionFE({
|
|
user,
|
|
suggestions,
|
|
suggested,
|
|
targetPlusTier,
|
|
}),
|
|
playerAlreadySuggested({ suggestions, suggested, targetPlusTier }),
|
|
targetPlusTierIsSmallerOrEqual({ user, targetPlusTier }),
|
|
]);
|
|
}
|
|
|
|
interface CanDeleteCommentArgs {
|
|
suggestionId: PlusSuggestion["id"];
|
|
author: Pick<User, "id">;
|
|
user?: Pick<User, "id" | "discordId">;
|
|
suggestions: PlusSuggestionRepository.FindAllByMonthItem[];
|
|
}
|
|
export function canDeleteComment(args: CanDeleteCommentArgs) {
|
|
const votingActive =
|
|
process.env.NODE_ENV === "test" ? false : isVotingActive();
|
|
|
|
if (isFirstSuggestion(args)) {
|
|
if (votingActive) return false;
|
|
|
|
return adminOverride(args.user)(
|
|
allTruthy([isOwnComment(args), suggestionHasNoOtherComments(args)]),
|
|
);
|
|
}
|
|
|
|
return isOwnComment(args);
|
|
}
|
|
|
|
export function isFirstSuggestion({
|
|
suggestionId,
|
|
suggestions,
|
|
}: Pick<CanDeleteCommentArgs, "suggestionId" | "suggestions">) {
|
|
for (const suggestedUser of Object.values(suggestions).flat()) {
|
|
for (const [i, suggestion] of suggestedUser.suggestions.entries()) {
|
|
if (suggestion.id !== suggestionId) continue;
|
|
|
|
return i === 0;
|
|
}
|
|
}
|
|
|
|
throw new Error(`Invalid suggestion id: ${suggestionId}`);
|
|
}
|
|
|
|
function alreadyCommentedByUser({
|
|
user,
|
|
suggestions,
|
|
suggested,
|
|
targetPlusTier,
|
|
}: CanAddCommentToSuggestionArgs) {
|
|
return suggestions.some(
|
|
(suggestion) =>
|
|
suggestion.tier === targetPlusTier &&
|
|
suggestion.suggested.id === suggested.id &&
|
|
suggestion.suggestions.some(
|
|
(suggestion) => suggestion.author.id === user?.id,
|
|
),
|
|
);
|
|
}
|
|
|
|
export function playerAlreadySuggested({
|
|
suggestions,
|
|
suggested,
|
|
targetPlusTier,
|
|
}: Pick<
|
|
CanAddCommentToSuggestionArgs,
|
|
"suggestions" | "suggested" | "targetPlusTier"
|
|
>) {
|
|
return suggestions.some(
|
|
(suggestion) =>
|
|
suggestion.suggested.id === suggested.id &&
|
|
suggestion.tier === targetPlusTier,
|
|
);
|
|
}
|
|
|
|
function targetPlusTierIsSmallerOrEqual({
|
|
user,
|
|
targetPlusTier,
|
|
}: Pick<CanAddCommentToSuggestionArgs, "user" | "targetPlusTier">) {
|
|
return user?.plusTier && user.plusTier <= targetPlusTier;
|
|
}
|
|
|
|
function isOwnComment({ author, user }: CanDeleteCommentArgs) {
|
|
return author.id === user?.id;
|
|
}
|
|
|
|
function suggestionHasNoOtherComments({
|
|
suggestions,
|
|
suggestionId,
|
|
}: Pick<CanDeleteCommentArgs, "suggestionId" | "suggestions">) {
|
|
for (const suggestedUser of Object.values(suggestions).flat()) {
|
|
for (const suggestion of suggestedUser.suggestions) {
|
|
if (suggestion.id !== suggestionId) continue;
|
|
|
|
return suggestedUser.suggestions.length === 1;
|
|
}
|
|
}
|
|
|
|
throw new Error(`Invalid suggestion id: ${suggestionId}`);
|
|
}
|
|
|
|
interface CanSuggestNewUserFEArgs {
|
|
user?: Pick<UserWithPlusTier, "id" | "plusTier">;
|
|
suggestions: PlusSuggestionRepository.FindAllByMonthItem[];
|
|
}
|
|
export function canSuggestNewUserFE({
|
|
user,
|
|
suggestions,
|
|
}: CanSuggestNewUserFEArgs) {
|
|
const votingActive =
|
|
process.env.NODE_ENV === "test" ? false : isVotingActive();
|
|
|
|
return allTruthy([
|
|
!votingActive,
|
|
!hasUserSuggestedThisMonth({ user, suggestions }),
|
|
isPlusServerMember(user),
|
|
]);
|
|
}
|
|
|
|
interface CanSuggestNewUserBEArgs extends CanSuggestNewUserFEArgs {
|
|
suggested: Pick<UserWithPlusTier, "id" | "plusTier">;
|
|
targetPlusTier: NonNullable<UserWithPlusTier["plusTier"]>;
|
|
}
|
|
export function canSuggestNewUserBE({
|
|
user,
|
|
suggestions,
|
|
suggested,
|
|
targetPlusTier,
|
|
}: CanSuggestNewUserBEArgs) {
|
|
return allTruthy([
|
|
canSuggestNewUserFE({ user, suggestions }),
|
|
!playerAlreadySuggested({ suggestions, suggested, targetPlusTier }),
|
|
targetPlusTierIsSmallerOrEqual({ user, targetPlusTier }),
|
|
!playerAlreadyMember({ suggested, targetPlusTier }),
|
|
]);
|
|
}
|
|
|
|
function isPlusServerMember(user?: Pick<UserWithPlusTier, "plusTier">) {
|
|
return Boolean(user?.plusTier);
|
|
}
|
|
|
|
export function playerAlreadyMember({
|
|
suggested,
|
|
targetPlusTier,
|
|
}: Pick<CanSuggestNewUserBEArgs, "suggested" | "targetPlusTier">) {
|
|
return suggested.plusTier && suggested.plusTier <= targetPlusTier;
|
|
}
|
|
|
|
function hasUserSuggestedThisMonth({
|
|
user,
|
|
suggestions,
|
|
}: Pick<CanSuggestNewUserFEArgs, "user" | "suggestions">) {
|
|
return suggestions.some(
|
|
(suggestion) => suggestion.suggestions[0].author.id === user?.id,
|
|
);
|
|
}
|
|
|
|
/** Some endpoints can only be accessed with an auth token. Used by Lohi bot and cron jobs. */
|
|
export function canAccessLohiEndpoint(request: Request) {
|
|
invariant(process.env["LOHI_TOKEN"], "LOHI_TOKEN is required");
|
|
return (
|
|
request.headers.get(LOHI_TOKEN_HEADER_NAME) === process.env["LOHI_TOKEN"]
|
|
);
|
|
}
|
|
|
|
interface CanEditBadgeOwnersArgs {
|
|
user?: Pick<User, "id">;
|
|
managers: { id: number }[];
|
|
}
|
|
|
|
export function canEditBadgeOwners({ user, managers }: CanEditBadgeOwnersArgs) {
|
|
return adminOverride(user)(isBadgeManager({ user, managers }));
|
|
}
|
|
|
|
function isBadgeManager({
|
|
user,
|
|
managers,
|
|
}: Pick<CanEditBadgeOwnersArgs, "user" | "managers">) {
|
|
if (!user) return false;
|
|
return managers.some((manager) => manager.id === user.id);
|
|
}
|
|
|
|
export function canEditBadgeManagers(user?: IsAdminUser) {
|
|
return isMod(user);
|
|
}
|
|
|
|
interface CanEditCalendarEventArgs {
|
|
user?: Pick<User, "id">;
|
|
event: Pick<CalendarEvent, "authorId">;
|
|
}
|
|
export function canEditCalendarEvent({
|
|
user,
|
|
event,
|
|
}: CanEditCalendarEventArgs) {
|
|
return adminOverride(user)(user?.id === event.authorId);
|
|
}
|
|
|
|
export function canDeleteCalendarEvent({
|
|
user,
|
|
event,
|
|
startTime,
|
|
}: CanEditCalendarEventArgs & { startTime: Date }) {
|
|
return adminOverride(user)(
|
|
user?.id === event.authorId && startTime.getTime() > new Date().getTime(),
|
|
);
|
|
}
|
|
|
|
interface CanReportCalendarEventWinnersArgs {
|
|
user?: Pick<User, "id">;
|
|
event: Pick<CalendarEvent, "authorId">;
|
|
startTimes: number[];
|
|
}
|
|
export function canReportCalendarEventWinners({
|
|
user,
|
|
event,
|
|
startTimes,
|
|
}: CanReportCalendarEventWinnersArgs) {
|
|
return allTruthy([
|
|
canEditCalendarEvent({ user, event }),
|
|
eventStartedInThePast(startTimes),
|
|
]);
|
|
}
|
|
|
|
function eventStartedInThePast(
|
|
startTimes: CanReportCalendarEventWinnersArgs["startTimes"],
|
|
) {
|
|
return startTimes.every(
|
|
(startTime) =>
|
|
databaseTimestampToDate(startTime).getTime() < new Date().getTime(),
|
|
);
|
|
}
|
|
|
|
export function canEnableTOTools(user?: IsAdminUser) {
|
|
return isAdmin(user);
|
|
}
|
|
|
|
interface CanAdminTournament {
|
|
user?: Pick<User, "id">;
|
|
tournament: TournamentRepository.FindById;
|
|
}
|
|
export function canAdminTournament({ user, tournament }: CanAdminTournament) {
|
|
// temporary hack to let Njok admin tournaments as well
|
|
// if (user?.id === 14710) return true;
|
|
// .jpg
|
|
// if (user?.id === 622) return true;
|
|
// toasty
|
|
// if (user?.id === 5036) return true;
|
|
|
|
return adminOverride(user)(user?.id === tournament.author.id);
|
|
}
|
|
|
|
export function canReportTournamentScore({
|
|
match,
|
|
user,
|
|
isMemberOfATeamInTheMatch,
|
|
tournament,
|
|
}: {
|
|
match: NonNullable<FindMatchById>;
|
|
user?: Pick<User, "id">;
|
|
isMemberOfATeamInTheMatch: boolean;
|
|
tournament: TournamentRepository.FindById;
|
|
}) {
|
|
const matchIsOver =
|
|
match.opponentOne?.result === "win" || match.opponentTwo?.result === "win";
|
|
|
|
return (
|
|
!matchIsOver &&
|
|
(isMemberOfATeamInTheMatch || canAdminTournament({ user, tournament }))
|
|
);
|
|
}
|
|
|
|
export function canAddCustomizedColorsToUserProfile(
|
|
user?: Pick<User, "id" | "patronTier">,
|
|
) {
|
|
if (!user) return false;
|
|
|
|
return adminOverride(user)(
|
|
Boolean(user?.patronTier) && user.patronTier! >= 2,
|
|
);
|
|
}
|