diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthFailedService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthFailedService.java index a76690ac..5b9ee082 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthFailedService.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthFailedService.java @@ -1,6 +1,7 @@ package com.iflytek.skillhub.auth.local; -import jakarta.annotation.Resource; +import jakarta.annotation.Nonnull; +import jakarta.persistence.EntityNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -29,14 +30,20 @@ public class LocalAuthFailedService { - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void handleFailedLogin(LocalCredential credential) { + @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) + public void handleFailedLogin(@Nonnull Long credentialId) { + + LocalCredential credential = credentialRepository.findById(credentialId) + .orElseThrow(() -> new EntityNotFoundException("Invalid credential id")); + int failedAttempts = credential.getFailedAttempts() + 1; - credential.setFailedAttempts(failedAttempts); - if (failedAttempts >= MAX_FAILED_ATTEMPTS) { - credential.setLockedUntil(currentTime().plus(LOCK_DURATION)); + Instant lockedUntil = credential.getLockedUntil(); + + if (failedAttempts >= MAX_FAILED_ATTEMPTS && lockedUntil == null) { + lockedUntil = currentTime().plus(LOCK_DURATION); } - credentialRepository.save(credential); + + credentialRepository.updateFailedAttemptsAndLockedUntil(credentialId, failedAttempts, lockedUntil); } private Instant currentTime() { diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalCredentialRepository.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalCredentialRepository.java index 8346b9c2..380e8257 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalCredentialRepository.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalCredentialRepository.java @@ -1,7 +1,10 @@ package com.iflytek.skillhub.auth.local; +import java.time.Instant; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; /** @@ -15,4 +18,8 @@ public interface LocalCredentialRepository extends JpaRepository findByUserId(String userId); boolean existsByUsernameIgnoreCase(String username); + + @Modifying + @Query("UPDATE LocalCredential c SET c.failedAttempts = :failedAttempts, c.lockedUntil = :lockedUntil WHERE c.id = :id") + int updateFailedAttemptsAndLockedUntil(Long id, int failedAttempts, Instant lockedUntil); } diff --git a/web/src/pages/login.tsx b/web/src/pages/login.tsx index 6acf8f1f..de737f37 100644 --- a/web/src/pages/login.tsx +++ b/web/src/pages/login.tsx @@ -35,17 +35,17 @@ export function LoginPage() { // Load saved username from localStorage on mount useEffect(() => { - const saved = localStorage.getItem(REMEMBER_ME_KEY) - if (saved) { - try { + try { + const saved = localStorage.getItem(REMEMBER_ME_KEY) + if (saved) { const { username: savedUsername } = JSON.parse(saved) if (savedUsername) { setUsername(savedUsername) setRememberMe(true) } - } catch { - // Invalid data, ignore } + } catch (e) { + console.warn('Failed to load remembered username:', e) } }, []) @@ -76,7 +76,12 @@ export function LoginPage() { setFieldErrors({}) try { await loginMutation.mutateAsync({ username: trimmedUsername, password }) - // Save username to localStorage if remember me is checked + } catch { + // mutation state drives the error UI + return + } + + try { if (rememberMe) { localStorage.setItem(REMEMBER_ME_KEY, JSON.stringify({ username: trimmedUsername @@ -84,10 +89,11 @@ export function LoginPage() { } else { localStorage.removeItem(REMEMBER_ME_KEY) } - await navigate({ to: returnTo }) - } catch { - // mutation state drives the error UI + } catch (e) { + console.warn('Failed to save remember-me preference:', e) } + + await navigate({ to: returnTo }) } return (