Allow dev user to impersonate

This commit is contained in:
Kalle 2026-03-07 15:03:20 +02:00
parent cbf95d0cd1
commit edd8512704
8 changed files with 83 additions and 9 deletions

View File

@ -3,6 +3,9 @@ export const ADMIN_ID = process.env.NODE_ENV === "test" ? 1 : 274;
// Panda Scep Acing Baja Michi
export const STAFF_IDS = [11329, 9719, 9342, 20774, 23094];
// hfcRed
export const DEV_IDS = [27883];
export const STAFF_DISCORD_IDS = [
"138757634500067328",
"184478601171828737",

View File

@ -1,14 +1,25 @@
import type { LoaderFunctionArgs } from "react-router";
import { isImpersonating } from "~/features/auth/core/user.server";
import {
getRealUserId,
isImpersonating,
requireUser,
} from "~/features/auth/core/user.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { requireRole } from "~/modules/permissions/guards.server";
import { isAdmin, isDev, isStaff } from "~/modules/permissions/utils";
import { parseSafeSearchParams } from "~/utils/remix.server";
import { adminActionSearchParamsSchema } from "../admin-schemas";
import { DANGEROUS_CAN_ACCESS_DEV_CONTROLS } from "../core/dev-controls";
export const loader = async ({ request }: LoaderFunctionArgs) => {
if (!DANGEROUS_CAN_ACCESS_DEV_CONTROLS) {
requireRole("STAFF");
const user = requireUser();
const realUserId = await getRealUserId(request);
const userToCheck =
realUserId && realUserId !== user.id ? { id: realUserId } : user;
if (!isAdmin(userToCheck) && !isStaff(userToCheck) && !isDev(userToCheck)) {
throw new Response("Forbidden", { status: 403 });
}
}
const parsedSearchParams = parseSafeSearchParams({

View File

@ -47,6 +47,17 @@ export const meta: MetaFunction = (args) => {
};
export default function AdminPage() {
const isStaff = useHasRole("STAFF");
// is dev user or is someone impersonating another user (allow them to stop)
if (!isStaff) {
return (
<Main>
<Impersonate />
</Main>
);
}
return (
<Main>
<SendouTabs>
@ -109,12 +120,15 @@ function FriendCodeLookUp() {
function AdminActions() {
const isStaff = useHasRole("STAFF");
const isAdmin = useHasRole("ADMIN");
const isDev = useHasRole("DEV");
return (
<div className="stack lg">
{DANGEROUS_CAN_ACCESS_DEV_CONTROLS ? <Seed /> : null}
{DANGEROUS_CAN_ACCESS_DEV_CONTROLS ? <TestAdminNotification /> : null}
{DANGEROUS_CAN_ACCESS_DEV_CONTROLS || isAdmin ? <Impersonate /> : null}
{DANGEROUS_CAN_ACCESS_DEV_CONTROLS || isAdmin || isDev ? (
<Impersonate />
) : null}
{isStaff ? <LinkPlayer /> : null}
{isStaff ? <GiveArtist /> : null}

View File

@ -3,8 +3,9 @@ import type { ActionFunction, LoaderFunction } from "react-router";
import { redirect } from "react-router";
import { z } from "zod";
import { DANGEROUS_CAN_ACCESS_DEV_CONTROLS } from "~/features/admin/core/dev-controls";
import { requireUser } from "~/features/auth/core/user.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { requireRole } from "~/modules/permissions/guards.server";
import { isAdmin, isStaff } from "~/modules/permissions/utils";
import { logger } from "~/utils/logger";
import {
canAccessLohiEndpoint,
@ -74,19 +75,36 @@ export const logInAction: ActionFunction = async ({ request }) => {
export const impersonateAction: ActionFunction = async ({ request }) => {
if (!DANGEROUS_CAN_ACCESS_DEV_CONTROLS) {
requireRole("ADMIN");
const user = requireUser();
if (!user.roles.includes("ADMIN") && !user.roles.includes("DEV")) {
throw new Response("Forbidden", { status: 403 });
}
if (user.roles.includes("DEV") && !user.roles.includes("ADMIN")) {
const url = new URL(request.url);
const targetId = Number(url.searchParams.get("id"));
if (isAdmin({ id: targetId }) || isStaff({ id: targetId })) {
throw new Response("Forbidden", { status: 403 });
}
}
}
const session = await authSessionStorage.getSession(
request.headers.get("Cookie"),
);
const realUserId = session.get(SESSION_KEY);
const url = new URL(request.url);
const rawId = url.searchParams.get("id");
const userId = Number(url.searchParams.get("id"));
if (!rawId || Number.isNaN(userId)) throw new Response(null, { status: 400 });
logger.info(
`Impersonation: user ${realUserId} started impersonating user ${userId}`,
);
session.set(IMPERSONATED_SESSION_KEY, userId);
throw redirect(ADMIN_PAGE, {
@ -99,6 +117,13 @@ export const stopImpersonatingAction: ActionFunction = async ({ request }) => {
request.headers.get("Cookie"),
);
const realUserId = session.get(SESSION_KEY);
const impersonatedUserId = session.get(IMPERSONATED_SESSION_KEY);
logger.info(
`Impersonation: user ${realUserId} stopped impersonating user ${impersonatedUserId}`,
);
session.unset(IMPERSONATED_SESSION_KEY);
throw redirect(ADMIN_PAGE, {

View File

@ -1,4 +1,4 @@
import { IMPERSONATED_SESSION_KEY } from "./authenticator.server";
import { IMPERSONATED_SESSION_KEY, SESSION_KEY } from "./authenticator.server";
import { authSessionStorage } from "./session.server";
import { type AuthenticatedUser, getUserContext } from "./user-context.server";
@ -24,3 +24,13 @@ export async function isImpersonating(request: Request) {
return Boolean(session.get(IMPERSONATED_SESSION_KEY));
}
export async function getRealUserId(
request: Request,
): Promise<number | undefined> {
const session = await authSessionStorage.getSession(
request.headers.get("Cookie"),
);
return session.get(SESSION_KEY) as number | undefined;
}

View File

@ -1,7 +1,7 @@
import type { UserWithPlusTier } from "~/db/tables";
import { userDiscordIdIsAged } from "~/utils/users";
import type { Role } from "./types";
import { isAdmin, isStaff, isSupporter } from "./utils";
import { isAdmin, isDev, isStaff, isSupporter } from "./utils";
export function userRoles(
user: Pick<
@ -26,6 +26,10 @@ export function userRoles(
result.push("STAFF");
}
if (isDev(user)) {
result.push("DEV");
}
if (typeof user.patronTier === "number") {
result.push("MINOR_SUPPORT");
}

View File

@ -14,5 +14,6 @@ export type Role =
| "CALENDAR_EVENT_ADDER"
| "TOURNAMENT_ADDER"
| "API_ACCESSER"
| "DEV"
| "SUPPORTER" // patrons of "Supporter" tier or higher
| "MINOR_SUPPORT"; // patrons of "Support" tier or higher

View File

@ -1,4 +1,4 @@
import { ADMIN_ID, STAFF_IDS } from "~/features/admin/admin-constants";
import { ADMIN_ID, DEV_IDS, STAFF_IDS } from "~/features/admin/admin-constants";
export function isAdmin(user?: { id: number }) {
return user?.id === ADMIN_ID;
@ -10,6 +10,12 @@ export function isStaff(user?: { id: number }) {
return STAFF_IDS.includes(user.id);
}
export function isDev(user?: { id: number }) {
if (!user) return false;
return DEV_IDS.includes(user.id);
}
export function isSupporter(user?: { patronTier: number | null }) {
return typeof user?.patronTier === "number" && user.patronTier >= 2;
}