mirror of
https://github.com/StefBuwalda/WebTech.git
synced 2025-11-01 04:10:02 +00:00
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1 +1,9 @@
|
|||||||
|
# venv's
|
||||||
venv/
|
venv/
|
||||||
|
|
||||||
|
# DB stuff
|
||||||
|
migrations/
|
||||||
|
instance/
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
25
README.md
25
README.md
@@ -1 +1,24 @@
|
|||||||
# WebTech
|
# 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
|
||||||
BIN
__pycache__/forms.cpython-312.pyc
Normal file
BIN
__pycache__/forms.cpython-312.pyc
Normal file
Binary file not shown.
15
app.py
Normal file
15
app.py
Normal file
@@ -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)
|
||||||
43
application/__init__.py
Normal file
43
application/__init__.py
Normal file
@@ -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")
|
||||||
BIN
application/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
application/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
application/auth/__pycache__/forms.cpython-312.pyc
Normal file
BIN
application/auth/__pycache__/forms.cpython-312.pyc
Normal file
Binary file not shown.
BIN
application/auth/__pycache__/models.cpython-312.pyc
Normal file
BIN
application/auth/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
application/auth/__pycache__/views.cpython-312.pyc
Normal file
BIN
application/auth/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
29
application/auth/forms.py
Normal file
29
application/auth/forms.py
Normal file
@@ -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()]
|
||||||
|
)
|
||||||
16
application/auth/models.py
Normal file
16
application/auth/models.py
Normal file
@@ -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
|
||||||
51
application/auth/templates/admin.html
Normal file
51
application/auth/templates/admin.html
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{% extends 'base_template.html' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Register
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form class="form bg-body-tertiary" method="POST">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||||
|
<symbol id="check-circle-fill" fill="currentColor" 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>
|
||||||
|
<symbol id="exclamation-triangle-fill" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
{% if feedback %}
|
||||||
|
{% if feedback=="User succesfully added" %}
|
||||||
|
<div class="alert alert-success d-flex align-items-center" role="alert">
|
||||||
|
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Success:"><use xlink:href="#check-circle-fill"/></svg>
|
||||||
|
<div>
|
||||||
|
{{feedback}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-danger d-flex align-items-center" role="alert">
|
||||||
|
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Danger:"><use xlink:href="#exclamation-triangle-fill"/></svg>
|
||||||
|
<div>
|
||||||
|
{{feedback}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
{{ form.username.label }} <br> {{ form.username() }}
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
{{ form.password.label }} <br> {{ form.password() }}
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
{{ form.confirm_password.label }} <br> {{ form.confirm_password() }}
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
{{ form.is_admin() }} {{ form.is_admin.label }}
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
{{ form.submit() }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
33
application/auth/templates/login.html
Normal file
33
application/auth/templates/login.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{% extends 'base_template.html' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Login
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form class="form bg-body-tertiary" method="POST">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||||
|
<symbol id="exclamation-triangle-fill" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
{% if feedback %}
|
||||||
|
<div class="alert alert-danger d-flex align-items-center" role="alert">
|
||||||
|
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Danger:"><use xlink:href="#exclamation-triangle-fill"/></svg>
|
||||||
|
<div>
|
||||||
|
{{feedback}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
{{ form.username.label }} <br> {{ form.username() }}
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
{{ form.password.label }} <br> {{ form.password() }}
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
{{ form.submit() }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
26
application/auth/templates/update_user.html
Normal file
26
application/auth/templates/update_user.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{% extends 'base_template.html' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Update
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form class="form bg-body-tertiary" method="POST">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{% if feedback %}
|
||||||
|
<p class="feedback">{{feedback}}</p>
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
Current password <br> {{ form.current_password() }}
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
New password <br> {{ form.password() }}
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
Confirm new password <br> {{ form.confirm_password() }}
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
{{ form.submit() }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
117
application/auth/views.py
Normal file
117
application/auth/views.py
Normal file
@@ -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"))
|
||||||
16
application/dash/forms.py
Normal file
16
application/dash/forms.py
Normal file
@@ -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")
|
||||||
18
application/dash/models.py
Normal file
18
application/dash/models.py
Normal file
@@ -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
|
||||||
57
application/dash/templates/add_service.html
Normal file
57
application/dash/templates/add_service.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{% extends 'base_template.html' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Add service
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form class="form bg-body-tertiary" method="POST" enctype="multipart/form-data">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||||
|
<symbol id="check-circle-fill" fill="currentColor" 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>
|
||||||
|
<symbol id="exclamation-triangle-fill" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
{% if feedback %}
|
||||||
|
{% if feedback=="Service succesfully added" %}
|
||||||
|
<div class="alert alert-success d-flex align-items-center" role="alert">
|
||||||
|
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Success:">
|
||||||
|
<use xlink:href="#check-circle-fill" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
{{feedback}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-danger d-flex align-items-center" role="alert">
|
||||||
|
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Danger:">
|
||||||
|
<use xlink:href="#exclamation-triangle-fill" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
{{feedback}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
{{ form.name.label }} <br> {{ form.name() }}
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
{{ form.url.label }} <br> {{ form.url() }}
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
Upload an icon:
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
{{ form.image() }}
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
{{ form.submit() }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
48
application/dash/templates/dashboard.html
Normal file
48
application/dash/templates/dashboard.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{% extends "base_template.html" %}
|
||||||
|
|
||||||
|
{%block title%}Dashboard{%endblock%}
|
||||||
|
|
||||||
|
{%block content%}
|
||||||
|
<div class="grid-container">
|
||||||
|
{% for service in services%}
|
||||||
|
<div class="bg-light container-xxl">
|
||||||
|
<div class="row row-cols-3">
|
||||||
|
<div class="col-sm-2" onclick="location.href='{{service.url}}';" style="cursor: pointer;">
|
||||||
|
<img class="fit-picture" src="{{ url_for('static', filename='icons/'+service['icon'])}}">
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-10" onclick="location.href='{{service.url}}';" style="cursor: pointer;">
|
||||||
|
{{service["name"]}}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2 dots dropdown">
|
||||||
|
<button class="btn btn-light py-0" type="button" id="threeDotDropdown" data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false">
|
||||||
|
⋮
|
||||||
|
</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>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<form action="{{ url_for('dash.delete_item', service_id=service.id) }}" method="POST" style="display:inline;">
|
||||||
|
<button style="color: red;" type="submit" class="dropdown-item">Delete</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" onclick="location.href='{{service.url}}';" style="cursor: pointer;">
|
||||||
|
{{service["url"]}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{%endblock%}
|
||||||
26
application/dash/templates/edit_service.html
Normal file
26
application/dash/templates/edit_service.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{% extends 'base_template.html' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Edit service
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form class="form bg-body-tertiary" method="POST" enctype="multipart/form-data">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div>
|
||||||
|
{{ form.name.label }} <br> {{ form.name() }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ form.url.label }} <br> {{ form.url() }}
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
Upload an icon:
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
{{ form.image() }}
|
||||||
|
</div>
|
||||||
|
<div class="padding">
|
||||||
|
{{ form.submit(value="Edit") }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
109
application/dash/views.py
Normal file
109
application/dash/views.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
from application import db
|
||||||
|
from flask import Blueprint, render_template, redirect, url_for
|
||||||
|
from application.dash.forms import ServiceForm
|
||||||
|
from flask_login import login_required, current_user # type: ignore
|
||||||
|
from application.dash.models import Service
|
||||||
|
from application.utils import saveImage
|
||||||
|
|
||||||
|
dash_blueprint = Blueprint("dash", __name__, template_folder="templates")
|
||||||
|
|
||||||
|
|
||||||
|
@dash_blueprint.route("/", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def index():
|
||||||
|
services = current_user.services # type: ignore
|
||||||
|
return render_template(
|
||||||
|
"dashboard.html", services=services, active_page="dashboard"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dash_blueprint.route("/delete_item/<int:service_id>", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def delete_item(service_id: int):
|
||||||
|
service = Service.query.get_or_404(service_id)
|
||||||
|
|
||||||
|
# Check ownership
|
||||||
|
if service.user_id != current_user.id:
|
||||||
|
return redirect(url_for("dash.index"))
|
||||||
|
|
||||||
|
db.session.delete(service)
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for("dash.index"))
|
||||||
|
|
||||||
|
|
||||||
|
@dash_blueprint.route("/service", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def service():
|
||||||
|
service_form = ServiceForm()
|
||||||
|
|
||||||
|
if service_form.validate_on_submit(): # type: ignore
|
||||||
|
image = service_form.image.data
|
||||||
|
name = service_form.name.data
|
||||||
|
url = service_form.url.data
|
||||||
|
filename2 = "google.png"
|
||||||
|
if image:
|
||||||
|
filename2 = saveImage(image)
|
||||||
|
|
||||||
|
new_service = Service(
|
||||||
|
name=name, # type: ignore
|
||||||
|
url=url, # type: ignore
|
||||||
|
user_id=current_user.id,
|
||||||
|
icon=filename2,
|
||||||
|
) # 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",
|
||||||
|
)
|
||||||
|
return render_template(
|
||||||
|
"add_service.html", form=service_form, active_page="service"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dash_blueprint.route(
|
||||||
|
"/edit_service/<int:service_id>", methods=["GET", "POST"]
|
||||||
|
)
|
||||||
|
@login_required
|
||||||
|
def edit_service(service_id: int):
|
||||||
|
service = Service.query.get_or_404(service_id)
|
||||||
|
|
||||||
|
if current_user.id != service.user_id:
|
||||||
|
redirect(url_for("dash.index"))
|
||||||
|
|
||||||
|
# Correcte gebruiker
|
||||||
|
form = ServiceForm()
|
||||||
|
if form.validate_on_submit(): # type: ignore
|
||||||
|
commit = False
|
||||||
|
if service.name != form.name.data:
|
||||||
|
service.name = form.name.data
|
||||||
|
commit = True
|
||||||
|
if service.url != form.url.data:
|
||||||
|
service.url = form.url.data
|
||||||
|
commit = True
|
||||||
|
if form.image.data:
|
||||||
|
service.icon = saveImage(form.image.data)
|
||||||
|
commit = True
|
||||||
|
if commit:
|
||||||
|
db.session.commit()
|
||||||
|
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
|
||||||
|
"""
|
||||||
16
application/decorators.py
Normal file
16
application/decorators.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from typing import Callable, Any
|
||||||
|
from flask_login import current_user # type: ignore
|
||||||
|
from functools import wraps
|
||||||
|
from flask import redirect, url_for
|
||||||
|
|
||||||
|
|
||||||
|
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"))
|
||||||
|
if not current_user.is_admin:
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
BIN
application/static/icons/1/123123123.png
Normal file
BIN
application/static/icons/1/123123123.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
application/static/icons/1/Google__G__logo.svg.png
Normal file
BIN
application/static/icons/1/Google__G__logo.svg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
BIN
application/static/icons/google.png
Normal file
BIN
application/static/icons/google.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
54
application/static/style.css
Normal file
54
application/static/style.css
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/* Base template */
|
||||||
|
body {
|
||||||
|
background-color: lightslategray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard page */
|
||||||
|
.grid-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto auto;
|
||||||
|
gap: 20px;
|
||||||
|
background-color: lightslategray;
|
||||||
|
padding-top: 20px;
|
||||||
|
margin-left: 10%;
|
||||||
|
margin-right: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-container > div {
|
||||||
|
height: fit-content;
|
||||||
|
width: 400px;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fit-picture {
|
||||||
|
width: 45px;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Login page */
|
||||||
|
.form {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
height: fit-content;
|
||||||
|
width: fit-content;
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback {
|
||||||
|
font-size: 16px;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
59
application/templates/base_template.html
Normal file
59
application/templates/base_template.html
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||||
|
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||||
|
<link href="{{url_for('static', filename='style.css')}}" rel="stylesheet">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
||||||
|
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<nav class="navbar sticky-top navbar-expand-lg bg-body-tertiary" data-bs-theme="dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="{{url_for('index')}}">ServiceHUB</a>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if active_page == 'dashboard' %}active{% endif %}" aria-current=" page"
|
||||||
|
href="{{url_for('dash.index')}}">Dashboard</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if active_page == 'service' %}active{% endif %}"
|
||||||
|
href="{{url_for('dash.service')}}">Add service</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.is_admin %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if active_page == 'register' %}active{% endif %}"
|
||||||
|
href="{{url_for('auth.register')}}">Add user</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</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">
|
||||||
|
Profile
|
||||||
|
</button>
|
||||||
|
<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>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
17
application/utils.py
Normal file
17
application/utils.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
import os
|
||||||
|
from application import app
|
||||||
|
from flask_login import current_user # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
@@ -1,8 +1,19 @@
|
|||||||
|
alembic==1.15.2
|
||||||
blinker==1.9.0
|
blinker==1.9.0
|
||||||
click==8.1.8
|
click==8.1.8
|
||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
Flask==3.1.0
|
Flask==3.1.0
|
||||||
|
Flask-Login==0.6.3
|
||||||
|
Flask-Migrate==4.1.0
|
||||||
|
Flask-SQLAlchemy==3.1.1
|
||||||
|
Flask-WTF==1.2.2
|
||||||
|
greenlet==3.1.1
|
||||||
itsdangerous==2.2.0
|
itsdangerous==2.2.0
|
||||||
Jinja2==3.1.6
|
Jinja2==3.1.6
|
||||||
|
Mako==1.3.9
|
||||||
MarkupSafe==3.0.2
|
MarkupSafe==3.0.2
|
||||||
|
pillow==11.1.0
|
||||||
|
SQLAlchemy==2.0.40
|
||||||
|
typing_extensions==4.13.0
|
||||||
Werkzeug==3.1.3
|
Werkzeug==3.1.3
|
||||||
|
WTForms==3.2.1
|
||||||
|
|||||||
54
seed.py
Normal file
54
seed.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from application import db, app
|
||||||
|
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),
|
||||||
|
]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
new_users = [
|
||||||
|
User(
|
||||||
|
username="admin",
|
||||||
|
password=generate_password_hash("admin"),
|
||||||
|
is_admin=True,
|
||||||
|
),
|
||||||
|
User(
|
||||||
|
username="Daan",
|
||||||
|
password=generate_password_hash("pass"),
|
||||||
|
is_admin=True,
|
||||||
|
),
|
||||||
|
User(
|
||||||
|
username="stef",
|
||||||
|
password=generate_password_hash("stef123"),
|
||||||
|
is_admin=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
new_services = [
|
||||||
|
Service(name="test123", url="http://google.com", user_id=1),
|
||||||
|
# Daan services
|
||||||
|
Service(name="Google", url="https://google.com", user_id=2),
|
||||||
|
Service(name="Netflix", url="https://www.netflix.com", user_id=2),
|
||||||
|
# Stef services
|
||||||
|
Service(name="Plex", url="https://plex.local", user_id=3),
|
||||||
|
Service(name="TrueNAS", url="https://truenas.local", user_id=3),
|
||||||
|
Service(name="Transmission", url="https://transmission.local", user_id=3),
|
||||||
|
Service(name="Tautulli", url="https://tautulli.local", user_id=3),
|
||||||
|
Service(name="Overseerr", url="https://overseerr.local", user_id=3),
|
||||||
|
Service(name="Plex", url="https://plex.local", user_id=3),
|
||||||
|
]
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
# Remove all existing
|
||||||
|
Service.query.delete()
|
||||||
|
User.query.delete()
|
||||||
|
db.session.commit()
|
||||||
|
# Then add new
|
||||||
|
db.session.add_all(new_services)
|
||||||
|
db.session.add_all(new_users)
|
||||||
|
db.session.commit()
|
||||||
BIN
test/123123123.png
Normal file
BIN
test/123123123.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Reference in New Issue
Block a user