diff --git a/__pycache__/app.cpython-311.pyc b/__pycache__/app.cpython-311.pyc new file mode 100644 index 0000000..0c17199 Binary files /dev/null and b/__pycache__/app.cpython-311.pyc differ diff --git a/app.py b/app.py index 05a5d7e..76b6922 100644 --- a/app.py +++ b/app.py @@ -1,14 +1,22 @@ import os -from werkzeug.utils import secure_filename from flask import Flask, render_template, request, redirect, url_for from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user +from flask_bcrypt import Bcrypt +from werkzeug.utils import secure_filename +from functools import wraps +from flask_migrate import Migrate app = Flask(__name__) +# Set the secret key to a fixed value +app.config['SECRET_KEY'] = 'Ana_Are_Multe_Mere-Si_Nu_Are_Pere' + # Configurare baza de date SQLite app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///dashboard.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) +bcrypt = Bcrypt(app) UPLOAD_FOLDER = 'static/uploads' app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER @@ -17,13 +25,30 @@ app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER if not os.path.exists(UPLOAD_FOLDER): os.makedirs(UPLOAD_FOLDER) +login_manager = LoginManager(app) +login_manager.login_view = 'login' + +migrate = Migrate(app, db) + + +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) + # Modele pentru baza de date +class User(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + password = db.Column(db.String(120), nullable=False) + role = db.Column(db.String(20), nullable=False, default='user') + class Player(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), nullable=False) hostname = db.Column(db.String(120), nullable=False) ip = db.Column(db.String(15), nullable=False) password = db.Column(db.String(120), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id', name='fk_user_id'), nullable=True) class Group(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -42,13 +67,52 @@ class Content(db.Model): player_id = db.Column(db.Integer, db.ForeignKey('player.id'), nullable=True) group_id = db.Column(db.Integer, db.ForeignKey('group.id'), nullable=True) +def admin_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if current_user.role != 'admin': + return redirect(url_for('dashboard')) + return f(*args, **kwargs) + return decorated_function + @app.route('/') +@login_required def dashboard(): players = Player.query.all() groups = Group.query.all() return render_template('dashboard.html', players=players, groups=groups) +@app.route('/register', methods=['GET', 'POST']) +def register(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + hashed_password = bcrypt.generate_password_hash(password).decode('utf-8') + new_user = User(username=username, password=hashed_password, role='user') + db.session.add(new_user) + db.session.commit() + return redirect(url_for('login')) + return render_template('register.html') + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + user = User.query.filter_by(username=username).first() + if user and bcrypt.check_password_hash(user.password, password): + login_user(user) + return redirect(url_for('dashboard')) + return render_template('login.html') + +@app.route('/logout') +@login_required +def logout(): + logout_user() + return redirect(url_for('login')) + @app.route('/add_player', methods=['GET', 'POST']) +@login_required def add_player(): if request.method == 'POST': username = request.form['username'] @@ -65,6 +129,7 @@ def add_player(): return render_template('add_player.html') @app.route('/add_group', methods=['GET', 'POST']) +@login_required def add_group(): if request.method == 'POST': group_name = request.form['group_name'] @@ -84,6 +149,7 @@ def add_group(): return render_template('add_group.html', players=players) @app.route('/upload_content', methods=['GET', 'POST']) +@login_required def upload_content(): if request.method == 'POST': target_type = request.form['target_type'] @@ -92,22 +158,18 @@ def upload_content(): duration = int(request.form['duration']) for file in files: - if target_type == 'player': - new_content = Content(file_name=file.filename, duration=duration, player_id=int(target_id)) - elif target_type == 'group': - new_content = Content(file_name=file.filename, duration=duration, group_id=int(target_id)) + filename = secure_filename(file.filename) + file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + file.save(file_path) + new_content = Content(file_name=filename, duration=duration, player_id=target_id if target_type == 'player' else None, group_id=target_id if target_type == 'group' else None) db.session.add(new_content) db.session.commit() return redirect(url_for('dashboard')) - - players = Player.query.all() - groups = Group.query.all() - return render_template('upload_content.html', players=players, groups=groups) - -# ...existing code... + return render_template('upload_content.html') @app.route('/content//edit', methods=['POST']) +@login_required def edit_content(content_id): content = Content.query.get_or_404(content_id) new_duration = int(request.form['duration']) @@ -116,6 +178,7 @@ def edit_content(content_id): return redirect(url_for('player_page', player_id=content.player_id)) @app.route('/content//delete', methods=['POST']) +@login_required def delete_content(content_id): content = Content.query.get_or_404(content_id) player_id = content.player_id @@ -123,21 +186,22 @@ def delete_content(content_id): db.session.commit() return redirect(url_for('player_page', player_id=player_id)) -# ...existing code... - @app.route('/player//fullscreen') +@login_required def player_fullscreen(player_id): player = Player.query.get_or_404(player_id) content = Content.query.filter_by(player_id=player_id).all() return render_template('player_fullscreen.html', player=player, content=content) @app.route('/player/') +@login_required def player_page(player_id): player = Player.query.get_or_404(player_id) content = Content.query.filter_by(player_id=player_id).all() return render_template('player_page.html', player=player, content=content) @app.route('/player//upload', methods=['POST']) +@login_required def upload_content_to_player(player_id): player = Player.query.get_or_404(player_id) files = request.files.getlist('files') @@ -154,12 +218,52 @@ def upload_content_to_player(player_id): return redirect(url_for('player_page', player_id=player_id)) @app.route('/player//delete', methods=['POST']) +@login_required def delete_player(player_id): player = Player.query.get_or_404(player_id) db.session.delete(player) db.session.commit() return redirect(url_for('dashboard')) +@app.route('/admin') +@login_required +@admin_required +def admin(): + users = User.query.all() + return render_template('admin.html', users=users) + +@app.route('/admin/change_role/', methods=['POST']) +@login_required +@admin_required +def change_role(user_id): + user = User.query.get_or_404(user_id) + new_role = request.form['role'] + user.role = new_role + db.session.commit() + return redirect(url_for('admin')) + +@app.route('/admin/delete_user/', methods=['POST']) +@login_required +@admin_required +def delete_user(user_id): + user = User.query.get_or_404(user_id) + db.session.delete(user) + db.session.commit() + return redirect(url_for('admin')) + +@app.route('/admin/create_user', methods=['POST']) +@login_required +@admin_required +def create_user(): + username = request.form['username'] + password = request.form['password'] + role = request.form['role'] + hashed_password = bcrypt.generate_password_hash(password).decode('utf-8') + new_user = User(username=username, password=hashed_password, role=role) + db.session.add(new_user) + db.session.commit() + return redirect(url_for('admin')) + if __name__ == '__main__': with app.app_context(): db.create_all() # Creează toate tabelele diff --git a/create_default_user.py b/create_default_user.py new file mode 100644 index 0000000..d6b822a --- /dev/null +++ b/create_default_user.py @@ -0,0 +1,20 @@ +from app import app, db, User, bcrypt + +# Create the default user +username = 'admin' +password = '1234' +hashed_password = bcrypt.generate_password_hash(password).decode('utf-8') + +with app.app_context(): + # Delete the existing user if it exists + existing_user = User.query.filter_by(username=username).first() + if existing_user: + db.session.delete(existing_user) + db.session.commit() + + # Add the new user to the database + default_user = User(username=username, password=hashed_password, role='admin') + db.session.add(default_user) + db.session.commit() + +print(f"Default user '{username}' created with password '{password}'") \ No newline at end of file diff --git a/enviroment.txt b/enviroment.txt index ecbc84c..4a27a74 100644 --- a/enviroment.txt +++ b/enviroment.txt @@ -4,6 +4,8 @@ source digiscreen/bin/activate pip install flask sqlalchemy flask-sqlalchemy +pip install flask-login flask-bcrypt + python3 setup.py sdist python3 setup.py bdist_wheel flask \ No newline at end of file diff --git a/instance/dashboard.db b/instance/dashboard.db index 9521142..4311077 100644 Binary files a/instance/dashboard.db and b/instance/dashboard.db differ diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/__pycache__/env.cpython-311.pyc b/migrations/__pycache__/env.cpython-311.pyc new file mode 100644 index 0000000..0205ce4 Binary files /dev/null and b/migrations/__pycache__/env.cpython-311.pyc differ diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/0de18b4ddaa3_initial_migration.py b/migrations/versions/0de18b4ddaa3_initial_migration.py new file mode 100644 index 0000000..0f61892 --- /dev/null +++ b/migrations/versions/0de18b4ddaa3_initial_migration.py @@ -0,0 +1,34 @@ +"""Initial migration + +Revision ID: 0de18b4ddaa3 +Revises: +Create Date: 2025-01-20 14:50:44.116314 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0de18b4ddaa3' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('player', schema=None) as batch_op: + batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True)) + batch_op.create_foreign_key('fk_user_id', 'user', ['user_id'], ['id']) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('player', schema=None) as batch_op: + batch_op.drop_constraint('fk_user_id', type_='foreignkey') + batch_op.drop_column('user_id') + + # ### end Alembic commands ### diff --git a/migrations/versions/__pycache__/0de18b4ddaa3_initial_migration.cpython-311.pyc b/migrations/versions/__pycache__/0de18b4ddaa3_initial_migration.cpython-311.pyc new file mode 100644 index 0000000..149862f Binary files /dev/null and b/migrations/versions/__pycache__/0de18b4ddaa3_initial_migration.cpython-311.pyc differ diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..7267c26 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,68 @@ + + + + Admin Panel + + + +
+

Admin Panel

+

Manage Users

+ + + + + + + + + + {% for user in users %} + + + + + + {% endfor %} + +
UsernameRoleActions
{{ user.username }}{{ user.role }} +
+ + +
+
+ +
+
+ +

Create New User

+
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ + + + \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html index debd26c..50412e3 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -10,6 +10,23 @@

Dashboard

+ +
+ Sign Out +
+ + + {% if current_user.role == 'admin' %} +
+
+

Users Management

+
+ +
+ {% endif %} +
@@ -18,8 +35,6 @@
    {% for player in players %} - -
  • {{ player.username }} ({{ player.ip }}) @@ -27,18 +42,20 @@
    View Schedule Full Screen + {% if current_user.role == 'admin' %}
    + {% endif %}
  • - - {% endfor %}
+ {% if current_user.role == 'admin' %} + {% endif %}
@@ -55,9 +72,11 @@ {% endfor %} + {% if current_user.role == 'admin' %} + {% endif %}
diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..c11439b --- /dev/null +++ b/templates/login.html @@ -0,0 +1,24 @@ + + + + Login + + + +
+

Login

+
+
+ + +
+
+ + +
+ +
+
+ + + \ No newline at end of file diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..982a6f0 --- /dev/null +++ b/templates/register.html @@ -0,0 +1,24 @@ + + + + Register + + + +
+

Register

+
+
+ + +
+
+ + +
+ +
+
+ + + \ No newline at end of file