diff --git a/.gitignore b/.gitignore index f7275bb..691eef1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,9 @@ +# venv's venv/ + +# DB stuff +migrations/ +instance/ + +# Byte-compiled / optimized / DLL files +__pycache__/ \ No newline at end of file diff --git a/README.md b/README.md index b407ac5..6c547bc 100644 --- a/README.md +++ b/README.md @@ -1 +1,24 @@ -# WebTech \ No newline at end of file +# How to install +## Setting up your virtual environment +### Creating virtual environment +python -m venv venv + +### Activating environment for package installation (windows) +.\venv\Scripts\activate.bat + +### Installing required packages +pip install -r requirements.txt + +## Setting up the database +### Initialize database +flask --app app.py db init + +### Migrate database +flask --app app.py db migrate + +### upgrade database +flask --app app.py db upgrade + +# Development commands +#### Updating requirements.txt +pip freeze > requirements.txt \ No newline at end of file diff --git a/__pycache__/forms.cpython-312.pyc b/__pycache__/forms.cpython-312.pyc new file mode 100644 index 0000000..b1d7f66 Binary files /dev/null and b/__pycache__/forms.cpython-312.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..89ab1c5 --- /dev/null +++ b/app.py @@ -0,0 +1,15 @@ +from application import app +from flask import redirect, url_for +from flask_login import login_required # type: ignore + + +# home route +@app.route("/") +@login_required +def index(): + return redirect(url_for("dash.index")) + + +# App deployment +if __name__ == "__main__": + app.run(debug=True, port=5000) diff --git a/application/__init__.py b/application/__init__.py new file mode 100644 index 0000000..d4cd08f --- /dev/null +++ b/application/__init__.py @@ -0,0 +1,43 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from flask_login import LoginManager # type: ignore +import os + +# App Config +app = Flask(__name__) +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///services.sqlite" +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False # Wat is dit? +app.config["SECRET_KEY"] = "bvjchsygvduycgsyugc" # Andere secret key +app.config["UPLOAD_FOLDER"] = r"application\static\icons" + +# Ensure the upload folder exists +os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True) # type: ignore + +# Object Relational Management +db = SQLAlchemy() +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 + +login_manager = LoginManager() +login_manager.init_app(app) # type: ignore +login_manager.login_view = "auth.login" # type: ignore + + +@login_manager.user_loader # type: ignore +def load_user(user_id): # type: ignore + return User.query.get(int(user_id)) # type: ignore + + +# Blueprint magic + +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 new file mode 100644 index 0000000..295154d Binary files /dev/null and b/application/__pycache__/__init__.cpython-312.pyc differ diff --git a/application/auth/__pycache__/forms.cpython-312.pyc b/application/auth/__pycache__/forms.cpython-312.pyc new file mode 100644 index 0000000..4a3f074 Binary files /dev/null and b/application/auth/__pycache__/forms.cpython-312.pyc differ diff --git a/application/auth/__pycache__/models.cpython-312.pyc b/application/auth/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..4a61f2a Binary files /dev/null and b/application/auth/__pycache__/models.cpython-312.pyc differ diff --git a/application/auth/__pycache__/views.cpython-312.pyc b/application/auth/__pycache__/views.cpython-312.pyc new file mode 100644 index 0000000..5a28b60 Binary files /dev/null and b/application/auth/__pycache__/views.cpython-312.pyc differ diff --git a/application/auth/forms.py b/application/auth/forms.py new file mode 100644 index 0000000..90c31fb --- /dev/null +++ b/application/auth/forms.py @@ -0,0 +1,29 @@ +from flask_wtf import FlaskForm # type: ignore +from wtforms import StringField, SubmitField, PasswordField, BooleanField +from wtforms.validators import DataRequired + + +class defaultForm(FlaskForm): + username = StringField("Username", validators=[DataRequired()]) + password = PasswordField("Password", validators=[DataRequired()]) + submit = SubmitField("Submit") + + +class LoginForm(defaultForm): + pass + + +class RegisterForm(defaultForm): + confirm_password = PasswordField( + "Confirm Password", validators=[DataRequired()] + ) + is_admin = BooleanField("Admin") + + +class UpdateForm(defaultForm): + confirm_password = PasswordField( + "Confirm Password", validators=[DataRequired()] + ) + current_password = PasswordField( + "Current Password", validators=[DataRequired()] + ) diff --git a/application/auth/models.py b/application/auth/models.py new file mode 100644 index 0000000..554a4fc --- /dev/null +++ b/application/auth/models.py @@ -0,0 +1,16 @@ +from application import db +from flask_login import UserMixin # type: ignore + + +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) + + services = db.relationship("Service", backref="user", lazy="joined") + + def __init__(self, username: str, password: str, is_admin: bool = False): + self.username = username + self.password = password + self.is_admin = is_admin diff --git a/application/auth/templates/admin.html b/application/auth/templates/admin.html new file mode 100644 index 0000000..0eb208f --- /dev/null +++ b/application/auth/templates/admin.html @@ -0,0 +1,51 @@ +{% extends 'base_template.html' %} + +{% block title %} +Register +{% endblock %} + +{% block content %} +
+{% endblock %} \ No newline at end of file diff --git a/application/auth/templates/login.html b/application/auth/templates/login.html new file mode 100644 index 0000000..2b3a94f --- /dev/null +++ b/application/auth/templates/login.html @@ -0,0 +1,33 @@ +{% extends 'base_template.html' %} + +{% block title %} +Login +{% endblock %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/application/auth/templates/update_user.html b/application/auth/templates/update_user.html new file mode 100644 index 0000000..674ddd6 --- /dev/null +++ b/application/auth/templates/update_user.html @@ -0,0 +1,26 @@ +{% extends 'base_template.html' %} + +{% block title %} +Update +{% endblock %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/application/auth/views.py b/application/auth/views.py new file mode 100644 index 0000000..7652eea --- /dev/null +++ b/application/auth/views.py @@ -0,0 +1,117 @@ +from flask import Blueprint, render_template, redirect, url_for + +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.auth.forms import RegisterForm, UpdateForm + +auth_blueprint = Blueprint("auth", __name__, template_folder="templates") + + +# Routes +@auth_blueprint.route("/register", methods=["GET", "POST"]) +@admin_required +def register(): + register_form = RegisterForm() + + if register_form.validate_on_submit(): # type: ignore + username = register_form.username.data + password = register_form.password.data + confirm_password = register_form.confirm_password.data + is_admin = register_form.is_admin.data + if confirm_password != password: + return render_template( + "admin.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", + form=register_form, + feedback="Username is already taken", + active_page="register", + ) + new_user = User( + username=username, # type: ignore + password=generate_password_hash(password), # type: ignore + is_admin=is_admin, + ) + db.session.add(new_user) + db.session.commit() + return render_template( + "admin.html", + form=RegisterForm(formdata=None), + feedback="User succesfully added", + active_page="register", + ) + return render_template( + "admin.html", form=register_form, active_page="register" + ) + + +@auth_blueprint.route("/update_user", methods=["GET", "POST"]) +@login_required +def update(): + form = UpdateForm(username=current_user.username) + if form.validate_on_submit(): # type: ignore + if not check_password_hash( + current_user.password, form.current_password.data # type: ignore + ): + return render_template( + "update_user.html", + form=form, + feedback="Current password incorrect", + active_page="update", + ) + if form.password.data != form.confirm_password.data: + return render_template( + "update_user.html", + form=form, + feedback="New password mismatched", + active_page="update", + ) + current_user.password = generate_password_hash( + form.password.data # type: ignore + ) + db.session.commit() + logout_user() + return redirect(url_for("auth.login")) + return render_template("update_user.html", form=form, active_page="update") + + +@auth_blueprint.route("/login", methods=["GET", "POST"]) +def login(): + login_form = LoginForm() + feedback = None + + if login_form.validate_on_submit(): # type: ignore + username = login_form.username.data + password = login_form.password.data + user = User.query.filter_by(username=username).first() # type: ignore + + if user and check_password_hash( + user.password, password # type: ignore + ): + login_user(user) # type: ignore + return redirect("/") + else: + feedback = "Username or password is incorrect" + + return render_template("login.html", form=login_form, feedback=feedback) + + +@auth_blueprint.route("/logout") +@login_required +def logout(): + logout_user() + return redirect(url_for("index")) diff --git a/application/dash/forms.py b/application/dash/forms.py new file mode 100644 index 0000000..69bfe06 --- /dev/null +++ b/application/dash/forms.py @@ -0,0 +1,16 @@ +from flask_wtf import FlaskForm # type: ignore +from wtforms import StringField, SubmitField, URLField +from wtforms.validators import DataRequired +from flask_wtf.file import FileField, FileAllowed # type: ignore + + +class ServiceForm(FlaskForm): + name = StringField("Service name:", validators=[DataRequired()]) + url = URLField("Service URL:", validators=[DataRequired()]) + image = FileField( + "Icon:", + validators=[ + FileAllowed(["jpg", "jpeg", "png"], "Unsupported file format"), + ], + ) + submit = SubmitField("Submit") diff --git a/application/dash/models.py b/application/dash/models.py new file mode 100644 index 0000000..588a913 --- /dev/null +++ b/application/dash/models.py @@ -0,0 +1,18 @@ +from application import db + + +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") + + user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) + + def __init__( + self, name: str, url: str, user_id: int, icon: str = "google.png" + ): + self.name = name + self.url = url + self.user_id = user_id + self.icon = icon diff --git a/application/dash/templates/add_service.html b/application/dash/templates/add_service.html new file mode 100644 index 0000000..a7b07b7 --- /dev/null +++ b/application/dash/templates/add_service.html @@ -0,0 +1,57 @@ +{% extends 'base_template.html' %} + +{% block title %} +Add service +{% endblock %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/application/dash/templates/dashboard.html b/application/dash/templates/dashboard.html new file mode 100644 index 0000000..238932f --- /dev/null +++ b/application/dash/templates/dashboard.html @@ -0,0 +1,48 @@ +{% extends "base_template.html" %} + +{%block title%}Dashboard{%endblock%} + +{%block content%} +