1
0
forked from baron/baron-sso

사용자 활성 세션 조회·종료 API 추가

This commit is contained in:
2026-04-02 11:01:23 +09:00
parent cdf2c36915
commit a2f2b2dd71
15 changed files with 1922 additions and 1 deletions

View File

@@ -170,3 +170,59 @@ class RpHistoryItem {
);
}
}
class UserSessionSummary {
final String sessionId;
final DateTime? authenticatedAt;
final DateTime? expiresAt;
final DateTime? issuedAt;
final DateTime? lastSeenAt;
final String ipAddress;
final String userAgent;
final String clientId;
final String appName;
final bool isCurrent;
final bool isActive;
UserSessionSummary({
required this.sessionId,
this.authenticatedAt,
this.expiresAt,
this.issuedAt,
this.lastSeenAt,
required this.ipAddress,
required this.userAgent,
required this.clientId,
required this.appName,
required this.isCurrent,
required this.isActive,
});
factory UserSessionSummary.fromJson(Map<String, dynamic> json) {
DateTime? parseDate(dynamic raw) {
final value = raw?.toString();
if (value == null || value.isEmpty) {
return null;
}
try {
return DateTime.parse(value).toLocal();
} catch (_) {
return null;
}
}
return UserSessionSummary(
sessionId: json['session_id']?.toString() ?? '',
authenticatedAt: parseDate(json['authenticated_at']),
expiresAt: parseDate(json['expires_at']),
issuedAt: parseDate(json['issued_at']),
lastSeenAt: parseDate(json['last_seen_at']),
ipAddress: json['ip_address']?.toString() ?? '',
userAgent: json['user_agent']?.toString() ?? '',
clientId: json['client_id']?.toString() ?? '',
appName: json['app_name']?.toString() ?? '',
isCurrent: json['is_current'] == true,
isActive: json['is_active'] != false,
);
}
}

View File

@@ -0,0 +1,68 @@
import 'dart:convert';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/services/auth_proxy_service.dart';
import '../../../../core/services/auth_token_store.dart';
import '../../../../core/services/http_client.dart';
import '../models.dart';
class UserSessionsNotifier extends AsyncNotifier<List<UserSessionSummary>> {
@override
Future<List<UserSessionSummary>> build() async {
return _fetchSessions();
}
String _envOrDefault(String key, String fallback) {
if (!dotenv.isInitialized) {
return fallback;
}
return dotenv.env[key] ?? fallback;
}
Future<List<UserSessionSummary>> _fetchSessions() async {
final baseUrl = _envOrDefault('BACKEND_URL', 'https://sso.hmac.kr');
final url = Uri.parse('$baseUrl/api/v1/user/sessions');
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';
}
try {
final response = await client.get(url, headers: headers);
if (response.statusCode != 200) {
throw Exception('Failed to load sessions: ${response.statusCode}');
}
final body = jsonDecode(response.body) as Map<String, dynamic>;
final items = (body['items'] as List?) ?? const [];
return items
.whereType<Map<String, dynamic>>()
.map(UserSessionSummary.fromJson)
.toList();
} finally {
client.close();
}
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(_fetchSessions);
}
Future<void> revokeSession(String sessionId) async {
await AuthProxyService.revokeSession(sessionId);
await refresh();
}
}
final userSessionsProvider =
AsyncNotifierProvider<UserSessionsNotifier, List<UserSessionSummary>>(() {
return UserSessionsNotifier();
});