11import { DEFAULT_BRANCH_ID , getSoleTenancyFromProjectBranch } from "@/lib/tenancies" ;
22import { getPrismaClientForTenancy } from "@/prisma-client" ;
3- import { NextRequest , NextResponse } from "next/server" ;
4- import { validateSupportAuth } from "../../../support-auth" ;
3+ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler" ;
4+ import { yupMixed , yupNumber , yupObject , yupString } from "@stackframe/stack-shared/dist/schema-fields" ;
5+ import { supportAuthSchema , validateSupportTeamMembership } from "../../../support-auth" ;
56
6- // Internal support endpoint for listing users in a project
7- // Protected by Stack Auth session - requires @stack-auth.com email
7+ export const GET = createSmartRouteHandler ( {
8+ metadata : {
9+ hidden : true ,
10+ summary : "List users in a project (Support)" ,
11+ description : "Internal support endpoint for listing users in a project. Requires support team membership." ,
12+ tags : [ "Internal" , "Support" ] ,
13+ } ,
14+ request : yupObject ( {
15+ auth : supportAuthSchema ,
16+ params : yupObject ( {
17+ projectId : yupString ( ) . defined ( ) ,
18+ } ) . defined ( ) ,
19+ query : yupObject ( {
20+ search : yupString ( ) . optional ( ) ,
21+ userId : yupString ( ) . optional ( ) ,
22+ limit : yupString ( ) . optional ( ) ,
23+ offset : yupString ( ) . optional ( ) ,
24+ } ) ,
25+ method : yupString ( ) . oneOf ( [ "GET" ] ) . defined ( ) ,
26+ } ) ,
27+ response : yupObject ( {
28+ statusCode : yupNumber ( ) . oneOf ( [ 200 ] ) . defined ( ) ,
29+ bodyType : yupString ( ) . oneOf ( [ "json" ] ) . defined ( ) ,
30+ body : yupObject ( {
31+ items : yupMixed ( ) . defined ( ) ,
32+ total : yupNumber ( ) . defined ( ) ,
33+ } ) . defined ( ) ,
34+ } ) ,
35+ handler : async ( req , fullReq ) => {
36+ await validateSupportTeamMembership ( fullReq . auth ! ) ;
837
9- export async function GET (
10- request : NextRequest ,
11- { params } : { params : Promise < { projectId : string } > }
12- ) {
13- const auth = await validateSupportAuth ( request ) ;
14- if ( ! auth . success ) {
15- return auth . response ;
16- }
38+ const { projectId } = req . params ;
39+ const search = req . query . search ;
40+ const userId = req . query . userId ;
41+ const limit = Math . min ( 100 , parseInt ( req . query . limit ?? "25" , 10 ) ) ;
42+ const offset = parseInt ( req . query . offset ?? "0" , 10 ) ;
1743
18- const { projectId } = await params ;
19- const searchParams = request . nextUrl . searchParams ;
20- const search = searchParams . get ( "search" ) ?? undefined ;
21- const userId = searchParams . get ( "userId" ) ?? undefined ; // Exact user ID lookup
22- const limit = Math . min ( 100 , parseInt ( searchParams . get ( "limit" ) ?? "25" , 10 ) ) ;
23- const offset = parseInt ( searchParams . get ( "offset" ) ?? "0" , 10 ) ;
24-
25- try {
26- console . log ( `[Support API] Fetching users for project: ${ projectId } , userId: ${ userId ?? 'none' } , search: ${ search ?? 'none' } ` ) ;
2744 const tenancy = await getSoleTenancyFromProjectBranch ( projectId , DEFAULT_BRANCH_ID ) ;
2845 const prisma = await getPrismaClientForTenancy ( tenancy ) ;
2946
3047 // Build search filter - exact userId takes priority
31- const searchFilter = userId
48+ const searchFilter = userId
3249 ? { projectUserId : userId }
3350 : search ? {
34- OR : [
35- { displayName : { contains : search , mode : "insensitive" as const } } ,
36- { projectUserId : { contains : search , mode : "insensitive" as const } } ,
37- {
38- contactChannels : {
39- some : {
40- value : { contains : search , mode : "insensitive" as const } ,
41- } ,
51+ OR : [
52+ { displayName : { contains : search , mode : "insensitive" as const } } ,
53+ { projectUserId : { contains : search , mode : "insensitive" as const } } ,
54+ {
55+ contactChannels : {
56+ some : {
57+ value : { contains : search , mode : "insensitive" as const } ,
4258 } ,
4359 } ,
44- ] ,
45- } : { } ;
60+ } ,
61+ ] ,
62+ } : { } ;
4663
4764 const [ users , total ] = await Promise . all ( [
4865 prisma . projectUser . findMany ( {
@@ -59,11 +76,18 @@ export async function GET(
5976 team : true ,
6077 } ,
6178 } ,
62- authMethods : true ,
79+ authMethods : {
80+ include : {
81+ otpAuthMethod : true ,
82+ passwordAuthMethod : true ,
83+ passkeyAuthMethod : true ,
84+ oauthAuthMethod : true ,
85+ } ,
86+ } ,
6387 contactChannels : {
6488 where : {
6589 type : "EMAIL" ,
66- isPrimary : "TRUE" , // This is a special enum value in Prisma
90+ isPrimary : "TRUE" ,
6791 } ,
6892 } ,
6993 } ,
@@ -77,32 +101,35 @@ export async function GET(
77101 ] ) ;
78102
79103 const items = users . map ( ( user ) => {
80- const primaryEmailChannel = user . contactChannels [ 0 ] ;
104+ const primaryEmailChannel = user . contactChannels . at ( 0 ) ;
81105 return {
82106 id : user . projectUserId ,
83107 displayName : user . displayName ,
84108 primaryEmail : primaryEmailChannel ?. value ?? null ,
85109 primaryEmailVerified : primaryEmailChannel ?. isVerified ?? false ,
86- isAnonymous : user . isAnonymous ?? false ,
110+ isAnonymous : user . isAnonymous ,
87111 createdAt : user . createdAt . toISOString ( ) ,
88112 profileImageUrl : user . profileImageUrl ,
89113 teams : user . teamMembers . map ( ( tm ) => ( {
90114 id : tm . team . teamId ,
91115 displayName : tm . team . displayName ,
92116 } ) ) ,
93- authMethods : user . authMethods . map ( ( am ) => am . authMethodIdentifier ) ,
117+ authMethods : user . authMethods . map ( ( am ) => {
118+ if ( am . oauthAuthMethod ) return `oauth:${ am . oauthAuthMethod . configOAuthProviderId } ` ;
119+ if ( am . passwordAuthMethod ) return 'password' ;
120+ if ( am . passkeyAuthMethod ) return 'passkey' ;
121+ if ( am . otpAuthMethod ) return 'otp' ;
122+ return 'unknown' ;
123+ } ) ,
94124 clientMetadata : user . clientMetadata ,
95125 serverMetadata : user . serverMetadata ,
96126 } ;
97127 } ) ;
98128
99- console . log ( `[Support API] Found ${ total } users` ) ;
100- return NextResponse . json ( { items, total } ) ;
101- } catch ( error ) {
102- console . error ( "[Support API] Error fetching users:" , error ) ;
103- return NextResponse . json (
104- { error : `Internal server error: ${ error instanceof Error ? error . message : String ( error ) } ` } ,
105- { status : 500 }
106- ) ;
107- }
108- }
129+ return {
130+ statusCode : 200 ,
131+ bodyType : "json" ,
132+ body : { items, total } ,
133+ } ;
134+ } ,
135+ } ) ;
0 commit comments