Skip to content

Commit ed44d75

Browse files
feat:Add delete project functionality in Project,Project-settings Issue Is:-No option to delete a project stack-auth#111 (stack-auth#127)
* Add delete project functionality in Project,Project-settings * removed changes to the old dashboard * added onDelete and backend endpoints --------- Co-authored-by: Zai Shi <zaishi00@outlook.com>
1 parent e5965cf commit ed44d75

11 files changed

Lines changed: 175 additions & 39 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
-- DropForeignKey
2+
ALTER TABLE "OAuthProviderConfig" DROP CONSTRAINT "OAuthProviderConfig_projectConfigId_fkey";
3+
4+
-- DropForeignKey
5+
ALTER TABLE "Permission" DROP CONSTRAINT "Permission_projectConfigId_fkey";
6+
7+
-- DropForeignKey
8+
ALTER TABLE "Project" DROP CONSTRAINT "Project_configId_fkey";
9+
10+
-- DropForeignKey
11+
ALTER TABLE "ProjectConfigOverride" DROP CONSTRAINT "ProjectConfigOverride_projectId_fkey";
12+
13+
-- DropForeignKey
14+
ALTER TABLE "ProjectUserOAuthAccount" DROP CONSTRAINT "ProjectUserOAuthAccount_projectConfigId_oauthProviderConfi_fkey";
15+
16+
-- DropForeignKey
17+
ALTER TABLE "ProxiedOAuthProviderConfig" DROP CONSTRAINT "ProxiedOAuthProviderConfig_projectConfigId_id_fkey";
18+
19+
-- DropForeignKey
20+
ALTER TABLE "StandardOAuthProviderConfig" DROP CONSTRAINT "StandardOAuthProviderConfig_projectConfigId_id_fkey";
21+
22+
-- AddForeignKey
23+
ALTER TABLE "Project" ADD CONSTRAINT "Project_configId_fkey" FOREIGN KEY ("configId") REFERENCES "ProjectConfig"("id") ON DELETE CASCADE ON UPDATE CASCADE;
24+
25+
-- AddForeignKey
26+
ALTER TABLE "ProjectConfigOverride" ADD CONSTRAINT "ProjectConfigOverride_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
27+
28+
-- AddForeignKey
29+
ALTER TABLE "Permission" ADD CONSTRAINT "Permission_projectConfigId_fkey" FOREIGN KEY ("projectConfigId") REFERENCES "ProjectConfig"("id") ON DELETE CASCADE ON UPDATE CASCADE;
30+
31+
-- AddForeignKey
32+
ALTER TABLE "ProjectUserOAuthAccount" ADD CONSTRAINT "ProjectUserOAuthAccount_projectConfigId_oauthProviderConfi_fkey" FOREIGN KEY ("projectConfigId", "oauthProviderConfigId") REFERENCES "OAuthProviderConfig"("projectConfigId", "id") ON DELETE CASCADE ON UPDATE CASCADE;
33+
34+
-- AddForeignKey
35+
ALTER TABLE "OAuthProviderConfig" ADD CONSTRAINT "OAuthProviderConfig_projectConfigId_fkey" FOREIGN KEY ("projectConfigId") REFERENCES "ProjectConfig"("id") ON DELETE CASCADE ON UPDATE CASCADE;
36+
37+
-- AddForeignKey
38+
ALTER TABLE "ProxiedOAuthProviderConfig" ADD CONSTRAINT "ProxiedOAuthProviderConfig_projectConfigId_id_fkey" FOREIGN KEY ("projectConfigId", "id") REFERENCES "OAuthProviderConfig"("projectConfigId", "id") ON DELETE CASCADE ON UPDATE CASCADE;
39+
40+
-- AddForeignKey
41+
ALTER TABLE "StandardOAuthProviderConfig" ADD CONSTRAINT "StandardOAuthProviderConfig_projectConfigId_id_fkey" FOREIGN KEY ("projectConfigId", "id") REFERENCES "OAuthProviderConfig"("projectConfigId", "id") ON DELETE CASCADE ON UPDATE CASCADE;

apps/backend/prisma/schema.prisma

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ model Project {
2121
displayName String
2222
description String? @default("")
2323
configId String @db.Uuid
24-
config ProjectConfig @relation(fields: [configId], references: [id])
24+
config ProjectConfig @relation(fields: [configId], references: [id], onDelete: Cascade)
2525
configOverride ProjectConfigOverride?
2626
isProductionMode Boolean
2727
@@ -83,7 +83,7 @@ model ProjectConfigOverride {
8383
createdAt DateTime @default(now())
8484
updatedAt DateTime @updatedAt
8585
86-
project Project @relation(fields: [projectId], references: [id])
86+
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
8787
}
8888

8989
model Team {
@@ -170,7 +170,7 @@ model Permission {
170170
171171
// The scope of the permission. If projectConfigId is set, may be GLOBAL or TEAM; if teamId is set, must be TEAM.
172172
scope PermissionScope
173-
projectConfig ProjectConfig? @relation(fields: [projectConfigId], references: [id])
173+
projectConfig ProjectConfig? @relation(fields: [projectConfigId], references: [id], onDelete: Cascade)
174174
team Team? @relation(fields: [projectId, teamId], references: [projectId, teamId], onDelete: Cascade)
175175
176176
parentEdges PermissionEdge[] @relation("ChildPermission")
@@ -258,7 +258,7 @@ model ProjectUserOAuthAccount {
258258
createdAt DateTime @default(now())
259259
updatedAt DateTime @updatedAt
260260
261-
providerConfig OAuthProviderConfig @relation(fields: [projectConfigId, oauthProviderConfigId], references: [projectConfigId, id])
261+
providerConfig OAuthProviderConfig @relation(fields: [projectConfigId, oauthProviderConfigId], references: [projectConfigId, id], onDelete: Cascade)
262262
projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade)
263263
oauthTokens OAuthToken[]
264264
oauthAccessToken OAuthAccessToken[]
@@ -514,7 +514,7 @@ model StandardEmailServiceConfig {
514514
// Exactly one of the xyzOAuthConfig variables should be set.
515515
model OAuthProviderConfig {
516516
projectConfigId String @db.Uuid
517-
projectConfig ProjectConfig @relation(fields: [projectConfigId], references: [id])
517+
projectConfig ProjectConfig @relation(fields: [projectConfigId], references: [id], onDelete: Cascade)
518518
id String
519519
520520
createdAt DateTime @default(now())
@@ -531,7 +531,7 @@ model OAuthProviderConfig {
531531

532532
model ProxiedOAuthProviderConfig {
533533
projectConfigId String @db.Uuid
534-
providerConfig OAuthProviderConfig @relation(fields: [projectConfigId, id], references: [projectConfigId, id])
534+
providerConfig OAuthProviderConfig @relation(fields: [projectConfigId, id], references: [projectConfigId, id], onDelete: Cascade)
535535
id String
536536
createdAt DateTime @default(now())
537537
updatedAt DateTime @updatedAt
@@ -552,7 +552,7 @@ enum ProxiedOAuthProviderType {
552552

553553
model StandardOAuthProviderConfig {
554554
projectConfigId String @db.Uuid
555-
providerConfig OAuthProviderConfig @relation(fields: [projectConfigId, id], references: [projectConfigId, id])
555+
providerConfig OAuthProviderConfig @relation(fields: [projectConfigId, id], references: [projectConfigId, id], onDelete: Cascade)
556556
id String
557557
createdAt DateTime @default(now())
558558
updatedAt DateTime @updatedAt

apps/backend/src/app/api/v1/projects/current/crud.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,4 +278,26 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro
278278
onRead: async ({ auth }) => {
279279
return auth.project;
280280
},
281+
onDelete: async ({ auth }) => {
282+
await prismaClient.$transaction(async (tx) => {
283+
const configs = await tx.projectConfig.findMany({
284+
where: {
285+
id: auth.project.config.id
286+
},
287+
include: {
288+
projects: true
289+
}
290+
});
291+
292+
if (configs.length !== 1) {
293+
throw new StatusError(StatusError.NotFound, 'Project config not found');
294+
}
295+
296+
await tx.projectConfig.delete({
297+
where: {
298+
id: auth.project.config.id
299+
},
300+
});
301+
});
302+
}
281303
}));

apps/backend/src/app/api/v1/projects/current/route.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ import { projectsCrudHandlers } from "./crud";
22

33
export const GET = projectsCrudHandlers.readHandler;
44
export const PATCH = projectsCrudHandlers.updateHandler;
5+
export const DELETE = projectsCrudHandlers.deleteHandler;

apps/dashboard/prisma/schema.prisma

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ model Project {
2121
displayName String
2222
description String? @default("")
2323
configId String @db.Uuid
24-
config ProjectConfig @relation(fields: [configId], references: [id])
24+
config ProjectConfig @relation(fields: [configId], references: [id], onDelete: Cascade)
2525
configOverride ProjectConfigOverride?
2626
isProductionMode Boolean
2727
@@ -83,7 +83,7 @@ model ProjectConfigOverride {
8383
createdAt DateTime @default(now())
8484
updatedAt DateTime @updatedAt
8585
86-
project Project @relation(fields: [projectId], references: [id])
86+
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
8787
}
8888

8989
model Team {
@@ -170,7 +170,7 @@ model Permission {
170170
171171
// The scope of the permission. If projectConfigId is set, may be GLOBAL or TEAM; if teamId is set, must be TEAM.
172172
scope PermissionScope
173-
projectConfig ProjectConfig? @relation(fields: [projectConfigId], references: [id])
173+
projectConfig ProjectConfig? @relation(fields: [projectConfigId], references: [id], onDelete: Cascade)
174174
team Team? @relation(fields: [projectId, teamId], references: [projectId, teamId], onDelete: Cascade)
175175
176176
parentEdges PermissionEdge[] @relation("ChildPermission")
@@ -258,7 +258,7 @@ model ProjectUserOAuthAccount {
258258
createdAt DateTime @default(now())
259259
updatedAt DateTime @updatedAt
260260
261-
providerConfig OAuthProviderConfig @relation(fields: [projectConfigId, oauthProviderConfigId], references: [projectConfigId, id])
261+
providerConfig OAuthProviderConfig @relation(fields: [projectConfigId, oauthProviderConfigId], references: [projectConfigId, id], onDelete: Cascade)
262262
projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade)
263263
oauthTokens OAuthToken[]
264264
oauthAccessToken OAuthAccessToken[]
@@ -514,7 +514,7 @@ model StandardEmailServiceConfig {
514514
// Exactly one of the xyzOAuthConfig variables should be set.
515515
model OAuthProviderConfig {
516516
projectConfigId String @db.Uuid
517-
projectConfig ProjectConfig @relation(fields: [projectConfigId], references: [id])
517+
projectConfig ProjectConfig @relation(fields: [projectConfigId], references: [id], onDelete: Cascade)
518518
id String
519519
520520
createdAt DateTime @default(now())
@@ -531,7 +531,7 @@ model OAuthProviderConfig {
531531

532532
model ProxiedOAuthProviderConfig {
533533
projectConfigId String @db.Uuid
534-
providerConfig OAuthProviderConfig @relation(fields: [projectConfigId, id], references: [projectConfigId, id])
534+
providerConfig OAuthProviderConfig @relation(fields: [projectConfigId, id], references: [projectConfigId, id], onDelete: Cascade)
535535
id String
536536
createdAt DateTime @default(now())
537537
updatedAt DateTime @updatedAt
@@ -552,7 +552,7 @@ enum ProxiedOAuthProviderType {
552552

553553
model StandardOAuthProviderConfig {
554554
projectConfigId String @db.Uuid
555-
providerConfig OAuthProviderConfig @relation(fields: [projectConfigId, id], references: [projectConfigId, id])
555+
providerConfig OAuthProviderConfig @relation(fields: [projectConfigId, id], references: [projectConfigId, id], onDelete: Cascade)
556556
id String
557557
createdAt DateTime @default(now())
558558
updatedAt DateTime @updatedAt

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { InputField } from "@/components/form-fields";
33
import { StyledLink } from "@/components/link";
44
import { FormSettingCard, SettingCard, SettingSwitch } from "@/components/settings";
5-
import { Alert, Typography } from "@stackframe/stack-ui";
5+
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, ActionDialog, Alert, Button, Typography } from "@stackframe/stack-ui";
66
import * as yup from "yup";
77
import { PageLayout } from "../page-layout";
88
import { useAdminApp } from "../use-admin-app";
@@ -12,29 +12,37 @@ const projectInformationSchema = yup.object().shape({
1212
description: yup.string(),
1313
});
1414

15-
1615
export default function PageClient() {
1716
const stackAdminApp = useAdminApp();
1817
const project = stackAdminApp.useProject();
1918
const productionModeErrors = project.useProductionModeErrors();
2019

2120
return (
2221
<PageLayout title="Project Settings" description="Manage your project">
23-
<SettingCard title="Production mode" description="Production mode disallows certain configuration options that are useful for development but deemed unsafe for production usage. To prevent accidental misconfigurations, it is strongly recommended to enable production mode on your production environments.">
22+
<SettingCard
23+
title="Production mode"
24+
description="Production mode disallows certain configuration options that are useful for development but deemed unsafe for production usage. To prevent accidental misconfigurations, it is strongly recommended to enable production mode on your production environments."
25+
>
2426
<SettingSwitch
2527
label="Enable production mode"
2628
checked={project.isProductionMode}
27-
disabled={!project.isProductionMode && productionModeErrors.length > 0}
28-
onCheckedChange={async (checked) => { await project.update({ isProductionMode: checked }); }}
29+
disabled={
30+
!project.isProductionMode && productionModeErrors.length > 0
31+
}
32+
onCheckedChange={async (checked) => {
33+
await project.update({ isProductionMode: checked });
34+
}}
2935
/>
3036

3137
{productionModeErrors.length === 0 ? (
3238
<Alert>
33-
Your configuration is ready for production and production mode can be enabled. Good job!
39+
Your configuration is ready for production and production mode can
40+
be enabled. Good job!
3441
</Alert>
3542
) : (
36-
<Alert variant='destructive'>
37-
Your configuration is not ready for production mode. Please fix the following issues:
43+
<Alert variant="destructive">
44+
Your configuration is not ready for production mode. Please fix the
45+
following issues:
3846
<ul className="mt-2 list-disc pl-5">
3947
{productionModeErrors.map((error) => (
4048
<li key={error.message}>
@@ -53,18 +61,61 @@ export default function PageClient() {
5361
description: project.description || undefined,
5462
}}
5563
formSchema={projectInformationSchema}
56-
onSubmit={async (values) => { await project.update(values); }}
64+
onSubmit={async (values) => {
65+
await project.update(values);
66+
}}
5767
render={(form) => (
5868
<>
59-
<InputField label="Display Name" control={form.control} name="displayName" required />
60-
<InputField label="Description" control={form.control} name="description" />
69+
<InputField
70+
label="Display Name"
71+
control={form.control}
72+
name="displayName"
73+
required
74+
/>
75+
<InputField
76+
label="Description"
77+
control={form.control}
78+
name="description"
79+
/>
6180

6281
<Typography variant="secondary" type="footnote">
63-
The display name and description may be publicly visible to the users of your app.
82+
The display name and description may be publicly visible to the
83+
users of your app.
6484
</Typography>
6585
</>
6686
)}
6787
/>
88+
89+
<SettingCard
90+
title="Danger Zone"
91+
description="Be careful with the options in this section. They can have irreversible effects."
92+
>
93+
<Accordion type="single" collapsible className="w-full">
94+
<AccordionItem value="item-1">
95+
<AccordionTrigger>Delete project</AccordionTrigger>
96+
<AccordionContent>
97+
<ActionDialog
98+
trigger={<Button variant="destructive">Delete Project</Button>}
99+
title="Delete domain"
100+
danger
101+
okButton={{
102+
label: "Delete Project",
103+
onClick: async () => {
104+
await project.delete();
105+
await stackAdminApp.redirectToHome();
106+
}
107+
}}
108+
cancelButton
109+
confirmText="I understand this action is IRREVERSIBLE and will delete ALL associated data."
110+
>
111+
<Typography>
112+
{`Are you sure that you want to delete the project with name "${project.displayName}" and ID "${project.id}"? This action is irreversible and will delete all associated data (including users, teams, API keys, project configs, etc.).`}
113+
</Typography>
114+
</ActionDialog>
115+
</AccordionContent>
116+
</AccordionItem>
117+
</Accordion>
118+
</SettingCard>
68119
</PageLayout>
69120
);
70121
}

apps/dashboard/src/app/api/v1/projects/route.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,4 @@ export const POST = deprecatedSmartRouteHandler(async (req: NextRequest) => {
6363
const project = await createProject(projectUser, typedUpdate);
6464

6565
return NextResponse.json(project);
66-
});
66+
});

apps/dashboard/src/lib/projects.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -812,4 +812,4 @@ export const projectSchemaToCreateOptions = (
812812
...projectSchemaToUpdateOptions(create),
813813
displayName: create.displayName,
814814
};
815-
};
815+
};

packages/stack-shared/src/interface/adminInterface.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,14 @@ export class StackAdminInterface extends StackServerInterface {
205205
);
206206
return await response.json();
207207
}
208+
209+
async deleteProject(): Promise<void> {
210+
await this.sendAdminRequest(
211+
"/projects/current",
212+
{
213+
method: "DELETE",
214+
},
215+
null,
216+
);
217+
}
208218
}

0 commit comments

Comments
 (0)