Login now based on email
Build LinkLog Development Image / development-image (push) Successful in 10s

This commit is contained in:
2026-08-26 15:03:48 +02:00
parent dd44b380ce
commit 079eb146f6
12 changed files with 64 additions and 40 deletions
+3 -3
View File
@@ -21,7 +21,7 @@ init_db()
class LoginRequest(BaseModel):
username: str
email: str
password: str
otp: str | None = None
@@ -33,9 +33,9 @@ class PasswordResetRequest(BaseModel):
@router.post('/login')
def login(payload: LoginRequest):
user = authenticate_user(payload.username, payload.password)
user = authenticate_user(payload.email.strip(), payload.password)
if user is None:
reset_user = find_user(payload.username)
reset_user = find_user(payload.email.strip())
if reset_user and reset_user['email_verified'] and smtp_configured():
try:
token = create_reset_token(reset_user['id'])
+4 -4
View File
@@ -5,9 +5,9 @@ from datetime import datetime, timezone
from backend.app.database import get_connection, hash_password, verify_password
def authenticate_user(username: str, password: str):
def authenticate_user(email: str, password: str):
with get_connection() as conn:
row = conn.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone()
row = conn.execute('SELECT * FROM users WHERE email = ?', (email,)).fetchone()
if row is None or not verify_password(password, row['password_hash']):
return None
user = dict(row)
@@ -23,7 +23,7 @@ def authenticate_user(username: str, password: str):
return user
def find_user(username: str):
def find_user(email: str):
with get_connection() as conn:
row = conn.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone()
row = conn.execute('SELECT * FROM users WHERE email = ?', (email,)).fetchone()
return dict(row) if row else None
+8 -8
View File
@@ -22,7 +22,7 @@ client = TestClient(app)
def login_headers(username='alice'):
token = client.post('/api/auth/login', json={
'username': username,
'email': 'alice@example.com' if username == 'alice' else f'{username}@example.com',
'password': 'secret123',
}).json()['access_token']
return {'Authorization': f'Bearer {token}'}
@@ -31,7 +31,7 @@ def login_headers(username='alice'):
def test_login_returns_token():
assert app.version == '0.1.0'
response = client.post('/api/auth/login', json={
'username': 'alice',
'email': 'alice@example.com',
'password': 'secret123',
})
assert response.status_code == 200
@@ -44,7 +44,7 @@ def test_login_returns_token():
assert admin_session.json()['is_admin'] is True
user_token = client.post('/api/auth/login', json={
'username': 'bob',
'email': 'bob@example.com',
'password': 'secret123',
}).json()['access_token']
user_session = client.get('/api/auth/me', params={'token': user_token})
@@ -71,7 +71,7 @@ def test_password_hashes_are_salted_and_legacy_hashes_upgrade_on_login():
(str(uuid4()), legacy_username, f'{legacy_username}@example.com', legacy_hash),
)
conn.commit()
response = client.post('/api/auth/login', json={'username': legacy_username, 'password': 'legacy-password'})
response = client.post('/api/auth/login', json={'email': legacy_username + '@example.com', 'password': 'legacy-password'})
assert response.status_code == 200
with get_connection() as conn:
upgraded = conn.execute('SELECT password_hash FROM users WHERE username = ?', (legacy_username,)).fetchone()['password_hash']
@@ -203,7 +203,7 @@ def test_new_user_must_verify_email_before_login():
assert created.status_code == 201
assert created.json()['email_verified'] is False
login = client.post('/api/auth/login', json={'username': username, 'password': 'secret123'})
login = client.post('/api/auth/login', json={'email': f'{username}@example.com', 'password': 'secret123'})
assert login.status_code == 403
assert login.json()['detail'] == 'Email address is not verified'
@@ -223,7 +223,7 @@ def test_email_verification_link_enables_login():
verified = client.get('/api/auth/verify-email', params={'token': verification_token})
assert verified.status_code == 200
assert client.post('/api/auth/login', json={'username': username, 'password': 'secret123'}).status_code == 200
assert client.post('/api/auth/login', json={'email': f'{username}@example.com', 'password': 'secret123'}).status_code == 200
assert client.get('/api/auth/verify-email', params={'token': verification_token}).status_code == 400
@@ -242,7 +242,7 @@ def test_mistyped_password_sends_reset_link_without_changing_login_error():
with patch('backend.app.api.auth.smtp_configured', return_value=True), \
patch('backend.app.api.auth.create_reset_token', return_value='reset-token') as create_token, \
patch('backend.app.api.auth.send_password_reset_email') as send_email:
response = client.post('/api/auth/login', json={'username': username, 'password': 'wrong-password'})
response = client.post('/api/auth/login', json={'email': f'{username}@example.com', 'password': 'wrong-password'})
assert response.status_code == 401
assert response.json()['detail'] == 'Invalid username or password'
@@ -267,7 +267,7 @@ def test_password_reset_is_single_use_and_revokes_sessions():
reset = client.post('/api/auth/reset-password', json={'token': token, 'password': 'new-secret123'})
assert reset.status_code == 200
assert client.post('/api/auth/reset-password', json={'token': token, 'password': 'another-secret'}).status_code == 400
assert client.post('/api/auth/login', json={'username': username, 'password': 'new-secret123'}).status_code == 200
assert client.post('/api/auth/login', json={'email': f'{username}@example.com', 'password': 'new-secret123'}).status_code == 200
def test_admin_can_remove_user_with_owned_data():
+6 -6
View File
@@ -12,7 +12,7 @@ client = TestClient(app)
def test_user_config_api_and_profile_page():
login = client.post('/api/auth/login', json={
'username': 'alice',
'email': 'alice@example.com',
'password': 'secret123',
}).json()
headers = {'Authorization': f"Bearer {login['access_token']}"}
@@ -47,7 +47,7 @@ def test_user_config_api_and_profile_page():
assert 'name="new_password_confirmation"' in page_response.text
bob_login = client.post('/api/auth/login', json={
'username': 'bob',
'email': 'bob@example.com',
'password': 'secret123',
}).json()
bob_headers = {'Authorization': f"Bearer {bob_login['access_token']}"}
@@ -57,7 +57,7 @@ def test_user_config_api_and_profile_page():
}, headers=bob_headers)
assert password_response.status_code == 200
assert client.post('/api/auth/login', json={
'username': 'bob',
'email': 'bob@example.com',
'password': 'new-secret-123',
}).status_code == 200
assert client.put('/api/user/password', json={
@@ -80,7 +80,7 @@ def test_user_config_api_and_profile_page():
def test_user_can_enable_and_use_otp():
login = client.post('/api/auth/login', json={'username': 'alice', 'password': 'secret123'}).json()
login = client.post('/api/auth/login', json={'email': 'alice@example.com', 'password': 'secret123'}).json()
headers = {'Authorization': f"Bearer {login['access_token']}"}
setup = client.post('/api/user/otp/setup', headers=headers)
assert setup.status_code == 200
@@ -92,10 +92,10 @@ def test_user_can_enable_and_use_otp():
})
assert enabled.status_code == 200
assert enabled.json()['enabled'] is True
assert client.post('/api/auth/login', json={'username': 'alice', 'password': 'secret123'}).status_code == 401
assert client.post('/api/auth/login', json={'email': 'alice@example.com', 'password': 'secret123'}).status_code == 401
otp_login = client.post('/api/auth/login', json={
'username': 'alice', 'password': 'secret123', 'otp': current_code(secret),
'email': 'alice@example.com', 'password': 'secret123', 'otp': current_code(secret),
})
assert otp_login.status_code == 200