첫 커밋: 로컬 프로젝트 업로드
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:userfront/core/i18n/locale_utils.dart';
|
||||
import 'package:userfront/core/services/auth_proxy_service.dart';
|
||||
|
||||
bool shouldRouteTenantAccessErrorToErrorScreen(Object error) {
|
||||
return error is AuthProxyException && error.errorCode == 'tenant_not_allowed';
|
||||
}
|
||||
|
||||
bool shouldRouteConsentErrorToErrorScreen(Object error) {
|
||||
return shouldRouteTenantAccessErrorToErrorScreen(error);
|
||||
}
|
||||
|
||||
String buildTenantAccessErrorPath(Object error, Uri baseUri) {
|
||||
final authError = error as AuthProxyException;
|
||||
final localeCode =
|
||||
extractLocaleFromPath(baseUri) ?? resolvePreferredLocaleCode();
|
||||
return buildLocalizedPath(
|
||||
localeCode,
|
||||
Uri(
|
||||
path: '/error',
|
||||
queryParameters: {
|
||||
'error': authError.errorCode,
|
||||
'error_description': authError.message,
|
||||
if (authError.details != null) 'details': jsonEncode(authError.details),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
bool shouldPromoteCookieSession({
|
||||
required String? currentToken,
|
||||
required String? loginChallenge,
|
||||
}) {
|
||||
final hasToken = currentToken != null && currentToken.trim().isNotEmpty;
|
||||
final hasChallenge =
|
||||
loginChallenge != null && loginChallenge.trim().isNotEmpty;
|
||||
|
||||
// 토큰 기반 세션이 이미 확보된 일반 로그인 흐름에서는
|
||||
// 뒤늦은 쿠키 세션 승격이 토큰을 덮어쓰지 않도록 차단합니다.
|
||||
if (hasToken && !hasChallenge) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
enum LoginChallengeSource { widget, uriQuery, rawSearch, rawHref, missing }
|
||||
|
||||
class LoginChallengeResolution {
|
||||
final String? value;
|
||||
final LoginChallengeSource source;
|
||||
final bool uriHasLoginChallenge;
|
||||
final bool rawSearchHasLoginChallenge;
|
||||
final bool rawHrefHasLoginChallenge;
|
||||
|
||||
const LoginChallengeResolution({
|
||||
required this.value,
|
||||
required this.source,
|
||||
required this.uriHasLoginChallenge,
|
||||
required this.rawSearchHasLoginChallenge,
|
||||
required this.rawHrefHasLoginChallenge,
|
||||
});
|
||||
|
||||
Map<String, Object?> toDiagnostics() {
|
||||
return {
|
||||
'resolved_value_len': value?.length ?? 0,
|
||||
'resolved_source': source.name,
|
||||
'uri_has_login_challenge': uriHasLoginChallenge,
|
||||
'raw_search_has_login_challenge': rawSearchHasLoginChallenge,
|
||||
'raw_href_has_login_challenge': rawHrefHasLoginChallenge,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
LoginChallengeResolution resolveLoginChallenge({
|
||||
String? widgetLoginChallenge,
|
||||
required Uri uri,
|
||||
String? rawSearch,
|
||||
String? rawHref,
|
||||
}) {
|
||||
final widgetValue = _normalizeChallenge(widgetLoginChallenge);
|
||||
if (widgetValue != null) {
|
||||
return const LoginChallengeResolution(
|
||||
value: null,
|
||||
source: LoginChallengeSource.widget,
|
||||
uriHasLoginChallenge: false,
|
||||
rawSearchHasLoginChallenge: false,
|
||||
rawHrefHasLoginChallenge: false,
|
||||
).copyWith(value: widgetValue);
|
||||
}
|
||||
|
||||
final uriValue = _normalizeChallenge(uri.queryParameters['login_challenge']);
|
||||
if (uriValue != null) {
|
||||
return const LoginChallengeResolution(
|
||||
value: null,
|
||||
source: LoginChallengeSource.uriQuery,
|
||||
uriHasLoginChallenge: true,
|
||||
rawSearchHasLoginChallenge: false,
|
||||
rawHrefHasLoginChallenge: false,
|
||||
).copyWith(value: uriValue);
|
||||
}
|
||||
|
||||
final rawSearchValue = _normalizeChallenge(
|
||||
_extractQueryParamFromRawQuery(rawSearch, 'login_challenge'),
|
||||
);
|
||||
if (rawSearchValue != null) {
|
||||
return const LoginChallengeResolution(
|
||||
value: null,
|
||||
source: LoginChallengeSource.rawSearch,
|
||||
uriHasLoginChallenge: false,
|
||||
rawSearchHasLoginChallenge: true,
|
||||
rawHrefHasLoginChallenge: false,
|
||||
).copyWith(value: rawSearchValue);
|
||||
}
|
||||
|
||||
final rawHrefValue = _normalizeChallenge(
|
||||
_extractQueryParamFromRawHref(rawHref, 'login_challenge'),
|
||||
);
|
||||
if (rawHrefValue != null) {
|
||||
return const LoginChallengeResolution(
|
||||
value: null,
|
||||
source: LoginChallengeSource.rawHref,
|
||||
uriHasLoginChallenge: false,
|
||||
rawSearchHasLoginChallenge: false,
|
||||
rawHrefHasLoginChallenge: true,
|
||||
).copyWith(value: rawHrefValue);
|
||||
}
|
||||
|
||||
return const LoginChallengeResolution(
|
||||
value: null,
|
||||
source: LoginChallengeSource.missing,
|
||||
uriHasLoginChallenge: false,
|
||||
rawSearchHasLoginChallenge: false,
|
||||
rawHrefHasLoginChallenge: false,
|
||||
);
|
||||
}
|
||||
|
||||
String? _normalizeChallenge(String? value) {
|
||||
final trimmed = value?.trim();
|
||||
if (trimmed == null || trimmed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
String? _extractQueryParamFromRawHref(String? rawHref, String key) {
|
||||
final href = rawHref?.trim();
|
||||
if (href == null || href.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final parsed = Uri.tryParse(href);
|
||||
final fromParsed = parsed?.queryParameters[key];
|
||||
final normalizedParsed = _normalizeChallenge(fromParsed);
|
||||
if (normalizedParsed != null) {
|
||||
return normalizedParsed;
|
||||
}
|
||||
|
||||
final question = href.indexOf('?');
|
||||
if (question < 0) {
|
||||
return null;
|
||||
}
|
||||
final hash = href.indexOf('#', question + 1);
|
||||
final rawQuery = hash < 0
|
||||
? href.substring(question + 1)
|
||||
: href.substring(question + 1, hash);
|
||||
return _extractQueryParamFromRawQuery(rawQuery, key);
|
||||
}
|
||||
|
||||
String? _extractQueryParamFromRawQuery(String? rawQuery, String key) {
|
||||
final query = rawQuery?.trim();
|
||||
if (query == null || query.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final normalizedQuery = query.startsWith('?') ? query.substring(1) : query;
|
||||
if (normalizedQuery.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final parsed = Uri.splitQueryString(normalizedQuery);
|
||||
final value = _normalizeChallenge(parsed[key]);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
} catch (_) {
|
||||
// URI 파싱이 실패하면 수동 파싱으로 보완합니다.
|
||||
}
|
||||
|
||||
for (final pair in normalizedQuery.split('&')) {
|
||||
if (pair.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final equalIndex = pair.indexOf('=');
|
||||
final rawKey = equalIndex < 0 ? pair : pair.substring(0, equalIndex);
|
||||
final decodedKey = _decodeQueryComponentSafe(rawKey);
|
||||
if (decodedKey != key) {
|
||||
continue;
|
||||
}
|
||||
if (equalIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
final rawValue = pair.substring(equalIndex + 1);
|
||||
final decodedValue = _normalizeChallenge(
|
||||
_decodeQueryComponentSafe(rawValue),
|
||||
);
|
||||
if (decodedValue != null) {
|
||||
return decodedValue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _decodeQueryComponentSafe(String value) {
|
||||
try {
|
||||
return Uri.decodeQueryComponent(value);
|
||||
} catch (_) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
extension on LoginChallengeResolution {
|
||||
LoginChallengeResolution copyWith({
|
||||
String? value,
|
||||
LoginChallengeSource? source,
|
||||
bool? uriHasLoginChallenge,
|
||||
bool? rawSearchHasLoginChallenge,
|
||||
bool? rawHrefHasLoginChallenge,
|
||||
}) {
|
||||
return LoginChallengeResolution(
|
||||
value: value ?? this.value,
|
||||
source: source ?? this.source,
|
||||
uriHasLoginChallenge: uriHasLoginChallenge ?? this.uriHasLoginChallenge,
|
||||
rawSearchHasLoginChallenge:
|
||||
rawSearchHasLoginChallenge ?? this.rawSearchHasLoginChallenge,
|
||||
rawHrefHasLoginChallenge:
|
||||
rawHrefHasLoginChallenge ?? this.rawHrefHasLoginChallenge,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import '../../../core/i18n/locale_utils.dart';
|
||||
|
||||
bool isPublicAuthPath(String path, Uri uri) {
|
||||
return path == '/signin' ||
|
||||
path == '/signup' ||
|
||||
path == '/login' ||
|
||||
path == '/registration' ||
|
||||
path == '/verify' ||
|
||||
path == '/verification' ||
|
||||
path == '/verify-complete' ||
|
||||
path.startsWith('/verify/') ||
|
||||
path.startsWith('/l/') ||
|
||||
path == '/approve' ||
|
||||
path.startsWith('/ql/') ||
|
||||
path == '/forgot-password' ||
|
||||
path == '/recovery' ||
|
||||
path == '/reset-password' ||
|
||||
path == '/error' ||
|
||||
path == '/settings' ||
|
||||
path == '/consent' ||
|
||||
path.startsWith('/consent/') ||
|
||||
uri.path.contains('/consent');
|
||||
}
|
||||
|
||||
String? extractLoginShortCode(Uri uri) {
|
||||
final normalizedPath = stripLocalePath(uri);
|
||||
final segments = normalizedPath
|
||||
.split('/')
|
||||
.where((segment) => segment.isNotEmpty)
|
||||
.toList();
|
||||
if (segments.length < 2 || segments.first != 'l') {
|
||||
return null;
|
||||
}
|
||||
return segments[1];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
enum PasswordLoginNextAction { redirectToOidc, acceptOidc, localLogin, invalid }
|
||||
|
||||
PasswordLoginNextAction decidePasswordLoginNextAction({
|
||||
required bool hasLoginChallenge,
|
||||
required String? redirectTo,
|
||||
required String? jwt,
|
||||
}) {
|
||||
final hasRedirectTo = redirectTo != null && redirectTo.isNotEmpty;
|
||||
if (hasRedirectTo) {
|
||||
return PasswordLoginNextAction.redirectToOidc;
|
||||
}
|
||||
|
||||
if (hasLoginChallenge) {
|
||||
return PasswordLoginNextAction.acceptOidc;
|
||||
}
|
||||
|
||||
final hasJwt = jwt != null && jwt.isNotEmpty;
|
||||
if (hasJwt) {
|
||||
return PasswordLoginNextAction.localLogin;
|
||||
}
|
||||
|
||||
return PasswordLoginNextAction.invalid;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import '../../../core/i18n/locale_utils.dart';
|
||||
|
||||
const verificationRoutePath = '/verify';
|
||||
const verificationCompletionRoutePath = '/verify-complete';
|
||||
const verificationCompletionRouteName = 'verify-complete';
|
||||
|
||||
String buildLocalizedVerificationCompletePath(String localeCode) {
|
||||
return '/$localeCode$verificationCompletionRoutePath';
|
||||
}
|
||||
|
||||
bool isDedicatedVerificationRoute(Uri uri) {
|
||||
final path = stripLocalePath(uri);
|
||||
return path == verificationRoutePath ||
|
||||
path == '/verification' ||
|
||||
path.startsWith('/verify/') ||
|
||||
path.startsWith('/l/');
|
||||
}
|
||||
|
||||
bool hasVerificationPayload(Uri uri) {
|
||||
final query = uri.queryParameters;
|
||||
final token = query['t'];
|
||||
final loginId = query['loginId'];
|
||||
final code = query['code'];
|
||||
return (token != null && token.isNotEmpty) ||
|
||||
(loginId != null &&
|
||||
loginId.isNotEmpty &&
|
||||
code != null &&
|
||||
code.isNotEmpty);
|
||||
}
|
||||
|
||||
String? buildDedicatedVerificationRedirect(
|
||||
Uri uri, {
|
||||
required String localeCode,
|
||||
}) {
|
||||
if (isDedicatedVerificationRoute(uri)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final query = uri.queryParameters;
|
||||
final token = query['t'];
|
||||
final loginId = query['loginId'];
|
||||
final code = query['code'];
|
||||
final pendingRef = query['pendingRef'];
|
||||
final sanitizedQuery = <String, String>{};
|
||||
|
||||
if (token != null && token.isNotEmpty) {
|
||||
sanitizedQuery['t'] = token;
|
||||
} else if (loginId != null &&
|
||||
loginId.isNotEmpty &&
|
||||
code != null &&
|
||||
code.isNotEmpty) {
|
||||
sanitizedQuery['loginId'] = loginId;
|
||||
sanitizedQuery['code'] = code;
|
||||
if (pendingRef != null && pendingRef.isNotEmpty) {
|
||||
sanitizedQuery['pendingRef'] = pendingRef;
|
||||
}
|
||||
}
|
||||
|
||||
if (sanitizedQuery.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Uri(
|
||||
path: '/$localeCode$verificationRoutePath',
|
||||
queryParameters: sanitizedQuery,
|
||||
).toString();
|
||||
}
|
||||
Reference in New Issue
Block a user