From fb3de15f8553be87e09c7f36ed0a36e5afdfd236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=9F=E4=BA=8C=E8=BF=9C?= Date: Thu, 16 Apr 2026 09:12:03 +0800 Subject: [PATCH] =?UTF-8?q?feat=20:=20=E5=B7=B2=E5=8F=82=E8=80=83=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E8=AF=B4=E6=98=8E=E7=9A=84=E6=96=B9=E6=A1=88=EF=BC=9A?= =?UTF-8?q?=201=E3=80=81=E7=99=BB=E5=BD=95=E8=AF=B7=E6=B1=82=20=E2=80=94?= =?UTF-8?q?=20=E5=A4=B1=E8=B4=A5=E6=97=B6=20return=EF=BC=8C=E7=94=B1=20mut?= =?UTF-8?q?ation=20=E7=8A=B6=E6=80=81=E9=A9=B1=E5=8A=A8=E9=94=99=E8=AF=AF?= =?UTF-8?q?=20UI=202.=E3=80=81ocalStorage=20=E5=AD=98=E5=82=A8=20=E2=80=94?= =?UTF-8?q?=20=E7=8B=AC=E7=AB=8B=20try/catch=EF=BC=8C=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E4=BB=85=20console.warn=EF=BC=8C=E4=B8=8D=E5=BD=B1=E5=93=8D?= =?UTF-8?q?=E5=90=8E=E7=BB=AD=E6=B5=81=E7=A8=8B=203=E3=80=81=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=AF=BC=E8=88=AA=20=E2=80=94=20=E5=A7=8B=E7=BB=88?= =?UTF-8?q?=E5=9C=A8=E7=99=BB=E5=BD=95=E6=88=90=E5=8A=9F=E5=90=8E=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=EF=BC=8C=E4=B8=8D=E5=8F=97=E5=AD=98=E5=82=A8=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E5=B9=B2=E6=89=B0=204=E3=80=81=E6=8A=8A=E6=95=B4?= =?UTF-8?q?=E4=B8=AA=E8=AF=BB=E5=8F=96=E8=BF=87=E7=A8=8B=E9=83=BD=E7=BA=B3?= =?UTF-8?q?=E5=85=A5=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E9=A1=B5=E9=9D=A2=E5=88=9D=E5=A7=8B=E5=8C=96=E9=98=B6?= =?UTF-8?q?=E6=AE=B5=E8=A2=AB=E5=AD=98=E5=82=A8=E5=BC=82=E5=B8=B8=E4=B8=AD?= =?UTF-8?q?=E6=96=AD=E3=80=82=205=E3=80=81=E5=B0=86=20save(detachedEntity)?= =?UTF-8?q?=20=E6=94=B9=E4=B8=BA=E5=AE=9A=E5=90=91=20JPQL=20update?= =?UTF-8?q?=EF=BC=8C=E5=8F=AA=E6=9B=B4=E6=96=B0=20failedAttempts=20?= =?UTF-8?q?=E5=92=8C=20lockedUntil=20=E4=B8=A4=E4=B8=AA=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=EF=BC=8C=E5=BD=BB=E5=BA=95=E6=B6=88=E9=99=A4=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E9=A3=8E=E9=99=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/local/LocalAuthFailedService.java | 21 ++++++++++------ .../auth/local/LocalCredentialRepository.java | 7 ++++++ web/src/pages/login.tsx | 24 ++++++++++++------- 3 files changed, 36 insertions(+), 16 deletions(-) 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 (