1
0
forked from baron/baron-sso

로그인 전 계정조회

This commit is contained in:
2026-01-27 17:16:36 +09:00
parent c914fad405
commit 1a0dc74cb5
3 changed files with 113 additions and 87 deletions

View File

@@ -516,9 +516,35 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error {
loginID := strings.ReplaceAll(req.LoginID, "-", "")
loginID = strings.ReplaceAll(loginID, " ", "")
// Generate secure tokens
token := GenerateSecureToken(32)
pendingRef := GenerateSecureToken(16)
// [New] Check if user exists before sending link
if h.DescopeClient != nil {
user, err := h.DescopeClient.Management.User().Load(context.Background(), loginID)
if err != nil || user == nil {
// Try searching by phone if not found by LoginID
searchPhone := loginID
if !strings.Contains(searchPhone, "@") {
if strings.HasPrefix(searchPhone, "010") {
searchPhone = "+82" + searchPhone[1:]
} else if strings.HasPrefix(searchPhone, "82") {
searchPhone = "+" + searchPhone
}
}
searchOptions := &descope.UserSearchOptions{
Phones: []string{searchPhone},
Limit: 1,
}
users, _, errSearch := h.DescopeClient.Management.User().SearchAll(context.Background(), searchOptions)
if errSearch != nil || len(users) == 0 {
slog.Warn("[Enchanted] User not found", "loginID", loginID)
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not registered"})
}
// 검색 결과가 있더라도 loginID는 사용자가 입력한 원래 값을 유지 (발송 수단 결정을 위해)
}
}
// [Changed] 토큰 길이를 사용자의 요청에 맞춰 6글자(3바이트)로, pendingRef를 8글자(4바이트)로 조정
token := GenerateSecureToken(3)
pendingRef := GenerateSecureToken(3)
slog.Info("[Enchanted] Initiating enchanted link", "loginID", loginID, "token", token, "pendingRef", pendingRef)
@@ -665,41 +691,16 @@ func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error {
targetLoginID = users[0].UserID
}
} else {
// Not found, or search error. Fallback to using the phone as LoginID.
// Use the normalized phone number to ensure consistency (+82...)
targetLoginID = searchPhone
slog.Info("[Verify] User not found by phone, will use/create", "loginID", targetLoginID)
// [Changed] If not found, do NOT auto-create. Return error.
slog.Warn("[Verify] User not found by phone", "loginID", searchPhone)
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not registered"})
}
slog.Info("[Verify] Generating embedded link", "loginID", targetLoginID)
embeddedToken, err := h.DescopeClient.Management.User().GenerateEmbeddedLink(context.Background(), targetLoginID, nil, 0)
if err != nil {
if strings.Contains(err.Error(), "User not found") || strings.Contains(err.Error(), "E062108") {
slog.Info("[Verify] User not found, creating...", "loginID", targetLoginID)
// Create User with Explicit Phone Attribute
userObj := &descope.UserRequest{}
if strings.Contains(targetLoginID, "@") {
userObj.Email = targetLoginID
} else {
userObj.Phone = targetLoginID // Must be E.164
}
_, errCreate := h.DescopeClient.Management.User().Create(context.Background(), targetLoginID, userObj)
if errCreate != nil {
slog.Error("[Verify] Failed to create user", "error", errCreate)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create new user"})
}
embeddedToken, err = h.DescopeClient.Management.User().GenerateEmbeddedLink(context.Background(), targetLoginID, nil, 0)
if err != nil {
slog.Error("[Verify] Failed to generate token after creation", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to generate upstream token"})
}
} else {
slog.Error("[Verify] Descope Error", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to generate upstream token"})
}
slog.Error("[Verify] Descope Error", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to generate upstream token"})
}
slog.Info("[Verify] Exchanging embedded token for session JWT")
@@ -749,44 +750,6 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
ale.Log(slog.LevelInfo, "Attempting to login")
// Validate password complexity before sending to Descope
password := req.Password
if len(password) < 8 {
ale.Status = fiber.StatusBadRequest
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = "Password must be at least 8 characters long"
ale.Log(slog.LevelWarn, "Validation failed: password too short")
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must be at least 8 characters long"})
}
if ok, _ := regexp.MatchString(`[a-z]`, password); !ok {
ale.Status = fiber.StatusBadRequest
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = "Password must contain at least one lowercase letter"
ale.Log(slog.LevelWarn, "Validation failed: no lowercase letter")
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one lowercase letter"})
}
if ok, _ := regexp.MatchString(`[A-Z]`, password); !ok {
ale.Status = fiber.StatusBadRequest
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = "Password must contain at least one uppercase letter"
ale.Log(slog.LevelWarn, "Validation failed: no uppercase letter")
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one uppercase letter"})
}
if ok, _ := regexp.MatchString(`[0-9]`, password); !ok {
ale.Status = fiber.StatusBadRequest
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = "Password must contain at least one number"
ale.Log(slog.LevelWarn, "Validation failed: no number")
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one number"})
}
if ok, _ := regexp.MatchString(`[\W_]`, password); !ok {
ale.Status = fiber.StatusBadRequest
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = "Password must contain at least one special character"
ale.Log(slog.LevelWarn, "Validation failed: no special character")
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Password must contain at least one special character"})
}
if h.DescopeClient == nil {
ale.Status = fiber.StatusInternalServerError
ale.LatencyMs = time.Since(startTime)
@@ -802,7 +765,12 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
ale.LatencyMs = time.Since(startTime)
ale.DescopeError = err.Error()
ale.Log(slog.LevelWarn, "Descope sign-in failed")
// It's good practice to return a generic error message for security.
// [Changed] Check if it's a "User not found" error to be more specific
if strings.Contains(err.Error(), "E062107") || strings.Contains(err.Error(), "not found") {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not registered"})
}
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid credentials"})
}

View File

@@ -12,6 +12,7 @@ import '../../../core/services/audit_service.dart';
import '../../../core/services/web_auth_integration.dart';
import '../../../core/services/auth_proxy_service.dart';
import '../../../core/notifiers/auth_notifier.dart';
import '../../profile/domain/notifiers/profile_notifier.dart';
class LoginScreen extends ConsumerStatefulWidget {
final String? verificationToken;
@@ -272,19 +273,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
final jwt = res['sessionJwt'];
if (jwt != null && mounted) {
Navigator.of(context).pop(); // 로딩 닫기
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);
}
} catch (e) {
if (mounted) Navigator.of(context).pop(); // 로딩 닫기
_showError("로그인 실패: ${e.toString().replaceFirst("Exception: ", "")}");
if (e.toString().contains("User not registered")) {
_showUnregisteredDialog();
} else {
_showError("로그인 실패: ${e.toString().replaceFirst("Exception: ", "")}");
}
}
}
@@ -303,7 +300,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
debugPrint("[Auth] Initiating Enchanted Link for: $loginId");
_startEnchantedFlow(loginId, isEmail: input.contains('@'));
// 링크 전송 전 사용자 존재 여부 체크 (백엔드에서 이미 처리하지만 에러 핸들링을 위해)
try {
await _startEnchantedFlow(loginId, isEmail: input.contains('@'));
} catch (e) {
if (e.toString().contains("User not registered")) {
_showUnregisteredDialog();
} else {
_showError("오류: $e");
}
}
}
Future<void> _startEnchantedFlow(String loginId, {required bool isEmail}) async {
@@ -316,7 +323,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
);
}
// 1. Init via Backend API (Now handles both SMS and SES Email)
// 1. Init via Backend API
final initResponse = await AuthProxyService.initEnchantedLink(loginId);
final pendingRef = initResponse['pendingRef'];
debugPrint("[Auth] Link Sent. PendingRef: $pendingRef");
@@ -356,7 +363,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
} catch (e) {
debugPrint("[Auth] Initialization failed: $e");
if (mounted && Navigator.canPop(context)) Navigator.of(context).pop();
_showError("전송 실패: $e");
if (e.toString().contains("User not registered")) {
_showUnregisteredDialog();
} else {
_showError("전송 실패: $e");
}
}
}
@@ -441,13 +452,34 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
}
void _onLoginSuccess(String token) {
void _onLoginSuccess(String token) async {
if (!mounted) return;
_logTokenDetails(token);
final userId = _getUserIdFromJwt(token);
// [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);
// 백엔드 GetMe 호출 (프로필 노티파이어 사용)
final profile = await ref.read(profileProvider.notifier).loadProfile();
if (profile != null) {
// 실제 정보로 세션 유저 정보 교체
final realUser = DescopeUser(
userId, [], 0, profile.name, null, profile.email, false, profile.phone, false, {}, '', '', '', false, 'enabled', [], [], [],
);
final realSession = DescopeSession.fromJwt(token, token, realUser);
Descope.sessionManager.manageSession(realSession);
}
} catch (e) {
debugPrint("[Auth] Failed to pre-fetch profile: $e");
}
// Record Audit Log
AuditService.logEvent(
userId: userId,
@@ -478,6 +510,30 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
}
// [New] 미등록 회원 안내 팝업
void _showUnregisteredDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("미등록 회원"),
content: const Text("가입되지 않은 정보입니다.\n회원가입 후 이용해 주세요."),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("취소"),
),
FilledButton(
onPressed: () {
Navigator.pop(context);
context.push('/signup');
},
child: const Text("회원가입 하기"),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(

View File

@@ -18,9 +18,11 @@ class ProfileNotifier extends AsyncNotifier<UserProfile?> {
return ref.read(profileRepositoryProvider).getMyProfile();
}
Future<void> loadProfile() async {
Future<UserProfile?> loadProfile() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => _fetch());
final profile = await _fetch();
state = AsyncValue.data(profile);
return profile;
}
Future<void> updateProfile({