forked from baron/baron-sso
headless login 접속환경 Headless(Server)로 표시
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import 'package:userfront/features/dashboard/domain/models.dart';
|
||||
|
||||
const headlessServerUserAgentSentinel = '__headless_server__';
|
||||
|
||||
bool looksLikeInternalAuditUserAgent(String userAgent) {
|
||||
final lower = userAgent.trim().toLowerCase();
|
||||
return lower.startsWith('go-http-client/') ||
|
||||
lower.startsWith('fasthttp') ||
|
||||
lower.startsWith('fiber') ||
|
||||
lower.startsWith('undici') ||
|
||||
lower.startsWith('node');
|
||||
}
|
||||
|
||||
String preferredAuditLogUserAgent(AuditLogEntry log) {
|
||||
final userAgent = log.userAgent.trim();
|
||||
final path = log.path.toLowerCase();
|
||||
|
||||
final isHeadlessLinkLog =
|
||||
path.contains('/api/v1/auth/magic-link/verify') ||
|
||||
path.contains('/api/v1/auth/login/code/verify');
|
||||
final isHeadlessPasswordLog = path.contains(
|
||||
'/api/v1/auth/headless/password/login',
|
||||
);
|
||||
|
||||
if ((isHeadlessLinkLog || isHeadlessPasswordLog) &&
|
||||
looksLikeInternalAuditUserAgent(userAgent)) {
|
||||
return headlessServerUserAgentSentinel;
|
||||
}
|
||||
|
||||
return userAgent;
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import '../../../../core/ui/toast_service.dart';
|
||||
import '../../profile/domain/notifiers/profile_notifier.dart';
|
||||
import '../domain/dashboard_providers.dart';
|
||||
import '../domain/models.dart' hide LinkedRp;
|
||||
import 'audit_device_utils.dart';
|
||||
import 'package:userfront/i18n.dart';
|
||||
|
||||
class DashboardScreen extends ConsumerStatefulWidget {
|
||||
@@ -690,6 +691,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
if (userAgent.isEmpty) {
|
||||
return tr('ui.common.hyphen', fallback: '-');
|
||||
}
|
||||
if (userAgent == headlessServerUserAgentSentinel) {
|
||||
return 'Headless(Server)';
|
||||
}
|
||||
final ua = userAgent.toLowerCase();
|
||||
if (ua.contains('iphone') || ua.contains('ipad') || ua.contains('ipod')) {
|
||||
return tr('ui.userfront.device.ios', fallback: 'Mobile(iOS)');
|
||||
@@ -1234,6 +1238,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
}
|
||||
|
||||
String _sessionBrowserLabel(String userAgent) {
|
||||
if (userAgent == headlessServerUserAgentSentinel) {
|
||||
return '';
|
||||
}
|
||||
final lower = userAgent.toLowerCase();
|
||||
if (lower.isEmpty || _looksLikeInternalUserAgent(lower)) {
|
||||
return '';
|
||||
@@ -2164,10 +2171,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final authMethod = log.authMethod.isNotEmpty
|
||||
? log.authMethod
|
||||
: _authMethodLabel();
|
||||
final deviceLabel = _deviceLabelFromUserAgent(
|
||||
log.userAgent,
|
||||
final preferredUserAgent = preferredAuditLogUserAgent(
|
||||
log,
|
||||
);
|
||||
final deviceLabel = _deviceLabelFromUserAgent(
|
||||
preferredUserAgent,
|
||||
);
|
||||
final browserLabel = _sessionBrowserLabel(
|
||||
preferredUserAgent,
|
||||
);
|
||||
final browserLabel = _sessionBrowserLabel(log.userAgent);
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
@@ -2387,7 +2399,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
tr(
|
||||
'msg.userfront.audit.device',
|
||||
params: {
|
||||
'value': _deviceLabelFromUserAgent(log.userAgent),
|
||||
'value': _deviceLabelFromUserAgent(
|
||||
preferredAuditLogUserAgent(log),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -2395,9 +2409,14 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
tr(
|
||||
'msg.userfront.audit.browser',
|
||||
params: {
|
||||
'value': _sessionBrowserLabel(log.userAgent).isEmpty
|
||||
'value':
|
||||
_sessionBrowserLabel(
|
||||
preferredAuditLogUserAgent(log),
|
||||
).isEmpty
|
||||
? tr('ui.common.hyphen', fallback: '-')
|
||||
: _sessionBrowserLabel(log.userAgent),
|
||||
: _sessionBrowserLabel(
|
||||
preferredAuditLogUserAgent(log),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
59
userfront/test/audit_device_utils_test.dart
Normal file
59
userfront/test/audit_device_utils_test.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:userfront/features/dashboard/domain/models.dart';
|
||||
import 'package:userfront/features/dashboard/presentation/audit_device_utils.dart';
|
||||
|
||||
AuditLogEntry _log({
|
||||
required String eventType,
|
||||
String userAgent = '',
|
||||
Map<String, dynamic>? details,
|
||||
}) {
|
||||
return AuditLogEntry.fromJson({
|
||||
'event_id': 'audit-1',
|
||||
'timestamp': '2026-04-14T00:00:00Z',
|
||||
'user_id': 'user-123',
|
||||
'event_type': eventType,
|
||||
'status': 'success',
|
||||
'user_agent': userAgent,
|
||||
'details': details == null ? '' : details.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('headless link login maps internal client user agent to sentinel', () {
|
||||
final log = AuditLogEntry.fromJson({
|
||||
'event_id': 'audit-1',
|
||||
'timestamp': '2026-04-14T00:00:00Z',
|
||||
'user_id': 'user-123',
|
||||
'event_type': 'POST /api/v1/auth/login/code/verify',
|
||||
'status': 'success',
|
||||
'user_agent': 'undici',
|
||||
'details':
|
||||
'{"approved_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/146.0.0.0 Safari/537.36"}',
|
||||
});
|
||||
|
||||
expect(preferredAuditLogUserAgent(log), headlessServerUserAgentSentinel);
|
||||
});
|
||||
|
||||
test(
|
||||
'headless password login maps internal client user agent to sentinel',
|
||||
() {
|
||||
final log = _log(
|
||||
eventType: 'POST /api/v1/auth/headless/password/login',
|
||||
userAgent: 'undici',
|
||||
);
|
||||
|
||||
expect(preferredAuditLogUserAgent(log), headlessServerUserAgentSentinel);
|
||||
},
|
||||
);
|
||||
|
||||
test('non-headless login preserves original browser user agent', () {
|
||||
const browserUa =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/146.0.0.0 Safari/537.36';
|
||||
final log = _log(
|
||||
eventType: 'POST /api/v1/auth/password/login',
|
||||
userAgent: browserUa,
|
||||
);
|
||||
|
||||
expect(preferredAuditLogUserAgent(log), browserUa);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user