mirror of
https://github.com/StefBuwalda/WebTech.git
synced 2025-10-29 19:00:00 +00:00
Merge branch 'development' into alternate-dashboard
This commit is contained in:
@@ -9,7 +9,7 @@ python -m venv venv
|
||||
### Installing required packages
|
||||
pip install -r requirements.txt
|
||||
|
||||
## Setting up the database
|
||||
## Setting up the database (in venv)
|
||||
### Initialize database
|
||||
flask --app app.py db init
|
||||
|
||||
|
||||
BIN
__pycache__/app.cpython-312.pyc
Normal file
BIN
__pycache__/app.cpython-312.pyc
Normal file
Binary file not shown.
4
app.py
4
app.py
@@ -1,11 +1,9 @@
|
||||
from application import app
|
||||
from flask import redirect, url_for
|
||||
from flask_login import login_required # type: ignore
|
||||
|
||||
|
||||
# home route
|
||||
# home route, place holder in case we want a home page
|
||||
@app.route("/")
|
||||
@login_required
|
||||
def index():
|
||||
return redirect(url_for("dash.index"))
|
||||
|
||||
|
||||
@@ -20,9 +20,6 @@ db.init_app(app)
|
||||
|
||||
migrate = Migrate(app, db)
|
||||
|
||||
# bp import
|
||||
from application.auth.views import auth_blueprint
|
||||
from application.dash.views import dash_blueprint
|
||||
|
||||
# Login manager
|
||||
from application.auth.models import User
|
||||
@@ -32,6 +29,7 @@ login_manager.init_app(app) # type: ignore
|
||||
login_manager.login_view = "auth.login" # type: ignore
|
||||
|
||||
|
||||
# Gets all the user data
|
||||
@login_manager.user_loader # type: ignore
|
||||
def load_user(user_id): # type: ignore
|
||||
return User.query.get(int(user_id)) # type: ignore
|
||||
@@ -39,5 +37,14 @@ def load_user(user_id): # type: ignore
|
||||
|
||||
# Blueprint magic
|
||||
|
||||
# bp import
|
||||
# Would like to do this at the top of the file,
|
||||
# but can't easily figure out how to do this.
|
||||
# I think everything that the views depend on have to be moved
|
||||
# into a seperate .py and imported.
|
||||
from application.auth.views import auth_blueprint
|
||||
from application.dash.views import dash_blueprint
|
||||
|
||||
# Register blueprints
|
||||
app.register_blueprint(dash_blueprint, url_prefix="/dash")
|
||||
app.register_blueprint(auth_blueprint, url_prefix="/auth")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,16 +3,21 @@ from wtforms import StringField, SubmitField, PasswordField, BooleanField
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
|
||||
# Default Form that inherits from FlaskForm and
|
||||
# contains a username, password and submit button
|
||||
class defaultForm(FlaskForm):
|
||||
username = StringField("Username", validators=[DataRequired()])
|
||||
password = PasswordField("Password", validators=[DataRequired()])
|
||||
submit = SubmitField("Submit")
|
||||
|
||||
|
||||
# LoginForm, contains exactly the same as defaultForm
|
||||
class LoginForm(defaultForm):
|
||||
pass
|
||||
|
||||
|
||||
# RegisterForm that inherits from the default.
|
||||
# Adds a password confirmation and if the user is an admin or not.
|
||||
class RegisterForm(defaultForm):
|
||||
confirm_password = PasswordField(
|
||||
"Confirm Password", validators=[DataRequired()]
|
||||
@@ -20,6 +25,8 @@ class RegisterForm(defaultForm):
|
||||
is_admin = BooleanField("Admin")
|
||||
|
||||
|
||||
# Form to update password information.
|
||||
# Needs a confirmation password and the current password
|
||||
class UpdateForm(defaultForm):
|
||||
confirm_password = PasswordField(
|
||||
"Confirm Password", validators=[DataRequired()]
|
||||
|
||||
@@ -2,14 +2,18 @@ from application import db
|
||||
from flask_login import UserMixin # type: ignore
|
||||
|
||||
|
||||
# User model
|
||||
class User(db.Model, UserMixin):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(150), unique=True, nullable=False)
|
||||
password = db.Column(db.String(150), nullable=False)
|
||||
is_admin = db.Column(db.Boolean, default=False)
|
||||
|
||||
# Purely a relationship not a column,
|
||||
# makes all the services accessible through User.services
|
||||
services = db.relationship("Service", backref="user", lazy="joined")
|
||||
|
||||
# Initialize user, prevents red stuff
|
||||
def __init__(self, username: str, password: str, is_admin: bool = False):
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash
|
||||
|
||||
from application import db
|
||||
from application.auth.models import User
|
||||
from application.auth.forms import LoginForm
|
||||
from flask_login import ( # type: ignore
|
||||
login_required, # type: ignore
|
||||
login_user, # type: ignore
|
||||
logout_user,
|
||||
current_user,
|
||||
)
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
from application.decorators import admin_required
|
||||
from application.decorators import admin_required, login_required
|
||||
from application.auth.forms import RegisterForm, UpdateForm
|
||||
|
||||
auth_blueprint = Blueprint("auth", __name__, template_folder="templates")
|
||||
|
||||
|
||||
# Routes
|
||||
@auth_blueprint.route("/register", methods=["GET", "POST"])
|
||||
# Add user
|
||||
@auth_blueprint.route("/register_user", methods=["GET", "POST"])
|
||||
@admin_required
|
||||
def register():
|
||||
register_form = RegisterForm()
|
||||
@@ -29,14 +28,14 @@ def register():
|
||||
is_admin = register_form.is_admin.data
|
||||
if confirm_password != password:
|
||||
return render_template(
|
||||
"admin.html",
|
||||
"register_user.html",
|
||||
form=register_form,
|
||||
feedback="Passwords don't match, please try again",
|
||||
active_page="register",
|
||||
)
|
||||
if User.query.filter_by(username=username).first():
|
||||
return render_template(
|
||||
"admin.html",
|
||||
"register_user.html",
|
||||
form=register_form,
|
||||
feedback="Username is already taken",
|
||||
active_page="register",
|
||||
@@ -49,16 +48,17 @@ def register():
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
return render_template(
|
||||
"admin.html",
|
||||
"register_user.html",
|
||||
form=RegisterForm(formdata=None),
|
||||
feedback="User succesfully added",
|
||||
active_page="register",
|
||||
)
|
||||
return render_template(
|
||||
"admin.html", form=register_form, active_page="register"
|
||||
"register_user.html", form=register_form, active_page="register"
|
||||
)
|
||||
|
||||
|
||||
# Update user (specifically password)
|
||||
@auth_blueprint.route("/update_user", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def update():
|
||||
@@ -85,10 +85,12 @@ def update():
|
||||
)
|
||||
db.session.commit()
|
||||
logout_user()
|
||||
flash("Password changed succesfully, please log back in")
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template("update_user.html", form=form, active_page="update")
|
||||
|
||||
|
||||
# Login as user or admin
|
||||
@auth_blueprint.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
login_form = LoginForm()
|
||||
@@ -103,6 +105,7 @@ def login():
|
||||
user.password, password # type: ignore
|
||||
):
|
||||
login_user(user) # type: ignore
|
||||
flash("Logged in succesfully")
|
||||
return redirect("/")
|
||||
else:
|
||||
feedback = "Username or password is incorrect"
|
||||
@@ -110,8 +113,10 @@ def login():
|
||||
return render_template("login.html", form=login_form, feedback=feedback)
|
||||
|
||||
|
||||
# Logout
|
||||
@auth_blueprint.route("/logout")
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
flash("Logged out succesfully")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
@@ -4,9 +4,19 @@ from wtforms.validators import DataRequired
|
||||
from flask_wtf.file import FileField, FileAllowed # type: ignore
|
||||
|
||||
|
||||
# Form for service on dashboard, connected to database through ORM
|
||||
class ServiceForm(FlaskForm):
|
||||
name = StringField("Service name:", validators=[DataRequired()])
|
||||
url = URLField("Service URL:", validators=[DataRequired()])
|
||||
name = StringField(
|
||||
"Service name:",
|
||||
validators=[DataRequired()],
|
||||
render_kw={"placeholder": "Service Name"},
|
||||
)
|
||||
url = URLField(
|
||||
"Service URL:",
|
||||
validators=[DataRequired()],
|
||||
render_kw={"placeholder": "https://example.com"},
|
||||
)
|
||||
# File field that only allows jpg, jpeg or png
|
||||
image = FileField(
|
||||
"Icon:",
|
||||
validators=[
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
from application import db
|
||||
|
||||
|
||||
# Service class for dashboard
|
||||
class Service(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String, nullable=False)
|
||||
url = db.Column(db.String, nullable=False)
|
||||
icon = db.Column(db.String, default="google.png")
|
||||
|
||||
# Foreign key to connect to User table
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
|
||||
# Initialize the service (prevents ugly red lines)
|
||||
def __init__(
|
||||
self, name: str, url: str, user_id: int, icon: str = "google.png"
|
||||
):
|
||||
|
||||
@@ -22,13 +22,15 @@
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="threeDotDropdown">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('dash.edit_service', service_id=service.id) }}">Edit</a>
|
||||
<a class="dropdown-item"
|
||||
href="{{ url_for('dash.edit_service', service_id=service.id) }}">Edit</a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li>
|
||||
<form action="{{ url_for('dash.delete_item', service_id=service.id) }}" method="POST" style="display:inline;">
|
||||
<form action="{{ url_for('dash.delete_service', service_id=service.id) }}" method="POST"
|
||||
style="display:inline;">
|
||||
<button style="color: red;" type="submit" class="dropdown-item">Delete</button>
|
||||
</form>
|
||||
</li>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
from application import db
|
||||
from flask import Blueprint, render_template, redirect, url_for
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash
|
||||
from application.dash.forms import ServiceForm
|
||||
from flask_login import login_required, current_user # type: ignore
|
||||
from flask_login import current_user # type: ignore
|
||||
from application.dash.models import Service
|
||||
from application.utils import saveImage
|
||||
from application.decorators import login_required
|
||||
|
||||
# Dashboard blueprint
|
||||
dash_blueprint = Blueprint("dash", __name__, template_folder="templates")
|
||||
|
||||
|
||||
# index
|
||||
@dash_blueprint.route("/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def index():
|
||||
@@ -17,23 +20,27 @@ def index():
|
||||
)
|
||||
|
||||
|
||||
@dash_blueprint.route("/delete_item/<int:service_id>", methods=["POST"])
|
||||
# Deleting a service
|
||||
@dash_blueprint.route("/delete_service/<int:service_id>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_item(service_id: int):
|
||||
def delete_service(service_id: int):
|
||||
service = Service.query.get_or_404(service_id)
|
||||
|
||||
# Check ownership
|
||||
if service.user_id != current_user.id:
|
||||
flash("This is not your service!")
|
||||
return redirect(url_for("dash.index"))
|
||||
|
||||
db.session.delete(service)
|
||||
db.session.commit()
|
||||
flash("Service deleted")
|
||||
return redirect(url_for("dash.index"))
|
||||
|
||||
|
||||
@dash_blueprint.route("/service", methods=["GET", "POST"])
|
||||
# Add a service
|
||||
@dash_blueprint.route("/add_service", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def service():
|
||||
def add_service():
|
||||
service_form = ServiceForm()
|
||||
|
||||
if service_form.validate_on_submit(): # type: ignore
|
||||
@@ -52,17 +59,14 @@ def service():
|
||||
) # type: ignore
|
||||
db.session.add(new_service)
|
||||
db.session.commit()
|
||||
return render_template(
|
||||
"add_service.html",
|
||||
form=ServiceForm(formdata=None),
|
||||
feedback="Service succesfully added",
|
||||
active_page="service",
|
||||
)
|
||||
flash("Service added")
|
||||
return redirect(url_for("dash.index"))
|
||||
return render_template(
|
||||
"add_service.html", form=service_form, active_page="service"
|
||||
)
|
||||
|
||||
|
||||
# Edit service
|
||||
@dash_blueprint.route(
|
||||
"/edit_service/<int:service_id>", methods=["GET", "POST"]
|
||||
)
|
||||
@@ -88,22 +92,8 @@ def edit_service(service_id: int):
|
||||
commit = True
|
||||
if commit:
|
||||
db.session.commit()
|
||||
return redirect(url_for("dash.index"))
|
||||
flash("Service edited")
|
||||
return redirect(url_for("dash.index"))
|
||||
# Fill in correct data
|
||||
form = ServiceForm(name=service.name, url=service.url)
|
||||
return render_template("edit_service.html", form=form)
|
||||
|
||||
|
||||
"""
|
||||
def saveImage(image: ...):
|
||||
filename = secure_filename(image.filename)
|
||||
save_path = os.path.join(
|
||||
app.config["UPLOAD_FOLDER"], # type: ignore
|
||||
str(current_user.id),
|
||||
filename,
|
||||
)
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
image.save(save_path) # type: ignore
|
||||
filename2 = str(current_user.id) + "/" + filename
|
||||
return filename2
|
||||
"""
|
||||
|
||||
@@ -4,13 +4,26 @@ from functools import wraps
|
||||
from flask import redirect, url_for
|
||||
|
||||
|
||||
# Decorator that checks if the current user is logged in and an admin
|
||||
# Could be shortened by adding the login_required decorator
|
||||
# and removing the logic here
|
||||
def admin_required(f: Callable[..., Any]) -> Callable[..., Any]:
|
||||
@wraps(f)
|
||||
def decorated_function(*args: ..., **kwargs: ...):
|
||||
if not current_user.is_authenticated:
|
||||
return redirect(url_for("login"))
|
||||
return redirect(url_for("auth.login"))
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for("index"))
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def login_required(f: Callable[..., Any]) -> Callable[..., Any]:
|
||||
@wraps(f)
|
||||
def decorated_function(*args: ..., **kwargs: ...):
|
||||
if not current_user.is_authenticated:
|
||||
return redirect(url_for("auth.login"))
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_page == 'service' %}active{% endif %}"
|
||||
href="{{url_for('dash.service')}}">Add service</a>
|
||||
href="{{url_for('dash.add_service')}}">Add service</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if current_user.is_admin %}
|
||||
@@ -38,20 +38,46 @@
|
||||
</ul>
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="dropstart">
|
||||
<button class="btn btn-outline-info" type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<button class="btn btn-outline-info" type="button" id="dropdownMenuButton1"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Profile
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-lg-start" aria-labelledby="dropdownMenuButton1">
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-lg-start"
|
||||
aria-labelledby="dropdownMenuButton1">
|
||||
<li><a class="dropdown-item">Username: {{current_user.username}}</a></li>
|
||||
<li><a class="dropdown-item {% if active_page == 'update' %}active{% endif %}" href="{{url_for('auth.update')}}">Change password</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" style="color: tomato;" data-bs-theme="dark" href="{{url_for('auth.logout')}}">Logout</a></li>
|
||||
<li><a class="dropdown-item {% if active_page == 'update' %}active{% endif %}"
|
||||
href="{{url_for('auth.update')}}">Change password</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><a class="dropdown-item" style="color: tomato;" data-bs-theme="dark"
|
||||
href="{{url_for('auth.logout')}}">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
|
||||
<symbol id="check-circle-fill" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" />
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 1050;">
|
||||
{% for message in get_flashed_messages() %}
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<svg class="bi flex-shrink-0 me-2" width="15" height="15" role="img" aria-label="Success:">
|
||||
<use xlink:href="#check-circle-fill" />
|
||||
</svg>
|
||||
{{message}}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
from werkzeug.utils import secure_filename
|
||||
import os
|
||||
from werkzeug.utils import secure_filename
|
||||
from application import app
|
||||
from flask_login import current_user # type: ignore
|
||||
|
||||
|
||||
# save image to static folder
|
||||
def saveImage(image: ...):
|
||||
filename = secure_filename(image.filename)
|
||||
# Path should be /application/static/[user_id]/[filename]
|
||||
save_path = os.path.join(
|
||||
app.config["UPLOAD_FOLDER"], # type: ignore
|
||||
str(current_user.id),
|
||||
filename,
|
||||
)
|
||||
# Create path is it doesn't exist
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
# Save the image
|
||||
image.save(save_path) # type: ignore
|
||||
filename2 = str(current_user.id) + "/" + filename
|
||||
# Return the filename that is stored in database.
|
||||
# Only done to keep a single default image, this should be done differently
|
||||
filename2 = str(current_user.id) + "/" + filename # [user_id]/[filename]
|
||||
return filename2
|
||||
|
||||
11
seed.py
11
seed.py
@@ -3,14 +3,7 @@ from application.dash.models import Service
|
||||
from application.auth.models import User
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
"""
|
||||
new_strikers = [
|
||||
se(name="Erik", strike="y", age=44),
|
||||
Striker(name="Henk", strike="n", age=88),
|
||||
]
|
||||
|
||||
"""
|
||||
|
||||
# User acounts to add
|
||||
new_users = [
|
||||
User(
|
||||
username="admin",
|
||||
@@ -29,6 +22,7 @@ new_users = [
|
||||
),
|
||||
]
|
||||
|
||||
# Services to add
|
||||
new_services = [
|
||||
Service(name="test123", url="http://google.com", user_id=1),
|
||||
# Daan services
|
||||
@@ -51,4 +45,5 @@ with app.app_context():
|
||||
# Then add new
|
||||
db.session.add_all(new_services)
|
||||
db.session.add_all(new_users)
|
||||
# Commit
|
||||
db.session.commit()
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Reference in New Issue
Block a user