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 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
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
|
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]
|
||||||
|
|||||||
Reference in New Issue
Block a user