1
0
forked from baron/baron-sso

headless login 접속환경 Headless(Server)로 표시

This commit is contained in:
2026-04-14 11:24:17 +09:00
parent 92f8e9a61a
commit c5317abada
3 changed files with 115 additions and 6 deletions

View File

@@ -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;
}

View File

@@ -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),
),
},
),
),

View 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);
});
}