added csrf protection & fixed minor functionality on admin page

This commit is contained in:
Andreas Jönsson 2025-11-17 10:30:47 +01:00
parent c5cd02a5ec
commit f188af4abf
6 changed files with 100 additions and 2 deletions

36
app.py
View File

@ -1,10 +1,12 @@
from flask import Flask, render_template, redirect, url_for, request from flask import Flask, render_template, redirect, url_for, request, abort
from flask_wtf.csrf import CSRFProtect
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
import os import os
app = Flask(__name__) app = Flask(__name__)
csrf = CSRFProtect(app)
app.config["SECRET_KEY"] = os.environ["FLASK_SECRET_KEY"] app.config["SECRET_KEY"] = os.environ["FLASK_SECRET_KEY"]
from urllib.parse import quote_plus from urllib.parse import quote_plus
@ -38,6 +40,7 @@ class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(100), unique=True) email = db.Column(db.String(100), unique=True)
password = db.Column(db.String(200)) # Increased for password hash password = db.Column(db.String(200)) # Increased for password hash
admin = db.Column(db.Boolean, default=False)
@login_manager.user_loader @login_manager.user_loader
@ -81,6 +84,11 @@ def register():
password = generate_password_hash(password) password = generate_password_hash(password)
new_user = User(email=email, password=password) new_user = User(email=email, password=password)
# Automatically make first user admin
if User.query.count() == 0:
new_user.admin = True
db.session.add(new_user) db.session.add(new_user)
db.session.commit() db.session.commit()
@ -99,3 +107,27 @@ def dashboard():
def logout(): def logout():
logout_user() logout_user()
return redirect(url_for("login")) return redirect(url_for("login"))
@app.route("/admin", methods=["GET", "POST"])
@login_required
def admin():
if not current_user.admin:
abort(403)
if request.method == "POST":
action = request.form.get("action")
user_id = request.form.get("user_id")
user = User.query.get(user_id)
if user and user != current_user: # Prevent self-modification
if action == "promote":
user.admin = True
elif action == "demote":
user.admin = False
elif action == "delete":
db.session.delete(user)
db.session.commit()
users = User.query.all()
return render_template("admin.html", users=users)

View File

@ -1,6 +1,7 @@
flask==3.0.0 flask==3.0.0
flask-sqlalchemy==3.1.1 flask-sqlalchemy==3.1.1
flask-login==0.6.3 flask-login==0.6.3
flask-wtf==1.2.1
werkzeug==3.0.0 werkzeug==3.0.0
gunicorn==21.2.0 gunicorn==21.2.0
psycopg2-binary==2.9.9 psycopg2-binary==2.9.9

60
templates/admin.html Normal file
View File

@ -0,0 +1,60 @@
{% extends "base.html" %}
{% block title %}Admin Panel{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="card">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">Admin Dashboard</h4>
</div>
<div class="card-body">
<h5>User Management</h5>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Email</th>
<th>Admin Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.email }}</td>
<td>{% if user.admin %}Admin{% else %}User{% endif %}</td>
<td>
{% if user.admin %}
<form method="POST" action="{{ url_for('admin') }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="user_id" value="{{ user.id }}">
<input type="hidden" name="action" value="demote">
<button type="submit" class="btn btn-sm btn-warning">Remove Admin</button>
</form>
{% else %}
<form method="POST" action="{{ url_for('admin') }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="user_id" value="{{ user.id }}">
<input type="hidden" name="action" value="promote">
<button type="submit" class="btn btn-sm btn-success">Make Admin</button>
</form>
{% endif %}
<form method="POST" action="{{ url_for('admin') }}" class="d-inline" onsubmit="return confirm('Delete this user permanently?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="user_id" value="{{ user.id }}">
<input type="hidden" name="action" value="delete">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -30,6 +30,9 @@
<div class="navbar-nav ms-auto"> <div class="navbar-nav ms-auto">
<a class="nav-link active" href="{{ url_for('dashboard') }}">Home</a> <a class="nav-link active" href="{{ url_for('dashboard') }}">Home</a>
<a class="nav-link" href="#">Example1</a> <a class="nav-link" href="#">Example1</a>
{% if current_user.admin %}
<a class="nav-link" href="{{ url_for('admin') }}">Admin</a>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -9,6 +9,7 @@
<div class="card-body"> <div class="card-body">
<h2 class="card-title text-center mb-4">Sign In</h2> <h2 class="card-title text-center mb-4">Sign In</h2>
<form method="POST"> <form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Email address</label> <label class="form-label">Email address</label>
<input type="email" class="form-control" name="email" required> <input type="email" class="form-control" name="email" required>

View File

@ -9,6 +9,7 @@
<div class="card-body"> <div class="card-body">
<h2 class="card-title text-center mb-4">Create Account</h2> <h2 class="card-title text-center mb-4">Create Account</h2>
<form method="POST"> <form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Email address</label> <label class="form-label">Email address</label>
<input type="email" class="form-control" name="email" required> <input type="email" class="form-control" name="email" required>