From e6c57068b8e677b0050474c9f7aee4386146fc54 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Fri, 27 Dec 2024 11:53:46 -0500 Subject: [PATCH 1/2] chore: improve idp integration using server action --- apps/login/package.json | 1 + .../login/src/components/idps/base-button.tsx | 18 +++++- .../components/idps/sign-in-with-github.tsx | 60 ++++++++++-------- .../login/src/components/sign-in-with-idp.tsx | 61 ++++++------------- apps/login/src/lib/server/idp.ts | 34 +++++++++++ pnpm-lock.yaml | 31 ++++++---- 6 files changed, 120 insertions(+), 85 deletions(-) diff --git a/apps/login/package.json b/apps/login/package.json index c200079530..1017c69310 100644 --- a/apps/login/package.json +++ b/apps/login/package.json @@ -46,6 +46,7 @@ "copy-to-clipboard": "^3.3.3", "deepmerge": "^4.3.1", "jose": "^5.3.0", + "lucide-react": "0.469.0", "moment": "^2.29.4", "next": "15.2.0-canary.33", "next-intl": "^3.25.1", diff --git a/apps/login/src/components/idps/base-button.tsx b/apps/login/src/components/idps/base-button.tsx index 4f24dd17bc..0185c57996 100644 --- a/apps/login/src/components/idps/base-button.tsx +++ b/apps/login/src/components/idps/base-button.tsx @@ -1,7 +1,9 @@ "use client"; import { clsx } from "clsx"; +import { Loader2Icon } from "lucide-react"; import { ButtonHTMLAttributes, DetailedHTMLProps, forwardRef } from "react"; +import { useFormStatus } from "react-dom"; export type SignInWithIdentityProviderProps = DetailedHTMLProps< ButtonHTMLAttributes, @@ -15,15 +17,25 @@ export const BaseButton = forwardRef< HTMLButtonElement, SignInWithIdentityProviderProps >(function BaseButton(props, ref) { + const formStatus = useFormStatus(); + return ( ); }); diff --git a/apps/login/src/components/idps/sign-in-with-github.tsx b/apps/login/src/components/idps/sign-in-with-github.tsx index 49b4ce7b26..45108d17f7 100644 --- a/apps/login/src/components/idps/sign-in-with-github.tsx +++ b/apps/login/src/components/idps/sign-in-with-github.tsx @@ -4,6 +4,39 @@ import { useTranslations } from "next-intl"; import { forwardRef } from "react"; import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; +function GitHubLogo() { + return ( + <> + + + + + + + + ); +} + export const SignInWithGithub = forwardRef< HTMLButtonElement, SignInWithIdentityProviderProps @@ -14,32 +47,7 @@ export const SignInWithGithub = forwardRef< return (
- - - - - - +
{children ? ( children diff --git a/apps/login/src/components/sign-in-with-idp.tsx b/apps/login/src/components/sign-in-with-idp.tsx index 5af5878759..c2fca07cc6 100644 --- a/apps/login/src/components/sign-in-with-idp.tsx +++ b/apps/login/src/components/sign-in-with-idp.tsx @@ -1,13 +1,12 @@ "use client"; import { idpTypeToSlug } from "@/lib/idp"; -import { startIDPFlow } from "@/lib/server/idp"; +import { redirectToIdp } from "@/lib/server/idp"; import { IdentityProvider, IdentityProviderType, } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; -import { useRouter } from "next/navigation"; -import { ReactNode, useCallback, useState } from "react"; +import { ReactNode, useActionState } from "react"; import { Alert } from "./alert"; import { SignInWithIdentityProviderProps } from "./idps/base-button"; import { SignInWithApple } from "./idps/sign-in-with-apple"; @@ -31,45 +30,10 @@ export function SignInWithIdp({ organization, linkOnly, }: Readonly) { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const router = useRouter(); + const [state, action, _isPending] = useActionState(redirectToIdp, {}); - const startFlow = useCallback( - async (idpId: string, provider: string) => { - setLoading(true); - const params = new URLSearchParams(); - if (linkOnly) params.set("link", "true"); - if (requestId) params.set("requestId", requestId); - if (organization) params.set("organization", organization); - - try { - const response = await startIDPFlow({ - idpId, - successUrl: `/idp/${provider}/success?` + params.toString(), - failureUrl: `/idp/${provider}/failure?` + params.toString(), - }); - - if (response && "error" in response && response?.error) { - setError(response.error); - return; - } - - if (response && "redirect" in response && response?.redirect) { - return router.push(response.redirect); - } - } catch { - setError("Could not start IDP flow"); - } finally { - setLoading(false); - } - }, - [requestId, organization, linkOnly, router], - ); - - const renderIDPButton = (idp: IdentityProvider) => { + const renderIDPButton = (idp: IdentityProvider, index: number) => { const { id, name, type } = idp; - const onClick = () => startFlow(id, idpTypeToSlug(type)); const components: Partial< Record< @@ -92,16 +56,27 @@ export function SignInWithIdp({ const Component = components[type]; return Component ? ( - +
+ + + + + + + ) : null; }; return (
{identityProviders?.map(renderIDPButton)} - {error && ( + {state?.error && (
- {error} + {state?.error}
)}
diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts index aa38a63f27..e6861a60c4 100644 --- a/apps/login/src/lib/server/idp.ts +++ b/apps/login/src/lib/server/idp.ts @@ -6,11 +6,45 @@ import { startIdentityProviderFlow, } from "@/lib/zitadel"; import { headers } from "next/headers"; +import { redirect } from "next/navigation"; import { getNextUrl } from "../client"; import { getServiceUrlFromHeaders } from "../service"; import { checkEmailVerification } from "../verify-helper"; import { createSessionForIdpAndUpdateCookie } from "./cookie"; +export type RedirectToIdpState = { error?: string | null } | undefined; + +export async function redirectToIdp( + prevState: RedirectToIdpState, + formData: FormData, +): Promise { + const params = new URLSearchParams(); + + const linkOnly = formData.get("linkOnly") === "true"; + const requestId = formData.get("requestId") as string; + const organization = formData.get("organization") as string; + const idpId = formData.get("id") as string; + const provider = formData.get("provider") as string; + + if (linkOnly) params.set("link", "true"); + if (requestId) params.set("requestId", requestId); + if (organization) params.set("organization", organization); + + const response = await startIDPFlow({ + idpId, + successUrl: `/idp/${provider}/success?` + params.toString(), + failureUrl: `/idp/${provider}/failure?` + params.toString(), + }); + + if (response && "error" in response && response?.error) { + return { error: response.error }; + } + + if (response && "redirect" in response && response?.redirect) { + redirect(response.redirect); + } +} + export type StartIDPFlowCommand = { idpId: string; successUrl: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b28520f9e2..c3bcfab38f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: jose: specifier: ^5.3.0 version: 5.8.0 + lucide-react: + specifier: 0.469.0 + version: 0.469.0(react@19.0.0) moment: specifier: ^2.29.4 version: 2.30.1 @@ -1479,9 +1482,6 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.13': - resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} - '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -3339,6 +3339,11 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-react@0.469.0: + resolution: {integrity: sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -5836,7 +5841,7 @@ snapshots: '@react-aria/ssr@3.9.6(react@19.0.0)': dependencies: - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 19.0.0 '@react-aria/utils@3.25.3(react@19.0.0)': @@ -5844,13 +5849,13 @@ snapshots: '@react-aria/ssr': 3.9.6(react@19.0.0) '@react-stately/utils': 3.10.4(react@19.0.0) '@react-types/shared': 3.25.0(react@19.0.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 clsx: 2.1.1 react: 19.0.0 '@react-stately/utils@3.10.4(react@19.0.0)': dependencies: - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.15 react: 19.0.0 '@react-types/shared@3.25.0(react@19.0.0)': @@ -5925,10 +5930,6 @@ snapshots: '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.13': - dependencies: - tslib: 2.8.1 - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -7114,7 +7115,7 @@ snapshots: debug: 4.3.7(supports-color@5.5.0) enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1))(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.0 is-bun-module: 1.1.0 @@ -7127,7 +7128,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): + eslint-module-utils@2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: @@ -7148,7 +7149,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -8135,6 +8136,10 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-react@0.469.0(react@19.0.0): + dependencies: + react: 19.0.0 + lz-string@1.5.0: {} magic-string@0.30.12: From 9ebb07bc004ec8dcb699139487d944e9973bb748 Mon Sep 17 00:00:00 2001 From: Fuchsoria Date: Mon, 7 Apr 2025 23:27:23 +0200 Subject: [PATCH 2/2] Add russian language --- apps/login/locales/ru.json | 196 +++++++++++++++++++++++++++++++++++++ apps/login/src/lib/i18n.ts | 4 + 2 files changed, 200 insertions(+) create mode 100644 apps/login/locales/ru.json diff --git a/apps/login/locales/ru.json b/apps/login/locales/ru.json new file mode 100644 index 0000000000..c52047897a --- /dev/null +++ b/apps/login/locales/ru.json @@ -0,0 +1,196 @@ +{ + "common": { + "back": "Назад" + }, + "accounts": { + "title": "Аккаунты", + "description": "Выберите аккаунт, который хотите использовать.", + "addAnother": "Добавить другой аккаунт", + "noResults": "Аккаунты не найдены" + }, + "loginname": { + "title": "С возвращением!", + "description": "Введите свои данные для входа.", + "register": "Зарегистрировать нового пользователя" + }, + "password": { + "verify": { + "title": "Пароль", + "description": "Введите ваш пароль.", + "resetPassword": "Сбросить пароль", + "submit": "Продолжить" + }, + "set": { + "title": "Установить пароль", + "description": "Установите пароль для вашего аккаунта", + "codeSent": "Код отправлен на ваш адрес электронной почты.", + "noCodeReceived": "Не получили код?", + "resend": "Отправить код повторно", + "submit": "Продолжить" + }, + "change": { + "title": "Изменить пароль", + "description": "Установите пароль для вашего аккаунта", + "submit": "Продолжить" + } + }, + "idp": { + "title": "Войти через SSO", + "description": "Выберите одного из провайдеров для входа", + "signInWithApple": "Войти через Apple", + "signInWithGoogle": "Войти через Google", + "signInWithAzureAD": "Войти через AzureAD", + "signInWithGithub": "Войти через GitHub", + "signInWithGitlab": "Войти через GitLab", + "loginSuccess": { + "title": "Вход выполнен успешно", + "description": "Вы успешно вошли в систему!" + }, + "linkingSuccess": { + "title": "Аккаунт привязан", + "description": "Аккаунт успешно привязан!" + }, + "registerSuccess": { + "title": "Регистрация завершена", + "description": "Вы успешно зарегистрировались!" + }, + "loginError": { + "title": "Ошибка входа", + "description": "Произошла ошибка при попытке входа." + }, + "linkingError": { + "title": "Ошибка привязки аккаунта", + "description": "Произошла ошибка при попытке привязать аккаунт." + } + }, + "mfa": { + "verify": { + "title": "Подтвердите вашу личность", + "description": "Выберите один из следующих факторов.", + "noResults": "Нет доступных методов двухфакторной аутентификации" + }, + "set": { + "title": "Настройка двухфакторной аутентификации", + "description": "Выберите один из следующих методов.", + "skip": "Пропустить" + } + }, + "otp": { + "verify": { + "title": "Подтверждение 2FA", + "totpDescription": "Введите код из приложения-аутентификатора.", + "smsDescription": "Введите код, полученный по SMS.", + "emailDescription": "Введите код, полученный по email.", + "noCodeReceived": "Не получили код?", + "resendCode": "Отправить код повторно", + "submit": "Продолжить" + }, + "set": { + "title": "Настройка двухфакторной аутентификации", + "totpDescription": "Отсканируйте QR-код в приложении-аутентификаторе.", + "smsDescription": "Введите номер телефона для получения кода по SMS.", + "emailDescription": "Введите email для получения кода.", + "totpRegisterDescription": "Отсканируйте QR-код или перейдите по ссылке вручную.", + "submit": "Продолжить" + } + }, + "passkey": { + "verify": { + "title": "Аутентификация с помощью пасскей", + "description": "Устройство запросит отпечаток пальца, лицо или экранный замок", + "usePassword": "Использовать пароль", + "submit": "Продолжить" + }, + "set": { + "title": "Настройка пасскей", + "description": "Устройство запросит отпечаток пальца, лицо или экранный замок", + "info": { + "description": "Пасскей — метод аутентификации через устройство (отпечаток пальца, Apple FaceID и аналоги).", + "link": "Аутентификация без пароля" + }, + "skip": "Пропустить", + "submit": "Продолжить" + } + }, + "u2f": { + "verify": { + "title": "Подтверждение 2FA", + "description": "Подтвердите аккаунт с помощью устройства." + }, + "set": { + "title": "Настройка двухфакторной аутентификации", + "description": "Настройте устройство как второй фактор.", + "submit": "Продолжить" + } + }, + "register": { + "methods": { + "passkey": "Пасскей", + "password": "Пароль" + }, + "disabled": { + "title": "Регистрация отключена", + "description": "Регистрация недоступна. Обратитесь к администратору." + }, + "missingdata": { + "title": "Недостаточно данных", + "description": "Укажите email, имя и фамилию для регистрации." + }, + "title": "Регистрация", + "description": "Создайте свой аккаунт ZITADEL.", + "selectMethod": "Выберите метод аутентификации", + "agreeTo": "Для регистрации необходимо принять условия:", + "termsOfService": "Условия использования", + "privacyPolicy": "Политика конфиденциальности", + "submit": "Продолжить", + "password": { + "title": "Установить пароль", + "description": "Установите пароль для вашего аккаунта", + "submit": "Продолжить" + } + }, + "invite": { + "title": "Пригласить пользователя", + "description": "Укажите email и имя пользователя для приглашения.", + "info": "Пользователь получит email с инструкциями.", + "notAllowed": "Ваши настройки не позволяют приглашать пользователей.", + "submit": "Продолжить", + "success": { + "title": "Пользователь приглашён", + "description": "Письмо успешно отправлено.", + "verified": "Пользователь приглашён и уже подтвердил email.", + "notVerifiedYet": "Пользователь приглашён. Он получит email с инструкциями.", + "submit": "Пригласить другого пользователя" + } + }, + "signedin": { + "title": "Добро пожаловать, {user}!", + "description": "Вы вошли в систему.", + "continue": "Продолжить" + }, + "verify": { + "userIdMissing": "Не указан userId!", + "success": "Пользователь успешно подтверждён.", + "setupAuthenticator": "Настроить аутентификатор", + "verify": { + "title": "Подтверждение пользователя", + "description": "Введите код из письма подтверждения.", + "noCodeReceived": "Не получили код?", + "resendCode": "Отправить код повторно", + "submit": "Продолжить" + } + }, + "authenticator": { + "title": "Выбор метода аутентификации", + "description": "Выберите предпочитаемый метод аутентификации", + "noMethodsAvailable": "Нет доступных методов аутентификации", + "allSetup": "Аутентификатор уже настроен!", + "linkWithIDP": "или привязать через Identity Provider" + }, + "error": { + "unknownContext": "Не удалось получить контекст пользователя. Укажите имя пользователя или loginName в параметрах поиска.", + "sessionExpired": "Ваша сессия истекла. Войдите снова.", + "failedLoading": "Ошибка загрузки данных. Попробуйте ещё раз.", + "tryagain": "Попробовать снова" + } +} diff --git a/apps/login/src/lib/i18n.ts b/apps/login/src/lib/i18n.ts index b97770e953..5a101dcc8f 100644 --- a/apps/login/src/lib/i18n.ts +++ b/apps/login/src/lib/i18n.ts @@ -28,6 +28,10 @@ export const LANGS: Lang[] = [ name: "简体中文", code: "zh", }, + { + name: "Русский", + code: "ru", + }, ]; export const LANGUAGE_COOKIE_NAME = "NEXT_LOCALE";