sec: server-side email domain check + IP block on violations
Only @tutlot.com emails allowed for registration (checked server-side, invisible to frontend inspect). Wrong domain → scary message + IP violation tracked. 5 violations → IP permanently blocked from login and registration. Block screen with OK button on frontend. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
47b89ded8d
commit
0562cb4e47
8 changed files with 123 additions and 30 deletions
|
|
@ -33,7 +33,7 @@ _WEBHOOK_SECRET = "test-webhook-secret"
|
|||
_WEBHOOK_HEADERS = {"X-Telegram-Bot-Api-Secret-Token": _WEBHOOK_SECRET}
|
||||
|
||||
_VALID_PAYLOAD = {
|
||||
"email": "user@example.com",
|
||||
"email": "user@tutlot.com",
|
||||
"login": "testuser",
|
||||
"password": "strongpassword123",
|
||||
}
|
||||
|
|
@ -68,7 +68,7 @@ async def test_auth_register_fire_and_forget_telegram_error_still_returns_201():
|
|||
):
|
||||
resp = await client.post(
|
||||
"/api/auth/register",
|
||||
json={**_VALID_PAYLOAD, "email": "other@example.com", "login": "otheruser"},
|
||||
json={**_VALID_PAYLOAD, "email": "other@tutlot.com", "login": "otheruser"},
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ async def test_auth_register_409_on_duplicate_login():
|
|||
|
||||
r2 = await client.post(
|
||||
"/api/auth/register",
|
||||
json={**_VALID_PAYLOAD, "email": "different@example.com"},
|
||||
json={**_VALID_PAYLOAD, "email": "different@tutlot.com"},
|
||||
)
|
||||
|
||||
assert r2.status_code == 409, f"Expected 409 on duplicate login, got {r2.status_code}"
|
||||
|
|
@ -365,7 +365,7 @@ async def test_register_without_push_subscription():
|
|||
with patch("backend.telegram.send_registration_notification", new_callable=AsyncMock):
|
||||
resp = await client.post(
|
||||
"/api/auth/register",
|
||||
json={**_VALID_PAYLOAD, "email": "nopush@example.com", "login": "nopushuser"},
|
||||
json={**_VALID_PAYLOAD, "email": "nopush@tutlot.com", "login": "nopushuser"},
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert resp.json()["status"] == "pending"
|
||||
|
|
@ -424,7 +424,7 @@ async def test_webhook_callback_approve_edits_message():
|
|||
with patch("backend.telegram.send_registration_notification", new_callable=AsyncMock):
|
||||
reg_resp = await client.post(
|
||||
"/api/auth/register",
|
||||
json={**_VALID_PAYLOAD, "email": "edit@example.com", "login": "edituser"},
|
||||
json={**_VALID_PAYLOAD, "email": "edit@tutlot.com", "login": "edituser"},
|
||||
)
|
||||
assert reg_resp.status_code == 201
|
||||
|
||||
|
|
@ -469,7 +469,7 @@ async def test_webhook_callback_answer_sent():
|
|||
with patch("backend.telegram.send_registration_notification", new_callable=AsyncMock):
|
||||
reg_resp = await client.post(
|
||||
"/api/auth/register",
|
||||
json={**_VALID_PAYLOAD, "email": "answer@example.com", "login": "answeruser"},
|
||||
json={**_VALID_PAYLOAD, "email": "answer@tutlot.com", "login": "answeruser"},
|
||||
)
|
||||
assert reg_resp.status_code == 201
|
||||
|
||||
|
|
@ -562,7 +562,7 @@ async def test_password_hash_stored_in_pbkdf2_format():
|
|||
with patch("backend.telegram.send_registration_notification", new_callable=AsyncMock):
|
||||
await client.post(
|
||||
"/api/auth/register",
|
||||
json={**_VALID_PAYLOAD, "email": "pbkdf2@example.com", "login": "pbkdf2user"},
|
||||
json={**_VALID_PAYLOAD, "email": "pbkdf2@tutlot.com", "login": "pbkdf2user"},
|
||||
)
|
||||
|
||||
async with aiosqlite.connect(_cfg.DB_PATH) as conn:
|
||||
|
|
@ -601,7 +601,7 @@ async def test_webhook_callback_double_approve_does_not_send_push():
|
|||
with patch("backend.telegram.send_registration_notification", new_callable=AsyncMock):
|
||||
reg_resp = await client.post(
|
||||
"/api/auth/register",
|
||||
json={**_VALID_PAYLOAD, "email": "double@example.com", "login": "doubleuser", "push_subscription": push_sub},
|
||||
json={**_VALID_PAYLOAD, "email": "double@tutlot.com", "login": "doubleuser", "push_subscription": push_sub},
|
||||
)
|
||||
assert reg_resp.status_code == 201
|
||||
|
||||
|
|
@ -653,7 +653,7 @@ async def test_webhook_callback_double_approve_status_stays_approved():
|
|||
with patch("backend.telegram.send_registration_notification", new_callable=AsyncMock):
|
||||
reg_resp = await client.post(
|
||||
"/api/auth/register",
|
||||
json={**_VALID_PAYLOAD, "email": "stay@example.com", "login": "stayuser"},
|
||||
json={**_VALID_PAYLOAD, "email": "stay@tutlot.com", "login": "stayuser"},
|
||||
)
|
||||
assert reg_resp.status_code == 201
|
||||
|
||||
|
|
@ -698,7 +698,7 @@ async def test_webhook_callback_approve_after_reject_status_stays_rejected():
|
|||
with patch("backend.telegram.send_registration_notification", new_callable=AsyncMock):
|
||||
reg_resp = await client.post(
|
||||
"/api/auth/register",
|
||||
json={**_VALID_PAYLOAD, "email": "artest@example.com", "login": "artestuser"},
|
||||
json={**_VALID_PAYLOAD, "email": "artest@tutlot.com", "login": "artestuser"},
|
||||
)
|
||||
assert reg_resp.status_code == 201
|
||||
|
||||
|
|
@ -759,7 +759,7 @@ async def test_auth_register_rate_limit_fourth_request_returns_429():
|
|||
r = await client.post(
|
||||
"/api/auth/register",
|
||||
json={
|
||||
"email": f"ratetest{i}@example.com",
|
||||
"email": f"ratetest{i}@tutlot.com",
|
||||
"login": f"ratetest{i}",
|
||||
"password": "strongpassword123",
|
||||
},
|
||||
|
|
@ -770,7 +770,7 @@ async def test_auth_register_rate_limit_fourth_request_returns_429():
|
|||
r4 = await client.post(
|
||||
"/api/auth/register",
|
||||
json={
|
||||
"email": "ratetest4@example.com",
|
||||
"email": "ratetest4@tutlot.com",
|
||||
"login": "ratetest4",
|
||||
"password": "strongpassword123",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ APP_JS = PROJECT_ROOT / "frontend" / "app.js"
|
|||
from tests.conftest import make_app_client
|
||||
|
||||
_VALID_PAYLOAD = {
|
||||
"email": "frontend_test@example.com",
|
||||
"email": "frontend_test@tutlot.com",
|
||||
"login": "frontenduser",
|
||||
"password": "strongpassword123",
|
||||
}
|
||||
|
|
@ -417,7 +417,7 @@ async def test_register_duplicate_login_returns_409():
|
|||
|
||||
r2 = await client.post(
|
||||
"/api/auth/register",
|
||||
json={**_VALID_PAYLOAD, "email": "another@example.com"},
|
||||
json={**_VALID_PAYLOAD, "email": "another@tutlot.com"},
|
||||
)
|
||||
|
||||
assert r2.status_code == 409, (
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ async def _reject(reg_id: int) -> None:
|
|||
async def test_login_by_login_field_returns_200_with_token():
|
||||
"""Approved user can login using their login field → 200 + token."""
|
||||
async with make_app_client() as client:
|
||||
reg_id = await _register_auth(client, "alice@example.com", "alice", "password123")
|
||||
reg_id = await _register_auth(client, "alice@tutlot.com", "alice", "password123")
|
||||
await _approve(reg_id)
|
||||
resp = await client.post(
|
||||
"/api/auth/login",
|
||||
|
|
@ -83,7 +83,7 @@ async def test_login_by_login_field_returns_200_with_token():
|
|||
async def test_login_by_login_field_token_is_non_empty_string():
|
||||
"""Token returned for approved login user is a non-empty string."""
|
||||
async with make_app_client() as client:
|
||||
reg_id = await _register_auth(client, "alice2@example.com", "alice2", "password123")
|
||||
reg_id = await _register_auth(client, "alice2@tutlot.com", "alice2", "password123")
|
||||
await _approve(reg_id)
|
||||
resp = await client.post(
|
||||
"/api/auth/login",
|
||||
|
|
@ -102,11 +102,11 @@ async def test_login_by_login_field_token_is_non_empty_string():
|
|||
async def test_login_by_email_field_returns_200_with_token():
|
||||
"""Approved user can login using their email field → 200 + token."""
|
||||
async with make_app_client() as client:
|
||||
reg_id = await _register_auth(client, "bob@example.com", "bobuser", "securepass1")
|
||||
reg_id = await _register_auth(client, "bob@tutlot.com", "bobuser", "securepass1")
|
||||
await _approve(reg_id)
|
||||
resp = await client.post(
|
||||
"/api/auth/login",
|
||||
json={"login_or_email": "bob@example.com", "password": "securepass1"},
|
||||
json={"login_or_email": "bob@tutlot.com", "password": "securepass1"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
|
|
@ -118,11 +118,11 @@ async def test_login_by_email_field_returns_200_with_token():
|
|||
async def test_login_by_email_field_token_login_matches_registration():
|
||||
"""Token response login field matches the login set during registration."""
|
||||
async with make_app_client() as client:
|
||||
reg_id = await _register_auth(client, "bob2@example.com", "bob2user", "securepass1")
|
||||
reg_id = await _register_auth(client, "bob2@tutlot.com", "bob2user", "securepass1")
|
||||
await _approve(reg_id)
|
||||
resp = await client.post(
|
||||
"/api/auth/login",
|
||||
json={"login_or_email": "bob2@example.com", "password": "securepass1"},
|
||||
json={"login_or_email": "bob2@tutlot.com", "password": "securepass1"},
|
||||
)
|
||||
assert resp.json()["login"] == "bob2user"
|
||||
|
||||
|
|
@ -136,7 +136,7 @@ async def test_login_by_email_field_token_login_matches_registration():
|
|||
async def test_wrong_password_returns_401():
|
||||
"""Wrong password returns 401 with generic message (no detail about which field failed)."""
|
||||
async with make_app_client() as client:
|
||||
reg_id = await _register_auth(client, "carol@example.com", "carol", "correctpass1")
|
||||
reg_id = await _register_auth(client, "carol@tutlot.com", "carol", "correctpass1")
|
||||
await _approve(reg_id)
|
||||
resp = await client.post(
|
||||
"/api/auth/login",
|
||||
|
|
@ -167,7 +167,7 @@ async def test_nonexistent_user_returns_401_same_message_as_wrong_password():
|
|||
async def test_pending_user_login_returns_403():
|
||||
"""User with pending status gets 403."""
|
||||
async with make_app_client() as client:
|
||||
await _register_auth(client, "dave@example.com", "dave", "password123")
|
||||
await _register_auth(client, "dave@tutlot.com", "dave", "password123")
|
||||
# Status is 'pending' by default — no approval step
|
||||
resp = await client.post(
|
||||
"/api/auth/login",
|
||||
|
|
@ -180,7 +180,7 @@ async def test_pending_user_login_returns_403():
|
|||
async def test_pending_user_login_403_message_is_human_readable():
|
||||
"""403 message for pending user contains readable Russian text about the waiting status."""
|
||||
async with make_app_client() as client:
|
||||
await _register_auth(client, "dave2@example.com", "dave2", "password123")
|
||||
await _register_auth(client, "dave2@tutlot.com", "dave2", "password123")
|
||||
resp = await client.post(
|
||||
"/api/auth/login",
|
||||
json={"login_or_email": "dave2", "password": "password123"},
|
||||
|
|
@ -197,7 +197,7 @@ async def test_pending_user_login_403_message_is_human_readable():
|
|||
async def test_rejected_user_login_returns_403():
|
||||
"""User with rejected status gets 403."""
|
||||
async with make_app_client() as client:
|
||||
reg_id = await _register_auth(client, "eve@example.com", "evegirl", "password123")
|
||||
reg_id = await _register_auth(client, "eve@tutlot.com", "evegirl", "password123")
|
||||
await _reject(reg_id)
|
||||
resp = await client.post(
|
||||
"/api/auth/login",
|
||||
|
|
@ -210,7 +210,7 @@ async def test_rejected_user_login_returns_403():
|
|||
async def test_rejected_user_login_403_message_is_human_readable():
|
||||
"""403 message for rejected user contains readable Russian text about rejection."""
|
||||
async with make_app_client() as client:
|
||||
reg_id = await _register_auth(client, "eve2@example.com", "eve2girl", "password123")
|
||||
reg_id = await _register_auth(client, "eve2@tutlot.com", "eve2girl", "password123")
|
||||
await _reject(reg_id)
|
||||
resp = await client.post(
|
||||
"/api/auth/login",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue