forked from baron/baron-sso
연동 ㅎ해지 UI 및 서비스 로직 구현
This commit is contained in:
@@ -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 {
|
static Future<void> sendLog(String level, String message, {Map<String, dynamic>? data}) async {
|
||||||
if (!_canSendClientLog()) {
|
if (!_canSendClientLog()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|||||||
import '../../../../core/notifiers/auth_notifier.dart';
|
import '../../../../core/notifiers/auth_notifier.dart';
|
||||||
import '../../../../core/services/auth_token_store.dart';
|
import '../../../../core/services/auth_token_store.dart';
|
||||||
import '../../../../core/services/http_client.dart';
|
import '../../../../core/services/http_client.dart';
|
||||||
|
import '../../../../core/services/auth_proxy_service.dart';
|
||||||
import '../../../../core/ui/layout_breakpoints.dart';
|
import '../../../../core/ui/layout_breakpoints.dart';
|
||||||
import '../../profile/domain/notifiers/profile_notifier.dart';
|
import '../../profile/domain/notifiers/profile_notifier.dart';
|
||||||
|
|
||||||
@@ -159,6 +160,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
bool _auditLoading = false;
|
bool _auditLoading = false;
|
||||||
bool _auditLoadingMore = false;
|
bool _auditLoadingMore = false;
|
||||||
String? _auditError;
|
String? _auditError;
|
||||||
|
bool _isRevoking = false;
|
||||||
|
|
||||||
Future<List<LinkedRp>>? _linkedRpsFuture;
|
Future<List<LinkedRp>>? _linkedRpsFuture;
|
||||||
bool _showAllActivities = false;
|
bool _showAllActivities = false;
|
||||||
@@ -182,6 +184,50 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
AuthNotifier.instance.notify();
|
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() {
|
void _onScanQR() {
|
||||||
context.push('/scan');
|
context.push('/scan');
|
||||||
}
|
}
|
||||||
@@ -828,10 +874,12 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
final name = rp.name.isNotEmpty ? rp.name : rp.id;
|
final name = rp.name.isNotEmpty ? rp.name : rp.id;
|
||||||
items.add(
|
items.add(
|
||||||
_ActivityItem(
|
_ActivityItem(
|
||||||
|
clientId: rp.id,
|
||||||
appName: name,
|
appName: name,
|
||||||
lastAuthAt: lastAuthLabel,
|
lastAuthAt: lastAuthLabel,
|
||||||
status: statusLabel,
|
status: statusLabel,
|
||||||
canLogout: false,
|
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),
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: _ink),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
SizedBox(
|
Row(
|
||||||
width: double.infinity,
|
children: [
|
||||||
child: OutlinedButton(
|
if (item.canLogout)
|
||||||
onPressed: item.canLogout ? item.onLogout : null,
|
Expanded(
|
||||||
style: OutlinedButton.styleFrom(
|
child: OutlinedButton(
|
||||||
foregroundColor: _ink,
|
onPressed: item.onLogout,
|
||||||
side: const BorderSide(color: _border),
|
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 {
|
class _ActivityItem {
|
||||||
|
final String clientId;
|
||||||
final String appName;
|
final String appName;
|
||||||
final String lastAuthAt;
|
final String lastAuthAt;
|
||||||
final String status;
|
final String status;
|
||||||
final bool canLogout;
|
final bool canLogout;
|
||||||
final VoidCallback? onLogout;
|
final VoidCallback? onLogout;
|
||||||
|
final VoidCallback? onRevoke;
|
||||||
|
|
||||||
_ActivityItem({
|
_ActivityItem({
|
||||||
|
required this.clientId,
|
||||||
required this.appName,
|
required this.appName,
|
||||||
required this.lastAuthAt,
|
required this.lastAuthAt,
|
||||||
required this.status,
|
required this.status,
|
||||||
required this.canLogout,
|
required this.canLogout,
|
||||||
this.onLogout,
|
this.onLogout,
|
||||||
|
this.onRevoke,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user