mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-06-13 04:31:33 -05:00
Audit log initial
This commit is contained in:
parent
e1278ef57f
commit
7e11bc0dee
|
|
@ -866,6 +866,43 @@ export interface TournamentTeamMember {
|
|||
isLooking: Generated<DBBoolean>;
|
||||
}
|
||||
|
||||
/** Stable shadow of a tournament team's identity that survives the team's hard-deletion, so the audit log can still resolve its name. */
|
||||
export interface TournamentTeamHistory {
|
||||
// xxx: maybe we need soft delete instead. it's possible the same id gets reused... alternative TournamentTeamHistory has its own tournamentTeamId that TournamentAuditLog refers to (also keep tournamentTeamId)
|
||||
/** Mirrors the original `TournamentTeam.id`. Not a live foreign key so it is not cascade-deleted with the team. */
|
||||
tournamentTeamId: number;
|
||||
tournamentId: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const TOURNAMENT_AUDIT_LOG_TYPES = [
|
||||
"MEMBER_ADDED",
|
||||
"MEMBER_REMOVED",
|
||||
"TEAM_REGISTERED",
|
||||
"TEAM_UNREGISTERED",
|
||||
"TEAM_CHECKED_IN",
|
||||
"TEAM_CHECKED_OUT",
|
||||
"TEAM_DROPPED_OUT",
|
||||
"TEAM_DROP_OUT_UNDONE",
|
||||
] as const;
|
||||
|
||||
export interface TournamentAuditLog {
|
||||
id: GeneratedAlways<number>;
|
||||
tournamentId: number;
|
||||
type: (typeof TOURNAMENT_AUDIT_LOG_TYPES)[number];
|
||||
/** The user who performed the action. */
|
||||
actorUserId: number;
|
||||
/** The affected member, for member-level events. `null` for team-level events. */
|
||||
subjectUserId: number | null;
|
||||
tournamentTeamId: number | null;
|
||||
metadata: JSONColumnTypeNullable<TournamentAuditLogMetadata>;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface TournamentAuditLogMetadata {
|
||||
bracketIdx?: number;
|
||||
}
|
||||
|
||||
export interface TournamentOrganization {
|
||||
id: GeneratedAlways<number>;
|
||||
name: string;
|
||||
|
|
@ -1436,6 +1473,8 @@ export interface DB {
|
|||
TournamentTeam: TournamentTeam;
|
||||
TournamentTeamCheckIn: TournamentTeamCheckIn;
|
||||
TournamentTeamMember: TournamentTeamMember;
|
||||
TournamentTeamHistory: TournamentTeamHistory;
|
||||
TournamentAuditLog: TournamentAuditLog;
|
||||
TournamentOrganization: TournamentOrganization;
|
||||
TournamentOrganizationMember: TournamentOrganizationMember;
|
||||
TournamentOrganizationBadge: TournamentOrganizationBadge;
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ export const action: ActionFunction = async ({ params, request }) => {
|
|||
);
|
||||
|
||||
await TournamentTeamRepository.checkIn(teamMemberOf.id, {
|
||||
actorUserId: user.id,
|
||||
bracketIdx: data.bracketIdx,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,240 @@
|
|||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import { db } from "~/db/sql";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import { dbInsertUsers, dbReset } from "~/utils/Test";
|
||||
import * as TournamentAuditLogRepository from "./TournamentAuditLogRepository.server";
|
||||
|
||||
const createTournament = () =>
|
||||
db
|
||||
.insertInto("Tournament")
|
||||
.values({
|
||||
mapPickingStyle: "TO",
|
||||
settings: JSON.stringify({ bracketProgression: [] }),
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
const createTeam = (tournamentId: number, name: string) =>
|
||||
db
|
||||
.insertInto("TournamentTeam")
|
||||
.values({
|
||||
tournamentId,
|
||||
name,
|
||||
inviteCode: `inv-${tournamentId}-${name}`,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
const insertEvent = (args: {
|
||||
type: Tables["TournamentAuditLog"]["type"];
|
||||
actorUserId: number;
|
||||
tournamentTeamId: number;
|
||||
subjectUserId?: number;
|
||||
metadata?: { bracketIdx?: number };
|
||||
}) =>
|
||||
db
|
||||
.transaction()
|
||||
.execute((trx) => TournamentAuditLogRepository.insert(trx, args));
|
||||
|
||||
describe("TournamentAuditLogRepository", () => {
|
||||
beforeEach(async () => {
|
||||
await dbInsertUsers(3);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
test("insert creates a stable history row from the live team", async () => {
|
||||
const tournament = await createTournament();
|
||||
const team = await createTeam(tournament.id, "Team Olive");
|
||||
|
||||
await insertEvent({
|
||||
type: "TEAM_REGISTERED",
|
||||
actorUserId: 1,
|
||||
tournamentTeamId: team.id,
|
||||
});
|
||||
|
||||
const teams = await TournamentAuditLogRepository.findTeamsByTournamentId(
|
||||
tournament.id,
|
||||
);
|
||||
|
||||
expect(teams).toHaveLength(1);
|
||||
expect(teams[0].id).toBe(team.id);
|
||||
expect(teams[0].name).toBe("Team Olive");
|
||||
});
|
||||
|
||||
test("findByTournamentId returns events newest first with resolved relations", async () => {
|
||||
const tournament = await createTournament();
|
||||
const team = await createTeam(tournament.id, "Team Olive");
|
||||
|
||||
await insertEvent({
|
||||
type: "TEAM_REGISTERED",
|
||||
actorUserId: 1,
|
||||
tournamentTeamId: team.id,
|
||||
subjectUserId: 1,
|
||||
});
|
||||
await insertEvent({
|
||||
type: "MEMBER_ADDED",
|
||||
actorUserId: 1,
|
||||
tournamentTeamId: team.id,
|
||||
subjectUserId: 2,
|
||||
});
|
||||
|
||||
const events = await TournamentAuditLogRepository.findByTournamentId({
|
||||
tournamentId: tournament.id,
|
||||
limit: 30,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
expect(events).toHaveLength(2);
|
||||
// newest first
|
||||
expect(events[0].type).toBe("MEMBER_ADDED");
|
||||
expect(events[0].actor?.id).toBe(1);
|
||||
expect(events[0].subject?.id).toBe(2);
|
||||
expect(events[0].team?.name).toBe("Team Olive");
|
||||
expect(events[1].type).toBe("TEAM_REGISTERED");
|
||||
});
|
||||
|
||||
test("team name survives the team being deleted", async () => {
|
||||
const tournament = await createTournament();
|
||||
const team = await createTeam(tournament.id, "Team Olive");
|
||||
|
||||
await insertEvent({
|
||||
type: "TEAM_UNREGISTERED",
|
||||
actorUserId: 1,
|
||||
tournamentTeamId: team.id,
|
||||
});
|
||||
|
||||
await db
|
||||
.deleteFrom("TournamentTeam")
|
||||
.where("TournamentTeam.id", "=", team.id)
|
||||
.execute();
|
||||
|
||||
const events = await TournamentAuditLogRepository.findByTournamentId({
|
||||
tournamentId: tournament.id,
|
||||
limit: 30,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].team?.name).toBe("Team Olive");
|
||||
});
|
||||
|
||||
test("filters by event type and by team", async () => {
|
||||
const tournament = await createTournament();
|
||||
const teamA = await createTeam(tournament.id, "Team A");
|
||||
const teamB = await createTeam(tournament.id, "Team B");
|
||||
|
||||
await insertEvent({
|
||||
type: "TEAM_REGISTERED",
|
||||
actorUserId: 1,
|
||||
tournamentTeamId: teamA.id,
|
||||
});
|
||||
await insertEvent({
|
||||
type: "TEAM_CHECKED_IN",
|
||||
actorUserId: 1,
|
||||
tournamentTeamId: teamA.id,
|
||||
});
|
||||
await insertEvent({
|
||||
type: "TEAM_REGISTERED",
|
||||
actorUserId: 1,
|
||||
tournamentTeamId: teamB.id,
|
||||
});
|
||||
|
||||
const byType = await TournamentAuditLogRepository.findByTournamentId({
|
||||
tournamentId: tournament.id,
|
||||
type: "TEAM_REGISTERED",
|
||||
limit: 30,
|
||||
offset: 0,
|
||||
});
|
||||
expect(byType).toHaveLength(2);
|
||||
|
||||
const byTeam = await TournamentAuditLogRepository.findByTournamentId({
|
||||
tournamentId: tournament.id,
|
||||
tournamentTeamId: teamA.id,
|
||||
limit: 30,
|
||||
offset: 0,
|
||||
});
|
||||
expect(byTeam).toHaveLength(2);
|
||||
|
||||
const count = await TournamentAuditLogRepository.countByTournamentId({
|
||||
tournamentId: tournament.id,
|
||||
type: "TEAM_REGISTERED",
|
||||
});
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
|
||||
test("paginates via limit and offset", async () => {
|
||||
const tournament = await createTournament();
|
||||
const team = await createTeam(tournament.id, "Team Olive");
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await insertEvent({
|
||||
type: "TEAM_CHECKED_IN",
|
||||
actorUserId: 1,
|
||||
tournamentTeamId: team.id,
|
||||
});
|
||||
}
|
||||
|
||||
const firstPage = await TournamentAuditLogRepository.findByTournamentId({
|
||||
tournamentId: tournament.id,
|
||||
limit: 2,
|
||||
offset: 0,
|
||||
});
|
||||
const secondPage = await TournamentAuditLogRepository.findByTournamentId({
|
||||
tournamentId: tournament.id,
|
||||
limit: 2,
|
||||
offset: 2,
|
||||
});
|
||||
|
||||
expect(firstPage).toHaveLength(2);
|
||||
expect(secondPage).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("stores and reads back metadata", async () => {
|
||||
const tournament = await createTournament();
|
||||
const team = await createTeam(tournament.id, "Team Olive");
|
||||
|
||||
await insertEvent({
|
||||
type: "TEAM_CHECKED_IN",
|
||||
actorUserId: 1,
|
||||
tournamentTeamId: team.id,
|
||||
metadata: { bracketIdx: 2 },
|
||||
});
|
||||
|
||||
const events = await TournamentAuditLogRepository.findByTournamentId({
|
||||
tournamentId: tournament.id,
|
||||
limit: 30,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
expect(events[0].metadata?.bracketIdx).toBe(2);
|
||||
});
|
||||
|
||||
test("updateTeamHistoryName keeps the preserved name current", async () => {
|
||||
const tournament = await createTournament();
|
||||
const team = await createTeam(tournament.id, "Old Name");
|
||||
|
||||
await insertEvent({
|
||||
type: "TEAM_REGISTERED",
|
||||
actorUserId: 1,
|
||||
tournamentTeamId: team.id,
|
||||
});
|
||||
|
||||
await db.transaction().execute((trx) =>
|
||||
TournamentAuditLogRepository.updateTeamHistoryName(trx, {
|
||||
tournamentTeamId: team.id,
|
||||
name: "New Name",
|
||||
}),
|
||||
);
|
||||
|
||||
const events = await TournamentAuditLogRepository.findByTournamentId({
|
||||
tournamentId: tournament.id,
|
||||
limit: 30,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
expect(events[0].team?.name).toBe("New Name");
|
||||
});
|
||||
});
|
||||
196
app/features/tournament/TournamentAuditLogRepository.server.ts
Normal file
196
app/features/tournament/TournamentAuditLogRepository.server.ts
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
import type { Transaction } from "kysely";
|
||||
import { jsonObjectFrom } from "kysely/helpers/sqlite";
|
||||
import { db } from "~/db/sql";
|
||||
import type { DB, Tables, TournamentAuditLogMetadata } from "~/db/tables";
|
||||
import { databaseTimestampNow } from "~/utils/dates";
|
||||
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
|
||||
|
||||
export const AUDIT_LOG_PAGE_SIZE = 30;
|
||||
|
||||
type TournamentAuditLogType = Tables["TournamentAuditLog"]["type"];
|
||||
|
||||
// xxx: can we use TablesInsertable + Pick
|
||||
interface InsertArgs {
|
||||
type: TournamentAuditLogType;
|
||||
/** The user who performed the action. */
|
||||
actorUserId: number;
|
||||
/** The team the event concerns. Its identity is preserved in `TournamentTeamHistory`. */
|
||||
tournamentTeamId: number;
|
||||
/** The affected member, for member-level events. */
|
||||
subjectUserId?: number | null;
|
||||
metadata?: TournamentAuditLogMetadata | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts an audit log event within the caller's transaction (so it commits or
|
||||
* rolls back atomically with the mutation it records). Ensures a stable
|
||||
* `TournamentTeamHistory` row exists for the team, so the event remains readable
|
||||
* even after the team is hard-deleted.
|
||||
*/
|
||||
export async function insert(trx: Transaction<DB>, args: InsertArgs) { // xxx: audit codebase, trx as second arg?
|
||||
await trx
|
||||
.insertInto("TournamentTeamHistory")
|
||||
.columns(["tournamentTeamId", "tournamentId", "name"])
|
||||
.expression((eb) =>
|
||||
eb
|
||||
.selectFrom("TournamentTeam")
|
||||
.select([
|
||||
"TournamentTeam.id",
|
||||
"TournamentTeam.tournamentId",
|
||||
"TournamentTeam.name",
|
||||
])
|
||||
.where("TournamentTeam.id", "=", args.tournamentTeamId),
|
||||
)
|
||||
.onConflict((oc) => oc.column("tournamentTeamId").doNothing())
|
||||
.execute();
|
||||
|
||||
const { tournamentId } = await trx
|
||||
.selectFrom("TournamentTeamHistory")
|
||||
.select("TournamentTeamHistory.tournamentId")
|
||||
.where("TournamentTeamHistory.tournamentTeamId", "=", args.tournamentTeamId)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
await trx
|
||||
.insertInto("TournamentAuditLog")
|
||||
.values({
|
||||
tournamentId,
|
||||
type: args.type,
|
||||
actorUserId: args.actorUserId,
|
||||
subjectUserId: args.subjectUserId ?? null,
|
||||
tournamentTeamId: args.tournamentTeamId,
|
||||
metadata: args.metadata ? JSON.stringify(args.metadata) : null,
|
||||
createdAt: databaseTimestampNow(),
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the team's preserved name current after a rename. No-op when the team
|
||||
* has no history row yet (it will be created with the up-to-date name on its
|
||||
* first audited event).
|
||||
*/
|
||||
export function updateTeamHistoryName(
|
||||
trx: Transaction<DB>,
|
||||
{ tournamentTeamId, name }: { tournamentTeamId: number; name: string },
|
||||
) {
|
||||
return trx
|
||||
.updateTable("TournamentTeamHistory")
|
||||
.set({ name })
|
||||
.where("TournamentTeamHistory.tournamentTeamId", "=", tournamentTeamId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a page of audit log events for a tournament, newest first, optionally
|
||||
* filtered by event type and/or team. Resolves the actor, the affected member
|
||||
* (when present) and the team name (preserved even for deleted teams).
|
||||
*/
|
||||
export function findByTournamentId({
|
||||
tournamentId,
|
||||
type,
|
||||
tournamentTeamId,
|
||||
limit,
|
||||
offset,
|
||||
}: {
|
||||
tournamentId: number;
|
||||
type?: TournamentAuditLogType;
|
||||
tournamentTeamId?: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}) {
|
||||
let query = db
|
||||
.selectFrom("TournamentAuditLog")
|
||||
.select((eb) => [
|
||||
"TournamentAuditLog.id",
|
||||
"TournamentAuditLog.type",
|
||||
"TournamentAuditLog.createdAt",
|
||||
"TournamentAuditLog.metadata",
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom("User")
|
||||
.select(COMMON_USER_FIELDS)
|
||||
.whereRef("User.id", "=", "TournamentAuditLog.actorUserId"),
|
||||
).as("actor"),
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom("User")
|
||||
.select(COMMON_USER_FIELDS)
|
||||
.whereRef("User.id", "=", "TournamentAuditLog.subjectUserId"),
|
||||
).as("subject"),
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom("TournamentTeamHistory")
|
||||
.select([
|
||||
"TournamentTeamHistory.tournamentTeamId as id",
|
||||
"TournamentTeamHistory.name",
|
||||
])
|
||||
.whereRef(
|
||||
"TournamentTeamHistory.tournamentTeamId",
|
||||
"=",
|
||||
"TournamentAuditLog.tournamentTeamId",
|
||||
),
|
||||
).as("team"),
|
||||
])
|
||||
.where("TournamentAuditLog.tournamentId", "=", tournamentId)
|
||||
.orderBy("TournamentAuditLog.createdAt", "desc")
|
||||
.orderBy("TournamentAuditLog.id", "desc")
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
|
||||
if (type) {
|
||||
query = query.where("TournamentAuditLog.type", "=", type);
|
||||
}
|
||||
if (typeof tournamentTeamId === "number") {
|
||||
query = query.where(
|
||||
"TournamentAuditLog.tournamentTeamId",
|
||||
"=",
|
||||
tournamentTeamId,
|
||||
);
|
||||
}
|
||||
|
||||
return query.execute();
|
||||
}
|
||||
|
||||
/** Counts audit log events for a tournament matching the same optional filters as {@link findByTournamentId}. Used for pagination. */
|
||||
export async function countByTournamentId({
|
||||
tournamentId,
|
||||
type,
|
||||
tournamentTeamId,
|
||||
}: {
|
||||
tournamentId: number;
|
||||
type?: TournamentAuditLogType;
|
||||
tournamentTeamId?: number;
|
||||
}) {
|
||||
let query = db
|
||||
.selectFrom("TournamentAuditLog")
|
||||
.select((eb) => eb.fn.countAll<number>().as("count"))
|
||||
.where("TournamentAuditLog.tournamentId", "=", tournamentId);
|
||||
|
||||
if (type) {
|
||||
query = query.where("TournamentAuditLog.type", "=", type);
|
||||
}
|
||||
if (typeof tournamentTeamId === "number") {
|
||||
query = query.where(
|
||||
"TournamentAuditLog.tournamentTeamId",
|
||||
"=",
|
||||
tournamentTeamId,
|
||||
);
|
||||
}
|
||||
|
||||
const result = await query.executeTakeFirstOrThrow();
|
||||
|
||||
return result.count;
|
||||
}
|
||||
|
||||
/** Returns every team (including deleted ones) that has appeared in the tournament's audit log, for the team filter dropdown. */
|
||||
export function findTeamsByTournamentId(tournamentId: number) {
|
||||
return db
|
||||
.selectFrom("TournamentTeamHistory")
|
||||
.select([
|
||||
"TournamentTeamHistory.tournamentTeamId as id",
|
||||
"TournamentTeamHistory.name",
|
||||
])
|
||||
.where("TournamentTeamHistory.tournamentId", "=", tournamentId)
|
||||
.orderBy("TournamentTeamHistory.name", "asc")
|
||||
.execute();
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { flatZip } from "~/utils/arrays";
|
|||
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import { shortNanoid } from "~/utils/id";
|
||||
import invariant from "~/utils/invariant";
|
||||
import * as TournamentAuditLogRepository from "./TournamentAuditLogRepository.server";
|
||||
|
||||
export function setActiveRoster({
|
||||
teamId,
|
||||
|
|
@ -111,11 +112,15 @@ export function create({
|
|||
team,
|
||||
avatarFileName,
|
||||
userId,
|
||||
actorUserId,
|
||||
tournamentId,
|
||||
}: {
|
||||
team: Pick<Tables["TournamentTeam"], "name" | "prefersNotToHost" | "teamId">;
|
||||
avatarFileName?: string;
|
||||
/** The user who becomes the team owner. */
|
||||
userId: number;
|
||||
/** The user performing the registration (differs from `userId` when an organizer registers a team for someone). */
|
||||
actorUserId: number;
|
||||
tournamentId: number;
|
||||
}) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
|
|
@ -152,6 +157,13 @@ export function create({
|
|||
})
|
||||
.execute();
|
||||
|
||||
await TournamentAuditLogRepository.insert(trx, {
|
||||
type: "TEAM_REGISTERED",
|
||||
actorUserId,
|
||||
tournamentTeamId: tournamentTeam.id,
|
||||
subjectUserId: userId,
|
||||
});
|
||||
|
||||
return tournamentTeam;
|
||||
});
|
||||
}
|
||||
|
|
@ -317,6 +329,11 @@ export function update({
|
|||
})
|
||||
.where("TournamentTeam.id", "=", team.id)
|
||||
.execute();
|
||||
|
||||
await TournamentAuditLogRepository.updateTeamHistoryName(trx, {
|
||||
tournamentTeamId: team.id,
|
||||
name: team.name,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -416,9 +433,9 @@ export function updateAbDivisions(
|
|||
*/
|
||||
export function checkIn(
|
||||
tournamentTeamId: number,
|
||||
options?: { bracketIdx: number },
|
||||
options: { actorUserId: number; bracketIdx?: number },
|
||||
) {
|
||||
const bracketIdx = options?.bracketIdx ?? null;
|
||||
const bracketIdx = options.bracketIdx ?? null;
|
||||
|
||||
return db.transaction().execute(async (trx) => {
|
||||
let query = trx
|
||||
|
|
@ -446,15 +463,24 @@ export function checkIn(
|
|||
bracketIdx,
|
||||
})
|
||||
.execute();
|
||||
|
||||
await TournamentAuditLogRepository.insert(trx, {
|
||||
type: "TEAM_CHECKED_IN",
|
||||
actorUserId: options.actorUserId,
|
||||
tournamentTeamId,
|
||||
metadata: typeof bracketIdx === "number" ? { bracketIdx } : null,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function checkOut({
|
||||
tournamentTeamId,
|
||||
bracketIdx,
|
||||
actorUserId,
|
||||
}: {
|
||||
tournamentTeamId: number;
|
||||
bracketIdx: number | null;
|
||||
actorUserId: number;
|
||||
}) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
let query = trx
|
||||
|
|
@ -478,6 +504,13 @@ export function checkOut({
|
|||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
await TournamentAuditLogRepository.insert(trx, {
|
||||
type: "TEAM_CHECKED_OUT",
|
||||
actorUserId,
|
||||
tournamentTeamId,
|
||||
metadata: typeof bracketIdx === "number" ? { bracketIdx } : null,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -488,21 +521,30 @@ export function updateName({
|
|||
tournamentTeamId: number;
|
||||
name: string;
|
||||
}) {
|
||||
return db
|
||||
.updateTable("TournamentTeam")
|
||||
.set({
|
||||
return db.transaction().execute(async (trx) => {
|
||||
await trx
|
||||
.updateTable("TournamentTeam")
|
||||
.set({
|
||||
name,
|
||||
})
|
||||
.where("id", "=", tournamentTeamId)
|
||||
.execute();
|
||||
|
||||
await TournamentAuditLogRepository.updateTeamHistoryName(trx, {
|
||||
tournamentTeamId,
|
||||
name,
|
||||
})
|
||||
.where("id", "=", tournamentTeamId)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function dropOut({
|
||||
tournamentTeamId,
|
||||
previewBracketIdxs,
|
||||
actorUserId,
|
||||
}: {
|
||||
tournamentTeamId: number;
|
||||
previewBracketIdxs: number[];
|
||||
actorUserId: number;
|
||||
}) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
await trx
|
||||
|
|
@ -518,17 +560,34 @@ export function dropOut({
|
|||
})
|
||||
.where("id", "=", tournamentTeamId)
|
||||
.execute();
|
||||
|
||||
await TournamentAuditLogRepository.insert(trx, {
|
||||
type: "TEAM_DROPPED_OUT",
|
||||
actorUserId,
|
||||
tournamentTeamId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function undoDropOut(tournamentTeamId: number) {
|
||||
return db
|
||||
.updateTable("TournamentTeam")
|
||||
.set({
|
||||
droppedOut: 0,
|
||||
})
|
||||
.where("id", "=", tournamentTeamId)
|
||||
.execute();
|
||||
export function undoDropOut(
|
||||
tournamentTeamId: number,
|
||||
{ actorUserId }: { actorUserId: number },
|
||||
) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
await trx
|
||||
.updateTable("TournamentTeam")
|
||||
.set({
|
||||
droppedOut: 0,
|
||||
})
|
||||
.where("id", "=", tournamentTeamId)
|
||||
.execute();
|
||||
|
||||
await TournamentAuditLogRepository.insert(trx, {
|
||||
type: "TEAM_DROP_OUT_UNDONE",
|
||||
actorUserId,
|
||||
tournamentTeamId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function join({
|
||||
|
|
@ -536,21 +595,36 @@ export function join({
|
|||
whatToDoWithPreviousTeam,
|
||||
newTeamId,
|
||||
userId,
|
||||
actorUserId,
|
||||
checkOutTeam = false,
|
||||
}: {
|
||||
previousTeamId?: number;
|
||||
whatToDoWithPreviousTeam?: "LEAVE" | "DELETE";
|
||||
newTeamId: number;
|
||||
/** The user joining the team. */
|
||||
userId: number;
|
||||
/** The user performing the action (differs from `userId` when an owner or organizer adds someone). */
|
||||
actorUserId: number;
|
||||
checkOutTeam?: boolean;
|
||||
}) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
if (whatToDoWithPreviousTeam === "DELETE") {
|
||||
await TournamentAuditLogRepository.insert(trx, {
|
||||
type: "TEAM_UNREGISTERED",
|
||||
actorUserId,
|
||||
tournamentTeamId: previousTeamId!,
|
||||
});
|
||||
await trx
|
||||
.deleteFrom("TournamentTeam")
|
||||
.where("TournamentTeam.id", "=", previousTeamId!)
|
||||
.execute();
|
||||
} else if (whatToDoWithPreviousTeam === "LEAVE") {
|
||||
await TournamentAuditLogRepository.insert(trx, {
|
||||
type: "MEMBER_REMOVED",
|
||||
actorUserId,
|
||||
tournamentTeamId: previousTeamId!,
|
||||
subjectUserId: userId,
|
||||
});
|
||||
await trx
|
||||
.deleteFrom("TournamentTeamMember")
|
||||
.where("TournamentTeamMember.tournamentTeamId", "=", previousTeamId!)
|
||||
|
|
@ -587,11 +661,27 @@ export function join({
|
|||
inGameName,
|
||||
})
|
||||
.execute();
|
||||
|
||||
await TournamentAuditLogRepository.insert(trx, {
|
||||
type: "MEMBER_ADDED",
|
||||
actorUserId,
|
||||
tournamentTeamId: newTeamId,
|
||||
subjectUserId: userId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function del(tournamentTeamId: number) {
|
||||
export function del(
|
||||
tournamentTeamId: number,
|
||||
{ actorUserId }: { actorUserId: number },
|
||||
) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
await TournamentAuditLogRepository.insert(trx, {
|
||||
type: "TEAM_UNREGISTERED",
|
||||
actorUserId,
|
||||
tournamentTeamId,
|
||||
});
|
||||
|
||||
await trx
|
||||
.deleteFrom("MapPoolMap")
|
||||
.where("MapPoolMap.tournamentTeamId", "=", tournamentTeamId)
|
||||
|
|
@ -604,12 +694,31 @@ export function del(tournamentTeamId: number) {
|
|||
});
|
||||
}
|
||||
|
||||
export function leave({ teamId, userId }: { teamId: number; userId: number }) {
|
||||
return db
|
||||
.deleteFrom("TournamentTeamMember")
|
||||
.where("TournamentTeamMember.tournamentTeamId", "=", teamId)
|
||||
.where("TournamentTeamMember.userId", "=", userId)
|
||||
.execute();
|
||||
export function leave({
|
||||
teamId,
|
||||
userId,
|
||||
actorUserId,
|
||||
}: {
|
||||
teamId: number;
|
||||
/** The member leaving the team. */
|
||||
userId: number;
|
||||
/** The user performing the action (differs from `userId` when an owner or organizer removes someone). */
|
||||
actorUserId: number;
|
||||
}) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
await TournamentAuditLogRepository.insert(trx, {
|
||||
type: "MEMBER_REMOVED",
|
||||
actorUserId,
|
||||
tournamentTeamId: teamId,
|
||||
subjectUserId: userId,
|
||||
});
|
||||
|
||||
await trx
|
||||
.deleteFrom("TournamentTeamMember")
|
||||
.where("TournamentTeamMember.tournamentTeamId", "=", teamId)
|
||||
.where("TournamentTeamMember.userId", "=", userId)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
export function transferOwnership(
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
teamId: null,
|
||||
},
|
||||
userId: data.userId,
|
||||
actorUserId: user.id,
|
||||
tournamentId,
|
||||
});
|
||||
await TournamentLFGRepository.leaveLfg({
|
||||
|
|
@ -141,11 +142,11 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
invariant(bracket, "Invalid bracket idx");
|
||||
errorToastIfFalsy(bracket.preview, "Bracket has been started");
|
||||
|
||||
await TournamentTeamRepository.checkIn(
|
||||
data.teamId,
|
||||
await TournamentTeamRepository.checkIn(data.teamId, {
|
||||
actorUserId: user.id,
|
||||
// no sources = regular check in
|
||||
bracket.sources ? { bracketIdx: data.bracketIdx } : undefined,
|
||||
);
|
||||
bracketIdx: bracket.sources ? data.bracketIdx : undefined,
|
||||
});
|
||||
|
||||
message = "Checked team in";
|
||||
break;
|
||||
|
|
@ -165,6 +166,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
|
||||
await TournamentTeamRepository.checkOut({
|
||||
tournamentTeamId: data.teamId,
|
||||
actorUserId: user.id,
|
||||
// no sources = regular check in
|
||||
bracketIdx: !bracket.sources ? null : data.bracketIdx,
|
||||
});
|
||||
|
|
@ -206,6 +208,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
await TournamentTeamRepository.leave({
|
||||
userId: data.memberId,
|
||||
teamId: team.id,
|
||||
actorUserId: user.id,
|
||||
});
|
||||
|
||||
ShowcaseTournaments.removeFromCached({
|
||||
|
|
@ -256,6 +259,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
});
|
||||
await TournamentTeamRepository.join({
|
||||
userId: data.userId,
|
||||
actorUserId: user.id,
|
||||
newTeamId: team.id,
|
||||
previousTeamId: previousTeam?.id,
|
||||
// this team is not checked in & tournament started, so we can simply delete it
|
||||
|
|
@ -300,7 +304,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
errorToastIfFalsy(team, "Invalid team id");
|
||||
errorToastIfFalsy(!tournament.hasStarted, "Tournament has started");
|
||||
|
||||
await TournamentTeamRepository.del(team.id);
|
||||
await TournamentTeamRepository.del(team.id, { actorUserId: user.id });
|
||||
|
||||
for (const member of team.members) {
|
||||
ShowcaseTournaments.removeFromCached({
|
||||
|
|
@ -399,6 +403,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
|
||||
await TournamentTeamRepository.dropOut({
|
||||
tournamentTeamId: data.teamId,
|
||||
actorUserId: user.id,
|
||||
previewBracketIdxs: tournament.brackets.flatMap((b, idx) =>
|
||||
b.preview ? idx : [],
|
||||
),
|
||||
|
|
@ -427,7 +432,9 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
case "UNDO_DROP_TEAM_OUT": {
|
||||
validateIsTournamentOrganizer();
|
||||
|
||||
await TournamentTeamRepository.undoDropOut(data.teamId);
|
||||
await TournamentTeamRepository.undoDropOut(data.teamId, {
|
||||
actorUserId: user.id,
|
||||
});
|
||||
|
||||
message = "Team drop out undone";
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
await TournamentLFGRepository.leaveLfg({ userId: user.id, tournamentId });
|
||||
await TournamentTeamRepository.join({
|
||||
userId: user.id,
|
||||
actorUserId: user.id,
|
||||
newTeamId: teamToJoin.id,
|
||||
previousTeamId: previousTeam?.id,
|
||||
// making sure they aren't unfilling one checking in condition i.e. having full roster
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
teamId: data.teamId ?? null,
|
||||
},
|
||||
userId: user.id,
|
||||
actorUserId: user.id,
|
||||
tournamentId,
|
||||
avatarFileName,
|
||||
});
|
||||
|
|
@ -165,6 +166,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
await TournamentTeamRepository.leave({
|
||||
teamId: ownTeam.id,
|
||||
userId: data.userId,
|
||||
actorUserId: user.id,
|
||||
});
|
||||
|
||||
ShowcaseTournaments.removeFromCached({
|
||||
|
|
@ -191,6 +193,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
await TournamentTeamRepository.leave({
|
||||
teamId: teamMemberOf.id,
|
||||
userId: user.id,
|
||||
actorUserId: user.id,
|
||||
});
|
||||
|
||||
ShowcaseTournaments.removeFromCached({
|
||||
|
|
@ -244,7 +247,9 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
`Can't check-in - ${tournament.checkInConditionsFulfilledByTeamId(teamMemberOf.id).reason}`,
|
||||
);
|
||||
|
||||
await TournamentTeamRepository.checkIn(teamMemberOf.id);
|
||||
await TournamentTeamRepository.checkIn(teamMemberOf.id, {
|
||||
actorUserId: user.id,
|
||||
});
|
||||
logger.info(
|
||||
`Checking in (success): tournament team id: ${teamMemberOf.id} - user id: ${user.id} - tournament id: ${tournamentId}`,
|
||||
);
|
||||
|
|
@ -286,6 +291,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
});
|
||||
await TournamentTeamRepository.join({
|
||||
userId: data.userId,
|
||||
actorUserId: user.id,
|
||||
newTeamId: ownTeam.id,
|
||||
});
|
||||
|
||||
|
|
@ -330,7 +336,9 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
"Unregistering from leagues is not possible after registration has closed",
|
||||
);
|
||||
|
||||
await TournamentTeamRepository.del(ownTeam.id);
|
||||
await TournamentTeamRepository.del(ownTeam.id, {
|
||||
actorUserId: user.id,
|
||||
});
|
||||
|
||||
for (const member of ownTeam.members) {
|
||||
ShowcaseTournaments.removeFromCached({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
.userCell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--s-1-5);
|
||||
white-space: nowrap;
|
||||
}
|
||||
211
app/features/tournament/components/TournamentAdminAuditLog.tsx
Normal file
211
app/features/tournament/components/TournamentAdminAuditLog.tsx
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useLoaderData, useSearchParams } from "react-router";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { Label } from "~/components/Label";
|
||||
import { LocaleTime } from "~/components/LocaleTime";
|
||||
import { Pagination } from "~/components/Pagination";
|
||||
import { Table } from "~/components/Table";
|
||||
import { TOURNAMENT_AUDIT_LOG_TYPES } from "~/db/tables";
|
||||
import type { CommonUser } from "~/utils/kysely.server";
|
||||
import { tournamentTeamPage, userPage } from "~/utils/urls";
|
||||
import type { TournamentAdminPageLoader } from "../loaders/to.$id.admin.server";
|
||||
import { useTournament } from "../routes/to.$id";
|
||||
import styles from "./TournamentAdminAuditLog.module.css";
|
||||
|
||||
const WHEN_FORMAT_OPTIONS = {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
} as const;
|
||||
|
||||
// xxx: data not loaded before full page refresh
|
||||
|
||||
export function TournamentAdminAuditLog() {
|
||||
const { t } = useTranslation(["tournament"]);
|
||||
const data = useLoaderData<TournamentAdminPageLoader>();
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
|
||||
const auditLog = data?.auditLog;
|
||||
if (!auditLog) return null;
|
||||
|
||||
const setPage = (page: number) => {
|
||||
setSearchParams((params) => {
|
||||
params.set("page", String(page));
|
||||
return params;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="stack md">
|
||||
<AuditLogFilters teams={auditLog.teams} />
|
||||
{auditLog.events.length === 0 ? (
|
||||
<div className="text-lighter text-sm">
|
||||
{t("tournament:admin.audit.empty")}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("tournament:admin.audit.column.when")}</th>
|
||||
<th>{t("tournament:admin.audit.column.event")}</th>
|
||||
<th>{t("tournament:admin.audit.column.team")}</th>
|
||||
<th>{t("tournament:admin.audit.column.actor")}</th>
|
||||
<th>{t("tournament:admin.audit.column.subject")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{auditLog.events.map((event) => (
|
||||
<AuditLogRow key={event.id} event={event} />
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
{auditLog.pagesCount > 1 ? (
|
||||
<Pagination
|
||||
currentPage={auditLog.currentPage}
|
||||
pagesCount={auditLog.pagesCount}
|
||||
nextPage={() => setPage(auditLog.currentPage + 1)}
|
||||
previousPage={() => setPage(auditLog.currentPage - 1)}
|
||||
setPage={setPage}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type AuditLogEvent = NonNullable<
|
||||
NonNullable<
|
||||
ReturnType<typeof useLoaderData<TournamentAdminPageLoader>>
|
||||
>["auditLog"]
|
||||
>["events"][number];
|
||||
|
||||
function AuditLogRow({ event }: { event: AuditLogEvent }) {
|
||||
const { t } = useTranslation(["tournament"]);
|
||||
const tournament = useTournament();
|
||||
|
||||
const bracketName =
|
||||
typeof event.metadata?.bracketIdx === "number"
|
||||
? tournament.brackets[event.metadata.bracketIdx]?.name
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<LocaleTime
|
||||
date={event.createdAt}
|
||||
options={WHEN_FORMAT_OPTIONS}
|
||||
inline
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{t(`tournament:admin.audit.event.${event.type}`)}
|
||||
{bracketName ? (
|
||||
<div className="text-lighter text-xs">{bracketName}</div>
|
||||
) : null}
|
||||
</td>
|
||||
<td>
|
||||
{event.team ? (
|
||||
tournament.teamById(event.team.id) ? (
|
||||
<Link
|
||||
to={tournamentTeamPage({
|
||||
tournamentId: tournament.ctx.id,
|
||||
tournamentTeamId: event.team.id,
|
||||
})}
|
||||
>
|
||||
{event.team.name}
|
||||
</Link>
|
||||
) : (
|
||||
event.team.name
|
||||
)
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<UserCell user={event.actor} />
|
||||
</td>
|
||||
<td>
|
||||
<UserCell user={event.subject} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function UserCell({ user }: { user: CommonUser | null }) {
|
||||
if (!user) return <>-</>;
|
||||
|
||||
return (
|
||||
<Link to={userPage(user)} className={styles.userCell}>
|
||||
<Avatar user={user} size="xxs" />
|
||||
{user.username}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function AuditLogFilters({
|
||||
teams,
|
||||
}: {
|
||||
teams: Array<{ id: number; name: string }>;
|
||||
}) {
|
||||
const { t } = useTranslation(["tournament"]);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const setFilter = (key: string, value: string) => {
|
||||
setSearchParams((params) => {
|
||||
if (value) {
|
||||
params.set(key, value);
|
||||
} else {
|
||||
params.delete(key);
|
||||
}
|
||||
params.delete("page");
|
||||
return params;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="stack horizontal sm flex-wrap">
|
||||
<div>
|
||||
<Label htmlFor="auditType">
|
||||
{t("tournament:admin.audit.filter.event")}
|
||||
</Label>
|
||||
<select
|
||||
id="auditType"
|
||||
value={searchParams.get("auditType") ?? ""}
|
||||
onChange={(e) => setFilter("auditType", e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
{t("tournament:admin.audit.filter.allEvents")}
|
||||
</option>
|
||||
{TOURNAMENT_AUDIT_LOG_TYPES.map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{t(`tournament:admin.audit.event.${type}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="auditTeam">
|
||||
{t("tournament:admin.audit.filter.team")}
|
||||
</Label>
|
||||
<select
|
||||
id="auditTeam"
|
||||
value={searchParams.get("auditTeam") ?? ""}
|
||||
onChange={(e) => setFilter("auditTeam", e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
{t("tournament:admin.audit.filter.allTeams")}
|
||||
</option>
|
||||
{teams.map((team) => (
|
||||
<option key={team.id} value={team.id}>
|
||||
{team.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
70
app/features/tournament/loaders/to.$id.admin.server.ts
Normal file
70
app/features/tournament/loaders/to.$id.admin.server.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import type { LoaderFunctionArgs } from "react-router";
|
||||
import { z } from "zod";
|
||||
import { TOURNAMENT_AUDIT_LOG_TYPES } from "~/db/tables";
|
||||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
import * as TournamentAuditLogRepository from "~/features/tournament/TournamentAuditLogRepository.server";
|
||||
import { AUDIT_LOG_PAGE_SIZE } from "~/features/tournament/TournamentAuditLogRepository.server";
|
||||
import { tournamentFromDBCached } from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import {
|
||||
parseParams,
|
||||
parseSearchParams,
|
||||
redirectIfPageOutOfBounds,
|
||||
} from "~/utils/remix.server";
|
||||
import { idObject } from "~/utils/zod";
|
||||
|
||||
// xxx: extract to different file
|
||||
const auditSearchParamsSchema = z.object({
|
||||
tab: z.string().optional().catch(undefined),
|
||||
page: z.coerce.number().int().min(1).catch(1),
|
||||
auditType: z.enum(TOURNAMENT_AUDIT_LOG_TYPES).optional().catch(undefined),
|
||||
auditTeam: z.coerce.number().int().optional().catch(undefined),
|
||||
});
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = requireUser();
|
||||
|
||||
const { id: tournamentId } = parseParams({ params, schema: idObject });
|
||||
|
||||
const tournament = await tournamentFromDBCached({ tournamentId, user });
|
||||
if (!tournament.isOrganizer(user)) return null;
|
||||
|
||||
const { tab, page, auditType, auditTeam } = parseSearchParams({
|
||||
request,
|
||||
schema: auditSearchParamsSchema,
|
||||
});
|
||||
|
||||
// xxx: probably just make proper different routes?
|
||||
// the audit log is paginated server-side, so only fetch it for its own tab
|
||||
if (tab !== "audit") return { auditLog: null };
|
||||
|
||||
const [events, totalCount, teams] = await Promise.all([
|
||||
TournamentAuditLogRepository.findByTournamentId({
|
||||
tournamentId,
|
||||
type: auditType,
|
||||
tournamentTeamId: auditTeam,
|
||||
limit: AUDIT_LOG_PAGE_SIZE,
|
||||
offset: (page - 1) * AUDIT_LOG_PAGE_SIZE,
|
||||
}),
|
||||
TournamentAuditLogRepository.countByTournamentId({
|
||||
tournamentId,
|
||||
type: auditType,
|
||||
tournamentTeamId: auditTeam,
|
||||
}),
|
||||
TournamentAuditLogRepository.findTeamsByTournamentId(tournamentId),
|
||||
]);
|
||||
|
||||
const pagesCount = Math.max(1, Math.ceil(totalCount / AUDIT_LOG_PAGE_SIZE));
|
||||
|
||||
redirectIfPageOutOfBounds({ request, page, pagesCount });
|
||||
|
||||
return {
|
||||
auditLog: {
|
||||
events,
|
||||
teams,
|
||||
currentPage: page,
|
||||
pagesCount,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export type TournamentAdminPageLoader = typeof loader;
|
||||
|
|
@ -1,5 +1,13 @@
|
|||
import clsx from "clsx";
|
||||
import { ListOrdered, Trash, Trophy, Tv, UserCog, Users } from "lucide-react";
|
||||
import {
|
||||
History,
|
||||
ListOrdered,
|
||||
Trash,
|
||||
Trophy,
|
||||
Tv,
|
||||
UserCog,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFetcher } from "react-router";
|
||||
|
|
@ -37,13 +45,15 @@ import {
|
|||
tournamentPage,
|
||||
} from "~/utils/urls";
|
||||
import { BracketProgressionSelector } from "../../calendar/components/BracketProgressionSelector";
|
||||
import { TournamentAdminAuditLog } from "../components/TournamentAdminAuditLog";
|
||||
import { TournamentSeeds } from "../components/TournamentSeeds";
|
||||
import { useTournament } from "./to.$id";
|
||||
import adminStyles from "./to.$id.admin.module.css";
|
||||
|
||||
export { action } from "../actions/to.$id.admin.server";
|
||||
export { loader } from "../loaders/to.$id.admin.server";
|
||||
|
||||
type AdminTab = "teams" | "seeds" | "staff" | "stream" | "brackets";
|
||||
type AdminTab = "teams" | "seeds" | "staff" | "stream" | "brackets" | "audit";
|
||||
|
||||
export default function TournamentAdminPage() {
|
||||
const { t } = useTranslation(["tournament", "calendar"]);
|
||||
|
|
@ -75,6 +85,7 @@ export default function TournamentAdminPage() {
|
|||
switch (tab) {
|
||||
case "teams":
|
||||
case "stream":
|
||||
case "audit":
|
||||
return true;
|
||||
case "seeds":
|
||||
return showSeedsTab;
|
||||
|
|
@ -160,6 +171,9 @@ export default function TournamentAdminPage() {
|
|||
{t("tournament:admin.tab.brackets")}
|
||||
</SendouTab>
|
||||
) : null}
|
||||
<SendouTab id="audit" icon={<History />}>
|
||||
{t("tournament:admin.tab.audit")}
|
||||
</SendouTab>
|
||||
</SendouTabList>
|
||||
<SendouTabPanel id="teams" className="stack lg">
|
||||
<TeamActions />
|
||||
|
|
@ -212,6 +226,9 @@ export default function TournamentAdminPage() {
|
|||
) : null}
|
||||
</SendouTabPanel>
|
||||
) : null}
|
||||
<SendouTabPanel id="audit">
|
||||
<TournamentAdminAuditLog />
|
||||
</SendouTabPanel>
|
||||
</SendouTabs>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ export async function dbInsertTournamentTeam({
|
|||
teamId: null,
|
||||
},
|
||||
userId: ownerId,
|
||||
actorUserId: ownerId,
|
||||
tournamentId,
|
||||
});
|
||||
|
||||
|
|
@ -70,11 +71,14 @@ export async function dbInsertTournamentTeam({
|
|||
|
||||
await TournamentTeamRepository.join({
|
||||
userId: memberId,
|
||||
actorUserId: memberId,
|
||||
newTeamId: tournamentTeam.id,
|
||||
});
|
||||
}
|
||||
|
||||
await TournamentTeamRepository.checkIn(tournamentTeam.id);
|
||||
await TournamentTeamRepository.checkIn(tournamentTeam.id, {
|
||||
actorUserId: ownerId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
BIN
db-test.sqlite3
BIN
db-test.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -80,6 +80,25 @@
|
|||
"admin.tab.staff": "",
|
||||
"admin.tab.stream": "",
|
||||
"admin.tab.brackets": "",
|
||||
"admin.tab.audit": "",
|
||||
"admin.audit.empty": "",
|
||||
"admin.audit.filter.event": "",
|
||||
"admin.audit.filter.team": "",
|
||||
"admin.audit.filter.allEvents": "",
|
||||
"admin.audit.filter.allTeams": "",
|
||||
"admin.audit.column.when": "",
|
||||
"admin.audit.column.event": "",
|
||||
"admin.audit.column.team": "",
|
||||
"admin.audit.column.actor": "",
|
||||
"admin.audit.column.subject": "",
|
||||
"admin.audit.event.MEMBER_ADDED": "",
|
||||
"admin.audit.event.MEMBER_REMOVED": "",
|
||||
"admin.audit.event.TEAM_REGISTERED": "",
|
||||
"admin.audit.event.TEAM_UNREGISTERED": "",
|
||||
"admin.audit.event.TEAM_CHECKED_IN": "",
|
||||
"admin.audit.event.TEAM_CHECKED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROPPED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROP_OUT_UNDONE": "",
|
||||
"admin.actions.CHANGE_TEAM_OWNER": "Ændr holdkaptajn",
|
||||
"admin.actions.CHANGE_TEAM_NAME": "Ændr holdnavn",
|
||||
"admin.actions.CHECK_IN": "Tjek ind",
|
||||
|
|
|
|||
|
|
@ -80,6 +80,25 @@
|
|||
"admin.tab.staff": "",
|
||||
"admin.tab.stream": "",
|
||||
"admin.tab.brackets": "",
|
||||
"admin.tab.audit": "",
|
||||
"admin.audit.empty": "",
|
||||
"admin.audit.filter.event": "",
|
||||
"admin.audit.filter.team": "",
|
||||
"admin.audit.filter.allEvents": "",
|
||||
"admin.audit.filter.allTeams": "",
|
||||
"admin.audit.column.when": "",
|
||||
"admin.audit.column.event": "",
|
||||
"admin.audit.column.team": "",
|
||||
"admin.audit.column.actor": "",
|
||||
"admin.audit.column.subject": "",
|
||||
"admin.audit.event.MEMBER_ADDED": "",
|
||||
"admin.audit.event.MEMBER_REMOVED": "",
|
||||
"admin.audit.event.TEAM_REGISTERED": "",
|
||||
"admin.audit.event.TEAM_UNREGISTERED": "",
|
||||
"admin.audit.event.TEAM_CHECKED_IN": "",
|
||||
"admin.audit.event.TEAM_CHECKED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROPPED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROP_OUT_UNDONE": "",
|
||||
"admin.actions.CHANGE_TEAM_OWNER": "Kapitän ändern",
|
||||
"admin.actions.CHANGE_TEAM_NAME": "Teamnamen ändern",
|
||||
"admin.actions.CHECK_IN": "Einchecken",
|
||||
|
|
|
|||
|
|
@ -80,6 +80,25 @@
|
|||
"admin.tab.staff": "Staff",
|
||||
"admin.tab.stream": "Stream",
|
||||
"admin.tab.brackets": "Brackets",
|
||||
"admin.tab.audit": "Audit log",
|
||||
"admin.audit.empty": "No events yet",
|
||||
"admin.audit.filter.event": "Event",
|
||||
"admin.audit.filter.team": "Team",
|
||||
"admin.audit.filter.allEvents": "All events",
|
||||
"admin.audit.filter.allTeams": "All teams",
|
||||
"admin.audit.column.when": "When",
|
||||
"admin.audit.column.event": "Event",
|
||||
"admin.audit.column.team": "Team",
|
||||
"admin.audit.column.actor": "By",
|
||||
"admin.audit.column.subject": "Affected player",
|
||||
"admin.audit.event.MEMBER_ADDED": "Player joined team",
|
||||
"admin.audit.event.MEMBER_REMOVED": "Player left team",
|
||||
"admin.audit.event.TEAM_REGISTERED": "Team registered",
|
||||
"admin.audit.event.TEAM_UNREGISTERED": "Team unregistered",
|
||||
"admin.audit.event.TEAM_CHECKED_IN": "Team checked in",
|
||||
"admin.audit.event.TEAM_CHECKED_OUT": "Team checked out",
|
||||
"admin.audit.event.TEAM_DROPPED_OUT": "Team dropped out",
|
||||
"admin.audit.event.TEAM_DROP_OUT_UNDONE": "Team drop out undone",
|
||||
"admin.actions.CHANGE_TEAM_OWNER": "Change captain",
|
||||
"admin.actions.CHANGE_TEAM_NAME": "Change team name",
|
||||
"admin.actions.CHECK_IN": "Check in",
|
||||
|
|
|
|||
|
|
@ -81,6 +81,25 @@
|
|||
"admin.tab.staff": "",
|
||||
"admin.tab.stream": "",
|
||||
"admin.tab.brackets": "",
|
||||
"admin.tab.audit": "",
|
||||
"admin.audit.empty": "",
|
||||
"admin.audit.filter.event": "",
|
||||
"admin.audit.filter.team": "",
|
||||
"admin.audit.filter.allEvents": "",
|
||||
"admin.audit.filter.allTeams": "",
|
||||
"admin.audit.column.when": "",
|
||||
"admin.audit.column.event": "",
|
||||
"admin.audit.column.team": "",
|
||||
"admin.audit.column.actor": "",
|
||||
"admin.audit.column.subject": "",
|
||||
"admin.audit.event.MEMBER_ADDED": "",
|
||||
"admin.audit.event.MEMBER_REMOVED": "",
|
||||
"admin.audit.event.TEAM_REGISTERED": "",
|
||||
"admin.audit.event.TEAM_UNREGISTERED": "",
|
||||
"admin.audit.event.TEAM_CHECKED_IN": "",
|
||||
"admin.audit.event.TEAM_CHECKED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROPPED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROP_OUT_UNDONE": "",
|
||||
"admin.actions.CHANGE_TEAM_OWNER": "Cambiar capitán",
|
||||
"admin.actions.CHANGE_TEAM_NAME": "Cambiar nombre de equipo",
|
||||
"admin.actions.CHECK_IN": "Check in",
|
||||
|
|
|
|||
|
|
@ -81,6 +81,25 @@
|
|||
"admin.tab.staff": "",
|
||||
"admin.tab.stream": "",
|
||||
"admin.tab.brackets": "",
|
||||
"admin.tab.audit": "",
|
||||
"admin.audit.empty": "",
|
||||
"admin.audit.filter.event": "",
|
||||
"admin.audit.filter.team": "",
|
||||
"admin.audit.filter.allEvents": "",
|
||||
"admin.audit.filter.allTeams": "",
|
||||
"admin.audit.column.when": "",
|
||||
"admin.audit.column.event": "",
|
||||
"admin.audit.column.team": "",
|
||||
"admin.audit.column.actor": "",
|
||||
"admin.audit.column.subject": "",
|
||||
"admin.audit.event.MEMBER_ADDED": "",
|
||||
"admin.audit.event.MEMBER_REMOVED": "",
|
||||
"admin.audit.event.TEAM_REGISTERED": "",
|
||||
"admin.audit.event.TEAM_UNREGISTERED": "",
|
||||
"admin.audit.event.TEAM_CHECKED_IN": "",
|
||||
"admin.audit.event.TEAM_CHECKED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROPPED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROP_OUT_UNDONE": "",
|
||||
"admin.actions.CHANGE_TEAM_OWNER": "Cambiar capitán",
|
||||
"admin.actions.CHANGE_TEAM_NAME": "Cambiar nombre de equipo",
|
||||
"admin.actions.CHECK_IN": "Check in",
|
||||
|
|
|
|||
|
|
@ -81,6 +81,25 @@
|
|||
"admin.tab.staff": "",
|
||||
"admin.tab.stream": "",
|
||||
"admin.tab.brackets": "",
|
||||
"admin.tab.audit": "",
|
||||
"admin.audit.empty": "",
|
||||
"admin.audit.filter.event": "",
|
||||
"admin.audit.filter.team": "",
|
||||
"admin.audit.filter.allEvents": "",
|
||||
"admin.audit.filter.allTeams": "",
|
||||
"admin.audit.column.when": "",
|
||||
"admin.audit.column.event": "",
|
||||
"admin.audit.column.team": "",
|
||||
"admin.audit.column.actor": "",
|
||||
"admin.audit.column.subject": "",
|
||||
"admin.audit.event.MEMBER_ADDED": "",
|
||||
"admin.audit.event.MEMBER_REMOVED": "",
|
||||
"admin.audit.event.TEAM_REGISTERED": "",
|
||||
"admin.audit.event.TEAM_UNREGISTERED": "",
|
||||
"admin.audit.event.TEAM_CHECKED_IN": "",
|
||||
"admin.audit.event.TEAM_CHECKED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROPPED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROP_OUT_UNDONE": "",
|
||||
"admin.actions.CHANGE_TEAM_OWNER": "Changer le capitaine",
|
||||
"admin.actions.CHANGE_TEAM_NAME": "",
|
||||
"admin.actions.CHECK_IN": "S'enregister",
|
||||
|
|
|
|||
|
|
@ -81,6 +81,25 @@
|
|||
"admin.tab.staff": "",
|
||||
"admin.tab.stream": "",
|
||||
"admin.tab.brackets": "",
|
||||
"admin.tab.audit": "",
|
||||
"admin.audit.empty": "",
|
||||
"admin.audit.filter.event": "",
|
||||
"admin.audit.filter.team": "",
|
||||
"admin.audit.filter.allEvents": "",
|
||||
"admin.audit.filter.allTeams": "",
|
||||
"admin.audit.column.when": "",
|
||||
"admin.audit.column.event": "",
|
||||
"admin.audit.column.team": "",
|
||||
"admin.audit.column.actor": "",
|
||||
"admin.audit.column.subject": "",
|
||||
"admin.audit.event.MEMBER_ADDED": "",
|
||||
"admin.audit.event.MEMBER_REMOVED": "",
|
||||
"admin.audit.event.TEAM_REGISTERED": "",
|
||||
"admin.audit.event.TEAM_UNREGISTERED": "",
|
||||
"admin.audit.event.TEAM_CHECKED_IN": "",
|
||||
"admin.audit.event.TEAM_CHECKED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROPPED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROP_OUT_UNDONE": "",
|
||||
"admin.actions.CHANGE_TEAM_OWNER": "Changer le capitaine",
|
||||
"admin.actions.CHANGE_TEAM_NAME": "Changer le nom de l'équipe ",
|
||||
"admin.actions.CHECK_IN": "S'enregister",
|
||||
|
|
|
|||
|
|
@ -81,6 +81,25 @@
|
|||
"admin.tab.staff": "",
|
||||
"admin.tab.stream": "",
|
||||
"admin.tab.brackets": "",
|
||||
"admin.tab.audit": "",
|
||||
"admin.audit.empty": "",
|
||||
"admin.audit.filter.event": "",
|
||||
"admin.audit.filter.team": "",
|
||||
"admin.audit.filter.allEvents": "",
|
||||
"admin.audit.filter.allTeams": "",
|
||||
"admin.audit.column.when": "",
|
||||
"admin.audit.column.event": "",
|
||||
"admin.audit.column.team": "",
|
||||
"admin.audit.column.actor": "",
|
||||
"admin.audit.column.subject": "",
|
||||
"admin.audit.event.MEMBER_ADDED": "",
|
||||
"admin.audit.event.MEMBER_REMOVED": "",
|
||||
"admin.audit.event.TEAM_REGISTERED": "",
|
||||
"admin.audit.event.TEAM_UNREGISTERED": "",
|
||||
"admin.audit.event.TEAM_CHECKED_IN": "",
|
||||
"admin.audit.event.TEAM_CHECKED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROPPED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROP_OUT_UNDONE": "",
|
||||
"admin.actions.CHANGE_TEAM_OWNER": "שינוי קפטן",
|
||||
"admin.actions.CHANGE_TEAM_NAME": "",
|
||||
"admin.actions.CHECK_IN": "קבלה",
|
||||
|
|
|
|||
|
|
@ -81,6 +81,25 @@
|
|||
"admin.tab.staff": "",
|
||||
"admin.tab.stream": "",
|
||||
"admin.tab.brackets": "",
|
||||
"admin.tab.audit": "",
|
||||
"admin.audit.empty": "",
|
||||
"admin.audit.filter.event": "",
|
||||
"admin.audit.filter.team": "",
|
||||
"admin.audit.filter.allEvents": "",
|
||||
"admin.audit.filter.allTeams": "",
|
||||
"admin.audit.column.when": "",
|
||||
"admin.audit.column.event": "",
|
||||
"admin.audit.column.team": "",
|
||||
"admin.audit.column.actor": "",
|
||||
"admin.audit.column.subject": "",
|
||||
"admin.audit.event.MEMBER_ADDED": "",
|
||||
"admin.audit.event.MEMBER_REMOVED": "",
|
||||
"admin.audit.event.TEAM_REGISTERED": "",
|
||||
"admin.audit.event.TEAM_UNREGISTERED": "",
|
||||
"admin.audit.event.TEAM_CHECKED_IN": "",
|
||||
"admin.audit.event.TEAM_CHECKED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROPPED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROP_OUT_UNDONE": "",
|
||||
"admin.actions.CHANGE_TEAM_OWNER": "Cambia capitano",
|
||||
"admin.actions.CHANGE_TEAM_NAME": "Cambia nome team",
|
||||
"admin.actions.CHECK_IN": "Check-in",
|
||||
|
|
|
|||
|
|
@ -78,6 +78,25 @@
|
|||
"admin.tab.staff": "",
|
||||
"admin.tab.stream": "",
|
||||
"admin.tab.brackets": "",
|
||||
"admin.tab.audit": "",
|
||||
"admin.audit.empty": "",
|
||||
"admin.audit.filter.event": "",
|
||||
"admin.audit.filter.team": "",
|
||||
"admin.audit.filter.allEvents": "",
|
||||
"admin.audit.filter.allTeams": "",
|
||||
"admin.audit.column.when": "",
|
||||
"admin.audit.column.event": "",
|
||||
"admin.audit.column.team": "",
|
||||
"admin.audit.column.actor": "",
|
||||
"admin.audit.column.subject": "",
|
||||
"admin.audit.event.MEMBER_ADDED": "",
|
||||
"admin.audit.event.MEMBER_REMOVED": "",
|
||||
"admin.audit.event.TEAM_REGISTERED": "",
|
||||
"admin.audit.event.TEAM_UNREGISTERED": "",
|
||||
"admin.audit.event.TEAM_CHECKED_IN": "",
|
||||
"admin.audit.event.TEAM_CHECKED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROPPED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROP_OUT_UNDONE": "",
|
||||
"admin.actions.CHANGE_TEAM_OWNER": "キャプテンを変更",
|
||||
"admin.actions.CHANGE_TEAM_NAME": "チーム名変更",
|
||||
"admin.actions.CHECK_IN": "チェックイン",
|
||||
|
|
|
|||
|
|
@ -78,6 +78,25 @@
|
|||
"admin.tab.staff": "",
|
||||
"admin.tab.stream": "",
|
||||
"admin.tab.brackets": "",
|
||||
"admin.tab.audit": "",
|
||||
"admin.audit.empty": "",
|
||||
"admin.audit.filter.event": "",
|
||||
"admin.audit.filter.team": "",
|
||||
"admin.audit.filter.allEvents": "",
|
||||
"admin.audit.filter.allTeams": "",
|
||||
"admin.audit.column.when": "",
|
||||
"admin.audit.column.event": "",
|
||||
"admin.audit.column.team": "",
|
||||
"admin.audit.column.actor": "",
|
||||
"admin.audit.column.subject": "",
|
||||
"admin.audit.event.MEMBER_ADDED": "",
|
||||
"admin.audit.event.MEMBER_REMOVED": "",
|
||||
"admin.audit.event.TEAM_REGISTERED": "",
|
||||
"admin.audit.event.TEAM_UNREGISTERED": "",
|
||||
"admin.audit.event.TEAM_CHECKED_IN": "",
|
||||
"admin.audit.event.TEAM_CHECKED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROPPED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROP_OUT_UNDONE": "",
|
||||
"admin.actions.CHANGE_TEAM_OWNER": "",
|
||||
"admin.actions.CHANGE_TEAM_NAME": "",
|
||||
"admin.actions.CHECK_IN": "",
|
||||
|
|
|
|||
|
|
@ -80,6 +80,25 @@
|
|||
"admin.tab.staff": "",
|
||||
"admin.tab.stream": "",
|
||||
"admin.tab.brackets": "",
|
||||
"admin.tab.audit": "",
|
||||
"admin.audit.empty": "",
|
||||
"admin.audit.filter.event": "",
|
||||
"admin.audit.filter.team": "",
|
||||
"admin.audit.filter.allEvents": "",
|
||||
"admin.audit.filter.allTeams": "",
|
||||
"admin.audit.column.when": "",
|
||||
"admin.audit.column.event": "",
|
||||
"admin.audit.column.team": "",
|
||||
"admin.audit.column.actor": "",
|
||||
"admin.audit.column.subject": "",
|
||||
"admin.audit.event.MEMBER_ADDED": "",
|
||||
"admin.audit.event.MEMBER_REMOVED": "",
|
||||
"admin.audit.event.TEAM_REGISTERED": "",
|
||||
"admin.audit.event.TEAM_UNREGISTERED": "",
|
||||
"admin.audit.event.TEAM_CHECKED_IN": "",
|
||||
"admin.audit.event.TEAM_CHECKED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROPPED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROP_OUT_UNDONE": "",
|
||||
"admin.actions.CHANGE_TEAM_OWNER": "",
|
||||
"admin.actions.CHANGE_TEAM_NAME": "",
|
||||
"admin.actions.CHECK_IN": "",
|
||||
|
|
|
|||
|
|
@ -82,6 +82,25 @@
|
|||
"admin.tab.staff": "",
|
||||
"admin.tab.stream": "",
|
||||
"admin.tab.brackets": "",
|
||||
"admin.tab.audit": "",
|
||||
"admin.audit.empty": "",
|
||||
"admin.audit.filter.event": "",
|
||||
"admin.audit.filter.team": "",
|
||||
"admin.audit.filter.allEvents": "",
|
||||
"admin.audit.filter.allTeams": "",
|
||||
"admin.audit.column.when": "",
|
||||
"admin.audit.column.event": "",
|
||||
"admin.audit.column.team": "",
|
||||
"admin.audit.column.actor": "",
|
||||
"admin.audit.column.subject": "",
|
||||
"admin.audit.event.MEMBER_ADDED": "",
|
||||
"admin.audit.event.MEMBER_REMOVED": "",
|
||||
"admin.audit.event.TEAM_REGISTERED": "",
|
||||
"admin.audit.event.TEAM_UNREGISTERED": "",
|
||||
"admin.audit.event.TEAM_CHECKED_IN": "",
|
||||
"admin.audit.event.TEAM_CHECKED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROPPED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROP_OUT_UNDONE": "",
|
||||
"admin.actions.CHANGE_TEAM_OWNER": "",
|
||||
"admin.actions.CHANGE_TEAM_NAME": "",
|
||||
"admin.actions.CHECK_IN": "",
|
||||
|
|
|
|||
|
|
@ -81,6 +81,25 @@
|
|||
"admin.tab.staff": "",
|
||||
"admin.tab.stream": "",
|
||||
"admin.tab.brackets": "",
|
||||
"admin.tab.audit": "",
|
||||
"admin.audit.empty": "",
|
||||
"admin.audit.filter.event": "",
|
||||
"admin.audit.filter.team": "",
|
||||
"admin.audit.filter.allEvents": "",
|
||||
"admin.audit.filter.allTeams": "",
|
||||
"admin.audit.column.when": "",
|
||||
"admin.audit.column.event": "",
|
||||
"admin.audit.column.team": "",
|
||||
"admin.audit.column.actor": "",
|
||||
"admin.audit.column.subject": "",
|
||||
"admin.audit.event.MEMBER_ADDED": "",
|
||||
"admin.audit.event.MEMBER_REMOVED": "",
|
||||
"admin.audit.event.TEAM_REGISTERED": "",
|
||||
"admin.audit.event.TEAM_UNREGISTERED": "",
|
||||
"admin.audit.event.TEAM_CHECKED_IN": "",
|
||||
"admin.audit.event.TEAM_CHECKED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROPPED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROP_OUT_UNDONE": "",
|
||||
"admin.actions.CHANGE_TEAM_OWNER": "Mudar capitão",
|
||||
"admin.actions.CHANGE_TEAM_NAME": "Mudar nome do time",
|
||||
"admin.actions.CHECK_IN": "Check-in",
|
||||
|
|
|
|||
|
|
@ -82,6 +82,25 @@
|
|||
"admin.tab.staff": "",
|
||||
"admin.tab.stream": "",
|
||||
"admin.tab.brackets": "",
|
||||
"admin.tab.audit": "",
|
||||
"admin.audit.empty": "",
|
||||
"admin.audit.filter.event": "",
|
||||
"admin.audit.filter.team": "",
|
||||
"admin.audit.filter.allEvents": "",
|
||||
"admin.audit.filter.allTeams": "",
|
||||
"admin.audit.column.when": "",
|
||||
"admin.audit.column.event": "",
|
||||
"admin.audit.column.team": "",
|
||||
"admin.audit.column.actor": "",
|
||||
"admin.audit.column.subject": "",
|
||||
"admin.audit.event.MEMBER_ADDED": "",
|
||||
"admin.audit.event.MEMBER_REMOVED": "",
|
||||
"admin.audit.event.TEAM_REGISTERED": "",
|
||||
"admin.audit.event.TEAM_UNREGISTERED": "",
|
||||
"admin.audit.event.TEAM_CHECKED_IN": "",
|
||||
"admin.audit.event.TEAM_CHECKED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROPPED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROP_OUT_UNDONE": "",
|
||||
"admin.actions.CHANGE_TEAM_OWNER": "Изменить капитана",
|
||||
"admin.actions.CHANGE_TEAM_NAME": "Изменить имя команды",
|
||||
"admin.actions.CHECK_IN": "Зарегистрировать",
|
||||
|
|
|
|||
|
|
@ -78,6 +78,25 @@
|
|||
"admin.tab.staff": "",
|
||||
"admin.tab.stream": "",
|
||||
"admin.tab.brackets": "",
|
||||
"admin.tab.audit": "",
|
||||
"admin.audit.empty": "",
|
||||
"admin.audit.filter.event": "",
|
||||
"admin.audit.filter.team": "",
|
||||
"admin.audit.filter.allEvents": "",
|
||||
"admin.audit.filter.allTeams": "",
|
||||
"admin.audit.column.when": "",
|
||||
"admin.audit.column.event": "",
|
||||
"admin.audit.column.team": "",
|
||||
"admin.audit.column.actor": "",
|
||||
"admin.audit.column.subject": "",
|
||||
"admin.audit.event.MEMBER_ADDED": "",
|
||||
"admin.audit.event.MEMBER_REMOVED": "",
|
||||
"admin.audit.event.TEAM_REGISTERED": "",
|
||||
"admin.audit.event.TEAM_UNREGISTERED": "",
|
||||
"admin.audit.event.TEAM_CHECKED_IN": "",
|
||||
"admin.audit.event.TEAM_CHECKED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROPPED_OUT": "",
|
||||
"admin.audit.event.TEAM_DROP_OUT_UNDONE": "",
|
||||
"admin.actions.CHANGE_TEAM_OWNER": "变更队长",
|
||||
"admin.actions.CHANGE_TEAM_NAME": "变更队名",
|
||||
"admin.actions.CHECK_IN": "登入",
|
||||
|
|
|
|||
52
migrations/145-tournament-audit-log.js
Normal file
52
migrations/145-tournament-audit-log.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
export function up(db) {
|
||||
db.transaction(() => {
|
||||
db.prepare(
|
||||
/* sql */ `
|
||||
create table "TournamentTeamHistory" (
|
||||
"tournamentTeamId" integer primary key,
|
||||
"tournamentId" integer not null,
|
||||
"name" text not null,
|
||||
foreign key ("tournamentId") references "Tournament"("id") on delete cascade
|
||||
) strict
|
||||
`,
|
||||
).run();
|
||||
|
||||
// xxx: no enum check, we will keep it dynamic
|
||||
db.prepare(
|
||||
/* sql */ `
|
||||
create table "TournamentAuditLog" (
|
||||
"id" integer primary key autoincrement,
|
||||
"tournamentId" integer not null,
|
||||
"type" text not null check (
|
||||
"type" in (
|
||||
'MEMBER_ADDED',
|
||||
'MEMBER_REMOVED',
|
||||
'TEAM_REGISTERED',
|
||||
'TEAM_UNREGISTERED',
|
||||
'TEAM_CHECKED_IN',
|
||||
'TEAM_CHECKED_OUT',
|
||||
'TEAM_DROPPED_OUT',
|
||||
'TEAM_DROP_OUT_UNDONE'
|
||||
)
|
||||
),
|
||||
"actorUserId" integer not null,
|
||||
"subjectUserId" integer,
|
||||
"tournamentTeamId" integer,
|
||||
"metadata" text,
|
||||
"createdAt" integer not null,
|
||||
foreign key ("tournamentId") references "Tournament"("id") on delete cascade,
|
||||
foreign key ("actorUserId") references "User"("id"),
|
||||
foreign key ("subjectUserId") references "User"("id"),
|
||||
foreign key ("tournamentTeamId") references "TournamentTeamHistory"("tournamentTeamId")
|
||||
) strict
|
||||
`,
|
||||
).run();
|
||||
|
||||
// xxx: which indexes are the best?
|
||||
db.prepare(
|
||||
/* sql */ `create index tournament_audit_log_tournament_id_created_at_idx on "TournamentAuditLog"("tournamentId", "createdAt")`,
|
||||
).run();
|
||||
|
||||
db.pragma("foreign_key_check");
|
||||
})();
|
||||
}
|
||||
|
|
@ -107,6 +107,7 @@ async function main() {
|
|||
teamId: entry.team?.id ?? null,
|
||||
},
|
||||
userId: owner.id,
|
||||
actorUserId: owner.id,
|
||||
tournamentId,
|
||||
});
|
||||
|
||||
|
|
@ -114,6 +115,7 @@ async function main() {
|
|||
await TournamentTeamRepository.join({
|
||||
newTeamId: tournamentTeam.id,
|
||||
userId: member.id,
|
||||
actorUserId: member.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user