1
0
forked from baron/baron-sso

내정보 페이지 사용성개선, adminFront user 정보 연동.

This commit is contained in:
Lectom C Han
2026-01-30 13:42:41 +09:00
parent 1cb5115f2a
commit 35552943d7
29 changed files with 1586 additions and 472 deletions

View File

@@ -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),