Log login attempts
This commit is contained in:
@@ -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 flask_login import login_user, login_required, logout_user
|
||||
from forms.login import LoginForm
|
||||
from forms.signup import SignupForm
|
||||
from extensions import db
|
||||
from utils import get_client_ip
|
||||
|
||||
auth = Blueprint('auth', __name__)
|
||||
|
||||
@@ -83,6 +84,17 @@ def create_person(name, email, password_hash):
|
||||
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
|
||||
# ---------------------
|
||||
@@ -109,9 +121,11 @@ def login():
|
||||
person = get_person_by_email(form.email.data)
|
||||
if person and check_password_hash(person.password_hash, form.password.data):
|
||||
login_user(person)
|
||||
record_login_attempt(form.email.data, True, person.id)
|
||||
flash("Logged in successfully.", "success")
|
||||
return redirect(url_for('calendar.get_calendar', person_id=person.id))
|
||||
else:
|
||||
record_login_attempt(form.email.data, False, person.id if person else None)
|
||||
flash("Invalid email or password.", "danger")
|
||||
return render_template('auth/login.html', form=form)
|
||||
|
||||
|
||||
43
scripts/run_migration.py
Normal file
43
scripts/run_migration.py
Normal 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 |
15
utils.py
15
utils.py
@@ -1,5 +1,6 @@
|
||||
import colorsys
|
||||
from datetime import datetime, date, timedelta
|
||||
from flask import request
|
||||
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
|
||||
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)
|
||||
if x_type == 'datetime':
|
||||
valid_dates = [d for d in x_values_raw if d is not None]
|
||||
|
||||
Reference in New Issue
Block a user