forked from baron/baron-sso
audit 로그 개선. kratos 코드발급 링크로 전송까지 진행 완료 #104
This commit is contained in:
@@ -8,9 +8,9 @@ import 'package:descope/descope.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import '../../../core/services/audit_service.dart';
|
||||
import '../../../core/services/web_auth_integration.dart';
|
||||
import '../../../core/services/auth_proxy_service.dart';
|
||||
import '../../../core/services/auth_token_store.dart';
|
||||
import '../../../core/notifiers/auth_notifier.dart';
|
||||
import '../../profile/domain/notifiers/profile_notifier.dart';
|
||||
|
||||
@@ -33,10 +33,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
// QR Login Variables
|
||||
String? _qrImageBase64;
|
||||
String? _qrPendingRef;
|
||||
String? _qrUserCode;
|
||||
bool _isQrLoading = false;
|
||||
Timer? _qrPollingTimer;
|
||||
int _qrRemainingSeconds = 0;
|
||||
Timer? _qrCountdownTimer;
|
||||
int _qrPollIntervalMs = 2000;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -47,22 +49,58 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
// Check for tokens (Path Parameter or Legacy Query Parameter)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (widget.verificationToken != null) {
|
||||
final uri = Uri.base;
|
||||
final loginIdParam = uri.queryParameters['loginId'];
|
||||
final codeParam = uri.queryParameters['code'];
|
||||
if (loginIdParam != null && codeParam != null) {
|
||||
_verifyLoginCode(loginIdParam, codeParam);
|
||||
} else if (widget.verificationToken != null) {
|
||||
_verifyToken(widget.verificationToken!);
|
||||
} else {
|
||||
final uri = Uri.base;
|
||||
if (uri.queryParameters.containsKey('t')) {
|
||||
_verifyToken(uri.queryParameters['t']!);
|
||||
}
|
||||
} else if (uri.queryParameters.containsKey('t')) {
|
||||
_verifyToken(uri.queryParameters['t']!);
|
||||
}
|
||||
|
||||
final uri = Uri.base;
|
||||
_tryCookieSession();
|
||||
|
||||
if (uri.queryParameters.containsKey('redirect_url')) {
|
||||
_redirectUrl = uri.queryParameters['redirect_url'];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _tryCookieSession({bool silent = true}) async {
|
||||
if (AuthTokenStore.getToken() != null) {
|
||||
return;
|
||||
}
|
||||
final pendingProvider = AuthTokenStore.getPendingProvider();
|
||||
final provider = pendingProvider ?? AuthTokenStore.getProvider();
|
||||
if (provider == null || !provider.toLowerCase().contains('ory')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await AuthProxyService.checkCookieSession();
|
||||
AuthTokenStore.setCookieMode(provider: provider);
|
||||
AuthTokenStore.clearPendingProvider();
|
||||
if (mounted) {
|
||||
await ref.read(profileProvider.notifier).loadProfile();
|
||||
_onCookieLoginSuccess(provider);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!silent) {
|
||||
_showError("로그인 확인 실패: ${e.toString().replaceFirst("Exception: ", "")}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onCookieLoginSuccess(String provider) {
|
||||
debugPrint("[Auth] Cookie-based login success. Provider: $provider");
|
||||
AuthNotifier.instance.notify();
|
||||
if (mounted) {
|
||||
context.go('/');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to decode JWT and get loginId
|
||||
String _getLoginIdFromJwt(String jwt) {
|
||||
try {
|
||||
@@ -107,6 +145,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
setState(() {
|
||||
_isQrLoading = true;
|
||||
_qrImageBase64 = null;
|
||||
_qrUserCode = null;
|
||||
_qrRemainingSeconds = 0;
|
||||
});
|
||||
|
||||
@@ -117,6 +156,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_qrImageBase64 = res['qrCode'];
|
||||
_qrPendingRef = res['pendingRef'];
|
||||
_qrRemainingSeconds = res['expiresIn'] ?? 300;
|
||||
_qrUserCode = res['userCode']?.toString();
|
||||
final interval = res['interval'];
|
||||
if (interval is int && interval > 0) {
|
||||
_qrPollIntervalMs = interval * 1000;
|
||||
} else {
|
||||
_qrPollIntervalMs = 2000;
|
||||
}
|
||||
_isQrLoading = false;
|
||||
});
|
||||
_startQrPolling();
|
||||
@@ -144,7 +190,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
void _startQrPolling() {
|
||||
_qrPollingTimer?.cancel();
|
||||
_qrPollingTimer = Timer.periodic(const Duration(milliseconds: 1500), (timer) async {
|
||||
_qrPollingTimer = Timer.periodic(Duration(milliseconds: _qrPollIntervalMs), (timer) async {
|
||||
if (_qrPendingRef == null || !mounted || _qrRemainingSeconds <= 0) {
|
||||
timer.cancel();
|
||||
return;
|
||||
@@ -152,6 +198,33 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
try {
|
||||
final res = await AuthProxyService.pollQrStatus(_qrPendingRef!);
|
||||
if (res['error'] == 'slow_down') {
|
||||
final interval = res['interval'];
|
||||
if (interval is int && interval > 0) {
|
||||
final nextIntervalMs = interval * 1000;
|
||||
if (nextIntervalMs != _qrPollIntervalMs) {
|
||||
_qrPollIntervalMs = nextIntervalMs;
|
||||
timer.cancel();
|
||||
_startQrPolling();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
_qrPollIntervalMs += 500;
|
||||
timer.cancel();
|
||||
_startQrPolling();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (res['error'] == 'authorization_pending') {
|
||||
return;
|
||||
}
|
||||
if (res['error'] == 'expired_token') {
|
||||
timer.cancel();
|
||||
_qrCountdownTimer?.cancel();
|
||||
_showError("QR 세션이 만료되었습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (res['status'] == 'ok' && res['sessionJwt'] != null) {
|
||||
timer.cancel();
|
||||
_qrCountdownTimer?.cancel();
|
||||
@@ -233,6 +306,34 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _verifyLoginCode(String loginId, String code) async {
|
||||
final sanitizedLoginId = loginId.replaceAll(' ', '+');
|
||||
debugPrint("[Auth] Starting code verification for loginId: $sanitizedLoginId");
|
||||
try {
|
||||
final res = await AuthProxyService.verifyLoginCode(sanitizedLoginId, code);
|
||||
final jwt = res['sessionJwt'] ?? res['token'];
|
||||
debugPrint("[Auth] Code verification successful for loginId: $sanitizedLoginId");
|
||||
|
||||
if (jwt != null && mounted) {
|
||||
final isJwt = (jwt as String).split('.').length == 3;
|
||||
if (isJwt) {
|
||||
final displayName = _getLoginIdFromJwt(jwt);
|
||||
final dummyUser = DescopeUser(
|
||||
'unknown', [], 0, displayName, null, '', false, '', false, {}, '', '', '', false, 'enabled', [], [], [],
|
||||
);
|
||||
final session = DescopeSession.fromJwt(jwt, jwt, dummyUser);
|
||||
Descope.sessionManager.manageSession(session);
|
||||
}
|
||||
_onLoginSuccess(jwt, provider: res['provider'] as String?);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[Auth] Code verification FAILED for loginId: $sanitizedLoginId. Error: $e");
|
||||
if (mounted) {
|
||||
_showError("Verification failed: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopQrPolling();
|
||||
@@ -271,9 +372,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
try {
|
||||
final res = await AuthProxyService.loginWithPassword(loginId, password);
|
||||
final jwt = res['sessionJwt'];
|
||||
final provider = res['provider'] as String?;
|
||||
if (jwt != null && mounted) {
|
||||
Navigator.of(context).pop(); // 로딩 닫기
|
||||
_onLoginSuccess(jwt);
|
||||
_onLoginSuccess(jwt, provider: provider);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) Navigator.of(context).pop(); // 로딩 닫기
|
||||
@@ -326,11 +428,42 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
// 1. Init via Backend API
|
||||
final initResponse = await AuthProxyService.initEnchantedLink(loginId);
|
||||
final pendingRef = initResponse['pendingRef'];
|
||||
debugPrint("[Auth] Link Sent. PendingRef: $pendingRef");
|
||||
final mode = (initResponse['mode'] ?? '').toString();
|
||||
final provider = (initResponse['provider'] ?? '').toString();
|
||||
final interval = initResponse['interval'];
|
||||
debugPrint("[Auth] Link Sent. PendingRef: $pendingRef, Mode: $mode, Provider: $provider");
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(); // Close Loading
|
||||
|
||||
if (mode == 'link' || provider.toLowerCase().contains('ory')) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(isEmail ? "이메일 전송됨" : "SMS 전송됨"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(isEmail
|
||||
? "입력하신 이메일로 로그인 링크를 보냈습니다."
|
||||
: "입력하신 번호로 로그인 링크를 보냈습니다."),
|
||||
const SizedBox(height: 12),
|
||||
const Text("메일/문자 링크를 열면 이 탭에서 자동으로 로그인됩니다."),
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text("닫기"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
@@ -339,9 +472,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(isEmail
|
||||
? "입력하신 이메일로 로그인 링크를 보냈습니다."
|
||||
: "입력하신 번호로 로그인 링크를 보냈습니다."),
|
||||
Text(isEmail
|
||||
? "입력하신 이메일로 로그인 링크를 보냈습니다."
|
||||
: "입력하신 번호로 로그인 링크를 보냈습니다."),
|
||||
const SizedBox(height: 16),
|
||||
const LinearProgressIndicator(),
|
||||
const SizedBox(height: 16),
|
||||
@@ -349,8 +482,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
onPressed: () {
|
||||
debugPrint("[Auth] Polling canceled by user");
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text("취소")
|
||||
},
|
||||
child: const Text("취소"),
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -358,7 +491,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
);
|
||||
|
||||
// 2. Poll Backend manually
|
||||
_pollForSession(pendingRef);
|
||||
final initialInterval = (interval is int && interval > 0)
|
||||
? Duration(seconds: interval)
|
||||
: const Duration(seconds: 2);
|
||||
_pollForSession(pendingRef, initialInterval: initialInterval);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[Auth] Initialization failed: $e");
|
||||
@@ -371,18 +507,39 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pollForSession(String pendingRef) async {
|
||||
Future<void> _pollForSession(String pendingRef, {Duration? initialInterval}) async {
|
||||
int attempts = 0;
|
||||
const maxAttempts = 60; // 2 minutes
|
||||
var pollInterval = initialInterval ?? const Duration(seconds: 2);
|
||||
debugPrint("[Auth] Starting poll for ref: $pendingRef");
|
||||
|
||||
while (attempts < maxAttempts && mounted) {
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
await Future.delayed(pollInterval);
|
||||
attempts++;
|
||||
|
||||
try {
|
||||
final result = await AuthProxyService.pollEnchantedLink(pendingRef);
|
||||
|
||||
if (result['error'] == 'slow_down') {
|
||||
final interval = result['interval'];
|
||||
if (interval is int && interval > 0) {
|
||||
pollInterval = Duration(seconds: interval);
|
||||
} else {
|
||||
pollInterval += const Duration(seconds: 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (result['error'] == 'authorization_pending') {
|
||||
continue;
|
||||
}
|
||||
if (result['error'] == 'expired_token') {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(); // Close Polling Dialog
|
||||
_showError("Login timed out.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (result['status'] == 'ok') {
|
||||
final jwt = result['sessionJwt'];
|
||||
if (jwt != null) {
|
||||
@@ -452,23 +609,31 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
}
|
||||
}
|
||||
|
||||
void _onLoginSuccess(String token) async {
|
||||
void _onLoginSuccess(String token, {String? provider}) async {
|
||||
if (!mounted) return;
|
||||
|
||||
_logTokenDetails(token);
|
||||
|
||||
final userId = _getUserIdFromJwt(token);
|
||||
final providerName = provider ?? AuthTokenStore.getProvider();
|
||||
final isJwt = token.split('.').length == 3;
|
||||
final isOry = (providerName ?? '').toLowerCase().contains('ory') || !isJwt;
|
||||
|
||||
AuthTokenStore.setToken(token, provider: providerName);
|
||||
AuthTokenStore.clearPendingProvider();
|
||||
|
||||
// [New] 로그인 성공 직후 백엔드에서 전체 프로필 정보를 가져와 세션 업데이트
|
||||
try {
|
||||
// 임시 세션 생성 (API 호출을 위해)
|
||||
final tempUser = DescopeUser(userId, [], 0, 'User', null, '', false, '', false, {}, '', '', '', false, 'enabled', [], [], []);
|
||||
final tempSession = DescopeSession.fromJwt(token, token, tempUser);
|
||||
Descope.sessionManager.manageSession(tempSession);
|
||||
if (!isOry) {
|
||||
// 임시 세션 생성 (API 호출을 위해)
|
||||
final tempUser = DescopeUser(userId, [], 0, 'User', null, '', false, '', false, {}, '', '', '', false, 'enabled', [], [], []);
|
||||
final tempSession = DescopeSession.fromJwt(token, token, tempUser);
|
||||
Descope.sessionManager.manageSession(tempSession);
|
||||
}
|
||||
|
||||
// 백엔드 GetMe 호출 (프로필 노티파이어 사용)
|
||||
final profile = await ref.read(profileProvider.notifier).loadProfile();
|
||||
if (profile != null) {
|
||||
if (profile != null && !isOry) {
|
||||
// 실제 정보로 세션 유저 정보 교체
|
||||
final realUser = DescopeUser(
|
||||
userId, [], 0, profile.name, null, profile.email, false, profile.phone, false, {}, '', '', '', false, 'enabled', [], [], [],
|
||||
@@ -480,14 +645,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
debugPrint("[Auth] Failed to pre-fetch profile: $e");
|
||||
}
|
||||
|
||||
// Record Audit Log
|
||||
AuditService.logEvent(
|
||||
userId: userId,
|
||||
eventType: "LOGIN_SUCCESS",
|
||||
status: "SUCCESS",
|
||||
details: "User logged in via Baron SSO",
|
||||
);
|
||||
|
||||
// 1. Handle Popup Flow
|
||||
if (WebAuthIntegration.isPopup()) {
|
||||
debugPrint("[Auth] Popup detected. Notifying opener and attempting to close.");
|
||||
@@ -680,6 +837,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (_qrUserCode != null) ...[
|
||||
Text(
|
||||
"코드: $_qrUserCode",
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
const Text(
|
||||
"모바일 앱으로 스캔하세요",
|
||||
textAlign: TextAlign.center,
|
||||
@@ -727,4 +892,4 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,13 +110,17 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
if (_isPolicyLoading) {
|
||||
return "비밀번호 정책을 불러오는 중입니다...";
|
||||
}
|
||||
final minLength = (_policy?['minLength'] as int?) ?? 8;
|
||||
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
||||
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
||||
final requiresLower = _policy?['lowercase'] ?? true;
|
||||
final requiresUpper = _policy?['uppercase'] ?? true;
|
||||
final requiresUpper = _policy?['uppercase'] ?? false;
|
||||
final requiresNumber = _policy?['number'] ?? true;
|
||||
final requiresSymbol = _policy?['nonAlphanumeric'] ?? true;
|
||||
|
||||
final parts = <String>["최소 ${minLength}자 이상"];
|
||||
if (minTypes > 0) {
|
||||
parts.add("영문 대/소문자/숫자/특수문자 중 ${minTypes}가지 이상");
|
||||
}
|
||||
if (requiresLower) parts.add("소문자 1개 이상");
|
||||
if (requiresUpper) parts.add("대문자 1개 이상");
|
||||
if (requiresNumber) parts.add("숫자 1개 이상");
|
||||
@@ -182,20 +186,35 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
if (val.isEmpty) {
|
||||
return '비밀번호를 입력해주세요.';
|
||||
}
|
||||
final minLength = (_policy?['minLength'] as int?) ?? 8;
|
||||
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
||||
if (val.length < minLength) {
|
||||
return '비밀번호는 최소 $minLength자 이상이어야 합니다.';
|
||||
}
|
||||
if ((_policy?['lowercase'] ?? true) && !RegExp(r'(?=.*[a-z])').hasMatch(val)) {
|
||||
final hasLower = RegExp(r'[a-z]').hasMatch(val);
|
||||
final hasUpper = RegExp(r'[A-Z]').hasMatch(val);
|
||||
final hasNumber = RegExp(r'[0-9]').hasMatch(val);
|
||||
final hasSymbol = RegExp(r'[\W_]').hasMatch(val);
|
||||
int typeCount = 0;
|
||||
if (hasLower) typeCount++;
|
||||
if (hasUpper) typeCount++;
|
||||
if (hasNumber) typeCount++;
|
||||
if (hasSymbol) typeCount++;
|
||||
|
||||
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
||||
if (minTypes > 0 && typeCount < minTypes) {
|
||||
return '비밀번호는 영문 대/소문자/숫자/특수문자 중 $minTypes가지 이상 포함해야 합니다.';
|
||||
}
|
||||
|
||||
if ((_policy?['lowercase'] ?? true) && !hasLower) {
|
||||
return '최소 1개 이상의 소문자를 포함해야 합니다.';
|
||||
}
|
||||
if ((_policy?['uppercase'] ?? true) && !RegExp(r'(?=.*[A-Z])').hasMatch(val)) {
|
||||
if ((_policy?['uppercase'] ?? false) && !hasUpper) {
|
||||
return '최소 1개 이상의 대문자를 포함해야 합니다.';
|
||||
}
|
||||
if ((_policy?['number'] ?? true) && !RegExp(r'(?=.*\d)').hasMatch(val)) {
|
||||
if ((_policy?['number'] ?? true) && !hasNumber) {
|
||||
return '최소 1개 이상의 숫자를 포함해야 합니다.';
|
||||
}
|
||||
if ((_policy?['nonAlphanumeric'] ?? true) && !RegExp(r'(?=.*[\W_])').hasMatch(val)) {
|
||||
if ((_policy?['nonAlphanumeric'] ?? true) && !hasSymbol) {
|
||||
return '최소 1개 이상의 특수문자를 포함해야 합니다.';
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -781,36 +781,47 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
if (_isPolicyLoading) {
|
||||
return "비밀번호 정책을 불러오는 중입니다...";
|
||||
}
|
||||
final minLength = (_policy?['minLength'] as int?) ?? 8;
|
||||
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
||||
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
||||
final requiresLower = _policy?['lowercase'] ?? true;
|
||||
final requiresUpper = _policy?['uppercase'] ?? true;
|
||||
final requiresUpper = _policy?['uppercase'] ?? false;
|
||||
final requiresNumber = _policy?['number'] ?? true;
|
||||
final requiresSymbol = _policy?['nonAlphanumeric'] ?? true;
|
||||
|
||||
final parts = <String>["최소 $minLength자 이상"];
|
||||
if (minTypes > 0) {
|
||||
parts.add("영문 대/소문자/숫자/특수문자 중 ${minTypes}가지 이상");
|
||||
}
|
||||
if (requiresUpper) parts.add("대문자");
|
||||
if (requiresLower) parts.add("소문자");
|
||||
if (requiresNumber) parts.add("숫자");
|
||||
if (requiresSymbol) parts.add("특수문자");
|
||||
|
||||
return "보안 정책: ${parts.join(', ')}를 각각 최소 1자 이상 포함해야 합니다.";
|
||||
return "보안 정책: ${parts.join(', ')}";
|
||||
}
|
||||
|
||||
Widget _buildStepPassword() {
|
||||
String p = _passwordController.text;
|
||||
|
||||
// Default Policy Fallback
|
||||
final minLength = (_policy?['minLength'] as int?) ?? 8;
|
||||
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
||||
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
||||
final requiresLower = _policy?['lowercase'] ?? true;
|
||||
final requiresUpper = _policy?['uppercase'] ?? true;
|
||||
final requiresUpper = _policy?['uppercase'] ?? false;
|
||||
final requiresNumber = _policy?['number'] ?? true;
|
||||
final requiresSymbol = _policy?['nonAlphanumeric'] ?? true;
|
||||
|
||||
bool hasLength = p.length >= minLength;
|
||||
bool hasUpper = !requiresUpper || p.contains(RegExp(r'[A-Z]'));
|
||||
bool hasLower = !requiresLower || p.contains(RegExp(r'[a-z]'));
|
||||
bool hasDigit = !requiresNumber || p.contains(RegExp(r'[0-9]'));
|
||||
bool hasSpecial = !requiresSymbol || p.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'));
|
||||
bool hasUpper = p.contains(RegExp(r'[A-Z]'));
|
||||
bool hasLower = p.contains(RegExp(r'[a-z]'));
|
||||
bool hasDigit = p.contains(RegExp(r'[0-9]'));
|
||||
bool hasSpecial = p.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'));
|
||||
int typeCount = 0;
|
||||
if (hasUpper) typeCount++;
|
||||
if (hasLower) typeCount++;
|
||||
if (hasDigit) typeCount++;
|
||||
if (hasSpecial) typeCount++;
|
||||
bool hasTypeCount = minTypes <= 0 || typeCount >= minTypes;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@@ -850,6 +861,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
spacing: 10,
|
||||
children: [
|
||||
_cryptoCheck('$minLength자 이상', hasLength),
|
||||
if (minTypes > 0) _cryptoCheck('문자 유형 ${minTypes}가지 이상', hasTypeCount),
|
||||
if (requiresUpper) _cryptoCheck('대문자', hasUpper),
|
||||
if (requiresLower) _cryptoCheck('소문자', hasLower),
|
||||
if (requiresNumber) _cryptoCheck('숫자', hasDigit),
|
||||
@@ -967,4 +979,4 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user