From 785ee506ade04ae10343e637967cdb2c3caf8303 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 7 Feb 2023 21:07:05 +0800 Subject: [PATCH 01/34] chore(console): replace `as` with `satisfies` (#3067) --- packages/console/src/pages/ApiResourceDetails/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/console/src/pages/ApiResourceDetails/index.tsx b/packages/console/src/pages/ApiResourceDetails/index.tsx index aa489f376..e125296ae 100644 --- a/packages/console/src/pages/ApiResourceDetails/index.tsx +++ b/packages/console/src/pages/ApiResourceDetails/index.tsx @@ -26,7 +26,7 @@ import { useTheme } from '@/hooks/use-theme'; import * as detailsStyles from '@/scss/details.module.scss'; import * as styles from './index.module.scss'; -import type { ApiResourceDetailsOutletContext } from './types'; +import { ApiResourceDetailsOutletContext } from './types'; const ApiResourceDetails = () => { const { pathname } = useLocation(); @@ -132,7 +132,6 @@ const ApiResourceDetails = () => { { onResourceUpdated: (resource: Resource) => { void mutate(resource); }, - } as ApiResourceDetailsOutletContext + } satisfies ApiResourceDetailsOutletContext } /> From af81c81a53b2e6a7e8993fd2b13665f4bc2d4373 Mon Sep 17 00:00:00 2001 From: Alanimdeo Date: Wed, 8 Feb 2023 13:46:31 +0900 Subject: [PATCH 02/34] fix(phrases): update translation for Korean (#3064) --- packages/phrases-ui/src/locales/ko.ts | 95 ++++++------ packages/phrases/src/locales/ko/errors.ts | 143 +++++++++--------- .../admin-console/api-resource-details.ts | 32 ++-- .../admin-console/api-resources.ts | 2 +- .../admin-console/application-details.ts | 20 +-- .../translation/admin-console/applications.ts | 12 +- .../admin-console/connector-details.ts | 18 +-- .../translation/admin-console/connectors.ts | 30 ++-- .../ko/translation/admin-console/contact.ts | 8 +- .../ko/translation/admin-console/errors.ts | 8 +- .../ko/translation/admin-console/general.ts | 4 +- .../translation/admin-console/get-started.ts | 16 +- .../translation/admin-console/log-details.ts | 2 +- .../ko/translation/admin-console/logs.ts | 2 +- .../translation/admin-console/permissions.ts | 10 +- .../translation/admin-console/role-details.ts | 73 +++++---- .../ko/translation/admin-console/roles.ts | 28 ++-- .../admin-console/session-expired.ts | 2 +- .../ko/translation/admin-console/settings.ts | 10 +- .../translation/admin-console/sign-in-exp.ts | 44 +++--- .../translation/admin-console/tab-sections.ts | 2 +- .../ko/translation/admin-console/tabs.ts | 2 +- .../translation/admin-console/user-details.ts | 34 ++--- .../ko/translation/admin-console/users.ts | 6 +- .../ko/translation/admin-console/welcome.ts | 2 +- 25 files changed, 301 insertions(+), 304 deletions(-) diff --git a/packages/phrases-ui/src/locales/ko.ts b/packages/phrases-ui/src/locales/ko.ts index be88b1d9c..fc8268398 100644 --- a/packages/phrases-ui/src/locales/ko.ts +++ b/packages/phrases-ui/src/locales/ko.ts @@ -24,7 +24,7 @@ const translation = { cancel: '취소', save_password: '저장', bind: '{{address}}로 연동', - bind_and_continue: 'Link and continue', // UNTRANSLATED + bind_and_continue: '연동하고 계속하기', back: '뒤로 가기', nav_back: '뒤로', agree: '동의', @@ -34,11 +34,11 @@ const translation = { switch_to: '{{method}}로 전환', sign_in_via_passcode: '인증번호로 로그인', sign_in_via_password: '비밀번호로 로그인', - change: 'Change {{change}}', // UNTRANSLATED, - link_another_email: 'Link another email', // UNTRANSLATED - link_another_phone: 'Link another phone', // UNTRANSLATED - link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED - show_password: 'Show password', // UNTRANSLATED + change: 'Change {{change}}', + link_another_email: '다른 이메일 연동', + link_another_phone: '다른 전화번호 연동', + link_another_email_or_phone: '다른 이메일 또는 전화번호 연동', + show_password: '비밀번호 보기', }, description: { email: '이메일', @@ -51,23 +51,22 @@ const translation = { create_account: '계정 생성', or: '또는', enter_passcode: '{{address}} {{target}} 으로 비밀번호가 전송되었어요.', - passcode_sent: '비밀번호가 재전송 되었습니다.', + passcode_sent: '비밀번호가 재전송되었어요.', resend_after_seconds: '{{seconds}} 초 후에 재전송', resend_passcode: '비밀번호 재전송', create_account_id_exists: '{{type}} {{value}} 계정이 이미 존재해요. 로그인하시겠어요?', - link_account_id_exists: - 'The account with {{type}} {{value}} already exists, would you like to link?', // UNTRANSLATED + link_account_id_exists: '{{type}} {{value}}와/과 연동된 계정이 이미 존재해요. 연동할까요?', sign_in_id_does_not_exist: '{type}} {{value}} 계정이 존재하지 않아요. 새로 만드시겠어요?', sign_in_id_does_not_exist_alert: '{{type}} {{value}} 계정이 존재하지 않아요.', create_account_id_exists_alert: '{{type}} {{value}} 이미 존재해요.', social_identity_exist: - 'The {{type}} {{value}} is linked to another account. Please try another {{type}}', // UNTRANSLATED - bind_account_title: 'Link or create account', // UNTRANSLATED - social_create_account: '계정이 없으신가요? 새로운 계정을 만들고 연동해보세요.', - social_link_email: 'You can link another email', // UNTRANSLATED, - social_link_phone: 'You can link another phone', // UNTRANSLATED, - social_link_email_or_phone: 'You can link another email or phone', // UNTRANSLATED, - social_bind_with_existing: '관련된 계정을 찾았어요. 해당 계정과 연동할 수 있습니다.', + '{{type}} {{value}}이/가 다른 계정과 연동되어 있어요. 다른 {{type}}을/를 시도해 보세요.', + bind_account_title: '계정 만들거나 연동하기', + social_create_account: '계정이 없으신가요? 새로운 계정을 만들고 연동해 보세요.', + social_link_email: '다른 이메일을 연동할 수 있어요', + social_link_phone: '다른 휴대전화를 연동할 수 있어요', + social_link_email_or_phone: '다른 이메일이나 휴대전화를 연동할 수 있어요', + social_bind_with_existing: '관련된 계정을 찾았어요. 해당 계정과 연동할 수 있어요.', reset_password: '암호를 재설정', reset_password_description_email: '계정과 연결된 이메일 주소를 입력하면 비밀번호 재설정을 위한 인증 코드를 이메일로 보내드립니다.', @@ -86,58 +85,58 @@ const translation = { link_email: '이메일 연동', link_phone: '휴대전화번호 연동', link_email_or_phone: '이메일 또는 휴대전화번호 연동', - link_email_description: '더 나은 보안을 위해 이메일을 연동해주세요.', - link_phone_description: '더 나은 보안을 위해 휴대전화번호를 연동해주세요.', - link_email_or_phone_description: '더 나은 보안을 위해 이메일 또는 휴대전화번호를 연동해주세요.', - continue_with_more_information: '더 나은 보안을 위해 아래 자세한 내용을 따라주세요.', + link_email_description: '더 나은 보안을 위해 이메일을 연동해 주세요.', + link_phone_description: '더 나은 보안을 위해 휴대전화번호를 연동해 주세요.', + link_email_or_phone_description: + '더 나은 보안을 위해 이메일 또는 휴대전화번호를 연동해 주세요.', + continue_with_more_information: '더 나은 보안을 위해 아래 자세한 내용을 따라 주세요.', }, profile: { - title: 'Account Settings', // UNTRANSLATED - description: - 'Change your account settings and manage your personal information here to ensure your account security.', // UNTRANSLATED + title: '계정 설정', + description: '계정 보안을 지키기 위해 여기서 계정 설정을 변경하고 개인 정보를 관리하세요.', settings: { - title: 'PROFILE SETTINGS', // UNTRANSLATED - profile_information: 'Profile Information', // UNTRANSLATED - avatar: 'Avatar', // UNTRANSLATED - name: 'Name', // UNTRANSLATED - username: 'Username', // UNTRANSLATED + title: '프로필 설정', + profile_information: '프로필 정보', + avatar: '아바타', + name: '이름', + username: '사용자명', }, password: { - title: 'PASSWORD', // UNTRANSLATED - reset_password: 'Reset Password', // UNTRANSLATED - reset_password_sc: 'Reset password', // UNTRANSLATED + title: '비밀번호', + reset_password: '비밀번호 초기화', + reset_password_sc: '비밀번호 초기화', }, link_account: { - title: 'LINK ACCOUNT', // UNTRANSLATED - email_phone_sign_in: 'Email / Phone Sign-In', // UNTRANSLATED - email: 'Email', // UNTRANSLATED - phone: 'Phone', // UNTRANSLATED - phone_sc: 'Phone number', // UNTRANSLATED - social: 'Social Sign-In', // UNTRANSLATED - social_sc: 'Social accounts', // UNTRANSLATED + title: '계정 연동', + email_phone_sign_in: '이메일/휴대전화 로그인', + email: '이메일', + phone: '휴대전화', + phone_sc: '휴대전화번호', + social: '소셜 로그인', + social_sc: '소셜 계정', }, - not_set: 'Not set', // UNTRANSLATED - edit: 'Edit', // UNTRANSLATED - change: 'Change', // UNTRANSLATED - link: 'Link', // UNTRANSLATED - unlink: 'Unlink', // UNTRANSLATED + not_set: '설정 안 됨', + edit: '수정', + change: '변경', + link: '연동', + unlink: '연동 해제', }, error: { username_password_mismatch: '사용자 이름 또는 비밀번호가 일치하지 않아요.', username_required: '사용자 이름은 필수예요.', password_required: '비밀번호는 필수예요.', username_exists: '사용자 이름이 이미 존재해요.', - username_should_not_start_with_number: '사용자 이름은 숫자로 시작하면 안되요.', - username_valid_charset: '사용자 이름은 문자, 숫자, _(밑줄 문자) 로만 이루어져야해요.', + username_should_not_start_with_number: '사용자 이름은 숫자로 시작하면 안 돼요.', + username_valid_charset: '사용자 이름은 문자, 숫자, _(밑줄 문자) 로만 이루어져야 해요.', invalid_email: '이메일이 유효하지 않아요.', invalid_phone: '휴대전화번호가 유효하지 않아요.', - password_min_length: '비밀번호는 최소 {{min}} 자리로 이루어져야해요.', + password_min_length: '비밀번호는 최소 {{min}} 자리로 이루어져야 해요.', passwords_do_not_match: '비밀번호가 일치하지 않아요.', invalid_passcode: '비밀번호가 유효하지 않아요.', invalid_connector_auth: '인증이 유효하지 않아요.', invalid_connector_request: '연동 정보가 유효하지 않아요.', - unknown: '알 수 없는 오류가 발생했어요. 잠시 후에 시도해주세요.', - invalid_session: '세션을 찾을 수 없어요. 다시 로그인을 해주세요.', + unknown: '알 수 없는 오류가 발생했어요. 잠시 후에 시도해 주세요.', + invalid_session: '세션을 찾을 수 없어요. 다시 로그인해 주세요.', }, }; diff --git a/packages/phrases/src/locales/ko/errors.ts b/packages/phrases/src/locales/ko/errors.ts index 66dd5dbb7..0d2ad1157 100644 --- a/packages/phrases/src/locales/ko/errors.ts +++ b/packages/phrases/src/locales/ko/errors.ts @@ -6,12 +6,12 @@ const errors = { auth: { authorization_header_missing: '인증 헤더가 존재하지 않아요.', authorization_token_type_not_supported: '해당 인증 방법을 지원하지 않아요.', - unauthorized: '인증되지 않았어요. 로그인 정보와 범위를 확인해주세요.', - forbidden: '접근이 금지되었어요. 로그인 권한와 직책을 확인해주세요.', + unauthorized: '인증되지 않았어요. 로그인 정보와 범위를 확인해 주세요.', + forbidden: '접근이 금지되었어요. 로그인 권한과 역할을 확인해 주세요.', expected_role_not_found: - '예상되는 직책을 찾을 수 없어요. 해당 사용자의 권한 또는 직책을 확인해주세요.', + '예상되는 역할을 찾을 수 없어요. 해당 사용자의 권한 또는 역할을 확인해 주세요.', jwt_sub_missing: 'JWT에서 `sub`를 찾을 수 없어요.', - require_re_authentication: '보호된 작업을 수행하려면 재인증이 필요합니다.', + require_re_authentication: '보호된 작업을 수행하려면 재인증이 필요해요.', }, guard: { invalid_input: '{{type}} 요청 타입은 유효하지 않아요.', @@ -21,14 +21,14 @@ const errors = { aborted: 'End 사용자가 상호 작용을 중단했어요.', invalid_scope: '{{scope}} 범위를 지원하지 않아요.', invalid_scope_plural: '{{scopes}} 범위들을 지원하지 않아요.', - invalid_token: '유요하지 않은 토큰이 제공되었어요.', + invalid_token: '유효하지 않은 토큰이 제공되었어요.', invalid_client_metadata: '유효하지 않은 클라이언트 메타데이터가 제공되었어요.', insufficient_scope: '요청된 {{scopes}} 범위에서 Access 토큰을 찾을 수 없어요.', invalid_request: '요청이 유효하지 않아요.', invalid_grant: '승인 요청이 유효하지 않아요.', invalid_redirect_uri: '`redirect_uri`가 등록된 클라이언트의 `redirect_uris`와 일치하지 않아요.', access_denied: '접근이 금지되었어요.', - invalid_target: '유요하지 않은 리소스 표시에요..', + invalid_target: '유효하지 않은 리소스 표시예요.', unsupported_grant_type: '지원하지 않는 `grant_type` 요청이에요.', unsupported_response_mode: '지원하지 않는 `response_mode` 요청이에요.', unsupported_response_type: '지원하지 않은 `response_type` 요청이에요.', @@ -38,60 +38,61 @@ const errors = { username_already_in_use: '이 사용자 이름은 다른 사람이 이미 사용 중이에요.', email_already_in_use: '이 이메일은 다른 계정에서 이미 사용 중이에요.', phone_already_in_use: '이 휴대전화번호는 다른 계정에서 이미 사용 중이에요.', - invalid_email: '유효하지 않은 이메일이예요.', - invalid_phone: '유효하지 않은 휴대전화번호에요', + invalid_email: '유효하지 않은 이메일이에요.', + invalid_phone: '유효하지 않은 휴대전화번호예요.', email_not_exist: '이메일 주소가 아직 등록되지 않았어요.', phone_not_exist: '휴대전화번호가 아직 등록되지 않았어요.', identity_not_exist: '소셜 계정이 아직 등록되지 않았어요.', - identity_already_in_use: '소셜 계정이 이미 등록되있어요.', + identity_already_in_use: '소셜 계정이 이미 등록되어 있어요.', cannot_delete_self: '자기 자신을 삭제할 수 없어요.', - sign_up_method_not_enabled: '이 회원가입 방법은 활성화 되어있지 않아요.', - sign_in_method_not_enabled: '이 로그인 방법은 활성화 되어있지 않아요.', - same_password: '새로운 비밀번호는 이전 비밀번호와 같으면 안되요.', - password_required_in_profile: '로그인 전에 비밀번호를 설정해야해요.', - new_password_required_in_profile: '새로운 비밀번호를 설정해야해요.', - password_exists_in_profile: '이미 비밀번호가 설정되어있어요.', - username_required_in_profile: '로그인 전에 사용자 이름을 설정해야해요.', - username_exists_in_profile: '이미 사용자 이름이 설정되어있어요.', - email_required_in_profile: '로그인 전에 이메일 주소를 설정해야해요.', - email_exists_in_profile: '이미 이메일 주소가 설정되어있어요.', - phone_required_in_profile: '로그인 전에 휴대전화번호를 설정해야해요.', - phone_exists_in_profile: '이미 휴대전화번호가 설정되어있어요.', - email_or_phone_required_in_profile: '로그인 전에 이메일 주소 또는 휴대전화번호를 설정해야해요.', - suspended: '이 계정은 일시 정시되었어요.', + sign_up_method_not_enabled: '이 회원가입 방법은 활성화되어있지 않아요.', + sign_in_method_not_enabled: '이 로그인 방법은 활성화되어있지 않아요.', + same_password: '새로운 비밀번호는 이전 비밀번호와 같으면 안 돼요.', + password_required_in_profile: '로그인 전에 비밀번호를 설정해야 해요.', + new_password_required_in_profile: '새로운 비밀번호를 설정해야 해요.', + password_exists_in_profile: '이미 비밀번호가 설정되어 있어요.', + username_required_in_profile: '로그인 전에 사용자 이름을 설정해야 해요.', + username_exists_in_profile: '이미 사용자 이름이 설정되어 있어요.', + email_required_in_profile: '로그인 전에 이메일 주소를 설정해야 해요.', + email_exists_in_profile: '이미 이메일 주소가 설정되어 있어요.', + phone_required_in_profile: '로그인 전에 휴대전화번호를 설정해야 해요.', + phone_exists_in_profile: '이미 휴대전화번호가 설정되어 있어요.', + email_or_phone_required_in_profile: + '로그인 전에 이메일 주소 또는 휴대전화번호를 설정해야 해요.', + suspended: '이 계정은 일시 정지되었어요.', user_not_exist: '{{identifier}}의 사용자가 아직 등록되지 않았어요.', - missing_profile: '로그인 전에 추가 정보를 제공해야해요.', - role_exists: 'The role id {{roleId}} is already been added to this user', // UNTRANSLATED + missing_profile: '로그인 전에 추가 정보를 제공해야 해요.', + role_exists: '역할 ID {{roleId}}은/는 이미 이 사용자에게 할당되어 있어요.', }, password: { unsupported_encryption_method: '{{name}} 암호화 방법을 지원하지 않아요.', - pepper_not_found: '비밀번호 Pepper를 찾을 수 없어요. Core 환경설정을 확인해주세요.', + pepper_not_found: '비밀번호 Pepper를 찾을 수 없어요. Core 환경설정을 확인해 주세요.', }, session: { - not_found: '세션을 찾을 수 없어요. 다시 로그인해주세요.', - invalid_credentials: '유효하지 않은 로그인 정보예요. 입력된 값을 다시 확인해주세요.', + not_found: '세션을 찾을 수 없어요. 다시 로그인해 주세요.', + invalid_credentials: '유효하지 않은 로그인 정보예요. 입력된 값을 다시 확인해 주세요.', invalid_sign_in_method: '현재 로그인 방법을 지원하지 않아요.', - invalid_connector_id: '소셜 ID {{connectorId}}를 찾을 수 없어요..', + invalid_connector_id: '소셜 ID {{connectorId}}를 찾을 수 없어요.', insufficient_info: '로그인 정보가 충분하지 않아요.', connector_id_mismatch: '연동 ID가 세션 정보와 일치하지 않아요.', - connector_session_not_found: '연동 세션을 찾을 수 없어요. 다시 로그인해주세요.', + connector_session_not_found: '연동 세션을 찾을 수 없어요. 다시 로그인해 주세요.', verification_session_not_found: - '검증을 실패했어요. 검증 과정을 다시 시작하고 다시 시도해주세요.', + '검증을 실패했어요. 검증 과정을 다시 시작하고 다시 시도해 주세요.', verification_expired: - '연결 시간이 초과되었어요. 검증을 다시 시작하고, 계정이 안전한지 확인해주세요.', - unauthorized: '로그인을 먼저 해주세요.', - unsupported_prompt_name: '지원하지 않는 Prompt 이름이예요.', - forgot_password_not_enabled: '비밀번호 찾기가 활성화 되어있지 않아요.', + '연결 시간이 초과되었어요. 검증을 다시 시작하고, 계정이 안전한지 확인해 주세요.', + unauthorized: '로그인을 먼저 해 주세요.', + unsupported_prompt_name: '지원하지 않는 Prompt 이름이에요.', + forgot_password_not_enabled: '비밀번호 찾기가 활성화되어있지 않아요.', verification_failed: - '인증이 성공적으로 완료되지 않았어요. 처음부터 다시 인증 과정을 거쳐주세요.', + '인증이 성공적으로 완료되지 않았어요. 처음부터 다시 인증 과정을 거쳐 주세요.', connector_validation_session_not_found: '연동 세션 유효성 검증을 위한 토큰을 찾을 수 없어요.', - identifier_not_found: '사용자 식별자를 찾을 수 없어요. 처음부터 다시 로그인을 시도해주세요.', - interaction_not_found: '인터렉션 세션을 찾을 수 없어요. 처음부터 다시 세션을 시작해주세요.', + identifier_not_found: '사용자 식별자를 찾을 수 없어요. 처음부터 다시 로그인을 시도해 주세요.', + interaction_not_found: '인터렉션 세션을 찾을 수 없어요. 처음부터 다시 세션을 시작해 주세요.', }, connector: { general: '연동 중에 알 수 없는 오류가 발생했어요. {{errorDescription}}', not_found: '{{type}} 값을 가진 연동 종류를 찾을 수 없어요.', - not_enabled: '연동이 활성화 되지 않았어요.', + not_enabled: '연동이 활성화되지 않았어요.', invalid_metadata: '연동 메타데이터가 유효하지 않아요.', invalid_config_guard: '연동 설정 데이터가 유효하지 않아요.', unexpected_type: '예상하지 않은 연동 종류에요.', @@ -105,62 +106,60 @@ const errors = { invalid_auth_code: '연동 서비스의 Auth 코드가 유효하지 않아요.', social_invalid_id_token: '연동 서비스의 ID 토큰이 유효하지 않아요.', authorization_failed: '사용자의 인증 과정이 성공적으로 마무리되지 않았어요.', - social_auth_code_invalid: 'Access 토큰을 가져올 수 없어요. Authorization 코드를 확인해주세요.', - more_than_one_sms: '연동된 SMS 서비스가 1개 이상이여야 해요.', - more_than_one_email: '연동된 이메일 서비스가 1개 이상이여야 해요.', + social_auth_code_invalid: 'Access 토큰을 가져올 수 없어요. Authorization 코드를 확인해 주세요.', + more_than_one_sms: 'SMS 서비스는 1개만 연동되어야 해요.', + more_than_one_email: '이메일 서비스는 1개만 연동되어야 해요.', db_connector_type_mismatch: '종류가 일치하지 않은 연동 서비스가 DB에 존재해요.', not_found_with_connector_id: '주어진 연동 ID로 연동 설정을 찾을 수 없어요.', multiple_instances_not_supported: '선택된 연동 기준으로 여러 인스턴스를 생성할 수 없어요.', invalid_type_for_syncing_profile: '소셜 연동만 사용자 프로파일을 동기화 할 수 있어요.', can_not_modify_target: '연동 목표를 수정할 수 없어요.', - should_specify_target: "'목표'를 반드시 지정해야해요.", + should_specify_target: "'목표'를 반드시 지정해야 해요.", multiple_target_with_same_platform: '같은 목표와 플랫폼에 여러 소셜 연동을 가질 수 없어요.', cannot_overwrite_metadata_for_non_standard_connector: '이 연동의 메타데이터를 덮어쓸 수 없어요.', }, verification_code: { - phone_email_empty: 'Both phone and email are empty.', // UNTRANSLATED - not_found: 'Verification code not found. Please send verification code first.', // UNTRANSLATED - phone_mismatch: 'Phone mismatch. Please request a new verification code.', // UNTRANSLATED - email_mismatch: 'Email mismatch. Please request a new verification code.', // UNTRANSLATED - code_mismatch: 'Invalid verification code.', // UNTRANSLATED - expired: 'Verification code has expired. Please request a new verification code.', // UNTRANSLATED - exceed_max_try: - 'Verification code retries limitation exceeded. Please request a new verification code.', // UNTRANSLATED + phone_email_empty: '전화번호와 이메일이 모두 비어 있어요.', + not_found: '인증 코드를 찾을 수 없어요. 인증 코드를 먼저 요청하세요.', + phone_mismatch: '전화번호가 맞지 않아요. 새 인증 코드를 요청해 주세요.', + email_mismatch: '이메일이 맞지 않아요. 새 인증 코드를 요청해 주세요.', + code_mismatch: '인증 코드가 일치하지 않아요.', + expired: '인증 코드가 만료되었어요. 새 인증 코드를 요청해 주세요.', + exceed_max_try: '인증 코드 재시도 한도에 도달했어요. 새 인증 코드를 요청해 주세요.', }, sign_in_experiences: { empty_content_url_of_terms_of_use: - '이용약관 URL이 비어있어요. 이용약관이 활성화되어있다면, 이용약관 URL를 설정해주세요.', - empty_logo: '로고 URL을 입력해주세요.', - empty_slogan: '브랜딩 슬로건이 비어있어요. 슬로건을 사용한다면, 내용을 설정해주세요.', - empty_social_connectors: '연동된 소셜이 없어요. 소셜 로그인을 사용한다면, 연동해주세요.', + '이용 약관 URL이 비어 있어요. 이용 약관이 활성화되어 있다면, 이용 약관 URL를 설정해 주세요.', + empty_logo: '로고 URL을 입력해 주세요.', + empty_slogan: '브랜딩 슬로건이 비어 있어요. 슬로건을 사용한다면, 내용을 설정해 주세요.', + empty_social_connectors: '연동된 소셜이 없어요. 소셜 로그인을 사용한다면, 연동해 주세요.', enabled_connector_not_found: '활성된 {{type}} 연동을 찾을 수 없어요.', not_one_and_only_one_primary_sign_in_method: '반드시 하나의 메인 로그인 방법이 설정되어야 해요. 입력된 값을 확인해주세요.', - username_requires_password: - '회원가입 식별자에 대한 비밀번호 설정을 사용하도록 설정해야 합니다.', + username_requires_password: '회원가입 식별자에 대한 비밀번호 설정을 사용하도록 설정해야 해요.', passwordless_requires_verify: - '이메일/휴대전화번호 가입 식별자에 대해 확인을 사용하도록 설정해야해요.', - miss_sign_up_identifier_in_sign_in: '로그인 방법에는 회원가입 ID가 포함되어야 합니다.', + '이메일/휴대전화번호 가입 식별자에 대해 확인을 사용하도록 설정해야 해요.', + miss_sign_up_identifier_in_sign_in: '로그인 방법에는 회원가입 ID가 포함되어야 해요.', password_sign_in_must_be_enabled: - '회원가입 시 비밀번호를 설정해야 할 경우 비밀번호 로그인을 사용하도록 설정해야 합니다.', + '회원가입 시 비밀번호를 설정해야 할 경우 비밀번호 로그인을 사용하도록 설정해야 해요.', code_sign_in_must_be_enabled: - '비밀번호를 설정할 필요가 없을 때는 인증 코드 로그인을 활성화해야 합니다.', + '비밀번호를 설정할 필요가 없을 때는 인증 코드 로그인을 활성화해야 해요.', unsupported_default_language: '{{language}} 언어는 아직 지원하지 않아요.', at_least_one_authentication_factor: '최소한 하나의 인증 방법을 선택해야 해요.', }, localization: { - cannot_delete_default_language: '{{languageTag}} 언어는 기본 언어이므로 삭제를 할 수 없어요.', + cannot_delete_default_language: '{{languageTag}} 언어는 기본 언어이므로 삭제할 수 없어요.', invalid_translation_structure: - '유효하지 않은 데이터 스키마에요. 입력된 값을 다시 확인해주세요.', + '유효하지 않은 데이터 스키마예요. 입력된 값을 다시 확인해 주세요.', }, swagger: { - invalid_zod_type: '유요하지 않은 Zod 종류에요. Route Guard 설정을 확인해주세요.', + invalid_zod_type: '유효하지 않은 Zod 종류에요. Route Guard 설정을 확인해 주세요.', not_supported_zod_type_for_params: - '지원되지 않는 Zod 종류예요. Route Guard 설정을 확인해주세요.', + '지원되지 않는 Zod 종류예요. Route Guard 설정을 확인해 주세요.', }, entity: { - create_failed: '{{name}} 생성을 실패하였어요..', + create_failed: '{{name}} 생성을 실패하였어요.', not_exists: '{{name}}는 존재하지 않아요.', not_exists_with_id: '{{id}} ID를 가진 {{name}}는 존재하지 않아요.', not_found: '리소스가 존재하지 않아요.', @@ -169,15 +168,15 @@ const errors = { invalid_type: '로그 종류가 유효하지 않아요.', }, role: { - name_in_use: 'This role name {{name}} is already in use', // UNTRANSLATED - scope_exists: 'The scope id {{scopeId}} has already been added to this role', // UNTRANSLATED - user_exists: 'The user id {{userId}} is already been added to this role', // UNTRANSLATED + name_in_use: '역할 이름 {{name}}이/가 이미 사용 중이에요.', + scope_exists: '범위 ID {{scopeId}}이/가 이미 이 역할에 추가되어 있어요.', + user_exists: '사용자 ID {{userId}}이/가 이미 이 역할에 추가되어 있어요.', default_role_missing: - 'Some of the default roleNames does not exist in database, please ensure to create roles first', // UNTRANSLATED + '기본 역할 이름의 일부가 데이터베이스에 존재하지 않아요. 먼저 역할을 생성해 주세요.', }, scope: { - name_exists: 'The scope name {{name}} is already in use', // UNTRANSLATED - name_with_space: 'The name of the scope cannot contain any spaces.', // UNTRANSLATED + name_exists: '범위 이름 {{name}}이/가 이미 사용 중이에요.', + name_with_space: '범위 이름에 공백을 포함할 수 없어요.', }, }; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/api-resource-details.ts b/packages/phrases/src/locales/ko/translation/admin-console/api-resource-details.ts index ef6b456e0..d71825153 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/api-resource-details.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/api-resource-details.ts @@ -1,30 +1,30 @@ const api_resource_details = { back_to_api_resources: 'API 리소스로 돌아가기', - settings_tab: 'Settings', // UNTRANSLATED - permissions_tab: 'Permissions', // UNTRANSLATED + settings_tab: '설정', + permissions_tab: '권한', settings: '설정', settings_description: - 'API 리소스(리소스 표시기) - 요청할 대상 서비스 또는 리소스(일반적으로 리소스의 ID를 나타내는 URI 형식 변수)를 나타냅니다.', + 'API 리소스(리소스 표시기) - 요청할 대상 서비스 또는 리소스(일반적으로 리소스의 ID를 나타내는 URI 형식 변수)를 나타내요.', token_expiration_time_in_seconds: '토큰 만료 시간 (초)', token_expiration_time_in_seconds_placeholder: '토큰 만료 시간을 입력해주세요', delete_description: - '이 행동은 취소될 수 없어요. 해당 API 리소스가 영원히 삭제될거예요. 삭제를 하기 위해 API 리소스 이름 "{{name}}"을 입력해주세요.', + '이 행동은 취소할 수 없어요. 해당 API 리소스가 영원히 삭제될 거예요. 삭제를 하기 위해 API 리소스 이름 "{{name}}"을 입력해주세요.', enter_your_api_resource_name: 'API 리소스 이름을 입력해주세요.', api_resource_deleted: '{name}} API 리소스가 성공적으로 삭제되었어요.', permission: { - create_button: 'Create Permission', // UNTRANSLATED - create_title: 'Create permission', // UNTRANSLATED - create_subtitle: 'Define the permissions (scopes) needed by this API.', // UNTRANSLATED - confirm_create: 'Create permission', // UNTRANSLATED - name: 'Permission name', // UNTRANSLATED - name_placeholder: 'read:resource', // UNTRANSLATED - forbidden_space_in_name: 'The permission name must not contain any spaces.', // UNTRANSLATED - description: 'Description', // UNTRANSLATED - description_placeholder: 'Able to read the resources', // UNTRANSLATED - permission_created: 'The permission {{name}} has been successfully created', // UNTRANSLATED + create_button: '권한 생성', + create_title: '권한 생성', + create_subtitle: '이 API에 필요한 권한을 정의해요.', + confirm_create: '권한 생성', + name: '권한 이름', + name_placeholder: 'read:resource', + forbidden_space_in_name: '권한 이름은 공백을 포함할 수 없어요.', + description: '설명', + description_placeholder: '리소스를 볼 수 있는 권한', + permission_created: '권한 {{name}}이 성공적으로 생성되었어요.', delete_description: - 'If this permission is deleted, the user who had this permission will lose the access granted by it.', // UNTRANSLATED - deleted: 'The permission "{{name}}" was successfully deleted!', // UNTRANSLATED + '이 권한이 삭제되면, 이 권한을 가지고 있던 사용자는 이 권한에 의해 부여받은 접근 권한을 잃게 돼요.', + deleted: '권한 "{{name}}"이 성공적으로 삭제되었어요.', }, }; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/api-resources.ts b/packages/phrases/src/locales/ko/translation/admin-console/api-resources.ts index 8ed1d3de0..f48adbcda 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/api-resources.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/api-resources.ts @@ -6,7 +6,7 @@ const api_resources = { api_name_placeholder: 'API 이름 입력', api_identifier: 'API 식별자', api_identifier_tip: - 'The unique identifier to the API resource. It must be an absolute URI and has no fragment (#) component. Equals to the resource parameter in OAuth 2.0.', // UNTRANSLATED + 'API 리소스에 대한 고유한 식별자예요. 절대 URI여야 하며 조각 (#) 컴포넌트가 없어야 해요. OAuth 2.0의 resource parameter와 같아요.', api_resource_created: '{{name}} API 리소스가 성공적으로 생성되었어요.', api_identifier_placeholder: 'https://your-api-identifier/', }; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/application-details.ts b/packages/phrases/src/locales/ko/translation/admin-console/application-details.ts index 47b19868e..734db85e9 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/application-details.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/application-details.ts @@ -3,48 +3,48 @@ const application_details = { check_guide: '가이드 확인', settings: '설정', settings_description: - '애플리케이션은 Logto for OIDC, 로그인 환경, 감사 로그 등에서 애플리케이션을 식별하는 데 사용됩니다.', + '애플리케이션은 Logto for OIDC, 로그인 환경, 감사 로그 등에서 애플리케이션을 식별하는 데 사용돼요.', advanced_settings: '고급 설정', advanced_settings_description: - '고급 설정에는 OIDC 관련 용어가 포함됩니다. 자세한 내용은 토큰 엔드포인트에서 확인할 수 있습니다.', + '고급 설정에는 OIDC 관련 용어가 포함돼요. 자세한 내용은 토큰 엔드포인트에서 확인할 수 있어요.', application_name: '어플리케이션 이름', application_name_placeholder: '나의 앱', description: '설명', description_placeholder: '어플리케이션 설명을 적어주세요.', authorization_endpoint: '인증 End-Point', authorization_endpoint_tip: - '인증 및 권한 부여를 진행할 End-Point예요. OpenID Connect 인증에서 사용되던 값 이에요.', + '인증 및 권한 부여를 진행할 End-Point예요. OpenID Connect 인증에서 사용되던 값이에요.', application_id: '앱 ID', application_id_tip: - '일반적으로 Logto에서 생성되는 고유한 응용 프로그램 식별자이에요. OpenID Connect에서 "client_id"의 약자이기도 해요.', + '일반적으로 Logto에서 생성되는 고유한 응용 프로그램 식별자예요. OpenID Connect에서 "client_id"의 약자이기도 해요.', application_secret: '앱 시크릿', redirect_uri: 'Redirect URI', redirect_uris: 'Redirect URIs', redirect_uri_placeholder: 'https://your.website.com/app', redirect_uri_placeholder_native: 'io.logto://callback', redirect_uri_tip: - '사용자 로그인 이후, 리다이렉트 될 URI 경로예요. 더욱 자세한 정보는 OpenID Connect AuthRequest를 참고해주세요.', + '사용자 로그인 이후, 리다이렉트될 URI 경로예요. 더욱 자세한 정보는 OpenID Connect AuthRequest를 참고해주세요.', post_sign_out_redirect_uri: '로그아웃 이후 Redirect URI', post_sign_out_redirect_uris: '로그아웃 이후 Redirect URIs', post_sign_out_redirect_uri_placeholder: 'https://your.website.com/home', post_sign_out_redirect_uri_tip: - '로그아웃 이후, 리다이렉트 될 URI 경로예요 (선택). 일부 앱에서는 실제 효과가 없을 수 있어요.', + '로그아웃 이후, 리다이렉트될 URI 경로예요 (선택). 일부 앱에서는 실제 효과가 없을 수 있어요.', cors_allowed_origins: 'CORS Allow Origins', cors_allowed_origins_placeholder: 'https://your.website.com', cors_allowed_origins_tip: - '기본으로 모든 리다이렉트의 오리진들은 허용되요. 대체적으로 이 값을 건들 필요는 없어요. 더욱 자세한 정보는 MDN doc를 확인해주세요.', + '기본으로 모든 리다이렉트의 오리진들은 허용돼요. 대체적으로 이 값을 건들 필요는 없어요. 더욱 자세한 정보는 MDN doc를 확인해주세요.', id_token_expiration: 'ID 토큰 만료', refresh_token_expiration: 'Refresh 토큰 만료', token_endpoint: '토큰 End-Point', user_info_endpoint: '사용자 정보 End-Point', enable_admin_access: '관리자 접근 활성화', enable_admin_access_label: - '관리 API에 대한 접근을 활성화, 비활성화 할 수 있어요. 활성화 한다면, 이 어플리케이션에서 Access 토큰을 통해 관리 API를 사용할 수 있어요.', + '관리 API에 대한 접근을 활성화, 비활성화할 수 있어요. 활성화한다면, 이 어플리케이션에서 Access 토큰을 통해 관리 API를 사용할 수 있어요.', delete_description: '이 행동은 취소될 수 없어요. 어플리케이션을 영원히 삭제할 거에요. 삭제를 진행하기 위해 {{name}} 를 입력해주세요.', - enter_your_application_name: '어플리케이션 이름을 입력해주세요.', + enter_your_application_name: '어플리케이션 이름을 입력해 주세요.', application_deleted: '{{name}} 어플리케이션이 성공적으로 삭제되었어요.', - redirect_uri_required: '반드시 최소 하나의 Redirect URI를 입력해야되요.', + redirect_uri_required: '반드시 최소 하나의 Redirect URI를 입력해야 해요.', }; export default application_details; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/applications.ts b/packages/phrases/src/locales/ko/translation/admin-console/applications.ts index e9e01e06c..9ecd2feac 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/applications.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/applications.ts @@ -1,15 +1,15 @@ const applications = { title: '어플리케이션', - subtitle: '인증에 Logto를 사용할 모바일, 단일 페이지 또는 기존 어플리케이션 설정할 수 있어요.', + subtitle: '인증에 Logto를 사용할 모바일, 단일 페이지 또는 기존 어플리케이션을 설정할 수 있어요.', create: '어플리케이션 생성', application_name: '어플리케이션 이름', application_name_placeholder: '나의 앱', application_description: '어플리케이션 설명', application_description_placeholder: '어플리케이션 설명을 적어주세요.', select_application_type: '어플리케이션 종류 선택', - no_application_type_selected: '어플리케이션 종류를 선택하지 않았아요.', + no_application_type_selected: '어플리케이션 종류를 선택하지 않았어요.', application_created: - '{{name}} 어플리케이션이 성공적으로 생성되었어요! \n이제 어플리케이션 설정을 마무리해주세요.', + '{{name}} 어플리케이션이 성공적으로 생성되었어요! \n이제 어플리케이션 설정을 마무리해 주세요.', app_id: '앱 ID', type: { native: { @@ -36,10 +36,10 @@ const applications = { guide: { get_sample_file: '예제 찾기', header_description: - '단계별 가이드에 따라 어플리케이션을 연동하거나, 오른쪽 버튼을 클릭하여 샘플 프로젝트를 받아보세요.', + '단계별 가이드에 따라 어플리케이션을 연동하거나, 오른쪽 버튼을 클릭하여 샘플 프로젝트를 받아 보세요.', title: '어플리케이션이 생성되었어요.', - subtitle: '앱 설정을 마치기 위해 아래 단계를 따라주세요. SDK 종류를 선택해주세요.', - description_by_sdk: '아래 과정을 따라서 Logto를 {{sdk}} 앱과 빠르게 연동해보세요.', + subtitle: '앱 설정을 마치기 위해 아래 단계를 따라주세요. SDK 종류를 선택해 주세요.', + description_by_sdk: '아래 과정을 따라서 Logto를 {{sdk}} 앱과 빠르게 연동해 보세요.', }, }; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/connector-details.ts b/packages/phrases/src/locales/ko/translation/admin-console/connector-details.ts index f3ce145e9..cabd81c9f 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/connector-details.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/connector-details.ts @@ -3,25 +3,25 @@ const connector_details = { check_readme: 'README 확인', settings: '설정', settings_description: - 'Logto에서 연동은 중요한 역할을 해요. 연동 시스템을 통하여, 사용자들에게 비밀번호가 없이 회원 가입을 하고 로그인을 할 수 있게 하거나, 소셜 계정을 통하여 로그인을 할 수 있게 도와줘요.', - save_error_empty_config: '설정을 입력해주세요.', + 'Logto에서 연동은 중요한 역할을 해요. 연동 시스템을 통하여, 사용자들에게 비밀번호 없이 회원 가입을 하고 로그인을 할 수 있게 하거나, 소셜 계정을 통하여 로그인을 할 수 있게 도와줘요.', + save_error_empty_config: '설정을 입력해 주세요.', send: '보내기', send_error_invalid_format: '유효하지 않은 입력', - edit_config_label: '여기에 JSON을 입력해주세요.', + edit_config_label: '여기에 JSON을 입력해 주세요.', test_email_sender: '이메일 연동 테스트', test_sms_sender: 'SMS 연동 테스트', - test_email_placeholder: '테스트 이메일 주소를 입력해주세요.', - test_sms_placeholder: '테스트 휴대전화번호를 입력해주세요.', - test_message_sent: '테스트 메세지 전송완료!', - test_sender_description: 'JSON 설정이 정확하다면, 메세지를 받을거에요.', + test_email_placeholder: '테스트 이메일 주소를 입력해 주세요.', + test_sms_placeholder: '테스트 휴대전화번호를 입력해 주세요.', + test_message_sent: '테스트 메세지 전송 완료!', + test_sender_description: 'JSON 설정이 정확하다면, 메세지를 받을 거에요.', options_change_email: '이메일 연동 수정', options_change_sms: 'SMS 연동 수정', - connector_deleted: '연동이 설공적으로 제거되었어요.', + connector_deleted: '연동이 성공적으로 제거되었어요.', type_email: '이메일 연동', type_sms: 'SMS 연동', type_social: '소셜 연동', in_use_deletion_description: - '이 연동은 로그인 환경에서 사용 중이에요. 삭제 시 로그인 경험 설정에서 로그인 경험이 삭제되요.', + '이 연동은 로그인 환경에서 사용 중이에요. 삭제 시 로그인 경험 설정에서 로그인 경험이 삭제돼요.', }; export default connector_details; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/connectors.ts b/packages/phrases/src/locales/ko/translation/admin-console/connectors.ts index e1b417523..c7bf6e615 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/connectors.ts @@ -1,9 +1,9 @@ const connectors = { title: '연동', - subtitle: '비밀번호 없이 또는 소셜 로그인을 제공하여 보다 나은 경험을 위해 연동해주세요.', + subtitle: '연동을 통해 비밀번호 없는 로그인이나 소셜 로그인을 제공해 보세요.', create: '소셜 연동 추가', config_sie_notice: - '새로운 연동이 설정되었어요. 다음 링크에서 설정을 마무리해주세요. {{link}}.', + '새로운 연동이 설정되었어요. 다음 링크에서 설정을 마무리해 주세요. {{link}}.', config_sie_link_text: '로그인 경험', tab_email_sms: '이메일/SMS 연동', tab_social: '소셜 연동', @@ -14,8 +14,8 @@ const connectors = { connector_status_not_in_use: '사용 중이 아님', not_in_use_tip: { content: - '사용 중이 아님은 로그인 환경에서 이 로그인 방법을 사용하지 않았음을 의미해요. {{link}} 이 로그인 방법를 추가해주세요.', - go_to_sie: '로그인 경험으로 가기', + '사용 중이 아님은 로그인 환경에서 이 로그인 방법을 사용하지 않았음을 의미해요. {{link}} 이 로그인 방법을 추가해주세요.', + go_to_sie: '로그인 경험으로 가서', }, social_connector_eg: '예) Google, Facebook, Github', save_and_done: '저장 및 완료', @@ -30,28 +30,28 @@ const connectors = { social: '소셜 연동 추가 및 설정', }, guide: { - subtitle: '단계별 가이드를 따라, 연동해주세요.', + subtitle: '단계별 가이드를 따라, 연동해 주세요.', connector_setting: '연동 설정', name: '연동 이름', - name_tip: '다음과 같이 연동 이름이 출력되요. "{{Connector Name}} 계속하기".', + name_tip: '다음과 같이 연동 이름이 출력돼요. "{{name}}으로 계속하기".', logo: '연동 로고 URL', logo_placeholder: 'https://your.cdn.domain/logo.png', - logo_tip: '이 이미지는 연동 버튼에 보여질거에요.', + logo_tip: '이 이미지는 연동 버튼에 보여질 거에요.', logo_dark: '연동 로고 URL (다크 모드)', logo_dark_placeholder: 'https://your.cdn.domain/logo.png', logo_dark_tip: - 'Set your connector’s logo for dark mode after enabling it in the Sign-in Experience of Admin Console.', // UNTRANSLATED + 'Admin Console의 로그인 경험에서 다크 모드를 위한 로고를 활성화한 후 다크 모드용 연동 로고를 설정해 주세요.', logo_dark_collapse: '최소화', - logo_dark_show: 'Show logo setting for dark mode', // UNTRANSLATED + logo_dark_show: '다크 모드를 위한 로고 설정 보이기', target: '연동 ID 대상', target_tip: '연동의 고유 식별자.', target_tooltip: 'Logto의 소셜 연동에서의 "목표"는 소셜 정보의 원천을 뜻해요. Logto의 디자인은 충돌을 피하기 위해서 같은 "목표"를 허용하지 않아요. 연동을 추가한 후에는 값을 변경할 수 없으므로 주의해주세요. 자세히 알아보기', config: '여기에 JSON을 입력', - sync_profile: 'Sync profile information', // UNTRANSLATED - sync_profile_only_at_sign_up: '회원가입때 동기화', - sync_profile_each_sign_in: '로그인 할때 마다 동기화', - sync_profile_tip: 'Sync basic user profile, e.g. name and avatar.', // UNTRANSLATED + sync_profile: '프로필 정보 동기화', + sync_profile_only_at_sign_up: '회원가입할 때 동기화', + sync_profile_each_sign_in: '로그인할 때마다 동기화', + sync_profile_tip: '이름과 아바타와 같은 기본적인 사용자 프로필을 동기화해요.', }, platform: { universal: 'Universal', @@ -60,8 +60,8 @@ const connectors = { }, add_multi_platform: ' 다양한 플랫폼 지원, 플랫폼을 선택해주세요.', drawer_title: '연동 가이드', - drawer_subtitle: '연동하기 위해 가이드를 따라주세요.', - unknown: '알수없는 연동', + drawer_subtitle: '연동하기 위해 가이드를 따라 주세요.', + unknown: '알 수 없는 연동', }; export default connectors; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/contact.ts b/packages/phrases/src/locales/ko/translation/admin-console/contact.ts index b425afc87..7c561daa3 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/contact.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/contact.ts @@ -1,20 +1,20 @@ const contact = { title: '연락처', description: - '커뮤니티에 참여하여 피드백을 제공하고 도움을 요청하며 다른 개발자와 생각을 공유해보세요.', + '커뮤니티에 참여하여 피드백을 제공하고 도움을 요청하며 다른 개발자와 생각을 공유해 보세요.', discord: { title: 'Discord 채널', - description: '공개 채널에 참여하여 다른 개발자와 채팅해보세요.', + description: '공개 채널에 참여하여 다른 개발자와 채팅해 보세요.', button: '참가', }, github: { title: 'GitHub', - description: 'GitHub에서 이슈를 생성해보세요.', + description: 'GitHub에서 이슈를 생성해 보세요.', button: '열기', }, email: { title: '이메일 보내기', - description: '추가 정보 및 도움말을 보려면 이메일을 보내주세요.', + description: '추가 정보 및 도움말을 보려면 이메일을 보내 주세요.', button: '보내기', }, }; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/errors.ts b/packages/phrases/src/locales/ko/translation/admin-console/errors.ts index 7a7596b20..94ecf8ed9 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/errors.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/errors.ts @@ -8,12 +8,12 @@ const errors = { invalid_origin_format: 'URI origin 형식이 유효하지 않음', invalid_json_format: 'JSON 형식이 유효하지 않음', invalid_error_message_format: '오류 메세지 형식이 유효하지 않아요.', - required_field_missing: '{{field}}을/를 입력해주세요.', - required_field_missing_plural: '최소 1개의 {{field}}을/를 입력해야해요.', + required_field_missing: '{{field}}을/를 입력해 주세요.', + required_field_missing_plural: '최소 1개의 {{field}}을/를 입력해야 해요.', more_details: '자세히', username_pattern_error: - '아이디는 반드시 문자, 숫자, _ 만으로 이루어져야 하며, 숫자로 시작하면 안되요.', - password_pattern_error: '비밀번호는 최소 6자리로 이루어져야해요.', + '아이디는 반드시 문자, 숫자, _ 만으로 이루어져야 하며, 숫자로 시작하면 안 돼요.', + password_pattern_error: '비밀번호는 최소 6자리로 이루어져야 해요.', insecure_contexts: '비보안 연결(non-HTTPS)는 지원하지 않아요.', unexpected_error: '알 수 없는 오류가 발생했어요.', }; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/general.ts b/packages/phrases/src/locales/ko/translation/admin-console/general.ts index af4cbcac7..290573b2f 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/general.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/general.ts @@ -41,8 +41,8 @@ const general = { page_info: '{{min, number}}-{{max, number}} / {{total, number}}', learn_more: '더 알아보기', tab_errors: '{{count, number}} 오류', - skip_for_now: 'Skip for now', // UNTRANSLATED - remove: 'Remove', // UNTRANSLATED + skip_for_now: '지금은 건너뛰기', + remove: '삭제', }; export default general; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/get-started.ts b/packages/phrases/src/locales/ko/translation/admin-console/get-started.ts index 342c3768f..5b4bf3a64 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/get-started.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/get-started.ts @@ -2,24 +2,24 @@ const get_started = { progress: '시작 가이드: {{completed}}/{{total}}', progress_dropdown_title: '해야할 것들...', title: 'Logto를 어떻게 시작할까요?', - subtitle_part1: 'Logto의 가치를 얻기 위해 해야할 것들이 있어요.', + subtitle_part1: 'Logto의 가치를 얻기 위해 해야 할 것들이 있어요.', subtitle_part2: '설정을 마칠게요 ', hide_this: '가리기', confirm_message: '정말로 이 페이지를 가릴까요? 이 행동은 취소할 수 없어요.', card1_title: '체험해보기', - card1_subtitle: 'Logto 로그인 경험을 체험해보세요.', - card2_title: '첫 어플리케이션 생성 및 연동해보기', - card2_subtitle: '모바일 앱 및 Single Page, Tranditional 웹에 Logto 인증을 적용해보세요.', + card1_subtitle: 'Logto 로그인 경험을 체험해 보세요.', + card2_title: '첫 어플리케이션 생성 및 연동해 보기', + card2_subtitle: '모바일 앱 및 Single Page, Tranditional 웹에 Logto 인증을 적용해 보세요.', card3_title: '로그인 경험 커스터마이징하기', - card3_subtitle: '로그인 화면을 브랜드에 맞게 커스터마이징 그리고 실시간으로 확인해보세요.', + card3_subtitle: '로그인 화면을 브랜드에 맞게 커스터마이징 그리고 실시간으로 확인해 보세요.', card4_title: 'SMS/이메일 연동하기', card4_subtitle: - 'SMS 또는 이메일을 통해 비밀번호가 없이, 그리고 더욱 안전한 로그인 경험을 사용자에게 제공해보세요.', + 'SMS 또는 이메일을 통해 비밀번호가 없이, 그리고 더욱 안전한 로그인 경험을 사용자에게 제공해 보세요.', card5_title: '소셜 연동', card5_subtitle: - '사용자의 소셜 정보를 통해 한 번의 클릭으로 로그인할 수 있는 경험을 사용자에게 제공해보세요.', + '사용자의 소셜 정보를 통해 한 번의 클릭으로 로그인할 수 있는 경험을 사용자에게 제공해 보세요.', card6_title: '더욱 나아가서', - card6_subtitle: '복잡하지 않은 단계별 시나리오 문서를 확인해보세요.', + card6_subtitle: '복잡하지 않은 단계별 시나리오 문서를 확인해 보세요.', }; export default get_started; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/log-details.ts b/packages/phrases/src/locales/ko/translation/admin-console/log-details.ts index 3f99109b9..687980384 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/log-details.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/log-details.ts @@ -3,7 +3,7 @@ const log_details = { back_to_user: '{{name}}으로 돌아가기', success: '성공', failed: '실패', - event_key: 'Event Key', // UNTRANSLATED + event_key: '이벤트 키', application: '어플리케이션', ip_address: 'IP 주소', user: '사용자', diff --git a/packages/phrases/src/locales/ko/translation/admin-console/logs.ts b/packages/phrases/src/locales/ko/translation/admin-console/logs.ts index 125c57caa..d06c476e8 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/logs.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/logs.ts @@ -1,6 +1,6 @@ const logs = { title: 'Audit 기록', - subtitle: '관리자나 사용자의 인증 기록을 확인해보세요.', + subtitle: '관리자나 사용자의 인증 기록을 확인해 보세요.', event: '활동', user: '사용자', application: '어플리케이션', diff --git a/packages/phrases/src/locales/ko/translation/admin-console/permissions.ts b/packages/phrases/src/locales/ko/translation/admin-console/permissions.ts index 438e7b572..b27194601 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/permissions.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/permissions.ts @@ -1,9 +1,9 @@ const permissions = { - search_placeholder: 'Search by API or permission name', // UNTRANSLATED - search_placeholder_without_api: 'Search by permission name', // UNTRANSLATED - name_column: 'Permission', // UNTRANSLATED - description_column: 'Description', // UNTRANSLATED - api_column: 'API', // UNTRANSLATED + search_placeholder: 'API 또는 권한 이름으로 검색', + search_placeholder_without_api: '권한 이름으로 검색', + name_column: '권한', + description_column: '설명', + api_column: 'API', }; export default permissions; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/role-details.ts b/packages/phrases/src/locales/ko/translation/admin-console/role-details.ts index 9e6d363e6..b8b9ffbf0 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/role-details.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/role-details.ts @@ -1,49 +1,48 @@ const role_details = { - back_to_roles: 'Back to Roles', // UNTRANSLATED - identifier: 'Identifier', // UNTRANSLATED + back_to_roles: '역할로 돌아가기', + identifier: '식별자', delete_description: - 'Doing so will remove the permissions associated with this role from the affected users and delete the mapping between roles, users, and permissions.', // UNTRANSLATED - role_deleted: '{{name}} was successfully deleted!', // UNTRANSLATED - settings_tab: 'Settings', // UNTRANSLATED - users_tab: 'Users', // UNTRANSLATED - permissions_tab: 'Permissions', // UNTRANSLATED - settings: 'Settings', // UNTRANSLATED + '이렇게 하면 영향을 받는 사용자에게서 이 역할과 관련된 권한이 제거되고 역할, 사용자 및 권한 간의 매핑이 삭제될 거예요.', + role_deleted: '{{name}}이 성공적으로 삭제되었어요.', + settings_tab: '설정', + users_tab: '사용자', + permissions_tab: '권한', + settings: '설정', settings_description: - 'Roles are a grouping of permissions that can be assigned to users. They also provide a way to aggregate permissions defined for different APIs, making it more efficient to add, remove, or adjust permissions compared to assigning them individually to users.', // UNTRANSLATED - field_name: 'Name', // UNTRANSLATED - field_description: 'Description', // UNTRANSLATED + '역할은 사용자에게 할당된 권한들의 모음이에요. 역할은 다양한 API에 정의된 권한들을 통합하는 방법을 제공하기 때문에, 사용자에게 개별적으로 할당하는 것보다 효율적으로 권한을 추가, 제거, 조정할 수 있어요.', + field_name: '이름', + field_description: '설명', permission: { - assign_button: 'Assign Permissions', // UNTRANSLATED - assign_title: 'Assign permissions', // UNTRANSLATED + assign_button: '권한 할당', + assign_title: '권한 할당', assign_subtitle: - 'Assign permissions to this role. The role will gain the added permissions, and users with this role will inherit these permissions.', // UNTRANSLATED - assign_form_field: 'Assign permissions', // UNTRANSLATED - added_text_one: '{{count, number}} permission added', // UNTRANSLATED - added_text_other: '{{count, number}} permissions added', // UNTRANSLATED - api_permission_count_one: '{{count, number}} permission', // UNTRANSLATED - api_permission_count_other: '{{count, number}} permissions', // UNTRANSLATED - confirm_assign: 'Assign Permissions', // UNTRANSLATED - permission_assigned: 'The selected permissions were successfully assigned to this role!', // UNTRANSLATED + '이 역할에 권한을 할당해요. 이 역할은 추가된 권한을 할당받고, 이 역할을 가진 이용자들은 이 권한들을 상속받을 거예요.', + assign_form_field: '권한 할당', + added_text_one: '권한 {{count, number}}개 추가됨', + added_text_other: '권한 {{count, number}}개 추가됨', + api_permission_count_one: '권한 {{count, number}}개', + api_permission_count_other: '권한 {{count, number}}개', + confirm_assign: '권한 할당', + permission_assigned: '선택된 권한들이 이 역할에 성공적으로 할당되었어요!', deletion_description: - 'If this permission is removed, the affected user with this role will lose the access granted by this permission.', // UNTRANSLATED - permission_deleted: 'The permission "{{name}}" was successfully removed from this role!', // UNTRANSLATED - empty: 'No permission available', // UNTRANSLATED + '이 권한이 삭제되면, 이 역할에 영향을 받는 사용자가 이 권한에 의해 부여된 접근 권한을 잃게 돼요.', + permission_deleted: '권한 "{{name}}"이 이 역할에서 성공적으로 삭제되었어요!', + empty: '권한 없음', }, users: { - assign_button: 'Assign Users', // UNTRANSLATED - name_column: 'User', // UNTRANSLATED - app_column: 'App', // UNTRANSLATED - latest_sign_in_column: 'Latest sign in', // UNTRANSLATED - delete_description: - 'It will remain in your user pool but lose the authorization for this role.', // UNTRANSLATED - deleted: '{{name}} was successfully removed from this role!', // UNTRANSLATED - assign_title: 'Assign users', // UNTRANSLATED + assign_button: '사용자 할당', + name_column: '사용자', + app_column: '앱', + latest_sign_in_column: '최근 로그인 시각', + delete_description: '사용자는 사용자 목록에 남지만 이 역할에 대한 접근 권한을 잃어버릴 거예요.', + deleted: '{{name}}이 이 역할에서 성공적으로 삭제되었어요!', + assign_title: '사용자 할당', assign_subtitle: - 'Assign users to this role. Find appropriate users by searching name, email, phone, or user ID.', // UNTRANSLATED - assign_users_field: 'Assign users', // UNTRANSLATED - confirm_assign: 'Assign users', // UNTRANSLATED - users_assigned: 'The selected users were successfully assigned to this role!', // UNTRANSLATED - empty: 'No user available', // UNTRANSLATED + '사용자를 이 역할에 할당해요. 이름, 이메일, 전화번호, 사용자 ID 등을 이용하여 적절한 사용자를 찾아 보세요.', + assign_users_field: '사용자 할당', + confirm_assign: '사용자 할당', + users_assigned: '선택된 이용자들이 이 역할에 성공적으로 할당되었어요!', + empty: '사용자 없음', }, }; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/roles.ts b/packages/phrases/src/locales/ko/translation/admin-console/roles.ts index 4dccb7f96..361774bba 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/roles.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/roles.ts @@ -1,20 +1,20 @@ const roles = { - title: 'Roles', // UNTRANSLATED + title: '역할', subtitle: - 'Roles include permissions that determine what a user can do. RBAC uses roles to give users access to resources for specific actions.', // UNTRANSLATED - create: 'Create Role', // UNTRANSLATED - role_name: 'Role', // UNTRANSLATED - role_description: 'Description', // UNTRANSLATED - role_name_placeholder: 'Enter your role name', // UNTRANSLATED - role_description_placeholder: 'Enter your role description', // UNTRANSLATED - assigned_users: 'Assigned users', // UNTRANSLATED - assign_permissions: 'Assign permissions', // UNTRANSLATED - create_role_title: 'Create Role', // UNTRANSLATED + '역할은 사용자가 무엇을 할 수 있는지를 결정하는 권한을 포함해요. RBAC는 사용자에게 특정 행동에 대한 접근 권한을 부여하기 위해 역할을 사용해요.', + create: '역할 생성', + role_name: '역할 이름', + role_description: '설명', + role_name_placeholder: '역할 이름을 입력하세요', + role_description_placeholder: '역할 설명을 입력하세요', + assigned_users: '할당된 사용자', + assign_permissions: '권한 할당', + create_role_title: '역할 생성', create_role_description: - 'Create and manage roles for your applications. Roles contain collections of permissions and can be assigned to users.', // UNTRANSLATED - create_role_button: 'Create Role', // UNTRANSLATED - role_created: 'The role {{name}} has been successfully created!', // UNTRANSLATED - search: 'Search by role name, description or ID', // UNTRANSLATED + '당신의 애플리케이션을 위한 역할을 생성하고 관리해요. 역할은 권한들의 모음을 포함하며 사용자에게 할당될 수 있어요.', + create_role_button: '역할 생성', + role_created: '역할 {{name}}이 성공적으로 생성되었어요!', + search: '역할 이름, 설명, ID로 검색', }; export default roles; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/session-expired.ts b/packages/phrases/src/locales/ko/translation/admin-console/session-expired.ts index 50c9ee278..da6f79c8a 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/session-expired.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/session-expired.ts @@ -1,6 +1,6 @@ const session_expired = { title: '세션 만료', - subtitle: '세션이 만료되었거나, 끊겼어요. 다시 로그인해주세요.', + subtitle: '세션이 만료되었거나, 끊겼어요. 다시 로그인해 주세요.', button: '다시 로그인하기', }; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/settings.ts b/packages/phrases/src/locales/ko/translation/admin-console/settings.ts index 984cf5b69..4cb94f5a8 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/settings.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/settings.ts @@ -1,6 +1,6 @@ const settings = { title: '설정', - description: '전체 설정을 관리해보세요.', + description: '전체 설정을 관리해 보세요.', settings: '설정', custom_domain: '커스텀 도메인', language: '언어', @@ -12,12 +12,12 @@ const settings = { change_password: '비밀번호 변경', change_password_description: '현재 계정의 비밀번호를 변경할 수 있어요.', change_modal_title: '계정 비밀번호 변경', - change_modal_description: '새로 변경된 비밀번호로 로그인해야해요.', + change_modal_description: '새로 변경된 비밀번호로 로그인해야 해요.', new_password: '새로운 비밀번호', - new_password_placeholder: '새로운 비밀번호를 입력해주세요.', + new_password_placeholder: '새로운 비밀번호를 입력해 주세요.', confirm_password: '비밀번호 확인', - confirm_password_placeholder: '비밀번호를 다시 입력해주세요.', - password_changed: '비밀번호 변경되었어요!', + confirm_password_placeholder: '비밀번호를 다시 입력해 주세요.', + password_changed: '비밀번호가 변경되었어요!', }; export default settings; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts index 60dce6269..de50423fe 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts @@ -1,15 +1,15 @@ const sign_in_exp = { title: '로그인 경험', - description: '로그인 화면을 브랜드에 맞게 커스터마이징 그리고 실시간으로 확인해보세요.', + description: '로그인 화면을 브랜드에 맞게 커스터마이징하고 실시간으로 확인해 보세요.', tabs: { branding: '브랜딩', sign_up_and_sign_in: '회원가입/로그인', others: '기타', }, welcome: { - title: '가이드를 따라, 필수 설정을 빠르게 수정해보세요.', + title: '가이드를 따라, 필수 설정을 빠르게 수정해 보세요.', get_started: '시작하기', - apply_remind: '이 계정이 관리하는 모든 앱의 로그인 경험이 수정되는 것을 주의해주세요.', + apply_remind: '이 계정이 관리하는 모든 앱의 로그인 경험이 수정되는 것을 주의해 주세요.', }, color: { title: '색상', @@ -32,7 +32,7 @@ const sign_in_exp = { dark_logo_image_url: '앱 로고 이미지 URL (다크 모드)', dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png', slogan: '슬로건', - slogan_placeholder: 'Unleash your creativity', + slogan_placeholder: '상상력을 펼쳐 보세요', }, sign_up_and_sign_in: { identifiers: '회원가입 ID', @@ -40,7 +40,7 @@ const sign_in_exp = { identifiers_phone: '휴대전화번호', identifiers_username: '사용자 이름', identifiers_email_or_sms: '이메일 주소 또는 휴대전화번호', - identifiers_none: '해당없음', + identifiers_none: '해당 없음', and: '그리고', or: '또는', sign_up: { @@ -52,18 +52,18 @@ const sign_in_exp = { authentication_description: '선택된 모든 작업들은 사용자가 모두 마무리 해야 해요.', set_a_password_option: '비밀번호 생성', verify_at_sign_up_option: '회원가입 인증', - social_only_creation_description: '(이것은 소셜 전용 계정 생성에 적용되요.)', + social_only_creation_description: '(이것은 소셜 전용 계정 생성에 적용돼요.)', }, sign_in: { title: '로그인', sign_in_identifier_and_auth: '로그인을 위한 ID 그리고 인증 설정', description: - '사용자는 주어진 옵션 중에 아무 방법으로 로그인을 할 수 있어요. 주어진 옵션을 드래그하여 조절해주세요.', + '사용자는 주어진 옵션 중에 아무 방법으로 로그인할 수 있어요. 주어진 옵션을 드래그하여 조절해 주세요.', add_sign_in_method: '로그인 방법 추가', password_auth: '비밀번호', verification_code_auth: '인증 코드', auth_swap_tip: '아래 옵션을 변경하여 흐름에 가장 먼저 나타나는 옵션을 설정할 수 있어요.', - require_auth_factor: '반드시 최소 하나의 인증 방법을 선택해야해요.', + require_auth_factor: '반드시 최소 하나의 인증 방법을 선택해야 해요.', }, social_sign_in: { title: '소셜 로그인', @@ -78,14 +78,14 @@ const sign_in_exp = { }, }, tip: { - set_a_password: '사용자 이름에 대한 고유한 암호 집합은 필수에요.', + set_a_password: '사용자 이름에 대한 고유한 암호 집합은 필수예요.', verify_at_sign_up: '현재 확인된 이메일만 지원해요. 유효성 검사가 없는 경우 사용자 기반에 품질이 낮은 전자 메일 주소가 많이 포함되어 있을 수 있어요.', password_auth: - '회원가입 중에 비밀번호를 설정하는 옵션을 사용으로 설정했기 때문에 이 옵션은 필수에요.', + '회원가입 중에 비밀번호를 설정하는 옵션을 사용으로 설정했기 때문에 이 옵션은 필수예요.', verification_code_auth: - '가입 시 인증 코드를 제공하는 옵션만 활성화했기 때문에 이것은 필수에요. 회원가입에서 비밀번호 설정이 허용되면 이 옵션을 취소할 수 있습니다.', - delete_sign_in_method: '{{identifier}}를 필수 ID로 설정했기 때문에 이 옵션은 필수에요.', + '가입 시 인증 코드를 제공하는 옵션만 활성화했기 때문에 이것은 필수예요. 회원가입에서 비밀번호 설정이 허용되면 이 옵션을 취소할 수 있어요.', + delete_sign_in_method: '{{identifier}}를 필수 ID로 설정했기 때문에 이 옵션은 필수예요.', }, }, others: { @@ -99,13 +99,13 @@ const sign_in_exp = { title: '언어', enable_auto_detect: '자동 감지 활성화', description: - '사용자의 언어 설정을 감지하고, 해당 언어로 자동으로 변경해요. 직접 번역하여 새로운 언어를 추가할 수 도 있어요.', + '사용자의 언어 설정을 감지하고, 해당 언어로 자동으로 변경해요. 직접 번역하여 새로운 언어를 추가할 수도 있어요.', manage_language: '언어 관리', default_language: '기본 언어', default_language_description_auto: '사용자의 언어를 지원하지 않을 경우, 기본 언어로 사용자에게 보여줘요.', default_language_description_fixed: - '자동 감지가 꺼져있을 경우, 기본 언어로만 사용자에게 보여줘요. 더욱 나은 경험을 위해, 자동 감지를 켜주세요.', + '자동 감지가 꺼져있을 경우, 기본 언어로만 사용자에게 보여줘요. 더욱 나은 경험을 위해, 자동 감지를 켜 주세요.', }, manage_language: { title: '언어 관리', @@ -119,9 +119,9 @@ const sign_in_exp = { clear_all_tip: '모든 값 삭제', unsaved_description: '이 페이지를 벗어날 경우, 변경점이 적용되지 않아요.', deletion_tip: '언어 삭제', - deletion_title: '추가된 언어를 삭제할건가요?', - deletion_description: '삭제된 후에 사용자들인 더 이상 해당 언어로 볼 수 없어요.', - default_language_deletion_title: '기본 언어는 삭제될 수 없어요.', + deletion_title: '추가된 언어를 삭제할까요?', + deletion_description: '삭제된 후에 사용자들이 더 이상 해당 언어로 볼 수 없어요.', + default_language_deletion_title: '기본 언어는 삭제할 수 없어요.', default_language_deletion_description: '{{language}} 언어는 기본 언어로 설정되어 있어요. 기본 언어를 변경한 후에 삭제할 수 있어요.', }, @@ -129,24 +129,24 @@ const sign_in_exp = { title: '고급 옵션', enable_user_registration: '회원가입 활성화', enable_user_registration_description: - '사용자 등록을 활성화하거나 비활성화합니다. 비활성화된 후에도 사용자를 관리 콘솔에 추가할 수 있지만 사용자는 더 이상 로그인 UI를 통해 계정을 설정할 수 없어요.', + '사용자 등록을 활성화하거나 비활성화해요. 비활성화된 후에도 사용자를 관리 콘솔에서 추가할 수 있지만 사용자는 더 이상 로그인 UI를 통해 계정을 설정할 수 없어요.', }, }, setup_warning: { no_connector: '', no_connector_sms: - 'SMS 연동 설정이 아직 없습니다. SMS 연동 구성을 완료할 때까지 로그인할 수 없습니다. {{link}} "연동"으로', + 'SMS 연동 설정이 아직 없어요. SMS 연동 구성을 완료할 때까지 로그인할 수 없어요. {{link}} "연동"으로', no_connector_email: - '이메일 연동 설정이 아직 없습니다. 이메일 연동 구성을 완료할 때까지 로그인할 수 없습니다. {{link}} "연동"으로', + '이메일 연동 설정이 아직 없어요. 이메일 연동 구성을 완료할 때까지 로그인할 수 없어요. {{link}} "연동"으로', no_connector_social: - '소셜 연동 설정이 아직 없습니다. 소셜 연동 구성을 완료할 때까지 로그인할 수 없습니다. {{link}} "연동"으로', + '소셜 연동 설정이 아직 없어요. 소셜 연동 구성을 완료할 때까지 로그인할 수 없어요. {{link}} "연동"으로', no_added_social_connector: '보다 많은 소셜 연동들을 설정하여, 고객에게 보다 나은 경험을 제공해보세요.', setup_link: '설정', }, save_alert: { description: - '새 로그인 및 회원가입 방법을 추가하고 있어요. 모든 사용자가 새 설정의 영향을 받을 수 있어요. 정말로 하실건가요?', + '새 로그인 및 회원가입 방법을 추가하고 있어요. 모든 사용자가 새 설정의 영향을 받을 수 있어요. 정말로 추가할까요?', before: '이전', after: '이후', sign_up: '회원가입', diff --git a/packages/phrases/src/locales/ko/translation/admin-console/tab-sections.ts b/packages/phrases/src/locales/ko/translation/admin-console/tab-sections.ts index 231af4797..805ba36a8 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/tab-sections.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/tab-sections.ts @@ -2,7 +2,7 @@ const tab_sections = { overview: '살펴보기', resource_management: '리소스 관리', user_management: '사용자 관리', - access_control: 'Access Control', // UNTRANSLATED + access_control: '접근 제어', help_and_support: '고객센터', }; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/tabs.ts b/packages/phrases/src/locales/ko/translation/admin-console/tabs.ts index b3fdd64f3..1c8068015 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/tabs.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/tabs.ts @@ -7,7 +7,7 @@ const tabs = { connectors: '연동', users: '사용자 관리', audit_logs: 'Audit 기록', - roles: 'Roles', // UNTRANSLATED + roles: '역할', docs: '문서', contact_us: '연락처', settings: '설정', diff --git a/packages/phrases/src/locales/ko/translation/admin-console/user-details.ts b/packages/phrases/src/locales/ko/translation/admin-console/user-details.ts index a91bbb990..db4743bb0 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/user-details.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/user-details.ts @@ -14,8 +14,8 @@ const user_details = { congratulations: '해당 사용자의 비밀번호가 성공적으로 초기화 되었어요.', new_password: '새로운 비밀번호:', }, - tab_settings: 'Settings', // UNTRANSLATED - tab_roles: 'Roles', // UNTRANSLATED + tab_settings: '설정', + tab_roles: '역할', tab_logs: '사용자 기록', settings: '설정', settings_description: @@ -40,22 +40,22 @@ const user_details = { }, suspended: '정지됨', roles: { - name_column: 'Role', // UNTRANSLATED - description_column: 'Description', // UNTRANSLATED - assign_button: 'Assign Roles', // UNTRANSLATED + name_column: '역할', + description_column: '설명', + assign_button: '역할 할당', delete_description: - 'This action will remove this role from this user. The role itself will still exist, but it will no longer be associated with this user.', // UNTRANSLATED - deleted: '{{name}} was successfully removed from this user!', // UNTRANSLATED - assign_title: 'Assign roles to {{name}}', // UNTRANSLATED - assign_subtitle: 'Authorize {{name}} one or more roles', // UNTRANSLATED - assign_role_field: 'Assign roles', // UNTRANSLATED - role_search_placeholder: 'Search by role name', // UNTRANSLATED - added_text: '{{value, number}} added', // UNTRANSLATED - assigned_user_count: '{{value, number}} users', // UNTRANSLATED - confirm_assign: 'Assign roles', // UNTRANSLATED - role_assigned: 'Successfully assigned role(s)', // UNTRANSLATED - search: 'Search by role name, description or ID', // UNTRANSLATED - empty: 'No role available', // UNTRANSLATED + '이 행동은 사용자에게서 이 역할을 삭제할 거예요. 역할은 그대로 존재하지만, 이 사용자에게 더 이상 할당되지 않아요.', + deleted: '{{name}}이/가 성공적으로 이 사용자에게서 제거되었어요!', + assign_title: '{{name}}에게 역할 할당', + assign_subtitle: '{{name}}에게 하나 이상의 역할을 할당하세요', + assign_role_field: '역할 할당', + role_search_placeholder: '역할 이름으로 검색', + added_text: '{{value, number}}이/가 추가되었어요', + assigned_user_count: '사용자 {{value, number}}명', + confirm_assign: '역할 할당', + role_assigned: '역할을 성공적으로 할당했어요', + search: '역할 이름, 설명, ID로 검색', + empty: '역할 없음', }, }; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/users.ts b/packages/phrases/src/locales/ko/translation/admin-console/users.ts index 92a5cdffd..ce3064b10 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/users.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/users.ts @@ -3,14 +3,14 @@ const users = { subtitle: '사용자의 신원을 추가, 삭제, 수정, 조회하여 관리해보세요.', create: '사용자 추가', user_name: '사용자', - application_name: '어플리케이션으로 부터', + application_name: '어플리케이션으로부터', latest_sign_in: '최근 로그인 시각', create_form_username: '사용자 이름', create_form_password: '비밀번호', create_form_name: '이름', unnamed: '이름없음', - search: 'Search by name, email, phone or username', // UNTRANSLATED - check_user_detail: 'Check user detail', // UNTRANSLATED + search: '이름, 이메일, 전화번호, ID로 검색', + check_user_detail: '사용자 상세정보 확인', }; export default users; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/welcome.ts b/packages/phrases/src/locales/ko/translation/admin-console/welcome.ts index 7ad093db8..cbbcf1600 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/welcome.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/welcome.ts @@ -1,7 +1,7 @@ const welcome = { title: '관리자 콘솔에 오신 것을 환영해요', description: - '관리자 콘솔를 통해 프로그래밍 없이 Logto를 관리할 수 있어요. 새로 계정을 생성하고, Logto를 당신의 회사에 알맞게 수정해보세요.', + '관리자 콘솔를 통해 프로그래밍 없이 Logto를 관리할 수 있어요. 새로 계정을 생성하고, Logto를 당신의 회사에 알맞게 수정해 보세요.', create_account: '계정 생성', }; From 3a5ac1edc9f04e1b1abd4946437f3204c3007ac9 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Wed, 8 Feb 2023 14:35:03 +0800 Subject: [PATCH 03/34] fix(core): should validate config when creating a new connector instance (#3068) --- packages/core/src/libraries/connector.ts | 15 ++---- packages/core/src/routes/connector.test.ts | 52 ++++++++++++++++++++- packages/core/src/routes/connector.ts | 10 +++- packages/core/src/utils/connectors/index.ts | 24 +++++++++- packages/shared/package.json | 1 + pnpm-lock.yaml | 2 + 6 files changed, 88 insertions(+), 16 deletions(-) diff --git a/packages/core/src/libraries/connector.ts b/packages/core/src/libraries/connector.ts index 4195442c7..09d589c3d 100644 --- a/packages/core/src/libraries/connector.ts +++ b/packages/core/src/libraries/connector.ts @@ -6,7 +6,7 @@ import type Queries from '#src/tenants/Queries.js'; import assertThat from '#src/utils/assert-that.js'; import { defaultConnectorMethods } from '#src/utils/connectors/consts.js'; import { loadConnectorFactories } from '#src/utils/connectors/factories.js'; -import { validateConnectorModule, parseMetadata } from '#src/utils/connectors/index.js'; +import { buildRawConnector } from '#src/utils/connectors/index.js'; import type { LogtoConnector } from '#src/utils/connectors/types.js'; export type ConnectorLibrary = ReturnType; @@ -39,16 +39,11 @@ export const createConnectorLibrary = (queries: Queries) => { return; } - const { createConnector, path: packagePath } = connectorFactory; - try { - const rawConnector = await createConnector({ - getConfig: async () => { - return getConnectorConfig(id); - }, - }); - validateConnectorModule(rawConnector); - const rawMetadata = await parseMetadata(rawConnector.metadata, packagePath); + const { rawConnector, rawMetadata } = await buildRawConnector( + connectorFactory, + async () => getConnectorConfig(id) + ); const connector: AllConnector = { ...defaultConnectorMethods, diff --git a/packages/core/src/routes/connector.test.ts b/packages/core/src/routes/connector.test.ts index b25ae3e1e..2a508eda9 100644 --- a/packages/core/src/routes/connector.test.ts +++ b/packages/core/src/routes/connector.test.ts @@ -1,6 +1,11 @@ /* eslint-disable max-lines */ import type { EmailConnector, SmsConnector } from '@logto/connector-kit'; -import { ConnectorPlatform, VerificationCodeType } from '@logto/connector-kit'; +import { + ConnectorError, + ConnectorErrorCodes, + ConnectorPlatform, + VerificationCodeType, +} from '@logto/connector-kit'; import type { Connector } from '@logto/schemas'; import { ConnectorType } from '@logto/schemas'; import { pickDefault, createMockUtils } from '@logto/shared/esm'; @@ -26,7 +31,7 @@ import type { LogtoConnector } from '#src/utils/connectors/types.js'; import { createRequester } from '#src/utils/test-utils.js'; const { jest } = import.meta; -const { mockEsm } = createMockUtils(jest); +const { mockEsm, mockEsmWithActual } = createMockUtils(jest); mockEsm('#src/utils/connectors/platform.js', () => ({ checkSocialConnectorTargetAndPlatformUniqueness: jest.fn(), @@ -56,6 +61,14 @@ const { loadConnectorFactories } = mockEsm('#src/utils/connectors/factories.js', loadConnectorFactories: jest.fn(), })); +const { buildRawConnector } = await mockEsmWithActual('#src/utils/connectors/index.js', () => ({ + buildRawConnector: jest.fn(), +})); + +const { validateConfig } = await mockEsmWithActual('@logto/connector-kit', () => ({ + validateConfig: jest.fn(), +})); + const tenantContext = new MockTenant( undefined, { connectors: connectorQueries }, @@ -178,6 +191,8 @@ describe('connector route', () => { ...mockLogtoConnector, }, ]); + validateConfig.mockReturnValueOnce(null); + buildRawConnector.mockResolvedValueOnce({ rawConnector: { configGuard: any() } }); await connectorRequest.post('/connectors').send({ connectorId: 'connectorId', config: { cliend_id: 'client_id', client_secret: 'client_secret' }, @@ -190,6 +205,33 @@ describe('connector route', () => { ); }); + it('throw when create a new connector record with wrong config', async () => { + loadConnectorFactories.mockResolvedValueOnce([ + { + ...mockConnectorFactory, + metadata: { ...mockConnectorFactory.metadata, id: 'connectorId' }, + }, + ]); + countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 }); + getLogtoConnectors.mockResolvedValueOnce([ + { + dbEntry: { ...mockConnector, connectorId: 'id0' }, + metadata: { ...mockMetadata, id: 'id0' }, + type: ConnectorType.Sms, + ...mockLogtoConnector, + }, + ]); + validateConfig.mockImplementationOnce((config: unknown) => { + throw new ConnectorError(ConnectorErrorCodes.General); + }); + buildRawConnector.mockResolvedValueOnce({ rawConnector: { configGuard: any() } }); + const response = await connectorRequest.post('/connectors').send({ + connectorId: 'connectorId', + config: { cliend_id: 'client_id', client_secret: 'client_secret' }, + }); + expect(response).toHaveProperty('statusCode', 500); + }); + it('throws when connector factory not found', async () => { loadConnectorFactories.mockResolvedValueOnce([ { @@ -225,6 +267,8 @@ describe('connector route', () => { ...mockLogtoConnector, }, ]); + validateConfig.mockReturnValueOnce(null); + buildRawConnector.mockResolvedValueOnce({ rawConnector: { configGuard: any() } }); await connectorRequest.post('/connectors').send({ connectorId: 'id0', config: { cliend_id: 'client_id', client_secret: 'client_secret' }, @@ -293,6 +337,8 @@ describe('connector route', () => { }, ]); countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 }); + validateConfig.mockReturnValueOnce(null); + buildRawConnector.mockResolvedValueOnce({ rawConnector: { configGuard: any() } }); await connectorRequest.post('/connectors').send({ connectorId: 'id1', config: { cliend_id: 'client_id', client_secret: 'client_secret' }, @@ -335,6 +381,8 @@ describe('connector route', () => { ...mockLogtoConnector, }, ]); + validateConfig.mockReturnValueOnce(null); + buildRawConnector.mockResolvedValueOnce({ rawConnector: { configGuard: any() } }); const response = await connectorRequest.post('/connectors').send({ connectorId: 'id0', metadata: { target: 'target' }, diff --git a/packages/core/src/routes/connector.ts b/packages/core/src/routes/connector.ts index 4ad949ca8..48e73143f 100644 --- a/packages/core/src/routes/connector.ts +++ b/packages/core/src/routes/connector.ts @@ -1,14 +1,15 @@ -import { VerificationCodeType } from '@logto/connector-kit'; +import { VerificationCodeType, validateConfig } from '@logto/connector-kit'; import { emailRegEx, phoneRegEx, buildIdGenerator } from '@logto/core-kit'; import type { ConnectorResponse, ConnectorFactoryResponse } from '@logto/schemas'; import { arbitraryObjectGuard, Connectors, ConnectorType } from '@logto/schemas'; import cleanDeep from 'clean-deep'; -import { object, string } from 'zod'; +import { string, object } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; import assertThat from '#src/utils/assert-that.js'; import { loadConnectorFactories } from '#src/utils/connectors/factories.js'; +import { buildRawConnector } from '#src/utils/connectors/index.js'; import { checkSocialConnectorTargetAndPlatformUniqueness } from '#src/utils/connectors/platform.js'; import type { LogtoConnector } from '#src/utils/connectors/types.js'; @@ -184,6 +185,11 @@ export default function connectorRoutes( ); } + if (config) { + const { rawConnector } = await buildRawConnector(connectorFactory); + validateConfig(config, rawConnector.configGuard); + } + const insertConnectorId = generateConnectorId(); await insertConnector({ id: insertConnectorId, diff --git a/packages/core/src/utils/connectors/index.ts b/packages/core/src/utils/connectors/index.ts index 4771b3d46..775571561 100644 --- a/packages/core/src/utils/connectors/index.ts +++ b/packages/core/src/utils/connectors/index.ts @@ -2,9 +2,12 @@ import { existsSync } from 'fs'; import { readFile } from 'fs/promises'; import path from 'path'; -import type { AllConnector, BaseConnector } from '@logto/connector-kit'; +import type { AllConnector, BaseConnector, GetConnectorConfig } from '@logto/connector-kit'; import { ConnectorError, ConnectorErrorCodes, ConnectorType } from '@logto/connector-kit'; +import { notImplemented } from './consts.js'; +import type { ConnectorFactory } from './types.js'; + export function validateConnectorModule( connector: Partial> ): asserts connector is BaseConnector { @@ -47,7 +50,10 @@ export const readUrl = async ( return readFile(path.join(baseUrl, url), 'utf8'); }; -export const parseMetadata = async (metadata: AllConnector['metadata'], packagePath: string) => { +export const parseMetadata = async ( + metadata: AllConnector['metadata'], + packagePath: string +): Promise => { return { ...metadata, logo: await readUrl(metadata.logo, packagePath, 'svg'), @@ -56,3 +62,17 @@ export const parseMetadata = async (metadata: AllConnector['metadata'], packageP configTemplate: await readUrl(metadata.configTemplate, packagePath, 'text'), }; }; + +export const buildRawConnector = async ( + connectorFactory: ConnectorFactory, + getConnectorConfig?: GetConnectorConfig +) => { + const { createConnector, path: packagePath } = connectorFactory; + const rawConnector = await createConnector({ + getConfig: getConnectorConfig ?? notImplemented, + }); + validateConnectorModule(rawConnector); + const rawMetadata = await parseMetadata(rawConnector.metadata, packagePath); + + return { rawConnector, rawMetadata }; +}; diff --git a/packages/shared/package.json b/packages/shared/package.json index 332f50fdc..6495f2090 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -32,6 +32,7 @@ "test:ci": "pnpm test:only" }, "devDependencies": { + "@logto/connector-kit": "workspace:*", "@logto/core-kit": "workspace:*", "@silverhand/eslint-config": "1.3.0", "@silverhand/ts-config": "1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f9d0f773..df1992522 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -625,6 +625,7 @@ importers: packages/shared: specifiers: + '@logto/connector-kit': workspace:* '@logto/core-kit': workspace:* '@logto/schemas': workspace:* '@silverhand/eslint-config': 1.3.0 @@ -647,6 +648,7 @@ importers: nanoid: 4.0.0 slonik: 30.1.2 devDependencies: + '@logto/connector-kit': link:../toolkit/connector-kit '@logto/core-kit': link:../toolkit/core-kit '@silverhand/eslint-config': 1.3.0_k3lfx77tsvurbevhk73p7ygch4 '@silverhand/ts-config': 1.2.1_typescript@4.9.4 From 514930eb211b632ee619d20ba99e250b17325e04 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 8 Feb 2023 15:55:04 +0800 Subject: [PATCH 04/34] feat(console): compact-style radio group (#3071) --- .../src/components/RadioGroup/Radio.tsx | 2 +- .../components/RadioGroup/index.module.scss | 72 +++++++++++++++++++ .../src/components/RadioGroup/index.tsx | 2 +- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/packages/console/src/components/RadioGroup/Radio.tsx b/packages/console/src/components/RadioGroup/Radio.tsx index b201a5413..74b54faac 100644 --- a/packages/console/src/components/RadioGroup/Radio.tsx +++ b/packages/console/src/components/RadioGroup/Radio.tsx @@ -24,7 +24,7 @@ export type Props = { isChecked?: boolean; onClick?: () => void; tabIndex?: number; - type?: 'card' | 'plain'; + type?: 'card' | 'plain' | 'compact'; isDisabled?: boolean; disabledLabel?: AdminConsoleKey; }; diff --git a/packages/console/src/components/RadioGroup/index.module.scss b/packages/console/src/components/RadioGroup/index.module.scss index 1576dee69..cc82ebfb5 100644 --- a/packages/console/src/components/RadioGroup/index.module.scss +++ b/packages/console/src/components/RadioGroup/index.module.scss @@ -10,7 +10,79 @@ } } +.compact { + display: flex; + flex-wrap: nowrap; + font: var(--font-body-2); + align-items: center; + + > .radio { + position: relative; + border: 1px solid var(--color-border); + user-select: none; + cursor: pointer; + flex: 1; + + &:first-child { + border-radius: 12px 0 0 12px; + } + + &:last-child { + border-radius: 0 12px 12px 0; + } + + &:not(:first-child) { + border-left: none; + } + + &.disabled { + cursor: not-allowed; + background-color: var(--color-layer-2); + } + + &:not(.disabled):focus { + border-color: var(--color-primary); + } + + &:not(.disabled):hover { + color: var(--color-text-link); + font: var(--font-label-2); + border-color: var(--color-primary); + background-color: var(--color-hover-variant); + + &:not(:first-child)::before { + position: absolute; + content: ''; + width: 1px; + top: -1px; + left: -1px; + bottom: -1px; + background-color: var(--color-primary); + } + } + + .content { + padding: _.unit(5); + } + + &.checked { + border-color: var(--color-primary); + + &:not(:first-child)::before { + position: absolute; + content: ''; + width: 1px; + top: -1px; + left: -1px; + bottom: -1px; + background-color: var(--color-primary); + } + } + } +} + .card { + /* stylelint-disable-next-line no-descending-specificity */ > .radio { padding: _.unit(3); border-radius: _.unit(4); diff --git a/packages/console/src/components/RadioGroup/index.tsx b/packages/console/src/components/RadioGroup/index.tsx index aa3530504..395ac8973 100644 --- a/packages/console/src/components/RadioGroup/index.tsx +++ b/packages/console/src/components/RadioGroup/index.tsx @@ -19,7 +19,7 @@ type Props = { name: string; children: RadioElement | RadioElement[]; value?: string; - type?: 'card' | 'plain'; + type?: 'card' | 'plain' | 'compact'; className?: string; onChange?: (value: string) => void; }; From eea02cca70d0633ae7ff98045786097e9fcce1e7 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Thu, 9 Feb 2023 09:09:18 +0800 Subject: [PATCH 05/34] refactor(core): move connector session read/write to social verification (#3072) --- .../routes/interaction/utils/interaction.ts | 37 ---------------- .../interaction/utils/social-verification.ts | 42 +++++++++++++++++-- 2 files changed, 38 insertions(+), 41 deletions(-) diff --git a/packages/core/src/routes/interaction/utils/interaction.ts b/packages/core/src/routes/interaction/utils/interaction.ts index a4361ef5e..67c8ce3fd 100644 --- a/packages/core/src/routes/interaction/utils/interaction.ts +++ b/packages/core/src/routes/interaction/utils/interaction.ts @@ -1,5 +1,3 @@ -import type { ConnectorSession } from '@logto/connector-kit'; -import { connectorSessionGuard } from '@logto/connector-kit'; import type { Profile } from '@logto/schemas'; import { InteractionEvent } from '@logto/schemas'; import { assert } from '@silverhand/essentials'; @@ -7,7 +5,6 @@ import type { Context } from 'koa'; import { errors } from 'oidc-provider'; import type { InteractionResults } from 'oidc-provider'; import type Provider from 'oidc-provider'; -import { z } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; import assertThat from '#src/utils/assert-that.js'; @@ -135,40 +132,6 @@ export const clearInteractionStorage = async (ctx: Context, provider: Provider) } }; -export const assignConnectorSessionResult = async ( - ctx: Context, - provider: Provider, - connectorSession: ConnectorSession -) => { - const details = await provider.interactionDetails(ctx.req, ctx.res); - await provider.interactionResult(ctx.req, ctx.res, { - ...details.result, - connectorSession, - }); -}; - -export const getConnectorSessionResult = async ( - ctx: Context, - provider: Provider -): Promise => { - const { result } = await provider.interactionDetails(ctx.req, ctx.res); - - const signInResult = z - .object({ - connectorSession: connectorSessionGuard, - }) - .safeParse(result); - - assertThat(result && signInResult.success, 'session.connector_validation_session_not_found'); - - const { connectorSession, ...rest } = result; - await provider.interactionResult(ctx.req, ctx.res, { - ...rest, - }); - - return signInResult.data.connectorSession; -}; - /** * The following three methods (`getInteractionFromProviderByJti`, `assignResultToInteraction` * and `epochTime`) refer to implementation in diff --git a/packages/core/src/routes/interaction/utils/social-verification.ts b/packages/core/src/routes/interaction/utils/social-verification.ts index 4276f0f9d..61b6223ad 100644 --- a/packages/core/src/routes/interaction/utils/social-verification.ts +++ b/packages/core/src/routes/interaction/utils/social-verification.ts @@ -1,12 +1,12 @@ import type { ConnectorSession, SocialUserInfo } from '@logto/connector-kit'; +import { connectorSessionGuard } from '@logto/connector-kit'; import type { SocialConnectorPayload } from '@logto/schemas'; import { ConnectorType } from '@logto/schemas'; +import type { Context } from 'koa'; +import type Provider from 'oidc-provider'; +import { z } from 'zod'; import type { WithLogContext } from '#src/middleware/koa-audit-log.js'; -import { - assignConnectorSessionResult, - getConnectorSessionResult, -} from '#src/routes/interaction/utils/interaction.js'; import type TenantContext from '#src/tenants/TenantContext.js'; import assertThat from '#src/utils/assert-that.js'; @@ -73,3 +73,37 @@ export const verifySocialIdentity = async ( return userInfo; }; + +const assignConnectorSessionResult = async ( + ctx: Context, + provider: Provider, + connectorSession: ConnectorSession +) => { + const details = await provider.interactionDetails(ctx.req, ctx.res); + await provider.interactionResult(ctx.req, ctx.res, { + ...details.result, + connectorSession, + }); +}; + +const getConnectorSessionResult = async ( + ctx: Context, + provider: Provider +): Promise => { + const { result } = await provider.interactionDetails(ctx.req, ctx.res); + + const signInResult = z + .object({ + connectorSession: connectorSessionGuard, + }) + .safeParse(result); + + assertThat(result && signInResult.success, 'session.connector_validation_session_not_found'); + + const { connectorSession, ...rest } = result; + await provider.interactionResult(ctx.req, ctx.res, { + ...rest, + }); + + return signInResult.data.connectorSession; +}; From 9ba7c0cdf0433cf4c89484e03d19bb0624fd66d5 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Thu, 9 Feb 2023 09:34:26 +0800 Subject: [PATCH 06/34] feat(console): multi card selector on cloud preview page (#3073) --- .../MultiCardSelector/index.module.scss | 29 ++++++++++ .../components/MultiCardSelector/index.tsx | 53 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 packages/console/src/pages/CloudPreview/components/MultiCardSelector/index.module.scss create mode 100644 packages/console/src/pages/CloudPreview/components/MultiCardSelector/index.tsx diff --git a/packages/console/src/pages/CloudPreview/components/MultiCardSelector/index.module.scss b/packages/console/src/pages/CloudPreview/components/MultiCardSelector/index.module.scss new file mode 100644 index 000000000..ab4f6dba9 --- /dev/null +++ b/packages/console/src/pages/CloudPreview/components/MultiCardSelector/index.module.scss @@ -0,0 +1,29 @@ +@use '@/scss/underscore' as _; + +.selector { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: _.unit(4); +} + +.option { + border: 1px solid var(--color-border); + border-radius: 12px; + min-height: 80px; + padding: _.unit(5); + font: var(--font-body-2); + cursor: pointer; + user-select: none; + background-color: var(--color-layer-1); + color: var(--color-text); + + &.selected { + border-color: var(--color-primary); + color: var(--color-text-link); + } + + &:hover { + background-color: var(--color-hover-variant); + color: var(--color-text-link); + } +} diff --git a/packages/console/src/pages/CloudPreview/components/MultiCardSelector/index.tsx b/packages/console/src/pages/CloudPreview/components/MultiCardSelector/index.tsx new file mode 100644 index 000000000..b2828d88a --- /dev/null +++ b/packages/console/src/pages/CloudPreview/components/MultiCardSelector/index.tsx @@ -0,0 +1,53 @@ +import classNames from 'classnames'; +import type { ReactNode } from 'react'; + +import { onKeyDownHandler } from '@/utilities/a11y'; + +import * as styles from './index.module.scss'; + +type Option = { + title: ReactNode; + value: string; +}; + +type Props = { + options: Option[]; + value: string[]; + onChange: (value: string[]) => void; +}; + +const MultiCardSelector = ({ options, value: selectedValues, onChange }: Props) => { + const onToggle = (value: string) => { + onChange( + selectedValues.includes(value) + ? selectedValues.filter((selected) => selected !== value) + : [...selectedValues, value] + ); + }; + + return ( +
+ {options.map((option) => ( +
{ + onToggle(option.value); + }} + onKeyDown={onKeyDownHandler(() => { + onToggle(option.value); + })} + > + {option.title} +
+ ))} +
+ ); +}; + +export default MultiCardSelector; From 2a0ec7f78c56a968a00864b7f2f0ae4b69657d2d Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Thu, 9 Feb 2023 15:39:50 +0800 Subject: [PATCH 07/34] refactor: update connector interfaces (#3080) --- .changeset-staged/nine-apes-attend.md | 5 +++++ packages/core/src/libraries/social.ts | 2 +- packages/toolkit/connector-kit/src/types.ts | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset-staged/nine-apes-attend.md diff --git a/.changeset-staged/nine-apes-attend.md b/.changeset-staged/nine-apes-attend.md new file mode 100644 index 000000000..aa8ee9443 --- /dev/null +++ b/.changeset-staged/nine-apes-attend.md @@ -0,0 +1,5 @@ +--- +"@logto/connector-kit": minor +--- + +`getSession` and `setSession` are actually used as REQUIRED parameters, update interface definition. diff --git a/packages/core/src/libraries/social.ts b/packages/core/src/libraries/social.ts index e22648475..462135926 100644 --- a/packages/core/src/libraries/social.ts +++ b/packages/core/src/libraries/social.ts @@ -65,7 +65,7 @@ export const createSocialLibrary = (queries: Queries, connectorLibrary: Connecto const getUserInfoByAuthCode = async ( connectorId: string, data: unknown, - getConnectorSession?: GetSession + getConnectorSession: GetSession ): Promise => { const connector = await getConnector(connectorId); diff --git a/packages/toolkit/connector-kit/src/types.ts b/packages/toolkit/connector-kit/src/types.ts index 2ea427157..3036587c4 100644 --- a/packages/toolkit/connector-kit/src/types.ts +++ b/packages/toolkit/connector-kit/src/types.ts @@ -191,7 +191,7 @@ export type GetAuthorizationUri = ( jti: string; headers: { userAgent?: string }; }, - setSession?: SetSession + setSession: SetSession ) => Promise; export const socialUserInfoGuard = z.object({ @@ -206,5 +206,5 @@ export type SocialUserInfo = z.infer; export type GetUserInfo = ( data: unknown, - getSession?: GetSession + getSession: GetSession ) => Promise>; From 7b9712db9adcd9043a9b79711e764bd25266dcf4 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Thu, 9 Feb 2023 15:42:54 +0800 Subject: [PATCH 08/34] refactor(console): support icon in radio item (#3077) --- .../components/RadioGroup/Radio.module.scss | 254 ++++++++++++++++++ .../src/components/RadioGroup/Radio.tsx | 8 +- .../components/RadioGroup/index.module.scss | 189 ------------- 3 files changed, 260 insertions(+), 191 deletions(-) create mode 100644 packages/console/src/components/RadioGroup/Radio.module.scss diff --git a/packages/console/src/components/RadioGroup/Radio.module.scss b/packages/console/src/components/RadioGroup/Radio.module.scss new file mode 100644 index 000000000..a38e971a8 --- /dev/null +++ b/packages/console/src/components/RadioGroup/Radio.module.scss @@ -0,0 +1,254 @@ +@use '@/scss/underscore' as _; + +// Default Styles +.radio { + user-select: none; + cursor: pointer; + font: var(--font-body-2); + + &:not(:last-child) { + margin-bottom: _.unit(2); + } + + .content { + display: flex; + align-items: center; + + .indicator { + border-radius: 50%; + border: 2px solid var(--color-neutral-60); + display: inline-block; + margin-right: _.unit(2); + + &::before { + content: ''; + background: var(--color-layer-1); + width: 10px; + height: 10px; + display: block; + border-radius: 50%; + border: 2px solid var(--color-layer-1); + } + } + + .icon { + margin-right: _.unit(2); + color: var(--color-text-secondary); + + > svg { + display: block; + } + } + } +} + +.card { + padding: _.unit(3); + border-radius: _.unit(4); + border: 1px solid transparent; + outline: 1px solid var(--color-neutral-90); + + &:not(:last-child) { + margin-bottom: unset; + } + + .content { + position: relative; + display: block; + + .indicator { + border-radius: unset; + border: unset; + display: block; + margin-right: unset; + position: absolute; + right: 0; + top: 0; + + svg { + opacity: 0%; + } + + &::before { + display: none; + } + } + + .icon { + margin-right: _.unit(2); + vertical-align: middle; + color: var(--color-text-secondary); + + > svg { + display: unset; + } + } + + .disabledLabel { + background: var(--color-neutral-90); + padding: _.unit(0.5) _.unit(2); + border-radius: 10px; + font: var(--font-label-3); + color: var(--color-text); + } + } +} + +.compact { + position: relative; + border: 1px solid var(--color-border); + flex: 1; + font: var(--font-label-2); + + &:first-child { + border-radius: 12px 0 0 12px; + } + + &:last-child { + border-radius: 0 12px 12px 0; + } + + &:not(:first-child) { + border-left: none; + } + + &:not(:last-child) { + margin-bottom: unset; + } + + .content { + padding: _.unit(5); + + .icon { + margin-right: _.unit(4); + } + } +} + +// Checked Styles +.radio.checked { + .content { + .indicator { + border-color: var(--color-primary); + + &::before { + background: var(--color-primary); + } + } + } +} + +.card.checked { + border-color: var(--color-primary); + outline: 1px solid var(--color-primary); + + .content { + .indicator { + svg { + opacity: 100%; + } + } + } +} + +.compact.checked { + color: var(--color-text-link); + border-color: var(--color-primary); + + .content { + .icon { + color: var(--color-primary); + } + } + + &:not(:first-child)::before { + position: absolute; + content: ''; + width: 1px; + top: -1px; + left: -1px; + bottom: -1px; + background-color: var(--color-primary); + } +} + + +// Disabled Styles +.radio.disabled { + cursor: not-allowed; + color: var(--color-disabled); + + .content { + .indicator { + border-color: var(--color-disabled); + + &::before { + background: var(--color-layer-1); + } + } + } +} + +.card.disabled { + background-color: var(--color-layer-2); + border-color: var(--color-layer-2); + outline: unset; +} + +.compact.disabled { + cursor: not-allowed; + background-color: var(--color-layer-2); + + .content { + .icon { + color: var(--color-text-secondary); + } + } +} + +// Not Disabled Behaviors +.card:not(.disabled) { + &:focus { + outline: 1px solid var(--color-primary); + box-shadow: var(--shadow-2); + } + + &:hover { + box-shadow: var(--shadow-2); + } +} + +.compact:not(.disabled) { + &:focus { + color: var(--color-text-link); + border-color: var(--color-primary); + + .content { + .icon { + color: var(--color-primary); + } + } + } + + &:hover { + color: var(--color-text-link); + border-color: var(--color-primary); + background-color: var(--color-hover-variant); + + .content { + .icon { + color: var(--color-primary); + } + } + + &:not(:first-child)::before { + position: absolute; + content: ''; + width: 1px; + top: -1px; + left: -1px; + bottom: -1px; + background-color: var(--color-primary); + } + } +} diff --git a/packages/console/src/components/RadioGroup/Radio.tsx b/packages/console/src/components/RadioGroup/Radio.tsx index 74b54faac..6d6054ba3 100644 --- a/packages/console/src/components/RadioGroup/Radio.tsx +++ b/packages/console/src/components/RadioGroup/Radio.tsx @@ -4,7 +4,7 @@ import type { KeyboardEventHandler, ReactNode } from 'react'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import * as styles from './index.module.scss'; +import * as styles from './Radio.module.scss'; const Check = () => ( @@ -27,6 +27,7 @@ export type Props = { type?: 'card' | 'plain' | 'compact'; isDisabled?: boolean; disabledLabel?: AdminConsoleKey; + icon?: ReactNode; }; const Radio = ({ @@ -38,9 +39,10 @@ const Radio = ({ isChecked, onClick, tabIndex, - type, + type = 'plain', isDisabled, disabledLabel, + icon, }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); @@ -62,6 +64,7 @@ const Radio = ({
} + {icon && {icon}} {title && t(title)} {isDisabled && disabledLabel && (
diff --git a/packages/console/src/components/RadioGroup/index.module.scss b/packages/console/src/components/RadioGroup/index.module.scss index cc82ebfb5..e37c3253c 100644 --- a/packages/console/src/components/RadioGroup/index.module.scss +++ b/packages/console/src/components/RadioGroup/index.module.scss @@ -13,194 +13,5 @@ .compact { display: flex; flex-wrap: nowrap; - font: var(--font-body-2); align-items: center; - - > .radio { - position: relative; - border: 1px solid var(--color-border); - user-select: none; - cursor: pointer; - flex: 1; - - &:first-child { - border-radius: 12px 0 0 12px; - } - - &:last-child { - border-radius: 0 12px 12px 0; - } - - &:not(:first-child) { - border-left: none; - } - - &.disabled { - cursor: not-allowed; - background-color: var(--color-layer-2); - } - - &:not(.disabled):focus { - border-color: var(--color-primary); - } - - &:not(.disabled):hover { - color: var(--color-text-link); - font: var(--font-label-2); - border-color: var(--color-primary); - background-color: var(--color-hover-variant); - - &:not(:first-child)::before { - position: absolute; - content: ''; - width: 1px; - top: -1px; - left: -1px; - bottom: -1px; - background-color: var(--color-primary); - } - } - - .content { - padding: _.unit(5); - } - - &.checked { - border-color: var(--color-primary); - - &:not(:first-child)::before { - position: absolute; - content: ''; - width: 1px; - top: -1px; - left: -1px; - bottom: -1px; - background-color: var(--color-primary); - } - } - } -} - -.card { - /* stylelint-disable-next-line no-descending-specificity */ - > .radio { - padding: _.unit(3); - border-radius: _.unit(4); - border: 1px solid transparent; - outline: 1px solid var(--color-neutral-90); - user-select: none; - cursor: pointer; - - &.disabled { - cursor: not-allowed; - background-color: var(--color-layer-2); - border-color: var(--color-layer-2); - outline: unset; - } - - &:not(.disabled):focus { - outline: 1px solid var(--color-primary); - box-shadow: var(--shadow-2); - } - - &:not(.disabled):hover { - box-shadow: var(--shadow-2); - } - - .content { - position: relative; - - .indicator { - position: absolute; - right: 0; - top: 0; - - svg { - opacity: 0%; - } - } - - .disabledLabel { - background: var(--color-neutral-90); - padding: _.unit(0.5) _.unit(2); - border-radius: 10px; - font: var(--font-label-3); - color: var(--color-text); - } - } - - &.checked { - border-color: var(--color-primary); - outline: 1px solid var(--color-primary); - - .indicator { - svg { - opacity: 100%; - } - } - } - } -} - -.plain { - font: var(--font-body-2); - - /* stylelint-disable-next-line no-descending-specificity */ - > .radio { - cursor: pointer; - - /* stylelint-disable-next-line no-descending-specificity */ - &:not(:last-child) { - margin-bottom: _.unit(2); - } - - .content { - display: flex; - align-items: center; - - .indicator { - border-radius: 50%; - border: 2px solid var(--color-neutral-60); - display: inline-block; - margin-right: _.unit(2); - - &::before { - content: ''; - background: var(--color-layer-1); - width: 10px; - height: 10px; - display: block; - border-radius: 50%; - border: 2px solid var(--color-layer-1); - } - } - } - - &.checked { - .content { - .indicator { - border-color: var(--color-primary); - - &::before { - background: var(--color-primary); - } - } - } - } - - &.disabled { - cursor: not-allowed; - color: var(--color-disabled); - - .content { - .indicator { - border-color: var(--color-disabled); - - &::before { - background: var(--color-layer-1); - } - } - } - } - } } From 9208aee684854a6a08f18f2de881036cab628831 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Thu, 9 Feb 2023 18:07:49 +0800 Subject: [PATCH 09/34] feat(console): add `CardSelector` component for cloud preview (#3085) --- .../components/CardSelector/index.tsx | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 packages/console/src/pages/CloudPreview/components/CardSelector/index.tsx diff --git a/packages/console/src/pages/CloudPreview/components/CardSelector/index.tsx b/packages/console/src/pages/CloudPreview/components/CardSelector/index.tsx new file mode 100644 index 000000000..469e94ac1 --- /dev/null +++ b/packages/console/src/pages/CloudPreview/components/CardSelector/index.tsx @@ -0,0 +1,27 @@ +import type { AdminConsoleKey } from '@logto/phrases'; +import type { ReactNode } from 'react'; + +import RadioGroup, { Radio } from '@/components/RadioGroup'; + +export type Option = { + icon?: ReactNode; + title: AdminConsoleKey; + value: string; +}; + +type Props = { + name: string; + value: string; + options: Option[]; + onChange: (value: string) => void; +}; + +const CardSelector = ({ name, value, options, onChange }: Props) => ( + + {options.map(({ value: optionValue, title, icon }) => ( + + ))} + +); + +export default CardSelector; From 401465e01df487b1c71c286b7638ec1ae58df70b Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Thu, 9 Feb 2023 19:07:40 +0800 Subject: [PATCH 10/34] chore(ui): remove unused profile related phrases (#3088) --- packages/phrases-ui/src/locales/de.ts | 31 ------------------------ packages/phrases-ui/src/locales/en.ts | 31 ------------------------ packages/phrases-ui/src/locales/fr.ts | 31 ------------------------ packages/phrases-ui/src/locales/ko.ts | 30 ----------------------- packages/phrases-ui/src/locales/pt-br.ts | 31 ------------------------ packages/phrases-ui/src/locales/pt-pt.ts | 31 ------------------------ packages/phrases-ui/src/locales/tr-tr.ts | 31 ------------------------ packages/phrases-ui/src/locales/zh-cn.ts | 31 ------------------------ 8 files changed, 247 deletions(-) diff --git a/packages/phrases-ui/src/locales/de.ts b/packages/phrases-ui/src/locales/de.ts index 98f0181e9..cd7e51038 100644 --- a/packages/phrases-ui/src/locales/de.ts +++ b/packages/phrases-ui/src/locales/de.ts @@ -97,37 +97,6 @@ const translation = { continue_with_more_information: 'Für zusätzliche Sicherheit, vervollständige bitte deine Informationen.', }, - profile: { - title: 'Konto Einstellungen', - description: - 'Ändere hier deine Konto Einstellungen und verwalte deine persönlichen Informationen, um die Sicherheit deines Kontos zu gewährleisten.', - settings: { - title: 'PROFIL EINSTELLUNGEN', - profile_information: 'Profil Informationen', - avatar: 'Avatar', - name: 'Name', - username: 'Benutzername', - }, - password: { - title: 'PASSWORT', - reset_password: 'Passwort zurücksetzen', - reset_password_sc: 'Passwort zurücksetzen', - }, - link_account: { - title: 'KONTO VERKNÜPFEN', - email_phone_sign_in: 'Anmeldung mit Email oder Telefonnummer', - email: 'Email', - phone: 'Telefonnummer', - phone_sc: 'Telefonnummer', - social: 'Social Anmeldung', - social_sc: 'Social Konten', - }, - not_set: 'Nicht gesetzt', - edit: 'Bearbeiten', - change: 'Ändern', - link: 'Verknüpfen', - unlink: 'Verknüpfung aufheben', - }, error: { username_password_mismatch: 'Benutzername oder Passwort ist falsch', username_required: 'Benutzername ist erforderlich', diff --git a/packages/phrases-ui/src/locales/en.ts b/packages/phrases-ui/src/locales/en.ts index ec9f56fa8..612b5baf8 100644 --- a/packages/phrases-ui/src/locales/en.ts +++ b/packages/phrases-ui/src/locales/en.ts @@ -92,37 +92,6 @@ const translation = { 'For added security, please link your email or phone with the account.', continue_with_more_information: 'For added security, please complete below account details.', }, - profile: { - title: 'Account Settings', - description: - 'Change your account settings and manage your personal information here to ensure your account security.', - settings: { - title: 'PROFILE SETTINGS', - profile_information: 'Profile Information', - avatar: 'Avatar', - name: 'Name', - username: 'Username', - }, - password: { - title: 'PASSWORD', - reset_password: 'Reset Password', - reset_password_sc: 'Reset password', - }, - link_account: { - title: 'LINK ACCOUNT', - email_phone_sign_in: 'Email / Phone Sign-In', - email: 'Email', - phone: 'Phone', - phone_sc: 'Phone number', - social: 'Social Sign-In', - social_sc: 'Social accounts', - }, - not_set: 'Not set', - edit: 'Edit', - change: 'Change', - link: 'Link', - unlink: 'Unlink', - }, error: { username_password_mismatch: 'Username and password do not match', username_required: 'Username is required', diff --git a/packages/phrases-ui/src/locales/fr.ts b/packages/phrases-ui/src/locales/fr.ts index d0eec0d4f..01ba292ed 100644 --- a/packages/phrases-ui/src/locales/fr.ts +++ b/packages/phrases-ui/src/locales/fr.ts @@ -95,37 +95,6 @@ const translation = { 'For added security, please link your email or phone with the account.', // UNTRANSLATED continue_with_more_information: 'For added security, please complete below account details.', // UNTRANSLATED }, - profile: { - title: 'Account Settings', // UNTRANSLATED - description: - 'Change your account settings and manage your personal information here to ensure your account security.', // UNTRANSLATED - settings: { - title: 'PROFILE SETTINGS', // UNTRANSLATED - profile_information: 'Profile Information', // UNTRANSLATED - avatar: 'Avatar', // UNTRANSLATED - name: 'Name', // UNTRANSLATED - username: 'Username', // UNTRANSLATED - }, - password: { - title: 'PASSWORD', // UNTRANSLATED - reset_password: 'Reset Password', // UNTRANSLATED - reset_password_sc: 'Reset password', // UNTRANSLATED - }, - link_account: { - title: 'LINK ACCOUNT', // UNTRANSLATED - email_phone_sign_in: 'Email / Phone Sign-In', // UNTRANSLATED - email: 'Email', // UNTRANSLATED - phone: 'Phone', // UNTRANSLATED - phone_sc: 'Phone number', // UNTRANSLATED - social: 'Social Sign-In', // UNTRANSLATED - social_sc: 'Social accounts', // UNTRANSLATED - }, - not_set: 'Not set', // UNTRANSLATED - edit: 'Edit', // UNTRANSLATED - change: 'Change', // UNTRANSLATED - link: 'Link', // UNTRANSLATED - unlink: 'Unlink', // UNTRANSLATED - }, error: { username_password_mismatch: "Le nom d'utilisateur et le mot de passe ne correspondent pas", username_required: "Le nom d'utilisateur est requis", diff --git a/packages/phrases-ui/src/locales/ko.ts b/packages/phrases-ui/src/locales/ko.ts index fc8268398..ff9f39909 100644 --- a/packages/phrases-ui/src/locales/ko.ts +++ b/packages/phrases-ui/src/locales/ko.ts @@ -91,36 +91,6 @@ const translation = { '더 나은 보안을 위해 이메일 또는 휴대전화번호를 연동해 주세요.', continue_with_more_information: '더 나은 보안을 위해 아래 자세한 내용을 따라 주세요.', }, - profile: { - title: '계정 설정', - description: '계정 보안을 지키기 위해 여기서 계정 설정을 변경하고 개인 정보를 관리하세요.', - settings: { - title: '프로필 설정', - profile_information: '프로필 정보', - avatar: '아바타', - name: '이름', - username: '사용자명', - }, - password: { - title: '비밀번호', - reset_password: '비밀번호 초기화', - reset_password_sc: '비밀번호 초기화', - }, - link_account: { - title: '계정 연동', - email_phone_sign_in: '이메일/휴대전화 로그인', - email: '이메일', - phone: '휴대전화', - phone_sc: '휴대전화번호', - social: '소셜 로그인', - social_sc: '소셜 계정', - }, - not_set: '설정 안 됨', - edit: '수정', - change: '변경', - link: '연동', - unlink: '연동 해제', - }, error: { username_password_mismatch: '사용자 이름 또는 비밀번호가 일치하지 않아요.', username_required: '사용자 이름은 필수예요.', diff --git a/packages/phrases-ui/src/locales/pt-br.ts b/packages/phrases-ui/src/locales/pt-br.ts index 1e82cb27f..b0d2e9271 100644 --- a/packages/phrases-ui/src/locales/pt-br.ts +++ b/packages/phrases-ui/src/locales/pt-br.ts @@ -94,37 +94,6 @@ const translation = { 'Para maior segurança, vincule seu e-mail ou telefone à conta.', continue_with_more_information: 'Para maior segurança, preencha os detalhes da conta abaixo.', }, - profile: { - title: 'Account Settings', // UNTRANSLATED - description: - 'Change your account settings and manage your personal information here to ensure your account security.', // UNTRANSLATED - settings: { - title: 'PROFILE SETTINGS', // UNTRANSLATED - profile_information: 'Profile Information', // UNTRANSLATED - avatar: 'Avatar', // UNTRANSLATED - name: 'Name', // UNTRANSLATED - username: 'Username', // UNTRANSLATED - }, - password: { - title: 'PASSWORD', // UNTRANSLATED - reset_password: 'Reset Password', // UNTRANSLATED - reset_password_sc: 'Reset password', // UNTRANSLATED - }, - link_account: { - title: 'LINK ACCOUNT', // UNTRANSLATED - email_phone_sign_in: 'Email / Phone Sign-In', // UNTRANSLATED - email: 'Email', // UNTRANSLATED - phone: 'Phone', // UNTRANSLATED - phone_sc: 'Phone number', // UNTRANSLATED - social: 'Social Sign-In', // UNTRANSLATED - social_sc: 'Social accounts', // UNTRANSLATED - }, - not_set: 'Not set', // UNTRANSLATED - edit: 'Edit', // UNTRANSLATED - change: 'Change', // UNTRANSLATED - link: 'Link', // UNTRANSLATED - unlink: 'Unlink', // UNTRANSLATED - }, error: { username_password_mismatch: 'Usuário e senha não correspondem', username_required: 'Nome de usuário é obrigatório', diff --git a/packages/phrases-ui/src/locales/pt-pt.ts b/packages/phrases-ui/src/locales/pt-pt.ts index 429f6ff11..fdbc4e8a9 100644 --- a/packages/phrases-ui/src/locales/pt-pt.ts +++ b/packages/phrases-ui/src/locales/pt-pt.ts @@ -92,37 +92,6 @@ const translation = { 'For added security, please link your email or phone with the account.', // UNTRANSLATED continue_with_more_information: 'For added security, please complete below account details.', // UNTRANSLATED }, - profile: { - title: 'Account Settings', // UNTRANSLATED - description: - 'Change your account settings and manage your personal information here to ensure your account security.', // UNTRANSLATED - settings: { - title: 'PROFILE SETTINGS', // UNTRANSLATED - profile_information: 'Profile Information', // UNTRANSLATED - avatar: 'Avatar', // UNTRANSLATED - name: 'Name', // UNTRANSLATED - username: 'Username', // UNTRANSLATED - }, - password: { - title: 'PASSWORD', // UNTRANSLATED - reset_password: 'Reset Password', // UNTRANSLATED - reset_password_sc: 'Reset password', // UNTRANSLATED - }, - link_account: { - title: 'LINK ACCOUNT', // UNTRANSLATED - email_phone_sign_in: 'Email / Phone Sign-In', // UNTRANSLATED - email: 'Email', // UNTRANSLATED - phone: 'Phone', // UNTRANSLATED - phone_sc: 'Phone number', // UNTRANSLATED - social: 'Social Sign-In', // UNTRANSLATED - social_sc: 'Social accounts', // UNTRANSLATED - }, - not_set: 'Not set', // UNTRANSLATED - edit: 'Edit', // UNTRANSLATED - change: 'Change', // UNTRANSLATED - link: 'Link', // UNTRANSLATED - unlink: 'Unlink', // UNTRANSLATED - }, error: { username_password_mismatch: 'O Utilizador e a password não correspondem', username_required: 'Utilizador necessário', diff --git a/packages/phrases-ui/src/locales/tr-tr.ts b/packages/phrases-ui/src/locales/tr-tr.ts index 36dc4368f..ccbfa4964 100644 --- a/packages/phrases-ui/src/locales/tr-tr.ts +++ b/packages/phrases-ui/src/locales/tr-tr.ts @@ -93,37 +93,6 @@ const translation = { 'For added security, please link your email or phone with the account.', // UNTRANSLATED continue_with_more_information: 'For added security, please complete below account details.', // UNTRANSLATED }, - profile: { - title: 'Account Settings', // UNTRANSLATED - description: - 'Change your account settings and manage your personal information here to ensure your account security.', // UNTRANSLATED - settings: { - title: 'PROFILE SETTINGS', // UNTRANSLATED - profile_information: 'Profile Information', // UNTRANSLATED - avatar: 'Avatar', // UNTRANSLATED - name: 'Name', // UNTRANSLATED - username: 'Username', // UNTRANSLATED - }, - password: { - title: 'PASSWORD', // UNTRANSLATED - reset_password: 'Reset Password', // UNTRANSLATED - reset_password_sc: 'Reset password', // UNTRANSLATED - }, - link_account: { - title: 'LINK ACCOUNT', // UNTRANSLATED - email_phone_sign_in: 'Email / Phone Sign-In', // UNTRANSLATED - email: 'Email', // UNTRANSLATED - phone: 'Phone', // UNTRANSLATED - phone_sc: 'Phone number', // UNTRANSLATED - social: 'Social Sign-In', // UNTRANSLATED - social_sc: 'Social accounts', // UNTRANSLATED - }, - not_set: 'Not set', // UNTRANSLATED - edit: 'Edit', // UNTRANSLATED - change: 'Change', // UNTRANSLATED - link: 'Link', // UNTRANSLATED - unlink: 'Unlink', // UNTRANSLATED - }, error: { username_password_mismatch: 'Kullanıcı adı ve şifre eşleşmiyor.', username_required: 'Kullanıcı adı gerekli.', diff --git a/packages/phrases-ui/src/locales/zh-cn.ts b/packages/phrases-ui/src/locales/zh-cn.ts index fcf787d59..78d13e86e 100644 --- a/packages/phrases-ui/src/locales/zh-cn.ts +++ b/packages/phrases-ui/src/locales/zh-cn.ts @@ -88,37 +88,6 @@ const translation = { link_email_or_phone_description: '绑定邮箱或手机号以保障您的账号安全', continue_with_more_information: '为保障您的账号安全,需要您补充以下信息。', }, - profile: { - title: 'Account Settings', // UNTRANSLATED - description: - 'Change your account settings and manage your personal information here to ensure your account security.', // UNTRANSLATED - settings: { - title: 'PROFILE SETTINGS', // UNTRANSLATED - profile_information: 'Profile Information', // UNTRANSLATED - avatar: 'Avatar', // UNTRANSLATED - name: 'Name', // UNTRANSLATED - username: 'Username', // UNTRANSLATED - }, - password: { - title: 'PASSWORD', // UNTRANSLATED - reset_password: 'Reset Password', // UNTRANSLATED - reset_password_sc: 'Reset password', // UNTRANSLATED - }, - link_account: { - title: 'LINK ACCOUNT', // UNTRANSLATED - email_phone_sign_in: 'Email / Phone Sign-In', // UNTRANSLATED - email: 'Email', // UNTRANSLATED - phone: 'Phone', // UNTRANSLATED - phone_sc: 'Phone number', // UNTRANSLATED - social: 'Social Sign-In', // UNTRANSLATED - social_sc: 'Social accounts', // UNTRANSLATED - }, - not_set: 'Not set', // UNTRANSLATED - edit: 'Edit', // UNTRANSLATED - change: 'Change', // UNTRANSLATED - link: 'Link', // UNTRANSLATED - unlink: 'Unlink', // UNTRANSLATED - }, error: { username_password_mismatch: '用户名和密码不匹配', username_required: '用户名必填', From fa57680b5533bb172745799371a1ed7b35e1a5b9 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Fri, 10 Feb 2023 10:25:34 +0800 Subject: [PATCH 11/34] feat(ui): optimize smart input field (#3046) --- .../InputFields/PasswordInputField/index.tsx | 17 +++---- .../SmartInputField/index.test.tsx | 1 + .../InputFields/SmartInputField/index.tsx | 33 +++++++++---- .../InputFields/SmartInputField/utils.test.ts | 49 +++++++++++++++++++ .../InputFields/SmartInputField/utils.ts | 41 ++++++++++++++++ packages/ui/src/hooks/use-update-effect.ts | 26 ---------- 6 files changed, 121 insertions(+), 46 deletions(-) create mode 100644 packages/ui/src/components/InputFields/SmartInputField/utils.test.ts create mode 100644 packages/ui/src/components/InputFields/SmartInputField/utils.ts delete mode 100644 packages/ui/src/hooks/use-update-effect.ts diff --git a/packages/ui/src/components/InputFields/PasswordInputField/index.tsx b/packages/ui/src/components/InputFields/PasswordInputField/index.tsx index e1cfe3533..e38c197d2 100644 --- a/packages/ui/src/components/InputFields/PasswordInputField/index.tsx +++ b/packages/ui/src/components/InputFields/PasswordInputField/index.tsx @@ -6,7 +6,6 @@ import PasswordHideIcon from '@/assets/icons/password-hide-icon.svg'; import PasswordShowIcon from '@/assets/icons/password-show-icon.svg'; import IconButton from '@/components/Button/IconButton'; import useToggle from '@/hooks/use-toggle'; -import useUpdateEffect from '@/hooks/use-update-effect'; import InputField from '../InputField'; import type { Props as InputFieldProps } from '../InputField'; @@ -19,21 +18,17 @@ const PasswordInputField = (props: Props, forwardRef: Ref innerRef.current); - // Refocus and move cursor to the end of the input field after password visibility is toggled - useUpdateEffect(() => { - if (innerRef.current) { - const { length } = innerRef.current.value; - innerRef.current.focus(); - innerRef.current.setSelectionRange(length, length); - } - }, [showPassword]); - return ( + { + event.preventDefault(); + }} + onClick={toggleShowPassword} + > {showPassword ? : } } diff --git a/packages/ui/src/components/InputFields/SmartInputField/index.test.tsx b/packages/ui/src/components/InputFields/SmartInputField/index.test.tsx index ab5f71d53..e57f1304c 100644 --- a/packages/ui/src/components/InputFields/SmartInputField/index.test.tsx +++ b/packages/ui/src/components/InputFields/SmartInputField/index.test.tsx @@ -8,6 +8,7 @@ import SmartInputField from '.'; jest.mock('i18next', () => ({ language: 'en', + t: (key: string) => key, })); describe('SmartInputField Component', () => { diff --git a/packages/ui/src/components/InputFields/SmartInputField/index.tsx b/packages/ui/src/components/InputFields/SmartInputField/index.tsx index bfc207028..06ead1dec 100644 --- a/packages/ui/src/components/InputFields/SmartInputField/index.tsx +++ b/packages/ui/src/components/InputFields/SmartInputField/index.tsx @@ -1,7 +1,8 @@ import { SignInIdentifier } from '@logto/schemas'; +import type { Nullable } from '@silverhand/essentials'; import { conditional } from '@silverhand/essentials'; -import type { ForwardedRef, HTMLProps } from 'react'; -import { forwardRef } from 'react'; +import type { HTMLProps, Ref } from 'react'; +import { useImperativeHandle, useRef, forwardRef } from 'react'; import ClearIcon from '@/assets/icons/clear-icon.svg'; import IconButton from '@/components/Button/IconButton'; @@ -12,6 +13,7 @@ import AnimatedPrefix from './AnimatedPrefix'; import CountryCodeSelector from './CountryCodeSelector'; import type { EnabledIdentifierTypes, IdentifierInputType } from './use-smart-input-field'; import useSmartInputField from './use-smart-input-field'; +import { getInputHtmlProps } from './utils'; export type { IdentifierInputType, EnabledIdentifierTypes } from './use-smart-input-field'; @@ -30,14 +32,16 @@ const SmartInputField = ( { value, onChange, - type = 'text', currentType = SignInIdentifier.Username, enabledTypes = [currentType], onTypeChange, ...rest }: Props, - ref: ForwardedRef + ref: Ref> ) => { + const innerRef = useRef(null); + useImperativeHandle(ref, () => innerRef.current); + const { countryCode, onCountryCodeChange, inputValue, onInputValueChange, onInputValueClear } = useSmartInputField({ onChange, @@ -51,20 +55,31 @@ const SmartInputField = ( return ( - + { + onCountryCodeChange(event); + innerRef.current?.focus(); + }} + /> ) )} suffix={ - + { + event.preventDefault(); + }} + onClick={onInputValueClear} + > } diff --git a/packages/ui/src/components/InputFields/SmartInputField/utils.test.ts b/packages/ui/src/components/InputFields/SmartInputField/utils.test.ts new file mode 100644 index 000000000..fdf2c101e --- /dev/null +++ b/packages/ui/src/components/InputFields/SmartInputField/utils.test.ts @@ -0,0 +1,49 @@ +import { SignInIdentifier } from '@logto/schemas'; + +import { getInputHtmlProps } from './utils'; + +jest.mock('i18next', () => ({ + t: (key: string) => key, +})); + +describe('Smart Input Field Util Methods', () => { + const enabledTypes = [SignInIdentifier.Username, SignInIdentifier.Email, SignInIdentifier.Phone]; + + describe('getInputHtmlProps', () => { + it('Should return correct html props for phone', () => { + const props = getInputHtmlProps(SignInIdentifier.Phone, [SignInIdentifier.Phone]); + expect(props.type).toBe('tel'); + expect(props.pattern).toBe('[0-9]*'); + expect(props.inputMode).toBe('numeric'); + expect(props.placeholder).toBe('input.phone_number'); + }); + + it('Should return correct html props for email', () => { + const props = getInputHtmlProps(SignInIdentifier.Email, [SignInIdentifier.Email]); + expect(props.type).toBe('email'); + expect(props.inputMode).toBe('email'); + expect(props.placeholder).toBe('input.email'); + }); + + it('Should return correct html props for username', () => { + const props = getInputHtmlProps(SignInIdentifier.Username, [SignInIdentifier.Username]); + expect(props.type).toBe('text'); + expect(props.placeholder).toBe('input.username'); + }); + + it('Should return correct html props for username email or phone', () => { + const props = getInputHtmlProps(SignInIdentifier.Username, enabledTypes); + expect(props.type).toBe('text'); + expect(props.placeholder).toBe('input.username / input.email / input.phone_number'); + }); + + it('Should return correct html props for email or phone', () => { + const props = getInputHtmlProps(SignInIdentifier.Email, [ + SignInIdentifier.Email, + SignInIdentifier.Phone, + ]); + expect(props.type).toBe('text'); + expect(props.placeholder).toBe('input.email / input.phone_number'); + }); + }); +}); diff --git a/packages/ui/src/components/InputFields/SmartInputField/utils.ts b/packages/ui/src/components/InputFields/SmartInputField/utils.ts new file mode 100644 index 000000000..d067553e5 --- /dev/null +++ b/packages/ui/src/components/InputFields/SmartInputField/utils.ts @@ -0,0 +1,41 @@ +import { SignInIdentifier } from '@logto/schemas'; +import i18next from 'i18next'; +import type { HTMLProps } from 'react'; +import type { TFuncKey } from 'react-i18next'; + +import type { IdentifierInputType, EnabledIdentifierTypes } from './use-smart-input-field'; + +const identifierInputPlaceholderMap: { [K in IdentifierInputType]: TFuncKey } = { + [SignInIdentifier.Phone]: 'input.phone_number', + [SignInIdentifier.Email]: 'input.email', + [SignInIdentifier.Username]: 'input.username', +}; + +export const getInputHtmlProps = ( + currentType: IdentifierInputType, + enabledTypes: EnabledIdentifierTypes +): Pick, 'type' | 'pattern' | 'inputMode' | 'placeholder'> => { + if (currentType === SignInIdentifier.Phone && enabledTypes.length === 1) { + return { + type: 'tel', + pattern: '[0-9]*', + inputMode: 'numeric', + placeholder: i18next.t<'translation', TFuncKey>('input.phone_number'), + }; + } + + if (currentType === SignInIdentifier.Email && enabledTypes.length === 1) { + return { + type: 'email', + inputMode: 'email', + placeholder: i18next.t<'translation', TFuncKey>('input.email'), + }; + } + + return { + type: 'text', + placeholder: enabledTypes + .map((type) => i18next.t<'translation', TFuncKey>(identifierInputPlaceholderMap[type])) + .join(' / '), + }; +}; diff --git a/packages/ui/src/hooks/use-update-effect.ts b/packages/ui/src/hooks/use-update-effect.ts deleted file mode 100644 index ba1d5b23b..000000000 --- a/packages/ui/src/hooks/use-update-effect.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { DependencyList, EffectCallback } from 'react'; -import { useEffect, useRef } from 'react'; - -const useUpdateEffect = (effect: EffectCallback, dependencies: DependencyList | undefined = []) => { - const isMounted = useRef(false); - - useEffect(() => { - return () => { - // eslint-disable-next-line @silverhand/fp/no-mutation - isMounted.current = false; - }; - }, []); - - useEffect(() => { - if (!isMounted.current) { - // eslint-disable-next-line @silverhand/fp/no-mutation - isMounted.current = true; - - return; - } - - return effect(); - }, [effect, ...dependencies]); -}; - -export default useUpdateEffect; From 98eb15e2057531776595ee2738ac966738854c01 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Fri, 10 Feb 2023 11:26:47 +0800 Subject: [PATCH 12/34] refactor(core)!: merge SAML assertion handler API to authn API (#3082) --- packages/core/src/routes/authn.test.ts | 76 +++++++++++++++- packages/core/src/routes/authn.ts | 66 +++++++++++++- packages/core/src/routes/init.ts | 2 - .../src/routes/saml-assertion-handler.test.ts | 87 ------------------- .../core/src/routes/saml-assertion-handler.ts | 72 --------------- 5 files changed, 138 insertions(+), 165 deletions(-) delete mode 100644 packages/core/src/routes/saml-assertion-handler.test.ts delete mode 100644 packages/core/src/routes/saml-assertion-handler.ts diff --git a/packages/core/src/routes/authn.test.ts b/packages/core/src/routes/authn.test.ts index 8811545b8..6b1c0759c 100644 --- a/packages/core/src/routes/authn.test.ts +++ b/packages/core/src/routes/authn.test.ts @@ -1,10 +1,14 @@ +import { ConnectorType } from '@logto/connector-kit'; import { pickDefault, createMockUtils } from '@logto/shared/esm'; import { mockRole } from '#src/__mocks__/index.js'; import RequestError from '#src/errors/RequestError/index.js'; import Libraries from '#src/tenants/Libraries.js'; +import { createMockProvider } from '#src/test-utils/oidc-provider.js'; import { MockTenant } from '#src/test-utils/tenant.js'; +import { mockConnector, mockMetadata, mockLogtoConnector } from '../__mocks__/connector.js'; + const { jest } = import.meta; const { mockEsmWithActual } = createMockUtils(jest); @@ -14,12 +18,52 @@ const { verifyBearerTokenFromRequest } = await mockEsmWithActual( verifyBearerTokenFromRequest: jest.fn(), }) ); +const validateSamlAssertion = jest.fn(); + +const mockSamlLogtoConnector = { + dbEntry: { ...mockConnector, connectorId: 'saml', id: 'saml_connector' }, + metadata: { ...mockMetadata, isStandard: true, id: 'saml', target: 'saml' }, + type: ConnectorType.Social, + ...mockLogtoConnector, + validateSamlAssertion, +}; + +const socialsLibraries = { + getConnector: jest.fn(async (connectorId: string) => { + if (connectorId !== 'saml_connector') { + throw new RequestError({ + code: 'entity.not_found', + connectorId, + status: 404, + }); + } + + return mockSamlLogtoConnector; + }), +}; + +const baseProviderMock = { + params: {}, + jti: 'jti', + client_id: 'client_id', +}; + +// Const samlAssertionHandlerRoutes = await pickDefault(import('./authn/saml.js')); +// const tenantContext = new MockTenant( +// createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)), +// undefined, +// { socials: socialsLibraries } +// ); const usersLibraries = { findUserRoles: jest.fn(async () => [mockRole]), } satisfies Partial; -const tenantContext = new MockTenant(undefined, {}, { users: usersLibraries }); +const tenantContext = new MockTenant( + createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)), + undefined, + { users: usersLibraries, socials: socialsLibraries } +); const { createRequester } = await import('#src/utils/test-utils.js'); const request = createRequester({ anonymousRoutes: await pickDefault(import('#src/routes/authn.js')), @@ -123,3 +167,33 @@ describe('authn route for Hasura', () => { }); }); }); + +describe('authn route for SAML', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('POST /authn/saml/non_saml_connector should throw 404', async () => { + const response = await request.post('/authn/saml/non_saml_connector'); + expect(response.status).toEqual(404); + }); + + it('POST /authn/saml/saml_connector should throw when `RelayState` missing', async () => { + const response = await request.post('/authn/saml/saml_connector').send({ + SAMLResponse: 'saml_response', + }); + expect(response.status).toEqual(500); + }); + + it('POST /authn/saml/saml_connector', async () => { + await request.post('/authn/saml/saml_connector').send({ + SAMLResponse: 'saml_response', + RelayState: 'relay_state', + }); + expect(validateSamlAssertion).toHaveBeenCalledWith( + { body: { RelayState: 'relay_state', SAMLResponse: 'saml_response' } }, + expect.anything(), + expect.anything() + ); + }); +}); diff --git a/packages/core/src/routes/authn.ts b/packages/core/src/routes/authn.ts index 147d78a0e..ce257de5e 100644 --- a/packages/core/src/routes/authn.ts +++ b/packages/core/src/routes/authn.ts @@ -1,21 +1,30 @@ +import type { ConnectorSession } from '@logto/connector-kit'; +import { ConnectorError, ConnectorErrorCodes, ConnectorType } from '@logto/connector-kit'; +import { arbitraryObjectGuard } from '@logto/schemas'; import { z } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; import { verifyBearerTokenFromRequest } from '#src/middleware/koa-auth.js'; import koaGuard from '#src/middleware/koa-guard.js'; import assertThat from '#src/utils/assert-that.js'; +import { + getConnectorSessionResultFromJti, + assignConnectorSessionResultViaJti, +} from '#src/utils/saml-assertion-handler.js'; import type { AnonymousRouter, RouterInitArgs } from './types.js'; /** * Authn stands for authentication. * This router will have a route `/authn` to authenticate tokens with a general manner. - * For now, we only implement the API for Hasura authentication. */ export default function authnRoutes( - ...[router, { envSet, libraries }]: RouterInitArgs + ...[router, { envSet, provider, libraries }]: RouterInitArgs ) { - const { findUserRoles } = libraries.users; + const { + users: { findUserRoles }, + socials: { getConnector }, + } = libraries; router.get( '/authn/hasura', @@ -72,4 +81,55 @@ export default function authnRoutes( return next(); } ); + + // Create an specialized API to handle SAML assertion + router.post( + '/authn/saml/:connectorId', + /** + * The API does not care the type of the SAML assertion request body, simply pass this to + * connector's built-in methods. + */ + koaGuard({ body: arbitraryObjectGuard, params: z.object({ connectorId: z.string().min(1) }) }), + async (ctx, next) => { + const { + params: { connectorId }, + body, + } = ctx.guard; + const connector = await getConnector(connectorId); + assertThat(connector.type === ConnectorType.Social, 'connector.unexpected_type'); + + const samlAssertionGuard = z.object({ SAMLResponse: z.string(), RelayState: z.string() }); + const samlAssertionParseResult = samlAssertionGuard.safeParse(body); + + if (!samlAssertionParseResult.success) { + throw new ConnectorError( + ConnectorErrorCodes.InvalidResponse, + samlAssertionParseResult.error + ); + } + + /** + * Since `RelayState` will be returned with value unchanged, we use it to pass `jti` + * to find the connector session we used to store essential information. + */ + const { RelayState: jti } = samlAssertionParseResult.data; + + const getSession = async () => getConnectorSessionResultFromJti(jti, provider); + const setSession = async (connectorSession: ConnectorSession) => + assignConnectorSessionResultViaJti(jti, provider, connectorSession); + + const { validateSamlAssertion } = connector; + assertThat( + validateSamlAssertion, + new ConnectorError(ConnectorErrorCodes.NotImplemented, { + message: 'Method `validateSamlAssertion()` is not implemented.', + }) + ); + const redirectTo = await validateSamlAssertion({ body }, getSession, setSession); + + ctx.redirect(redirectTo); + + return next(); + } + ); } diff --git a/packages/core/src/routes/init.ts b/packages/core/src/routes/init.ts index a80f0c064..0ea1fe8e4 100644 --- a/packages/core/src/routes/init.ts +++ b/packages/core/src/routes/init.ts @@ -20,7 +20,6 @@ import phraseRoutes from './phrase.js'; import resourceRoutes from './resource.js'; import roleRoutes from './role.js'; import roleScopeRoutes from './role.scope.js'; -import samlAssertionHandlerRoutes from './saml-assertion-handler.js'; import signInExperiencesRoutes from './sign-in-experience/index.js'; import statusRoutes from './status.js'; import swaggerRoutes from './swagger.js'; @@ -50,7 +49,6 @@ const createRouters = (tenant: TenantContext) => { verificationCodeRoutes(managementRouter, tenant); const anonymousRouter: AnonymousRouter = new Router(); - samlAssertionHandlerRoutes(anonymousRouter, tenant); phraseRoutes(anonymousRouter, tenant); wellKnownRoutes(anonymousRouter, tenant); statusRoutes(anonymousRouter, tenant); diff --git a/packages/core/src/routes/saml-assertion-handler.test.ts b/packages/core/src/routes/saml-assertion-handler.test.ts deleted file mode 100644 index 248009827..000000000 --- a/packages/core/src/routes/saml-assertion-handler.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { ConnectorType } from '@logto/connector-kit'; -import { pickDefault } from '@logto/shared/esm'; - -import RequestError from '#src/errors/RequestError/index.js'; -import { createMockProvider } from '#src/test-utils/oidc-provider.js'; -import { MockTenant } from '#src/test-utils/tenant.js'; -import { createRequester } from '#src/utils/test-utils.js'; - -import { mockConnector, mockMetadata, mockLogtoConnector } from '../__mocks__/connector.js'; - -const { jest } = import.meta; - -const validateSamlAssertion = jest.fn(); - -const mockSamlLogtoConnector = { - dbEntry: { ...mockConnector, connectorId: 'saml', id: 'saml_connector' }, - metadata: { ...mockMetadata, isStandard: true, id: 'saml', target: 'saml' }, - type: ConnectorType.Social, - ...mockLogtoConnector, - validateSamlAssertion, -}; - -const socialsLibraries = { - getConnector: jest.fn(async (connectorId: string) => { - if (connectorId !== 'saml_connector') { - throw new RequestError({ - code: 'entity.not_found', - connectorId, - status: 404, - }); - } - - return mockSamlLogtoConnector; - }), -}; - -const baseProviderMock = { - params: {}, - jti: 'jti', - client_id: 'client_id', -}; - -const samlAssertionHandlerRoutes = await pickDefault(import('./saml-assertion-handler.js')); -const tenantContext = new MockTenant( - createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)), - undefined, - { socials: socialsLibraries } -); - -describe('samlAssertionHandlerRoutes', () => { - const assertionHandlerRequest = createRequester({ - anonymousRoutes: samlAssertionHandlerRoutes, - tenantContext, - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('POST /saml-assertion-handler/non_saml_connector should throw 404', async () => { - const response = await assertionHandlerRequest.post( - '/saml-assertion-handler/non_saml_connector' - ); - expect(response.status).toEqual(404); - }); - - it('POST /saml-assertion-handler/saml_connector should throw when `RelayState` missing', async () => { - const response = await assertionHandlerRequest - .post('/saml-assertion-handler/saml_connector') - .send({ - SAMLResponse: 'saml_response', - }); - expect(response.status).toEqual(500); - }); - - it('POST /saml-assertion-handler/saml_connector', async () => { - await assertionHandlerRequest.post('/saml-assertion-handler/saml_connector').send({ - SAMLResponse: 'saml_response', - RelayState: 'relay_state', - }); - expect(validateSamlAssertion).toHaveBeenCalledWith( - { body: { RelayState: 'relay_state', SAMLResponse: 'saml_response' } }, - expect.anything(), - expect.anything() - ); - }); -}); diff --git a/packages/core/src/routes/saml-assertion-handler.ts b/packages/core/src/routes/saml-assertion-handler.ts deleted file mode 100644 index c0ea99cda..000000000 --- a/packages/core/src/routes/saml-assertion-handler.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { ConnectorSession } from '@logto/connector-kit'; -import { ConnectorError, ConnectorErrorCodes, ConnectorType } from '@logto/connector-kit'; -import { arbitraryObjectGuard } from '@logto/schemas'; -import { z } from 'zod'; - -import koaGuard from '#src/middleware/koa-guard.js'; -import assertThat from '#src/utils/assert-that.js'; -import { - getConnectorSessionResultFromJti, - assignConnectorSessionResultViaJti, -} from '#src/utils/saml-assertion-handler.js'; - -import type { AnonymousRouter, RouterInitArgs } from './types.js'; - -export default function samlAssertionHandlerRoutes( - ...[router, { provider, libraries }]: RouterInitArgs -) { - const { - socials: { getConnector }, - } = libraries; - - // Create an specialized API to handle SAML assertion - router.post( - '/saml-assertion-handler/:connectorId', - /** - * The API does not care the type of the SAML assertion request body, simply pass this to - * connector's built-in methods. - */ - koaGuard({ body: arbitraryObjectGuard, params: z.object({ connectorId: z.string().min(1) }) }), - async (ctx, next) => { - const { - params: { connectorId }, - body, - } = ctx.guard; - const connector = await getConnector(connectorId); - assertThat(connector.type === ConnectorType.Social, 'connector.unexpected_type'); - - const samlAssertionGuard = z.object({ SAMLResponse: z.string(), RelayState: z.string() }); - const samlAssertionParseResult = samlAssertionGuard.safeParse(body); - - if (!samlAssertionParseResult.success) { - throw new ConnectorError( - ConnectorErrorCodes.InvalidResponse, - samlAssertionParseResult.error - ); - } - - /** - * Since `RelayState` will be returned with value unchanged, we use it to pass `jti` - * to find the connector session we used to store essential information. - */ - const { RelayState: jti } = samlAssertionParseResult.data; - - const getSession = async () => getConnectorSessionResultFromJti(jti, provider); - const setSession = async (connectorSession: ConnectorSession) => - assignConnectorSessionResultViaJti(jti, provider, connectorSession); - - const { validateSamlAssertion } = connector; - assertThat( - validateSamlAssertion, - new ConnectorError(ConnectorErrorCodes.NotImplemented, { - message: 'Method `validateSamlAssertion()` is not implemented.', - }) - ); - const redirectTo = await validateSamlAssertion({ body }, getSession, setSession); - - ctx.redirect(redirectTo); - - return next(); - } - ); -} From 04ee73e94367e0809e85b9c5d925447762981b13 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Fri, 10 Feb 2023 11:38:41 +0800 Subject: [PATCH 13/34] refactor(console): multi-card selector (#3089) --- .../components/CardSelector/CardSelector.tsx | 20 +++++++++++++ .../MultiCardSelector/index.module.scss | 23 +++++++++++++- .../MultiCardSelector/index.tsx | 26 +++++++--------- .../components/CardSelector/index.tsx | 30 ++----------------- .../components/CardSelector/types.ts | 8 +++++ 5 files changed, 64 insertions(+), 43 deletions(-) create mode 100644 packages/console/src/pages/CloudPreview/components/CardSelector/CardSelector.tsx rename packages/console/src/pages/CloudPreview/components/{ => CardSelector}/MultiCardSelector/index.module.scss (62%) rename packages/console/src/pages/CloudPreview/components/{ => CardSelector}/MultiCardSelector/index.tsx (63%) create mode 100644 packages/console/src/pages/CloudPreview/components/CardSelector/types.ts diff --git a/packages/console/src/pages/CloudPreview/components/CardSelector/CardSelector.tsx b/packages/console/src/pages/CloudPreview/components/CardSelector/CardSelector.tsx new file mode 100644 index 000000000..e33eee5de --- /dev/null +++ b/packages/console/src/pages/CloudPreview/components/CardSelector/CardSelector.tsx @@ -0,0 +1,20 @@ +import RadioGroup, { Radio } from '@/components/RadioGroup'; + +import type { Option } from './types'; + +type Props = { + name: string; + value: string; + options: Option[]; + onChange: (value: string) => void; +}; + +const CardSelector = ({ name, value, options, onChange }: Props) => ( + + {options.map(({ value: optionValue, title, icon }) => ( + + ))} + +); + +export default CardSelector; diff --git a/packages/console/src/pages/CloudPreview/components/MultiCardSelector/index.module.scss b/packages/console/src/pages/CloudPreview/components/CardSelector/MultiCardSelector/index.module.scss similarity index 62% rename from packages/console/src/pages/CloudPreview/components/MultiCardSelector/index.module.scss rename to packages/console/src/pages/CloudPreview/components/CardSelector/MultiCardSelector/index.module.scss index ab4f6dba9..59d4d8964 100644 --- a/packages/console/src/pages/CloudPreview/components/MultiCardSelector/index.module.scss +++ b/packages/console/src/pages/CloudPreview/components/CardSelector/MultiCardSelector/index.module.scss @@ -11,19 +11,40 @@ border-radius: 12px; min-height: 80px; padding: _.unit(5); - font: var(--font-body-2); + font: var(--font-label-2); cursor: pointer; user-select: none; background-color: var(--color-layer-1); color: var(--color-text); + display: flex; + align-items: center; + + .icon { + color: var(--color-text-secondary); + margin-right: _.unit(4); + vertical-align: middle; + + > svg { + display: block; + } + } &.selected { border-color: var(--color-primary); color: var(--color-text-link); + + .icon { + color: var(--color-primary); + } } &:hover { background-color: var(--color-hover-variant); color: var(--color-text-link); + + .icon { + color: var(--color-primary); + } } } + diff --git a/packages/console/src/pages/CloudPreview/components/MultiCardSelector/index.tsx b/packages/console/src/pages/CloudPreview/components/CardSelector/MultiCardSelector/index.tsx similarity index 63% rename from packages/console/src/pages/CloudPreview/components/MultiCardSelector/index.tsx rename to packages/console/src/pages/CloudPreview/components/CardSelector/MultiCardSelector/index.tsx index b2828d88a..a348457c0 100644 --- a/packages/console/src/pages/CloudPreview/components/MultiCardSelector/index.tsx +++ b/packages/console/src/pages/CloudPreview/components/CardSelector/MultiCardSelector/index.tsx @@ -1,15 +1,11 @@ import classNames from 'classnames'; -import type { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; import { onKeyDownHandler } from '@/utilities/a11y'; +import type { Option } from '../types'; import * as styles from './index.module.scss'; -type Option = { - title: ReactNode; - value: string; -}; - type Props = { options: Option[]; value: string[]; @@ -17,6 +13,8 @@ type Props = { }; const MultiCardSelector = ({ options, value: selectedValues, onChange }: Props) => { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const onToggle = (value: string) => { onChange( selectedValues.includes(value) @@ -27,23 +25,21 @@ const MultiCardSelector = ({ options, value: selectedValues, onChange }: Props) return (
- {options.map((option) => ( + {options.map(({ icon, title, value }) => (
{ - onToggle(option.value); + onToggle(value); }} onKeyDown={onKeyDownHandler(() => { - onToggle(option.value); + onToggle(value); })} > - {option.title} + {icon && {icon}} + {t(title)}
))}
diff --git a/packages/console/src/pages/CloudPreview/components/CardSelector/index.tsx b/packages/console/src/pages/CloudPreview/components/CardSelector/index.tsx index 469e94ac1..97ce741b7 100644 --- a/packages/console/src/pages/CloudPreview/components/CardSelector/index.tsx +++ b/packages/console/src/pages/CloudPreview/components/CardSelector/index.tsx @@ -1,27 +1,3 @@ -import type { AdminConsoleKey } from '@logto/phrases'; -import type { ReactNode } from 'react'; - -import RadioGroup, { Radio } from '@/components/RadioGroup'; - -export type Option = { - icon?: ReactNode; - title: AdminConsoleKey; - value: string; -}; - -type Props = { - name: string; - value: string; - options: Option[]; - onChange: (value: string) => void; -}; - -const CardSelector = ({ name, value, options, onChange }: Props) => ( - - {options.map(({ value: optionValue, title, icon }) => ( - - ))} - -); - -export default CardSelector; +export type { Option } from './types'; +export { default as CardSelector } from './CardSelector'; +export { default as MultiCardSelector } from './MultiCardSelector'; diff --git a/packages/console/src/pages/CloudPreview/components/CardSelector/types.ts b/packages/console/src/pages/CloudPreview/components/CardSelector/types.ts new file mode 100644 index 000000000..5d4ab6360 --- /dev/null +++ b/packages/console/src/pages/CloudPreview/components/CardSelector/types.ts @@ -0,0 +1,8 @@ +import type { AdminConsoleKey } from '@logto/phrases'; +import type { ReactNode } from 'react'; + +export type Option = { + icon?: ReactNode; + title: AdminConsoleKey; + value: string; +}; From f5116b30e814381e06fdf0de8b022c281f1f46e7 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Fri, 10 Feb 2023 12:27:32 +0800 Subject: [PATCH 14/34] fix(phrases): update sync profile tip (#3078) --- .../src/locales/de/translation/admin-console/connectors.ts | 3 ++- .../src/locales/en/translation/admin-console/connectors.ts | 3 ++- .../src/locales/fr/translation/admin-console/connectors.ts | 3 ++- .../src/locales/pt-br/translation/admin-console/connectors.ts | 3 ++- .../src/locales/pt-pt/translation/admin-console/connectors.ts | 3 ++- .../src/locales/tr-tr/translation/admin-console/connectors.ts | 3 ++- .../src/locales/zh-cn/translation/admin-console/connectors.ts | 2 +- 7 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/phrases/src/locales/de/translation/admin-console/connectors.ts b/packages/phrases/src/locales/de/translation/admin-console/connectors.ts index 8087aec2d..b0a6223ec 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/connectors.ts @@ -53,7 +53,8 @@ const connectors = { sync_profile: 'Sync profile information', // UNTRANSLATED sync_profile_only_at_sign_up: 'Only sync at sign-up', // UNTRANSLATED sync_profile_each_sign_in: 'Always do a sync at each sign-in', // UNTRANSLATED - sync_profile_tip: 'Sync basic user profile, e.g. name and avatar.', // UNTRANSLATED + sync_profile_tip: + "Sync the basic profile from the social provider, such as users' names and their avatars.", // UNTRANSLATED }, platform: { universal: 'Universal', diff --git a/packages/phrases/src/locales/en/translation/admin-console/connectors.ts b/packages/phrases/src/locales/en/translation/admin-console/connectors.ts index 8af0a79dc..d3b762441 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/connectors.ts @@ -53,7 +53,8 @@ const connectors = { sync_profile: 'Sync profile information', sync_profile_only_at_sign_up: 'Only sync at sign-up', sync_profile_each_sign_in: 'Always do a sync at each sign-in', - sync_profile_tip: 'Sync basic user profile, e.g. name and avatar.', + sync_profile_tip: + "Sync the basic profile from the social provider, such as users' names and their avatars.", }, platform: { universal: 'Universal', diff --git a/packages/phrases/src/locales/fr/translation/admin-console/connectors.ts b/packages/phrases/src/locales/fr/translation/admin-console/connectors.ts index 885a53bba..974f692f8 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/connectors.ts @@ -54,7 +54,8 @@ const connectors = { sync_profile: 'Sync profile information', // UNTRANSLATED sync_profile_only_at_sign_up: 'Only sync at sign-up', // UNTRANSLATED sync_profile_each_sign_in: 'Always do a sync at each sign-in', // UNTRANSLATED - sync_profile_tip: 'Sync basic user profile, e.g. name and avatar.', // UNTRANSLATED + sync_profile_tip: + "Sync the basic profile from the social provider, such as users' names and their avatars.", // UNTRANSLATED }, platform: { universal: 'Universel', diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/connectors.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/connectors.ts index 6397e421b..6213bd310 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/connectors.ts @@ -51,7 +51,8 @@ const connectors = { sync_profile: 'Sincronizar informações de perfil', sync_profile_only_at_sign_up: 'Sincronizar apenas no registro', sync_profile_each_sign_in: 'Sempre sincronizar a cada login', - sync_profile_tip: 'Sync basic user profile, e.g. name and avatar.', // UNTRANSLATED + sync_profile_tip: + "Sync the basic profile from the social provider, such as users' names and their avatars.", // UNTRANSLATED }, platform: { universal: 'Universal', diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/connectors.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/connectors.ts index 171421bb5..d159291f4 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/connectors.ts @@ -53,7 +53,8 @@ const connectors = { sync_profile: 'Sync profile information', // UNTRANSLATED sync_profile_only_at_sign_up: 'Only sync at sign-up', // UNTRANSLATED sync_profile_each_sign_in: 'Always do a sync at each sign-in', // UNTRANSLATED - sync_profile_tip: 'Sync basic user profile, e.g. name and avatar.', // UNTRANSLATED + sync_profile_tip: + "Sync the basic profile from the social provider, such as users' names and their avatars.", // UNTRANSLATED }, platform: { universal: 'Universal', diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/connectors.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/connectors.ts index 63cc10a62..3cb1b2014 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/connectors.ts @@ -54,7 +54,8 @@ const connectors = { sync_profile: 'Sync profile information', // UNTRANSLATED sync_profile_only_at_sign_up: 'Only sync at sign-up', // UNTRANSLATED sync_profile_each_sign_in: 'Always do a sync at each sign-in', // UNTRANSLATED - sync_profile_tip: 'Sync basic user profile, e.g. name and avatar.', // UNTRANSLATED + sync_profile_tip: + "Sync the basic profile from the social provider, such as users' names and their avatars.", // UNTRANSLATED }, platform: { universal: 'Evrensel', diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/connectors.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/connectors.ts index 65bac3771..2fd7d8d99 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/connectors.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/connectors.ts @@ -48,7 +48,7 @@ const connectors = { sync_profile: '开启用户资料同步', sync_profile_only_at_sign_up: '首次注册时同步', sync_profile_each_sign_in: '每次登录时同步', - sync_profile_tip: '同步基本用户信息,如昵称,头像等', + sync_profile_tip: '同步用户的用户名、头像等个人资料信息', }, platform: { universal: '通用', From d4743ec1b980544869e0f5b5cd7caa7f650a96db Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Fri, 10 Feb 2023 12:56:05 +0800 Subject: [PATCH 15/34] chore: add web compatibility section in README (#3079) --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 332e77d80..7934f3ad6 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,14 @@ npm init @logto const languages = ['Deutsch', 'English', 'Français', 'Português', '简体中文', 'Türkçe', '한국어']; ``` +## Web compatibility + +Logto uses the [default browserlist config](https://github.com/browserslist/browserslist#full-list) to compile frontend projects, which is: + +``` +> 0.5%, last 2 versions, Firefox ESR, not dead +``` + ## Bug report, feature request, feedback - Our team takes security seriously, especially when it relates to identity. If you find any existing or potential security issues, please do not hesitate to email 🔒 [security@logto.io](mailto:security@logto.io). From 24f2cd20e71255818faeeecb9c530a9b671264cd Mon Sep 17 00:00:00 2001 From: Grayson Adkins Date: Fri, 10 Feb 2023 02:03:53 -0600 Subject: [PATCH 16/34] chore: add Uffizzi demo button in README (#3032) --- README.md | 14 +++++++++---- docker-compose.demo.uffizzi.yml | 37 +++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 docker-compose.demo.uffizzi.yml diff --git a/README.md b/README.md index 7934f3ad6..3e54237bd 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,18 @@ Boringly, we call it "[customer identity access management](https://en.wikipedia - Visit our 🎨 [website](https://logto.io) for a brief introduction if you are new to Logto. - A step-by-step guide is available on 📖 [docs.logto.io](https://docs.logto.io). +### Interactive demo + +[![Uffizzi](https://cdn.uffizzi.com/demo-button.svg)](https://app.uffizzi.com/demo/github.com/logto-io/logto) + +Recommended. Click and wait for a few seconds to start exploring Logto in your own browser! + +[![GitPod](https://raw.githubusercontent.com/gitpod-io/gitpod/30da76375c996109f243491b23e47feefab7217f/components/dashboard/public/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/logto-io/demo) + +If you launch Logto via GitPod, please wait until you see the message like `App is running at https://3001-...gitpod.io` in the terminal, press Cmd (or Ctrl on Windows) and click the URL to continue your Logto journey. + ### Launch Logto -#### Online demo (GitPod) - -[Click here](https://gitpod.io/#https://github.com/logto-io/demo) to launch Logto via GitPod. Once you see the message like `App is running at https://3001-...gitpod.io` in the terminal, press Cmd (or Ctrl) and click the URL to continue your Logto journey. - #### Docker Compose Docker Compose CLI usually comes with [Docker Desktop](https://www.docker.com/products/docker-desktop). diff --git a/docker-compose.demo.uffizzi.yml b/docker-compose.demo.uffizzi.yml new file mode 100644 index 000000000..bca82a9f8 --- /dev/null +++ b/docker-compose.demo.uffizzi.yml @@ -0,0 +1,37 @@ +# This compose file is for demonstration only, do not use in prod. +version: "3.9" + +x-uffizzi: + ingress: + service: app + port: 3001 + +services: + app: + depends_on: + - "postgres" + image: registry.uffizzi.com/logto-io/logto:prerelease + ports: + - 3001:3001 + environment: + TRUST_PROXY_HEADER: 1 + DB_URL: postgres://postgres:p0stgr3s@localhost:5432/logto + deploy: + resources: + limits: + memory: 2000M + entrypoint: /bin/sh + command: + - "-c" + - "npm run cli db seed -- --swe && ENDPOINT=$$UFFIZZI_URL npm start" + + postgres: + image: postgres:14-alpine + user: postgres + environment: + POSTGRES_USER: "postgres" + POSTGRES_PASSWORD: "p0stgr3s" + deploy: + resources: + limits: + memory: 500M From 111e2973c2d677ce19c97cd4b1380d65ac093486 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Fri, 10 Feb 2023 16:59:32 +0800 Subject: [PATCH 17/34] feat(console): connector config form (#3074) --- packages/console/package.json | 1 + .../src/components/Textarea/index.module.scss | 10 +- .../console/src/components/Textarea/index.tsx | 8 +- .../components/ConnectorContent.tsx | 39 ++--- .../components/ConfigForm/index.tsx | 151 ++++++++++++++++++ .../components/ConnectorForm/hooks.tsx | 26 +++ .../components/ConnectorForm/index.tsx | 46 +++--- .../components/ConnectorForm/utils.ts | 56 +++++++ .../Connectors/components/Guide/index.tsx | 27 ++-- .../console/src/pages/Connectors/types.ts | 2 +- packages/core/src/utils/connectors/index.ts | 3 +- packages/toolkit/connector-kit/src/types.ts | 43 ++++- pnpm-lock.yaml | 2 + 13 files changed, 353 insertions(+), 61 deletions(-) create mode 100644 packages/console/src/pages/Connectors/components/ConfigForm/index.tsx create mode 100644 packages/console/src/pages/Connectors/components/ConnectorForm/hooks.tsx create mode 100644 packages/console/src/pages/Connectors/components/ConnectorForm/utils.ts diff --git a/packages/console/package.json b/packages/console/package.json index 7e92f4fa3..eb95cb0b0 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -19,6 +19,7 @@ }, "devDependencies": { "@fontsource/roboto-mono": "^4.5.7", + "@logto/connector-kit": "workspace:*", "@logto/core-kit": "workspace:*", "@logto/language-kit": "workspace:*", "@logto/phrases": "workspace:*", diff --git a/packages/console/src/components/Textarea/index.module.scss b/packages/console/src/components/Textarea/index.module.scss index 936bb7f3b..6611c4cf4 100644 --- a/packages/console/src/components/Textarea/index.module.scss +++ b/packages/console/src/components/Textarea/index.module.scss @@ -11,6 +11,14 @@ outline-color: var(--color-focused-variant); } + &.error { + border-color: var(--color-error); + + &:focus-within { + outline-color: var(--color-danger-focused); + } + } + textarea { width: 100%; height: 100%; @@ -23,7 +31,7 @@ padding: 0; &::placeholder { - color: var(--color-caption); + color: var(--color-placeholder); } } } diff --git a/packages/console/src/components/Textarea/index.tsx b/packages/console/src/components/Textarea/index.tsx index d18f8037c..6936a600f 100644 --- a/packages/console/src/components/Textarea/index.tsx +++ b/packages/console/src/components/Textarea/index.tsx @@ -6,11 +6,15 @@ import * as styles from './index.module.scss'; type Props = HTMLProps & { className?: string; + hasError?: boolean; }; -const Textarea = ({ className, ...rest }: Props, reference: ForwardedRef) => { +const Textarea = ( + { className, hasError, ...rest }: Props, + reference: ForwardedRef +) => { return ( -
+