From ef3be90ec29bef66141c02ff42370c887c1f8630 Mon Sep 17 00:00:00 2001 From: vincanger <70215737+vincanger@users.noreply.github.com> Date: Fri, 17 Nov 2023 12:44:13 +0100 Subject: [PATCH 1/4] add delete user functionality --- wasp-ai/.prettierrc | 6 ++ wasp-ai/main.wasp | 7 +- .../20231117102034_delete_user/migration.sql | 5 ++ wasp-ai/src/client/components/Dialog.jsx | 4 +- .../client/components/WaitingRoomContent.jsx | 2 +- wasp-ai/src/client/pages/ResultPage.jsx | 19 ++-- wasp-ai/src/client/pages/UserPage.jsx | 88 +++++++++++++++++-- wasp-ai/src/server/operations.ts | 24 +++-- 8 files changed, 129 insertions(+), 26 deletions(-) create mode 100644 wasp-ai/.prettierrc create mode 100644 wasp-ai/migrations/20231117102034_delete_user/migration.sql diff --git a/wasp-ai/.prettierrc b/wasp-ai/.prettierrc new file mode 100644 index 0000000000..5bc7e9e888 --- /dev/null +++ b/wasp-ai/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "es5", + "printWidth": 120 +} diff --git a/wasp-ai/main.wasp b/wasp-ai/main.wasp index 78646385a6..db588d6ac6 100644 --- a/wasp-ai/main.wasp +++ b/wasp-ai/main.wasp @@ -106,6 +106,11 @@ action createFeedback { entities: [Feedback] } +action deleteUser { + fn: import { deleteUser } from "@server/operations.js", + entities: [User] +} + query getFeedback { fn: import { getFeedback } from "@server/operations.js", entities: [Feedback] @@ -154,7 +159,7 @@ entity SocialLogin {=psl providerId String userId Int - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) psl=} diff --git a/wasp-ai/migrations/20231117102034_delete_user/migration.sql b/wasp-ai/migrations/20231117102034_delete_user/migration.sql new file mode 100644 index 0000000000..7f4a42b4b8 --- /dev/null +++ b/wasp-ai/migrations/20231117102034_delete_user/migration.sql @@ -0,0 +1,5 @@ +-- DropForeignKey +ALTER TABLE "SocialLogin" DROP CONSTRAINT "SocialLogin_userId_fkey"; + +-- AddForeignKey +ALTER TABLE "SocialLogin" ADD CONSTRAINT "SocialLogin_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/wasp-ai/src/client/components/Dialog.jsx b/wasp-ai/src/client/components/Dialog.jsx index f6e0b7b1f2..c7142000c0 100644 --- a/wasp-ai/src/client/components/Dialog.jsx +++ b/wasp-ai/src/client/components/Dialog.jsx @@ -49,9 +49,9 @@ export function MyDialog({ isOpen, onClose, title, children }) { xmlns="http://www.w3.org/2000/svg" > Close modal diff --git a/wasp-ai/src/client/components/WaitingRoomContent.jsx b/wasp-ai/src/client/components/WaitingRoomContent.jsx index 717e5076c9..ba4e566c43 100644 --- a/wasp-ai/src/client/components/WaitingRoomContent.jsx +++ b/wasp-ai/src/client/components/WaitingRoomContent.jsx @@ -77,7 +77,7 @@ export function WaitingRoomContent(props) {
{showcaseSamples.map((sample) => ( - + ))}
diff --git a/wasp-ai/src/client/pages/ResultPage.jsx b/wasp-ai/src/client/pages/ResultPage.jsx index fc855b4c3d..9272c47e6e 100644 --- a/wasp-ai/src/client/pages/ResultPage.jsx +++ b/wasp-ai/src/client/pages/ResultPage.jsx @@ -392,14 +392,19 @@ export function OnSuccessModal({ isOpen, setIsOpen, appGenerationResult }) { const logText = appGenerationResult?.project?.logs?.find((log) => log.content.includes("tokens usage") )?.content; - const regex = /total tokens usage: ~(\d+(\.\d+)?)/i; - const match = logText?.match(regex); - if (match && match[1]) { - const num = Number(match[1]) * 1000; - if (Number.isInteger(num)) { - setNumTokensSpent(num); + + if (logText) { + const regex = /Total\s+tokens\s+usage\s*:\s*~\s*(\d+(?:\.\d+){0,1})\s*k\b/; + const match = logText.match(regex); + + if (match) { + const num = parseFloat(match[1]); + setNumTokensSpent(num * 1000); + } else { + console.log('No match found'); } } + }, [appGenerationResult]); useEffect(() => { @@ -638,7 +643,7 @@ function Feedback({ projectId }) {
{scoreOptions.map((option) => ( - + {({ active, checked }) => (
+ + +

@@ -34,9 +49,18 @@ export function UserPage({ user }) {

-
- -
+ {isLoading ? ( + "Loading..." + ) : ( +
+ +
+ )} +
+
+
); @@ -63,7 +87,7 @@ function UserProjectsTable({ projects }) { - {projects?.length > 0 && + {!!projects && projects?.length > 0 ? ( projects.map((project) => ( @@ -100,8 +124,54 @@ function UserProjectsTable({ projects }) { - ))} + )) + ) : ( + + + you have not generated any apps yet. + + + )} ); -} \ No newline at end of file +} + +function DeleteUserModal({ isOpen, setIsOpen, deleteUser, user }) { + async function deleteUserHandler() { + await deleteUser({ userId: user.id }); + logout(); + } + + return ( + setIsOpen(false)} + title={ +
+ Are You Sure You Want to Delete Your Account? +
+ } + > +
+

+ You will lose access to your current projects and data. +

+
+ + +
+
+
+ ); +} diff --git a/wasp-ai/src/server/operations.ts b/wasp-ai/src/server/operations.ts index 3ab2fdea1e..674114c502 100644 --- a/wasp-ai/src/server/operations.ts +++ b/wasp-ai/src/server/operations.ts @@ -1,9 +1,9 @@ -import { RegisterZipDownload, StartGeneratingNewApp, CreateFeedback } from "@wasp/actions/types"; +import { RegisterZipDownload, StartGeneratingNewApp, CreateFeedback, DeleteUser } from "@wasp/actions/types"; import { GetAppGenerationResult, GetStats, GetFeedback, GetNumProjects, GetProjectsByUser } from "@wasp/queries/types"; import HttpError from "@wasp/core/HttpError.js"; import { checkPendingAppsJob } from "@wasp/jobs/checkPendingAppsJob.js"; import { getNowInUTC } from "./utils.js"; -import type { Project } from "@wasp/entities"; +import type { Project, User } from "@wasp/entities"; export const startGeneratingNewApp: StartGeneratingNewApp< { @@ -206,10 +206,10 @@ export const getStats = (async (_args, context) => { zipDownloadedAt: true, user: { select: { - email: true - } - } - } + email: true, + }, + }, + }, }); return { @@ -237,3 +237,15 @@ export const getProjectsByUser: GetProjectsByUser = async (_arg }, }); }; + +export const deleteUser: DeleteUser<{ userId: number }, User> = async (args, context) => { + if (!context.user || context.user.id !== args.userId) { + throw new HttpError(401, "Not authorized"); + } + + return await context.entities.User.delete({ + where: { + id: args.userId, + }, + }); +}; From 23640bda58cf663e57d943fa6cc0317e4d926351 Mon Sep 17 00:00:00 2001 From: vincanger <70215737+vincanger@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:35:56 +0100 Subject: [PATCH 2/4] refactor to deleteMyself --- wasp-ai/.prettierrc | 2 +- wasp-ai/main.wasp | 4 ++-- wasp-ai/src/client/pages/ResultPage.jsx | 2 +- wasp-ai/src/client/pages/UserPage.jsx | 9 ++++----- wasp-ai/src/server/operations.ts | 8 ++++---- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/wasp-ai/.prettierrc b/wasp-ai/.prettierrc index 5bc7e9e888..59e8c2d7a7 100644 --- a/wasp-ai/.prettierrc +++ b/wasp-ai/.prettierrc @@ -2,5 +2,5 @@ "semi": true, "singleQuote": false, "trailingComma": "es5", - "printWidth": 120 + "printWidth": 100 } diff --git a/wasp-ai/main.wasp b/wasp-ai/main.wasp index db588d6ac6..18ab868404 100644 --- a/wasp-ai/main.wasp +++ b/wasp-ai/main.wasp @@ -106,8 +106,8 @@ action createFeedback { entities: [Feedback] } -action deleteUser { - fn: import { deleteUser } from "@server/operations.js", +action deleteMyself { + fn: import { deleteMyself } from "@server/operations.js", entities: [User] } diff --git a/wasp-ai/src/client/pages/ResultPage.jsx b/wasp-ai/src/client/pages/ResultPage.jsx index 9272c47e6e..e8850184fd 100644 --- a/wasp-ai/src/client/pages/ResultPage.jsx +++ b/wasp-ai/src/client/pages/ResultPage.jsx @@ -401,7 +401,7 @@ export function OnSuccessModal({ isOpen, setIsOpen, appGenerationResult }) { const num = parseFloat(match[1]); setNumTokensSpent(num * 1000); } else { - console.log('No match found'); + console.log("Failed to parse total number of tokens used: no regex match."); } } diff --git a/wasp-ai/src/client/pages/UserPage.jsx b/wasp-ai/src/client/pages/UserPage.jsx index 38f8951af7..d25200045f 100644 --- a/wasp-ai/src/client/pages/UserPage.jsx +++ b/wasp-ai/src/client/pages/UserPage.jsx @@ -15,7 +15,7 @@ import { projectStatusToDisplayableText, } from "../project/utils"; import { HomeButton } from "../components/Header"; -import deleteUser from "@wasp/actions/deleteUser"; +import deleteMyself from "@wasp/actions/deleteMyself"; import { MyDialog } from "../components/Dialog"; export function UserPage({ user }) { @@ -33,8 +33,7 @@ export function UserPage({ user }) {
@@ -137,9 +136,9 @@ function UserProjectsTable({ projects }) { ); } -function DeleteUserModal({ isOpen, setIsOpen, deleteUser, user }) { +function DeleteUserModal({ isOpen, setIsOpen, deleteUser }) { async function deleteUserHandler() { - await deleteUser({ userId: user.id }); + await deleteUser(); logout(); } diff --git a/wasp-ai/src/server/operations.ts b/wasp-ai/src/server/operations.ts index 674114c502..274ad4214b 100644 --- a/wasp-ai/src/server/operations.ts +++ b/wasp-ai/src/server/operations.ts @@ -1,4 +1,4 @@ -import { RegisterZipDownload, StartGeneratingNewApp, CreateFeedback, DeleteUser } from "@wasp/actions/types"; +import { RegisterZipDownload, StartGeneratingNewApp, CreateFeedback, DeleteMyself } from "@wasp/actions/types"; import { GetAppGenerationResult, GetStats, GetFeedback, GetNumProjects, GetProjectsByUser } from "@wasp/queries/types"; import HttpError from "@wasp/core/HttpError.js"; import { checkPendingAppsJob } from "@wasp/jobs/checkPendingAppsJob.js"; @@ -238,14 +238,14 @@ export const getProjectsByUser: GetProjectsByUser = async (_arg }); }; -export const deleteUser: DeleteUser<{ userId: number }, User> = async (args, context) => { - if (!context.user || context.user.id !== args.userId) { +export const deleteMyself: DeleteMyself = async (args, context) => { + if (!context.user) { throw new HttpError(401, "Not authorized"); } return await context.entities.User.delete({ where: { - id: args.userId, + id: context.user.id, }, }); }; From bd607e0adde299347d52f110b261dc2686b69653 Mon Sep 17 00:00:00 2001 From: vincanger <70215737+vincanger@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:23:39 +0100 Subject: [PATCH 3/4] delete user relevant info from projects --- wasp-ai/main.wasp | 2 +- wasp-ai/src/server/operations.ts | 61 +++++++++++++++++++++++++++----- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/wasp-ai/main.wasp b/wasp-ai/main.wasp index 18ab868404..cb4cd80177 100644 --- a/wasp-ai/main.wasp +++ b/wasp-ai/main.wasp @@ -108,7 +108,7 @@ action createFeedback { action deleteMyself { fn: import { deleteMyself } from "@server/operations.js", - entities: [User] + entities: [User, Project, File, Log] } query getFeedback { diff --git a/wasp-ai/src/server/operations.ts b/wasp-ai/src/server/operations.ts index 274ad4214b..70bb70e0ad 100644 --- a/wasp-ai/src/server/operations.ts +++ b/wasp-ai/src/server/operations.ts @@ -1,5 +1,16 @@ -import { RegisterZipDownload, StartGeneratingNewApp, CreateFeedback, DeleteMyself } from "@wasp/actions/types"; -import { GetAppGenerationResult, GetStats, GetFeedback, GetNumProjects, GetProjectsByUser } from "@wasp/queries/types"; +import { + RegisterZipDownload, + StartGeneratingNewApp, + CreateFeedback, + DeleteMyself, +} from "@wasp/actions/types"; +import { + GetAppGenerationResult, + GetStats, + GetFeedback, + GetNumProjects, + GetProjectsByUser, +} from "@wasp/queries/types"; import HttpError from "@wasp/core/HttpError.js"; import { checkPendingAppsJob } from "@wasp/jobs/checkPendingAppsJob.js"; import { getNowInUTC } from "./utils.js"; @@ -242,10 +253,44 @@ export const deleteMyself: DeleteMyself = async (args, context) => { if (!context.user) { throw new HttpError(401, "Not authorized"); } - - return await context.entities.User.delete({ - where: { - id: context.user.id, - }, - }); + try { + await context.entities.Log.deleteMany({ + where: { + project: { + user: { + id: context.user.id, + }, + }, + }, + }); + await context.entities.File.deleteMany({ + where: { + project: { + user: { + id: context.user.id, + }, + }, + }, + }); + await context.entities.Project.updateMany({ + where: { + user: { + id: context.user.id, + }, + }, + data: { + zipDownloadedAt: undefined, + name: "Deleted project", + description: "Deleted project", + }, + }); + return await context.entities.User.delete({ + where: { + id: context.user.id, + }, + }); + } catch (error) { + console.error(error); + throw new HttpError(500, "Error deleting user"); + } }; From 6516c62b5d266123170fe2394b6b3f078a575580 Mon Sep 17 00:00:00 2001 From: vincanger <70215737+vincanger@users.noreply.github.com> Date: Tue, 21 Nov 2023 15:07:19 +0100 Subject: [PATCH 4/4] update status to deleted --- wasp-ai/src/client/components/StatusPill.jsx | 1 + wasp-ai/src/client/pages/ResultPage.jsx | 129 ++++++++++++++----- wasp-ai/src/server/operations.ts | 1 + 3 files changed, 100 insertions(+), 31 deletions(-) diff --git a/wasp-ai/src/client/components/StatusPill.jsx b/wasp-ai/src/client/components/StatusPill.jsx index fc8234a755..ab5e2d04a2 100644 --- a/wasp-ai/src/client/components/StatusPill.jsx +++ b/wasp-ai/src/client/components/StatusPill.jsx @@ -7,6 +7,7 @@ export function StatusPill({ children, status, className = "", sm = false }) { error: "bg-red-100 text-red-700", cancelled: "bg-red-100 text-red-700", warning: "bg-yellow-100 text-yellow-700", + deleted: "bg-red-100 text-red-700", }; return (
diff --git a/wasp-ai/src/client/pages/ResultPage.jsx b/wasp-ai/src/client/pages/ResultPage.jsx index e8850184fd..c73d092179 100644 --- a/wasp-ai/src/client/pages/ResultPage.jsx +++ b/wasp-ai/src/client/pages/ResultPage.jsx @@ -42,7 +42,11 @@ export const ResultPage = () => { data: appGenerationResult, isError, isLoading, - } = useQuery(getAppGenerationResult, { appId }, { enabled: !!appId && !generationDone, refetchInterval: 3000 }); + } = useQuery( + getAppGenerationResult, + { appId }, + { enabled: !!appId && !generationDone, refetchInterval: 3000 } + ); const [activeFilePath, setActiveFilePath] = useState(null); const [currentStatus, setCurrentStatus] = useState({ status: "idle", @@ -211,7 +215,10 @@ export const ResultPage = () => { return (
-
+
@@ -225,7 +232,8 @@ export const ResultPage = () => { {isError && (
- We couldn't find the app generation result. Maybe the link is incorrect or the app generation has failed. + We couldn't find the app generation result. Maybe the link is incorrect or the app + generation has failed.
Generate a new one @@ -244,7 +252,16 @@ export const ResultPage = () => { )} - + {appGenerationResult?.project.status.includes("deleted") ? ( +
+ This app has been deleted. + + ← Go back and generate a new app + +
+ ) : ( + + )}
- 🔮 This is a Wasp powered project. If you like it, star us on GitHub! + 🔮 This is a Wasp powered project. If you like it,{" "} + star us on GitHub!
{currentStatus.status === "pending" && ( - + )} {interestingFilePaths.length > 0 && ( <>
-

{appGenerationResult?.project?.name}

+

+ {appGenerationResult?.project?.name} +

-