forked from baron/baron-sso
feat(i18n): apply ORY bypass whitelist policy and add error-code tests
This commit is contained in:
@@ -80,6 +80,29 @@ type = "Type"
|
||||
|
||||
[msg.userfront.error.whitelist]
|
||||
settings_disabled = "Account settings are currently unavailable."
|
||||
invalid_session = "Your session has expired. Please sign in again."
|
||||
verification_required = "Additional verification is required. Please follow the instructions."
|
||||
recovery_expired = "The recovery link has expired. Please request a new one."
|
||||
recovery_invalid = "The recovery link is invalid."
|
||||
rate_limited = "Too many requests. Please try again later."
|
||||
not_found = "The requested page could not be found."
|
||||
bad_request = "Please check your input."
|
||||
password_or_email_mismatch = "Email or password does not match."
|
||||
|
||||
[msg.userfront.error.ory]
|
||||
access_denied = "The user denied the consent request."
|
||||
consent_required = "Consent is required to continue."
|
||||
interaction_required = "Additional interaction is required. Please try again."
|
||||
invalid_client = "Client authentication failed."
|
||||
invalid_grant = "The authorization grant is invalid or expired."
|
||||
invalid_request = "The request is invalid."
|
||||
invalid_scope = "The requested scope is invalid."
|
||||
login_required = "Login is required."
|
||||
request_forbidden = "The request was forbidden."
|
||||
server_error = "An authentication server error occurred."
|
||||
temporarily_unavailable = "The authentication server is temporarily unavailable."
|
||||
unauthorized_client = "The client is not authorized for this request."
|
||||
unsupported_response_type = "The response type is not supported."
|
||||
|
||||
[msg.userfront.forgot]
|
||||
description = "Description"
|
||||
@@ -96,7 +119,7 @@ link_send_failed = "Link Send Failed"
|
||||
link_sent_email = "Link Sent Email"
|
||||
link_sent_phone = "Link Sent Phone"
|
||||
link_timeout = "Time expired."
|
||||
no_account = "No Account"
|
||||
no_account = "New to Baron?"
|
||||
oidc_failed = "OIDC Failed"
|
||||
qr_expired = "Time expired."
|
||||
qr_init_failed = "QR Init Failed"
|
||||
@@ -297,7 +320,7 @@ save = "Save"
|
||||
search = "Search"
|
||||
show_more = "Show More"
|
||||
language = "Language"
|
||||
language_ko = "Korean"
|
||||
language_ko = "한국어"
|
||||
language_en = "English"
|
||||
theme_dark = "Dark"
|
||||
theme_light = "Light"
|
||||
@@ -534,3 +557,4 @@ verify = "Verify"
|
||||
|
||||
[ui.userfront.signup.success]
|
||||
action = "Action"
|
||||
|
||||
|
||||
@@ -80,6 +80,29 @@ type = "오류 종류: {type}"
|
||||
|
||||
[msg.userfront.error.whitelist]
|
||||
settings_disabled = "현재 계정 설정 화면은 준비 중입니다."
|
||||
invalid_session = "세션이 만료되었습니다. 다시 로그인해 주세요."
|
||||
verification_required = "추가 인증이 필요합니다. 안내에 따라 진행해 주세요."
|
||||
recovery_expired = "재설정 링크가 만료되었습니다. 다시 요청해 주세요."
|
||||
recovery_invalid = "재설정 링크가 유효하지 않습니다."
|
||||
rate_limited = "요청이 많습니다. 잠시 후 다시 시도해 주세요."
|
||||
not_found = "요청한 페이지를 찾을 수 없습니다."
|
||||
bad_request = "입력값을 확인해 주세요."
|
||||
password_or_email_mismatch = "이메일 혹은 비밀번호가 일치하지 않습니다."
|
||||
|
||||
[msg.userfront.error.ory]
|
||||
access_denied = "사용자가 동의를 거부했습니다."
|
||||
consent_required = "앱 접근 동의가 필요합니다."
|
||||
interaction_required = "추가 상호작용이 필요합니다. 다시 시도해 주세요."
|
||||
invalid_client = "클라이언트 인증 정보가 유효하지 않습니다."
|
||||
invalid_grant = "인증 요청이 만료되었거나 유효하지 않습니다."
|
||||
invalid_request = "잘못된 요청입니다."
|
||||
invalid_scope = "요청한 권한 범위가 유효하지 않습니다."
|
||||
login_required = "로그인이 필요합니다."
|
||||
request_forbidden = "요청이 거부되었습니다."
|
||||
server_error = "인증 서버 오류가 발생했습니다."
|
||||
temporarily_unavailable = "인증 서버를 일시적으로 사용할 수 없습니다."
|
||||
unauthorized_client = "해당 클라이언트는 이 요청을 수행할 수 없습니다."
|
||||
unsupported_response_type = "지원하지 않는 응답 타입입니다."
|
||||
|
||||
[msg.userfront.forgot]
|
||||
description = "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다."
|
||||
@@ -298,7 +321,7 @@ search = "검색"
|
||||
show_more = "+ 더보기"
|
||||
language = "언어"
|
||||
language_ko = "한국어"
|
||||
language_en = "영어"
|
||||
language_en = "English"
|
||||
theme_dark = "Dark"
|
||||
theme_light = "Light"
|
||||
theme_toggle = "테마 전환"
|
||||
@@ -534,3 +557,4 @@ verify = "본인인증"
|
||||
|
||||
[ui.userfront.signup.success]
|
||||
action = "로그인하기"
|
||||
|
||||
|
||||
@@ -80,6 +80,29 @@ type = ""
|
||||
|
||||
[msg.userfront.error.whitelist]
|
||||
settings_disabled = ""
|
||||
invalid_session = ""
|
||||
verification_required = ""
|
||||
recovery_expired = ""
|
||||
recovery_invalid = ""
|
||||
rate_limited = ""
|
||||
not_found = ""
|
||||
bad_request = ""
|
||||
password_or_email_mismatch = ""
|
||||
|
||||
[msg.userfront.error.ory]
|
||||
access_denied = ""
|
||||
consent_required = ""
|
||||
interaction_required = ""
|
||||
invalid_client = ""
|
||||
invalid_grant = ""
|
||||
invalid_request = ""
|
||||
invalid_scope = ""
|
||||
login_required = ""
|
||||
request_forbidden = ""
|
||||
server_error = ""
|
||||
temporarily_unavailable = ""
|
||||
unauthorized_client = ""
|
||||
unsupported_response_type = ""
|
||||
|
||||
[msg.userfront.forgot]
|
||||
description = ""
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
const Map<String, String> errorWhitelistMessages = {
|
||||
const Map<String, String> internalErrorWhitelistMessages = {
|
||||
'settings_disabled': '현재 계정 설정 화면은 준비 중입니다.',
|
||||
'invalid_session': '세션이 만료되었습니다. 다시 로그인해 주세요.',
|
||||
'verification_required': '추가 인증이 필요합니다. 안내에 따라 진행해 주세요.',
|
||||
'recovery_expired': '재설정 링크가 만료되었습니다. 다시 요청해 주세요.',
|
||||
'recovery_invalid': '재설정 링크가 유효하지 않습니다.',
|
||||
'consent_required': '앱 접근 동의가 필요합니다.',
|
||||
'rate_limited': '요청이 많습니다. 잠시 후 다시 시도해 주세요.',
|
||||
'not_found': '요청한 페이지를 찾을 수 없습니다.',
|
||||
'bad_request': '입력값을 확인해 주세요.',
|
||||
'password_or_email_mismatch': '이메일 혹은 비밀번호가 일치하지 않습니다.',
|
||||
};
|
||||
|
||||
const Set<String> oryBypassErrorCodes = {
|
||||
'access_denied',
|
||||
'consent_required',
|
||||
'interaction_required',
|
||||
'invalid_client',
|
||||
'invalid_grant',
|
||||
'invalid_request',
|
||||
'invalid_scope',
|
||||
'login_required',
|
||||
'request_forbidden',
|
||||
'server_error',
|
||||
'temporarily_unavailable',
|
||||
'unauthorized_client',
|
||||
'unsupported_response_type',
|
||||
};
|
||||
|
||||
@@ -24,10 +24,13 @@ class ErrorScreen extends StatelessWidget {
|
||||
final isProd = isProdOverride ?? AuthProxyService.isProdEnv;
|
||||
final normalizedCode = (errorCode ?? '').trim();
|
||||
final hasCode = normalizedCode.isNotEmpty;
|
||||
final whitelistFallback = errorWhitelistMessages[normalizedCode];
|
||||
final isWhitelisted = whitelistFallback != null;
|
||||
final internalWhitelistFallback =
|
||||
internalErrorWhitelistMessages[normalizedCode];
|
||||
final isInternalWhitelisted = internalWhitelistFallback != null;
|
||||
final isOryBypass = hasCode && oryBypassErrorCodes.contains(normalizedCode);
|
||||
final isKnownProdCode = hasCode && (isInternalWhitelisted || isOryBypass);
|
||||
final errorType = isProd
|
||||
? (isWhitelisted && hasCode ? normalizedCode : 'unknown_error')
|
||||
? (isKnownProdCode ? normalizedCode : 'unknown_error')
|
||||
: (hasCode ? normalizedCode : 'unknown_error');
|
||||
final title = isProd
|
||||
? tr('msg.userfront.error.title')
|
||||
@@ -40,14 +43,22 @@ class ErrorScreen extends StatelessWidget {
|
||||
'msg.userfront.error.title_generic',
|
||||
));
|
||||
final detail = isProd
|
||||
? (isWhitelisted
|
||||
? (isInternalWhitelisted
|
||||
? tr(
|
||||
'msg.userfront.error.whitelist.$normalizedCode',
|
||||
fallback: whitelistFallback,
|
||||
fallback: internalWhitelistFallback,
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.error.detail_contact',
|
||||
))
|
||||
: (isOryBypass
|
||||
? tr(
|
||||
'msg.userfront.error.ory.$normalizedCode',
|
||||
fallback:
|
||||
(description?.isNotEmpty == true)
|
||||
? description
|
||||
: tr('msg.userfront.error.detail_request'),
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.error.detail_contact',
|
||||
)))
|
||||
: ((description?.isNotEmpty == true)
|
||||
? description!
|
||||
: (hasCode
|
||||
|
||||
@@ -74,7 +74,7 @@ void main() {
|
||||
);
|
||||
final detail = tr(
|
||||
'msg.userfront.error.whitelist.settings_disabled',
|
||||
fallback: errorWhitelistMessages['settings_disabled']!,
|
||||
fallback: internalErrorWhitelistMessages['settings_disabled']!,
|
||||
);
|
||||
final type = tr(
|
||||
'msg.userfront.error.type',
|
||||
@@ -88,6 +88,34 @@ void main() {
|
||||
expect(find.text(type), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('프로덕션은 ORY 코드를 bypass 처리한다', (WidgetTester tester) async {
|
||||
await _pumpErrorScreen(
|
||||
tester,
|
||||
errorCode: 'access_denied',
|
||||
description: '원문 메시지',
|
||||
isProdOverride: true,
|
||||
);
|
||||
|
||||
final title = tr(
|
||||
'msg.userfront.error.title',
|
||||
fallback: '인증 과정에서 오류가 발생했습니다',
|
||||
);
|
||||
final detail = tr(
|
||||
'msg.userfront.error.ory.access_denied',
|
||||
fallback: '사용자가 동의를 거부했습니다.',
|
||||
);
|
||||
final type = tr(
|
||||
'msg.userfront.error.type',
|
||||
fallback: '오류 종류: {{type}}',
|
||||
params: {'type': 'access_denied'},
|
||||
);
|
||||
|
||||
expect(find.text(title), findsOneWidget);
|
||||
expect(find.text(detail), findsOneWidget);
|
||||
expect(find.text('원문 메시지'), findsNothing);
|
||||
expect(find.text(type), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('프로덕션은 비허용 에러를 unknown_error로 처리한다', (WidgetTester tester) async {
|
||||
await _pumpErrorScreen(
|
||||
tester,
|
||||
|
||||
Reference in New Issue
Block a user