mirror of
https://github.com/StefBuwalda/WebTech.git
synced 2025-10-29 19:00:00 +00:00
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1 +1,9 @@
|
||||
# venv's
|
||||
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
|
||||
click==8.1.8
|
||||
colorama==0.4.6
|
||||
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
|
||||
Jinja2==3.1.6
|
||||
Mako==1.3.9
|
||||
MarkupSafe==3.0.2
|
||||
pillow==11.1.0
|
||||
SQLAlchemy==2.0.40
|
||||
typing_extensions==4.13.0
|
||||
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