diff --git a/__pycache__/app.cpython-312.pyc b/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000..8a525d5 Binary files /dev/null and b/__pycache__/app.cpython-312.pyc differ diff --git a/app.py b/app.py index 89ab1c5..32c493b 100644 --- a/app.py +++ b/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")) diff --git a/application/__init__.py b/application/__init__.py index d4cd08f..67c3339 100644 --- a/application/__init__.py +++ b/application/__init__.py @@ -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") diff --git a/application/__pycache__/__init__.cpython-312.pyc b/application/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 295154d..0000000 Binary files a/application/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/application/auth/__pycache__/forms.cpython-312.pyc b/application/auth/__pycache__/forms.cpython-312.pyc deleted file mode 100644 index 4a3f074..0000000 Binary files a/application/auth/__pycache__/forms.cpython-312.pyc and /dev/null differ diff --git a/application/auth/__pycache__/models.cpython-312.pyc b/application/auth/__pycache__/models.cpython-312.pyc deleted file mode 100644 index 4a61f2a..0000000 Binary files a/application/auth/__pycache__/models.cpython-312.pyc and /dev/null differ diff --git a/application/auth/__pycache__/views.cpython-312.pyc b/application/auth/__pycache__/views.cpython-312.pyc deleted file mode 100644 index 5a28b60..0000000 Binary files a/application/auth/__pycache__/views.cpython-312.pyc and /dev/null differ diff --git a/application/auth/forms.py b/application/auth/forms.py index 90c31fb..4db03b1 100644 --- a/application/auth/forms.py +++ b/application/auth/forms.py @@ -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()] diff --git a/application/auth/models.py b/application/auth/models.py index 554a4fc..086f42c 100644 --- a/application/auth/models.py +++ b/application/auth/models.py @@ -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 diff --git a/application/auth/templates/admin.html b/application/auth/templates/register_user.html similarity index 100% rename from application/auth/templates/admin.html rename to application/auth/templates/register_user.html diff --git a/application/auth/views.py b/application/auth/views.py index 7652eea..05c0a8e 100644 --- a/application/auth/views.py +++ b/application/auth/views.py @@ -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")) diff --git a/application/dash/forms.py b/application/dash/forms.py index 69bfe06..681d8d4 100644 --- a/application/dash/forms.py +++ b/application/dash/forms.py @@ -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=[ diff --git a/application/dash/models.py b/application/dash/models.py index 588a913..a152b40 100644 --- a/application/dash/models.py +++ b/application/dash/models.py @@ -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" ): diff --git a/application/dash/templates/dashboard.html b/application/dash/templates/dashboard.html index 238932f..249266e 100644 --- a/application/dash/templates/dashboard.html +++ b/application/dash/templates/dashboard.html @@ -6,43 +6,41 @@
{% for service in services%}
-
-
+
+
+ {{service["name"]}} +
+ + +
+
+
-
-
-
- {{service["name"]}} -
- -
-
-
- {{service["url"]}} -
-
+
+
+
+ {{service["url"]}}
{% endfor %}
-{%endblock%} \ No newline at end of file +{%endblock%} \ No newline at end of file diff --git a/application/dash/views.py b/application/dash/views.py index a8192fc..7fb36b2 100644 --- a/application/dash/views.py +++ b/application/dash/views.py @@ -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/", methods=["POST"]) +# Deleting a service +@dash_blueprint.route("/delete_service/", 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/", 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 -""" diff --git a/application/decorators.py b/application/decorators.py index 285930e..cd03e02 100644 --- a/application/decorators.py +++ b/application/decorators.py @@ -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 diff --git a/application/static/icons/2/google.png b/application/static/icons/2/google.png new file mode 100644 index 0000000..67fed90 Binary files /dev/null and b/application/static/icons/2/google.png differ diff --git a/application/static/icons/2/netflix.png b/application/static/icons/2/netflix.png new file mode 100644 index 0000000..a8315c9 Binary files /dev/null and b/application/static/icons/2/netflix.png differ diff --git a/application/static/icons/2/spotify.png b/application/static/icons/2/spotify.png new file mode 100644 index 0000000..88bdc71 Binary files /dev/null and b/application/static/icons/2/spotify.png differ diff --git a/application/static/style.css b/application/static/style.css index 2967361..d03e0e1 100644 --- a/application/static/style.css +++ b/application/static/style.css @@ -5,28 +5,43 @@ body { /* Dashboard page */ .grid-container { - display: grid; - grid-template-columns: auto auto auto; + display: flex; + grid-template-columns: auto auto auto auto auto; gap: 20px; background-color: lightslategray; padding-top: 20px; margin-left: 10%; margin-right: 10%; + justify-content: center; } .grid-container > div { height: fit-content; - width: 400px; + width: 200px; padding-top: 5px; padding-bottom: 5px; border: 2px solid black; border-radius: 10px; + box-shadow: 5px 5px 10px black; + font-weight: bold; + } .fit-picture { - width: 45px; - padding-top: 5px; - padding-bottom: 5px; + width: 175px; + margin-top: 5px; + margin-bottom: 5px; + border: 1px solid black; + border-radius: 5px; + box-shadow: 0px 0px 5px black; +} + +.url { + font-weight: normal; +} + +.name { + margin-left: 60px; } /* Login page */ diff --git a/application/templates/base_template.html b/application/templates/base_template.html index af81024..f88d4c6 100644 --- a/application/templates/base_template.html +++ b/application/templates/base_template.html @@ -26,7 +26,7 @@ {% endif %} {% if current_user.is_admin %} @@ -38,20 +38,46 @@ {% if current_user.is_authenticated %} {% endif %}
+ + + + + + + +
+ {% for message in get_flashed_messages() %} + + {% endfor %} +
+ {% block content %} {% endblock %} diff --git a/application/utils.py b/application/utils.py index 9cbc656..55a63a9 100644 --- a/application/utils.py +++ b/application/utils.py @@ -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 diff --git a/seed.py b/seed.py index 251e061..77f7a2d 100644 --- a/seed.py +++ b/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() diff --git a/test/123123123.png b/test/123123123.png deleted file mode 100644 index dfef930..0000000 Binary files a/test/123123123.png and /dev/null differ