forked from baron/baron-sso
내정보 페이지 사용성개선, adminFront user 정보 연동.
This commit is contained in:
@@ -4,7 +4,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:descope/descope.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import '../../../../core/notifiers/auth_notifier.dart';
|
||||
import '../../../../core/services/auth_token_store.dart';
|
||||
@@ -84,6 +83,45 @@ class AuditLogEntry {
|
||||
}
|
||||
}
|
||||
|
||||
class LinkedRp {
|
||||
final String id;
|
||||
final String name;
|
||||
final String logo;
|
||||
final String status;
|
||||
final List<String> scopes;
|
||||
final DateTime? lastAuthenticatedAt;
|
||||
|
||||
LinkedRp({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.logo,
|
||||
required this.status,
|
||||
required this.scopes,
|
||||
required this.lastAuthenticatedAt,
|
||||
});
|
||||
|
||||
factory LinkedRp.fromJson(Map<String, dynamic> json) {
|
||||
final rawLastAuth = json['lastAuthenticatedAt']?.toString() ?? '';
|
||||
DateTime? parsedLastAuth;
|
||||
if (rawLastAuth.isNotEmpty) {
|
||||
try {
|
||||
parsedLastAuth = DateTime.parse(rawLastAuth).toLocal();
|
||||
} catch (_) {
|
||||
parsedLastAuth = null;
|
||||
}
|
||||
}
|
||||
|
||||
return LinkedRp(
|
||||
id: json['id']?.toString() ?? '',
|
||||
name: json['name']?.toString() ?? '',
|
||||
logo: json['logo']?.toString() ?? '',
|
||||
status: json['status']?.toString() ?? '',
|
||||
scopes: (json['scopes'] as List?)?.whereType<String>().toList() ?? [],
|
||||
lastAuthenticatedAt: parsedLastAuth,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DashboardScreen extends ConsumerStatefulWidget {
|
||||
const DashboardScreen({super.key});
|
||||
|
||||
@@ -98,12 +136,14 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
static const _subtle = Color(0xFFF7F8FA);
|
||||
|
||||
Future<List<AuditLogEntry>>? _auditFuture;
|
||||
Future<List<LinkedRp>>? _linkedRpsFuture;
|
||||
bool _showAllActivities = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_auditFuture = _fetchAuditLogs();
|
||||
_linkedRpsFuture = _fetchLinkedRps();
|
||||
}
|
||||
|
||||
Future<void> _logout() async {
|
||||
@@ -172,10 +212,14 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
await ref.read(profileProvider.notifier).loadProfile();
|
||||
setState(() {
|
||||
_auditFuture = _fetchAuditLogs();
|
||||
_linkedRpsFuture = _fetchLinkedRps();
|
||||
});
|
||||
if (_auditFuture != null) {
|
||||
await _auditFuture;
|
||||
}
|
||||
if (_linkedRpsFuture != null) {
|
||||
await _linkedRpsFuture;
|
||||
}
|
||||
}
|
||||
|
||||
static String _envOrDefault(String key, String fallback) {
|
||||
@@ -216,6 +260,37 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
return logs;
|
||||
}
|
||||
|
||||
Future<List<LinkedRp>> _fetchLinkedRps() async {
|
||||
final baseUrl = _envOrDefault('BACKEND_URL', 'https://sso.hmac.kr');
|
||||
final url = Uri.parse('$baseUrl/api/v1/user/rp/linked');
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
final token = AuthTokenStore.getToken();
|
||||
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (!useCookie && token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
final response = await client.get(url, headers: headers);
|
||||
client.close();
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to load linked rps');
|
||||
}
|
||||
|
||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final items = (body['items'] as List?) ?? [];
|
||||
final linkedRps = items
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(LinkedRp.fromJson)
|
||||
.toList();
|
||||
|
||||
return linkedRps;
|
||||
}
|
||||
|
||||
DateTime? _getJwtIssuedAt() {
|
||||
final token = AuthTokenStore.getToken();
|
||||
if (token == null || token.isEmpty) {
|
||||
@@ -391,7 +466,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'Baron 통합로그인',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.bold),
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: _surface,
|
||||
@@ -442,7 +517,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
],
|
||||
_buildSectionTitle('활동상황', '현재 연결된 앱과 최근 인증 상태입니다.'),
|
||||
const SizedBox(height: 12),
|
||||
_buildActivityGrid(sessionIssuedAt, isMobile),
|
||||
_buildActivitySection(isMobile),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle('접속이력', 'Baron 통합로그인 기준의 최근 접근 기록입니다.'),
|
||||
const SizedBox(height: 12),
|
||||
@@ -545,16 +620,52 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivityGrid(DateTime? signupAt, bool isMobile) {
|
||||
final signupLabel = signupAt != null ? _formatDateTime(signupAt) : '확인 필요';
|
||||
final activities = [
|
||||
_ActivityItem(
|
||||
appName: 'Baron 통합로그인',
|
||||
lastAuthAt: signupLabel,
|
||||
status: '활성',
|
||||
canLogout: true,
|
||||
onLogout: _logout,
|
||||
),
|
||||
Widget _buildActivitySection(bool isMobile) {
|
||||
return FutureBuilder<List<LinkedRp>>(
|
||||
future: _linkedRpsFuture,
|
||||
builder: (context, snapshot) {
|
||||
final activities = _buildActivityItems(snapshot.data ?? []);
|
||||
final grid = _buildActivityGrid(activities, isMobile);
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
grid,
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'연동 정보를 불러오지 못했습니다.',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return grid;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<_ActivityItem> _buildActivityItems(List<LinkedRp> linkedRps) {
|
||||
final items = <_ActivityItem>[];
|
||||
for (final rp in linkedRps) {
|
||||
final lastAuthLabel = rp.lastAuthenticatedAt != null
|
||||
? _formatDateTime(rp.lastAuthenticatedAt!)
|
||||
: '연동됨';
|
||||
final normalizedStatus = rp.status.toLowerCase();
|
||||
final statusLabel = normalizedStatus.isEmpty || normalizedStatus == 'active' ? '활성' : '비활성';
|
||||
final name = rp.name.isNotEmpty ? rp.name : rp.id;
|
||||
items.add(
|
||||
_ActivityItem(
|
||||
appName: name,
|
||||
lastAuthAt: lastAuthLabel,
|
||||
status: statusLabel,
|
||||
canLogout: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
items.addAll([
|
||||
_ActivityItem(
|
||||
appName: 'BEPs',
|
||||
lastAuthAt: '연동 필요',
|
||||
@@ -579,7 +690,12 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
status: '미연동',
|
||||
canLogout: false,
|
||||
),
|
||||
];
|
||||
]);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
Widget _buildActivityGrid(List<_ActivityItem> activities, bool isMobile) {
|
||||
|
||||
if (!isMobile) {
|
||||
return Wrap(
|
||||
@@ -660,7 +776,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'가입일시',
|
||||
'최근 인증',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
Reference in New Issue
Block a user