checkbox, complexity policy

This commit is contained in:
Max Peintner
2023-04-26 15:14:28 +02:00
parent 485dd8d3d3
commit 2047e19ed9
8 changed files with 376 additions and 159 deletions

View File

@@ -1,9 +1,9 @@
"use client";
import { TextInput } from "#/ui/Input";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { ClientError } from "nice-grpc";
import PasswordComplexityPolicy from "#/ui/PasswordComplexityPolicy";
type Props = {
userId?: string;
@@ -127,18 +127,6 @@ export default function Page() {
error={errors.confirmPassword?.message}
/>
</div>
<PasswordComplexityPolicy
password={watchNewPassword}
equals={
!!watchNewPassword && watchNewPassword === watchConfirmPassword
}
isValid={(valid: boolean) => {
if (valid !== policyValid) {
setPolicyValid(valid);
}
}}
></PasswordComplexityPolicy>
</form>
</>
);

View File

@@ -1,55 +1,25 @@
"use client";
import {
getPasswordComplexityPolicy,
getPrivacyPolicy,
server,
} from "#/lib/zitadel";
import RegisterForm from "#/ui/RegisterForm";
import { Button, ButtonVariants } from "#/ui/Button";
import IdentityProviders from "#/ui/IdentityProviders";
import { TextInput } from "#/ui/Input";
import { useRouter } from "next/navigation";
export default async function Page() {
const privacyPolicy = await getPrivacyPolicy(server);
const passwordComplexityPolicy = await getPasswordComplexityPolicy(server);
export default function Page() {
const router = useRouter();
function submit() {
router.push("/password");
}
return (
<div className="flex flex-col items-center space-y-4">
<h1>Register</h1>
<p className="ztdl-p">Create your ZITADEL account.</p>
<form className="" onSubmit={() => submit()}>
<div className="grid grid-cols-2 gap-4">
<div className="">
<TextInput label="Firstname" />
</div>
<div className="">
<TextInput label="Lastname" />
</div>
<div className="">
<TextInput label="Email" />
</div>
<div className="">
<TextInput label="Password" />
</div>
<div className="">
<TextInput label="Password Confirmation" />
</div>
</div>
<PrivacyPolicyCheckboxes />
<div className="mt-8 flex w-full flex-row items-center justify-between">
<Button type="button" variant={ButtonVariants.Secondary}>
back
</Button>
<Button
type="submit"
variant={ButtonVariants.Primary}
onClick={() => submit()}
>
continue
</Button>
</div>
</form>
{privacyPolicy && passwordComplexityPolicy && (
<RegisterForm
privacyPolicy={privacyPolicy}
passwordComplexityPolicy={passwordComplexityPolicy}
></RegisterForm>
)}
</div>
);
}

View File

@@ -8,6 +8,8 @@ import {
getServers,
LabelPolicy,
initializeServer,
PrivacyPolicy,
PasswordComplexityPolicy,
} from "@zitadel/server";
// import { getAuth } from "@zitadel/server/auth";
@@ -36,9 +38,28 @@ export function getBranding(
.then((resp) => resp.policy);
}
export function getPrivacyPolicy(
server: ZitadelServer
): Promise<PrivacyPolicy | undefined> {
const mgmt = getManagement(server);
return mgmt
.getPrivacyPolicy(
{},
{ metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "") }
)
.then((resp) => resp.policy);
}
export function getPasswordComplexityPolicy(
server: ZitadelServer
): Promise<PasswordComplexityPolicy | undefined> {
const mgmt = getManagement(server);
return mgmt
.getPasswordComplexityPolicy(
{},
{ metadata: orgMetadata(process.env.ZITADEL_ORG_ID ?? "") }
)
.then((resp) => resp.policy);
}
export { server };
// export async function getMyUser(): Promise<GetMyUserResponse> {
// const auth = await getAuth();
// const response = await auth.getMyUser({});
// return response;
// }

View File

@@ -0,0 +1,65 @@
import classNames from "clsx";
import React, {
DetailedHTMLProps,
forwardRef,
InputHTMLAttributes,
useEffect,
useState,
} from "react";
export type CheckboxProps = DetailedHTMLProps<
InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> & {
checked: boolean;
disabled?: boolean;
onChangeVal?: (checked: boolean) => void;
};
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
(
{
className = "",
checked = false,
disabled = false,
onChangeVal,
children,
...props
},
ref
) => {
const [enabled, setEnabled] = useState<boolean>(checked);
useEffect(() => {
setEnabled(checked);
}, [checked]);
return (
<div className="relative flex items-start">
<div className="flex items-center h-5">
<input
ref={ref}
checked={enabled}
onChange={(event) => {
setEnabled(event.target?.checked);
onChangeVal && onChangeVal(event.target?.checked);
}}
disabled={disabled}
type="checkbox"
className={classNames(
enabled
? "border-none text-primary-light-500 dark:text-primary-dark-500 bg-primary-light-500 active:bg-primary-light-500 dark:bg-primary-dark-500 active:dark:bg-primary-dark-500"
: "border-2 border-gray-500 dark:border-white bg-transparent dark:bg-transparent",
"focus:border-gray-500 focus:dark:border-white focus:ring-opacity-40 focus:dark:ring-opacity-40 focus:ring-offset-0 focus:ring-2 dark:focus:ring-offset-0 dark:focus:ring-2 focus:ring-gray-500 focus:dark:ring-white",
"h-4 w-4 rounded-sm ring-0 outline-0 checked:ring-0 checked:dark:ring-0 active:border-none active:ring-0",
"disabled:bg-gray-500 disabled:text-gray-500 disabled:border-gray-200 disabled:cursor-not-allowed",
className
)}
{...props}
/>
</div>
{children}
</div>
);
}
);

View File

@@ -0,0 +1,98 @@
import {
lowerCaseValidator,
numberValidator,
symbolValidator,
upperCaseValidator,
} from "#/utils/validators";
import { PasswordComplexityPolicy } from "@zitadel/server";
type Props = {
passwordComplexityPolicy: PasswordComplexityPolicy;
password: string;
equals: boolean;
};
const check = (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6 las la-check text-state-success-light-color dark:text-state-success-dark-color mr-2 text-lg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
);
const cross = (
<svg
className="w-6 h-6 las la-times text-warn-light-500 dark:text-warn-dark-500 mr-2 text-lg"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
);
const desc =
"text-14px leading-4 text-input-light-label dark:text-input-dark-label";
export default function PasswordComplexity({
passwordComplexityPolicy,
password,
equals,
}: Props) {
const hasMinLength = password?.length >= passwordComplexityPolicy.minLength;
const hasSymbol = symbolValidator(password);
const hasNumber = numberValidator(password);
const hasUppercase = upperCaseValidator(password);
const hasLowercase = lowerCaseValidator(password);
const policyIsValid =
(passwordComplexityPolicy.hasLowercase ? hasLowercase : true) &&
(passwordComplexityPolicy.hasNumber ? hasNumber : true) &&
(passwordComplexityPolicy.hasUppercase ? hasUppercase : true) &&
(passwordComplexityPolicy.hasSymbol ? hasSymbol : true) &&
hasMinLength;
return (
<div className="mb-4 grid grid-cols-2 gap-x-8 gap-y-2">
<div className="flex flex-row items-center">
{hasMinLength ? check : cross}
<span className={desc}>
Password length {passwordComplexityPolicy.minLength}
</span>
</div>
<div className="flex flex-row items-center">
{hasSymbol ? check : cross}
<span className={desc}>has Symbol</span>
</div>
<div className="flex flex-row items-center">
{hasNumber ? check : cross}
<span className={desc}>has Number</span>
</div>
<div className="flex flex-row items-center">
{hasUppercase ? check : cross}
<span className={desc}>has uppercase</span>
</div>
<div className="flex flex-row items-center">
{hasLowercase ? check : cross}
<span className={desc}>has lowercase</span>
</div>
<div className="flex flex-row items-center">
{equals ? check : cross}
<span className={desc}>equals</span>
</div>
</div>
);
}

View File

@@ -1,94 +0,0 @@
const fetcher = (url: string) =>
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
.then((res) => {
if (res.ok) {
return res.json();
} else {
return res.json().then((error) => {
throw error;
});
}
})
.then((resp) => resp.policy);
type Props = {
password: string;
equals: boolean;
isValid: (valid: boolean) => void;
isMe?: boolean;
userId?: string;
};
const check = (
<i className="las la-check text-state-success-light-color dark:text-state-success-dark-color mr-4 text-lg"></i>
);
const cross = (
<i className="las la-times text-warn-light-500 dark:text-warn-dark-500 mr-4 text-lg"></i>
);
const desc =
"text-14px leading-4 text-input-light-label dark:text-input-dark-label";
export default function PasswordComplexityPolicy({
password,
equals,
isValid,
isMe,
userId,
}: Props) {
// const { data: policy } = useSWR<Policy, ClientError>(
// `/api/user/passwordpolicy/${isMe ? 'me' : userId}`,
// fetcher,
// );
// if (policy) {
// const hasMinLength = password?.length >= policy.minLength;
// const hasSymbol = symbolValidator(password);
// const hasNumber = numberValidator(password);
// const hasUppercase = upperCaseValidator(password);
// const hasLowercase = lowerCaseValidator(password);
// const policyIsValid =
// (policy.hasLowercase ? hasLowercase : true) &&
// (policy.hasNumber ? hasNumber : true) &&
// (policy.hasUppercase ? hasUppercase : true) &&
// (policy.hasSymbol ? hasSymbol : true) &&
// hasMinLength;
// isValid(policyIsValid);
// return (
// <div className="mb-4 grid grid-cols-2 gap-x-8 gap-y-2">
// <div className="flex flex-row items-center">
// {hasMinLength ? check : cross}
// <span className={desc}>Password length {policy.minLength}</span>
// </div>
// <div className="flex flex-row items-center">
// {hasSymbol ? check : cross}
// <span className={desc}>has Symbol</span>
// </div>
// <div className="flex flex-row items-center">
// {hasNumber ? check : cross}
// <span className={desc}>has Number</span>
// </div>
// <div className="flex flex-row items-center">
// {hasUppercase ? check : cross}
// <span className={desc}>has uppercase</span>
// </div>
// <div className="flex flex-row items-center">
// {hasLowercase ? check : cross}
// <span className={desc}>has lowercase</span>
// </div>
// <div className="flex flex-row items-center">
// {equals ? check : cross}
// <span className={desc}>equals</span>
// </div>
// </div>
// );
// } else {
return null;
// }
}

View File

@@ -1,3 +1,106 @@
export default function PrivacyPolicyCheckboxes() {
return <div></div>;
"use client";
import React, { useState } from "react";
import Link from "next/link";
import { Checkbox } from "./Checkbox";
import { PrivacyPolicy } from "@zitadel/server";
type Props = {
privacyPolicy: PrivacyPolicy;
onChange: (allAccepted: boolean) => void;
};
type AcceptanceState = {
tosAccepted: boolean;
privacyPolicyAccepted: boolean;
};
export function PrivacyPolicyCheckboxes({ privacyPolicy, onChange }: Props) {
const [acceptanceState, setAcceptanceState] = useState<AcceptanceState>({
tosAccepted: false,
privacyPolicyAccepted: false,
});
return (
<>
<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 our terms and conditions
{privacyPolicy?.helpLink && (
<span>
<Link href={privacyPolicy.helpLink} target="_blank">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="ml-1 w-5 h-5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"
/>
</svg>
</Link>
</span>
)}
</p>
{privacyPolicy?.tosLink && (
<div className="mt-4 flex items-center">
<Checkbox
className="mr-4"
checked={false}
onChangeVal={(checked: boolean) => {
setAcceptanceState({
...acceptanceState,
tosAccepted: checked,
});
onChange(checked && acceptanceState.privacyPolicyAccepted);
}}
/>
<div className="mr-4 w-[28rem]">
<p className="text-sm text-text-light-500 dark:text-text-dark-500">
Agree&nbsp;
<Link
href={privacyPolicy.tosLink}
className="underline"
target="_blank"
>
Terms of Service
</Link>
</p>
</div>
</div>
)}
{privacyPolicy?.privacyLink && (
<div className="mt-4 flex items-center">
<Checkbox
className="mr-4"
checked={false}
onChangeVal={(checked: boolean) => {
setAcceptanceState({
...acceptanceState,
privacyPolicyAccepted: checked,
});
onChange(checked && acceptanceState.tosAccepted);
}}
/>
<div className="mr-4 w-[28rem]">
<p className="text-sm text-text-light-500 dark:text-text-dark-500">
Agree&nbsp;
<Link
href={privacyPolicy.privacyLink}
className="underline"
target="_blank"
>
Privacy Policy
</Link>
</p>
</div>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,66 @@
"use client";
import { PasswordComplexityPolicy, PrivacyPolicy } from "@zitadel/server";
import PasswordComplexity from "./PasswordComplexity";
import { useState } from "react";
import { Button, ButtonVariants } from "./Button";
import { TextInput } from "./Input";
import { PrivacyPolicyCheckboxes } from "./PrivacyPolicyCheckboxes";
type Props = {
privacyPolicy: PrivacyPolicy;
passwordComplexityPolicy: PasswordComplexityPolicy;
};
export default function RegisterForm({
privacyPolicy,
passwordComplexityPolicy,
}: Props) {
const [tosAndPolicyAccepted, setTosAndPolicyAccepted] = useState(false);
return (
<form className="w-full">
<div className="grid grid-cols-2 gap-4 mb-4">
<div className="">
<TextInput label="Firstname" />
</div>
<div className="">
<TextInput label="Lastname" />
</div>
<div className="col-span-2">
<TextInput label="Email" />
</div>
<div className="">
<TextInput label="Password" />
</div>
<div className="">
<TextInput label="Password Confirmation" />
</div>
</div>
{passwordComplexityPolicy && (
<PasswordComplexity
passwordComplexityPolicy={passwordComplexityPolicy}
password={""}
equals={false}
/>
)}
{privacyPolicy && (
<PrivacyPolicyCheckboxes
privacyPolicy={privacyPolicy}
onChange={setTosAndPolicyAccepted}
/>
)}
<div className="mt-8 flex w-full flex-row items-center justify-between">
<Button type="button" variant={ButtonVariants.Secondary}>
back
</Button>
<Button type="submit" variant={ButtonVariants.Primary}>
continue
</Button>
</div>
</form>
);
}