@@ -2,7 +2,7 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react";
22import { conformZodMessage , parseWithZod } from "@conform-to/zod" ;
33import { ExclamationTriangleIcon , FolderIcon , TrashIcon } from "@heroicons/react/20/solid" ;
44import { Form , useActionData , useNavigation } from "@remix-run/react" ;
5- import { type ActionFunction , json } from "@remix-run/server-runtime" ;
5+ import { json } from "@remix-run/server-runtime" ;
66import { z } from "zod" ;
77import { InlineCode } from "~/components/code/InlineCode" ;
88import { MainHorizontallyCenteredContainer } from "~/components/layout/AppLayout" ;
@@ -19,9 +19,10 @@ import { Label } from "~/components/primitives/Label";
1919import { SpinnerWhite } from "~/components/primitives/Spinner" ;
2020import { useProject } from "~/hooks/useProject" ;
2121import { redirectWithErrorMessage , redirectWithSuccessMessage } from "~/models/message.server" ;
22+ import { resolveOrgIdFromSlug } from "~/models/organization.server" ;
2223import { ProjectSettingsService } from "~/services/projectSettings.server" ;
2324import { logger } from "~/services/logger.server" ;
24- import { requireUserId } from "~/services/session.server " ;
25+ import { dashboardAction } from "~/services/routeBuilders/dashboardBuilder " ;
2526import { organizationPath , v3ProjectPath } from "~/utils/pathBuilder" ;
2627import { useState } from "react" ;
2728
@@ -59,98 +60,112 @@ function createSchema(
5960 ] ) ;
6061}
6162
62- export const action : ActionFunction = async ( { request, params } ) => {
63- const userId = await requireUserId ( request ) ;
64- const { organizationSlug, projectParam } = params ;
65- if ( ! organizationSlug || ! projectParam ) {
66- return json (
67- { errors : { body : "organizationSlug and projectParam are required" } } ,
68- { status : 400 }
69- ) ;
70- }
71-
72- const formData = await request . formData ( ) ;
63+ const Params = z . object ( {
64+ organizationSlug : z . string ( ) ,
65+ projectParam : z . string ( ) ,
66+ } ) ;
7367
74- const schema = createSchema ( {
75- getSlugMatch : ( slug ) => {
76- return { isMatch : slug === projectParam , projectSlug : projectParam } ;
68+ export const action = dashboardAction (
69+ {
70+ params : Params ,
71+ context : async ( params ) => {
72+ const orgId = await resolveOrgIdFromSlug ( params . organizationSlug ) ;
73+ return orgId ? { organizationId : orgId } : { } ;
7774 } ,
78- } ) ;
79- const submission = parseWithZod ( formData , { schema } ) ;
75+ } ,
76+ async ( { user, ability, request, params } ) => {
77+ const userId = user . id ;
78+ const { organizationSlug, projectParam } = params ;
8079
81- if ( submission . status !== "success" ) {
82- return json ( submission . reply ( ) ) ;
83- }
80+ const formData = await request . formData ( ) ;
8481
85- const projectSettingsService = new ProjectSettingsService ( ) ;
86- const membershipResultOrFail = await projectSettingsService . verifyProjectMembership (
87- organizationSlug ,
88- projectParam ,
89- userId
90- ) ;
82+ const schema = createSchema ( {
83+ getSlugMatch : ( slug ) => {
84+ return { isMatch : slug === projectParam , projectSlug : projectParam } ;
85+ } ,
86+ } ) ;
87+ const submission = parseWithZod ( formData , { schema } ) ;
9188
92- if ( membershipResultOrFail . isErr ( ) ) {
93- return json ( { errors : { body : membershipResultOrFail . error . type } } , { status : 404 } ) ;
94- }
89+ if ( submission . status !== "success" ) {
90+ return json ( submission . reply ( ) ) ;
91+ }
9592
96- const { projectId } = membershipResultOrFail . value ;
93+ const projectSettingsService = new ProjectSettingsService ( ) ;
94+ const membershipResultOrFail = await projectSettingsService . verifyProjectMembership (
95+ organizationSlug ,
96+ projectParam ,
97+ userId
98+ ) ;
9799
98- switch ( submission . value . action ) {
99- case "rename" : {
100- const resultOrFail = await projectSettingsService . renameProject (
101- projectId ,
102- submission . value . projectName
103- ) ;
100+ if ( membershipResultOrFail . isErr ( ) ) {
101+ return json ( { errors : { body : membershipResultOrFail . error . type } } , { status : 404 } ) ;
102+ }
104103
105- if ( resultOrFail . isErr ( ) ) {
106- switch ( resultOrFail . error . type ) {
107- case "other" :
108- default : {
109- resultOrFail . error . type satisfies "other" ;
104+ const { projectId } = membershipResultOrFail . value ;
110105
111- logger . error ( "Failed to rename project" , {
112- error : resultOrFail . error ,
113- } ) ;
114- return json ( { errors : { body : "Failed to rename project" } } , { status : 400 } ) ;
106+ switch ( submission . value . action ) {
107+ case "rename" : {
108+ if ( ! ability . can ( "manage" , { type : "project" } ) ) {
109+ return json ( { ok : false , error : "Unauthorized" } as const , { status : 403 } ) ;
110+ }
111+ const resultOrFail = await projectSettingsService . renameProject (
112+ projectId ,
113+ submission . value . projectName
114+ ) ;
115+
116+ if ( resultOrFail . isErr ( ) ) {
117+ switch ( resultOrFail . error . type ) {
118+ case "other" :
119+ default : {
120+ resultOrFail . error . type satisfies "other" ;
121+
122+ logger . error ( "Failed to rename project" , {
123+ error : resultOrFail . error ,
124+ } ) ;
125+ return json ( { errors : { body : "Failed to rename project" } } , { status : 400 } ) ;
126+ }
115127 }
116128 }
117- }
118129
119- return redirectWithSuccessMessage (
120- v3ProjectPath ( { slug : organizationSlug } , { slug : projectParam } ) ,
121- request ,
122- `Project renamed to ${ submission . value . projectName } `
123- ) ;
124- }
125- case "delete" : {
126- const resultOrFail = await projectSettingsService . deleteProject ( projectId , userId ) ;
130+ return redirectWithSuccessMessage (
131+ v3ProjectPath ( { slug : organizationSlug } , { slug : projectParam } ) ,
132+ request ,
133+ `Project renamed to ${ submission . value . projectName } `
134+ ) ;
135+ }
136+ case "delete" : {
137+ if ( ! ability . can ( "manage" , { type : "project" } ) ) {
138+ return json ( { ok : false , error : "Unauthorized" } as const , { status : 403 } ) ;
139+ }
140+ const resultOrFail = await projectSettingsService . deleteProject ( projectId , userId ) ;
127141
128- if ( resultOrFail . isErr ( ) ) {
129- switch ( resultOrFail . error . type ) {
130- case "other" :
131- default : {
132- resultOrFail . error . type satisfies "other" ;
142+ if ( resultOrFail . isErr ( ) ) {
143+ switch ( resultOrFail . error . type ) {
144+ case "other" :
145+ default : {
146+ resultOrFail . error . type satisfies "other" ;
133147
134- logger . error ( "Failed to delete project" , {
135- error : resultOrFail . error ,
136- } ) ;
137- return redirectWithErrorMessage (
138- v3ProjectPath ( { slug : organizationSlug } , { slug : projectParam } ) ,
139- request ,
140- `Project ${ projectParam } could not be deleted`
141- ) ;
148+ logger . error ( "Failed to delete project" , {
149+ error : resultOrFail . error ,
150+ } ) ;
151+ return redirectWithErrorMessage (
152+ v3ProjectPath ( { slug : organizationSlug } , { slug : projectParam } ) ,
153+ request ,
154+ `Project ${ projectParam } could not be deleted`
155+ ) ;
156+ }
142157 }
143158 }
144- }
145159
146- return redirectWithSuccessMessage (
147- organizationPath ( { slug : organizationSlug } ) ,
148- request ,
149- "Project deleted"
150- ) ;
160+ return redirectWithSuccessMessage (
161+ organizationPath ( { slug : organizationSlug } ) ,
162+ request ,
163+ "Project deleted"
164+ ) ;
165+ }
151166 }
152167 }
153- } ;
168+ ) ;
154169
155170export default function GeneralSettingsPage ( ) {
156171 const project = useProject ( ) ;
0 commit comments