mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-12 01:52:54 +00:00
checkbox, complexity policy
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
|
||||
65
apps/login/ui/Checkbox.tsx
Normal file
65
apps/login/ui/Checkbox.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
);
|
||||
98
apps/login/ui/PasswordComplexity.tsx
Normal file
98
apps/login/ui/PasswordComplexity.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
// }
|
||||
}
|
||||
@@ -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
|
||||
<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
|
||||
<Link
|
||||
href={privacyPolicy.privacyLink}
|
||||
className="underline"
|
||||
target="_blank"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
66
apps/login/ui/RegisterForm.tsx
Normal file
66
apps/login/ui/RegisterForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user