11import { MagnifyingGlassIcon } from "@heroicons/react/20/solid" ;
22import { Form } from "@remix-run/react" ;
3- import type { ActionFunctionArgs , LoaderFunctionArgs } from "@remix-run/server-runtime" ;
4- import { redirect } from "@remix-run/server-runtime" ;
3+ import type { LoaderFunctionArgs } from "@remix-run/server-runtime" ;
54import { typedjson , useTypedLoaderData } from "remix-typedjson" ;
65import { z } from "zod" ;
76import { Button , LinkButton } from "~/components/primitives/Buttons" ;
87import { CopyableText } from "~/components/primitives/CopyableText" ;
9- import { Header1 } from "~/components/primitives/Headers" ;
108import { Input } from "~/components/primitives/Input" ;
119import { PaginationControls } from "~/components/primitives/Pagination" ;
1210import { Paragraph } from "~/components/primitives/Paragraph" ;
@@ -19,14 +17,9 @@ import {
1917 TableHeaderCell ,
2018 TableRow ,
2119} from "~/components/primitives/Table" ;
22- import { useUser } from "~/hooks/useUser" ;
23- import { adminGetUsers , redirectWithImpersonation } from "~/models/admin.server" ;
24- import { requireUser , requireUserId } from "~/services/session.server" ;
25- import {
26- validateAndConsumeImpersonationToken ,
27- } from "~/services/impersonation.server" ;
20+ import { adminGetUsers } from "~/models/admin.server" ;
21+ import { requireUserId } from "~/services/session.server" ;
2822import { createSearchParams } from "~/utils/searchParams" ;
29- import { logger } from "~/services/logger.server" ;
3023
3124export const SearchParams = z . object ( {
3225 page : z . coerce . number ( ) . optional ( ) ,
@@ -35,44 +28,7 @@ export const SearchParams = z.object({
3528
3629export type SearchParams = z . infer < typeof SearchParams > ;
3730
38- const FormSchema = z . object ( { id : z . string ( ) } ) ;
39-
40- async function handleImpersonationRequest (
41- request : Request ,
42- userId : string
43- ) : Promise < Response > {
44- const user = await requireUser ( request ) ;
45- if ( ! user . admin ) {
46- return redirect ( "/" ) ;
47- }
48- return redirectWithImpersonation ( request , userId , "/" ) ;
49- }
50-
51- export const loader = async ( { request, params } : LoaderFunctionArgs ) => {
52- // Check if this is an impersonation request via query parameter (e.g., from Plain customer cards)
53- const url = new URL ( request . url ) ;
54- const impersonateUserId = url . searchParams . get ( "impersonate" ) ;
55- const impersonationToken = url . searchParams . get ( "impersonationToken" ) ;
56-
57- if ( impersonateUserId ) {
58- // Require both userId and token for GET-based impersonation
59- if ( ! impersonationToken ) {
60- logger . warn ( "Impersonation request missing token" ) ;
61- return redirect ( "/" ) ;
62- }
63-
64- // Validate and consume the token (prevents replay attacks)
65- const validatedUserId = await validateAndConsumeImpersonationToken ( impersonationToken ) ;
66-
67- if ( ! validatedUserId || validatedUserId !== impersonateUserId ) {
68- logger . warn ( "Invalid or expired impersonation token" ) ;
69- return redirect ( "/" ) ;
70- }
71-
72- return handleImpersonationRequest ( request , impersonateUserId ) ;
73- }
74-
75- // Normal loader logic for admin dashboard
31+ export const loader = async ( { request } : LoaderFunctionArgs ) => {
7632 const userId = await requireUserId ( request ) ;
7733
7834 const searchParams = createSearchParams ( request . url , SearchParams ) ;
@@ -84,19 +40,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
8440 return typedjson ( result ) ;
8541} ;
8642
87- export async function action ( { request } : ActionFunctionArgs ) {
88- if ( request . method . toLowerCase ( ) !== "post" ) {
89- return new Response ( "Method not allowed" , { status : 405 } ) ;
90- }
91-
92- const payload = Object . fromEntries ( await request . formData ( ) ) ;
93- const { id } = FormSchema . parse ( payload ) ;
94-
95- return handleImpersonationRequest ( request , id ) ;
96- }
97-
9843export default function AdminDashboardRoute ( ) {
99- const user = useUser ( ) ;
10044 const { users, filters, page, pageCount } = useTypedLoaderData < typeof loader > ( ) as any ;
10145
10246 return (
@@ -174,7 +118,7 @@ export default function AdminDashboardRoute() {
174118 </ TableCell >
175119 < TableCell > { user . admin ? "✅" : "" } </ TableCell >
176120 < TableCell isSticky = { true } >
177- < Form method = "post" reloadDocument >
121+ < Form method = "post" action = "/admin/impersonate" reloadDocument >
178122 < input type = "hidden" name = "id" value = { user . id } />
179123 < Button
180124 type = "submit"
0 commit comments