fix(503-4 로그인 잠금): 5 번 틀림 뒤 잠금 · 경고 메일 — 잠금 판정 별도 UPDATE · 신뢰 기기 확인 읽기만

- record_failed_login: 한 UPDATE 안 왼→오 대입으로 4 번째에 잠기던 흠 — 잠금은 failures >= 5 별도 UPDATE
- has_trusted_device: last_used_at UPDATE 뺌 — 마무리 트랜잭션 _trust_device 가 갱신(커밋 1)
- 시험: 4 번 안 잠김 · 5 번 잠김 · 경고 메일 4/5 · 쿠키 재로그인 커밋 1 · 살아 있는 서버 손 시험 helper_503_4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 12:29:20 +09:00
co-authored by Claude Opus 5.5
parent 9d85852aa5
commit 3abcbdd515
3 changed files with 241 additions and 14 deletions
+10 -13
View File
@@ -212,13 +212,17 @@ async def record_failed_login(user_id: int) -> int:
async with pool.acquire() as connection, connection.cursor() as cursor:
await cursor.execute(
"""UPDATE users SET login_failures = login_failures + 1,
last_failed_at = CURRENT_TIMESTAMP,
account_locked_until = CASE WHEN login_failures + 1 >= 5
THEN DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 15 MINUTE)
ELSE account_locked_until END
last_failed_at = CURRENT_TIMESTAMP
WHERE id = %s""",
(user_id,),
)
# 잠금은 따로 — 한 UPDATE 안에서는 MariaDB 가 앞 대입(+1)을 본 값으로 판정해 4 번째에 잠김
await cursor.execute(
"""UPDATE users
SET account_locked_until = DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 15 MINUTE)
WHERE id = %s AND login_failures >= 5""",
(user_id,),
)
await cursor.execute("SELECT login_failures FROM users WHERE id = %s", (user_id,))
failures = (await cursor.fetchone())[0]
await connection.commit()
@@ -265,6 +269,7 @@ async def finish_login(
async def has_trusted_device(user_id: int, token_hash: str) -> bool:
"""읽기만 — last_used_at 갱신은 finish_login 트랜잭션의 _trust_device 가 함."""
valid_after = datetime.utcnow() - timedelta(days=EMAIL_REVERIFY_DAYS)
pool = get_db_pool()
async with pool.acquire() as connection, connection.cursor() as cursor:
@@ -273,15 +278,7 @@ async def has_trusted_device(user_id: int, token_hash: str) -> bool:
WHERE user_id = %s AND token_hash = %s AND last_used_at >= %s LIMIT 1""",
(user_id, token_hash, valid_after),
)
row = await cursor.fetchone()
if not row:
return False
await cursor.execute(
"UPDATE trusted_devices SET last_used_at = CURRENT_TIMESTAMP WHERE id = %s",
(row[0],),
)
await connection.commit()
return True
return bool(await cursor.fetchone())
async def trust_device(