forked from baron/baron-sso
userfront 로그인 후 /dashboard로 이동하게 변경
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -7,6 +8,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import '../domain/providers/linked_rps_provider.dart';
|
||||
import '../../../../core/notifiers/auth_notifier.dart';
|
||||
import '../../../../core/services/auth_proxy_service.dart';
|
||||
import '../../../../core/services/auth_token_store.dart';
|
||||
import '../../../../core/services/http_client.dart';
|
||||
import '../../../../core/i18n/locale_utils.dart';
|
||||
@@ -38,6 +40,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
bool _auditLoadingMore = false;
|
||||
bool _isRevoking = false;
|
||||
bool _redirectingToSignin = false;
|
||||
bool _authBootstrapInProgress = false;
|
||||
|
||||
bool _showAllActivities = false;
|
||||
final Set<String> _revokedClientIds = {};
|
||||
@@ -47,11 +50,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
super.initState();
|
||||
_pageScrollController.addListener(_onPageScroll);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!_isLoggedIn()) {
|
||||
_redirectToSignin();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_loadAuditLogs(reset: true);
|
||||
unawaited(_bootstrapAuthAndLoad());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -254,7 +256,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
if (closeOnTap) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
context.go('/');
|
||||
context.go(buildLocalizedHomePath(Uri.base));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
@@ -302,8 +304,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
|
||||
Future<void> _refreshAll() async {
|
||||
if (!_isLoggedIn()) {
|
||||
_redirectToSignin();
|
||||
return;
|
||||
final recovered = await _recoverSessionFromCookie();
|
||||
if (!recovered) {
|
||||
_redirectToSignin();
|
||||
return;
|
||||
}
|
||||
}
|
||||
await ref.read(profileProvider.notifier).loadProfile();
|
||||
setState(() {
|
||||
@@ -372,7 +377,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
if (_auditLoading || _auditLoadingMore) {
|
||||
return;
|
||||
}
|
||||
if (!reset && (_auditNextCursor == null || _auditNextCursor!.isEmpty)) {
|
||||
final nextCursor = _auditNextCursor;
|
||||
if (!reset && (nextCursor == null || nextCursor.isEmpty)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -706,109 +712,133 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_isLoggedIn()) {
|
||||
_redirectToSignin();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final isWide = MediaQuery.of(context).size.width >= sideMenuBreakpoint;
|
||||
final profileState = ref.watch(profileProvider);
|
||||
final profile = profileState.value;
|
||||
final timelineState = ref.watch(authTimelineProvider);
|
||||
final userName =
|
||||
profile?.name ??
|
||||
profile?.email ??
|
||||
profile?.phone ??
|
||||
tr('ui.userfront.profile.user_fallback', fallback: 'User');
|
||||
final department = profile?.department.isNotEmpty == true
|
||||
? profile!.department
|
||||
: tr('ui.userfront.profile.department_empty');
|
||||
final sessionIssuedAt = _getJwtIssuedAt();
|
||||
try {
|
||||
if (!_isLoggedIn()) {
|
||||
_redirectToSignin();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final isWide = MediaQuery.of(context).size.width >= sideMenuBreakpoint;
|
||||
final profileState = ref.watch(profileProvider);
|
||||
final profile = profileState.value;
|
||||
final timelineState = ref.watch(authTimelineProvider);
|
||||
final userName =
|
||||
profile?.name ??
|
||||
profile?.email ??
|
||||
profile?.phone ??
|
||||
tr('ui.userfront.profile.user_fallback', fallback: 'User');
|
||||
final departmentValue = profile?.department ?? '';
|
||||
final department = departmentValue.isNotEmpty
|
||||
? departmentValue
|
||||
: tr('ui.userfront.profile.department_empty');
|
||||
final sessionIssuedAt = _getJwtIssuedAt();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: _subtle,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
tr('ui.userfront.app_title'),
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: _surface,
|
||||
foregroundColor: Colors.black,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person_outline),
|
||||
tooltip: tr('ui.userfront.nav.profile'),
|
||||
onPressed: () => context.push('/profile'),
|
||||
return Scaffold(
|
||||
backgroundColor: _subtle,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
tr('ui.userfront.app_title'),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
tooltip: tr('ui.userfront.nav.qr_scan'),
|
||||
onPressed: _onScanQR,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
tooltip: tr('ui.userfront.nav.logout'),
|
||||
onPressed: _logout,
|
||||
),
|
||||
],
|
||||
),
|
||||
drawer: isWide
|
||||
? null
|
||||
: Drawer(child: _buildSideMenu(context, closeOnTap: true)),
|
||||
body: Row(
|
||||
children: [
|
||||
if (isWide)
|
||||
SizedBox(
|
||||
width: 240,
|
||||
child: _buildSideMenu(context, closeOnTap: false),
|
||||
elevation: 0,
|
||||
backgroundColor: _surface,
|
||||
foregroundColor: Colors.black,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person_outline),
|
||||
tooltip: tr('ui.userfront.nav.profile'),
|
||||
onPressed: () => context.push('/profile'),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _refreshAll,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final timelineWide = constraints.maxWidth >= 900;
|
||||
final isMobile = constraints.maxWidth < 600;
|
||||
return SingleChildScrollView(
|
||||
controller: _pageScrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isMobile) ...[
|
||||
_buildHeaderCard(
|
||||
userName,
|
||||
department,
|
||||
sessionIssuedAt,
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
tooltip: tr('ui.userfront.nav.qr_scan'),
|
||||
onPressed: _onScanQR,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
tooltip: tr('ui.userfront.nav.logout'),
|
||||
onPressed: _logout,
|
||||
),
|
||||
],
|
||||
),
|
||||
drawer: isWide
|
||||
? null
|
||||
: Drawer(child: _buildSideMenu(context, closeOnTap: true)),
|
||||
body: Row(
|
||||
children: [
|
||||
if (isWide)
|
||||
SizedBox(
|
||||
width: 240,
|
||||
child: _buildSideMenu(context, closeOnTap: false),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _refreshAll,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final timelineWide = constraints.maxWidth >= 900;
|
||||
final isMobile = constraints.maxWidth < 600;
|
||||
return SingleChildScrollView(
|
||||
controller: _pageScrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isMobile) ...[
|
||||
_buildHeaderCard(
|
||||
userName,
|
||||
department,
|
||||
sessionIssuedAt,
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
],
|
||||
_buildSectionTitle(
|
||||
tr('ui.userfront.sections.apps'),
|
||||
tr('msg.userfront.sections.apps_subtitle'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildActivitySection(isMobile),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle(
|
||||
tr('ui.userfront.sections.audit'),
|
||||
tr('msg.userfront.sections.audit_subtitle'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildAccessHistory(timelineState, timelineWide),
|
||||
],
|
||||
_buildSectionTitle(
|
||||
tr('ui.userfront.sections.apps'),
|
||||
tr('msg.userfront.sections.apps_subtitle'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildActivitySection(isMobile),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle(
|
||||
tr('ui.userfront.sections.audit'),
|
||||
tr('msg.userfront.sections.audit_subtitle'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildAccessHistory(timelineState, timelineWide),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
AuthProxyService.logError(
|
||||
'DASHBOARD_RENDER_ERROR: $error\nuri=${Uri.base}',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return Scaffold(
|
||||
backgroundColor: _subtle,
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.render_error',
|
||||
fallback: '대시보드 렌더링 중 오류가 발생했습니다. 다시 시도해 주세요.',
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildHeaderCard(
|
||||
@@ -973,8 +1003,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
normalizedStatus == 'active' || normalizedStatus == '';
|
||||
final isRevoked = !isActiveInApi;
|
||||
|
||||
final lastAuthLabel = rp.lastAuthenticatedAt != null
|
||||
? _formatDateTime(rp.lastAuthenticatedAt!)
|
||||
final lastAuthAt = rp.lastAuthenticatedAt;
|
||||
final lastAuthLabel = lastAuthAt != null
|
||||
? _formatDateTime(lastAuthAt)
|
||||
: tr('ui.userfront.dashboard.activity.linked');
|
||||
|
||||
final statusCode = isRevoked ? 'revoked' : 'active';
|
||||
@@ -1004,8 +1035,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
if (!aActive && bActive) return 1;
|
||||
|
||||
// 둘 다 활성이거나 둘 다 비활성인 경우 최근 인증순 내림차순
|
||||
if (a.lastAuthDateTime != null && b.lastAuthDateTime != null) {
|
||||
return b.lastAuthDateTime!.compareTo(a.lastAuthDateTime!);
|
||||
final aLastAuth = a.lastAuthDateTime;
|
||||
final bLastAuth = b.lastAuthDateTime;
|
||||
if (aLastAuth != null && bLastAuth != null) {
|
||||
return bLastAuth.compareTo(aLastAuth);
|
||||
}
|
||||
if (a.lastAuthDateTime != null) return -1;
|
||||
if (b.lastAuthDateTime != null) return 1;
|
||||
@@ -1045,7 +1078,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
}
|
||||
|
||||
// 카드의 너비를 화면 너비에 맞춰 계산 (여백 고려)
|
||||
final double spacing = 12.0;
|
||||
const spacing = 12.0;
|
||||
final double cardWidth =
|
||||
(maxWidth - (spacing * (crossAxisCount - 1))) / crossAxisCount;
|
||||
|
||||
@@ -1244,8 +1277,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
if (item.url != null && item.url!.isNotEmpty) {
|
||||
final uri = Uri.parse(item.url!);
|
||||
final itemUrl = item.url;
|
||||
if (itemUrl != null && itemUrl.isNotEmpty) {
|
||||
final uri = Uri.parse(itemUrl);
|
||||
final canOpen = await canLaunchUrl(uri);
|
||||
if (!mounted) return;
|
||||
if (canOpen) {
|
||||
@@ -1568,7 +1602,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (state.nextCursor == null || state.nextCursor!.isEmpty) {
|
||||
final nextCursor = state.nextCursor;
|
||||
if (nextCursor == null || nextCursor.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
@@ -1581,7 +1616,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
}
|
||||
|
||||
bool _isLoggedIn() {
|
||||
return AuthTokenStore.getToken() != null || AuthTokenStore.usesCookie();
|
||||
final token = AuthTokenStore.getToken();
|
||||
return (token != null && token.isNotEmpty) || AuthTokenStore.usesCookie();
|
||||
}
|
||||
|
||||
void _redirectToSignin() {
|
||||
@@ -1593,13 +1629,60 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final uri = GoRouterState.of(context).uri;
|
||||
Uri uri;
|
||||
try {
|
||||
uri = GoRouterState.of(context).uri;
|
||||
} catch (_) {
|
||||
uri = Uri.base;
|
||||
}
|
||||
final localeCode =
|
||||
extractLocaleFromPath(uri) ?? resolvePreferredLocaleCode();
|
||||
context.go('/$localeCode/signin');
|
||||
_redirectingToSignin = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _bootstrapAuthAndLoad() async {
|
||||
if (!mounted || _authBootstrapInProgress) {
|
||||
return;
|
||||
}
|
||||
_authBootstrapInProgress = true;
|
||||
try {
|
||||
var authenticated = _isLoggedIn();
|
||||
if (!authenticated) {
|
||||
authenticated = await _recoverSessionFromCookie();
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (!authenticated) {
|
||||
_redirectToSignin();
|
||||
return;
|
||||
}
|
||||
await _loadAuditLogs(reset: true);
|
||||
} finally {
|
||||
_authBootstrapInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _recoverSessionFromCookie() async {
|
||||
try {
|
||||
await AuthProxyService.checkCookieSession();
|
||||
final provider =
|
||||
AuthTokenStore.getProvider() ??
|
||||
AuthTokenStore.getPendingProvider() ??
|
||||
'ory';
|
||||
AuthTokenStore.setCookieMode(provider: provider);
|
||||
AuthTokenStore.clearPendingProvider();
|
||||
AuthNotifier.instance.notify();
|
||||
try {
|
||||
await ref.read(profileProvider.notifier).loadProfile();
|
||||
} catch (_) {}
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ActivityItem {
|
||||
|
||||
Reference in New Issue
Block a user