Implement user authentication and management system with FastAPI

- Added authentication routes for login, registration, password reset, and account deletion in `auth.py`.
- Implemented user profile management, including updating user information and changing passwords.
- Created dashboard views to display user-specific information and recent activity in `dashboard.py`.
- Developed leaderboard views to show team rankings and points in `leaderboard.py`.
- Added QR code generation and redemption functionality in `qr.py` and `redeem.py`.
- Implemented team management features, allowing users to create and join teams in `teams.py`.
- Introduced session debugging utility to assist with session-related issues in `debug_session.py`.
- Configured Docker Compose for MySQL database and FastAPI application with environment variables.
- Updated requirements.txt to include necessary dependencies for the application.
This commit is contained in:
Christian Krakau-Louis
2025-04-11 17:46:59 +02:00
parent 90bef433fd
commit 0de2eb5830
43 changed files with 4427 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
{% extends "base.html" %}
{% block content %}
<div class="max-w-3xl mx-auto">
<div class="bg-white rounded-lg shadow-md p-6">
<!-- Header -->
<h1 class="text-2xl font-bold text-irish-green mb-6">
{% if is_new %}Create{% else %}Edit{% endif %} {{ display_name[:-1] if display_name.endswith('s') else display_name }}
</h1>
<!-- Form -->
<form method="post" action="/admin/{{ model_name }}{% if is_new %}/new{% else %}/{{ record.id }}{% endif %}" class="space-y-6">
{% for column_name, column_info in columns_info.items() %}
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
<label for="{{ column_name }}" class="block text-sm font-medium text-gray-700">
{{ column_name|replace('_', ' ')|title }}
{% if column_info.primary_key %}
<span class="ml-1 text-xs text-irish-green">(Primary Key)</span>
{% endif %}
{% if column_info.foreign_key %}
<span class="ml-1 text-xs text-blue-600">(Foreign Key)</span>
{% endif %}
</label>
<div class="md:col-span-2">
{% if column_info.primary_key and is_new %}
<!-- For new records, primary key is often auto-generated -->
<input type="text" id="{{ column_name }}" name="{{ column_name }}" placeholder="Auto-generated"
class="bg-gray-100 border border-gray-300 text-gray-500 rounded-md px-3 py-2 w-full"
{% if not column_info.nullable %}disabled{% endif %}>
{% elif column_info.foreign_key and column_name in foreign_key_options %}
<!-- Foreign key dropdown -->
<select id="{{ column_name }}" name="{{ column_name }}"
class="border border-gray-300 rounded-md px-3 py-2 w-full focus:outline-none focus:ring-2 focus:ring-irish-green"
{% if not column_info.nullable %}required{% endif %}>
{% if column_info.nullable %}
<option value="">-- None --</option>
{% endif %}
{% for value, label in foreign_key_options[column_name] %}
<option value="{{ value }}" {% if record and record[column_name] == value %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
{% elif column_info.type.startswith('BOOLEAN') %}
<!-- Boolean field -->
<div class="flex items-center">
<input type="checkbox" id="{{ column_name }}" name="{{ column_name }}" class="h-5 w-5"
value="True" {% if record and record[column_name] %}checked{% endif %}>
<span class="ml-2 text-sm text-gray-600">Yes</span>
</div>
{% elif 'text' in column_info.type.lower() %}
<!-- Text area for longer text -->
<textarea id="{{ column_name }}" name="{{ column_name }}" rows="4"
class="border border-gray-300 rounded-md px-3 py-2 w-full focus:outline-none focus:ring-2 focus:ring-irish-green"
{% if not column_info.nullable %}required{% endif %}>{{ record[column_name] if record else '' }}</textarea>
{% else %}
<!-- Standard input field -->
<input type="{{ 'number' if 'int' in column_info.type.lower() else 'text' }}"
id="{{ column_name }}" name="{{ column_name }}"
value="{{ record[column_name] if record and record[column_name] is not none else '' }}"
class="border border-gray-300 rounded-md px-3 py-2 w-full focus:outline-none focus:ring-2 focus:ring-irish-green"
{% if not column_info.nullable and not column_info.primary_key %}required{% endif %}>
{% endif %}
{% if column_info.nullable %}
<p class="text-xs text-gray-500 mt-1">Optional field</p>
{% endif %}
</div>
</div>
{% endfor %}
<!-- Form buttons -->
<div class="flex justify-end space-x-4 pt-6 border-t">
<a href="/admin/{{ model_name }}" class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition">
Cancel
</a>
<button type="submit" class="px-4 py-2 bg-irish-green text-white rounded-md hover:bg-opacity-90 transition">
{% if is_new %}Create{% else %}Update{% endif %}
</button>
</div>
</form>
</div>
<!-- Back links -->
<div class="mt-6 text-center">
<a href="/admin/{{ model_name }}" class="text-irish-green hover:underline mr-4">
<i class="fas fa-list mr-1"></i> Back to {{ display_name }}
</a>
<a href="/admin/" class="text-irish-green hover:underline">
<i class="fas fa-tachometer-alt mr-1"></i> Back to Admin Dashboard
</a>
</div>
</div>
{% endblock %}
+36
View File
@@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block content %}
<div class="max-w-5xl mx-auto">
<div class="bg-white rounded-lg shadow-md p-6">
<h1 class="text-2xl font-bold text-irish-green mb-6">Admin Dashboard</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for model_key, display_name in models %}
<div class="bg-cream-white rounded-lg p-6 shadow-sm hover:shadow-md transition-shadow">
<h3 class="text-xl font-bold text-irish-green mb-3">{{ display_name }}</h3>
<div class="flex items-center justify-between mt-4">
<a href="/admin/{{ model_key }}" class="bg-irish-green text-white px-4 py-2 rounded-md hover:bg-opacity-90 transition">
Manage
</a>
<a href="/admin/{{ model_key }}/new" class="text-irish-green hover:underline">
<i class="fas fa-plus"></i> Add New
</a>
</div>
</div>
{% endfor %}
</div>
<div class="mt-10 pt-6 border-t border-gray-200">
<h2 class="text-xl font-bold text-irish-green mb-4">Quick Actions</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<a href="/qr/generate/10" class="bg-white border border-irish-green text-irish-green hover:bg-irish-green hover:text-white px-4 py-3 rounded-md transition flex items-center">
<i class="fas fa-qrcode mr-2"></i> Generate QR Code (10 points)
</a>
<a href="/teams/" class="bg-white border border-irish-green text-irish-green hover:bg-irish-green hover:text-white px-4 py-3 rounded-md transition flex items-center">
<i class="fas fa-users mr-2"></i> View Teams
</a>
</div>
</div>
</div>
</div>
{% endblock %}
+122
View File
@@ -0,0 +1,122 @@
{% extends "base.html" %}
{% block content %}
<div class="max-w-6xl mx-auto">
<div class="bg-white rounded-lg shadow-md p-6">
<!-- Header -->
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6">
<div>
<h1 class="text-2xl font-bold text-irish-green">{{ display_name }}</h1>
<p class="text-gray-600">Manage your {{ display_name.lower() }} data</p>
</div>
<div class="mt-4 md:mt-0">
<a href="/admin/{{ model_name }}/new" class="bg-irish-green hover:bg-opacity-90 text-white font-medium py-2 px-4 rounded-md transition">
<i class="fas fa-plus mr-1"></i> Create New
</a>
</div>
</div>
<!-- Records Table -->
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="bg-irish-green text-white">
<th class="p-3 text-left">ID</th>
{% for column in columns %}
{% if column != 'id' %}
<th class="p-3 text-left">{{ column|replace('_', ' ')|title }}</th>
{% endif %}
{% endfor %}
<th class="p-3 text-center">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for record in records %}
<tr class="hover:bg-gray-50">
<td class="p-3 font-medium">{{ record.id }}</td>
{% for column in columns %}
{% if column != 'id' %}
<td class="p-3">
{% if columns_info[column].foreign_key %}
<span class="bg-gray-100 text-gray-800 px-2 py-1 rounded text-xs">FK: {{ record[column] }}</span>
{% elif record[column] is none %}
<span class="text-gray-400 italic">NULL</span>
{% elif columns_info[column].type.startswith('BOOLEAN') %}
{% if record[column] %}
<span class="bg-green-100 text-green-800 px-2 py-1 rounded-full text-xs">Yes</span>
{% else %}
<span class="bg-red-100 text-red-800 px-2 py-1 rounded-full text-xs">No</span>
{% endif %}
{% else %}
{{ record[column]|string|truncate(50) }}
{% endif %}
</td>
{% endif %}
{% endfor %}
<td class="p-3 text-center">
<div class="flex justify-center space-x-2">
<a href="/admin/{{ model_name }}/{{ record.id }}" class="text-irish-green hover:text-opacity-70" title="Edit">
<i class="fas fa-edit"></i>
</a>
<a href="/admin/{{ model_name }}/{{ record.id }}/delete" class="text-guinness-red hover:text-opacity-70" title="Delete"
onclick="return confirm('Are you sure you want to delete this record?');">
<i class="fas fa-trash"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
{% if not records %}
<tr>
<td colspan="{{ columns|length + 1 }}" class="p-4 text-center text-gray-500">
No records found
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if total_pages > 1 %}
<div class="mt-6 flex justify-between items-center">
<div class="text-gray-600 text-sm">
Showing {{ (page - 1) * per_page + 1 }}-{{ [page * per_page, total_records]|min }} of {{ total_records }} records
</div>
<div class="flex space-x-1">
{% if page > 1 %}
<a href="?page={{ page - 1 }}&per_page={{ per_page }}" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded">
&laquo; Prev
</a>
{% endif %}
{% for p in range(1, total_pages + 1) %}
{% if p == page %}
<span class="px-3 py-1 bg-irish-green text-white rounded">{{ p }}</span>
{% elif p <= 3 or p >= total_pages - 2 or (p >= page - 1 and p <= page + 1) %}
<a href="?page={{ p }}&per_page={{ per_page }}" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded">
{{ p }}
</a>
{% elif p == 4 and page > 5 or p == total_pages - 3 and page < total_pages - 4 %}
<span class="px-3 py-1">...</span>
{% endif %}
{% endfor %}
{% if page < total_pages %}
<a href="?page={{ page + 1 }}&per_page={{ per_page }}" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded">
Next &raquo;
</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
<!-- Back to Admin -->
<div class="mt-6 text-center">
<a href="/admin/" class="text-irish-green hover:underline">
<i class="fas fa-arrow-left mr-1"></i> Back to Admin Dashboard
</a>
</div>
</div>
{% endblock %}