mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-14 10:27:40 +00:00
Merge branch 'main' into refactor/login
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
"title": "Passwort festlegen",
|
||||
"description": "Legen Sie das Passwort für Ihr Konto fest",
|
||||
"codeSent": "Ein Code wurde an Ihre E-Mail-Adresse gesendet.",
|
||||
"noCodeReceived": "Keinen Code erhalten?",
|
||||
"resend": "Erneut senden",
|
||||
"submit": "Weiter"
|
||||
},
|
||||
@@ -173,6 +174,7 @@
|
||||
"verify": {
|
||||
"title": "Benutzer verifizieren",
|
||||
"description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.",
|
||||
"noCodeReceived": "Keinen Code erhalten?",
|
||||
"resendCode": "Code erneut senden",
|
||||
"submit": "Weiter"
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@
|
||||
"title": "Set Password",
|
||||
"description": "Set the password for your account",
|
||||
"codeSent": "A code has been sent to your email address.",
|
||||
"noCodeReceived": "Didn't receive a code?",
|
||||
"resend": "Resend code",
|
||||
"submit": "Continue"
|
||||
},
|
||||
@@ -173,6 +174,7 @@
|
||||
"verify": {
|
||||
"title": "Verify user",
|
||||
"description": "Enter the Code provided in the verification email.",
|
||||
"noCodeReceived": "Didn't receive a code?",
|
||||
"resendCode": "Resend code",
|
||||
"submit": "Continue"
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@
|
||||
"title": "Establecer Contraseña",
|
||||
"description": "Establece la contraseña para tu cuenta",
|
||||
"codeSent": "Se ha enviado un código a su correo electrónico.",
|
||||
"noCodeReceived": "¿No recibiste un código?",
|
||||
"resend": "Reenviar código",
|
||||
"submit": "Continuar"
|
||||
},
|
||||
@@ -173,6 +174,7 @@
|
||||
"verify": {
|
||||
"title": "Verificar usuario",
|
||||
"description": "Introduce el código proporcionado en el correo electrónico de verificación.",
|
||||
"noCodeReceived": "¿No recibiste un código?",
|
||||
"resendCode": "Reenviar código",
|
||||
"submit": "Continuar"
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@
|
||||
"title": "Imposta Password",
|
||||
"description": "Imposta la password per il tuo account",
|
||||
"codeSent": "Un codice è stato inviato al tuo indirizzo email.",
|
||||
"noCodeReceived": "Non hai ricevuto un codice?",
|
||||
"resend": "Invia di nuovo",
|
||||
"submit": "Continua"
|
||||
},
|
||||
@@ -173,6 +174,7 @@
|
||||
"verify": {
|
||||
"title": "Verifica utente",
|
||||
"description": "Inserisci il codice fornito nell'email di verifica.",
|
||||
"noCodeReceived": "Non hai ricevuto un codice?",
|
||||
"resendCode": "Invia di nuovo il codice",
|
||||
"submit": "Continua"
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@
|
||||
"title": "设置密码",
|
||||
"description": "为您的账户设置密码",
|
||||
"codeSent": "验证码已发送到您的邮箱。",
|
||||
"noCodeReceived": "没有收到验证码?",
|
||||
"resend": "重发验证码",
|
||||
"submit": "继续"
|
||||
},
|
||||
@@ -122,6 +123,18 @@
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
"methods": {
|
||||
"passkey": "密钥",
|
||||
"password": "密码"
|
||||
},
|
||||
"disabled": {
|
||||
"title": "注册已禁用",
|
||||
"description": "您的设置不允许注册新用户。"
|
||||
},
|
||||
"missingdata": {
|
||||
"title": "缺少数据",
|
||||
"description": "请提供所有必需的数据。"
|
||||
},
|
||||
"title": "注册",
|
||||
"description": "创建您的 ZITADEL 账户。",
|
||||
"selectMethod": "选择您想使用的认证方法",
|
||||
@@ -151,7 +164,8 @@
|
||||
},
|
||||
"signedin": {
|
||||
"title": "欢迎 {user}!",
|
||||
"description": "您已登录。"
|
||||
"description": "您已登录。",
|
||||
"continue": "继续"
|
||||
},
|
||||
"verify": {
|
||||
"userIdMissing": "未提供用户 ID!",
|
||||
@@ -160,6 +174,7 @@
|
||||
"verify": {
|
||||
"title": "验证用户",
|
||||
"description": "输入验证邮件中的验证码。",
|
||||
"noCodeReceived": "没有收到验证码?",
|
||||
"resendCode": "重发验证码",
|
||||
"submit": "继续"
|
||||
}
|
||||
|
@@ -1,8 +1,13 @@
|
||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { SessionsList } from "@/components/sessions-list";
|
||||
import { getAllSessionCookieIds } from "@/lib/cookies";
|
||||
import { getBrandingSettings, listSessions } from "@/lib/zitadel";
|
||||
import {
|
||||
getBrandingSettings,
|
||||
getDefaultOrg,
|
||||
listSessions,
|
||||
} from "@/lib/zitadel";
|
||||
import { UserPlusIcon } from "@heroicons/react/24/outline";
|
||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||
import { getLocale, getTranslations } from "next-intl/server";
|
||||
import Link from "next/link";
|
||||
|
||||
@@ -30,9 +35,19 @@ export default async function Page(props: {
|
||||
const authRequestId = searchParams?.authRequestId;
|
||||
const organization = searchParams?.organization;
|
||||
|
||||
let defaultOrganization;
|
||||
if (!organization) {
|
||||
const org: Organization | null = await getDefaultOrg();
|
||||
if (org) {
|
||||
defaultOrganization = org.id;
|
||||
}
|
||||
}
|
||||
|
||||
let sessions = await loadSessions();
|
||||
|
||||
const branding = await getBrandingSettings(organization);
|
||||
const branding = await getBrandingSettings(
|
||||
organization ?? defaultOrganization,
|
||||
);
|
||||
|
||||
return (
|
||||
<DynamicTheme branding={branding}>
|
||||
|
@@ -32,7 +32,9 @@ export default async function Page(props: {
|
||||
sessionFactors?.factors?.user?.organizationId,
|
||||
);
|
||||
|
||||
const loginSettings = await getLoginSettings(organization);
|
||||
const loginSettings = await getLoginSettings(
|
||||
sessionFactors?.factors?.user?.organizationId,
|
||||
);
|
||||
|
||||
return (
|
||||
<DynamicTheme branding={branding}>
|
||||
|
@@ -1,30 +0,0 @@
|
||||
import { getAllSessions } from "@/lib/cookies";
|
||||
import { listSessions } from "@/lib/zitadel";
|
||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
async function loadSessions(ids: string[]): Promise<Session[]> {
|
||||
const response = await listSessions(
|
||||
ids.filter((id: string | undefined) => !!id),
|
||||
);
|
||||
|
||||
return response?.sessions ?? [];
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const sessionCookies = await getAllSessions();
|
||||
const ids = sessionCookies.map((s) => s.id);
|
||||
let sessions: Session[] = [];
|
||||
if (ids && ids.length) {
|
||||
sessions = await loadSessions(ids);
|
||||
}
|
||||
|
||||
const responseHeaders = new Headers();
|
||||
responseHeaders.set("Access-Control-Allow-Origin", "*");
|
||||
responseHeaders.set("Access-Control-Allow-Headers", "*");
|
||||
|
||||
return NextResponse.json(
|
||||
{ sessions },
|
||||
{ status: 200, headers: responseHeaders },
|
||||
);
|
||||
}
|
@@ -32,7 +32,7 @@ const LinkWrapper = ({
|
||||
|
||||
export const TOTP = (alreadyAdded: boolean, link: string) => {
|
||||
return (
|
||||
<LinkWrapper alreadyAdded={alreadyAdded} link={link} key={link}>
|
||||
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
|
||||
<div
|
||||
className={clsx(
|
||||
"font-medium flex items-center",
|
||||
@@ -40,45 +40,12 @@ export const TOTP = (alreadyAdded: boolean, link: string) => {
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
className="h-9 w-9 transform -translate-x-[2px] mr-4"
|
||||
version="1.1"
|
||||
baseProfile="basic"
|
||||
id="Layer_1"
|
||||
className="h-8 w-8 transform -translate-x-[2px] mr-4 fill-current text-black dark:text-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 512 512"
|
||||
xmlSpace="preserve"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="#1A73E8"
|
||||
d="M440,255.99997v0.00006C440,273.12085,426.12085,287,409.00003,287H302l-46-93.01001l49.6507-85.9951
|
||||
c8.56021-14.82629,27.51834-19.9065,42.34518-11.34724l0.00586,0.0034c14.82776,8.55979,19.90875,27.51928,11.34857,42.34682
|
||||
L309.70001,225h99.30002C426.12085,225,440,238.87917,440,255.99997z"
|
||||
/>
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M348.00174,415.34897l-0.00586,0.00339c-14.82684,8.55927-33.78497,3.47903-42.34518-11.34723L256,318.01001
|
||||
l-49.65065,85.99509c-8.5602,14.82629-27.51834,19.90652-42.34517,11.34729l-0.00591-0.00342
|
||||
c-14.82777-8.55978-19.90875-27.51929-11.34859-42.34683L202.29999,287L256,285l53.70001,2l49.6503,86.00214
|
||||
C367.91049,387.82968,362.8295,406.78918,348.00174,415.34897z"
|
||||
/>
|
||||
<path
|
||||
fill="#FBBC04"
|
||||
d="M256,193.98999L242,232l-39.70001-7l-49.6503-86.00212
|
||||
c-8.56017-14.82755-3.47919-33.78705,11.34859-42.34684l0.00591-0.00341c14.82683-8.55925,33.78497-3.47903,42.34517,11.34726
|
||||
L256,193.98999z"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M248,225l-36,62H102.99997C85.87916,287,72,273.12085,72,256.00003v-0.00006
|
||||
C72,238.87917,85.87916,225,102.99997,225H248z"
|
||||
/>
|
||||
<polygon
|
||||
fill="#185DB7"
|
||||
points="309.70001,287 202.29999,287 256,193.98999 "
|
||||
/>
|
||||
<title>timer-lock-outline</title>
|
||||
<path d="M11 8H13V14H11V8M13 19.92C12.67 19.97 12.34 20 12 20C8.13 20 5 16.87 5 13S8.13 6 12 6C14.82 6 17.24 7.67 18.35 10.06C18.56 10.04 18.78 10 19 10C19.55 10 20.07 10.11 20.57 10.28C20.23 9.22 19.71 8.24 19.03 7.39L20.45 5.97C20 5.46 19.55 5 19.04 4.56L17.62 6C16.07 4.74 14.12 4 12 4C7.03 4 3 8.03 3 13S7.03 22 12 22C12.42 22 12.83 21.96 13.24 21.91C13.09 21.53 13 21.12 13 20.7V19.92M15 1H9V3H15V1M23 17.3V20.8C23 21.4 22.4 22 21.7 22H16.2C15.6 22 15 21.4 15 20.7V17.2C15 16.6 15.6 16 16.2 16V14.5C16.2 13.1 17.6 12 19 12S21.8 13.1 21.8 14.5V16C22.4 16 23 16.6 23 17.3M20.5 14.5C20.5 13.7 19.8 13.2 19 13.2S17.5 13.7 17.5 14.5V16H20.5V14.5Z" />
|
||||
</svg>{" "}
|
||||
<span>Authenticator App</span>
|
||||
</div>
|
||||
@@ -93,7 +60,7 @@ C72,238.87917,85.87916,225,102.99997,225H248z"
|
||||
|
||||
export const U2F = (alreadyAdded: boolean, link: string) => {
|
||||
return (
|
||||
<LinkWrapper alreadyAdded={alreadyAdded} link={link}>
|
||||
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
|
||||
<div
|
||||
className={clsx(
|
||||
"font-medium flex items-center",
|
||||
@@ -127,7 +94,7 @@ export const U2F = (alreadyAdded: boolean, link: string) => {
|
||||
|
||||
export const EMAIL = (alreadyAdded: boolean, link: string) => {
|
||||
return (
|
||||
<LinkWrapper alreadyAdded={alreadyAdded} link={link}>
|
||||
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
|
||||
<div
|
||||
className={clsx(
|
||||
"font-medium flex items-center",
|
||||
@@ -162,7 +129,7 @@ export const EMAIL = (alreadyAdded: boolean, link: string) => {
|
||||
|
||||
export const SMS = (alreadyAdded: boolean, link: string) => {
|
||||
return (
|
||||
<LinkWrapper alreadyAdded={alreadyAdded} link={link}>
|
||||
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
|
||||
<div
|
||||
className={clsx(
|
||||
"font-medium flex items-center",
|
||||
@@ -196,7 +163,7 @@ export const SMS = (alreadyAdded: boolean, link: string) => {
|
||||
|
||||
export const PASSKEYS = (alreadyAdded: boolean, link: string) => {
|
||||
return (
|
||||
<LinkWrapper alreadyAdded={alreadyAdded} link={link}>
|
||||
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
|
||||
<div
|
||||
className={clsx(
|
||||
"font-medium flex items-center",
|
||||
@@ -230,7 +197,7 @@ export const PASSKEYS = (alreadyAdded: boolean, link: string) => {
|
||||
|
||||
export const PASSWORD = (alreadyAdded: boolean, link: string) => {
|
||||
return (
|
||||
<LinkWrapper alreadyAdded={alreadyAdded} link={link}>
|
||||
<LinkWrapper key={link} alreadyAdded={alreadyAdded} link={link}>
|
||||
<div
|
||||
className={clsx(
|
||||
"font-medium flex items-center",
|
||||
|
@@ -6,8 +6,10 @@ import {
|
||||
symbolValidator,
|
||||
upperCaseValidator,
|
||||
} from "@/helpers/validators";
|
||||
import { setMyPassword } from "@/lib/self";
|
||||
import { sendPassword } from "@/lib/server/password";
|
||||
import {
|
||||
checkSessionAndSetPassword,
|
||||
sendPassword,
|
||||
} from "@/lib/server/password";
|
||||
import { create } from "@zitadel/client";
|
||||
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
||||
@@ -60,8 +62,9 @@ export function ChangePasswordForm({
|
||||
|
||||
async function submitChange(values: Inputs) {
|
||||
setLoading(true);
|
||||
const changeResponse = await setMyPassword({
|
||||
sessionId: sessionId,
|
||||
|
||||
const changeResponse = checkSessionAndSetPassword({
|
||||
sessionId,
|
||||
password: values.password,
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -72,8 +75,12 @@ export function ChangePasswordForm({
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
if (changeResponse && "error" in changeResponse) {
|
||||
setError(changeResponse.error);
|
||||
if (changeResponse && "error" in changeResponse && changeResponse.error) {
|
||||
setError(
|
||||
typeof changeResponse.error === "string"
|
||||
? changeResponse.error
|
||||
: "Unknown error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -18,7 +18,7 @@ import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { FieldValues, useForm } from "react-hook-form";
|
||||
import { Alert } from "./alert";
|
||||
import { Alert, AlertType } from "./alert";
|
||||
import { BackButton } from "./back-button";
|
||||
import { Button, ButtonVariants } from "./button";
|
||||
import { TextInput } from "./input";
|
||||
@@ -192,34 +192,42 @@ export function SetPasswordForm({
|
||||
<form className="w-full">
|
||||
<div className="pt-4 grid grid-cols-1 gap-4 mb-4">
|
||||
{codeRequired && (
|
||||
<div className="flex flex-row items-end">
|
||||
<div className="flex-1">
|
||||
<TextInput
|
||||
type="text"
|
||||
required
|
||||
{...register("code", {
|
||||
required: "This field is required",
|
||||
})}
|
||||
label="Code"
|
||||
autoComplete="one-time-code"
|
||||
error={errors.code?.message as string}
|
||||
data-testid="code-text-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="ml-4 mb-1">
|
||||
<Button
|
||||
variant={ButtonVariants.Secondary}
|
||||
data-testid="resend-button"
|
||||
onClick={() => resendCode()}
|
||||
<Alert type={AlertType.INFO}>
|
||||
<div className="flex flex-row">
|
||||
<span className="flex-1 mr-auto text-left">
|
||||
{t("set.noCodeReceived")}
|
||||
</span>
|
||||
<button
|
||||
aria-label="Resend OTP Code"
|
||||
disabled={loading}
|
||||
type="button"
|
||||
className="ml-4 text-primary-light-500 dark:text-primary-dark-500 hover:dark:text-primary-dark-400 hover:text-primary-light-400 cursor-pointer disabled:cursor-default disabled:text-gray-400 dark:disabled:text-gray-700"
|
||||
onClick={() => {
|
||||
resendCode();
|
||||
}}
|
||||
data-testid="resend-button"
|
||||
>
|
||||
{t("set.resend")}
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
{codeRequired && (
|
||||
<div>
|
||||
<TextInput
|
||||
type="text"
|
||||
required
|
||||
{...register("code", {
|
||||
required: "This field is required",
|
||||
})}
|
||||
label="Code"
|
||||
autoComplete="one-time-code"
|
||||
error={errors.code?.message as string}
|
||||
data-testid="code-text-input"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="">
|
||||
<div>
|
||||
<TextInput
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
@@ -232,7 +240,7 @@ export function SetPasswordForm({
|
||||
data-testid="password-text-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="">
|
||||
<div>
|
||||
<TextInput
|
||||
type="password"
|
||||
required
|
||||
|
@@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { Alert } from "@/components/alert";
|
||||
import { Alert, AlertType } from "@/components/alert";
|
||||
import { resendVerification, sendVerification } from "@/lib/server/email";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { BackButton } from "./back-button";
|
||||
import { Button, ButtonVariants } from "./button";
|
||||
import { TextInput } from "./input";
|
||||
import { Spinner } from "./spinner";
|
||||
@@ -96,7 +97,26 @@ export function VerifyForm({ userId, code, isInvite, params }: Props) {
|
||||
return (
|
||||
<>
|
||||
<form className="w-full">
|
||||
<div className="">
|
||||
<Alert type={AlertType.INFO}>
|
||||
<div className="flex flex-row">
|
||||
<span className="flex-1 mr-auto text-left">
|
||||
{t("verify.noCodeReceived")}
|
||||
</span>
|
||||
<button
|
||||
aria-label="Resend OTP Code"
|
||||
disabled={loading}
|
||||
type="button"
|
||||
className="ml-4 text-primary-light-500 dark:text-primary-dark-500 hover:dark:text-primary-dark-400 hover:text-primary-light-400 cursor-pointer disabled:cursor-default disabled:text-gray-400 dark:disabled:text-gray-700"
|
||||
onClick={() => {
|
||||
resendCode();
|
||||
}}
|
||||
data-testid="resend-button"
|
||||
>
|
||||
{t("verify.resendCode")}
|
||||
</button>
|
||||
</div>
|
||||
</Alert>
|
||||
<div className="mt-4">
|
||||
<TextInput
|
||||
type="text"
|
||||
autoComplete="one-time-code"
|
||||
@@ -112,13 +132,7 @@ export function VerifyForm({ userId, code, isInvite, params }: Props) {
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex w-full flex-row items-center">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => resendCode()}
|
||||
variant={ButtonVariants.Secondary}
|
||||
>
|
||||
{t("verify.resendCode")}
|
||||
</Button>
|
||||
<BackButton />
|
||||
<span className="flex-grow"></span>
|
||||
<Button
|
||||
type="submit"
|
||||
|
@@ -1,12 +1,28 @@
|
||||
import { LANGUAGE_COOKIE_NAME } from "@/lib/i18n";
|
||||
import { LANGS, LANGUAGE_COOKIE_NAME, LANGUAGE_HEADER_NAME } from "@/lib/i18n";
|
||||
import deepmerge from "deepmerge";
|
||||
import { getRequestConfig } from "next-intl/server";
|
||||
import { cookies } from "next/headers";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
export default getRequestConfig(async () => {
|
||||
const fallback = "en";
|
||||
const cookiesList = await cookies();
|
||||
const locale: string = cookiesList.get(LANGUAGE_COOKIE_NAME)?.value ?? "en";
|
||||
|
||||
let locale: string = fallback;
|
||||
|
||||
const languageHeader = await (await headers()).get(LANGUAGE_HEADER_NAME);
|
||||
if (languageHeader) {
|
||||
const headerLocale = languageHeader.split(",")[0].split("-")[0]; // Extract the language code
|
||||
if (LANGS.map((l) => l.code).includes(headerLocale)) {
|
||||
locale = headerLocale;
|
||||
}
|
||||
}
|
||||
|
||||
const languageCookie = cookiesList?.get(LANGUAGE_COOKIE_NAME);
|
||||
if (languageCookie && languageCookie.value) {
|
||||
if (LANGS.map((l) => l.code).includes(languageCookie.value)) {
|
||||
locale = languageCookie.value;
|
||||
}
|
||||
}
|
||||
|
||||
const userMessages = (await import(`../../locales/${locale}.json`)).default;
|
||||
const fallbackMessages = (await import(`../../locales/${fallback}.json`))
|
||||
|
@@ -27,3 +27,4 @@ export const LANGS: Lang[] = [
|
||||
];
|
||||
|
||||
export const LANGUAGE_COOKIE_NAME = "NEXT_LOCALE";
|
||||
export const LANGUAGE_HEADER_NAME = "accept-language";
|
||||
|
@@ -1,9 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import {
|
||||
createSessionServiceClient,
|
||||
createUserServiceClient,
|
||||
} from "@zitadel/client/v2";
|
||||
import { createUserServiceClient } from "@zitadel/client/v2";
|
||||
import { createServerTransport } from "@zitadel/node";
|
||||
import { getSessionCookieById } from "./cookies";
|
||||
import { getSession } from "./zitadel";
|
||||
@@ -13,12 +10,6 @@ const transport = (token: string) =>
|
||||
baseUrl: process.env.ZITADEL_API_URL!,
|
||||
});
|
||||
|
||||
const sessionService = (sessionId: string) => {
|
||||
return getSessionCookieById({ sessionId }).then((session) => {
|
||||
return createSessionServiceClient(transport(session.token));
|
||||
});
|
||||
};
|
||||
|
||||
const myUserService = (sessionToken: string) => {
|
||||
return createUserServiceClient(transport(sessionToken));
|
||||
};
|
||||
@@ -41,7 +32,7 @@ export async function setMyPassword({
|
||||
return { error: "Could not load session" };
|
||||
}
|
||||
|
||||
const service = await myUserService(sessionCookie.token);
|
||||
const service = await myUserService(`${sessionCookie.token}`);
|
||||
|
||||
if (!session?.factors?.user?.id) {
|
||||
return { error: "No user id found in session" };
|
||||
@@ -56,6 +47,7 @@ export async function setMyPassword({
|
||||
{},
|
||||
)
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
if (error.code === 7) {
|
||||
return { error: "Session is not valid." };
|
||||
}
|
||||
|
@@ -6,23 +6,30 @@ import {
|
||||
} from "@/lib/server/cookie";
|
||||
import {
|
||||
getLoginSettings,
|
||||
getSession,
|
||||
getUserByID,
|
||||
listAuthenticationMethodTypes,
|
||||
listUsers,
|
||||
passwordReset,
|
||||
setPassword,
|
||||
setUserPassword,
|
||||
} from "@/lib/zitadel";
|
||||
import { create } from "@zitadel/client";
|
||||
import { createUserServiceClient } from "@zitadel/client/v2";
|
||||
import { createServerTransport } from "@zitadel/node";
|
||||
import {
|
||||
Checks,
|
||||
ChecksSchema,
|
||||
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||
import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||
import {
|
||||
AuthenticationMethodType,
|
||||
SetPasswordRequestSchema,
|
||||
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||
import { headers } from "next/headers";
|
||||
import { getNextUrl } from "../client";
|
||||
import { getSessionCookieByLoginName } from "../cookies";
|
||||
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
|
||||
|
||||
type ResetPasswordCommand = {
|
||||
loginName: string;
|
||||
@@ -142,18 +149,10 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
||||
return { error: "Could not verify password!" };
|
||||
}
|
||||
|
||||
const availableSecondFactors = authMethods?.filter(
|
||||
(m: AuthenticationMethodType) =>
|
||||
m !== AuthenticationMethodType.PASSWORD &&
|
||||
m !== AuthenticationMethodType.PASSKEY,
|
||||
);
|
||||
|
||||
const humanUser = user.type.case === "human" ? user.type.value : undefined;
|
||||
|
||||
if (
|
||||
availableSecondFactors?.length == 0 &&
|
||||
humanUser?.passwordChangeRequired
|
||||
) {
|
||||
// check if the user has to change password first
|
||||
if (humanUser?.passwordChangeRequired) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: session.factors?.user?.loginName,
|
||||
});
|
||||
@@ -169,7 +168,13 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
||||
return { redirect: "/password/change?" + params };
|
||||
}
|
||||
|
||||
if (availableSecondFactors?.length == 1) {
|
||||
const availableMultiFactors = authMethods?.filter(
|
||||
(m: AuthenticationMethodType) =>
|
||||
m !== AuthenticationMethodType.PASSWORD &&
|
||||
m !== AuthenticationMethodType.PASSKEY,
|
||||
);
|
||||
|
||||
if (availableMultiFactors?.length == 1) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: session.factors?.user.loginName,
|
||||
});
|
||||
@@ -185,7 +190,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
||||
);
|
||||
}
|
||||
|
||||
const factor = availableSecondFactors[0];
|
||||
const factor = availableMultiFactors[0];
|
||||
// if passwordless is other method, but user selected password as alternative, perform a login
|
||||
if (factor === AuthenticationMethodType.TOTP) {
|
||||
return { redirect: `/otp/time-based?` + params };
|
||||
@@ -196,7 +201,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
||||
} else if (factor === AuthenticationMethodType.U2F) {
|
||||
return { redirect: `/u2f?` + params };
|
||||
}
|
||||
} else if (availableSecondFactors?.length >= 1) {
|
||||
} else if (availableMultiFactors?.length >= 1) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: session.factors.user.loginName,
|
||||
});
|
||||
@@ -219,7 +224,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
||||
return { error: "Initial User not supported" };
|
||||
} else if (
|
||||
(loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) &&
|
||||
!availableSecondFactors.length
|
||||
!availableMultiFactors.length
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: session.factors.user.loginName,
|
||||
@@ -302,5 +307,99 @@ export async function changePassword(command: {
|
||||
}
|
||||
const userId = user.userId;
|
||||
|
||||
return setPassword(userId, command.password, user, command.code);
|
||||
return setUserPassword(userId, command.password, user, command.code);
|
||||
}
|
||||
|
||||
type CheckSessionAndSetPasswordCommand = {
|
||||
sessionId: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export async function checkSessionAndSetPassword({
|
||||
sessionId,
|
||||
password,
|
||||
}: CheckSessionAndSetPasswordCommand) {
|
||||
const sessionCookie = await getSessionCookieById({ sessionId });
|
||||
|
||||
const { session } = await getSession({
|
||||
sessionId: sessionCookie.id,
|
||||
sessionToken: sessionCookie.token,
|
||||
});
|
||||
|
||||
if (!session || !session.factors?.user?.id) {
|
||||
return { error: "Could not load session" };
|
||||
}
|
||||
|
||||
const payload = create(SetPasswordRequestSchema, {
|
||||
userId: session.factors.user.id,
|
||||
newPassword: {
|
||||
password,
|
||||
},
|
||||
});
|
||||
|
||||
// check if the user has no password set in order to set a password
|
||||
const authmethods = await listAuthenticationMethodTypes(
|
||||
session.factors.user.id,
|
||||
);
|
||||
|
||||
if (!authmethods) {
|
||||
return { error: "Could not load auth methods" };
|
||||
}
|
||||
|
||||
const requiredAuthMethodsForForceMFA = [
|
||||
AuthenticationMethodType.OTP_EMAIL,
|
||||
AuthenticationMethodType.OTP_SMS,
|
||||
AuthenticationMethodType.TOTP,
|
||||
AuthenticationMethodType.U2F,
|
||||
];
|
||||
|
||||
const hasNoMFAMethods = requiredAuthMethodsForForceMFA.every(
|
||||
(method) => !authmethods.authMethodTypes.includes(method),
|
||||
);
|
||||
|
||||
const loginSettings = await getLoginSettings(
|
||||
session.factors.user.organizationId,
|
||||
);
|
||||
|
||||
const forceMfa = !!(
|
||||
loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly
|
||||
);
|
||||
|
||||
// if the user has no MFA but MFA is enforced, we can set a password otherwise we use the token of the user
|
||||
if (forceMfa && hasNoMFAMethods) {
|
||||
return setPassword(payload).catch((error) => {
|
||||
// throw error if failed precondition (ex. User is not yet initialized)
|
||||
if (error.code === 9 && error.message) {
|
||||
return { error: "Failed precondition" };
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const myUserService = (sessionToken: string) => {
|
||||
return createUserServiceClient(
|
||||
createServerTransport(sessionToken, {
|
||||
baseUrl: process.env.ZITADEL_API_URL!,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const selfService = await myUserService(`${sessionCookie.token}`);
|
||||
|
||||
return selfService
|
||||
.setPassword(
|
||||
{
|
||||
userId: session.factors.user.id,
|
||||
newPassword: { password, changeRequired: false },
|
||||
},
|
||||
{},
|
||||
)
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
if (error.code === 7) {
|
||||
return { error: "Session is not valid." };
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -32,23 +32,25 @@ export async function addU2F(command: RegisterU2FCommand) {
|
||||
sessionToken: sessionCookie.token,
|
||||
});
|
||||
|
||||
const domain = (await headers()).get("host");
|
||||
const host = (await headers()).get("host");
|
||||
|
||||
if (!domain) {
|
||||
if (!host) {
|
||||
return { error: "Could not get domain" };
|
||||
}
|
||||
|
||||
const [hostname, port] = host.split(":");
|
||||
|
||||
if (!hostname) {
|
||||
throw new Error("Could not get hostname");
|
||||
}
|
||||
|
||||
const userId = session?.session?.factors?.user?.id;
|
||||
|
||||
if (!session || !userId) {
|
||||
return { error: "Could not get session" };
|
||||
}
|
||||
|
||||
return registerU2F(
|
||||
userId,
|
||||
domain,
|
||||
// sessionCookie.token
|
||||
);
|
||||
return registerU2F(userId, hostname);
|
||||
}
|
||||
|
||||
export async function verifyU2F(command: VerifyU2FCommand) {
|
||||
|
@@ -13,6 +13,7 @@ import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||
import { IDPInformation } from "@zitadel/proto/zitadel/user/v2/idp_pb";
|
||||
import {
|
||||
RetrieveIdentityProviderIntentRequest,
|
||||
SetPasswordRequest,
|
||||
SetPasswordRequestSchema,
|
||||
VerifyPasskeyRegistrationRequest,
|
||||
VerifyU2FRegistrationRequest,
|
||||
@@ -536,7 +537,7 @@ export async function passwordReset(
|
||||
* @param code optional if the password should be set with a code (reset), no code for initial setup of password
|
||||
* @returns
|
||||
*/
|
||||
export async function setPassword(
|
||||
export async function setUserPassword(
|
||||
userId: string,
|
||||
password: string,
|
||||
user: User,
|
||||
@@ -582,6 +583,10 @@ export async function setPassword(
|
||||
});
|
||||
}
|
||||
|
||||
export async function setPassword(payload: SetPasswordRequest) {
|
||||
return userService.setPassword(payload, {});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param server
|
||||
@@ -615,17 +620,7 @@ export async function createPasskeyRegistrationLink(
|
||||
* @returns the newly set email
|
||||
*/
|
||||
|
||||
// TODO check for token requirements!
|
||||
export async function registerU2F(
|
||||
userId: string,
|
||||
domain: string,
|
||||
// token: string,
|
||||
) {
|
||||
// const transport = createServerTransport(token, {
|
||||
// baseUrl: process.env.ZITADEL_API_URL!,
|
||||
// });
|
||||
|
||||
// const service = createUserServiceClient(transport);
|
||||
export async function registerU2F(userId: string, domain: string) {
|
||||
return userService.registerU2F({
|
||||
userId,
|
||||
domain,
|
||||
|
Reference in New Issue
Block a user