forked from baron/baron-sso
린트 적용
This commit is contained in:
@@ -68,8 +68,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.revoke.confirm',
|
||||
fallback:
|
||||
'{{app}} 앱과의 연동을 해지하시겠습니까?\\n해지하면 다음 로그인 시 다시 동의가 필요합니다.',
|
||||
fallback: '{{app}} 앱과의 연동을 해지하시겠습니까?\\n해지하면 다음 로그인 시 다시 동의가 필요합니다.',
|
||||
params: {'app': appName},
|
||||
),
|
||||
),
|
||||
@@ -81,8 +80,12 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child:
|
||||
Text(tr('ui.userfront.dashboard.revoke.confirm_button', fallback: '해지하기')),
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.dashboard.revoke.confirm_button',
|
||||
fallback: '해지하기',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -158,31 +161,45 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tr('ui.userfront.dashboard.scopes.title',
|
||||
fallback: '권한 (Scopes)'),
|
||||
tr(
|
||||
'ui.userfront.dashboard.scopes.title',
|
||||
fallback: '권한 (Scopes)',
|
||||
),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (item.scopes.isEmpty)
|
||||
Text(
|
||||
tr('msg.userfront.dashboard.scopes.empty',
|
||||
fallback: '요청된 권한이 없습니다.'),
|
||||
tr(
|
||||
'msg.userfront.dashboard.scopes.empty',
|
||||
fallback: '요청된 권한이 없습니다.',
|
||||
),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
)
|
||||
else
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: item.scopes.map((s) => Chip(
|
||||
label: Text(s, style: const TextStyle(fontSize: 12)),
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
)).toList(),
|
||||
children: item.scopes
|
||||
.map(
|
||||
(s) => Chip(
|
||||
label: Text(
|
||||
s,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
tr('ui.userfront.dashboard.status_history',
|
||||
fallback: '상태 이력'),
|
||||
tr(
|
||||
'ui.userfront.dashboard.status_history',
|
||||
fallback: '상태 이력',
|
||||
),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -200,10 +217,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final statusLabel = item.status == 'active'
|
||||
? tr('ui.common.status.active',
|
||||
fallback: '활성')
|
||||
: tr('ui.userfront.dashboard.status.revoked',
|
||||
fallback: '해지됨');
|
||||
? tr('ui.common.status.active', fallback: '활성')
|
||||
: tr(
|
||||
'ui.userfront.dashboard.status.revoked',
|
||||
fallback: '해지됨',
|
||||
);
|
||||
return Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.current_status',
|
||||
@@ -242,8 +260,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.home_outlined),
|
||||
title:
|
||||
Text(tr('ui.userfront.nav.dashboard', fallback: '대시보드')),
|
||||
title: Text(tr('ui.userfront.nav.dashboard', fallback: '대시보드')),
|
||||
selected: true,
|
||||
onTap: () {
|
||||
if (closeOnTap) {
|
||||
@@ -254,8 +271,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title:
|
||||
Text(tr('ui.userfront.nav.profile', fallback: '내 정보')),
|
||||
title: Text(tr('ui.userfront.nav.profile', fallback: '내 정보')),
|
||||
onTap: () {
|
||||
if (closeOnTap) {
|
||||
Navigator.of(context).pop();
|
||||
@@ -265,8 +281,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.qr_code_scanner),
|
||||
title:
|
||||
Text(tr('ui.userfront.nav.qr_scan', fallback: 'QR 스캔')),
|
||||
title: Text(tr('ui.userfront.nav.qr_scan', fallback: 'QR 스캔')),
|
||||
onTap: () {
|
||||
if (closeOnTap) {
|
||||
Navigator.of(context).pop();
|
||||
@@ -277,8 +292,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title:
|
||||
Text(tr('ui.userfront.nav.logout', fallback: '로그아웃')),
|
||||
title: Text(tr('ui.userfront.nav.logout', fallback: '로그아웃')),
|
||||
onTap: () async {
|
||||
if (closeOnTap) {
|
||||
Navigator.of(context).pop();
|
||||
@@ -297,12 +311,12 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
_revokedClientIds.clear();
|
||||
});
|
||||
ref.invalidate(linkedRpsProvider);
|
||||
|
||||
|
||||
await Future.wait([
|
||||
ref.read(linkedRpsProvider.future),
|
||||
ref.read(authTimelineProvider.notifier).refresh(),
|
||||
]);
|
||||
|
||||
|
||||
await _loadAuditLogs(reset: true);
|
||||
await ref.read(linkedRpsProvider.notifier).refresh();
|
||||
}
|
||||
@@ -316,21 +330,18 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
|
||||
Future<AuditPage> _fetchAuditLogs({String? cursor}) async {
|
||||
final baseUrl = _envOrDefault('BACKEND_URL', 'https://sso.hmac.kr');
|
||||
final queryParameters = <String, String>{
|
||||
'limit': '20',
|
||||
};
|
||||
final queryParameters = <String, String>{'limit': '20'};
|
||||
if (cursor != null && cursor.isNotEmpty) {
|
||||
queryParameters['cursor'] = cursor;
|
||||
}
|
||||
final url = Uri.parse('$baseUrl/api/v1/audit/auth/timeline')
|
||||
.replace(queryParameters: queryParameters);
|
||||
final url = Uri.parse(
|
||||
'$baseUrl/api/v1/audit/auth/timeline',
|
||||
).replace(queryParameters: queryParameters);
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
final token = AuthTokenStore.getToken();
|
||||
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (!useCookie && token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
@@ -401,11 +412,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
if (parts.length != 3) {
|
||||
return null;
|
||||
}
|
||||
final payload = utf8.decode(base64Url.decode(base64Url.normalize(parts[1])));
|
||||
final payload = utf8.decode(
|
||||
base64Url.decode(base64Url.normalize(parts[1])),
|
||||
);
|
||||
final data = json.decode(payload) as Map<String, dynamic>;
|
||||
final iatValue = data['iat'] ?? data['auth_time'];
|
||||
if (iatValue is num) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(iatValue.toInt() * 1000).toLocal();
|
||||
return DateTime.fromMillisecondsSinceEpoch(
|
||||
iatValue.toInt() * 1000,
|
||||
).toLocal();
|
||||
}
|
||||
} catch (_) {
|
||||
return null;
|
||||
@@ -467,9 +482,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Widget _buildAuthMethodCell(AuditLogEntry log, String authMethod) {
|
||||
final isOidc = authMethod.contains('OIDC');
|
||||
if (authMethod != 'QR' && !isOidc) {
|
||||
final approvedUserAgent = log.detailMap['approved_user_agent']?.toString() ?? '';
|
||||
final approvedUserAgent =
|
||||
log.detailMap['approved_user_agent']?.toString() ?? '';
|
||||
final approvedIp = log.detailMap['approved_ip']?.toString() ?? '';
|
||||
final hasApproverMeta = approvedUserAgent.isNotEmpty || approvedIp.isNotEmpty;
|
||||
final hasApproverMeta =
|
||||
approvedUserAgent.isNotEmpty || approvedIp.isNotEmpty;
|
||||
if (!authMethod.startsWith('링크') || !hasApproverMeta) {
|
||||
return _selectableText(authMethod);
|
||||
}
|
||||
@@ -497,7 +514,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
final approvedSessionId = (log.detailMap['approved_session_id']?.toString().trim().isNotEmpty ?? false)
|
||||
final approvedSessionId =
|
||||
(log.detailMap['approved_session_id']?.toString().trim().isNotEmpty ??
|
||||
false)
|
||||
? log.detailMap['approved_session_id'].toString()
|
||||
: log.sessionId;
|
||||
final tooltipLabel = isOidc
|
||||
@@ -544,8 +563,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
isOidc ? authMethod : tr('ui.common.qr', fallback: 'QR'),
|
||||
style: TextStyle(
|
||||
color: approvedSessionId.isEmpty ? _ink : Colors.blueAccent,
|
||||
decoration:
|
||||
approvedSessionId.isEmpty ? null : TextDecoration.underline,
|
||||
decoration: approvedSessionId.isEmpty
|
||||
? null
|
||||
: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -555,9 +575,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Widget _buildAuthMethodLine(AuditLogEntry log, String authMethod) {
|
||||
final isOidc = authMethod.contains('OIDC');
|
||||
if (authMethod != 'QR' && !isOidc) {
|
||||
final approvedUserAgent = log.detailMap['approved_user_agent']?.toString() ?? '';
|
||||
final approvedUserAgent =
|
||||
log.detailMap['approved_user_agent']?.toString() ?? '';
|
||||
final approvedIp = log.detailMap['approved_ip']?.toString() ?? '';
|
||||
final hasApproverMeta = approvedUserAgent.isNotEmpty || approvedIp.isNotEmpty;
|
||||
final hasApproverMeta =
|
||||
approvedUserAgent.isNotEmpty || approvedIp.isNotEmpty;
|
||||
if (!authMethod.startsWith('링크') || !hasApproverMeta) {
|
||||
return _selectableText(
|
||||
tr(
|
||||
@@ -595,7 +617,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
final approvedSessionId = (log.detailMap['approved_session_id']?.toString().trim().isNotEmpty ?? false)
|
||||
final approvedSessionId =
|
||||
(log.detailMap['approved_session_id']?.toString().trim().isNotEmpty ??
|
||||
false)
|
||||
? log.detailMap['approved_session_id'].toString()
|
||||
: log.sessionId;
|
||||
final tooltipLabel = isOidc
|
||||
@@ -642,7 +666,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
'msg.userfront.dashboard.auth_method',
|
||||
fallback: '인증수단: {{method}}',
|
||||
params: {
|
||||
'method': isOidc ? authMethod : tr('ui.common.qr', fallback: 'QR'),
|
||||
'method': isOidc
|
||||
? authMethod
|
||||
: tr('ui.common.qr', fallback: 'QR'),
|
||||
},
|
||||
),
|
||||
style: TextStyle(
|
||||
@@ -714,7 +740,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final profileState = ref.watch(profileProvider);
|
||||
final profile = profileState.value;
|
||||
final timelineState = ref.watch(authTimelineProvider);
|
||||
final userName = profile?.name ??
|
||||
final userName =
|
||||
profile?.name ??
|
||||
profile?.email ??
|
||||
profile?.phone ??
|
||||
tr('ui.userfront.profile.user_fallback', fallback: 'User');
|
||||
@@ -751,7 +778,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
drawer: isWide ? null : Drawer(child: _buildSideMenu(context, closeOnTap: true)),
|
||||
drawer: isWide
|
||||
? null
|
||||
: Drawer(child: _buildSideMenu(context, closeOnTap: true)),
|
||||
body: Row(
|
||||
children: [
|
||||
if (isWide)
|
||||
@@ -775,11 +804,18 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isMobile) ...[
|
||||
_buildHeaderCard(userName, department, sessionIssuedAt),
|
||||
_buildHeaderCard(
|
||||
userName,
|
||||
department,
|
||||
sessionIssuedAt,
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
],
|
||||
_buildSectionTitle(
|
||||
tr('ui.userfront.sections.apps', fallback: '나의 App 현황'),
|
||||
tr(
|
||||
'ui.userfront.sections.apps',
|
||||
fallback: '나의 App 현황',
|
||||
),
|
||||
tr(
|
||||
'msg.userfront.sections.apps_subtitle',
|
||||
fallback: '현재 연결된 앱과 최근 인증 상태입니다.',
|
||||
@@ -810,7 +846,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderCard(String userName, String department, DateTime? issuedAt) {
|
||||
Widget _buildHeaderCard(
|
||||
String userName,
|
||||
String department,
|
||||
DateTime? issuedAt,
|
||||
) {
|
||||
final sessionLabel = issuedAt != null
|
||||
? _formatDateTime(issuedAt)
|
||||
: tr('ui.userfront.session.unknown', fallback: '알 수 없음');
|
||||
@@ -823,7 +863,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
fallback: '안녕하세요, {{name}}님',
|
||||
params: {'name': userName},
|
||||
),
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: _ink),
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _ink,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
@@ -871,13 +915,14 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: _ink),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _ink,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey[600]),
|
||||
),
|
||||
Text(subtitle, style: TextStyle(fontSize: 13, color: Colors.grey[600])),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -897,7 +942,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 12, color: _ink, fontWeight: FontWeight.w600),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: _ink,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -920,7 +969,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
'msg.userfront.dashboard.activities.empty',
|
||||
fallback: '연동된 앱이 없습니다.',
|
||||
),
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[700], fontWeight: FontWeight.w600),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
@@ -959,14 +1012,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
List<_ActivityItem> _buildActivityItems(List<LinkedRp> linkedRps) {
|
||||
final items = <_ActivityItem>[];
|
||||
for (final rp in linkedRps) {
|
||||
final normalizedStatus = rp.status.toLowerCase();
|
||||
// status가 'inactive'로 내려올 수 있으므로 이를 반영
|
||||
final isActiveInApi = normalizedStatus == 'active' || normalizedStatus == '';
|
||||
final isActiveInApi =
|
||||
normalizedStatus == 'active' || normalizedStatus == '';
|
||||
final isRevoked = !isActiveInApi;
|
||||
|
||||
final lastAuthLabel = rp.lastAuthenticatedAt != null
|
||||
@@ -975,7 +1027,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
|
||||
final statusCode = isRevoked ? 'revoked' : 'active';
|
||||
final name = rp.name.isNotEmpty ? rp.name : rp.id;
|
||||
|
||||
|
||||
items.add(
|
||||
_ActivityItem(
|
||||
clientId: rp.id,
|
||||
@@ -995,17 +1047,17 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
items.sort((a, b) {
|
||||
final aActive = a.status == 'active';
|
||||
final bActive = b.status == 'active';
|
||||
|
||||
|
||||
if (aActive && !bActive) return -1;
|
||||
if (!aActive && bActive) return 1;
|
||||
|
||||
|
||||
// 둘 다 활성이거나 둘 다 비활성인 경우 최근 인증순 내림차순
|
||||
if (a.lastAuthDateTime != null && b.lastAuthDateTime != null) {
|
||||
return b.lastAuthDateTime!.compareTo(a.lastAuthDateTime!);
|
||||
}
|
||||
if (a.lastAuthDateTime != null) return -1;
|
||||
if (b.lastAuthDateTime != null) return 1;
|
||||
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
@@ -1018,7 +1070,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth;
|
||||
|
||||
|
||||
// 화면 너비에 따른 컬럼 수 및 초기 표시 개수 결정
|
||||
int crossAxisCount;
|
||||
if (maxWidth > 1200) {
|
||||
@@ -1042,7 +1094,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
|
||||
// 카드의 너비를 화면 너비에 맞춰 계산 (여백 고려)
|
||||
final double spacing = 12.0;
|
||||
final double cardWidth = (maxWidth - (spacing * (crossAxisCount - 1))) / crossAxisCount;
|
||||
final double cardWidth =
|
||||
(maxWidth - (spacing * (crossAxisCount - 1))) / crossAxisCount;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -1063,18 +1116,24 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: () => setState(() => _showAllActivities = !_showAllActivities),
|
||||
onPressed: () => setState(
|
||||
() => _showAllActivities = !_showAllActivities,
|
||||
),
|
||||
icon: Icon(
|
||||
_showAllActivities ? Icons.keyboard_arrow_up : Icons.add,
|
||||
size: 18,
|
||||
color: _showAllActivities ? Colors.grey : Colors.blueAccent,
|
||||
color: _showAllActivities
|
||||
? Colors.grey
|
||||
: Colors.blueAccent,
|
||||
),
|
||||
label: Text(
|
||||
_showAllActivities
|
||||
? tr('ui.common.collapse', fallback: '접기')
|
||||
: tr('ui.common.show_more', fallback: '+ 더보기'),
|
||||
style: TextStyle(
|
||||
color: _showAllActivities ? Colors.grey : Colors.blueAccent,
|
||||
color: _showAllActivities
|
||||
? Colors.grey
|
||||
: Colors.blueAccent,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
@@ -1090,7 +1149,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Widget _buildActivityCard(_ActivityItem item, {double? cardWidth}) {
|
||||
final isActive = item.status == 'active';
|
||||
final statusColor = isActive ? Colors.green : Colors.grey;
|
||||
final borderColor = isActive ? Colors.green.withValues(alpha: 128) : _border;
|
||||
final borderColor = isActive
|
||||
? Colors.green.withValues(alpha: 128)
|
||||
: _border;
|
||||
final borderWidth = isActive ? 1.5 : 1.0;
|
||||
|
||||
// 활성 상태면 클릭 가능 (URL 유무와 관계없이)
|
||||
@@ -1104,13 +1165,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
color: _surface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: borderColor, width: borderWidth),
|
||||
boxShadow: isActive ? [
|
||||
BoxShadow(
|
||||
color: Colors.green.withValues(alpha: 13),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
] : null,
|
||||
boxShadow: isActive
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.green.withValues(alpha: 13),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -1120,7 +1183,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.appName,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: _ink),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _ink,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
@@ -1132,8 +1199,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
child: Text(
|
||||
item.status == 'active'
|
||||
? tr('ui.common.status.active', fallback: '활성')
|
||||
: tr('ui.userfront.dashboard.status.revoked', fallback: '해지됨'),
|
||||
style: TextStyle(fontSize: 11, color: statusColor, fontWeight: FontWeight.w600),
|
||||
: tr(
|
||||
'ui.userfront.dashboard.status.revoked',
|
||||
fallback: '해지됨',
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1146,7 +1220,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
item.lastAuthAt,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: _ink),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _ink,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
@@ -1168,22 +1246,38 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: (_isRevoking || item.isRevoked) ? null : item.onRevoke,
|
||||
onPressed: (_isRevoking || item.isRevoked)
|
||||
? null
|
||||
: item.onRevoke,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: item.isRevoked ? Colors.grey : Colors.redAccent,
|
||||
side: BorderSide(color: item.isRevoked ? Colors.grey : Colors.redAccent, width: 0.5),
|
||||
foregroundColor: item.isRevoked
|
||||
? Colors.grey
|
||||
: Colors.redAccent,
|
||||
side: BorderSide(
|
||||
color: item.isRevoked ? Colors.grey : Colors.redAccent,
|
||||
width: 0.5,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
child: _isRevoking && !item.isRevoked
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.redAccent),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
item.isRevoked
|
||||
? tr('ui.userfront.dashboard.status.revoked', fallback: '해지됨')
|
||||
: tr('ui.userfront.dashboard.revoke.title', fallback: '연동 해지'),
|
||||
? tr(
|
||||
'ui.userfront.dashboard.status.revoked',
|
||||
fallback: '해지됨',
|
||||
)
|
||||
: tr(
|
||||
'ui.userfront.dashboard.revoke.title',
|
||||
fallback: '연동 해지',
|
||||
),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
@@ -1268,7 +1362,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () => ref.read(authTimelineProvider.notifier).refresh(),
|
||||
onPressed: () =>
|
||||
ref.read(authTimelineProvider.notifier).refresh(),
|
||||
child: Text(tr('ui.common.retry', fallback: '다시 시도')),
|
||||
),
|
||||
],
|
||||
@@ -1326,7 +1421,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr('ui.userfront.audit.table.session_id', fallback: 'Session ID'),
|
||||
tr(
|
||||
'ui.userfront.audit.table.session_id',
|
||||
fallback: 'Session ID',
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
@@ -1336,7 +1434,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr('ui.userfront.audit.table.app', fallback: '애플리케이션'),
|
||||
tr(
|
||||
'ui.userfront.audit.table.app',
|
||||
fallback: '애플리케이션',
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
@@ -1346,17 +1447,26 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr('ui.userfront.audit.table.device', fallback: '접속환경'),
|
||||
tr(
|
||||
'ui.userfront.audit.table.device',
|
||||
fallback: '접속환경',
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr('ui.userfront.audit.table.auth_method', fallback: '인증수단'),
|
||||
tr(
|
||||
'ui.userfront.audit.table.auth_method',
|
||||
fallback: '인증수단',
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr('ui.userfront.audit.table.result', fallback: '인증결과'),
|
||||
tr(
|
||||
'ui.userfront.audit.table.result',
|
||||
fallback: '인증결과',
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
@@ -1369,47 +1479,57 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final statusLabel = log.status == 'success'
|
||||
? tr('ui.common.status.success', fallback: '성공')
|
||||
: tr('ui.common.status.failure', fallback: '실패');
|
||||
final statusColor =
|
||||
log.status == 'success' ? Colors.green : Colors.redAccent;
|
||||
final statusColor = log.status == 'success'
|
||||
? Colors.green
|
||||
: Colors.redAccent;
|
||||
final authMethod = log.authMethod.isNotEmpty
|
||||
? log.authMethod
|
||||
: _authMethodLabel();
|
||||
final deviceLabel = _deviceLabelFromUserAgent(log.userAgent);
|
||||
return DataRow(cells: [
|
||||
DataCell(
|
||||
_selectableText(
|
||||
log.sessionId.isEmpty
|
||||
? tr('ui.common.hyphen', fallback: '-')
|
||||
: log.sessionId,
|
||||
),
|
||||
),
|
||||
DataCell(_selectableText(_formatDateTime(log.timestamp))),
|
||||
DataCell(_buildAppCell(log)),
|
||||
DataCell(
|
||||
_selectableText(
|
||||
log.ipAddress.isEmpty
|
||||
? tr('ui.common.hyphen', fallback: '-')
|
||||
: log.ipAddress,
|
||||
),
|
||||
),
|
||||
DataCell(_selectableText(deviceLabel)),
|
||||
DataCell(_buildAuthMethodCell(log, authMethod)),
|
||||
DataCell(
|
||||
_selectableText(
|
||||
statusLabel,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
final deviceLabel = _deviceLabelFromUserAgent(
|
||||
log.userAgent,
|
||||
);
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
_selectableText(
|
||||
log.sessionId.isEmpty
|
||||
? tr('ui.common.hyphen', fallback: '-')
|
||||
: log.sessionId,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
_selectableText(
|
||||
tr('ui.userfront.audit.table.pending', fallback: '(준비중)'),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
DataCell(
|
||||
_selectableText(_formatDateTime(log.timestamp)),
|
||||
),
|
||||
),
|
||||
]);
|
||||
DataCell(_buildAppCell(log)),
|
||||
DataCell(
|
||||
_selectableText(
|
||||
log.ipAddress.isEmpty
|
||||
? tr('ui.common.hyphen', fallback: '-')
|
||||
: log.ipAddress,
|
||||
),
|
||||
),
|
||||
DataCell(_selectableText(deviceLabel)),
|
||||
DataCell(_buildAuthMethodCell(log, authMethod)),
|
||||
DataCell(
|
||||
_selectableText(
|
||||
statusLabel,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
_selectableText(
|
||||
tr(
|
||||
'ui.userfront.audit.table.pending',
|
||||
fallback: '(준비중)',
|
||||
),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
@@ -1443,7 +1563,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Expanded(
|
||||
child: _buildAppCell(
|
||||
log,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, color: _ink),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _ink,
|
||||
),
|
||||
),
|
||||
),
|
||||
_selectableText(
|
||||
@@ -1451,7 +1574,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
? tr('ui.common.status.success', fallback: '성공')
|
||||
: tr('ui.common.status.failure', fallback: '실패'),
|
||||
style: TextStyle(
|
||||
color: log.status == 'success' ? Colors.green : Colors.redAccent,
|
||||
color: log.status == 'success'
|
||||
? Colors.green
|
||||
: Colors.redAccent,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
@@ -1491,10 +1616,17 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
tr(
|
||||
'msg.userfront.audit.device',
|
||||
fallback: '접속환경: {{value}}',
|
||||
params: {'value': _deviceLabelFromUserAgent(log.userAgent)},
|
||||
params: {
|
||||
'value': _deviceLabelFromUserAgent(log.userAgent),
|
||||
},
|
||||
),
|
||||
),
|
||||
_buildAuthMethodLine(log, log.authMethod.isNotEmpty ? log.authMethod : _authMethodLabel()),
|
||||
_buildAuthMethodLine(
|
||||
log,
|
||||
log.authMethod.isNotEmpty
|
||||
? log.authMethod
|
||||
: _authMethodLabel(),
|
||||
),
|
||||
_selectableText(
|
||||
tr(
|
||||
'msg.userfront.audit.result',
|
||||
@@ -1507,10 +1639,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
),
|
||||
_selectableText(
|
||||
tr(
|
||||
'msg.userfront.audit.status',
|
||||
fallback: '현황: (준비중)',
|
||||
),
|
||||
tr('msg.userfront.audit.status', fallback: '현황: (준비중)'),
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
@@ -1542,7 +1671,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => ref.read(authTimelineProvider.notifier).loadMore(),
|
||||
onPressed: () =>
|
||||
ref.read(authTimelineProvider.notifier).loadMore(),
|
||||
child: Text(tr('ui.common.retry', fallback: '재시도')),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user