Merge pull request #167 from zitadel/qa

fix: general improvements and fixes
This commit is contained in:
Max Peintner
2024-09-13 08:07:39 +02:00
committed by GitHub
18 changed files with 5851 additions and 4442 deletions

View File

@@ -1,3 +1,45 @@
# ZITADEL Login UI
This is going to be our next UI for the hosted login. It's based on Next.js 13 and its introduced `app/` directory.
## Flow Diagram
```mermaid
flowchart TD
A[Start] --> register
A[Start] --> accounts
A[Start] --> loginname
A[Start] --> register
A[Start] -- signInWithIDP --> idp
idp --> idp-success
idp --> idp-failure
idp-success --> B[signedin]
idp-failure --> loginname
loginname --> password
loginname -- hasPasskey --> passkey
loginname -- allowRegister --> register
passkey-add --passwordAllowed --> password
passkey -- hasPassword --> password
passkey --> B[signedin]
password -- hasMFA --> mfa
password -- allowPasskeys --> passkey-add
mfa --> otp
otp --> B[signedin]
mfa--> u2f
u2f -->B[signedin]
register --> passkey-add
register --> password-set
password-set --> B[signedin]
passkey-add --> B[signedin]
password --> B[signedin]
password-- forceMFA -->mfaset
mfaset --> u2fset
mfaset --> otpset
u2fset --> B[signedin]
otpset --> B[signedin]
accounts--> loginname
password -- not verified yet -->verify
register-- withpassword -->verify
passkey-- notVerified --> verify
verify --> B[signedin]
```

View File

@@ -1,4 +1,4 @@
import { PROVIDER_MAPPING } from "@/lib/idp";
import { idpTypeToIdentityProviderType, PROVIDER_MAPPING } from "@/lib/idp";
import {
addIDPLink,
createUser,
@@ -51,11 +51,17 @@ export default async function Page({
const idp = await getIDPByID(idpInformation.idpId);
const options = idp?.config?.options;
if (!idp) {
throw new Error("IDP not found");
}
const providerType = idpTypeToIdentityProviderType(idp.type);
// search for potential user via username, then link
if (options?.isLinkingAllowed) {
let foundUser;
const email =
PROVIDER_MAPPING[provider](idpInformation).email?.email;
PROVIDER_MAPPING[providerType](idpInformation).email?.email;
if (options.autoLinking === AutoLinkingOption.EMAIL && email) {
foundUser = await listUsers({ email }).then((response) => {
@@ -118,7 +124,7 @@ export default async function Page({
}
if (options?.isCreationAllowed && options.isAutoCreation) {
const newUser = await createUser(provider, idpInformation);
const newUser = await createUser(providerType, idpInformation);
if (newUser) {
return (

View File

@@ -45,7 +45,6 @@ export default async function Page({
<p className="ztdl-p">Enter your login data.</p>
<UsernameForm
loginSettings={loginSettings}
loginName={loginName}
authRequestId={authRequestId}
organization={organization}

View File

@@ -1,35 +0,0 @@
"use client";
import { Button, ButtonVariants } from "@/ui/Button";
import { TextInput } from "@/ui/Input";
import UserAvatar from "@/ui/UserAvatar";
import { useRouter } from "next/navigation";
export default function Page() {
const router = useRouter();
return (
<div className="flex flex-col items-center space-y-4">
<h1>Password</h1>
<p className="ztdl-p mb-6 block">Enter your password.</p>
<UserAvatar
showDropdown
displayName="Max Peintner"
loginName="max@zitadel.com"
></UserAvatar>
<div className="w-full">
<TextInput type="password" label="Password" />
</div>
<div className="flex w-full flex-row items-center justify-between">
<Button
onClick={() => router.back()}
variant={ButtonVariants.Secondary}
>
back
</Button>
<Button variant={ButtonVariants.Primary}>continue</Button>
</div>
</div>
);
}

View File

@@ -1,14 +1,24 @@
import { getBrandingSettings } from "@/lib/zitadel";
import { getBrandingSettings, getLoginSettings } from "@/lib/zitadel";
import Alert from "@/ui/Alert";
import DynamicTheme from "@/ui/DynamicTheme";
import VerifyEmailForm from "@/ui/VerifyEmailForm";
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
export default async function Page({ searchParams }: { searchParams: any }) {
const { userId, sessionId, code, submit, organization, authRequestId } =
searchParams;
const {
userId,
loginName,
sessionId,
code,
submit,
organization,
authRequestId,
} = searchParams;
const branding = await getBrandingSettings(organization);
const loginSettings = await getLoginSettings(organization);
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4">
@@ -17,14 +27,25 @@ export default async function Page({ searchParams }: { searchParams: any }) {
Enter the Code provided in the verification email.
</p>
{!userId && (
<div className="py-4">
<Alert>
Could not get the context of the user. Make sure to provide a
userId as searchParam.
</Alert>
</div>
)}
{userId ? (
<VerifyEmailForm
userId={userId}
loginName={loginName}
code={code}
submit={submit === "true"}
organization={organization}
authRequestId={authRequestId}
sessionId={sessionId}
loginSettings={loginSettings}
/>
) : (
<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">

View File

@@ -1,4 +1,5 @@
import { create } from "@zitadel/client";
import { IDPType } from "@zitadel/proto/zitadel/idp/v2/idp_pb";
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { IDPInformation } from "@zitadel/proto/zitadel/user/v2/idp_pb";
import {
@@ -11,12 +12,22 @@ export function idpTypeToSlug(idpType: IdentityProviderType) {
switch (idpType) {
case IdentityProviderType.GITHUB:
return "github";
case IdentityProviderType.GITHUB_ES:
return "github_es";
case IdentityProviderType.GITLAB:
return "gitlab";
case IdentityProviderType.GITLAB_SELF_HOSTED:
return "gitlab_es";
case IdentityProviderType.APPLE:
return "apple";
case IdentityProviderType.GOOGLE:
return "google";
case IdentityProviderType.AZURE_AD:
return "azure";
case IdentityProviderType.SAML:
return "saml";
case IdentityProviderType.OAUTH:
return "oauth";
case IdentityProviderType.OIDC:
return "oidc";
default:
@@ -24,6 +35,45 @@ export function idpTypeToSlug(idpType: IdentityProviderType) {
}
}
// TODO: this is ugly but needed atm as the getIDPByID returns a IDPType and not a IdentityProviderType
export function idpTypeToIdentityProviderType(
idpType: IDPType,
): IdentityProviderType {
switch (idpType) {
case IDPType.IDP_TYPE_GITHUB:
return IdentityProviderType.GITHUB;
case IDPType.IDP_TYPE_GITHUB_ES:
return IdentityProviderType.GITHUB_ES;
case IDPType.IDP_TYPE_GITLAB:
return IdentityProviderType.GITLAB;
case IDPType.IDP_TYPE_GITLAB_SELF_HOSTED:
return IdentityProviderType.GITLAB_SELF_HOSTED;
case IDPType.IDP_TYPE_APPLE:
return IdentityProviderType.APPLE;
case IDPType.IDP_TYPE_GOOGLE:
return IdentityProviderType.GOOGLE;
case IDPType.IDP_TYPE_AZURE_AD:
return IdentityProviderType.AZURE_AD;
case IDPType.IDP_TYPE_SAML:
return IdentityProviderType.SAML;
case IDPType.IDP_TYPE_OAUTH:
return IdentityProviderType.OAUTH;
case IDPType.IDP_TYPE_OIDC:
return IdentityProviderType.OIDC;
default:
throw new Error("Unknown identity provider type");
}
}
// this maps the IDPInformation to the AddHumanUserRequest which is used when creating a user or linking a user (email)
// TODO: extend this object from a other file which can be overwritten by customers like map = { ...PROVIDER_MAPPING, ...customerMap }
export type OIDC_USER = {
@@ -35,10 +85,89 @@ export type OIDC_USER = {
};
};
const GITLAB_MAPPING = (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as {
name: string;
email: string;
email_verified: boolean;
};
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.email,
verification: { case: "isVerified", value: rawInfo.email_verified },
},
profile: {
displayName: rawInfo.name || idp.userName || "",
givenName: "",
familyName: "",
},
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
};
const OIDC_MAPPING = (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as OIDC_USER;
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.User?.email,
verification: { case: "isVerified", value: true },
},
profile: {
displayName: rawInfo.User?.name ?? "",
givenName: rawInfo.User?.given_name ?? "",
familyName: rawInfo.User?.family_name ?? "",
},
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
};
const GITHUB_MAPPING = (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as {
email: string;
name: string;
};
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.email,
verification: { case: "isVerified", value: true },
},
profile: {
displayName: rawInfo.name ?? "",
givenName: rawInfo.name ?? "",
familyName: rawInfo.name ?? "",
},
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
};
export const PROVIDER_MAPPING: {
[provider: string]: (rI: IDPInformation) => AddHumanUserRequest;
[provider: number]: (rI: IDPInformation) => AddHumanUserRequest;
} = {
[idpTypeToSlug(IdentityProviderType.GOOGLE)]: (idp: IDPInformation) => {
[IdentityProviderType.GOOGLE]: (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as OIDC_USER;
console.log(rawInfo);
@@ -62,7 +191,12 @@ export const PROVIDER_MAPPING: {
],
});
},
[idpTypeToSlug(IdentityProviderType.AZURE_AD)]: (idp: IDPInformation) => {
[IdentityProviderType.GITLAB]: GITLAB_MAPPING,
[IdentityProviderType.GITLAB_SELF_HOSTED]: GITLAB_MAPPING,
[IdentityProviderType.OIDC]: OIDC_MAPPING,
// check
[IdentityProviderType.OAUTH]: OIDC_MAPPING,
[IdentityProviderType.AZURE_AD]: (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as {
jobTitle: string;
mail: string;
@@ -76,8 +210,6 @@ export const PROVIDER_MAPPING: {
userPrincipalName: string;
};
console.log(rawInfo, rawInfo.userPrincipalName);
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
@@ -98,22 +230,26 @@ export const PROVIDER_MAPPING: {
],
});
},
[idpTypeToSlug(IdentityProviderType.GITHUB)]: (idp: IDPInformation) => {
[IdentityProviderType.GITHUB]: GITHUB_MAPPING,
[IdentityProviderType.GITHUB_ES]: GITHUB_MAPPING,
[IdentityProviderType.APPLE]: (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as {
email: string;
name: string;
name?: string;
firstName?: string;
lastName?: string;
email?: string;
};
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.email,
email: rawInfo.email ?? "",
verification: { case: "isVerified", value: true },
},
profile: {
displayName: rawInfo.name ?? "",
givenName: rawInfo.name ?? "",
familyName: rawInfo.name ?? "",
givenName: rawInfo.firstName ?? "",
familyName: rawInfo.lastName ?? "",
},
idpLinks: [
{

View File

@@ -1,5 +1,7 @@
"use server";
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { createSessionForUserIdAndUpdateCookie } from "../../utils/session";
@@ -24,6 +26,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
organizationId: command.organization,
});
const loginSettings = await getLoginSettings(command.organization);
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
const userId = users.result[0].userId;
const session = await createSessionForUserIdAndUpdateCookie(
@@ -41,14 +45,102 @@ export async function sendLoginname(command: SendLoginnameCommand) {
session.factors?.user?.id,
);
return {
authMethodTypes: methods.authMethodTypes,
sessionId: session.id,
factors: session.factors,
};
if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
throw Error(
"User has no available authentication methods. Contact your administrator to setup authentication for the requested user.",
);
}
if (methods.authMethodTypes.length == 1) {
const method = methods.authMethodTypes[0];
switch (method) {
case AuthenticationMethodType.PASSWORD: // user has only password as auth method
const paramsPassword: any = {
loginName: session.factors?.user?.loginName,
};
// TODO: does this have to be checked in loginSettings.allowDomainDiscovery
if (command.organization || session.factors?.user?.organizationId) {
paramsPassword.organization =
command.organization ?? session.factors?.user?.organizationId;
}
if (
loginSettings?.passkeysType &&
loginSettings?.passkeysType === PasskeysType.ALLOWED
) {
paramsPassword.promptPasswordless = `true`;
}
if (command.authRequestId) {
paramsPassword.authRequestId = command.authRequestId;
}
return redirect("/password?" + new URLSearchParams(paramsPassword));
case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
const paramsPasskey: any = { loginName: command.loginName };
if (command.authRequestId) {
paramsPasskey.authRequestId = command.authRequestId;
}
if (command.organization || session.factors?.user?.organizationId) {
paramsPasskey.organization =
command.organization ?? session.factors?.user?.organizationId;
}
return redirect(
"/passkey/login?" + new URLSearchParams(paramsPasskey),
);
}
} else {
// prefer passkey in favor of other methods
if (methods.authMethodTypes.includes(AuthenticationMethodType.PASSKEY)) {
const passkeyParams: any = {
loginName: command.loginName,
altPassword: `${methods.authMethodTypes.includes(1)}`, // show alternative password option
};
if (command.authRequestId) {
passkeyParams.authRequestId = command.authRequestId;
}
if (command.organization || session.factors?.user?.organizationId) {
passkeyParams.organization =
command.organization ?? session.factors?.user?.organizationId;
}
return redirect("/passkey/login?" + new URLSearchParams(passkeyParams));
} else if (
methods.authMethodTypes.includes(AuthenticationMethodType.IDP)
) {
// TODO: redirect user to idp
} else if (
methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD)
) {
// user has no passkey setup and login settings allow passkeys
const paramsPasswordDefault: any = { loginName: command.loginName };
if (loginSettings?.passkeysType === 1) {
paramsPasswordDefault.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED,
}
if (command.authRequestId) {
paramsPasswordDefault.authRequestId = command.authRequestId;
}
if (command.organization || session.factors?.user?.organizationId) {
paramsPasswordDefault.organization =
command.organization ?? session.factors?.user?.organizationId;
}
return redirect(
"/password?" + new URLSearchParams(paramsPasswordDefault),
);
}
}
}
const loginSettings = await getLoginSettings(command.organization);
// TODO: check if allowDomainDiscovery has to be allowed too, to redirect to the register page
// user not found, check if register is enabled on organization
@@ -76,7 +168,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
params.set("organization", command.organization);
}
return startIdentityProviderFlow({
const resp = await startIdentityProviderFlow({
idpId: identityProviders[0].id,
urls: {
successUrl:
@@ -84,14 +176,14 @@ export async function sendLoginname(command: SendLoginnameCommand) {
failureUrl:
`${host}/idp/${provider}/failure?` + new URLSearchParams(params),
},
}).then((resp: any) => {
if (resp.authUrl) {
return redirect(resp.authUrl);
}
});
} else {
throw Error("Could not find user");
if (resp?.nextStep.case === "authUrl") {
return redirect(resp.nextStep.value);
}
}
throw Error("Could not find user");
} else if (
loginSettings?.allowRegister &&
loginSettings?.allowUsernamePassword

View File

@@ -20,6 +20,7 @@ import {
import { create } from "@zitadel/client";
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb";
import {
SearchQuery,
@@ -30,8 +31,6 @@ import { PROVIDER_MAPPING } from "./idp";
const SESSION_LIFETIME_S = 3600;
console.log("Session lifetime", SESSION_LIFETIME_S);
const transport = createServerTransport(
process.env.ZITADEL_SERVICE_USER_TOKEN!,
{
@@ -438,7 +437,10 @@ export function addIDPLink(
);
}
export function createUser(provider: string, info: IDPInformation) {
export function createUser(
provider: IdentityProviderType,
info: IDPInformation,
) {
const userData = PROVIDER_MAPPING[provider](info);
console.log("ud", userData);
return userService.addHumanUser(userData, {});

View File

@@ -12,6 +12,7 @@ import { useRouter } from "next/navigation";
import { useState } from "react";
import { FieldValues, useForm } from "react-hook-form";
import Alert from "./Alert";
import BackButton from "./BackButton";
import { Button, ButtonVariants } from "./Button";
import { TextInput } from "./Input";
import PasswordComplexity from "./PasswordComplexity";
@@ -55,13 +56,13 @@ export default function ChangePasswordForm({
userId: userId,
password: values.password,
}).catch((error: Error) => {
setError(error.message ?? "Could not register user");
setError(error.message ?? "Could not change password");
});
setLoading(false);
if (!response) {
setError("Could not register user");
setError("Could not change password");
return;
}
@@ -129,9 +130,7 @@ export default function ChangePasswordForm({
{error && <Alert>{error}</Alert>}
<div className="mt-8 flex w-full flex-row items-center justify-between">
<Button type="button" variant={ButtonVariants.Secondary}>
back
</Button>
<BackButton />
<Button
type="submit"
variant={ButtonVariants.Primary}

View File

@@ -89,9 +89,7 @@ export default function RegisterFormWithoutPassword({
if (withPassword) {
return router.push(`/register?` + new URLSearchParams(registerParams));
} else {
const session = await submitAndRegister(value).catch((error) => {
setError(error.message ?? "Could not register user");
});
const session = await submitAndRegister(value);
const params = new URLSearchParams({});
if (session?.factors?.user?.loginName) {

View File

@@ -12,6 +12,7 @@ import { useRouter } from "next/navigation";
import { useState } from "react";
import { FieldValues, useForm } from "react-hook-form";
import Alert from "./Alert";
import BackButton from "./BackButton";
import { Button, ButtonVariants } from "./Button";
import { TextInput } from "./Input";
import PasswordComplexity from "./PasswordComplexity";
@@ -74,19 +75,23 @@ export default function SetPasswordForm({
setError("Could not register user");
return;
}
const params: any = { userId: response.userId };
const params = new URLSearchParams({ userId: response.userId });
if (response.factors?.user?.loginName) {
params.append("loginName", response.factors.user.loginName);
}
if (authRequestId) {
params.authRequestId = authRequestId;
params.append("authRequestId", authRequestId);
}
if (organization) {
params.organization = organization;
params.append("organization", organization);
}
if (response && response.sessionId) {
params.sessionId = response.sessionId;
params.append("sessionId", response.sessionId);
}
return router.push(`/verify?` + new URLSearchParams(params));
return router.push(`/verify?` + params);
}
const { errors } = formState;
@@ -150,9 +155,7 @@ export default function SetPasswordForm({
{error && <Alert>{error}</Alert>}
<div className="mt-8 flex w-full flex-row items-center justify-between">
<Button type="button" variant={ButtonVariants.Secondary}>
back
</Button>
<BackButton />
<Button
type="submit"
variant={ButtonVariants.Primary}

View File

@@ -9,7 +9,9 @@ import {
import { useRouter } from "next/navigation";
import { ReactNode, useState } from "react";
import Alert from "./Alert";
import { SignInWithApple } from "./idps/SignInWithApple";
import { SignInWithAzureAD } from "./idps/SignInWithAzureAD";
import { SignInWithGeneric } from "./idps/SignInWithGeneric";
import { SignInWithGithub } from "./idps/SignInWithGithub";
import { SignInWithGitlab } from "./idps/SignInWithGitlab";
import { SignInWithGoogle } from "./idps/SignInWithGoogle";
@@ -76,10 +78,41 @@ export function SignInWithIDP({
{identityProviders &&
identityProviders.map((idp, i) => {
switch (idp.type) {
case IdentityProviderType.APPLE:
return (
<SignInWithApple
key={`idp-${i}`}
name={idp.name}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.APPLE)
}
></SignInWithApple>
);
case IdentityProviderType.OAUTH:
return (
<SignInWithGeneric
key={`idp-${i}`}
name={idp.name}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.OAUTH)
}
></SignInWithGeneric>
);
case IdentityProviderType.OIDC:
return (
<SignInWithGeneric
key={`idp-${i}`}
name={idp.name}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.OIDC)
}
></SignInWithGeneric>
);
case IdentityProviderType.GITHUB:
return (
<SignInWithGithub
key={`idp-${i}`}
name={idp.name}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.GITHUB)
}
@@ -89,6 +122,7 @@ export function SignInWithIDP({
return (
<SignInWithGithub
key={`idp-${i}`}
name={idp.name}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.GITHUB_ES)
}
@@ -98,6 +132,7 @@ export function SignInWithIDP({
return (
<SignInWithAzureAD
key={`idp-${i}`}
name={idp.name}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.AZURE_AD)
}
@@ -118,6 +153,7 @@ export function SignInWithIDP({
return (
<SignInWithGitlab
key={`idp-${i}`}
name={idp.name}
onClick={() =>
navigateToAuthUrl(idp.id, IdentityProviderType.GITLAB)
}
@@ -127,6 +163,7 @@ export function SignInWithIDP({
return (
<SignInWithGitlab
key={`idp-${i}`}
name={idp.name}
onClick={() =>
navigateToAuthUrl(
idp.id,

View File

@@ -1,11 +1,6 @@
"use client";
import { sendLoginname } from "@/lib/server/loginname";
import {
LoginSettings,
PasskeysType,
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { useRouter } from "next/navigation";
import { ReactNode, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -20,7 +15,6 @@ type Inputs = {
};
type Props = {
loginSettings: LoginSettings | undefined;
loginName: string | undefined;
authRequestId: string | undefined;
organization?: string;
@@ -30,7 +24,6 @@ type Props = {
};
export default function UsernameForm({
loginSettings,
loginName,
authRequestId,
organization,
@@ -59,7 +52,6 @@ export default function UsernameForm({
authRequestId,
}).catch((error: Error) => {
setError(error.message ?? "An internal error occurred");
return Promise.reject(error ?? "An internal error occurred");
});
setLoading(false);
@@ -72,124 +64,6 @@ export default function UsernameForm({
organization?: string,
) {
const response = await submitLoginName(values, organization);
if (!response) {
setError("An internal error occurred");
return;
}
if (response?.authMethodTypes && response.authMethodTypes.length === 0) {
setError(
"User has no available authentication methods. Contact your administrator to setup authentication for the requested user.",
);
return;
}
if (response?.authMethodTypes.length == 1) {
const method = response.authMethodTypes[0];
switch (method) {
case AuthenticationMethodType.PASSWORD: // user has only password as auth method
const paramsPassword: any = {
loginName: response?.factors?.user?.loginName,
};
// TODO: does this have to be checked in loginSettings.allowDomainDiscovery
if (organization || response?.factors?.user?.organizationId) {
paramsPassword.organization =
organization ?? response?.factors?.user?.organizationId;
}
if (
loginSettings?.passkeysType &&
loginSettings?.passkeysType === PasskeysType.ALLOWED
) {
paramsPassword.promptPasswordless = `true`;
}
if (authRequestId) {
paramsPassword.authRequestId = authRequestId;
}
return router.push(
"/password?" + new URLSearchParams(paramsPassword),
);
case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
const paramsPasskey: any = { loginName: values.loginName };
if (authRequestId) {
paramsPasskey.authRequestId = authRequestId;
}
if (organization || response?.factors?.user?.organizationId) {
paramsPasskey.organization =
organization ?? response?.factors?.user?.organizationId;
}
return router.push(
"/passkey/login?" + new URLSearchParams(paramsPasskey),
);
default:
const paramsPasskeyDefault: any = { loginName: values.loginName };
if (loginSettings?.passkeysType === 1) {
paramsPasskeyDefault.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED,
}
if (authRequestId) {
paramsPasskeyDefault.authRequestId = authRequestId;
}
if (organization || response?.factors?.user?.organizationId) {
paramsPasskeyDefault.organization =
organization ?? response?.factors?.user?.organizationId;
}
return router.push(
"/password?" + new URLSearchParams(paramsPasskeyDefault),
);
}
} else {
// prefer passkey in favor of other methods
if (response?.authMethodTypes.includes(2)) {
const passkeyParams: any = {
loginName: values.loginName,
altPassword: `${response.authMethodTypes.includes(1)}`, // show alternative password option
};
if (authRequestId) {
passkeyParams.authRequestId = authRequestId;
}
if (organization || response?.factors?.user?.organizationId) {
passkeyParams.organization =
organization ?? response?.factors?.user?.organizationId;
}
return router.push(
"/passkey/login?" + new URLSearchParams(passkeyParams),
);
} else {
// user has no passkey setup and login settings allow passkeys
const paramsPasswordDefault: any = { loginName: values.loginName };
if (loginSettings?.passkeysType === 1) {
paramsPasswordDefault.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED,
}
if (authRequestId) {
paramsPasswordDefault.authRequestId = authRequestId;
}
if (organization || response?.factors?.user?.organizationId) {
paramsPasswordDefault.organization =
organization ?? response?.factors?.user?.organizationId;
}
return router.push(
"/password?" + new URLSearchParams(paramsPasswordDefault),
);
}
}
}
useEffect(() => {

View File

@@ -2,6 +2,7 @@
import { resendVerifyEmail, verifyUserByEmail } from "@/lib/server/email";
import Alert from "@/ui/Alert";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -15,20 +16,24 @@ type Inputs = {
type Props = {
userId: string;
loginName: string;
code: string;
submit: boolean;
organization?: string;
authRequestId?: string;
sessionId?: string;
loginSettings?: LoginSettings;
};
export default function VerifyEmailForm({
userId,
loginName,
code,
submit,
organization,
authRequestId,
sessionId,
loginSettings,
}: Props) {
const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onBlur",
@@ -71,7 +76,7 @@ export default function VerifyEmailForm({
userId,
}).catch((error: Error) => {
setLoading(false);
setError(error.message);
setError(error.message ?? "Could not verify email");
});
setLoading(false);

View File

@@ -0,0 +1,34 @@
"use client";
import { ReactNode, forwardRef } from "react";
import { IdpButtonClasses, SignInWithIdentityProviderProps } from "./classes";
export const SignInWithApple = forwardRef<
HTMLButtonElement,
SignInWithIdentityProviderProps
>(
({ children, className = "", name = "", ...props }, ref): ReactNode => (
<button
type="button"
ref={ref}
className={`${IdpButtonClasses} ${className}`}
{...props}
>
<div className="h-12 w-12 flex items-center justify-center">
<div className="h-6 w-6">
<svg viewBox="0 0 170 170" fill="currentColor">
<title>Apple Logo</title>
<path d="M150.37 130.25c-2.45 5.66-5.35 10.87-8.71 15.66-4.58 6.53-8.33 11.05-11.22 13.56-4.48 4.12-9.28 6.23-14.42 6.35-3.69 0-8.14-1.05-13.32-3.18-5.197-2.12-9.973-3.17-14.34-3.17-4.58 0-9.492 1.05-14.746 3.17-5.262 2.13-9.501 3.24-12.742 3.35-4.929.21-9.842-1.96-14.746-6.52-3.13-2.73-7.045-7.41-11.735-14.04-5.032-7.08-9.169-15.29-12.41-24.65-3.471-10.11-5.211-19.9-5.211-29.378 0-10.857 2.346-20.221 7.045-28.068 3.693-6.303 8.606-11.275 14.755-14.925s12.793-5.51 19.948-5.629c3.915 0 9.049 1.211 15.429 3.591 6.362 2.388 10.447 3.599 12.238 3.599 1.339 0 5.877-1.416 13.57-4.239 7.275-2.618 13.415-3.702 18.445-3.275 13.63 1.1 23.87 6.473 30.68 16.153-12.19 7.386-18.22 17.731-18.1 31.002.11 10.337 3.86 18.939 11.23 25.769 3.34 3.17 7.07 5.62 11.22 7.36-.9 2.61-1.85 5.11-2.86 7.51zM119.11 7.24c0 8.102-2.96 15.667-8.86 22.669-7.12 8.324-15.732 13.134-25.071 12.375a25.222 25.222 0 0 1-.188-3.07c0-7.778 3.386-16.102 9.399-22.908 3.002-3.446 6.82-6.311 11.45-8.597 4.62-2.252 8.99-3.497 13.1-3.71.12 1.083.17 2.166.17 3.24z" />
</svg>
</div>
</div>
{children ? (
children
) : (
<span className="ml-4">{name ? name : "Sign in with Apple"}</span>
)}
</button>
),
);
SignInWithApple.displayName = "SignInWithApple";

View File

@@ -0,0 +1,25 @@
"use client";
import { ReactNode, forwardRef } from "react";
import { IdpButtonClasses, SignInWithIdentityProviderProps } from "./classes";
export const SignInWithGeneric = forwardRef<
HTMLButtonElement,
SignInWithIdentityProviderProps
>(
(
{ children, className = "h-[50px] pl-20", name = "", ...props },
ref,
): ReactNode => (
<button
type="button"
ref={ref}
className={`${IdpButtonClasses} ${className}`}
{...props}
>
{children ? children : <span className="">{name}</span>}
</button>
),
);
SignInWithGeneric.displayName = "SignInWithGeneric";

View File

@@ -3,5 +3,5 @@ export { NewAuthorizationBearerInterceptor } from "./interceptors";
// TODO: Move this to `./protobuf.ts` and export it from there
export { create, fromJson, toJson } from "@bufbuild/protobuf";
export { TimestampSchema, timestampDate } from "@bufbuild/protobuf/wkt";
export { TimestampSchema, timestampDate, timestampFromDate } from "@bufbuild/protobuf/wkt";
export type { Timestamp } from "@bufbuild/protobuf/wkt";

9629
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff