forked from baron/baron-sso
접근 제한 UX 구현
This commit is contained in:
@@ -137,6 +137,16 @@ title_generic = "An error occurred."
|
||||
title_with_code = "Error: {code}"
|
||||
type = "Error type: {type}"
|
||||
|
||||
[msg.userfront.error.tenant]
|
||||
account = "Account"
|
||||
account_unknown = "Unknown"
|
||||
detail = "The currently signed-in account cannot access this application."
|
||||
load_failed = "We could not confirm the account details. Please try again."
|
||||
loading = "Loading the current account details."
|
||||
tenant = "Tenant"
|
||||
tenant_unknown = "Unknown"
|
||||
title = "Access restriction details"
|
||||
|
||||
[msg.userfront.error.ory]
|
||||
"$normalizedCode" = "{error}"
|
||||
access_denied = "The user denied the consent request."
|
||||
@@ -506,6 +516,7 @@ windows = "Desktop(Windows)"
|
||||
[ui.userfront.error]
|
||||
go_home = "Go Home"
|
||||
go_login = "Go Login"
|
||||
switch_account = "Sign in with another account"
|
||||
|
||||
[ui.userfront.forgot]
|
||||
heading = "Forgot your password?"
|
||||
|
||||
@@ -78,6 +78,16 @@ title_generic = "오류가 발생했습니다"
|
||||
title_with_code = "오류: {code}"
|
||||
type = "오류 종류: {type}"
|
||||
|
||||
[msg.userfront.error.tenant]
|
||||
account = "계정"
|
||||
account_unknown = "알 수 없음"
|
||||
detail = "현재 로그인된 계정은 이 애플리케이션에 접근할 수 없습니다."
|
||||
load_failed = "계정 정보를 확인하지 못했습니다. 다시 시도해 주세요."
|
||||
loading = "현재 계정 정보를 불러오는 중입니다."
|
||||
tenant = "소속 테넌트"
|
||||
tenant_unknown = "알 수 없음"
|
||||
title = "접근 제한 정보"
|
||||
|
||||
[msg.userfront.forgot]
|
||||
description = "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다."
|
||||
dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다."
|
||||
@@ -190,6 +200,7 @@ windows = "Desktop(Windows)"
|
||||
[ui.userfront.error]
|
||||
go_home = "홈으로 이동"
|
||||
go_login = "로그인으로 이동"
|
||||
switch_account = "다른 계정으로 로그인"
|
||||
|
||||
[ui.userfront.forgot]
|
||||
heading = "비밀번호를 잊으셨나요?"
|
||||
|
||||
@@ -50,6 +50,16 @@ title_generic = ""
|
||||
title_with_code = ""
|
||||
type = ""
|
||||
|
||||
[msg.userfront.error.tenant]
|
||||
account = ""
|
||||
account_unknown = ""
|
||||
detail = ""
|
||||
load_failed = ""
|
||||
loading = ""
|
||||
tenant = ""
|
||||
tenant_unknown = ""
|
||||
title = ""
|
||||
|
||||
[msg.userfront.forgot]
|
||||
description = ""
|
||||
dry_send = ""
|
||||
@@ -162,6 +172,7 @@ windows = ""
|
||||
[ui.userfront.error]
|
||||
go_home = ""
|
||||
go_login = ""
|
||||
switch_account = ""
|
||||
|
||||
[ui.userfront.forgot]
|
||||
heading = ""
|
||||
|
||||
@@ -8,6 +8,7 @@ const Map<String, String> internalErrorWhitelistMessages = {
|
||||
'not_found': '요청한 페이지를 찾을 수 없습니다.',
|
||||
'bad_request': '입력값을 확인해 주세요.',
|
||||
'password_or_email_mismatch': '이메일 혹은 비밀번호가 일치하지 않습니다.',
|
||||
'tenant_not_allowed': '허용되지 않은 테넌트입니다.',
|
||||
};
|
||||
|
||||
const Set<String> oryBypassErrorCodes = {
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../core/constants/error_whitelist.dart';
|
||||
import '../../../core/i18n/locale_utils.dart';
|
||||
import '../../../core/services/auth_proxy_service.dart';
|
||||
import '../../../core/services/logout_service.dart';
|
||||
import '../../../core/widgets/theme_toggle_button.dart';
|
||||
import 'package:userfront/i18n.dart';
|
||||
|
||||
class ErrorScreen extends StatelessWidget {
|
||||
class ErrorScreen extends StatefulWidget {
|
||||
final String? errorId;
|
||||
final String? errorCode;
|
||||
final String? description;
|
||||
final bool? isProdOverride;
|
||||
final Future<Map<String, dynamic>> Function()? sessionProfileLoader;
|
||||
|
||||
const ErrorScreen({
|
||||
super.key,
|
||||
@@ -18,20 +22,124 @@ class ErrorScreen extends StatelessWidget {
|
||||
this.errorCode,
|
||||
this.description,
|
||||
this.isProdOverride,
|
||||
this.sessionProfileLoader,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ErrorScreen> createState() => _ErrorScreenState();
|
||||
}
|
||||
|
||||
class _ErrorScreenState extends State<ErrorScreen> {
|
||||
Map<String, dynamic>? _sessionProfile;
|
||||
bool _isLoadingSessionProfile = false;
|
||||
String? _sessionProfileError;
|
||||
|
||||
bool get _isTenantAccessBlocked =>
|
||||
(widget.errorCode ?? '').trim() == 'tenant_not_allowed';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_isTenantAccessBlocked) {
|
||||
unawaited(_loadSessionProfile());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadSessionProfile() async {
|
||||
if (_isLoadingSessionProfile) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isLoadingSessionProfile = true;
|
||||
_sessionProfileError = null;
|
||||
});
|
||||
try {
|
||||
final loader = widget.sessionProfileLoader ?? AuthProxyService.getMe;
|
||||
final profile = await loader();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_sessionProfile = profile;
|
||||
});
|
||||
} catch (error) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_sessionProfileError = error.toString();
|
||||
});
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoadingSessionProfile = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _extractTenantLabel(Map<String, dynamic>? profile) {
|
||||
if (profile == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
final tenant = profile['tenant'];
|
||||
if (tenant is Map) {
|
||||
final name = tenant['name']?.toString().trim() ?? '';
|
||||
if (name.isNotEmpty) {
|
||||
return name;
|
||||
}
|
||||
final slug = tenant['slug']?.toString().trim() ?? '';
|
||||
if (slug.isNotEmpty) {
|
||||
return slug;
|
||||
}
|
||||
}
|
||||
|
||||
final joinedTenants = profile['joinedTenants'];
|
||||
if (joinedTenants is List) {
|
||||
for (final item in joinedTenants) {
|
||||
if (item is Map) {
|
||||
final name = item['name']?.toString().trim() ?? '';
|
||||
if (name.isNotEmpty) {
|
||||
return name;
|
||||
}
|
||||
final slug = item['slug']?.toString().trim() ?? '';
|
||||
if (slug.isNotEmpty) {
|
||||
return slug;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final companyCode = profile['companyCode']?.toString().trim() ?? '';
|
||||
if (companyCode.isNotEmpty) {
|
||||
return companyCode;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
Future<void> _switchAccount() async {
|
||||
await LogoutService().logout();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
context.go(buildLocalizedSigninPath(Uri.base));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final isProd = isProdOverride ?? AuthProxyService.isProdEnv;
|
||||
final normalizedCode = (errorCode ?? '').trim();
|
||||
final isProd = widget.isProdOverride ?? AuthProxyService.isProdEnv;
|
||||
final normalizedCode = (widget.errorCode ?? '').trim();
|
||||
final hasCode = normalizedCode.isNotEmpty;
|
||||
final internalWhitelistFallback =
|
||||
internalErrorWhitelistMessages[normalizedCode];
|
||||
final isInternalWhitelisted = internalWhitelistFallback != null;
|
||||
final isOryBypass = hasCode && oryBypassErrorCodes.contains(normalizedCode);
|
||||
final isKnownProdCode = hasCode && (isInternalWhitelisted || isOryBypass);
|
||||
final isTenantAccessBlocked = normalizedCode == 'tenant_not_allowed';
|
||||
final errorType = isProd
|
||||
? (isKnownProdCode ? normalizedCode : 'unknown_error')
|
||||
: (hasCode ? normalizedCode : 'unknown_error');
|
||||
@@ -43,44 +151,55 @@ class ErrorScreen extends StatelessWidget {
|
||||
params: {'code': normalizedCode},
|
||||
)
|
||||
: tr('msg.userfront.error.title_generic'));
|
||||
final detail = isProd
|
||||
? (isInternalWhitelisted
|
||||
? tr(
|
||||
'msg.userfront.error.whitelist.$normalizedCode',
|
||||
fallback: internalWhitelistFallback,
|
||||
)
|
||||
: (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
|
||||
? tr('msg.userfront.error.detail_generic')
|
||||
: tr('msg.userfront.error.detail_request')));
|
||||
final detail = isTenantAccessBlocked
|
||||
? tr(
|
||||
'msg.userfront.error.tenant.detail',
|
||||
fallback:
|
||||
'현재 로그인된 계정은 이 애플리케이션에 접근할 수 없습니다.',
|
||||
)
|
||||
: isProd
|
||||
? (isInternalWhitelisted
|
||||
? tr(
|
||||
'msg.userfront.error.whitelist.$normalizedCode',
|
||||
fallback: internalWhitelistFallback,
|
||||
)
|
||||
: (isOryBypass
|
||||
? tr(
|
||||
'msg.userfront.error.ory.$normalizedCode',
|
||||
fallback: (widget.description?.isNotEmpty == true)
|
||||
? widget.description
|
||||
: tr('msg.userfront.error.detail_request'),
|
||||
)
|
||||
: tr('msg.userfront.error.detail_contact')))
|
||||
: ((widget.description?.isNotEmpty == true)
|
||||
? widget.description!
|
||||
: (hasCode
|
||||
? tr('msg.userfront.error.detail_generic')
|
||||
: tr('msg.userfront.error.detail_request')));
|
||||
final tenantLabel = _extractTenantLabel(_sessionProfile);
|
||||
final emailLabel = _sessionProfile?['email']?.toString().trim() ?? '';
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||
body: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(28, 28, 28, 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(28, 28, 28, 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -103,6 +222,101 @@ class ErrorScreen extends StatelessWidget {
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
if (isTenantAccessBlocked) ...[
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.error.tenant.title',
|
||||
fallback: '접근 제한 정보',
|
||||
),
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (_isLoadingSessionProfile)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Flexible(
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.error.tenant.loading',
|
||||
fallback:
|
||||
'현재 계정 정보를 불러오는 중입니다.',
|
||||
),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else ...[
|
||||
_InfoRow(
|
||||
label: tr(
|
||||
'msg.userfront.error.tenant.account',
|
||||
fallback: '계정',
|
||||
),
|
||||
value: emailLabel.isNotEmpty
|
||||
? emailLabel
|
||||
: tr(
|
||||
'msg.userfront.error.tenant.account_unknown',
|
||||
fallback: '알 수 없음',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_InfoRow(
|
||||
label: tr(
|
||||
'msg.userfront.error.tenant.tenant',
|
||||
fallback: '소속 테넌트',
|
||||
),
|
||||
value: tenantLabel.isNotEmpty
|
||||
? tenantLabel
|
||||
: tr(
|
||||
'msg.userfront.error.tenant.tenant_unknown',
|
||||
fallback: '알 수 없음',
|
||||
),
|
||||
),
|
||||
if (_sessionProfileError != null &&
|
||||
_sessionProfileError!.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.error.tenant.load_failed',
|
||||
fallback:
|
||||
'계정 정보를 확인하지 못했습니다. 다시 시도해 주세요.',
|
||||
),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
tr('msg.userfront.error.type', params: {'type': errorType}),
|
||||
@@ -110,10 +324,11 @@ class ErrorScreen extends StatelessWidget {
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (errorId != null && errorId!.isNotEmpty) ...[
|
||||
if (widget.errorId != null && widget.errorId!.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
tr('msg.userfront.error.id', params: {'id': errorId!}),
|
||||
tr('msg.userfront.error.id',
|
||||
params: {'id': widget.errorId!}),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -125,7 +340,9 @@ class ErrorScreen extends StatelessWidget {
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go('/login'),
|
||||
onPressed: isTenantAccessBlocked
|
||||
? _switchAccount
|
||||
: () => context.go('/login'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
@@ -137,7 +354,11 @@ class ErrorScreen extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: Text(tr('ui.userfront.error.go_login')),
|
||||
child: Text(
|
||||
isTenantAccessBlocked
|
||||
? tr('ui.userfront.error.switch_account')
|
||||
: tr('ui.userfront.error.go_login'),
|
||||
),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () =>
|
||||
@@ -157,7 +378,9 @@ class ErrorScreen extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -166,3 +389,39 @@ class ErrorScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const _InfoRow({required this.label, required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,6 +503,16 @@ const Map<String, String> koStrings = {
|
||||
"msg.userfront.error.title_generic": "오류가 발생했습니다",
|
||||
"msg.userfront.error.title_with_code": "오류: {{code}}",
|
||||
"msg.userfront.error.type": "오류 종류: {{type}}",
|
||||
"msg.userfront.error.tenant.account": "계정",
|
||||
"msg.userfront.error.tenant.account_unknown": "알 수 없음",
|
||||
"msg.userfront.error.tenant.detail":
|
||||
"현재 로그인된 계정은 이 애플리케이션에 접근할 수 없습니다.",
|
||||
"msg.userfront.error.tenant.load_failed":
|
||||
"계정 정보를 확인하지 못했습니다. 다시 시도해 주세요.",
|
||||
"msg.userfront.error.tenant.loading": "현재 계정 정보를 불러오는 중입니다.",
|
||||
"msg.userfront.error.tenant.tenant": "소속 테넌트",
|
||||
"msg.userfront.error.tenant.tenant_unknown": "알 수 없음",
|
||||
"msg.userfront.error.tenant.title": "접근 제한 정보",
|
||||
"msg.userfront.error.whitelist.\"\$normalizedCode\"": "{{error}}",
|
||||
"msg.userfront.error.whitelist.bad_request": "입력값을 확인해 주세요.",
|
||||
"msg.userfront.error.whitelist.invalid_session": "세션이 만료되었습니다. 다시 로그인해 주세요.",
|
||||
@@ -514,6 +524,7 @@ const Map<String, String> koStrings = {
|
||||
"재설정 링크가 만료되었습니다. 다시 요청해 주세요.",
|
||||
"msg.userfront.error.whitelist.recovery_invalid": "재설정 링크가 유효하지 않습니다.",
|
||||
"msg.userfront.error.whitelist.settings_disabled": "현재 계정 설정 화면은 준비 중입니다.",
|
||||
"msg.userfront.error.whitelist.tenant_not_allowed": "허용되지 않은 테넌트입니다.",
|
||||
"msg.userfront.error.whitelist.verification_required":
|
||||
"추가 인증이 필요합니다. 안내에 따라 진행해 주세요.",
|
||||
"msg.userfront.forgot.description":
|
||||
@@ -1738,6 +1749,7 @@ const Map<String, String> koStrings = {
|
||||
"ui.userfront.device.windows": "Desktop(Windows)",
|
||||
"ui.userfront.error.go_home": "홈으로 이동",
|
||||
"ui.userfront.error.go_login": "로그인으로 이동",
|
||||
"ui.userfront.error.switch_account": "다른 계정으로 로그인",
|
||||
"ui.userfront.forgot.heading": "비밀번호를 잊으셨나요?",
|
||||
"ui.userfront.forgot.input_label": "이메일 또는 휴대폰 번호",
|
||||
"ui.userfront.forgot.submit": "재설정 링크 전송",
|
||||
@@ -2436,6 +2448,16 @@ const Map<String, String> enStrings = {
|
||||
"msg.userfront.error.title_generic": "An error occurred.",
|
||||
"msg.userfront.error.title_with_code": "Error: {{code}}",
|
||||
"msg.userfront.error.type": "Error type: {{type}}",
|
||||
"msg.userfront.error.tenant.account": "Account",
|
||||
"msg.userfront.error.tenant.account_unknown": "Unknown",
|
||||
"msg.userfront.error.tenant.detail":
|
||||
"The currently signed-in account cannot access this application.",
|
||||
"msg.userfront.error.tenant.load_failed":
|
||||
"We could not confirm the account details. Please try again.",
|
||||
"msg.userfront.error.tenant.loading": "Loading the current account details.",
|
||||
"msg.userfront.error.tenant.tenant": "Tenant",
|
||||
"msg.userfront.error.tenant.tenant_unknown": "Unknown",
|
||||
"msg.userfront.error.tenant.title": "Access restriction details",
|
||||
"msg.userfront.error.whitelist.\"\$normalizedCode\"": "{{error}}",
|
||||
"msg.userfront.error.whitelist.bad_request": "Please check your input.",
|
||||
"msg.userfront.error.whitelist.invalid_session":
|
||||
@@ -2452,6 +2474,8 @@ const Map<String, String> enStrings = {
|
||||
"The recovery link is invalid.",
|
||||
"msg.userfront.error.whitelist.settings_disabled":
|
||||
"Account settings are currently unavailable.",
|
||||
"msg.userfront.error.whitelist.tenant_not_allowed":
|
||||
"This tenant is not allowed.",
|
||||
"msg.userfront.error.whitelist.verification_required":
|
||||
"Additional verification is required. Please follow the instructions.",
|
||||
"msg.userfront.forgot.description":
|
||||
@@ -3752,6 +3776,7 @@ const Map<String, String> enStrings = {
|
||||
"ui.userfront.device.windows": "Desktop(Windows)",
|
||||
"ui.userfront.error.go_home": "Go Home",
|
||||
"ui.userfront.error.go_login": "Go Login",
|
||||
"ui.userfront.error.switch_account": "Sign in with another account",
|
||||
"ui.userfront.forgot.heading": "Forgot your password?",
|
||||
"ui.userfront.forgot.input_label": "Email address or phone number",
|
||||
"ui.userfront.forgot.submit": "Send reset link",
|
||||
|
||||
@@ -9,6 +9,7 @@ Future<void> _pumpErrorScreen(
|
||||
String? errorCode,
|
||||
String? description,
|
||||
bool? isProdOverride,
|
||||
Future<Map<String, dynamic>> Function()? sessionProfileLoader,
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
@@ -16,6 +17,7 @@ Future<void> _pumpErrorScreen(
|
||||
errorCode: errorCode,
|
||||
description: description,
|
||||
isProdOverride: isProdOverride,
|
||||
sessionProfileLoader: sessionProfileLoader,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -193,4 +195,50 @@ void main() {
|
||||
expect(find.text(type), findsOneWidget);
|
||||
expect(find.text('원문 메시지'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('tenant_not_allowed는 전용 차단 정보를 노출한다', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await _pumpErrorScreen(
|
||||
tester,
|
||||
errorCode: 'tenant_not_allowed',
|
||||
description: '원문 메시지',
|
||||
isProdOverride: true,
|
||||
sessionProfileLoader: () async {
|
||||
return {
|
||||
'email': 'employee@example.com',
|
||||
'tenant': {'name': 'Baron HQ', 'slug': 'baron-hq'},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
final title = tr(
|
||||
'msg.userfront.error.title',
|
||||
fallback: '인증 과정에서 오류가 발생했습니다',
|
||||
);
|
||||
final detail = tr(
|
||||
'msg.userfront.error.tenant.detail',
|
||||
fallback: '현재 로그인된 계정은 이 애플리케이션에 접근할 수 없습니다.',
|
||||
);
|
||||
final account = tr(
|
||||
'msg.userfront.error.tenant.account',
|
||||
fallback: '계정',
|
||||
);
|
||||
final tenant = tr(
|
||||
'msg.userfront.error.tenant.tenant',
|
||||
fallback: '소속 테넌트',
|
||||
);
|
||||
final switchAccount = tr(
|
||||
'ui.userfront.error.switch_account',
|
||||
fallback: '다른 계정으로 로그인',
|
||||
);
|
||||
|
||||
expect(find.text(title), findsOneWidget);
|
||||
expect(find.text(detail), findsOneWidget);
|
||||
expect(find.text(account), findsOneWidget);
|
||||
expect(find.text('employee@example.com'), findsOneWidget);
|
||||
expect(find.text(tenant), findsOneWidget);
|
||||
expect(find.text('Baron HQ'), findsOneWidget);
|
||||
expect(find.text(switchAccount), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user