account settings actions, account deletion left
This commit is contained in:
parent
98b89ae0f7
commit
48b3604116
@ -2,6 +2,7 @@ import { type ActionFunction, json } from "@remix-run/node";
|
|||||||
import { GlobalRole, MembershipRole } from "@prisma/client";
|
import { GlobalRole, MembershipRole } from "@prisma/client";
|
||||||
|
|
||||||
import db from "~/utils/db.server";
|
import db from "~/utils/db.server";
|
||||||
|
import logger from "~/utils/logger.server";
|
||||||
import { authenticate, hashPassword } from "~/utils/auth.server";
|
import { authenticate, hashPassword } from "~/utils/auth.server";
|
||||||
import { type FormError, validate } from "~/utils/validation.server";
|
import { type FormError, validate } from "~/utils/validation.server";
|
||||||
import { Register } from "../validations";
|
import { Register } from "../validations";
|
||||||
@ -17,7 +18,7 @@ const action: ActionFunction = async ({ request }) => {
|
|||||||
return json<RegisterActionData>({ errors: validation.errors });
|
return json<RegisterActionData>({ errors: validation.errors });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgName, fullName, email, password } = validation.data;
|
const { fullName, email, password } = validation.data;
|
||||||
const hashedPassword = await hashPassword(password.trim());
|
const hashedPassword = await hashPassword(password.trim());
|
||||||
try {
|
try {
|
||||||
await db.user.create({
|
await db.user.create({
|
||||||
@ -30,13 +31,15 @@ const action: ActionFunction = async ({ request }) => {
|
|||||||
create: {
|
create: {
|
||||||
role: MembershipRole.OWNER,
|
role: MembershipRole.OWNER,
|
||||||
organization: {
|
organization: {
|
||||||
create: { name: orgName },
|
create: {}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
logger.error(error);
|
||||||
|
|
||||||
if (error.code === "P2002") {
|
if (error.code === "P2002") {
|
||||||
if (error.meta.target[0] === "email") {
|
if (error.meta.target[0] === "email") {
|
||||||
return json<RegisterActionData>({
|
return json<RegisterActionData>({
|
||||||
|
@ -37,21 +37,13 @@ export default function RegisterPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<LabeledTextField
|
|
||||||
name="orgName"
|
|
||||||
type="text"
|
|
||||||
label="Organization name"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
error={actionData?.errors?.orgName}
|
|
||||||
tabIndex={1}
|
|
||||||
/>
|
|
||||||
<LabeledTextField
|
<LabeledTextField
|
||||||
name="fullName"
|
name="fullName"
|
||||||
type="text"
|
type="text"
|
||||||
label="Full name"
|
label="Full name"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
error={actionData?.errors?.fullName}
|
error={actionData?.errors?.fullName}
|
||||||
tabIndex={2}
|
tabIndex={1}
|
||||||
/>
|
/>
|
||||||
<LabeledTextField
|
<LabeledTextField
|
||||||
name="email"
|
name="email"
|
||||||
@ -59,7 +51,7 @@ export default function RegisterPage() {
|
|||||||
label="Email"
|
label="Email"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
error={actionData?.errors?.email}
|
error={actionData?.errors?.email}
|
||||||
tabIndex={3}
|
tabIndex={2}
|
||||||
/>
|
/>
|
||||||
<LabeledTextField
|
<LabeledTextField
|
||||||
name="password"
|
name="password"
|
||||||
@ -67,13 +59,13 @@ export default function RegisterPage() {
|
|||||||
label="Password"
|
label="Password"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
error={actionData?.errors?.password}
|
error={actionData?.errors?.password}
|
||||||
tabIndex={4}
|
tabIndex={3}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={transition.state === "submitting"}
|
disabled={transition.state === "submitting"}
|
||||||
tabIndex={5}
|
tabIndex={4}
|
||||||
>
|
>
|
||||||
Register
|
Register
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -3,7 +3,6 @@ import { z } from "zod";
|
|||||||
export const password = z.string().min(10).max(100);
|
export const password = z.string().min(10).max(100);
|
||||||
|
|
||||||
export const Register = z.object({
|
export const Register = z.object({
|
||||||
orgName: z.string().nonempty(),
|
|
||||||
fullName: z.string().nonempty(),
|
fullName: z.string().nonempty(),
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password,
|
password,
|
||||||
|
@ -7,7 +7,7 @@ type Props = {
|
|||||||
sideLabel?: ReactNode;
|
sideLabel?: ReactNode;
|
||||||
type?: "text" | "password" | "email";
|
type?: "text" | "password" | "email";
|
||||||
error?: string;
|
error?: string;
|
||||||
} & InputHTMLAttributes<HTMLInputElement>;
|
} & Omit<InputHTMLAttributes<HTMLInputElement>, "name" | "type">;
|
||||||
|
|
||||||
const LabeledTextField: FunctionComponent<Props> = ({ name, label, sideLabel, type = "text", error, ...props }) => {
|
const LabeledTextField: FunctionComponent<Props> = ({ name, label, sideLabel, type = "text", error, ...props }) => {
|
||||||
const hasSideLabel = !!sideLabel;
|
const hasSideLabel = !!sideLabel;
|
||||||
@ -19,6 +19,7 @@ const LabeledTextField: FunctionComponent<Props> = ({ name, label, sideLabel, ty
|
|||||||
className={clsx("text-sm font-medium leading-5 text-gray-700", {
|
className={clsx("text-sm font-medium leading-5 text-gray-700", {
|
||||||
block: !hasSideLabel,
|
block: !hasSideLabel,
|
||||||
"flex justify-between": hasSideLabel,
|
"flex justify-between": hasSideLabel,
|
||||||
|
// "text-red-600": !!error,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@ -29,7 +30,10 @@ const LabeledTextField: FunctionComponent<Props> = ({ name, label, sideLabel, ty
|
|||||||
id={name}
|
id={name}
|
||||||
name={name}
|
name={name}
|
||||||
type={type}
|
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
|
required
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
126
app/features/settings/actions/account.ts
Normal file
126
app/features/settings/actions/account.ts
Normal file
@ -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<typeof validations.changePassword>; 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<ChangePasswordActionData>({
|
||||||
|
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<ChangePasswordActionData>({
|
||||||
|
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<ChangePasswordActionData>({
|
||||||
|
changePassword: { submitted: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateUserFailureActionData = { errors: FormError<typeof validations.updateUser>; 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<UpdateUserActionData>({
|
||||||
|
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<UpdateUserActionData>({
|
||||||
|
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;
|
@ -1,4 +1,5 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
|
import { useFetcher, useSubmit, useTransition } from "@remix-run/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import Button from "../button";
|
import Button from "../button";
|
||||||
@ -6,9 +7,14 @@ import SettingsSection from "../settings-section";
|
|||||||
import Modal, { ModalTitle } from "~/features/core/components/modal";
|
import Modal, { ModalTitle } from "~/features/core/components/modal";
|
||||||
|
|
||||||
export default function DangerZone() {
|
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 [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false);
|
||||||
const modalCancelButtonRef = useRef<HTMLButtonElement>(null);
|
const modalCancelButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const fetcher = useFetcher();
|
||||||
|
const submit = useSubmit();
|
||||||
|
// TODO
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
if (isDeletingUser) {
|
if (isDeletingUser) {
|
||||||
@ -17,10 +23,6 @@ export default function DangerZone() {
|
|||||||
|
|
||||||
setIsConfirmationModalOpen(false);
|
setIsConfirmationModalOpen(false);
|
||||||
};
|
};
|
||||||
const onConfirm = () => {
|
|
||||||
setIsDeletingUser(true);
|
|
||||||
// return deleteUserMutation(); // TODO
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSection className="border border-red-300">
|
<SettingsSection className="border border-red-300">
|
||||||
@ -63,7 +65,6 @@ export default function DangerZone() {
|
|||||||
"bg-red-600 hover:bg-red-700": !isDeletingUser,
|
"bg-red-600 hover:bg-red-700": !isDeletingUser,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
onClick={onConfirm}
|
|
||||||
disabled={isDeletingUser}
|
disabled={isDeletingUser}
|
||||||
>
|
>
|
||||||
Delete my account
|
Delete my account
|
||||||
|
@ -1,47 +1,48 @@
|
|||||||
import type { FunctionComponent } from "react";
|
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 Button from "../button";
|
||||||
import SettingsSection from "../settings-section";
|
import SettingsSection from "../settings-section";
|
||||||
import useSession from "~/features/core/hooks/use-session";
|
|
||||||
|
|
||||||
const ProfileInformations: FunctionComponent = () => {
|
const ProfileInformations: FunctionComponent = () => {
|
||||||
const user = useSession();
|
const user = useSession();
|
||||||
const transition = useTransition();
|
const transition = useTransition();
|
||||||
const actionData = useActionData();
|
const actionData = useActionData<UpdateUserActionData>()?.updateUser;
|
||||||
|
|
||||||
const isSubmitting = transition.state === "submitting";
|
const errors = actionData?.errors;
|
||||||
const isSuccess = actionData?.submitted === true;
|
const topErrorMessage = errors?.general;
|
||||||
const error = actionData?.error;
|
const isError = typeof topErrorMessage !== "undefined";
|
||||||
const isError = !!error;
|
const isSuccess = actionData?.submitted;
|
||||||
|
const isCurrentFormTransition = transition.submission?.formData.get("_action") === "updateUser";
|
||||||
const onSubmit = async () => {
|
const isSubmitting = isCurrentFormTransition && transition.state === "submitting";
|
||||||
// await updateUserMutation({ email, fullName }); // TODO
|
console.log("isSuccess", isSuccess, actionData);
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit}>
|
<Form method="post">
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
footer={
|
footer={
|
||||||
<div className="px-4 py-3 bg-gray-50 text-right text-sm font-medium sm:px-6">
|
<div className="px-4 py-3 bg-gray-50 text-right text-sm font-medium sm:px-6">
|
||||||
<Button variant="default" type="submit" isDisabled={isSubmitting}>
|
<Button variant="default" type="submit" isDisabled={isSubmitting}>
|
||||||
{isSubmitting ? "Saving..." : "Save"}
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isError ? (
|
{isError ? (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<Alert title="Oops, there was an issue" message={error} variant="error" />
|
<Alert title="Oops, there was an issue" message={topErrorMessage} variant="error" />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{isSuccess ? (
|
{isSuccess && (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<Alert title="Saved successfully" message="Your changes have been saved." variant="success" />
|
<Alert title="Saved successfully" message="Your changes have been saved." variant="success" />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
|
|
||||||
<div className="col-span-3 sm:col-span-2">
|
<div className="col-span-3 sm:col-span-2">
|
||||||
<label htmlFor="fullName" className="block text-sm font-medium leading-5 text-gray-700">
|
<label htmlFor="fullName" className="block text-sm font-medium leading-5 text-gray-700">
|
||||||
Full name
|
Full name
|
||||||
@ -75,8 +76,10 @@ const ProfileInformations: FunctionComponent = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="_action" value="updateUser" />
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,37 +1,36 @@
|
|||||||
import type { FunctionComponent } from "react";
|
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 Alert from "~/features/core/components/alert";
|
||||||
|
import LabeledTextField from "~/features/core/components/labeled-text-field";
|
||||||
import Button from "../button";
|
import Button from "../button";
|
||||||
import SettingsSection from "../settings-section";
|
import SettingsSection from "../settings-section";
|
||||||
import { useActionData, useTransition } from "@remix-run/react";
|
|
||||||
|
|
||||||
const UpdatePassword: FunctionComponent = () => {
|
const UpdatePassword: FunctionComponent = () => {
|
||||||
const transition = useTransition();
|
const transition = useTransition();
|
||||||
const actionData = useActionData();
|
const actionData = useActionData<ChangePasswordActionData>()?.changePassword;
|
||||||
|
|
||||||
const isSubmitting = transition.state === "submitting";
|
const topErrorMessage = actionData?.errors?.general;
|
||||||
const isSuccess = actionData?.submitted === true;
|
const isError = typeof topErrorMessage !== "undefined";
|
||||||
const error = actionData?.error;
|
const isSuccess = actionData?.submitted;
|
||||||
const isError = !!error;
|
const isCurrentFormTransition = transition.submission?.formData.get("_action") === "changePassword";
|
||||||
|
const isSubmitting = isCurrentFormTransition && transition.state === "submitting";
|
||||||
const onSubmit = async () => {
|
|
||||||
// await changePasswordMutation({ currentPassword, newPassword }); // TODO
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit}>
|
<Form method="post">
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
footer={
|
footer={
|
||||||
<div className="px-4 py-3 bg-gray-50 text-right text-sm font-medium sm:px-6">
|
<div className="px-4 py-3 bg-gray-50 text-right text-sm font-medium sm:px-6">
|
||||||
<Button variant="default" type="submit" isDisabled={isSubmitting}>
|
<Button variant="default" type="submit" isDisabled={isSubmitting}>
|
||||||
{isSubmitting ? "Saving..." : "Save"}
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isError ? (
|
{isError ? (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<Alert title="Oops, there was an issue" message={error} variant="error" />
|
<Alert title="Oops, there was an issue" message={topErrorMessage} variant="error" />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@ -40,45 +39,28 @@ const UpdatePassword: FunctionComponent = () => {
|
|||||||
<Alert title="Saved successfully" message="Your changes have been saved." variant="success" />
|
<Alert title="Saved successfully" message="Your changes have been saved." variant="success" />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div>
|
|
||||||
<label
|
<LabeledTextField
|
||||||
htmlFor="currentPassword"
|
|
||||||
className="flex justify-between text-sm font-medium leading-5 text-gray-700"
|
|
||||||
>
|
|
||||||
<div>Current password</div>
|
|
||||||
</label>
|
|
||||||
<div className="mt-1 rounded-md shadow-sm">
|
|
||||||
<input
|
|
||||||
id="currentPassword"
|
|
||||||
name="currentPassword"
|
name="currentPassword"
|
||||||
|
label="Current password"
|
||||||
type="password"
|
type="password"
|
||||||
tabIndex={3}
|
tabIndex={3}
|
||||||
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"
|
error={actionData?.errors?.currentPassword}
|
||||||
required
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<LabeledTextField
|
||||||
<label
|
|
||||||
htmlFor="newPassword"
|
|
||||||
className="flex justify-between text-sm font-medium leading-5 text-gray-700"
|
|
||||||
>
|
|
||||||
<div>New password</div>
|
|
||||||
</label>
|
|
||||||
<div className="mt-1 rounded-md shadow-sm">
|
|
||||||
<input
|
|
||||||
id="newPassword"
|
|
||||||
name="newPassword"
|
name="newPassword"
|
||||||
|
label="New password"
|
||||||
type="password"
|
type="password"
|
||||||
tabIndex={4}
|
tabIndex={4}
|
||||||
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"
|
error={actionData?.errors?.newPassword}
|
||||||
required
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
<input type="hidden" name="_action" value="changePassword" />
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,8 +5,7 @@ type Props = {
|
|||||||
variant: Variant;
|
variant: Variant;
|
||||||
onClick?: MouseEventHandler;
|
onClick?: MouseEventHandler;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
type: ButtonHTMLAttributes<HTMLButtonElement>["type"];
|
} & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||||
};
|
|
||||||
|
|
||||||
const Button: FunctionComponent<PropsWithChildren<Props>> = ({ children, type, variant, onClick, isDisabled }) => {
|
const Button: FunctionComponent<PropsWithChildren<Props>> = ({ children, type, variant, onClick, isDisabled }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -12,7 +12,7 @@ export default function __App() {
|
|||||||
const hideFooter = false;
|
const hideFooter = false;
|
||||||
const matches = useMatches();
|
const matches = useMatches();
|
||||||
// matches[0].handle
|
// matches[0].handle
|
||||||
console.log("matches", matches);
|
// console.log("matches", matches);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full overflow-hidden fixed bg-gray-100">
|
<div className="h-full w-full overflow-hidden fixed bg-gray-100">
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
|
import accountAction from "~/features/settings/actions/account";
|
||||||
import ProfileInformations from "~/features/settings/components/account/profile-informations";
|
import ProfileInformations from "~/features/settings/components/account/profile-informations";
|
||||||
import UpdatePassword from "~/features/settings/components/account/update-password";
|
import UpdatePassword from "~/features/settings/components/account/update-password";
|
||||||
import DangerZone from "~/features/settings/components/account/danger-zone";
|
import DangerZone from "~/features/settings/components/account/danger-zone";
|
||||||
|
|
||||||
|
export const action = accountAction;
|
||||||
|
|
||||||
export default function Account() {
|
export default function Account() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-6">
|
<div className="flex flex-col space-y-6">
|
||||||
|
@ -9,7 +9,7 @@ import authenticator from "./authenticator.server";
|
|||||||
import { AuthenticationError } from "./errors";
|
import { AuthenticationError } from "./errors";
|
||||||
import { commitSession, destroySession, getSession } from "./session.server";
|
import { commitSession, destroySession, getSession } from "./session.server";
|
||||||
|
|
||||||
export type SessionOrganization = Pick<Organization, "name" | "id"> & { role: MembershipRole };
|
export type SessionOrganization = Pick<Organization, "id"> & { role: MembershipRole };
|
||||||
export type SessionUser = Omit<User, "hashedPassword"> & {
|
export type SessionUser = Omit<User, "hashedPassword"> & {
|
||||||
organizations: SessionOrganization[];
|
organizations: SessionOrganization[];
|
||||||
};
|
};
|
||||||
@ -38,7 +38,7 @@ export async function login({ form }: FormStrategyVerifyParams): Promise<Session
|
|||||||
memberships: {
|
memberships: {
|
||||||
select: {
|
select: {
|
||||||
organization: {
|
organization: {
|
||||||
select: { name: true, id: true },
|
select: { id: true },
|
||||||
},
|
},
|
||||||
role: true,
|
role: true,
|
||||||
},
|
},
|
||||||
@ -150,7 +150,7 @@ export async function refreshSessionData(request: Request) {
|
|||||||
memberships: {
|
memberships: {
|
||||||
select: {
|
select: {
|
||||||
organization: {
|
organization: {
|
||||||
select: { name: true, id: true },
|
select: { id: true },
|
||||||
},
|
},
|
||||||
role: true,
|
role: true,
|
||||||
},
|
},
|
||||||
|
1
helpers.d.ts
vendored
Normal file
1
helpers.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
type RequireAtLeastOne<T> = { [K in keyof T]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<keyof T, K>>> }[keyof T];
|
@ -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;
|
|
@ -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
|
-- CreateEnum
|
||||||
CREATE TYPE "SubscriptionStatus" AS ENUM ('active', 'trialing', 'past_due', 'paused', 'deleted');
|
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
|
-- CreateEnum
|
||||||
CREATE TYPE "Direction" AS ENUM ('Inbound', 'Outbound');
|
CREATE TYPE "Direction" AS ENUM ('Inbound', 'Outbound');
|
||||||
|
|
||||||
@ -20,21 +19,14 @@ CREATE TYPE "MessageStatus" AS ENUM ('Queued', 'Sending', 'Sent', 'Failed', 'Del
|
|||||||
-- CreateEnum
|
-- CreateEnum
|
||||||
CREATE TYPE "CallStatus" AS ENUM ('Queued', 'Ringing', 'InProgress', 'Completed', 'Busy', 'Failed', 'NoAnswer', 'Canceled');
|
CREATE TYPE "CallStatus" AS ENUM ('Queued', 'Ringing', 'InProgress', 'Completed', 'Busy', 'Failed', 'NoAnswer', 'Canceled');
|
||||||
|
|
||||||
-- DropIndex
|
-- CreateTable
|
||||||
DROP INDEX "Organization_slug_key";
|
CREATE TABLE "Organization" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMPTZ NOT NULL,
|
||||||
|
|
||||||
-- DropIndex
|
CONSTRAINT "Organization_pkey" PRIMARY KEY ("id")
|
||||||
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";
|
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "Subscription" (
|
CREATE TABLE "Subscription" (
|
||||||
@ -56,6 +48,57 @@ CREATE TABLE "Subscription" (
|
|||||||
CONSTRAINT "Subscription_pkey" PRIMARY KEY ("paddleSubscriptionId")
|
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
|
-- CreateTable
|
||||||
CREATE TABLE "Message" (
|
CREATE TABLE "Message" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
@ -89,8 +132,8 @@ CREATE TABLE "PhoneNumber" (
|
|||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"number" TEXT NOT NULL,
|
"number" TEXT NOT NULL,
|
||||||
"hasFetchedMessages" BOOLEAN,
|
"isFetchingMessages" BOOLEAN,
|
||||||
"hasFetchedCalls" BOOLEAN,
|
"isFetchingCalls" BOOLEAN,
|
||||||
"organizationId" TEXT NOT NULL,
|
"organizationId" TEXT NOT NULL,
|
||||||
|
|
||||||
CONSTRAINT "PhoneNumber_pkey" PRIMARY KEY ("id")
|
CONSTRAINT "PhoneNumber_pkey" PRIMARY KEY ("id")
|
||||||
@ -99,12 +142,39 @@ CREATE TABLE "PhoneNumber" (
|
|||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "Subscription_paddleSubscriptionId_key" ON "Subscription"("paddleSubscriptionId");
|
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
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "PhoneNumber_organizationId_id_key" ON "PhoneNumber"("organizationId", "id");
|
CREATE UNIQUE INDEX "PhoneNumber_organizationId_id_key" ON "PhoneNumber"("organizationId", "id");
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
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
|
-- AddForeignKey
|
||||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_phoneNumberId_fkey" FOREIGN KEY ("phoneNumberId") REFERENCES "PhoneNumber"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "Message" ADD CONSTRAINT "Message_phoneNumberId_fkey" FOREIGN KEY ("phoneNumberId") REFERENCES "PhoneNumber"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
@ -11,7 +11,6 @@ model Organization {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz
|
createdAt DateTime @default(now()) @db.Timestamptz
|
||||||
updatedAt DateTime @updatedAt @db.Timestamptz
|
updatedAt DateTime @updatedAt @db.Timestamptz
|
||||||
name String
|
|
||||||
|
|
||||||
memberships Membership[]
|
memberships Membership[]
|
||||||
phoneNumbers PhoneNumber[]
|
phoneNumbers PhoneNumber[]
|
||||||
|
@ -3,18 +3,21 @@ import { GlobalRole, MembershipRole } from "@prisma/client";
|
|||||||
|
|
||||||
import db from "~/utils/db.server";
|
import db from "~/utils/db.server";
|
||||||
import { hashPassword } from "~/utils/auth.server";
|
import { hashPassword } from "~/utils/auth.server";
|
||||||
import slugify from "~/utils/slugify";
|
|
||||||
|
|
||||||
async function seed() {
|
async function seed() {
|
||||||
const email = "remixtape@admin.local";
|
const email = "remixtape@admin.local";
|
||||||
const orgName = "Get Psyched";
|
|
||||||
const orgSlug = slugify(orgName);
|
|
||||||
const password = crypto.randomBytes(8).toString("hex");
|
const password = crypto.randomBytes(8).toString("hex");
|
||||||
|
|
||||||
// cleanup the existing database
|
// cleanup the existing database
|
||||||
await db.user.delete({ where: { email } }).catch(() => {});
|
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({
|
await db.user.create({
|
||||||
data: {
|
data: {
|
||||||
@ -26,7 +29,7 @@ async function seed() {
|
|||||||
create: {
|
create: {
|
||||||
role: MembershipRole.OWNER,
|
role: MembershipRole.OWNER,
|
||||||
organization: {
|
organization: {
|
||||||
create: { name: orgName, slug: orgSlug },
|
create: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user