Add login history to settings
This commit is contained in:
28
db.py
28
db.py
@@ -547,4 +547,30 @@ ORDER BY invocation_time DESC""", [http_function_id])
|
|||||||
|
|
||||||
return (True, f"Imported shared environment '{env_data['name']}'", result['id'])
|
return (True, f"Imported shared environment '{env_data['name']}'", result['id'])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return (False, f"Error importing environment '{env_data.get('name', 'unknown')}': {str(e)}", None)
|
return (False, f"Error importing environment '{env_data.get('name', 'unknown')}': {str(e)}", None)
|
||||||
|
|
||||||
|
def record_login(self, user_id, ip_address, user_agent, success=True, failure_reason=None):
|
||||||
|
"""Record a login attempt"""
|
||||||
|
try:
|
||||||
|
self.execute(
|
||||||
|
"""INSERT INTO login_history
|
||||||
|
(user_id, ip_address, user_agent, success, failure_reason)
|
||||||
|
VALUES (%s, %s, %s, %s, %s)""",
|
||||||
|
(user_id, ip_address, user_agent, success, failure_reason),
|
||||||
|
commit=True
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error recording login: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_login_history(self, user_id, limit=50):
|
||||||
|
"""Get login history for a user"""
|
||||||
|
return self.execute(
|
||||||
|
"""SELECT id, login_time, ip_address, user_agent, success, failure_reason
|
||||||
|
FROM login_history
|
||||||
|
WHERE user_id = %s
|
||||||
|
ORDER BY login_time DESC
|
||||||
|
LIMIT %s""",
|
||||||
|
(user_id, limit)
|
||||||
|
)
|
||||||
|
|||||||
@@ -41,13 +41,20 @@ def login():
|
|||||||
|
|
||||||
user_data = db.get_user_by_username(username)
|
user_data = db.get_user_by_username(username)
|
||||||
if not user_data:
|
if not user_data:
|
||||||
|
# Record failed login attempt
|
||||||
|
db.record_login(None, request.remote_addr, str(request.user_agent), False, "User not found")
|
||||||
return render_template("login.html", error="User does not exist")
|
return render_template("login.html", error="User does not exist")
|
||||||
|
|
||||||
if not check_password_hash(user_data['password_hash'], password):
|
if not check_password_hash(user_data['password_hash'], password):
|
||||||
|
# Record failed login attempt
|
||||||
|
db.record_login(user_data['id'], request.remote_addr, str(request.user_agent), False, "Invalid password")
|
||||||
return render_template("login.html", error="Invalid username or password")
|
return render_template("login.html", error="Invalid username or password")
|
||||||
|
|
||||||
user = User(id=str(user_data['id']), username=user_data['username'], password_hash=user_data['password_hash'], created_at=user_data['created_at'], theme_preference=user_data.get('theme_preference', 'light'))
|
user = User(id=str(user_data['id']), username=user_data['username'], password_hash=user_data['password_hash'], created_at=user_data['created_at'], theme_preference=user_data.get('theme_preference', 'light'))
|
||||||
|
|
||||||
|
# Record successful login
|
||||||
|
db.record_login(user.id, request.remote_addr, str(request.user_agent), True)
|
||||||
|
|
||||||
login_user(user)
|
login_user(user)
|
||||||
|
|
||||||
next = request.args.get('next')
|
next = request.args.get('next')
|
||||||
|
|||||||
@@ -133,6 +133,23 @@ def database_schema():
|
|||||||
)
|
)
|
||||||
return render_template("dashboard/settings/database_schema.html", schema_info=schema_info)
|
return render_template("dashboard/settings/database_schema.html", schema_info=schema_info)
|
||||||
|
|
||||||
|
@settings.route("/login-history", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def login_history():
|
||||||
|
"""Display login history for the current user"""
|
||||||
|
user_id = current_user.id
|
||||||
|
history = db.get_login_history(user_id, limit=50)
|
||||||
|
|
||||||
|
if htmx:
|
||||||
|
return render_block(
|
||||||
|
environment,
|
||||||
|
"dashboard/settings/login_history.html",
|
||||||
|
"page",
|
||||||
|
history=history
|
||||||
|
)
|
||||||
|
return render_template("dashboard/settings/login_history.html", history=history)
|
||||||
|
|
||||||
|
|
||||||
def get_database_schema():
|
def get_database_schema():
|
||||||
"""Fetch database schema information for ERD generation"""
|
"""Fetch database schema information for ERD generation"""
|
||||||
# Get all tables
|
# Get all tables
|
||||||
@@ -413,3 +430,4 @@ def import_data():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": f"Import failed: {str(e)}"}, 500
|
return {"error": f"Import failed: {str(e)}"}, 500
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,11 @@
|
|||||||
class="border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 py-4 px-1 text-sm font-medium cursor-pointer">
|
class="border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 py-4 px-1 text-sm font-medium cursor-pointer">
|
||||||
Database Schema
|
Database Schema
|
||||||
</a>
|
</a>
|
||||||
|
<a hx-get="{{ url_for('settings.login_history') }}" hx-target="#container" hx-swap="innerHTML"
|
||||||
|
hx-push-url="true"
|
||||||
|
class="border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 py-4 px-1 text-sm font-medium cursor-pointer">
|
||||||
|
Login History
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,11 @@
|
|||||||
class="border-b-2 border-blue-500 text-blue-600 dark:text-blue-400 py-4 px-1 text-sm font-medium cursor-pointer">
|
class="border-b-2 border-blue-500 text-blue-600 dark:text-blue-400 py-4 px-1 text-sm font-medium cursor-pointer">
|
||||||
Database Schema
|
Database Schema
|
||||||
</a>
|
</a>
|
||||||
|
<a hx-get="{{ url_for('settings.login_history') }}" hx-target="#container" hx-swap="innerHTML"
|
||||||
|
hx-push-url="true"
|
||||||
|
class="border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 py-4 px-1 text-sm font-medium cursor-pointer">
|
||||||
|
Login History
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,11 @@
|
|||||||
class="border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 py-4 px-1 text-sm font-medium cursor-pointer">
|
class="border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 py-4 px-1 text-sm font-medium cursor-pointer">
|
||||||
Database Schema
|
Database Schema
|
||||||
</a>
|
</a>
|
||||||
|
<a hx-get="{{ url_for('settings.login_history') }}" hx-target="#container" hx-swap="innerHTML"
|
||||||
|
hx-push-url="true"
|
||||||
|
class="border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 py-4 px-1 text-sm font-medium cursor-pointer">
|
||||||
|
Login History
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
115
templates/dashboard/settings/login_history.html
Normal file
115
templates/dashboard/settings/login_history.html
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
{% extends 'dashboard.html' %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
<div class="p-6 max-w-6xl mx-auto">
|
||||||
|
<!-- Settings Navigation -->
|
||||||
|
<div class="mb-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<nav class="-mb-px flex space-x-8">
|
||||||
|
<a hx-get="{{ url_for('settings.api_keys') }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true"
|
||||||
|
class="border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 py-4 px-1 text-sm font-medium cursor-pointer">
|
||||||
|
API Keys
|
||||||
|
</a>
|
||||||
|
<a hx-get="{{ url_for('settings.export') }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true"
|
||||||
|
class="border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 py-4 px-1 text-sm font-medium cursor-pointer">
|
||||||
|
Export Data
|
||||||
|
</a>
|
||||||
|
<a hx-get="{{ url_for('settings.database_schema') }}" hx-target="#container" hx-swap="innerHTML"
|
||||||
|
hx-push-url="true"
|
||||||
|
class="border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 py-4 px-1 text-sm font-medium cursor-pointer">
|
||||||
|
Database Schema
|
||||||
|
</a>
|
||||||
|
<a hx-get="{{ url_for('settings.login_history') }}" hx-target="#container" hx-swap="innerHTML"
|
||||||
|
hx-push-url="true"
|
||||||
|
class="border-b-2 border-blue-500 text-blue-600 dark:text-blue-400 py-4 px-1 text-sm font-medium cursor-pointer">
|
||||||
|
Login History
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Login History</h1>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">View your recent login activity and security events</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow">
|
||||||
|
{% if history %}
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<thead class="bg-gray-50 dark:bg-gray-900">
|
||||||
|
<tr>
|
||||||
|
<th scope="col"
|
||||||
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
|
Date & Time
|
||||||
|
</th>
|
||||||
|
<th scope="col"
|
||||||
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
|
IP Address
|
||||||
|
</th>
|
||||||
|
<th scope="col"
|
||||||
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
|
Browser / Device
|
||||||
|
</th>
|
||||||
|
<th scope="col"
|
||||||
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{% for entry in history %}
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
||||||
|
<div>{{ entry.login_time.strftime('%b %d, %Y') }}</div>
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ entry.login_time.strftime('%I:%M %p') }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
||||||
|
{{ entry.ip_address or 'N/A' }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-900 dark:text-white">
|
||||||
|
<div class="max-w-xs truncate" title="{{ entry.user_agent }}">
|
||||||
|
{{ entry.user_agent or 'Unknown' }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
{% if entry.success %}
|
||||||
|
<span
|
||||||
|
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||||
|
Success
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span
|
||||||
|
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
|
||||||
|
title="{{ entry.failure_reason }}">
|
||||||
|
Failed
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="p-8 text-center">
|
||||||
|
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">No login history</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Your login activity will appear here
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if history %}
|
||||||
|
<div class="mt-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<p>Showing last {{ history|length }} login{% if history|length != 1 %}s{% endif %}. Login history is kept for
|
||||||
|
security purposes.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user