1
0
forked from baron/baron-sso

App 현황 카드 클릭 시 init_url 우선 진입 지원

This commit is contained in:
2026-04-08 10:48:39 +09:00
parent f5c4ffa92f
commit c3605cc86b
5 changed files with 104 additions and 1 deletions

View File

@@ -0,0 +1,21 @@
import 'providers/linked_rps_provider.dart';
String? resolveLinkedRpLaunchUrl(LinkedRp rp) {
final normalizedStatus = rp.status.trim().toLowerCase();
final isActive = normalizedStatus.isEmpty || normalizedStatus == 'active';
if (!isActive) {
return null;
}
final initUrl = rp.initUrl.trim();
if (initUrl.isNotEmpty) {
return initUrl;
}
final url = rp.url.trim();
if (url.isNotEmpty) {
return url;
}
return null;
}

View File

@@ -96,6 +96,7 @@ class LinkedRp {
final String name;
final String logo;
final String url;
final String initUrl;
final String status;
final List<String> scopes;
final DateTime? lastAuthenticatedAt;
@@ -105,6 +106,7 @@ class LinkedRp {
required this.name,
required this.logo,
required this.url,
required this.initUrl,
required this.status,
required this.scopes,
this.lastAuthenticatedAt,
@@ -126,6 +128,7 @@ class LinkedRp {
name: json['name']?.toString() ?? '',
logo: json['logo']?.toString() ?? '',
url: json['url']?.toString() ?? '',
initUrl: json['init_url']?.toString() ?? '',
status: json['status']?.toString() ?? '',
scopes: (json['scopes'] as List?)?.whereType<String>().toList() ?? [],
lastAuthenticatedAt: parsedLastAuth,

View File

@@ -10,6 +10,7 @@ class LinkedRp {
final String name;
final String logo;
final String url;
final String initUrl;
final String status;
final List<String> scopes;
final DateTime? lastAuthenticatedAt;
@@ -19,6 +20,7 @@ class LinkedRp {
required this.name,
required this.logo,
required this.url,
required this.initUrl,
required this.status,
required this.scopes,
required this.lastAuthenticatedAt,
@@ -40,6 +42,7 @@ class LinkedRp {
name: json['name']?.toString() ?? '',
logo: json['logo']?.toString() ?? '',
url: json['url']?.toString() ?? '',
initUrl: json['init_url']?.toString() ?? '',
status: json['status']?.toString() ?? 'unknown',
scopes: (json['scopes'] as List?)?.whereType<String>().toList() ?? [],
lastAuthenticatedAt: parsedLastAuth,

View File

@@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../domain/linked_rp_launch.dart';
import '../domain/session_time_resolver.dart';
import '../domain/providers/linked_rps_provider.dart';
import '../domain/providers/user_sessions_provider.dart';
@@ -1216,6 +1217,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
isRevoked: isRevoked,
onRevoke: isRevoked ? null : () => _onRevokeLink(rp.id, name),
url: rp.url,
launchUrl: resolveLinkedRpLaunchUrl(rp),
lastAuthDateTime: rp.lastAuthenticatedAt,
),
);
@@ -1460,7 +1462,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () async {
final itemUrl = item.url;
final itemUrl = item.launchUrl;
if (itemUrl != null && itemUrl.isNotEmpty) {
final uri = Uri.parse(itemUrl);
final canOpen = await canLaunchUrl(uri);
@@ -2291,6 +2293,7 @@ class _ActivityItem {
final String lastAuthAt;
final String status;
final String? url;
final String? launchUrl;
final List<String> scopes;
final bool isRevoked;
final VoidCallback? onRevoke;
@@ -2303,6 +2306,7 @@ class _ActivityItem {
required this.status,
required this.scopes,
this.url,
this.launchUrl,
this.isRevoked = false,
this.onRevoke,
this.lastAuthDateTime,

View File

@@ -0,0 +1,72 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:userfront/features/dashboard/domain/linked_rp_launch.dart';
import 'package:userfront/features/dashboard/domain/providers/linked_rps_provider.dart';
LinkedRp _linkedRp({
required String status,
String url = '',
String initUrl = '',
}) {
return LinkedRp(
id: 'client-1',
name: 'Example App',
logo: '',
url: url,
initUrl: initUrl,
status: status,
scopes: const ['openid', 'profile'],
lastAuthenticatedAt: null,
);
}
void main() {
test('LinkedRp.fromJson은 init_url을 읽는다', () {
final rp = LinkedRp.fromJson({
'id': 'client-1',
'name': 'Example App',
'status': 'active',
'url': 'https://example.com',
'init_url': 'https://sso.example.com/oidc/oauth2/auth?client_id=client-1',
});
expect(
rp.initUrl,
'https://sso.example.com/oidc/oauth2/auth?client_id=client-1',
);
});
test('활성 앱은 initUrl을 우선 진입 URL로 사용한다', () {
final launchUrl = resolveLinkedRpLaunchUrl(
_linkedRp(
status: 'active',
url: 'https://example.com',
initUrl: 'https://sso.example.com/oidc/oauth2/auth?client_id=client-1',
),
);
expect(
launchUrl,
'https://sso.example.com/oidc/oauth2/auth?client_id=client-1',
);
});
test('활성 앱은 initUrl이 없으면 기존 url로 폴백한다', () {
final launchUrl = resolveLinkedRpLaunchUrl(
_linkedRp(status: 'active', url: 'https://example.com'),
);
expect(launchUrl, 'https://example.com');
});
test('비활성 앱은 진입 URL을 만들지 않는다', () {
final launchUrl = resolveLinkedRpLaunchUrl(
_linkedRp(
status: 'inactive',
url: 'https://example.com',
initUrl: 'https://sso.example.com/oidc/oauth2/auth?client_id=client-1',
),
);
expect(launchUrl, isNull);
});
}