Create flow to add password after account activation
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<b79143becc90a4776f321d0e91db49bd>>
|
||||
* @generated SignedSource<<852ecee082c322aba273f04575353fbd>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type ActivateAccountInput = {
|
||||
password?: string | null | undefined;
|
||||
token: string;
|
||||
};
|
||||
export type ActivateAccountPageMutation$variables = {
|
||||
@@ -18,9 +17,7 @@ export type ActivateAccountPageMutation$variables = {
|
||||
};
|
||||
export type ActivateAccountPageMutation$data = {
|
||||
readonly activateAccount: {
|
||||
readonly profile: {
|
||||
readonly id: string;
|
||||
} | null | undefined;
|
||||
readonly createPasswordToken: string | null | undefined;
|
||||
} | null | undefined;
|
||||
};
|
||||
export type ActivateAccountPageMutation = {
|
||||
@@ -54,19 +51,8 @@ v1 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Profile",
|
||||
"kind": "LinkedField",
|
||||
"name": "profile",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"kind": "ScalarField",
|
||||
"name": "createPasswordToken",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
@@ -91,16 +77,16 @@ return {
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "57d92d0ae2a6220af84fbd9fe27f7aa3",
|
||||
"cacheID": "388c90522ecf3a3365e171a623fb2d2e",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "ActivateAccountPageMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation ActivateAccountPageMutation(\n $input: ActivateAccountInput!\n) {\n activateAccount(input: $input) {\n profile {\n id\n }\n }\n}\n"
|
||||
"text": "mutation ActivateAccountPageMutation(\n $input: ActivateAccountInput!\n) {\n activateAccount(input: $input) {\n createPasswordToken\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "0e073ce00eb7c435a875b5797f3e6db0";
|
||||
(node as any).hash = "c27b723383934423bf92926820426653";
|
||||
|
||||
export default node;
|
||||
|
||||
93
apps/console/src/__generated__/iam/CreatePasswordPageMutation.graphql.ts
generated
Normal file
93
apps/console/src/__generated__/iam/CreatePasswordPageMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @generated SignedSource<<cde2278cb4ed7c350f1c8727933cd92d>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type ResetPasswordInput = {
|
||||
password: string;
|
||||
token: string;
|
||||
};
|
||||
export type CreatePasswordPageMutation$variables = {
|
||||
input: ResetPasswordInput;
|
||||
};
|
||||
export type CreatePasswordPageMutation$data = {
|
||||
readonly resetPassword: {
|
||||
readonly success: boolean;
|
||||
} | null | undefined;
|
||||
};
|
||||
export type CreatePasswordPageMutation = {
|
||||
response: CreatePasswordPageMutation$data;
|
||||
variables: CreatePasswordPageMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
"concreteType": "ResetPasswordPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "resetPassword",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "success",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "CreatePasswordPageMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "CreatePasswordPageMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "aedb0b874e2e55a106724665d51f840e",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "CreatePasswordPageMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation CreatePasswordPageMutation(\n $input: ResetPasswordInput!\n) {\n resetPassword(input: $input) {\n success\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "d9bd91787098b9272f0046ad09554a1a";
|
||||
|
||||
export default node;
|
||||
@@ -7,16 +7,14 @@ import { useMutation } from "react-relay";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import type { ActivateAccountPageMutation } from "#/__generated__/iam/ActivateAccountPageMutation.graphql";
|
||||
import type { ActivateAccountPageMutation$data, ActivateAccountPageMutation } from "#/__generated__/iam/ActivateAccountPageMutation.graphql";
|
||||
|
||||
const activateAccountMutation = graphql`
|
||||
mutation ActivateAccountPageMutation(
|
||||
$input: ActivateAccountInput!
|
||||
) {
|
||||
activateAccount(input: $input) {
|
||||
profile {
|
||||
id
|
||||
}
|
||||
createPasswordToken
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -39,7 +37,7 @@ export default function ActivateAccountPage() {
|
||||
variables: {
|
||||
input: { token },
|
||||
},
|
||||
onCompleted: (_, errors: GraphQLError[] | null) => {
|
||||
onCompleted: (response: ActivateAccountPageMutation$data, errors: GraphQLError[] | null) => {
|
||||
if (errors) {
|
||||
for (const err of errors) {
|
||||
if (err.extensions?.code === "ALREADY_AUTHENTICATED") {
|
||||
@@ -63,7 +61,21 @@ export default function ActivateAccountPage() {
|
||||
),
|
||||
variant: "success",
|
||||
});
|
||||
void navigate("/", { replace: true });
|
||||
|
||||
const { activateAccount } = response;
|
||||
|
||||
if (!activateAccount) {
|
||||
throw new Error("mutation data missing");
|
||||
}
|
||||
|
||||
if (activateAccount.createPasswordToken) {
|
||||
void navigate(
|
||||
{ pathname: "/auth/create-password", search: `?token=${activateAccount.createPasswordToken}` },
|
||||
{ replace: true },
|
||||
);
|
||||
} else {
|
||||
void navigate("/", { replace: true });
|
||||
}
|
||||
},
|
||||
onError: (e) => {
|
||||
toast({
|
||||
|
||||
114
apps/console/src/pages/iam/auth/CreatePasswordPage.tsx
Normal file
114
apps/console/src/pages/iam/auth/CreatePasswordPage.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { formatError } from "@probo/helpers";
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button, Field, useToast } from "@probo/ui";
|
||||
import { useMutation } from "react-relay";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { CreatePasswordPageMutation } from "#/__generated__/iam/CreatePasswordPageMutation.graphql";
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
|
||||
const createPasswordMutation = graphql`
|
||||
mutation CreatePasswordPageMutation($input: ResetPasswordInput!) {
|
||||
resetPassword(input: $input) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const schema = z.object({
|
||||
password: z.string().min(8),
|
||||
});
|
||||
|
||||
export default function CreatePasswordPage() {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
usePageTitle(__("Create Password"));
|
||||
|
||||
const { register, handleSubmit, formState } = useFormWithSchema(schema, {
|
||||
defaultValues: {
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
const [createPassword, isCreatingPassword] = useMutation<CreatePasswordPageMutation>(createPasswordMutation);
|
||||
|
||||
const onSubmit = (data: z.infer<typeof schema>) => {
|
||||
createPassword({
|
||||
variables: {
|
||||
input: {
|
||||
password: data.password,
|
||||
token: searchParams.get("token") ?? "",
|
||||
},
|
||||
},
|
||||
onCompleted: (_, e) => {
|
||||
if (e) {
|
||||
toast({
|
||||
title: __("Password creation failed"),
|
||||
description: formatError(__("Password creation failed"), e),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: __("Account created successfully"),
|
||||
variant: "success",
|
||||
});
|
||||
void navigate("/auth/login", { replace: true });
|
||||
},
|
||||
onError: (e) => {
|
||||
toast({
|
||||
title: __("Password creation failed"),
|
||||
description: e.message,
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-3xl font-bold">{__("Create a password")}</h1>
|
||||
<p className="text-txt-tertiary">
|
||||
{__("Set a password for your account, with at least 8 characters")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4">
|
||||
<Field
|
||||
label={__("Password")}
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
{...register("password")}
|
||||
required
|
||||
error={formState.errors.password?.message}
|
||||
/>
|
||||
|
||||
<Button type="submit" className="w-xs h-10 mx-auto mt-6" disabled={formState.isLoading || isCreatingPassword}>
|
||||
{__("Save")}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-txt-tertiary">
|
||||
{__("Already have an account?")}
|
||||
{" "}
|
||||
<Link
|
||||
to="/auth/login"
|
||||
className="underline text-txt-primary hover:text-txt-secondary"
|
||||
>
|
||||
{__("Log in here")}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -68,6 +68,12 @@ const routes = [
|
||||
() => import("./pages/iam/auth/ActivateAccountPage"),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "create-password",
|
||||
Component: lazy(
|
||||
() => import("./pages/iam/auth/CreatePasswordPage"),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "forgot-password",
|
||||
Component: lazy(() => import("./pages/iam/auth/ForgotPasswordPage")),
|
||||
|
||||
Reference in New Issue
Block a user