forked from baron/baron-sso
Merge branch 'dev' into fix/rebac-env-sync-issue
This commit is contained in:
@@ -57,6 +57,40 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
};
|
||||
}
|
||||
|
||||
String _renderConsentText(String key, {String? fallback}) {
|
||||
return tr(
|
||||
key,
|
||||
fallback: fallback,
|
||||
).replaceAll(r'\\n', '\n').replaceAll(r'\n', '\n').replaceAll('\\\n', '\n');
|
||||
}
|
||||
|
||||
String _renderScopeCountLabel(int count) {
|
||||
return tr(
|
||||
'msg.userfront.consent.scope_count',
|
||||
fallback: 'Total {{count}}',
|
||||
params: {'count': '$count'},
|
||||
).replaceAll('{$count}', '$count');
|
||||
}
|
||||
|
||||
String _scopeDisplayLabel(String scope) {
|
||||
if (scope == 'offline_access') {
|
||||
return 'offline access';
|
||||
}
|
||||
return scope.replaceAll('_', ' ');
|
||||
}
|
||||
|
||||
String _renderClientIdLabel(String clientId) {
|
||||
final raw = tr(
|
||||
'msg.userfront.consent.client_id',
|
||||
fallback: 'Client ID: {{id}}',
|
||||
);
|
||||
final normalized = raw
|
||||
.replaceAll('{{id}}', '')
|
||||
.replaceAll('{id}', '')
|
||||
.trimRight();
|
||||
return '$normalized $clientId';
|
||||
}
|
||||
|
||||
Future<void> _fetchConsentInfo() async {
|
||||
try {
|
||||
final info = await AuthProxyService.getConsentInfo(
|
||||
@@ -271,7 +305,7 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
tr('msg.userfront.consent.description'),
|
||||
_renderConsentText('msg.userfront.consent.description'),
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -318,11 +352,7 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.consent.client_id',
|
||||
fallback: 'Client ID: {{id}}',
|
||||
params: {'id': clientId},
|
||||
),
|
||||
_renderClientIdLabel(clientId),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[500],
|
||||
@@ -349,11 +379,7 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.consent.scope_count',
|
||||
fallback: 'Total {{count}}',
|
||||
params: {'count': '${requestedScopes.length}'},
|
||||
),
|
||||
_renderScopeCountLabel(requestedScopes.length),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).primaryColor,
|
||||
@@ -371,7 +397,7 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
|
||||
return CheckboxListTile(
|
||||
title: Text(
|
||||
scope, // 스코프 키 (예: openid)
|
||||
_scopeDisplayLabel(scope),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
subtitle: Text(description),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart';
|
||||
import '../../../core/constants/error_whitelist.dart';
|
||||
import '../../../core/i18n/locale_utils.dart';
|
||||
import '../../../core/services/auth_proxy_service.dart';
|
||||
import '../../../core/widgets/theme_toggle_button.dart';
|
||||
import 'package:userfront/i18n.dart';
|
||||
|
||||
class ErrorScreen extends StatelessWidget {
|
||||
@@ -22,6 +23,7 @@ class ErrorScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final isProd = isProdOverride ?? AuthProxyService.isProdEnv;
|
||||
final normalizedCode = (errorCode ?? '').trim();
|
||||
final hasCode = normalizedCode.isNotEmpty;
|
||||
@@ -62,7 +64,7 @@ class ErrorScreen extends StatelessWidget {
|
||||
: tr('msg.userfront.error.detail_request')));
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF7F8FA),
|
||||
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||
body: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
@@ -71,7 +73,7 @@ class ErrorScreen extends StatelessWidget {
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: const BorderSide(color: Color(0xFFE5E7EB)),
|
||||
side: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(28, 28, 28, 24),
|
||||
@@ -79,18 +81,25 @@ class ErrorScreen extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFF111827),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
const ThemeToggleButton(compact: true),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
detail,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: const Color(0xFF4B5563),
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -98,7 +107,7 @@ class ErrorScreen extends StatelessWidget {
|
||||
Text(
|
||||
tr('msg.userfront.error.type', params: {'type': errorType}),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: const Color(0xFF6B7280),
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (errorId != null && errorId!.isNotEmpty) ...[
|
||||
@@ -106,7 +115,7 @@ class ErrorScreen extends StatelessWidget {
|
||||
Text(
|
||||
tr('msg.userfront.error.id', params: {'id': errorId!}),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: const Color(0xFF6B7280),
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -118,8 +127,8 @@ class ErrorScreen extends StatelessWidget {
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go('/login'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF111827),
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
@@ -134,12 +143,12 @@ class ErrorScreen extends StatelessWidget {
|
||||
onPressed: () =>
|
||||
context.go(buildLocalizedHomePath(Uri.base)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF111827),
|
||||
foregroundColor: colorScheme.onSurface,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
side: const BorderSide(color: Color(0xFFCBD5F5)),
|
||||
side: BorderSide(color: colorScheme.outline),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,18 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
Map<String, dynamic>? _policy;
|
||||
bool _isPolicyLoading = false;
|
||||
|
||||
String _renderTranslatedText(
|
||||
String key, {
|
||||
String? fallback,
|
||||
Map<String, String> values = const {},
|
||||
}) {
|
||||
var text = tr(key, fallback: fallback);
|
||||
values.forEach((name, value) {
|
||||
text = text.replaceAll('{{$name}}', value).replaceAll('{$name}', value);
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -123,16 +135,16 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
final requiresSymbol = _policy?['nonAlphanumeric'] ?? true;
|
||||
|
||||
final parts = <String>[
|
||||
tr(
|
||||
_renderTranslatedText(
|
||||
'msg.userfront.reset.policy.min_length',
|
||||
params: {'count': '$minLength'},
|
||||
values: {'count': '$minLength'},
|
||||
),
|
||||
];
|
||||
if (minTypes > 0) {
|
||||
parts.add(
|
||||
tr(
|
||||
_renderTranslatedText(
|
||||
'msg.userfront.reset.policy.min_types',
|
||||
params: {'count': '$minTypes'},
|
||||
values: {'count': '$minTypes'},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,18 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
Timer? _phoneTimer;
|
||||
int _phoneSeconds = 0;
|
||||
|
||||
String _renderTranslatedText(
|
||||
String key, {
|
||||
String? fallback,
|
||||
Map<String, String> values = const {},
|
||||
}) {
|
||||
var text = tr(key, fallback: fallback);
|
||||
values.forEach((name, value) {
|
||||
text = text.replaceAll('{{$name}}', value).replaceAll('{$name}', value);
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -1663,16 +1675,16 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
final requiresSymbol = _policy?['nonAlphanumeric'] ?? true;
|
||||
|
||||
final parts = <String>[
|
||||
tr(
|
||||
_renderTranslatedText(
|
||||
'msg.userfront.signup.policy.min_length',
|
||||
params: {'count': minLength.toString()},
|
||||
values: {'count': minLength.toString()},
|
||||
),
|
||||
];
|
||||
if (minTypes > 0) {
|
||||
parts.add(
|
||||
tr(
|
||||
_renderTranslatedText(
|
||||
'msg.userfront.signup.policy.min_types',
|
||||
params: {'count': minTypes.toString()},
|
||||
values: {'count': minTypes.toString()},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1689,9 +1701,9 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
parts.add(tr('msg.userfront.signup.policy.symbol'));
|
||||
}
|
||||
|
||||
return tr(
|
||||
return _renderTranslatedText(
|
||||
'msg.userfront.signup.policy.summary',
|
||||
params: {'rules': parts.join(', ')},
|
||||
values: {'rules': parts.join(', ')},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
@@ -170,3 +173,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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,13 +3,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:userfront/i18n.dart';
|
||||
import '../../../../core/notifiers/auth_notifier.dart';
|
||||
import '../../../../core/i18n/locale_utils.dart';
|
||||
import '../../../../core/services/auth_proxy_service.dart';
|
||||
import '../../../../core/services/auth_token_store.dart';
|
||||
import '../../../../core/services/logout_service.dart';
|
||||
import '../../../../core/ui/layout_breakpoints.dart';
|
||||
import '../../../../core/ui/toast_service.dart';
|
||||
import '../../../../core/widgets/language_selector.dart';
|
||||
import '../../../../core/widgets/theme_toggle_button.dart';
|
||||
import '../../data/models/user_profile_model.dart';
|
||||
import '../../domain/notifiers/profile_notifier.dart';
|
||||
|
||||
@@ -21,10 +21,6 @@ class ProfilePage extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
static const _ink = Color(0xFF1A1F2C);
|
||||
static const _surface = Colors.white;
|
||||
static const _border = Color(0xFFE5E7EB);
|
||||
static const _subtle = Color(0xFFF7F8FA);
|
||||
static final _log = Logger('ProfilePage');
|
||||
|
||||
UserProfile? _cachedProfile;
|
||||
@@ -55,9 +51,27 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
bool _showCurrentPassword = false;
|
||||
bool _showNewPassword = false;
|
||||
bool _showConfirmPassword = false;
|
||||
bool _isDesktopSideMenuOpen = true;
|
||||
Map<String, dynamic>? _passwordPolicy;
|
||||
bool _isPasswordPolicyLoading = false;
|
||||
|
||||
Color get _ink => Theme.of(context).colorScheme.onSurface;
|
||||
Color get _surface => Theme.of(context).colorScheme.surface;
|
||||
Color get _border => Theme.of(context).colorScheme.outlineVariant;
|
||||
Color get _subtle => Theme.of(context).colorScheme.surfaceContainerLowest;
|
||||
|
||||
String _renderTranslatedText(
|
||||
String key, {
|
||||
String? fallback,
|
||||
Map<String, String> values = const {},
|
||||
}) {
|
||||
var text = tr(key, fallback: fallback);
|
||||
values.forEach((name, value) {
|
||||
text = text.replaceAll('{{$name}}', value).replaceAll('{$name}', value);
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -99,16 +113,16 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
final requiresSymbol = _passwordPolicy?['nonAlphanumeric'] ?? true;
|
||||
|
||||
final parts = <String>[
|
||||
tr(
|
||||
_renderTranslatedText(
|
||||
'msg.userfront.signup.policy.min_length',
|
||||
params: {'count': '$minLength'},
|
||||
values: {'count': '$minLength'},
|
||||
),
|
||||
];
|
||||
if (minTypes > 0) {
|
||||
parts.add(
|
||||
tr(
|
||||
_renderTranslatedText(
|
||||
'msg.userfront.signup.policy.min_types',
|
||||
params: {'count': '$minTypes'},
|
||||
values: {'count': '$minTypes'},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -125,9 +139,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
parts.add(tr('msg.userfront.signup.policy.symbol'));
|
||||
}
|
||||
|
||||
return tr(
|
||||
return _renderTranslatedText(
|
||||
'msg.userfront.signup.policy.summary',
|
||||
params: {'rules': parts.join(", ")},
|
||||
values: {'rules': parts.join(", ")},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -164,8 +178,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
}
|
||||
|
||||
Future<void> _logout() async {
|
||||
AuthTokenStore.clear();
|
||||
AuthNotifier.instance.notify();
|
||||
await LogoutService().logout();
|
||||
}
|
||||
|
||||
void _ensureControllers(UserProfile profile) {
|
||||
@@ -605,7 +618,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 16),
|
||||
child: LanguageSelector(compact: true),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ThemeToggleButton(),
|
||||
SizedBox(height: 8),
|
||||
LanguageSelector(compact: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -617,7 +637,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _ink,
|
||||
@@ -644,7 +664,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _ink,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -690,8 +710,12 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tr('msg.userfront.profile.greeting', params: {'name': name}),
|
||||
style: const TextStyle(
|
||||
_renderTranslatedText(
|
||||
'msg.userfront.profile.greeting',
|
||||
fallback: 'Hello, {{name}}.',
|
||||
values: {'name': name},
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _ink,
|
||||
@@ -982,12 +1006,17 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
tr('msg.userfront.profile.password.subtitle'),
|
||||
style: const TextStyle(color: Color(0xFF6B7280)),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_buildPasswordPolicyDescription(),
|
||||
style: const TextStyle(color: Color(0xFF6B7280), fontSize: 12),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
@@ -1217,14 +1246,35 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
return Scaffold(
|
||||
backgroundColor: _subtle,
|
||||
appBar: AppBar(
|
||||
leading: isWide
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
_isDesktopSideMenuOpen ? Icons.menu_open : Icons.menu,
|
||||
),
|
||||
tooltip: _isDesktopSideMenuOpen
|
||||
? tr('ui.common.collapse')
|
||||
: '펼치기',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isDesktopSideMenuOpen = !_isDesktopSideMenuOpen;
|
||||
});
|
||||
},
|
||||
)
|
||||
: Builder(
|
||||
builder: (context) => IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
tooltip: MaterialLocalizations.of(
|
||||
context,
|
||||
).openAppDrawerTooltip,
|
||||
onPressed: () => Scaffold.of(context).openDrawer(),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
tr('ui.userfront.app_title'),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: _surface,
|
||||
foregroundColor: Colors.black,
|
||||
actions: [
|
||||
const ThemeToggleButton(compact: true),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.home_outlined),
|
||||
tooltip: tr('ui.userfront.nav.dashboard'),
|
||||
@@ -1245,7 +1295,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
drawer: isWide ? null : Drawer(child: _buildSideMenu(context)),
|
||||
body: Row(
|
||||
children: [
|
||||
if (isWide) SizedBox(width: 240, child: _buildSideMenu(context)),
|
||||
if (isWide && _isDesktopSideMenuOpen)
|
||||
SizedBox(width: 240, child: _buildSideMenu(context)),
|
||||
Expanded(child: _buildContent(profile, isUpdating)),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user