1- import type { ActionFunctionArgs } from "@remix-run/server-runtime" ;
21import { json } from "@remix-run/server-runtime" ;
32import { z } from "zod" ;
43import { prisma } from "~/db.server" ;
54import { removeTeamMember } from "~/models/member.server" ;
6- import { logger } from "~/services/logger.server" ;
7- import {
8- authorizePatOrganizationAccess ,
9- resolveOrganizationForApiUser ,
10- } from "~/services/organizationApiAccess.server" ;
11- import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server" ;
5+ import { resolveOrganizationForApiUser } from "~/services/organizationApiAccess.server" ;
6+ import { createActionPATApiRoute } from "~/services/routeBuilders/apiBuilder.server" ;
127
138const ParamsSchema = z . object ( {
149 orgParam : z . string ( ) ,
1510 memberId : z . string ( ) ,
1611} ) ;
1712
18- export async function action ( { request, params } : ActionFunctionArgs ) {
19- if ( request . method . toUpperCase ( ) !== "DELETE" ) {
20- return json ( { error : "Method Not Allowed" } , { status : 405 } ) ;
21- }
22-
23- const parsedParams = ParamsSchema . safeParse ( params ) ;
24-
25- if ( ! parsedParams . success ) {
26- return json ( { error : "Invalid Params" } , { status : 400 } ) ;
27- }
28-
29- const { orgParam, memberId } = parsedParams . data ;
30-
31- try {
32- const authenticationResult = await authenticateApiRequestWithPersonalAccessToken ( request ) ;
33-
34- if ( ! authenticationResult ) {
35- return json ( { error : "Invalid or Missing Access Token" } , { status : 401 } ) ;
36- }
37-
13+ export const action = createActionPATApiRoute (
14+ {
15+ method : "DELETE" ,
16+ params : ParamsSchema ,
17+ // Resolve the org (id only, no membership) so the plugin can compute the
18+ // caller's role floor for the manage:members gate below.
19+ context : async ( { orgParam } ) => {
20+ const org = await prisma . organization . findFirst ( {
21+ where : { OR : [ { id : orgParam } , { slug : orgParam } ] , deletedAt : null } ,
22+ select : { id : true } ,
23+ } ) ;
24+ return org ? { organizationId : org . id } : { } ;
25+ } ,
26+ authorization : { action : "manage" , resource : ( ) => ( { type : "members" } ) } ,
27+ } ,
28+ async ( { params, authentication } ) => {
29+ // Membership floor: a non-member gets a 404.
3830 const organization = await resolveOrganizationForApiUser ( {
39- orgParam,
40- userId : authenticationResult . userId ,
31+ orgParam : params . orgParam ,
32+ userId : authentication . userId ,
4133 } ) ;
4234
4335 if ( ! organization ) {
4436 return json ( { error : "Organization not found" } , { status : 404 } ) ;
4537 }
4638
47- const denied = await authorizePatOrganizationAccess ( {
48- request,
49- organizationId : organization . id ,
50- resource : "members" ,
51- action : "manage" ,
52- } ) ;
53- if ( denied ) return denied ;
54-
5539 // An org must keep at least one member. The dashboard guards this in the
5640 // UI only; enforce it here since removeTeamMember doesn't.
5741 const memberCount = await prisma . orgMember . count ( {
@@ -61,10 +45,12 @@ export async function action({ request, params }: ActionFunctionArgs) {
6145 return json ( { error : "Cannot remove the last member of an organization" } , { status : 400 } ) ;
6246 }
6347
48+ // removeTeamMember throws ServiceValidationError; the builder maps it to
49+ // its status (e.g. 404 for a member not in this org).
6450 const removed = await removeTeamMember ( {
65- userId : authenticationResult . userId ,
51+ userId : authentication . userId ,
6652 slug : organization . slug ,
67- memberId,
53+ memberId : params . memberId ,
6854 } ) ;
6955
7056 return json ( {
@@ -75,12 +61,5 @@ export async function action({ request, params }: ActionFunctionArgs) {
7561 email : removed . user . email ,
7662 } ,
7763 } ) ;
78- } catch ( error ) {
79- if ( error instanceof Response ) throw error ;
80- if ( error instanceof Error && error . message === "Member not found in this organization" ) {
81- return json ( { error : error . message } , { status : 404 } ) ;
82- }
83- logger . error ( "Failed to remove org member" , { error } ) ;
84- return json ( { error : "Internal Server Error" } , { status : 500 } ) ;
8564 }
86- }
65+ ) ;
0 commit comments