diff --git a/app/features/auth/actions/register.ts b/app/features/auth/actions/register.ts index 564dc06..2404ec1 100644 --- a/app/features/auth/actions/register.ts +++ b/app/features/auth/actions/register.ts @@ -2,6 +2,7 @@ import { type ActionFunction, json } from "@remix-run/node"; import { GlobalRole, MembershipRole } from "@prisma/client"; import db from "~/utils/db.server"; +import logger from "~/utils/logger.server"; import { authenticate, hashPassword } from "~/utils/auth.server"; import { type FormError, validate } from "~/utils/validation.server"; import { Register } from "../validations"; @@ -17,7 +18,7 @@ const action: ActionFunction = async ({ request }) => { return json({ errors: validation.errors }); } - const { orgName, fullName, email, password } = validation.data; + const { fullName, email, password } = validation.data; const hashedPassword = await hashPassword(password.trim()); try { await db.user.create({ @@ -30,13 +31,15 @@ const action: ActionFunction = async ({ request }) => { create: { role: MembershipRole.OWNER, organization: { - create: { name: orgName }, + create: {} }, }, }, }, }); } catch (error: any) { + logger.error(error); + if (error.code === "P2002") { if (error.meta.target[0] === "email") { return json({ diff --git a/app/features/auth/pages/register.tsx b/app/features/auth/pages/register.tsx index 89f5697..5ae4dcc 100644 --- a/app/features/auth/pages/register.tsx +++ b/app/features/auth/pages/register.tsx @@ -37,21 +37,13 @@ export default function RegisterPage() { ) : null} - diff --git a/app/features/auth/validations.ts b/app/features/auth/validations.ts index ef0b9b9..9a469b5 100644 --- a/app/features/auth/validations.ts +++ b/app/features/auth/validations.ts @@ -3,7 +3,6 @@ import { z } from "zod"; export const password = z.string().min(10).max(100); export const Register = z.object({ - orgName: z.string().nonempty(), fullName: z.string().nonempty(), email: z.string().email(), password, diff --git a/app/features/core/components/labeled-text-field.tsx b/app/features/core/components/labeled-text-field.tsx index b4550de..bf30707 100644 --- a/app/features/core/components/labeled-text-field.tsx +++ b/app/features/core/components/labeled-text-field.tsx @@ -7,7 +7,7 @@ type Props = { sideLabel?: ReactNode; type?: "text" | "password" | "email"; error?: string; -} & InputHTMLAttributes; +} & Omit, "name" | "type">; const LabeledTextField: FunctionComponent = ({ name, label, sideLabel, type = "text", error, ...props }) => { const hasSideLabel = !!sideLabel; @@ -19,6 +19,7 @@ const LabeledTextField: FunctionComponent = ({ name, label, sideLabel, ty className={clsx("text-sm font-medium leading-5 text-gray-700", { block: !hasSideLabel, "flex justify-between": hasSideLabel, + // "text-red-600": !!error, })} > {label} @@ -29,7 +30,10 @@ const LabeledTextField: FunctionComponent = ({ name, label, sideLabel, ty id={name} name={name} type={type} - className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:shadow-outline-primary focus:border-primary-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5" + className={clsx("appearance-none block w-full px-3 py-2 border rounded-md placeholder-gray-400 focus:outline-none focus:shadow-outline-primary focus:border-primary-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5", { + "border-gray-300": !error, + "border-red-300": error, + })} required {...props} /> diff --git a/app/features/settings/actions/account.ts b/app/features/settings/actions/account.ts new file mode 100644 index 0000000..cdb2f42 --- /dev/null +++ b/app/features/settings/actions/account.ts @@ -0,0 +1,126 @@ +import { type ActionFunction, json, redirect } from "@remix-run/node"; +import { badRequest } from "remix-utils"; +import { z } from "zod"; +import SecurePassword from "secure-password"; + +import db from "~/utils/db.server"; +import logger from "~/utils/logger.server"; +import { hashPassword, requireLoggedIn, verifyPassword } from "~/utils/auth.server"; +import { type FormError, validate } from "~/utils/validation.server"; +import { destroySession, getSession } from "~/utils/session.server"; +import deleteUserQueue from "~/queues/delete-user-data.server"; + +const action: ActionFunction = async ({ request }) => { + const formData = Object.fromEntries(await request.formData()); + if (!formData._action) { + const errorMessage = "POST /settings without any _action"; + logger.error(errorMessage); + return badRequest({ errorMessage }); + } + + switch (formData._action as Action) { + case "deleteUser": + return deleteUser(request); + case "changePassword": + return changePassword(request, formData); + case "updateUser": + return updateUser(request, formData); + default: + const errorMessage = `POST /settings with an invalid _action=${formData._action}`; + logger.error(errorMessage); + return badRequest({ errorMessage }); + } +}; + +export default action; + +async function deleteUser(request: Request) { + const { id } = await requireLoggedIn(request); + + await db.user.update({ + where: { id }, + data: { hashedPassword: "pending deletion" }, + }); + await deleteUserQueue.add(`delete user ${id}`, { userId: id }); + + return redirect("/", { + headers: { + "Set-Cookie": await destroySession(await getSession(request)), + }, + }); +} + +type ChangePasswordFailureActionData = { errors: FormError; submitted?: never }; +type ChangePasswordSuccessfulActionData = { errors?: never; submitted: true }; +export type ChangePasswordActionData = { + changePassword: ChangePasswordFailureActionData | ChangePasswordSuccessfulActionData; +}; + +async function changePassword(request: Request, formData: unknown) { + const validation = validate(validations.changePassword, formData); + if (validation.errors) { + return json({ + changePassword: { errors: validation.errors }, + }); + } + + const { id } = await requireLoggedIn(request); + const user = await db.user.findUnique({ where: { id } }); + const { currentPassword, newPassword } = validation.data; + const verificationResult = await verifyPassword(user!.hashedPassword!, currentPassword); + if ([SecurePassword.INVALID, SecurePassword.INVALID_UNRECOGNIZED_HASH, false].includes(verificationResult)) { + return json({ + changePassword: { errors: { currentPassword: "Current password is incorrect" } }, + }); + } + + const hashedPassword = await hashPassword(newPassword.trim()); + await db.user.update({ + where: { id: user!.id }, + data: { hashedPassword }, + }); + + return json({ + changePassword: { submitted: true }, + }); +} + +type UpdateUserFailureActionData = { errors: FormError; submitted?: never }; +type UpdateUserSuccessfulActionData = { errors?: never; submitted: true }; +export type UpdateUserActionData = { + updateUser: UpdateUserFailureActionData | UpdateUserSuccessfulActionData; +}; + +async function updateUser(request: Request, formData: unknown) { + const validation = validate(validations.updateUser, formData); + if (validation.errors) { + return json({ + updateUser: { errors: validation.errors }, + }); + } + + const user = await requireLoggedIn(request); + const { email, fullName } = validation.data; + await db.user.update({ + where: { id: user.id }, + data: { email, fullName }, + }); + + return json({ + updateUser: { submitted: true }, + }); +} + +type Action = "deleteUser" | "updateUser" | "changePassword"; + +const validations = { + deleteUser: null, + changePassword: z.object({ + currentPassword: z.string(), + newPassword: z.string().min(10).max(100), + }), + updateUser: z.object({ + fullName: z.string(), + email: z.string(), + }), +} as const; diff --git a/app/features/settings/components/account/danger-zone.tsx b/app/features/settings/components/account/danger-zone.tsx index f549ca9..9c843d0 100644 --- a/app/features/settings/components/account/danger-zone.tsx +++ b/app/features/settings/components/account/danger-zone.tsx @@ -1,4 +1,5 @@ import { useRef, useState } from "react"; +import { useFetcher, useSubmit, useTransition } from "@remix-run/react"; import clsx from "clsx"; import Button from "../button"; @@ -6,9 +7,14 @@ import SettingsSection from "../settings-section"; import Modal, { ModalTitle } from "~/features/core/components/modal"; export default function DangerZone() { - const [isDeletingUser, setIsDeletingUser] = useState(false); + const transition = useTransition(); + const isCurrentFormTransition = transition.submission?.formData.get("_action") === "deleteUser"; + const isDeletingUser = isCurrentFormTransition && transition.state === "submitting"; const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false); const modalCancelButtonRef = useRef(null); + const fetcher = useFetcher(); + const submit = useSubmit(); + // TODO const closeModal = () => { if (isDeletingUser) { @@ -17,10 +23,6 @@ export default function DangerZone() { setIsConfirmationModalOpen(false); }; - const onConfirm = () => { - setIsDeletingUser(true); - // return deleteUserMutation(); // TODO - }; return ( @@ -63,7 +65,6 @@ export default function DangerZone() { "bg-red-600 hover:bg-red-700": !isDeletingUser, }, )} - onClick={onConfirm} disabled={isDeletingUser} > Delete my account diff --git a/app/features/settings/components/account/profile-informations.tsx b/app/features/settings/components/account/profile-informations.tsx index d724af1..6327eea 100644 --- a/app/features/settings/components/account/profile-informations.tsx +++ b/app/features/settings/components/account/profile-informations.tsx @@ -1,47 +1,48 @@ import type { FunctionComponent } from "react"; -import { useActionData, useTransition } from "@remix-run/react"; +import { Form, useActionData, useTransition } from "@remix-run/react"; -import Alert from "../../../core/components/alert"; +import type { UpdateUserActionData } from "~/features/settings/actions/account"; +import useSession from "~/features/core/hooks/use-session"; +import Alert from "~/features/core/components/alert"; import Button from "../button"; import SettingsSection from "../settings-section"; -import useSession from "~/features/core/hooks/use-session"; const ProfileInformations: FunctionComponent = () => { const user = useSession(); const transition = useTransition(); - const actionData = useActionData(); + const actionData = useActionData()?.updateUser; - const isSubmitting = transition.state === "submitting"; - const isSuccess = actionData?.submitted === true; - const error = actionData?.error; - const isError = !!error; - - const onSubmit = async () => { - // await updateUserMutation({ email, fullName }); // TODO - }; + const errors = actionData?.errors; + const topErrorMessage = errors?.general; + const isError = typeof topErrorMessage !== "undefined"; + const isSuccess = actionData?.submitted; + const isCurrentFormTransition = transition.submission?.formData.get("_action") === "updateUser"; + const isSubmitting = isCurrentFormTransition && transition.state === "submitting"; + console.log("isSuccess", isSuccess, actionData); return ( -
+ } > {isError ? (
- +
) : null} - {isSuccess ? ( + {isSuccess && (
- ) : null} + )} +
+ +
-
+ ); }; diff --git a/app/features/settings/components/account/update-password.tsx b/app/features/settings/components/account/update-password.tsx index 9a32827..8b23442 100644 --- a/app/features/settings/components/account/update-password.tsx +++ b/app/features/settings/components/account/update-password.tsx @@ -1,37 +1,36 @@ import type { FunctionComponent } from "react"; +import { Form, useActionData, useTransition } from "@remix-run/react"; +import type { ChangePasswordActionData } from "~/features/settings/actions/account"; import Alert from "~/features/core/components/alert"; +import LabeledTextField from "~/features/core/components/labeled-text-field"; import Button from "../button"; import SettingsSection from "../settings-section"; -import { useActionData, useTransition } from "@remix-run/react"; const UpdatePassword: FunctionComponent = () => { const transition = useTransition(); - const actionData = useActionData(); + const actionData = useActionData()?.changePassword; - const isSubmitting = transition.state === "submitting"; - const isSuccess = actionData?.submitted === true; - const error = actionData?.error; - const isError = !!error; - - const onSubmit = async () => { - // await changePasswordMutation({ currentPassword, newPassword }); // TODO - }; + const topErrorMessage = actionData?.errors?.general; + const isError = typeof topErrorMessage !== "undefined"; + const isSuccess = actionData?.submitted; + const isCurrentFormTransition = transition.submission?.formData.get("_action") === "changePassword"; + const isSubmitting = isCurrentFormTransition && transition.state === "submitting"; return ( -
+ } > {isError ? (
- +
) : null} @@ -40,45 +39,28 @@ const UpdatePassword: FunctionComponent = () => { ) : null} -
- -
- -
-
-
- -
- -
-
+ + + + +
-
+ ); }; diff --git a/app/features/settings/components/button.tsx b/app/features/settings/components/button.tsx index 3999604..dbf5423 100644 --- a/app/features/settings/components/button.tsx +++ b/app/features/settings/components/button.tsx @@ -5,8 +5,7 @@ type Props = { variant: Variant; onClick?: MouseEventHandler; isDisabled?: boolean; - type: ButtonHTMLAttributes["type"]; -}; +} & ButtonHTMLAttributes; const Button: FunctionComponent> = ({ children, type, variant, onClick, isDisabled }) => { return ( diff --git a/app/routes/__app.tsx b/app/routes/__app.tsx index 3a1cc2d..76fac84 100644 --- a/app/routes/__app.tsx +++ b/app/routes/__app.tsx @@ -12,7 +12,7 @@ export default function __App() { const hideFooter = false; const matches = useMatches(); // matches[0].handle - console.log("matches", matches); + // console.log("matches", matches); return (
diff --git a/app/routes/__app/settings/account.tsx b/app/routes/__app/settings/account.tsx index 3be53ac..9e31450 100644 --- a/app/routes/__app/settings/account.tsx +++ b/app/routes/__app/settings/account.tsx @@ -1,7 +1,10 @@ +import accountAction from "~/features/settings/actions/account"; import ProfileInformations from "~/features/settings/components/account/profile-informations"; import UpdatePassword from "~/features/settings/components/account/update-password"; import DangerZone from "~/features/settings/components/account/danger-zone"; +export const action = accountAction; + export default function Account() { return (
diff --git a/app/utils/auth.server.ts b/app/utils/auth.server.ts index 8502703..dd204e4 100644 --- a/app/utils/auth.server.ts +++ b/app/utils/auth.server.ts @@ -9,7 +9,7 @@ import authenticator from "./authenticator.server"; import { AuthenticationError } from "./errors"; import { commitSession, destroySession, getSession } from "./session.server"; -export type SessionOrganization = Pick & { role: MembershipRole }; +export type SessionOrganization = Pick & { role: MembershipRole }; export type SessionUser = Omit & { organizations: SessionOrganization[]; }; @@ -38,7 +38,7 @@ export async function login({ form }: FormStrategyVerifyParams): Promise = { [K in keyof T]-?: Required> & Partial>> }[keyof T]; diff --git a/prisma/migrations/20211231000316_init/migration.sql b/prisma/migrations/20211231000316_init/migration.sql deleted file mode 100644 index 19cd449..0000000 --- a/prisma/migrations/20211231000316_init/migration.sql +++ /dev/null @@ -1,110 +0,0 @@ --- CreateEnum -CREATE TYPE "MembershipRole" AS ENUM ('OWNER', 'USER'); - --- CreateEnum -CREATE TYPE "GlobalRole" AS ENUM ('SUPERADMIN', 'CUSTOMER'); - --- CreateEnum -CREATE TYPE "TokenType" AS ENUM ('RESET_PASSWORD', 'INVITE_MEMBER'); - --- CreateTable -CREATE TABLE "Organization" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMPTZ NOT NULL, - "slug" TEXT NOT NULL, - "name" TEXT NOT NULL, - "stripeCustomerId" TEXT, - "stripeSubscriptionId" TEXT, - "stripePriceId" TEXT, - "stripeCurrentPeriodEnd" TIMESTAMP(3), - - CONSTRAINT "Organization_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Membership" ( - "id" TEXT NOT NULL, - "role" "MembershipRole" NOT NULL, - "organizationId" TEXT NOT NULL, - "userId" TEXT, - "invitedEmail" TEXT, - - CONSTRAINT "Membership_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "User" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMPTZ NOT NULL, - "fullName" TEXT NOT NULL, - "email" TEXT NOT NULL, - "hashedPassword" TEXT, - "role" "GlobalRole" NOT NULL DEFAULT E'CUSTOMER', - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Session" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMPTZ NOT NULL, - "expiresAt" TIMESTAMPTZ, - "data" TEXT NOT NULL, - "userId" TEXT, - - CONSTRAINT "Session_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Token" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMPTZ NOT NULL, - "hashedToken" TEXT NOT NULL, - "type" "TokenType" NOT NULL, - "expiresAt" TIMESTAMPTZ NOT NULL, - "sentTo" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "membershipId" TEXT NOT NULL, - - CONSTRAINT "Token_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "Organization_slug_key" ON "Organization"("slug"); - --- CreateIndex -CREATE UNIQUE INDEX "Organization_stripeCustomerId_key" ON "Organization"("stripeCustomerId"); - --- CreateIndex -CREATE UNIQUE INDEX "Organization_stripeSubscriptionId_key" ON "Organization"("stripeSubscriptionId"); - --- CreateIndex -CREATE UNIQUE INDEX "Membership_organizationId_invitedEmail_key" ON "Membership"("organizationId", "invitedEmail"); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- CreateIndex -CREATE UNIQUE INDEX "Token_membershipId_key" ON "Token"("membershipId"); - --- CreateIndex -CREATE UNIQUE INDEX "Token_hashedToken_type_key" ON "Token"("hashedToken", "type"); - --- AddForeignKey -ALTER TABLE "Membership" ADD CONSTRAINT "Membership_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Membership" ADD CONSTRAINT "Membership_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Token" ADD CONSTRAINT "Token_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Token" ADD CONSTRAINT "Token_membershipId_fkey" FOREIGN KEY ("membershipId") REFERENCES "Membership"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20220512232520_import_schema/migration.sql b/prisma/migrations/20220514120159_init/migration.sql similarity index 50% rename from prisma/migrations/20220512232520_import_schema/migration.sql rename to prisma/migrations/20220514120159_init/migration.sql index 388ccc4..7d08766 100644 --- a/prisma/migrations/20220512232520_import_schema/migration.sql +++ b/prisma/migrations/20220514120159_init/migration.sql @@ -1,16 +1,15 @@ -/* - Warnings: - - - You are about to drop the column `slug` on the `Organization` table. All the data in the column will be lost. - - You are about to drop the column `stripeCurrentPeriodEnd` on the `Organization` table. All the data in the column will be lost. - - You are about to drop the column `stripeCustomerId` on the `Organization` table. All the data in the column will be lost. - - You are about to drop the column `stripePriceId` on the `Organization` table. All the data in the column will be lost. - - You are about to drop the column `stripeSubscriptionId` on the `Organization` table. All the data in the column will be lost. - -*/ -- CreateEnum CREATE TYPE "SubscriptionStatus" AS ENUM ('active', 'trialing', 'past_due', 'paused', 'deleted'); +-- CreateEnum +CREATE TYPE "MembershipRole" AS ENUM ('OWNER', 'USER'); + +-- CreateEnum +CREATE TYPE "GlobalRole" AS ENUM ('SUPERADMIN', 'CUSTOMER'); + +-- CreateEnum +CREATE TYPE "TokenType" AS ENUM ('RESET_PASSWORD', 'INVITE_MEMBER'); + -- CreateEnum CREATE TYPE "Direction" AS ENUM ('Inbound', 'Outbound'); @@ -20,21 +19,14 @@ CREATE TYPE "MessageStatus" AS ENUM ('Queued', 'Sending', 'Sent', 'Failed', 'Del -- CreateEnum CREATE TYPE "CallStatus" AS ENUM ('Queued', 'Ringing', 'InProgress', 'Completed', 'Busy', 'Failed', 'NoAnswer', 'Canceled'); --- DropIndex -DROP INDEX "Organization_slug_key"; +-- CreateTable +CREATE TABLE "Organization" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ NOT NULL, --- DropIndex -DROP INDEX "Organization_stripeCustomerId_key"; - --- DropIndex -DROP INDEX "Organization_stripeSubscriptionId_key"; - --- AlterTable -ALTER TABLE "Organization" DROP COLUMN "slug", -DROP COLUMN "stripeCurrentPeriodEnd", -DROP COLUMN "stripeCustomerId", -DROP COLUMN "stripePriceId", -DROP COLUMN "stripeSubscriptionId"; + CONSTRAINT "Organization_pkey" PRIMARY KEY ("id") +); -- CreateTable CREATE TABLE "Subscription" ( @@ -56,6 +48,57 @@ CREATE TABLE "Subscription" ( CONSTRAINT "Subscription_pkey" PRIMARY KEY ("paddleSubscriptionId") ); +-- CreateTable +CREATE TABLE "Membership" ( + "id" TEXT NOT NULL, + "role" "MembershipRole" NOT NULL, + "organizationId" TEXT NOT NULL, + "userId" TEXT, + "invitedEmail" TEXT, + + CONSTRAINT "Membership_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ NOT NULL, + "fullName" TEXT NOT NULL, + "email" TEXT NOT NULL, + "hashedPassword" TEXT, + "role" "GlobalRole" NOT NULL DEFAULT E'CUSTOMER', + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ NOT NULL, + "expiresAt" TIMESTAMPTZ, + "data" TEXT NOT NULL, + "userId" TEXT, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Token" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ NOT NULL, + "hashedToken" TEXT NOT NULL, + "type" "TokenType" NOT NULL, + "expiresAt" TIMESTAMPTZ NOT NULL, + "sentTo" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "membershipId" TEXT NOT NULL, + + CONSTRAINT "Token_pkey" PRIMARY KEY ("id") +); + -- CreateTable CREATE TABLE "Message" ( "id" TEXT NOT NULL, @@ -89,8 +132,8 @@ CREATE TABLE "PhoneNumber" ( "id" TEXT NOT NULL, "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, "number" TEXT NOT NULL, - "hasFetchedMessages" BOOLEAN, - "hasFetchedCalls" BOOLEAN, + "isFetchingMessages" BOOLEAN, + "isFetchingCalls" BOOLEAN, "organizationId" TEXT NOT NULL, CONSTRAINT "PhoneNumber_pkey" PRIMARY KEY ("id") @@ -99,12 +142,39 @@ CREATE TABLE "PhoneNumber" ( -- CreateIndex CREATE UNIQUE INDEX "Subscription_paddleSubscriptionId_key" ON "Subscription"("paddleSubscriptionId"); +-- CreateIndex +CREATE UNIQUE INDEX "Membership_organizationId_invitedEmail_key" ON "Membership"("organizationId", "invitedEmail"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Token_membershipId_key" ON "Token"("membershipId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Token_hashedToken_type_key" ON "Token"("hashedToken", "type"); + -- CreateIndex CREATE UNIQUE INDEX "PhoneNumber_organizationId_id_key" ON "PhoneNumber"("organizationId", "id"); -- AddForeignKey ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; +-- AddForeignKey +ALTER TABLE "Membership" ADD CONSTRAINT "Membership_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Membership" ADD CONSTRAINT "Membership_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Token" ADD CONSTRAINT "Token_membershipId_fkey" FOREIGN KEY ("membershipId") REFERENCES "Membership"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Token" ADD CONSTRAINT "Token_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE "Message" ADD CONSTRAINT "Message_phoneNumberId_fkey" FOREIGN KEY ("phoneNumberId") REFERENCES "PhoneNumber"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bd350e1..ec2be58 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,7 +11,6 @@ model Organization { id String @id @default(cuid()) createdAt DateTime @default(now()) @db.Timestamptz updatedAt DateTime @updatedAt @db.Timestamptz - name String memberships Membership[] phoneNumbers PhoneNumber[] diff --git a/prisma/seed.ts b/prisma/seed.ts index 562d02b..fcaff07 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -3,18 +3,21 @@ import { GlobalRole, MembershipRole } from "@prisma/client"; import db from "~/utils/db.server"; import { hashPassword } from "~/utils/auth.server"; -import slugify from "~/utils/slugify"; async function seed() { const email = "remixtape@admin.local"; - const orgName = "Get Psyched"; - const orgSlug = slugify(orgName); const password = crypto.randomBytes(8).toString("hex"); // cleanup the existing database await db.user.delete({ where: { email } }).catch(() => {}); - await db.organization.delete({ where: { slug: orgSlug } }).catch(() => {}); + await db.organization.deleteMany({ + where: { + memberships: { + some: { user: { email } }, + }, + }, + }).catch(() => {}); await db.user.create({ data: { @@ -26,7 +29,7 @@ async function seed() { create: { role: MembershipRole.OWNER, organization: { - create: { name: orgName, slug: orgSlug }, + create: {}, }, }, },