mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-12 13:52:20 +00:00
verify email
This commit is contained in:
@@ -2,7 +2,6 @@ import { listSessions, server } from "#/lib/zitadel";
|
||||
import { Avatar, AvatarSize } from "#/ui/Avatar";
|
||||
import { getAllSessionIds } from "#/utils/cookies";
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
ExclamationTriangleIcon,
|
||||
XCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
@@ -11,6 +11,8 @@ export default async function Page() {
|
||||
server
|
||||
);
|
||||
|
||||
console.log(legal);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<h1>Register</h1>
|
||||
@@ -18,7 +20,7 @@ export default async function Page() {
|
||||
|
||||
{legal && passwordComplexitySettings && (
|
||||
<RegisterForm
|
||||
privacyPolicy={legal}
|
||||
legal={legal}
|
||||
passwordComplexityPolicy={passwordComplexitySettings}
|
||||
></RegisterForm>
|
||||
)}
|
||||
|
||||
24
apps/login/app/(login)/verify/page.tsx
Normal file
24
apps/login/app/(login)/verify/page.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import VerifyEmailForm from "#/ui/VerifyEmailForm";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export default async function Page({ searchParams }: { searchParams: any }) {
|
||||
const { userID, code, orgID, loginname, passwordset } = searchParams;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<h1>Verify user</h1>
|
||||
<p className="ztdl-p mb-6 block">
|
||||
Enter the Code provided in the verification email.
|
||||
</p>
|
||||
|
||||
{userID ? (
|
||||
<VerifyEmailForm userId={userID} />
|
||||
) : (
|
||||
<div className="w-full flex flex-row items-center justify-center border border-yellow-600/40 dark:border-yellow-500/20 bg-yellow-200/30 text-yellow-600 dark:bg-yellow-700/20 dark:text-yellow-200 rounded-md py-2 scroll-px-40">
|
||||
<ExclamationTriangleIcon className="h-5 w-5 mr-2" />
|
||||
<span className="text-center text-sm">No userId provided!</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
apps/login/app/email/verify/route.ts
Normal file
15
apps/login/app/email/verify/route.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { server, verifyEmail } from "#/lib/zitadel";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
if (body) {
|
||||
const { userId, code } = body;
|
||||
|
||||
return verifyEmail(server, userId, code).then((resp) => {
|
||||
return NextResponse.json(resp);
|
||||
});
|
||||
} else {
|
||||
return NextResponse.error();
|
||||
}
|
||||
}
|
||||
@@ -19,35 +19,40 @@ export const demos: { name: string; items: Item[] }[] = [
|
||||
description: "The page to request a users password",
|
||||
},
|
||||
{
|
||||
name: "Set Password",
|
||||
slug: "password/set",
|
||||
description: "The page to set a users password",
|
||||
},
|
||||
{
|
||||
name: "MFA",
|
||||
slug: "mfa",
|
||||
description: "The page to request a users mfa method",
|
||||
},
|
||||
{
|
||||
name: "MFA Set",
|
||||
slug: "mfa/set",
|
||||
description: "The page to set a users mfa method",
|
||||
},
|
||||
{
|
||||
name: "MFA Create",
|
||||
slug: "mfa/create",
|
||||
description: "The page to create a users mfa method",
|
||||
},
|
||||
{
|
||||
name: "Passwordless",
|
||||
slug: "passwordless",
|
||||
description: "The page to login a user with his passwordless device",
|
||||
},
|
||||
{
|
||||
name: "Passwordless Create",
|
||||
slug: "passwordless/create",
|
||||
description: "The page to add a users passwordless device",
|
||||
name: "Accounts",
|
||||
slug: "accounts",
|
||||
description: "List active and inactive sessions",
|
||||
},
|
||||
// {
|
||||
// name: "Set Password",
|
||||
// slug: "password/set",
|
||||
// description: "The page to set a users password",
|
||||
// },
|
||||
// {
|
||||
// name: "MFA",
|
||||
// slug: "mfa",
|
||||
// description: "The page to request a users mfa method",
|
||||
// },
|
||||
// {
|
||||
// name: "MFA Set",
|
||||
// slug: "mfa/set",
|
||||
// description: "The page to set a users mfa method",
|
||||
// },
|
||||
// {
|
||||
// name: "MFA Create",
|
||||
// slug: "mfa/create",
|
||||
// description: "The page to create a users mfa method",
|
||||
// },
|
||||
// {
|
||||
// name: "Passwordless",
|
||||
// slug: "passwordless",
|
||||
// description: "The page to login a user with his passwordless device",
|
||||
// },
|
||||
// {
|
||||
// name: "Passwordless Create",
|
||||
// slug: "passwordless/create",
|
||||
// description: "The page to add a users passwordless device",
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -58,6 +63,11 @@ export const demos: { name: string; items: Item[] }[] = [
|
||||
slug: "register",
|
||||
description: "Create your ZITADEL account",
|
||||
},
|
||||
{
|
||||
name: "Verify email",
|
||||
slug: "verify",
|
||||
description: "Verify your account with an email code",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
ZitadelServer,
|
||||
ZitadelServerOptions,
|
||||
management,
|
||||
user,
|
||||
settings,
|
||||
getServers,
|
||||
initializeServer,
|
||||
@@ -127,18 +127,19 @@ export type AddHumanUserData = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export function addHumanUser(
|
||||
server: ZitadelServer,
|
||||
{ email, firstName, lastName, password }: AddHumanUserData
|
||||
): Promise<string> {
|
||||
const mgmt = management.getManagement(server);
|
||||
const mgmt = user.getUser(server);
|
||||
return mgmt
|
||||
.addHumanUser(
|
||||
{
|
||||
email: { email, isEmailVerified: false },
|
||||
userName: email,
|
||||
email: { email, isVerified: false },
|
||||
username: email,
|
||||
profile: { firstName, lastName },
|
||||
initialPassword: password,
|
||||
password: { password },
|
||||
},
|
||||
{
|
||||
// metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "")
|
||||
@@ -150,4 +151,19 @@ export function addHumanUser(
|
||||
});
|
||||
}
|
||||
|
||||
export function verifyEmail(
|
||||
server: ZitadelServer,
|
||||
userId: string,
|
||||
verificationCode: string
|
||||
): Promise<any> {
|
||||
const mgmt = user.getUser(server);
|
||||
return mgmt.verifyEmail(
|
||||
{
|
||||
userId,
|
||||
verificationCode,
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
export { server };
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Checkbox } from "./Checkbox";
|
||||
import { PrivacyPolicy } from "@zitadel/server";
|
||||
import { LegalAndSupportSettings } from "@zitadel/server";
|
||||
|
||||
type Props = {
|
||||
privacyPolicy: PrivacyPolicy;
|
||||
legal: LegalAndSupportSettings;
|
||||
onChange: (allAccepted: boolean) => void;
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ type AcceptanceState = {
|
||||
privacyPolicyAccepted: boolean;
|
||||
};
|
||||
|
||||
export function PrivacyPolicyCheckboxes({ privacyPolicy, onChange }: Props) {
|
||||
export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) {
|
||||
const [acceptanceState, setAcceptanceState] = useState<AcceptanceState>({
|
||||
tosAccepted: false,
|
||||
privacyPolicyAccepted: false,
|
||||
@@ -24,9 +24,9 @@ export function PrivacyPolicyCheckboxes({ privacyPolicy, onChange }: Props) {
|
||||
<>
|
||||
<p className="flex flex-row items-center text-text-light-secondary-500 dark:text-text-dark-secondary-500 mt-4 text-sm">
|
||||
To register you must agree to the terms and conditions
|
||||
{privacyPolicy?.helpLink && (
|
||||
{legal?.helpLink && (
|
||||
<span>
|
||||
<Link href={privacyPolicy.helpLink} target="_blank">
|
||||
<Link href={legal.helpLink} target="_blank">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
@@ -45,7 +45,7 @@ export function PrivacyPolicyCheckboxes({ privacyPolicy, onChange }: Props) {
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
{privacyPolicy?.tosLink && (
|
||||
{legal?.tosLink && (
|
||||
<div className="mt-4 flex items-center">
|
||||
<Checkbox
|
||||
className="mr-4"
|
||||
@@ -62,18 +62,14 @@ export function PrivacyPolicyCheckboxes({ privacyPolicy, onChange }: Props) {
|
||||
<div className="mr-4 w-[28rem]">
|
||||
<p className="text-sm text-text-light-500 dark:text-text-dark-500">
|
||||
Agree
|
||||
<Link
|
||||
href={privacyPolicy.tosLink}
|
||||
className="underline"
|
||||
target="_blank"
|
||||
>
|
||||
<Link href={legal.tosLink} className="underline" target="_blank">
|
||||
Terms of Service
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{privacyPolicy?.privacyLink && (
|
||||
{legal?.privacyPolicyLink && (
|
||||
<div className="mt-4 flex items-center">
|
||||
<Checkbox
|
||||
className="mr-4"
|
||||
@@ -91,7 +87,7 @@ export function PrivacyPolicyCheckboxes({ privacyPolicy, onChange }: Props) {
|
||||
<p className="text-sm text-text-light-500 dark:text-text-dark-500">
|
||||
Agree
|
||||
<Link
|
||||
href={privacyPolicy.privacyLink}
|
||||
href={legal.privacyPolicyLink}
|
||||
className="underline"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
@@ -30,12 +30,12 @@ type Inputs =
|
||||
| FieldValues;
|
||||
|
||||
type Props = {
|
||||
privacyPolicy: LegalAndSupportSettings;
|
||||
legal: LegalAndSupportSettings;
|
||||
passwordComplexityPolicy: PasswordComplexitySettings;
|
||||
};
|
||||
|
||||
export default function RegisterForm({
|
||||
privacyPolicy,
|
||||
legal,
|
||||
passwordComplexityPolicy,
|
||||
}: Props) {
|
||||
const { register, handleSubmit, watch, formState } = useForm<Inputs>({
|
||||
@@ -166,9 +166,9 @@ export default function RegisterForm({
|
||||
/>
|
||||
)}
|
||||
|
||||
{privacyPolicy && (
|
||||
{legal && (
|
||||
<PrivacyPolicyCheckboxes
|
||||
privacyPolicy={privacyPolicy}
|
||||
legal={legal}
|
||||
onChange={setTosAndPolicyAccepted}
|
||||
/>
|
||||
)}
|
||||
|
||||
90
apps/login/ui/VerifyEmailForm.tsx
Normal file
90
apps/login/ui/VerifyEmailForm.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button, ButtonVariants } from "./Button";
|
||||
import { TextInput } from "./Input";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Spinner } from "./Spinner";
|
||||
|
||||
type Inputs = {
|
||||
code: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export default function VerifyEmailForm({ userId }: Props) {
|
||||
const { register, handleSubmit, formState } = useForm<Inputs>({
|
||||
mode: "onBlur",
|
||||
});
|
||||
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
async function submitCode(values: Inputs) {
|
||||
setLoading(true);
|
||||
const res = await fetch("/email/verify", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
code: values.code,
|
||||
userId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
setLoading(false);
|
||||
throw new Error("Failed to verify email");
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function submitCodeAndContinue(value: Inputs): Promise<boolean | void> {
|
||||
console.log(value);
|
||||
return submitCode(value).then((resp: any) => {
|
||||
return router.push(`/accounts`);
|
||||
});
|
||||
}
|
||||
|
||||
const { errors } = formState;
|
||||
|
||||
return (
|
||||
<form className="w-full">
|
||||
<div className="">
|
||||
<TextInput
|
||||
type="text"
|
||||
autoComplete="one-time-code"
|
||||
{...register("code", { required: "This field is required" })}
|
||||
label="Code"
|
||||
// error={errors.username?.message as string}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex w-full flex-row items-center">
|
||||
{/* <Button type="button" variant={ButtonVariants.Secondary}>
|
||||
back
|
||||
</Button> */}
|
||||
<span className="flex-grow"></span>
|
||||
<Button
|
||||
type="submit"
|
||||
className="self-end"
|
||||
variant={ButtonVariants.Primary}
|
||||
disabled={loading || !formState.isValid}
|
||||
onClick={handleSubmit(submitCodeAndContinue)}
|
||||
>
|
||||
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||
continue
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user