mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
221 lines
5.3 KiB
TypeScript
221 lines
5.3 KiB
TypeScript
import type { ActionFunctionArgs } from "@remix-run/node";
|
|
import { z } from "zod/v4";
|
|
import * as AdminRepository from "~/features/admin/AdminRepository.server";
|
|
import { requireUser } from "~/features/auth/core/user.server";
|
|
import { refreshBannedCache } from "~/features/ban/core/banned.server";
|
|
import * as BuildRepository from "~/features/builds/BuildRepository.server";
|
|
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
|
import { requireRole } from "~/modules/permissions/guards.server";
|
|
import {
|
|
errorToast,
|
|
parseRequestPayload,
|
|
successToast,
|
|
} from "~/utils/remix.server";
|
|
import { errorIsSqliteForeignKeyConstraintFailure } from "~/utils/sql";
|
|
import { assertUnreachable } from "~/utils/types";
|
|
import { _action, actualNumber, friendCode } from "~/utils/zod";
|
|
import { plusTiersFromVotingAndLeaderboard } from "../core/plus-tier.server";
|
|
|
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
|
const data = await parseRequestPayload({
|
|
request,
|
|
schema: adminActionSchema,
|
|
});
|
|
const user = await requireUser(request);
|
|
|
|
let message: string;
|
|
switch (data._action) {
|
|
case "MIGRATE": {
|
|
requireRole(user, "STAFF");
|
|
|
|
try {
|
|
const errorMessage = await AdminRepository.migrate({
|
|
oldUserId: data["old-user"],
|
|
newUserId: data["new-user"],
|
|
});
|
|
|
|
if (errorMessage) {
|
|
errorToast(`Migration failed. Reason: ${errorMessage}`);
|
|
}
|
|
|
|
message = "Account migrated";
|
|
break;
|
|
} catch (err) {
|
|
if (errorIsSqliteForeignKeyConstraintFailure(err)) {
|
|
errorToast(
|
|
"New user has data preventing the migration (e.g. member of tournament teams or SendouQ played)",
|
|
);
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
}
|
|
case "REFRESH": {
|
|
requireRole(user, "ADMIN");
|
|
|
|
await AdminRepository.replacePlusTiers(
|
|
await plusTiersFromVotingAndLeaderboard(),
|
|
);
|
|
|
|
await BuildRepository.recalculateAllTiers();
|
|
|
|
message = "Plus tiers refreshed";
|
|
break;
|
|
}
|
|
case "FORCE_PATRON": {
|
|
requireRole(user, "ADMIN");
|
|
|
|
await AdminRepository.forcePatron({
|
|
id: data.user,
|
|
patronSince: new Date(),
|
|
patronTier: data.patronTier,
|
|
patronTill: new Date(data.patronTill),
|
|
});
|
|
|
|
message = "Patron status updated";
|
|
break;
|
|
}
|
|
case "CLEAN_UP": {
|
|
requireRole(user, "ADMIN");
|
|
|
|
// on purpose sync
|
|
AdminRepository.cleanUp();
|
|
|
|
message = "Clean up done";
|
|
break;
|
|
}
|
|
case "ARTIST": {
|
|
requireRole(user, "STAFF");
|
|
|
|
await AdminRepository.makeArtistByUserId(data.user);
|
|
|
|
message = "Artist permissions given";
|
|
break;
|
|
}
|
|
case "VIDEO_ADDER": {
|
|
requireRole(user, "STAFF");
|
|
|
|
await AdminRepository.makeVideoAdderByUserId(data.user);
|
|
|
|
message = "VoD adder permissions given";
|
|
break;
|
|
}
|
|
case "TOURNAMENT_ORGANIZER": {
|
|
requireRole(user, "ADMIN");
|
|
|
|
await AdminRepository.makeTournamentOrganizerByUserId(data.user);
|
|
|
|
message = "Tournament permissions given";
|
|
break;
|
|
}
|
|
case "LINK_PLAYER": {
|
|
requireRole(user, "STAFF");
|
|
|
|
await AdminRepository.linkUserAndPlayer({
|
|
userId: data.user,
|
|
playerId: data.playerId,
|
|
});
|
|
|
|
message = "Linked user and player";
|
|
break;
|
|
}
|
|
case "BAN_USER": {
|
|
requireRole(user, "STAFF");
|
|
|
|
await AdminRepository.banUser({
|
|
bannedReason: data.reason ?? null,
|
|
userId: data.user,
|
|
banned: data.duration ? new Date(data.duration) : 1,
|
|
bannedByUserId: user.id,
|
|
});
|
|
|
|
refreshBannedCache();
|
|
|
|
message = "User banned";
|
|
break;
|
|
}
|
|
case "UNBAN_USER": {
|
|
requireRole(user, "STAFF");
|
|
|
|
await AdminRepository.unbanUser({
|
|
userId: data.user,
|
|
unbannedByUserId: user.id,
|
|
});
|
|
|
|
refreshBannedCache();
|
|
|
|
message = "User unbanned";
|
|
break;
|
|
}
|
|
case "UPDATE_FRIEND_CODE": {
|
|
requireRole(user, "STAFF");
|
|
|
|
await UserRepository.insertFriendCode({
|
|
friendCode: data.friendCode,
|
|
submitterUserId: user.id,
|
|
userId: data.user,
|
|
});
|
|
|
|
message = "Friend code updated";
|
|
break;
|
|
}
|
|
default: {
|
|
assertUnreachable(data);
|
|
}
|
|
}
|
|
|
|
return successToast(message);
|
|
};
|
|
|
|
export const adminActionSchema = z.union([
|
|
z.object({
|
|
_action: _action("MIGRATE"),
|
|
"old-user": z.preprocess(actualNumber, z.number().positive()),
|
|
"new-user": z.preprocess(actualNumber, z.number().positive()),
|
|
}),
|
|
z.object({
|
|
_action: _action("REFRESH"),
|
|
}),
|
|
z.object({
|
|
_action: _action("CLEAN_UP"),
|
|
}),
|
|
z.object({
|
|
_action: _action("FORCE_PATRON"),
|
|
user: z.preprocess(actualNumber, z.number().positive()),
|
|
patronTier: z.preprocess(actualNumber, z.number()),
|
|
patronTill: z.string(),
|
|
}),
|
|
z.object({
|
|
_action: _action("VIDEO_ADDER"),
|
|
user: z.preprocess(actualNumber, z.number().positive()),
|
|
}),
|
|
z.object({
|
|
_action: _action("TOURNAMENT_ORGANIZER"),
|
|
user: z.preprocess(actualNumber, z.number().positive()),
|
|
}),
|
|
z.object({
|
|
_action: _action("ARTIST"),
|
|
user: z.preprocess(actualNumber, z.number().positive()),
|
|
}),
|
|
z.object({
|
|
_action: _action("LINK_PLAYER"),
|
|
user: z.preprocess(actualNumber, z.number().positive()),
|
|
playerId: z.preprocess(actualNumber, z.number().positive()),
|
|
}),
|
|
z.object({
|
|
_action: _action("BAN_USER"),
|
|
user: z.preprocess(actualNumber, z.number().positive()),
|
|
reason: z.string().nullish(),
|
|
duration: z.string().nullish(),
|
|
}),
|
|
z.object({
|
|
_action: _action("UNBAN_USER"),
|
|
user: z.preprocess(actualNumber, z.number().positive()),
|
|
}),
|
|
z.object({
|
|
_action: _action("UPDATE_FRIEND_CODE"),
|
|
friendCode,
|
|
user: z.preprocess(actualNumber, z.number().positive()),
|
|
}),
|
|
]);
|