1
0
forked from baron/baron-sso

연동 ㅎ해지 UI 및 서비스 로직 구현

This commit is contained in:
2026-02-03 17:59:23 +09:00
parent c5cec11c03
commit 0eda7bde13
2 changed files with 122 additions and 9 deletions

View File

@@ -594,6 +594,44 @@ class AuthProxyService {
}
}
static Future<List<dynamic>> fetchLinkedRps() async {
final url = Uri.parse('$_baseUrl/api/v1/user/rp/linked');
final client = createHttpClient(withCredentials: true);
try {
final response = await client.get(
url,
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['items'] ?? [];
} else {
throw Exception('연동된 앱 목록을 불러오지 못했습니다.');
}
} finally {
client.close();
}
}
static Future<void> revokeLinkedRp(String clientId) async {
final url = Uri.parse('$_baseUrl/api/v1/user/rp/linked/$clientId');
final client = createHttpClient(withCredentials: true);
try {
final response = await client.delete(
url,
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode != 200) {
final errorBody = jsonDecode(response.body);
throw Exception(errorBody['error'] ?? '연동 해지에 실패했습니다.');
}
} finally {
client.close();
}
}
static Future<void> sendLog(String level, String message, {Map<String, dynamic>? data}) async {
if (!_canSendClientLog()) {
return;

View File

@@ -7,6 +7,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../../../core/notifiers/auth_notifier.dart';
import '../../../../core/services/auth_token_store.dart';
import '../../../../core/services/http_client.dart';
import '../../../../core/services/auth_proxy_service.dart';
import '../../../../core/ui/layout_breakpoints.dart';
import '../../profile/domain/notifiers/profile_notifier.dart';
@@ -159,6 +160,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
bool _auditLoading = false;
bool _auditLoadingMore = false;
String? _auditError;
bool _isRevoking = false;
Future<List<LinkedRp>>? _linkedRpsFuture;
bool _showAllActivities = false;
@@ -182,6 +184,50 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
AuthNotifier.instance.notify();
}
Future<void> _onRevokeLink(String clientId, String appName) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('연동 해지'),
content: Text('$appName 앱과의 연동을 해지하시겠습니까?\n해지하면 다음 로그인 시 다시 동의가 필요합니다.'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('취소'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('해지하기'),
),
],
),
);
if (confirmed != true) return;
setState(() => _isRevoking = true);
try {
await AuthProxyService.revokeLinkedRp(clientId);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$appName 연동이 해지되었습니다.')),
);
_refreshAll();
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('해지 실패: $e')),
);
}
} finally {
if (mounted) {
setState(() => _isRevoking = false);
}
}
}
void _onScanQR() {
context.push('/scan');
}
@@ -828,10 +874,12 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
final name = rp.name.isNotEmpty ? rp.name : rp.id;
items.add(
_ActivityItem(
clientId: rp.id,
appName: name,
lastAuthAt: lastAuthLabel,
status: statusLabel,
canLogout: false,
onRevoke: () => _onRevokeLink(rp.id, name),
),
);
}
@@ -929,16 +977,39 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: _ink),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: item.canLogout ? item.onLogout : null,
style: OutlinedButton.styleFrom(
foregroundColor: _ink,
side: const BorderSide(color: _border),
Row(
children: [
if (item.canLogout)
Expanded(
child: OutlinedButton(
onPressed: item.onLogout,
style: OutlinedButton.styleFrom(
foregroundColor: _ink,
side: const BorderSide(color: _border),
padding: const EdgeInsets.symmetric(vertical: 8),
),
child: const Text('로그아웃', style: TextStyle(fontSize: 13)),
),
),
if (item.canLogout) const SizedBox(width: 8),
Expanded(
child: OutlinedButton(
onPressed: _isRevoking ? null : item.onRevoke,
style: OutlinedButton.styleFrom(
foregroundColor: Colors.redAccent,
side: const BorderSide(color: Colors.redAccent, width: 0.5),
padding: const EdgeInsets.symmetric(vertical: 8),
),
child: _isRevoking
? const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.redAccent),
)
: const Text('연동 해지', style: TextStyle(fontSize: 13)),
),
),
child: const Text('로그아웃'),
),
],
),
],
),
@@ -1136,17 +1207,21 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
}
class _ActivityItem {
final String clientId;
final String appName;
final String lastAuthAt;
final String status;
final bool canLogout;
final VoidCallback? onLogout;
final VoidCallback? onRevoke;
_ActivityItem({
required this.clientId,
required this.appName,
required this.lastAuthAt,
required this.status,
required this.canLogout,
this.onLogout,
this.onRevoke,
});
}