Log login attempts

This commit is contained in:
Peter Stockings
2026-01-30 19:07:09 +11:00
parent b6443bc1e2
commit 4dcf589b63
4 changed files with 74 additions and 2 deletions

View File

@@ -1,9 +1,10 @@
from flask import Blueprint, render_template, redirect, url_for, flash from flask import Blueprint, render_template, redirect, url_for, flash, request
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import login_user, login_required, logout_user from flask_login import login_user, login_required, logout_user
from forms.login import LoginForm from forms.login import LoginForm
from forms.signup import SignupForm from forms.signup import SignupForm
from extensions import db from extensions import db
from utils import get_client_ip
auth = Blueprint('auth', __name__) auth = Blueprint('auth', __name__)
@@ -83,6 +84,17 @@ def create_person(name, email, password_hash):
return row['person_id'] return row['person_id']
def record_login_attempt(email, success, person_id=None):
"""
Record a login attempt in the database.
"""
sql = """
INSERT INTO login_attempts (email, ip_address, success, user_agent, person_id)
VALUES (%s, %s, %s, %s, %s)
"""
db.execute(sql, [email, get_client_ip(), success, request.user_agent.string, person_id], commit=True)
# --------------------- # ---------------------
# Blueprint endpoints # Blueprint endpoints
# --------------------- # ---------------------
@@ -109,9 +121,11 @@ def login():
person = get_person_by_email(form.email.data) person = get_person_by_email(form.email.data)
if person and check_password_hash(person.password_hash, form.password.data): if person and check_password_hash(person.password_hash, form.password.data):
login_user(person) login_user(person)
record_login_attempt(form.email.data, True, person.id)
flash("Logged in successfully.", "success") flash("Logged in successfully.", "success")
return redirect(url_for('calendar.get_calendar', person_id=person.id)) return redirect(url_for('calendar.get_calendar', person_id=person.id))
else: else:
record_login_attempt(form.email.data, False, person.id if person else None)
flash("Invalid email or password.", "danger") flash("Invalid email or password.", "danger")
return render_template('auth/login.html', form=form) return render_template('auth/login.html', form=form)

43
scripts/run_migration.py Normal file
View File

@@ -0,0 +1,43 @@
"""
Database migration runner
Execute SQL migration files using the Flask app's database connection
"""
import os
import sys
# Add parent directory to path to import app
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from extensions import db as database
def run_migration(migration_file):
"""Execute a SQL migration file"""
try:
# Read migration file
migration_path = os.path.join('migrations', migration_file)
if not os.path.exists(migration_path):
print(f"ERROR: Migration file not found: {migration_path}")
sys.exit(1)
with open(migration_path, 'r') as f:
sql = f.read()
# Execute migration using the database connection
database.execute(sql, commit=True)
print(f"✓ Successfully executed migration: {migration_file}")
except Exception as e:
print(f"ERROR: {e}")
sys.exit(1)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python run_migration.py <migration_file>")
print("Example: python run_migration.py 003_create_login_history.sql")
sys.exit(1)
# Initialize the app to set up database connection
from app import app
with app.app_context():
migration_file = sys.argv[1]
run_migration(migration_file)

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 156 KiB

View File

@@ -1,5 +1,6 @@
import colorsys import colorsys
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta
from flask import request
import numpy as np import numpy as np
@@ -253,6 +254,20 @@ def prepare_svg_plot_data(results, columns, title):
plot_data['plot_type'] = 'table' # Fallback if essential data is missing plot_data['plot_type'] = 'table' # Fallback if essential data is missing
return plot_data return plot_data
def get_client_ip():
"""Get real client IP address, checking proxy headers first"""
# Check common proxy headers in order of preference
if request.headers.get('X-Forwarded-For'):
# X-Forwarded-For can contain multiple IPs, get the first (original client)
return request.headers.get('X-Forwarded-For').split(',')[0].strip()
elif request.headers.get('X-Real-IP'):
return request.headers.get('X-Real-IP')
elif request.headers.get('CF-Connecting-IP'): # Cloudflare
return request.headers.get('CF-Connecting-IP')
else:
# Fallback to direct connection IP
return request.remote_addr
# Calculate ranges (handle datetime separately) # Calculate ranges (handle datetime separately)
if x_type == 'datetime': if x_type == 'datetime':
valid_dates = [d for d in x_values_raw if d is not None] valid_dates = [d for d in x_values_raw if d is not None]