forked from baron/baron-sso
feat: i18n 개선 및 userfront 로그인/로케일 보완
This commit is contained in:
51
userfront/lib/core/i18n/locale_gate.dart
Normal file
51
userfront/lib/core/i18n/locale_gate.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:easy_localization/easy_localization.dart' hide tr;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:userfront/i18n.dart';
|
||||
import '../services/web_window.dart';
|
||||
import 'locale_storage.dart';
|
||||
import 'locale_utils.dart';
|
||||
|
||||
class LocaleGate extends StatefulWidget {
|
||||
const LocaleGate({super.key, required this.localeCode, required this.child});
|
||||
|
||||
final String localeCode;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<LocaleGate> createState() => _LocaleGateState();
|
||||
}
|
||||
|
||||
class _LocaleGateState extends State<LocaleGate> {
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_applyLocale();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(LocaleGate oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.localeCode != widget.localeCode) {
|
||||
_applyLocale();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _applyLocale() async {
|
||||
final normalized = normalizeLocaleCode(widget.localeCode);
|
||||
LocaleStorage.write(normalized);
|
||||
webWindow.setTitle(
|
||||
tr('ui.userfront.app_title'),
|
||||
);
|
||||
if (context.locale.languageCode == normalized) {
|
||||
return;
|
||||
}
|
||||
await context.setLocale(Locale(normalized));
|
||||
webWindow.setTitle(
|
||||
tr('ui.userfront.app_title'),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
7
userfront/lib/core/i18n/locale_storage.dart
Normal file
7
userfront/lib/core/i18n/locale_storage.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'locale_storage_stub.dart'
|
||||
if (dart.library.html) 'locale_storage_web.dart';
|
||||
|
||||
abstract class LocaleStorage {
|
||||
static String? read() => localeStorage.read();
|
||||
static void write(String locale) => localeStorage.write(locale);
|
||||
}
|
||||
11
userfront/lib/core/i18n/locale_storage_stub.dart
Normal file
11
userfront/lib/core/i18n/locale_storage_stub.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
class LocaleStorageImpl {
|
||||
String? _locale;
|
||||
|
||||
String? read() => _locale;
|
||||
|
||||
void write(String locale) {
|
||||
_locale = locale;
|
||||
}
|
||||
}
|
||||
|
||||
final localeStorage = LocaleStorageImpl();
|
||||
120
userfront/lib/core/i18n/locale_storage_web.dart
Normal file
120
userfront/lib/core/i18n/locale_storage_web.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
// ignore_for_file: avoid_web_libraries_in_flutter
|
||||
|
||||
import 'dart:html' as html;
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class LocaleStorageImpl {
|
||||
static const _key = 'locale';
|
||||
static const _legacyKey = 'baron_locale';
|
||||
static final Map<String, String> _memory = {};
|
||||
static bool _forceMemory = false;
|
||||
static bool _forceSession = false;
|
||||
|
||||
@visibleForTesting
|
||||
static void forceMemoryStorageForTests(bool value) {
|
||||
_forceMemory = value;
|
||||
if (!value) {
|
||||
_memory.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static void forceSessionStorageForTests(bool value) {
|
||||
_forceSession = value;
|
||||
}
|
||||
|
||||
String? _read(String key) {
|
||||
if (!_forceMemory && !_forceSession) {
|
||||
try {
|
||||
return html.window.localStorage[key];
|
||||
} catch (_) {
|
||||
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
|
||||
try {
|
||||
return html.window.sessionStorage[key];
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!_forceMemory) {
|
||||
try {
|
||||
return html.window.sessionStorage[key];
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
return _memory[key];
|
||||
}
|
||||
|
||||
void _write(String key, String value) {
|
||||
if (!_forceMemory && !_forceSession) {
|
||||
try {
|
||||
html.window.localStorage[key] = value;
|
||||
return;
|
||||
} catch (_) {
|
||||
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
|
||||
try {
|
||||
html.window.sessionStorage[key] = value;
|
||||
return;
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!_forceMemory) {
|
||||
try {
|
||||
html.window.sessionStorage[key] = value;
|
||||
return;
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
_memory[key] = value;
|
||||
}
|
||||
|
||||
void _remove(String key) {
|
||||
if (!_forceMemory && !_forceSession) {
|
||||
try {
|
||||
html.window.localStorage.remove(key);
|
||||
return;
|
||||
} catch (_) {
|
||||
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
|
||||
try {
|
||||
html.window.sessionStorage.remove(key);
|
||||
return;
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!_forceMemory) {
|
||||
try {
|
||||
html.window.sessionStorage.remove(key);
|
||||
return;
|
||||
} catch (_) {
|
||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||
}
|
||||
}
|
||||
_memory.remove(key);
|
||||
}
|
||||
|
||||
String? read() {
|
||||
final current = _read(_key);
|
||||
if (current != null && current.isNotEmpty) {
|
||||
return current;
|
||||
}
|
||||
final legacy = _read(_legacyKey);
|
||||
if (legacy != null && legacy.isNotEmpty) {
|
||||
_write(_key, legacy);
|
||||
_remove(_legacyKey);
|
||||
return legacy;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void write(String locale) {
|
||||
_write(_key, locale);
|
||||
}
|
||||
}
|
||||
|
||||
final localeStorage = LocaleStorageImpl();
|
||||
69
userfront/lib/core/i18n/locale_utils.dart
Normal file
69
userfront/lib/core/i18n/locale_utils.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'locale_storage.dart';
|
||||
|
||||
const supportedLocaleCodes = ['en', 'ko'];
|
||||
const defaultLocaleCode = 'en';
|
||||
|
||||
String normalizeLocaleCode(String? code) {
|
||||
if (code == null || code.isEmpty) {
|
||||
return defaultLocaleCode;
|
||||
}
|
||||
final normalized = code.toLowerCase();
|
||||
if (normalized == 'ko' || normalized.startsWith('ko-')) {
|
||||
return 'ko';
|
||||
}
|
||||
if (normalized == 'en' || normalized.startsWith('en-')) {
|
||||
return 'en';
|
||||
}
|
||||
return defaultLocaleCode;
|
||||
}
|
||||
|
||||
String resolvePreferredLocaleCode() {
|
||||
final stored = LocaleStorage.read();
|
||||
if (stored != null && supportedLocaleCodes.contains(stored)) {
|
||||
return stored;
|
||||
}
|
||||
final deviceLocale = PlatformDispatcher.instance.locale;
|
||||
return normalizeLocaleCode(deviceLocale.languageCode);
|
||||
}
|
||||
|
||||
String? extractLocaleFromPath(Uri uri) {
|
||||
if (uri.pathSegments.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final code = uri.pathSegments.first.toLowerCase();
|
||||
if (supportedLocaleCodes.contains(code)) {
|
||||
return code;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String stripLocalePath(Uri uri) {
|
||||
final segments = uri.pathSegments;
|
||||
if (segments.isNotEmpty && supportedLocaleCodes.contains(segments.first)) {
|
||||
final rest = segments.skip(1).join('/');
|
||||
if (rest.isEmpty) {
|
||||
return '/';
|
||||
}
|
||||
return '/$rest';
|
||||
}
|
||||
return uri.path;
|
||||
}
|
||||
|
||||
String buildLocalizedPath(String localeCode, Uri uri) {
|
||||
final segments = uri.pathSegments;
|
||||
Iterable<String> restSegments = segments;
|
||||
if (segments.isNotEmpty) {
|
||||
final head = segments.first.toLowerCase();
|
||||
if (supportedLocaleCodes.contains(head) || head.length == 2) {
|
||||
restSegments = segments.skip(1);
|
||||
}
|
||||
}
|
||||
final newSegments = [localeCode, ...restSegments];
|
||||
final path = '/${newSegments.join('/')}';
|
||||
if (uri.queryParameters.isEmpty) {
|
||||
return path;
|
||||
}
|
||||
return Uri(path: path, queryParameters: uri.queryParameters).toString();
|
||||
}
|
||||
22
userfront/lib/core/i18n/toml_asset_loader.dart
Normal file
22
userfront/lib/core/i18n/toml_asset_loader.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:toml/toml.dart';
|
||||
|
||||
class TomlAssetLoader extends AssetLoader {
|
||||
const TomlAssetLoader();
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> load(String path, Locale locale) async {
|
||||
final assetPath = '$path/${locale.languageCode}.toml';
|
||||
try {
|
||||
final content = await rootBundle.loadString(assetPath);
|
||||
final document = TomlDocument.parse(content);
|
||||
return document.toMap();
|
||||
} catch (e) {
|
||||
// 로딩 실패 시 빈 맵을 반환해 렌더링을 지속합니다.
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,18 +11,34 @@ class AuthProxyService {
|
||||
if (!dotenv.isInitialized) {
|
||||
return fallback;
|
||||
}
|
||||
return dotenv.env[key] ?? fallback;
|
||||
final value = dotenv.env[key];
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return fallback;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static String _fallbackOrigin() {
|
||||
try {
|
||||
final origin = Uri.base.origin;
|
||||
if (origin.isNotEmpty && origin != 'null') {
|
||||
return origin;
|
||||
}
|
||||
} catch (_) {}
|
||||
return 'http://sso.hmac.kr';
|
||||
}
|
||||
|
||||
static String get _baseUrl {
|
||||
final rawUrl = _envOrDefault('BACKEND_URL', 'https://sso.hmac.kr');
|
||||
final rawUrl = _envOrDefault('BACKEND_URL', _fallbackOrigin());
|
||||
// 배포 환경에서 $ 기호나 공백이 섞여 들어오는 경우를 방지하기 위해 정제합니다.
|
||||
return rawUrl.replaceAll(r'$', '').trim().replaceAll(RegExp(r'/$'), '');
|
||||
}
|
||||
|
||||
static bool get _isProd {
|
||||
final env = _envOrDefault('APP_ENV', 'dev').toLowerCase();
|
||||
return env == 'prod' || env == 'production';
|
||||
}
|
||||
|
||||
static bool get isProdEnv => _isProd;
|
||||
static bool _shouldSendDrySend(bool? drySend) {
|
||||
if (_isProd) {
|
||||
@@ -76,13 +92,14 @@ class AuthProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<int> getSessionStatus({String? token, bool useCookie = false}) async {
|
||||
static Future<int> getSessionStatus({
|
||||
String? token,
|
||||
bool useCookie = false,
|
||||
}) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/user/me');
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
try {
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (!useCookie && token != null && token.isNotEmpty) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
@@ -100,12 +117,9 @@ class AuthProxyService {
|
||||
bool? drySend,
|
||||
}) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/enchanted-link/init');
|
||||
final userfrontUrl = _envOrDefault('USERFRONT_URL', 'http://sso.hmac.kr');
|
||||
|
||||
final body = <String, dynamic>{
|
||||
'loginId': loginId,
|
||||
'uri': userfrontUrl,
|
||||
};
|
||||
final userfrontUrl = _envOrDefault('USERFRONT_URL', _fallbackOrigin());
|
||||
|
||||
final body = <String, dynamic>{'loginId': loginId, 'uri': userfrontUrl};
|
||||
if (_shouldSendDrySend(drySend)) {
|
||||
body['drySend'] = true;
|
||||
}
|
||||
@@ -115,7 +129,7 @@ class AuthProxyService {
|
||||
if (codeOnly == true) {
|
||||
body['codeOnly'] = true;
|
||||
}
|
||||
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
@@ -133,15 +147,15 @@ class AuthProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> pollEnchantedLink(String pendingRef) async {
|
||||
static Future<Map<String, dynamic>> pollEnchantedLink(
|
||||
String pendingRef,
|
||||
) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/enchanted-link/poll');
|
||||
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'pendingRef': pendingRef,
|
||||
}),
|
||||
body: jsonEncode({'pendingRef': pendingRef}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
@@ -157,16 +171,16 @@ class AuthProxyService {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> verifyMagicLink(String token, {bool verifyOnly = false}) async {
|
||||
static Future<Map<String, dynamic>> verifyMagicLink(
|
||||
String token, {
|
||||
bool verifyOnly = false,
|
||||
}) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/magic-link/verify');
|
||||
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'token': token,
|
||||
'verifyOnly': verifyOnly,
|
||||
}),
|
||||
body: jsonEncode({'token': token, 'verifyOnly': verifyOnly}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
@@ -223,10 +237,7 @@ class AuthProxyService {
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'shortCode': shortCode,
|
||||
'verifyOnly': verifyOnly,
|
||||
}),
|
||||
body: jsonEncode({'shortCode': shortCode, 'verifyOnly': verifyOnly}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
@@ -240,13 +251,18 @@ class AuthProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> loginWithPassword(String loginId, String password, {String? loginChallenge}) async {
|
||||
static Future<Map<String, dynamic>> loginWithPassword(
|
||||
String loginId,
|
||||
String password, {
|
||||
String? loginChallenge,
|
||||
}) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/password/login');
|
||||
|
||||
|
||||
final payload = {
|
||||
'loginId': loginId,
|
||||
'password': password,
|
||||
if (loginChallenge != null && loginChallenge.isNotEmpty) 'login_challenge': loginChallenge,
|
||||
if (loginChallenge != null && loginChallenge.isNotEmpty)
|
||||
'login_challenge': loginChallenge,
|
||||
};
|
||||
|
||||
final response = await http.post(
|
||||
@@ -267,13 +283,17 @@ class AuthProxyService {
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.login_failed',
|
||||
fallback: '로그인에 실패했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
static Future<Map<String, dynamic>> getConsentInfo(String consentChallenge) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/consent').replace(queryParameters: {'consent_challenge': consentChallenge});
|
||||
|
||||
static Future<Map<String, dynamic>> getConsentInfo(
|
||||
String consentChallenge,
|
||||
) async {
|
||||
final url = Uri.parse(
|
||||
'$_baseUrl/api/v1/auth/consent',
|
||||
).replace(queryParameters: {'consent_challenge': consentChallenge});
|
||||
final response = await http.get(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
@@ -287,17 +307,17 @@ class AuthProxyService {
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.consent_fetch',
|
||||
fallback: '동의 정보를 가져오지 못했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> acceptConsent(String consentChallenge, {List<String>? grantScope}) async {
|
||||
static Future<Map<String, dynamic>> acceptConsent(
|
||||
String consentChallenge, {
|
||||
List<String>? grantScope,
|
||||
}) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/consent/accept');
|
||||
final body = <String, dynamic>{
|
||||
'consent_challenge': consentChallenge,
|
||||
};
|
||||
final body = <String, dynamic>{'consent_challenge': consentChallenge};
|
||||
if (grantScope != null) {
|
||||
body['grant_scope'] = grantScope;
|
||||
}
|
||||
@@ -316,17 +336,16 @@ class AuthProxyService {
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.consent_accept',
|
||||
fallback: '동의 처리에 실패했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> rejectConsent(String consentChallenge) async {
|
||||
static Future<Map<String, dynamic>> rejectConsent(
|
||||
String consentChallenge,
|
||||
) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/consent/reject');
|
||||
final body = <String, dynamic>{
|
||||
'consent_challenge': consentChallenge,
|
||||
};
|
||||
final body = <String, dynamic>{'consent_challenge': consentChallenge};
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
@@ -342,7 +361,6 @@ class AuthProxyService {
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.consent_reject',
|
||||
fallback: '동의 거부에 실패했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -353,9 +371,7 @@ class AuthProxyService {
|
||||
String? token,
|
||||
}) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/oidc/login/accept');
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (token != null && token.isNotEmpty) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
@@ -364,9 +380,7 @@ class AuthProxyService {
|
||||
final response = await client.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: jsonEncode({
|
||||
'login_challenge': loginChallenge,
|
||||
}),
|
||||
body: jsonEncode({'login_challenge': loginChallenge}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
@@ -377,7 +391,6 @@ class AuthProxyService {
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.oidc_accept',
|
||||
fallback: 'OIDC 로그인 승인에 실패했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -386,8 +399,10 @@ class AuthProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static Future<Map<String, dynamic>> initiatePasswordReset(String loginId, {bool? drySend}) async {
|
||||
static Future<Map<String, dynamic>> initiatePasswordReset(
|
||||
String loginId, {
|
||||
bool? drySend,
|
||||
}) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/password/reset/initiate');
|
||||
final response = await http.post(
|
||||
url,
|
||||
@@ -406,7 +421,6 @@ class AuthProxyService {
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.password_reset_init',
|
||||
fallback: '비밀번호 재설정을 시작하지 못했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -424,7 +438,9 @@ class AuthProxyService {
|
||||
if (token != null && token.isNotEmpty) {
|
||||
query['token'] = token;
|
||||
}
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/password/reset/complete').replace(queryParameters: query);
|
||||
final url = Uri.parse(
|
||||
'$_baseUrl/api/v1/auth/password/reset/complete',
|
||||
).replace(queryParameters: query);
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
@@ -439,7 +455,6 @@ class AuthProxyService {
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.password_reset_complete',
|
||||
fallback: '비밀번호 재설정에 실패했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -447,13 +462,11 @@ class AuthProxyService {
|
||||
|
||||
static Future<void> sendSms(String phoneNumber) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/sms');
|
||||
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'phoneNumber': phoneNumber,
|
||||
}),
|
||||
body: jsonEncode({'phoneNumber': phoneNumber}),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
@@ -465,16 +478,16 @@ class AuthProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> verifySmsCode(String phoneNumber, String code) async {
|
||||
static Future<Map<String, dynamic>> verifySmsCode(
|
||||
String phoneNumber,
|
||||
String code,
|
||||
) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/verify-sms');
|
||||
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'phoneNumber': phoneNumber,
|
||||
'code': code,
|
||||
}),
|
||||
body: jsonEncode({'phoneNumber': phoneNumber, 'code': code}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
@@ -532,10 +545,10 @@ class AuthProxyService {
|
||||
String? token,
|
||||
bool withCredentials = false,
|
||||
}) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/qr/approve'); // Mapping to ScanQRLogin on backend
|
||||
final payload = <String, dynamic>{
|
||||
'pendingRef': pendingRef,
|
||||
};
|
||||
final url = Uri.parse(
|
||||
'$_baseUrl/api/v1/auth/qr/approve',
|
||||
); // Mapping to ScanQRLogin on backend
|
||||
final payload = <String, dynamic>{'pendingRef': pendingRef};
|
||||
if (token != null && token.isNotEmpty) {
|
||||
payload['token'] = token;
|
||||
}
|
||||
@@ -593,7 +606,7 @@ class AuthProxyService {
|
||||
String? displayName,
|
||||
}) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/admin/users');
|
||||
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {
|
||||
@@ -617,7 +630,10 @@ class AuthProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<dynamic>> listUsers(String adminPassword, {String? query}) async {
|
||||
static Future<List<dynamic>> listUsers(
|
||||
String adminPassword, {
|
||||
String? query,
|
||||
}) async {
|
||||
var uri = Uri.parse('$_baseUrl/api/v1/admin/users');
|
||||
if (query != null && query.isNotEmpty) {
|
||||
uri = uri.replace(queryParameters: {'text': query});
|
||||
@@ -646,7 +662,7 @@ class AuthProxyService {
|
||||
static Future<void> deleteUser(String adminPassword, String loginId) async {
|
||||
final encodedId = Uri.encodeComponent(loginId);
|
||||
final url = Uri.parse('$_baseUrl/api/v1/admin/users/$encodedId');
|
||||
|
||||
|
||||
final response = await http.delete(
|
||||
url,
|
||||
headers: {
|
||||
@@ -664,10 +680,14 @@ class AuthProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> updateUserStatus(String adminPassword, String loginId, String status) async {
|
||||
static Future<void> updateUserStatus(
|
||||
String adminPassword,
|
||||
String loginId,
|
||||
String status,
|
||||
) async {
|
||||
final encodedId = Uri.encodeComponent(loginId);
|
||||
final url = Uri.parse('$_baseUrl/api/v1/admin/users/$encodedId/status');
|
||||
|
||||
|
||||
final response = await http.patch(
|
||||
url,
|
||||
headers: {
|
||||
@@ -695,7 +715,7 @@ class AuthProxyService {
|
||||
}) async {
|
||||
final encodedId = Uri.encodeComponent(loginId);
|
||||
final url = Uri.parse('$_baseUrl/api/v1/admin/users/$encodedId');
|
||||
|
||||
|
||||
final body = <String, dynamic>{};
|
||||
if (email != null) body['email'] = email;
|
||||
if (phone != null) body['phone'] = phone;
|
||||
@@ -725,18 +745,13 @@ class AuthProxyService {
|
||||
final token = AuthTokenStore.getToken();
|
||||
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
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,
|
||||
);
|
||||
final response = await client.get(url, headers: headers);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
@@ -758,18 +773,13 @@ class AuthProxyService {
|
||||
final token = AuthTokenStore.getToken();
|
||||
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (!useCookie && token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await client.delete(
|
||||
url,
|
||||
headers: headers,
|
||||
);
|
||||
final response = await client.delete(url, headers: headers);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
final errorBody = jsonDecode(response.body);
|
||||
@@ -777,7 +787,6 @@ class AuthProxyService {
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.linked_app_revoke',
|
||||
fallback: '연동 해지에 실패했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -786,7 +795,11 @@ class AuthProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> sendLog(String level, String message, {Map<String, dynamic>? data}) async {
|
||||
static Future<void> sendLog(
|
||||
String level,
|
||||
String message, {
|
||||
Map<String, dynamic>? data,
|
||||
}) async {
|
||||
if (!_canSendClientLog()) {
|
||||
return;
|
||||
}
|
||||
@@ -808,11 +821,15 @@ class AuthProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> logError(String message, {dynamic error, StackTrace? stackTrace}) async {
|
||||
static Future<void> logError(
|
||||
String message, {
|
||||
dynamic error,
|
||||
StackTrace? stackTrace,
|
||||
}) async {
|
||||
final data = <String, dynamic>{};
|
||||
if (error != null) data['error'] = error.toString();
|
||||
if (stackTrace != null) data['stack'] = stackTrace.toString();
|
||||
|
||||
|
||||
await sendLog('ERROR', message, data: data);
|
||||
}
|
||||
|
||||
@@ -861,7 +878,7 @@ class AuthProxyService {
|
||||
static Future<void> sendSignupCode(String target, String type) async {
|
||||
final path = type == 'email' ? 'send-email-code' : 'send-sms-code';
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/signup/$path');
|
||||
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
@@ -877,17 +894,17 @@ class AuthProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> verifySignupCode(String target, String type, String code) async {
|
||||
static Future<bool> verifySignupCode(
|
||||
String target,
|
||||
String type,
|
||||
String code,
|
||||
) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/signup/verify-code');
|
||||
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'target': target,
|
||||
'type': type,
|
||||
'code': code,
|
||||
}),
|
||||
body: jsonEncode({'target': target, 'type': type, 'code': code}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
@@ -908,7 +925,7 @@ class AuthProxyService {
|
||||
required bool termsAccepted,
|
||||
}) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/auth/signup');
|
||||
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class WebWindow {
|
||||
void setTitle(String title) {}
|
||||
|
||||
void redirectTo(String url) {}
|
||||
|
||||
void alert(String message) {}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
import 'dart:html' as html;
|
||||
|
||||
class WebWindow {
|
||||
void setTitle(String title) {
|
||||
html.document.title = title;
|
||||
}
|
||||
|
||||
void redirectTo(String url) {
|
||||
html.window.location.href = url;
|
||||
}
|
||||
|
||||
65
userfront/lib/core/widgets/language_selector.dart
Normal file
65
userfront/lib/core/widgets/language_selector.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'package:easy_localization/easy_localization.dart' hide tr;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:userfront/i18n.dart';
|
||||
|
||||
import '../i18n/locale_storage.dart';
|
||||
import '../i18n/locale_utils.dart';
|
||||
|
||||
class LanguageSelector extends StatelessWidget {
|
||||
const LanguageSelector({super.key, this.compact = false});
|
||||
|
||||
final bool compact;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final current = context.locale.languageCode;
|
||||
final items = [
|
||||
DropdownMenuItem(
|
||||
value: 'ko',
|
||||
child: Text(tr('ui.common.language_ko')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'en',
|
||||
child: Text(tr('ui.common.language_en', fallback: 'English')),
|
||||
),
|
||||
];
|
||||
|
||||
final iconSize = compact ? 16.0 : 18.0;
|
||||
final dropdown = DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: current,
|
||||
items: items,
|
||||
isDense: true,
|
||||
icon: Icon(Icons.arrow_drop_down, size: compact ? 18 : 20),
|
||||
onChanged: (value) async {
|
||||
if (value == null || value == current) {
|
||||
return;
|
||||
}
|
||||
LocaleStorage.write(value);
|
||||
await context.setLocale(Locale(value));
|
||||
final uri = GoRouterState.of(context).uri;
|
||||
final target = buildLocalizedPath(value, uri);
|
||||
if (context.mounted) {
|
||||
context.go(target);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: compact ? 0 : 2),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: compact ? 24 : 28),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.language, size: iconSize),
|
||||
const SizedBox(width: 6),
|
||||
dropdown,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user