Migrate Prettier/Eslint/Stylelint setup to Biome (#1772)

* Initial

* CSS lint

* Test CI

* Add 1v1, 2v2, and 3v3 Tags (#1771)

* Initial

* CSS lint

* Test CI

* Rename step

---------

Co-authored-by: xi <104683822+ximk@users.noreply.github.com>
This commit is contained in:
Kalle
2024-06-24 13:07:17 +03:00
committed by GitHub
parent 73e9fca492
commit fd48bced91
1070 changed files with 219485 additions and 224310 deletions

View File

@@ -28,116 +28,116 @@ const cleanUpStm = sql.prepare(/*sql*/ `
`);
export const cleanUp = () => {
removeOldLikesStm.run();
removeOldGroupStm.run();
cleanUpStm.run();
removeOldLikesStm.run();
removeOldGroupStm.run();
cleanUpStm.run();
};
export function migrate(args: { newUserId: number; oldUserId: number }) {
return db.transaction().execute(async (trx) => {
const deletedUser = await trx
.deleteFrom("User")
.where("User.id", "=", args.newUserId)
.returning("discordId")
.executeTakeFirstOrThrow();
return db.transaction().execute(async (trx) => {
const deletedUser = await trx
.deleteFrom("User")
.where("User.id", "=", args.newUserId)
.returning("discordId")
.executeTakeFirstOrThrow();
await trx
.updateTable("User")
.set({ discordId: deletedUser.discordId })
.where("User.id", "=", args.oldUserId)
.execute();
});
await trx
.updateTable("User")
.set({ discordId: deletedUser.discordId })
.where("User.id", "=", args.oldUserId)
.execute();
});
}
export function replacePlusTiers(
plusTiers: Array<{ userId: number; tier: number }>,
plusTiers: Array<{ userId: number; tier: number }>,
) {
invariant(plusTiers.length > 0, "plusTiers must not be empty");
invariant(plusTiers.length > 0, "plusTiers must not be empty");
return db.transaction().execute(async (trx) => {
await trx.deleteFrom("PlusTier").execute();
await trx.insertInto("PlusTier").values(plusTiers).execute();
});
return db.transaction().execute(async (trx) => {
await trx.deleteFrom("PlusTier").execute();
await trx.insertInto("PlusTier").values(plusTiers).execute();
});
}
export function allPlusTiersFromLatestVoting() {
return db
.selectFrom("FreshPlusTier")
.select(["FreshPlusTier.userId", "FreshPlusTier.tier"])
.where("FreshPlusTier.tier", "is not", null)
.execute() as Promise<{ userId: number; tier: number }[]>;
return db
.selectFrom("FreshPlusTier")
.select(["FreshPlusTier.userId", "FreshPlusTier.tier"])
.where("FreshPlusTier.tier", "is not", null)
.execute() as Promise<{ userId: number; tier: number }[]>;
}
export function makeVideoAdderByUserId(userId: number) {
return db
.updateTable("User")
.set({ isVideoAdder: 1 })
.where("User.id", "=", userId)
.execute();
return db
.updateTable("User")
.set({ isVideoAdder: 1 })
.where("User.id", "=", userId)
.execute();
}
export async function linkUserAndPlayer({
userId,
playerId,
userId,
playerId,
}: {
userId: number;
playerId: number;
userId: number;
playerId: number;
}) {
await db
.updateTable("SplatoonPlayer")
.set({ userId: null })
.where("SplatoonPlayer.userId", "=", userId)
.execute();
await db
.updateTable("SplatoonPlayer")
.set({ userId: null })
.where("SplatoonPlayer.userId", "=", userId)
.execute();
await db
.updateTable("SplatoonPlayer")
.set({ userId })
.where("SplatoonPlayer.id", "=", playerId)
.execute();
await db
.updateTable("SplatoonPlayer")
.set({ userId })
.where("SplatoonPlayer.id", "=", playerId)
.execute();
syncXPBadges();
syncXPBadges();
}
export function forcePatron(args: {
id: number;
patronTier: Tables["User"]["patronTier"];
patronSince: Date;
patronTill: Date;
id: number;
patronTier: Tables["User"]["patronTier"];
patronSince: Date;
patronTill: Date;
}) {
return db
.updateTable("User")
.set({
patronTier: args.patronTier,
patronSince: dateToDatabaseTimestamp(args.patronSince),
patronTill: dateToDatabaseTimestamp(args.patronTill),
})
.where("User.id", "=", args.id)
.execute();
return db
.updateTable("User")
.set({
patronTier: args.patronTier,
patronSince: dateToDatabaseTimestamp(args.patronSince),
patronTill: dateToDatabaseTimestamp(args.patronTill),
})
.where("User.id", "=", args.id)
.execute();
}
export function banUser({
userId,
banned,
bannedReason,
userId,
banned,
bannedReason,
}: {
userId: number;
banned: 1 | Date;
bannedReason: string | null;
userId: number;
banned: 1 | Date;
bannedReason: string | null;
}) {
return db
.updateTable("User")
.set({
banned: banned === 1 ? banned : dateToDatabaseTimestamp(banned),
bannedReason,
})
.where("User.id", "=", userId)
.execute();
return db
.updateTable("User")
.set({
banned: banned === 1 ? banned : dateToDatabaseTimestamp(banned),
bannedReason,
})
.where("User.id", "=", userId)
.execute();
}
export function unbanUser(userId: number) {
return db
.updateTable("User")
.set({ banned: 0 })
.where("User.id", "=", userId)
.execute();
return db
.updateTable("User")
.set({ banned: 0 })
.where("User.id", "=", userId)
.execute();
}

View File

@@ -3,162 +3,162 @@ import { z } from "zod";
import * as AdminRepository from "~/features/admin/AdminRepository.server";
import { makeArtist } from "~/features/art/queries/makeArtist.server";
import { requireUserId } from "~/features/auth/core/user.server";
import { refreshBannedCache } from "~/features/ban/core/banned.server";
import { isAdmin, isMod } from "~/permissions";
import { logger } from "~/utils/logger";
import { parseRequestFormData, validate } from "~/utils/remix";
import { assertUnreachable } from "~/utils/types";
import { _action, actualNumber } from "~/utils/zod";
import { plusTiersFromVotingAndLeaderboard } from "../core/plus-tier.server";
import { refreshBannedCache } from "~/features/ban/core/banned.server";
import { logger } from "~/utils/logger";
export const action = async ({ request }: ActionFunctionArgs) => {
const data = await parseRequestFormData({
request,
schema: adminActionSchema,
});
const user = await requireUserId(request);
const data = await parseRequestFormData({
request,
schema: adminActionSchema,
});
const user = await requireUserId(request);
switch (data._action) {
case "MIGRATE": {
validate(isAdmin(user), "Admin needed", 401);
switch (data._action) {
case "MIGRATE": {
validate(isAdmin(user), "Admin needed", 401);
await AdminRepository.migrate({
oldUserId: data["old-user"],
newUserId: data["new-user"],
});
break;
}
case "REFRESH": {
validate(isAdmin(user));
await AdminRepository.migrate({
oldUserId: data["old-user"],
newUserId: data["new-user"],
});
break;
}
case "REFRESH": {
validate(isAdmin(user));
await AdminRepository.replacePlusTiers(
await plusTiersFromVotingAndLeaderboard(),
);
break;
}
case "FORCE_PATRON": {
validate(isAdmin(user), "Admin needed", 401);
await AdminRepository.replacePlusTiers(
await plusTiersFromVotingAndLeaderboard(),
);
break;
}
case "FORCE_PATRON": {
validate(isAdmin(user), "Admin needed", 401);
await AdminRepository.forcePatron({
id: data["user"],
patronSince: new Date(),
patronTier: data.patronTier,
patronTill: new Date(data.patronTill),
});
break;
}
case "CLEAN_UP": {
validate(isAdmin(user), "Admin needed", 401);
await AdminRepository.forcePatron({
id: data.user,
patronSince: new Date(),
patronTier: data.patronTier,
patronTill: new Date(data.patronTill),
});
break;
}
case "CLEAN_UP": {
validate(isAdmin(user), "Admin needed", 401);
// on purpose sync
AdminRepository.cleanUp();
break;
}
case "ARTIST": {
validate(isMod(user), "Mod needed", 401);
// on purpose sync
AdminRepository.cleanUp();
break;
}
case "ARTIST": {
validate(isMod(user), "Mod needed", 401);
makeArtist(data["user"]);
break;
}
case "VIDEO_ADDER": {
validate(isMod(user), "Mod needed", 401);
makeArtist(data.user);
break;
}
case "VIDEO_ADDER": {
validate(isMod(user), "Mod needed", 401);
await AdminRepository.makeVideoAdderByUserId(data["user"]);
break;
}
case "LINK_PLAYER": {
validate(isMod(user), "Mod needed", 401);
await AdminRepository.makeVideoAdderByUserId(data.user);
break;
}
case "LINK_PLAYER": {
validate(isMod(user), "Mod needed", 401);
await AdminRepository.linkUserAndPlayer({
userId: data["user"],
playerId: data.playerId,
});
await AdminRepository.linkUserAndPlayer({
userId: data.user,
playerId: data.playerId,
});
break;
}
case "BAN_USER": {
validate(isMod(user), "Mod needed", 401);
break;
}
case "BAN_USER": {
validate(isMod(user), "Mod needed", 401);
await AdminRepository.banUser({
bannedReason: data.reason ?? null,
userId: data["user"],
banned: data.duration ? new Date(data.duration) : 1,
});
await AdminRepository.banUser({
bannedReason: data.reason ?? null,
userId: data.user,
banned: data.duration ? new Date(data.duration) : 1,
});
refreshBannedCache();
refreshBannedCache();
logger.info("Banned user", {
userId: data["user"],
byUserId: user.id,
reason: data.reason,
duration: data.duration
? new Date(data.duration).toLocaleString()
: undefined,
});
logger.info("Banned user", {
userId: data.user,
byUserId: user.id,
reason: data.reason,
duration: data.duration
? new Date(data.duration).toLocaleString()
: undefined,
});
break;
}
case "UNBAN_USER": {
validate(isMod(user), "Mod needed", 401);
break;
}
case "UNBAN_USER": {
validate(isMod(user), "Mod needed", 401);
await AdminRepository.unbanUser(data["user"]);
await AdminRepository.unbanUser(data.user);
refreshBannedCache();
refreshBannedCache();
logger.info("Unbanned user", {
userId: data["user"],
byUserId: user.id,
});
logger.info("Unbanned user", {
userId: data.user,
byUserId: user.id,
});
break;
}
default: {
assertUnreachable(data);
}
}
break;
}
default: {
assertUnreachable(data);
}
}
return { ok: true };
return { ok: true };
};
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("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("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("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()),
}),
]);

View File

@@ -1,44 +1,44 @@
import invariant from "~/utils/invariant";
import * as AdminRepository from "~/features/admin/AdminRepository.server";
import { addPendingPlusTiers } from "~/features/leaderboards/core/leaderboards.server";
import { userSPLeaderboard } from "~/features/leaderboards/queries/userSPLeaderboard.server";
import { previousSeason } from "~/features/mmr/season";
import * as AdminRepository from "~/features/admin/AdminRepository.server";
import invariant from "~/utils/invariant";
export async function plusTiersFromVotingAndLeaderboard() {
const newMembersFromLeaderboard = fromLeaderboard();
const newMembersFromVoting =
await AdminRepository.allPlusTiersFromLatestVoting();
const newMembersFromLeaderboard = fromLeaderboard();
const newMembersFromVoting =
await AdminRepository.allPlusTiersFromLatestVoting();
return [
...newMembersFromLeaderboard,
// filter to ensure that user gets their highest tier
...newMembersFromVoting.filter(
(member) =>
!newMembersFromLeaderboard.some(
(leaderboardMember) => leaderboardMember.userId === member.userId,
),
),
];
return [
...newMembersFromLeaderboard,
// filter to ensure that user gets their highest tier
...newMembersFromVoting.filter(
(member) =>
!newMembersFromLeaderboard.some(
(leaderboardMember) => leaderboardMember.userId === member.userId,
),
),
];
}
function fromLeaderboard() {
const now = new Date();
const lastCompletedSeason = previousSeason(now);
invariant(lastCompletedSeason, "No previous season found");
const now = new Date();
const lastCompletedSeason = previousSeason(now);
invariant(lastCompletedSeason, "No previous season found");
// there has been voting after this season ended, the results no longer apply
if (now.getMonth() !== lastCompletedSeason.ends.getMonth()) return [];
// there has been voting after this season ended, the results no longer apply
if (now.getMonth() !== lastCompletedSeason.ends.getMonth()) return [];
const leaderboard = addPendingPlusTiers(
userSPLeaderboard(lastCompletedSeason.nth),
);
const leaderboard = addPendingPlusTiers(
userSPLeaderboard(lastCompletedSeason.nth),
);
return leaderboard.flatMap((entry) => {
if (!entry.pendingPlusTier) return [];
return leaderboard.flatMap((entry) => {
if (!entry.pendingPlusTier) return [];
return {
userId: entry.id,
tier: entry.pendingPlusTier,
};
});
return {
userId: entry.id,
tier: entry.pendingPlusTier,
};
});
}

View File

@@ -4,13 +4,13 @@ import { getUserId, isImpersonating } from "~/features/auth/core/user.server";
import { isMod } from "~/permissions";
export const loader: LoaderFunction = async ({ request }) => {
const user = await getUserId(request);
const user = await getUserId(request);
if (process.env.NODE_ENV === "production" && !isMod(user)) {
throw redirect("/");
}
if (process.env.NODE_ENV === "production" && !isMod(user)) {
throw redirect("/");
}
return {
isImpersonating: await isImpersonating(request),
};
return {
isImpersonating: await isImpersonating(request),
};
};

View File

@@ -1,260 +1,260 @@
import MockDate from "mockdate";
import { suite } from "uvu";
import * as assert from "uvu/assert";
import * as Test from "~/utils/Test";
import { action } from "./admin";
import { db } from "~/db/sql";
import MockDate from "mockdate";
import * as PlusVotingRepository from "~/features/plus-voting/PlusVotingRepository.server";
import * as Test from "~/utils/Test";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import type { adminActionSchema } from "../actions/admin.server";
import { action } from "./admin";
const PlusVoting = suite("Plus voting");
const adminAction = Test.wrappedAction<typeof adminActionSchema>({ action });
const voteArgs = ({
score,
votedId,
authorId = 1,
month = 6,
year = 2021,
score,
votedId,
authorId = 1,
month = 6,
year = 2021,
}: {
score: number;
votedId: number;
authorId?: number;
month?: number;
year?: number;
score: number;
votedId: number;
authorId?: number;
month?: number;
year?: number;
}) => ({
score,
votedId,
authorId,
month,
tier: 1,
validAfter: dateToDatabaseTimestamp(new Date("2021-12-11T00:00:00.000Z")),
year,
score,
votedId,
authorId,
month,
tier: 1,
validAfter: dateToDatabaseTimestamp(new Date("2021-12-11T00:00:00.000Z")),
year,
});
const countPlusTierMembers = (tier = 1) =>
db
.selectFrom("PlusTier")
.where("PlusTier.tier", "=", tier)
.select(({ fn }) => fn.count<number>("PlusTier.tier").as("count"))
.executeTakeFirstOrThrow()
.then((row) => row.count);
db
.selectFrom("PlusTier")
.where("PlusTier.tier", "=", tier)
.select(({ fn }) => fn.count<number>("PlusTier.tier").as("count"))
.executeTakeFirstOrThrow()
.then((row) => row.count);
const createLeaderboard = (userIds: number[]) =>
db
.insertInto("Skill")
.values(
userIds.map((userId, i) => ({
matchesCount: 10,
mu: 25,
sigma: 8.333333333333334,
ordinal: 0.5 - i * 0.001,
userId,
season: 1,
})),
)
.execute();
db
.insertInto("Skill")
.values(
userIds.map((userId, i) => ({
matchesCount: 10,
mu: 25,
sigma: 8.333333333333334,
ordinal: 0.5 - i * 0.001,
userId,
season: 1,
})),
)
.execute();
PlusVoting.after.each(() => {
MockDate.reset();
Test.database.reset();
MockDate.reset();
Test.database.reset();
});
PlusVoting("gives correct amount of plus tiers", async () => {
MockDate.set(new Date("2023-12-12T00:00:00.000Z"));
MockDate.set(new Date("2023-12-12T00:00:00.000Z"));
await Test.database.insertUsers(10);
await PlusVotingRepository.upsertMany(
Array.from({ length: 10 }).map((_, i) => {
const id = i + 1;
await Test.database.insertUsers(10);
await PlusVotingRepository.upsertMany(
Array.from({ length: 10 }).map((_, i) => {
const id = i + 1;
return voteArgs({
score: id <= 5 ? -1 : 1,
votedId: id,
});
}),
);
return voteArgs({
score: id <= 5 ? -1 : 1,
votedId: id,
});
}),
);
await adminAction({ _action: "REFRESH" }, { user: "admin" });
await adminAction({ _action: "REFRESH" }, { user: "admin" });
assert.equal(await countPlusTierMembers(), 5);
assert.equal(await countPlusTierMembers(), 5);
});
PlusVoting("60% is the criteria to pass voting", async () => {
MockDate.set(new Date("2023-12-12T00:00:00.000Z"));
MockDate.set(new Date("2023-12-12T00:00:00.000Z"));
await Test.database.insertUsers(10);
await Test.database.insertUsers(10);
// 50%
await PlusVotingRepository.upsertMany(
Array.from({ length: 10 }).map((_, i) => {
return voteArgs({
authorId: i + 1,
score: i < 5 ? -1 : 1,
votedId: 1,
});
}),
);
// 60%
await PlusVotingRepository.upsertMany(
Array.from({ length: 10 }).map((_, i) => {
return voteArgs({
authorId: i + 1,
score: i < 4 ? -1 : 1,
votedId: 2,
});
}),
);
// 50%
await PlusVotingRepository.upsertMany(
Array.from({ length: 10 }).map((_, i) => {
return voteArgs({
authorId: i + 1,
score: i < 5 ? -1 : 1,
votedId: 1,
});
}),
);
// 60%
await PlusVotingRepository.upsertMany(
Array.from({ length: 10 }).map((_, i) => {
return voteArgs({
authorId: i + 1,
score: i < 4 ? -1 : 1,
votedId: 2,
});
}),
);
await adminAction({ _action: "REFRESH" }, { user: "admin" });
await adminAction({ _action: "REFRESH" }, { user: "admin" });
const rows = await db
.selectFrom("PlusTier")
.select(["PlusTier.tier", "PlusTier.userId"])
.where("PlusTier.tier", "=", 1)
.execute();
const rows = await db
.selectFrom("PlusTier")
.select(["PlusTier.tier", "PlusTier.userId"])
.where("PlusTier.tier", "=", 1)
.execute();
assert.equal(rows.length, 1);
assert.equal(rows[0].userId, 2);
assert.equal(rows.length, 1);
assert.equal(rows[0].userId, 2);
});
PlusVoting(
"combines leaderboard and voting results (after season over)",
async () => {
MockDate.set(new Date("2023-11-29T00:00:00.000Z"));
"combines leaderboard and voting results (after season over)",
async () => {
MockDate.set(new Date("2023-11-29T00:00:00.000Z"));
await Test.database.insertUsers(2);
await PlusVotingRepository.upsertMany([
voteArgs({
score: 1,
votedId: 1,
}),
]);
await createLeaderboard([2]);
await Test.database.insertUsers(2);
await PlusVotingRepository.upsertMany([
voteArgs({
score: 1,
votedId: 1,
}),
]);
await createLeaderboard([2]);
await adminAction({ _action: "REFRESH" }, { user: "admin" });
await adminAction({ _action: "REFRESH" }, { user: "admin" });
assert.equal(await countPlusTierMembers(), 2);
},
assert.equal(await countPlusTierMembers(), 2);
},
);
PlusVoting(
"skips users from leaderboard with the skip flag for the season",
async () => {
MockDate.set(new Date("2023-11-29T00:00:00.000Z"));
"skips users from leaderboard with the skip flag for the season",
async () => {
MockDate.set(new Date("2023-11-29T00:00:00.000Z"));
await Test.database.insertUsers(11);
await createLeaderboard(Array.from({ length: 11 }).map((_, i) => i + 1));
await Test.database.insertUsers(11);
await createLeaderboard(Array.from({ length: 11 }).map((_, i) => i + 1));
await db
.updateTable("User")
.set({ plusSkippedForSeasonNth: 1 })
.where("User.id", "=", 1)
.execute();
await db
.updateTable("User")
.set({ plusSkippedForSeasonNth: 1 })
.where("User.id", "=", 1)
.execute();
await adminAction({ _action: "REFRESH" }, { user: "admin" });
await adminAction({ _action: "REFRESH" }, { user: "admin" });
assert.equal(await countPlusTierMembers(1), 10);
assert.equal(await countPlusTierMembers(2), 0);
},
assert.equal(await countPlusTierMembers(1), 10);
assert.equal(await countPlusTierMembers(2), 0);
},
);
PlusVoting("plus server skip flag ignored if for past season", async () => {
MockDate.set(new Date("2023-11-29T00:00:00.000Z"));
MockDate.set(new Date("2023-11-29T00:00:00.000Z"));
await Test.database.insertUsers(11);
await createLeaderboard(Array.from({ length: 11 }).map((_, i) => i + 1));
await Test.database.insertUsers(11);
await createLeaderboard(Array.from({ length: 11 }).map((_, i) => i + 1));
await db
.updateTable("User")
.set({ plusSkippedForSeasonNth: 0 })
.where("User.id", "=", 1)
.execute();
await db
.updateTable("User")
.set({ plusSkippedForSeasonNth: 0 })
.where("User.id", "=", 1)
.execute();
await adminAction({ _action: "REFRESH" }, { user: "admin" });
await adminAction({ _action: "REFRESH" }, { user: "admin" });
assert.equal(await countPlusTierMembers(1), 10);
assert.equal(await countPlusTierMembers(2), 1);
assert.equal(await countPlusTierMembers(1), 10);
assert.equal(await countPlusTierMembers(2), 1);
});
PlusVoting("ignores leaderboard while season is ongoing", async () => {
MockDate.set(new Date("2024-02-15T00:00:00.000Z"));
MockDate.set(new Date("2024-02-15T00:00:00.000Z"));
await Test.database.insertUsers(2);
await PlusVotingRepository.upsertMany([
voteArgs({
score: 1,
votedId: 1,
}),
]);
await createLeaderboard([2]);
await Test.database.insertUsers(2);
await PlusVotingRepository.upsertMany([
voteArgs({
score: 1,
votedId: 1,
}),
]);
await createLeaderboard([2]);
await adminAction({ _action: "REFRESH" }, { user: "admin" });
await adminAction({ _action: "REFRESH" }, { user: "admin" });
assert.equal(await countPlusTierMembers(), 1);
assert.equal(await countPlusTierMembers(2), 0);
assert.equal(await countPlusTierMembers(), 1);
assert.equal(await countPlusTierMembers(2), 0);
});
PlusVoting("leaderboard gives members to all tiers", async () => {
MockDate.set(new Date("2023-11-20T00:00:00.000Z"));
MockDate.set(new Date("2023-11-20T00:00:00.000Z"));
await Test.database.insertUsers(60);
await createLeaderboard(Array.from({ length: 60 }, (_, i) => i + 1));
await Test.database.insertUsers(60);
await createLeaderboard(Array.from({ length: 60 }, (_, i) => i + 1));
await adminAction({ _action: "REFRESH" }, { user: "admin" });
await adminAction({ _action: "REFRESH" }, { user: "admin" });
assert.ok((await countPlusTierMembers()) > 0);
assert.ok((await countPlusTierMembers(2)) > 0);
assert.ok((await countPlusTierMembers(3)) > 0);
assert.ok((await countPlusTierMembers()) > 0);
assert.ok((await countPlusTierMembers(2)) > 0);
assert.ok((await countPlusTierMembers(3)) > 0);
});
PlusVoting(
"gives membership if failed voting and is on the leaderboard",
async () => {
MockDate.set(new Date("2023-11-29T00:00:00.000Z"));
"gives membership if failed voting and is on the leaderboard",
async () => {
MockDate.set(new Date("2023-11-29T00:00:00.000Z"));
await Test.database.insertUsers(1);
await PlusVotingRepository.upsertMany([
voteArgs({
score: -1,
votedId: 1,
}),
]);
await createLeaderboard([1]);
await Test.database.insertUsers(1);
await PlusVotingRepository.upsertMany([
voteArgs({
score: -1,
votedId: 1,
}),
]);
await createLeaderboard([1]);
await adminAction({ _action: "REFRESH" }, { user: "admin" });
await adminAction({ _action: "REFRESH" }, { user: "admin" });
assert.equal(await countPlusTierMembers(1), 1);
},
assert.equal(await countPlusTierMembers(1), 1);
},
);
PlusVoting("members who fails voting drops one tier", async () => {
MockDate.set(new Date("2024-02-15T00:00:00.000Z"));
MockDate.set(new Date("2024-02-15T00:00:00.000Z"));
await Test.database.insertUsers(1);
await PlusVotingRepository.upsertMany([
voteArgs({
score: 1,
votedId: 1,
month: 11,
year: 2023,
}),
]);
await Test.database.insertUsers(1);
await PlusVotingRepository.upsertMany([
voteArgs({
score: 1,
votedId: 1,
month: 11,
year: 2023,
}),
]);
await PlusVotingRepository.upsertMany([
voteArgs({
score: -1,
votedId: 1,
month: 2,
year: 2024,
}),
]);
await PlusVotingRepository.upsertMany([
voteArgs({
score: -1,
votedId: 1,
month: 2,
year: 2024,
}),
]);
await adminAction({ _action: "REFRESH" }, { user: "admin" });
await adminAction({ _action: "REFRESH" }, { user: "admin" });
assert.equal(await countPlusTierMembers(2), 1);
assert.equal(await countPlusTierMembers(2), 1);
});
PlusVoting.run();

View File

@@ -1,9 +1,9 @@
import type { MetaFunction } from "@remix-run/node";
import {
Form,
useFetcher,
useLoaderData,
useNavigation,
Form,
useFetcher,
useLoaderData,
useNavigation,
} from "@remix-run/react";
import * as React from "react";
import { Button } from "~/components/Button";
@@ -13,7 +13,7 @@ import { SubmitButton } from "~/components/SubmitButton";
import { UserSearch } from "~/components/UserSearch";
import { useUser } from "~/features/auth/core/user";
import { isAdmin, isMod } from "~/permissions";
import { type SendouRouteHandle } from "~/utils/remix";
import type { SendouRouteHandle } from "~/utils/remix";
import { makeTitle } from "~/utils/strings";
import { SEED_URL, STOP_IMPERSONATING_URL, impersonateUrl } from "~/utils/urls";
@@ -22,311 +22,311 @@ import { loader } from "../loaders/admin.server";
export { action, loader };
export const meta: MetaFunction = () => {
return [{ title: makeTitle("Admin page") }];
return [{ title: makeTitle("Admin page") }];
};
export const handle: SendouRouteHandle = {
navItemName: "admin",
navItemName: "admin",
};
export default function AdminPage() {
const user = useUser();
const user = useUser();
return (
<Main className="stack lg">
{process.env.NODE_ENV !== "production" && <Seed />}
return (
<Main className="stack lg">
{process.env.NODE_ENV !== "production" && <Seed />}
{isMod(user) ? <LinkPlayer /> : null}
{isMod(user) ? <GiveArtist /> : null}
{isMod(user) ? <GiveVideoAdder /> : null}
{isMod(user) ? <LinkPlayer /> : null}
{isMod(user) ? <GiveArtist /> : null}
{isMod(user) ? <GiveVideoAdder /> : null}
{process.env.NODE_ENV !== "production" || isAdmin(user) ? (
<Impersonate />
) : null}
{isAdmin(user) ? <MigrateUser /> : null}
{isAdmin(user) ? <ForcePatron /> : null}
{isMod(user) ? <BanUser /> : null}
{isMod(user) ? <UnbanUser /> : null}
{isAdmin(user) ? <RefreshPlusTiers /> : null}
{isAdmin(user) ? <CleanUp /> : null}
</Main>
);
{process.env.NODE_ENV !== "production" || isAdmin(user) ? (
<Impersonate />
) : null}
{isAdmin(user) ? <MigrateUser /> : null}
{isAdmin(user) ? <ForcePatron /> : null}
{isMod(user) ? <BanUser /> : null}
{isMod(user) ? <UnbanUser /> : null}
{isAdmin(user) ? <RefreshPlusTiers /> : null}
{isAdmin(user) ? <CleanUp /> : null}
</Main>
);
}
function Impersonate() {
const [userId, setUserId] = React.useState<number>();
const { isImpersonating } = useLoaderData<typeof loader>();
const [userId, setUserId] = React.useState<number>();
const { isImpersonating } = useLoaderData<typeof loader>();
return (
<Form
method="post"
action={impersonateUrl(userId ?? 0)}
className="stack md"
reloadDocument
>
<h2>Impersonate user</h2>
<div>
<label>User to log in as</label>
<UserSearch
inputName="user"
onChange={(newUser) => setUserId(newUser.id)}
/>
</div>
<div className="stack horizontal md">
<Button type="submit" disabled={!userId}>
Go
</Button>
{isImpersonating ? (
<Button type="submit" formAction={STOP_IMPERSONATING_URL}>
Stop impersonating
</Button>
) : null}
</div>
</Form>
);
return (
<Form
method="post"
action={impersonateUrl(userId ?? 0)}
className="stack md"
reloadDocument
>
<h2>Impersonate user</h2>
<div>
<label>User to log in as</label>
<UserSearch
inputName="user"
onChange={(newUser) => setUserId(newUser.id)}
/>
</div>
<div className="stack horizontal md">
<Button type="submit" disabled={!userId}>
Go
</Button>
{isImpersonating ? (
<Button type="submit" formAction={STOP_IMPERSONATING_URL}>
Stop impersonating
</Button>
) : null}
</div>
</Form>
);
}
function MigrateUser() {
const [oldUserId, setOldUserId] = React.useState<number>();
const [newUserId, setNewUserId] = React.useState<number>();
const navigation = useNavigation();
const fetcher = useFetcher();
const [oldUserId, setOldUserId] = React.useState<number>();
const [newUserId, setNewUserId] = React.useState<number>();
const navigation = useNavigation();
const fetcher = useFetcher();
const submitButtonText =
navigation.state === "submitting"
? "Migrating..."
: navigation.state === "loading"
? "Migrated!"
: "Migrate";
const submitButtonText =
navigation.state === "submitting"
? "Migrating..."
: navigation.state === "loading"
? "Migrated!"
: "Migrate";
return (
<fetcher.Form className="stack md" method="post">
<h2>Migrate user data</h2>
<div className="stack horizontal md">
<div>
<label>Old user</label>
<UserSearch
inputName="old-user"
onChange={(newUser) => setOldUserId(newUser.id)}
/>
</div>
<div>
<label>New user</label>
<UserSearch
inputName="new-user"
onChange={(newUser) => setNewUserId(newUser.id)}
/>
</div>
</div>
<div className="stack horizontal md">
<SubmitButton
type="submit"
disabled={!oldUserId || !newUserId || navigation.state !== "idle"}
_action="MIGRATE"
state={fetcher.state}
>
{submitButtonText}
</SubmitButton>
</div>
</fetcher.Form>
);
return (
<fetcher.Form className="stack md" method="post">
<h2>Migrate user data</h2>
<div className="stack horizontal md">
<div>
<label>Old user</label>
<UserSearch
inputName="old-user"
onChange={(newUser) => setOldUserId(newUser.id)}
/>
</div>
<div>
<label>New user</label>
<UserSearch
inputName="new-user"
onChange={(newUser) => setNewUserId(newUser.id)}
/>
</div>
</div>
<div className="stack horizontal md">
<SubmitButton
type="submit"
disabled={!oldUserId || !newUserId || navigation.state !== "idle"}
_action="MIGRATE"
state={fetcher.state}
>
{submitButtonText}
</SubmitButton>
</div>
</fetcher.Form>
);
}
function LinkPlayer() {
const fetcher = useFetcher();
const fetcher = useFetcher();
return (
<fetcher.Form className="stack md" method="post">
<h2>Link player</h2>
<div className="stack horizontal md">
<div>
<label>User</label>
<UserSearch inputName="user" />
</div>
<div>
<label>Player ID</label>
<input type="number" name="playerId" />
</div>
</div>
<div className="stack horizontal md">
<SubmitButton type="submit" _action="LINK_PLAYER" state={fetcher.state}>
Link player
</SubmitButton>
</div>
</fetcher.Form>
);
return (
<fetcher.Form className="stack md" method="post">
<h2>Link player</h2>
<div className="stack horizontal md">
<div>
<label>User</label>
<UserSearch inputName="user" />
</div>
<div>
<label>Player ID</label>
<input type="number" name="playerId" />
</div>
</div>
<div className="stack horizontal md">
<SubmitButton type="submit" _action="LINK_PLAYER" state={fetcher.state}>
Link player
</SubmitButton>
</div>
</fetcher.Form>
);
}
function GiveArtist() {
const fetcher = useFetcher();
const fetcher = useFetcher();
return (
<fetcher.Form className="stack md" method="post">
<h2>Add as artist</h2>
<div className="stack horizontal md">
<div>
<label>User</label>
<UserSearch inputName="user" />
</div>
</div>
<div className="stack horizontal md">
<SubmitButton type="submit" _action="ARTIST" state={fetcher.state}>
Add as artist
</SubmitButton>
</div>
</fetcher.Form>
);
return (
<fetcher.Form className="stack md" method="post">
<h2>Add as artist</h2>
<div className="stack horizontal md">
<div>
<label>User</label>
<UserSearch inputName="user" />
</div>
</div>
<div className="stack horizontal md">
<SubmitButton type="submit" _action="ARTIST" state={fetcher.state}>
Add as artist
</SubmitButton>
</div>
</fetcher.Form>
);
}
function GiveVideoAdder() {
const fetcher = useFetcher();
const fetcher = useFetcher();
return (
<fetcher.Form className="stack md" method="post">
<h2>Give video adder</h2>
<div className="stack horizontal md">
<div>
<label>User</label>
<UserSearch inputName="user" />
</div>
</div>
<div className="stack horizontal md">
<SubmitButton type="submit" _action="VIDEO_ADDER" state={fetcher.state}>
Add as video adder
</SubmitButton>
</div>
</fetcher.Form>
);
return (
<fetcher.Form className="stack md" method="post">
<h2>Give video adder</h2>
<div className="stack horizontal md">
<div>
<label>User</label>
<UserSearch inputName="user" />
</div>
</div>
<div className="stack horizontal md">
<SubmitButton type="submit" _action="VIDEO_ADDER" state={fetcher.state}>
Add as video adder
</SubmitButton>
</div>
</fetcher.Form>
);
}
function ForcePatron() {
const fetcher = useFetcher();
const fetcher = useFetcher();
return (
<fetcher.Form className="stack md" method="post">
<h2>Force patron</h2>
<div className="stack horizontal md">
<div>
<label>User</label>
<UserSearch inputName="user" />
</div>
return (
<fetcher.Form className="stack md" method="post">
<h2>Force patron</h2>
<div className="stack horizontal md">
<div>
<label>User</label>
<UserSearch inputName="user" />
</div>
<div>
<label>Tier</label>
<select name="patronTier">
<option value="1">Support</option>
<option value="2">Supporter</option>
<option value="3">Supporter+</option>
</select>
</div>
<div>
<label>Tier</label>
<select name="patronTier">
<option value="1">Support</option>
<option value="2">Supporter</option>
<option value="3">Supporter+</option>
</select>
</div>
<div>
<label>Patron till</label>
<input name="patronTill" type="date" />
</div>
</div>
<div className="stack horizontal md">
<SubmitButton
type="submit"
_action="FORCE_PATRON"
state={fetcher.state}
>
Save
</SubmitButton>
</div>
</fetcher.Form>
);
<div>
<label>Patron till</label>
<input name="patronTill" type="date" />
</div>
</div>
<div className="stack horizontal md">
<SubmitButton
type="submit"
_action="FORCE_PATRON"
state={fetcher.state}
>
Save
</SubmitButton>
</div>
</fetcher.Form>
);
}
function BanUser() {
const fetcher = useFetcher();
const fetcher = useFetcher();
return (
<fetcher.Form className="stack md" method="post">
<h2 className="text-warning">Ban user</h2>
<div className="stack horizontal md">
<div>
<label>User</label>
<UserSearch inputName="user" />
</div>
return (
<fetcher.Form className="stack md" method="post">
<h2 className="text-warning">Ban user</h2>
<div className="stack horizontal md">
<div>
<label>User</label>
<UserSearch inputName="user" />
</div>
<div>
<label>Banned till</label>
<input name="duration" type="datetime-local" />
</div>
<div>
<label>Banned till</label>
<input name="duration" type="datetime-local" />
</div>
<div>
<label>Reason</label>
<input name="reason" type="text" />
</div>
</div>
<div className="stack horizontal md">
<SubmitButton type="submit" _action="BAN_USER" state={fetcher.state}>
Save
</SubmitButton>
</div>
</fetcher.Form>
);
<div>
<label>Reason</label>
<input name="reason" type="text" />
</div>
</div>
<div className="stack horizontal md">
<SubmitButton type="submit" _action="BAN_USER" state={fetcher.state}>
Save
</SubmitButton>
</div>
</fetcher.Form>
);
}
function UnbanUser() {
const fetcher = useFetcher();
const fetcher = useFetcher();
return (
<fetcher.Form className="stack md" method="post">
<h2 className="text-warning">Unban user</h2>
<div>
<label>User</label>
<UserSearch inputName="user" />
</div>
<div className="stack horizontal md">
<SubmitButton type="submit" _action="UNBAN_USER" state={fetcher.state}>
Save
</SubmitButton>
</div>
</fetcher.Form>
);
return (
<fetcher.Form className="stack md" method="post">
<h2 className="text-warning">Unban user</h2>
<div>
<label>User</label>
<UserSearch inputName="user" />
</div>
<div className="stack horizontal md">
<SubmitButton type="submit" _action="UNBAN_USER" state={fetcher.state}>
Save
</SubmitButton>
</div>
</fetcher.Form>
);
}
function RefreshPlusTiers() {
const fetcher = useFetcher();
const fetcher = useFetcher();
return (
<fetcher.Form method="post">
<h2>Refresh Plus Tiers</h2>
<SubmitButton type="submit" _action="REFRESH" state={fetcher.state}>
Refresh
</SubmitButton>
</fetcher.Form>
);
return (
<fetcher.Form method="post">
<h2>Refresh Plus Tiers</h2>
<SubmitButton type="submit" _action="REFRESH" state={fetcher.state}>
Refresh
</SubmitButton>
</fetcher.Form>
);
}
function CleanUp() {
const fetcher = useFetcher();
const fetcher = useFetcher();
return (
<fetcher.Form method="post">
<h2>DB Clean up</h2>
<SubmitButton type="submit" _action="CLEAN_UP" state={fetcher.state}>
Clean up
</SubmitButton>
</fetcher.Form>
);
return (
<fetcher.Form method="post">
<h2>DB Clean up</h2>
<SubmitButton type="submit" _action="CLEAN_UP" state={fetcher.state}>
Clean up
</SubmitButton>
</fetcher.Form>
);
}
function Seed() {
const fetcher = useFetcher();
const fetcher = useFetcher();
return (
<fetcher.Form
className="stack md items-start"
method="post"
action={SEED_URL}
>
<h2>Seed</h2>
<SubmitButton state={fetcher.state}>Seed</SubmitButton>
</fetcher.Form>
);
return (
<fetcher.Form
className="stack md items-start"
method="post"
action={SEED_URL}
>
<h2>Seed</h2>
<SubmitButton state={fetcher.state}>Seed</SubmitButton>
</fetcher.Form>
);
}
export const ErrorBoundary = Catcher;