mfa, otp i18n

This commit is contained in:
peintnermax
2024-10-09 17:03:03 +02:00
parent b2e8384902
commit b5e81d426f
7 changed files with 48 additions and 23 deletions

View File

@@ -59,6 +59,25 @@
"description": "Choose one of the following second factors." "description": "Choose one of the following second factors."
} }
}, },
"otp": {
"verify": {
"title": "Verify 2-Factor",
"totpDescription": "Enter the code from your authenticator app.",
"smsDescription": "Enter the code you received via SMS.",
"emailDescription": "Enter the code you received via email.",
"noCodeReceived": "Didn't receive a code?",
"resendCode": "Resend code",
"submit": "Continue"
},
"set": {
"title": "Set up 2-Factor",
"totpDescription": "Scan the QR code with your authenticator app.",
"smsDescription": "Enter your phone number to receive a code via SMS.",
"emailDescription": "Enter your email address to receive a code via email.",
"totpRegisterDescription": "Scan the QR Code or navigate to the URL manually.",
"submit": "Continue"
}
},
"error": { "error": {
"unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.", "unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.",
"sessionExpired": "You need to have a valid session in order to continue." "sessionExpired": "You need to have a valid session in order to continue."

View File

@@ -85,7 +85,7 @@ export default async function Page({
)} )}
{!(loginName || sessionId) && ( {!(loginName || sessionId) && (
<Alert>{t("error.unknownContext")}</Alert> <Alert>{t("error:unknownContext")}</Alert>
)} )}
{sessionFactors ? ( {sessionFactors ? (

View File

@@ -122,7 +122,7 @@ export default async function Page({
)} )}
{!(loginName || sessionId) && ( {!(loginName || sessionId) && (
<Alert>{t("error.unknownContext")}</Alert> <Alert>{t("error:unknownContext")}</Alert>
)} )}
{!valid && <Alert>{t("error.sessionExpired")}</Alert>} {!valid && <Alert>{t("error.sessionExpired")}</Alert>}

View File

@@ -4,6 +4,7 @@ import { LoginOTP } from "@/components/login-otp";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings } from "@/lib/zitadel"; import { getBrandingSettings } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server";
export default async function Page({ export default async function Page({
searchParams, searchParams,
@@ -12,6 +13,9 @@ export default async function Page({
searchParams: Record<string | number | symbol, string | undefined>; searchParams: Record<string | number | symbol, string | undefined>;
params: Record<string | number | symbol, string | undefined>; params: Record<string | number | symbol, string | undefined>;
}) { }) {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "otp" });
const { loginName, authRequestId, sessionId, organization, code, submit } = const { loginName, authRequestId, sessionId, organization, code, submit } =
searchParams; searchParams;
@@ -27,23 +31,20 @@ export default async function Page({
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>Verify 2-Factor</h1> <h1>{t("verify.title")}</h1>
{method === "time-based" && ( {method === "time-based" && (
<p className="ztdl-p">Enter the code from your authenticator app.</p> <p className="ztdl-p">{t("verify.totpDescription")}</p>
)} )}
{method === "sms" && ( {method === "sms" && (
<p className="ztdl-p">Enter the code you got on your phone.</p> <p className="ztdl-p">{t("verify.smsDescription")}</p>
)} )}
{method === "email" && ( {method === "email" && (
<p className="ztdl-p">Enter the code you got via your email.</p> <p className="ztdl-p">{t("verify.emailDescription")}</p>
)} )}
{!session && ( {!session && (
<div className="py-4"> <div className="py-4">
<Alert> <Alert>{t("error:unknownContext")}</Alert>
Could not get the context of the user. Make sure to enter the
username first or provide a loginName as searchParam.
</Alert>
</div> </div>
)} )}

View File

@@ -12,6 +12,7 @@ import {
registerTOTP, registerTOTP,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { RegisterTOTPResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { RegisterTOTPResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { getLocale, getTranslations } from "next-intl/server";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@@ -22,6 +23,9 @@ export default async function Page({
searchParams: Record<string | number | symbol, string | undefined>; searchParams: Record<string | number | symbol, string | undefined>;
params: Record<string | number | symbol, string | undefined>; params: Record<string | number | symbol, string | undefined>;
}) { }) {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "otp" });
const { loginName, organization, sessionId, authRequestId, checkAfter } = const { loginName, organization, sessionId, authRequestId, checkAfter } =
searchParams; searchParams;
const { method } = params; const { method } = params;
@@ -98,13 +102,10 @@ export default async function Page({
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<h1>Register 2-factor</h1> <h1>{t("set.title")}</h1>
{!session && ( {!session && (
<div className="py-4"> <div className="py-4">
<Alert> <Alert>{t("error:unknownContext")}</Alert>
Could not get the context of the user. Make sure to enter the
username first or provide a loginName as searchParam.
</Alert>
</div> </div>
)} )}
@@ -125,9 +126,7 @@ export default async function Page({
{totpResponse && "uri" in totpResponse && "secret" in totpResponse ? ( {totpResponse && "uri" in totpResponse && "secret" in totpResponse ? (
<> <>
<p className="ztdl-p"> <p className="ztdl-p">{t("set.totpRegisterDescription")}</p>
Scan the QR Code or navigate to the URL manually.
</p>
<div> <div>
<TotpRegister <TotpRegister
uri={totpResponse.uri as string} uri={totpResponse.uri as string}
@@ -160,7 +159,7 @@ export default async function Page({
className="self-end" className="self-end"
variant={ButtonVariants.Primary} variant={ButtonVariants.Primary}
> >
continue {t("set.submit")}
</Button> </Button>
</Link> </Link>
</div> </div>

View File

@@ -4,6 +4,7 @@ import { updateSession } from "@/lib/server/session";
import { create } from "@zitadel/client"; import { create } from "@zitadel/client";
import { RequestChallengesSchema } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; import { RequestChallengesSchema } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -35,6 +36,8 @@ export function LoginOTP({
method, method,
code, code,
}: Props) { }: Props) {
const t = useTranslations("otp");
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
@@ -191,7 +194,7 @@ export function LoginOTP({
<Alert type={AlertType.INFO}> <Alert type={AlertType.INFO}>
<div className="flex flex-row"> <div className="flex flex-row">
<span className="flex-1 mr-auto text-left"> <span className="flex-1 mr-auto text-left">
Did not get the Code? {t("noCodeReceived")}
</span> </span>
<button <button
aria-label="Resend OTP Code" aria-label="Resend OTP Code"
@@ -209,7 +212,7 @@ export function LoginOTP({
}); });
}} }}
> >
Resend {t("resendCode")}
</button> </button>
</div> </div>
</Alert> </Alert>
@@ -241,7 +244,7 @@ export function LoginOTP({
})} })}
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}
continue {t("submit")}
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -1,5 +1,6 @@
"use client"; "use client";
import { verifyTOTP } from "@/lib/server-actions"; import { verifyTOTP } from "@/lib/server-actions";
import { useTranslations } from "next-intl";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { QRCodeSVG } from "qrcode.react"; import { QRCodeSVG } from "qrcode.react";
@@ -33,6 +34,8 @@ export function TotpRegister({
organization, organization,
checkAfter, checkAfter,
}: Props) { }: Props) {
const t = useTranslations("otp");
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const router = useRouter(); const router = useRouter();
@@ -138,7 +141,7 @@ export function TotpRegister({
onClick={handleSubmit(continueWithCode)} onClick={handleSubmit(continueWithCode)}
> >
{loading && <Spinner className="h-5 w-5 mr-2" />} {loading && <Spinner className="h-5 w-5 mr-2" />}
continue {t("set.submit")}
</Button> </Button>
</div> </div>
</form> </form>