From 94efe3bf66ff0a6eb60f42467f902b701d4b2f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Wed, 2 Jul 2025 11:43:43 +0200 Subject: [PATCH] first commit --- .gitignore | 6 + Dockerfile | 24 +++ app.py | 281 ++++++++++++++++++++++++++++ config.py | 9 + docker-compose.yml | 0 entrypoint.sh | 3 + requirements.txt | 6 + static/js/live.js | 109 +++++++++++ templates/admin/add_user.html | 24 +++ templates/admin/admin_panel.html | 50 +++++ templates/admin/list_users.html | 32 ++++ templates/admin/reset_password.html | 21 +++ templates/base.html | 30 +++ templates/dashboard.html | 44 +++++ templates/index.html | 45 +++++ templates/list.html | 47 +++++ templates/list_guest.html | 20 ++ templates/login.html | 24 +++ templates/system_auth.html | 21 +++ 19 files changed, 796 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 app.py create mode 100644 config.py create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh create mode 100644 requirements.txt create mode 100644 static/js/live.js create mode 100644 templates/admin/add_user.html create mode 100644 templates/admin/admin_panel.html create mode 100644 templates/admin/list_users.html create mode 100644 templates/admin/reset_password.html create mode 100644 templates/base.html create mode 100644 templates/dashboard.html create mode 100644 templates/index.html create mode 100644 templates/list.html create mode 100644 templates/list_guest.html create mode 100644 templates/login.html create mode 100644 templates/system_auth.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1363273 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +venv +.env +env +*.db +__pycache__ +instance \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..925cfc4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# Używamy lekkiego obrazu Pythona +FROM python:3.11-slim + +# Ustawiamy katalog roboczy +WORKDIR /app + +# Kopiujemy wymagania +COPY requirements.txt requirements.txt + +# Instalujemy zależności +RUN pip install --no-cache-dir -r requirements.txt + +# Kopiujemy resztę aplikacji +COPY . . + +# Kopiujemy entrypoint i ustawiamy uprawnienia +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Otwieramy port +EXPOSE 8000 + +# Ustawiamy entrypoint +ENTRYPOINT ["/entrypoint.sh"] diff --git a/app.py b/app.py new file mode 100644 index 0000000..8b33a57 --- /dev/null +++ b/app.py @@ -0,0 +1,281 @@ +import os +import secrets +from datetime import datetime, timedelta +from flask import Flask, render_template, redirect, url_for, request, flash +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user +from flask_socketio import SocketIO, emit, join_room +from werkzeug.security import generate_password_hash, check_password_hash +from config import Config + +app = Flask(__name__) +app.config.from_object(Config) +SYSTEM_PASSWORD = app.config.get('SYSTEM_PASSWORD', 'changeme') + +db = SQLAlchemy(app) +socketio = SocketIO(app) +login_manager = LoginManager(app) +login_manager.login_view = 'login' + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(150), unique=True, nullable=False) + password_hash = db.Column(db.String(150), nullable=False) + is_admin = db.Column(db.Boolean, default=False) + +class ShoppingList(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(150), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + owner_id = db.Column(db.Integer, db.ForeignKey('user.id')) + is_temporary = db.Column(db.Boolean, default=False) + share_token = db.Column(db.String(64), unique=True, nullable=True) + expires_at = db.Column(db.DateTime, nullable=True) + +class Item(db.Model): + id = db.Column(db.Integer, primary_key=True) + list_id = db.Column(db.Integer, db.ForeignKey('shopping_list.id')) + name = db.Column(db.String(150), nullable=False) + added_at = db.Column(db.DateTime, default=datetime.utcnow) + added_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) + purchased = db.Column(db.Boolean, default=False) + purchased_at = db.Column(db.DateTime, nullable=True) + +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) + +@app.before_request +def require_system_password(): + if 'authorized' not in request.cookies and request.endpoint != 'system_auth' and not request.endpoint.startswith('static') and not request.endpoint.startswith('login'): + return redirect(url_for('system_auth')) + +@app.route('/system-auth', methods=['GET', 'POST']) +def system_auth(): + if request.method == 'POST': + if request.form['password'] == SYSTEM_PASSWORD: + db.create_all() + if not User.query.filter_by(is_admin=True).first(): + admin_user = User(username='admin', password_hash=generate_password_hash('admin123'), is_admin=True) + db.session.add(admin_user) + db.session.commit() + flash('Utworzono konto administratora: login=admin, hasło=admin123') + resp = redirect(url_for('index_guest')) + resp.set_cookie('authorized', 'true') + return resp + flash('Nieprawidłowe hasło do systemu') + return render_template('system_auth.html') + +@app.route('/') +def index_guest(): + lists = ShoppingList.query.all() + return render_template('index.html', lists=lists) + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + user = User.query.filter_by(username=request.form['username']).first() + if user and check_password_hash(user.password_hash, request.form['password']): + login_user(user) + flash('Zalogowano pomyślnie') + return redirect(url_for('dashboard')) + flash('Nieprawidłowy login lub hasło') + return render_template('login.html') + +@app.route('/dashboard') +@login_required +def dashboard(): + lists = ShoppingList.query.filter_by(owner_id=current_user.id).all() + return render_template('dashboard.html', lists=lists) + +@app.route('/logout') +@login_required +def logout(): + logout_user() + return redirect(url_for('login')) + +@app.route('/create', methods=['POST']) +@login_required +def create_list(): + title = request.form.get('title') + is_temporary = 'temporary' in request.form + token = secrets.token_hex(16) + expires_at = datetime.utcnow() + timedelta(days=7) if is_temporary else None + new_list = ShoppingList(title=title, owner_id=current_user.id, is_temporary=is_temporary, share_token=token, expires_at=expires_at) + db.session.add(new_list) + db.session.commit() + return redirect(url_for('view_list', list_id=new_list.id)) + +@app.route('/list/') +@login_required +def view_list(list_id): + shopping_list = ShoppingList.query.get_or_404(list_id) + items = Item.query.filter_by(list_id=list_id).all() + return render_template('list.html', list=shopping_list, items=items) + +@app.route('/share/') +def share_list(token): + shopping_list = ShoppingList.query.filter_by(share_token=token).first_or_404() + items = Item.query.filter_by(list_id=shopping_list.id).all() + return render_template('list_guest.html', list=shopping_list, items=items) + +@app.route('/copy/') +@login_required +def copy_list(list_id): + original = ShoppingList.query.get_or_404(list_id) + token = secrets.token_hex(16) + new_list = ShoppingList(title=original.title + ' (Kopia)', owner_id=current_user.id, share_token=token) + db.session.add(new_list) + db.session.commit() + original_items = Item.query.filter_by(list_id=original.id).all() + for item in original_items: + copy_item = Item(list_id=new_list.id, name=item.name) + db.session.add(copy_item) + db.session.commit() + return redirect(url_for('view_list', list_id=new_list.id)) + +@app.route('/admin') +@login_required +def admin_panel(): + if not current_user.is_admin: + return redirect(url_for('index_guest')) + user_count = User.query.count() + list_count = ShoppingList.query.count() + item_count = Item.query.count() + all_lists = ShoppingList.query.all() + return render_template('admin/admin_panel.html', user_count=user_count, list_count=list_count, item_count=item_count, all_lists=all_lists) + +@app.route('/admin/delete_list/') +@login_required +def delete_list(list_id): + if not current_user.is_admin: + return redirect(url_for('index_guest')) + list_to_delete = ShoppingList.query.get_or_404(list_id) + # Usuń wszystkie powiązane produkty + Item.query.filter_by(list_id=list_to_delete.id).delete() + db.session.delete(list_to_delete) + db.session.commit() + flash(f'Usunięto listę: {list_to_delete.title}') + return redirect(url_for('admin_panel')) + +@app.route('/admin/delete_all_lists') +@login_required +def delete_all_lists(): + if not current_user.is_admin: + return redirect(url_for('index_guest')) + Item.query.delete() + ShoppingList.query.delete() + db.session.commit() + flash('Usunięto wszystkie listy') + return redirect(url_for('admin_panel')) + +@app.route('/admin/add_user', methods=['GET', 'POST']) +@login_required +def add_user(): + if not current_user.is_admin: + return redirect(url_for('index_guest')) + if request.method == 'POST': + username = request.form['username'] + password = generate_password_hash(request.form['password']) + new_user = User(username=username, password_hash=password) + db.session.add(new_user) + db.session.commit() + flash('Dodano nowego użytkownika') + return redirect(url_for('admin_panel')) + return render_template('admin/add_user.html') + +@app.route('/admin/users') +@login_required +def list_users(): + if not current_user.is_admin: + return redirect(url_for('index_guest')) + users = User.query.all() + user_count = User.query.count() + list_count = ShoppingList.query.count() + item_count = Item.query.count() + activity_log = ["Utworzono listę: Zakupy weekendowe", "Dodano produkt: Mleko"] + return render_template('admin/list_users.html', users=users, user_count=user_count, list_count=list_count, item_count=item_count, activity_log=activity_log) + +@app.route('/admin/reset_password/', methods=['GET', 'POST']) +@login_required +def reset_password(user_id): + if not current_user.is_admin: + return redirect(url_for('index_guest')) + user = User.query.get_or_404(user_id) + if request.method == 'POST': + new_password = generate_password_hash(request.form['password']) + user.password_hash = new_password + db.session.commit() + flash('Hasło zresetowane') + return redirect(url_for('list_users')) + return render_template('admin/reset_password.html', user=user) + +@app.route('/admin/delete_user/') +@login_required +def delete_user(user_id): + if not current_user.is_admin: + return redirect(url_for('index_guest')) + user = User.query.get_or_404(user_id) + db.session.delete(user) + db.session.commit() + flash('Użytkownik usunięty') + return redirect(url_for('list_users')) + +@socketio.on('delete_item') +def handle_delete_item(data): + item = Item.query.get(data['item_id']) + if item: + db.session.delete(item) + db.session.commit() + emit('item_deleted', {'item_id': item.id}, to=str(item.list_id)) + +@socketio.on('edit_item') +def handle_edit_item(data): + item = Item.query.get(data['item_id']) + new_name = data['new_name'] + if item and new_name.strip(): + item.name = new_name + db.session.commit() + emit('item_edited', {'item_id': item.id, 'new_name': item.name}, to=str(item.list_id)) + + +@socketio.on('join_list') +def handle_join(data): + room = str(data['room']) + username = data.get('username', 'Gość') + join_room(room) + emit('user_joined', {'username': username}, to=room) + +@socketio.on('add_item') +def handle_add_item(data): + list_id = data['list_id'] + name = data['name'] + new_item = Item( + list_id=list_id, + name=name, + added_by=current_user.id if current_user.is_authenticated else None + ) + db.session.add(new_item) + db.session.commit() + emit('item_added', { + 'id': new_item.id, + 'name': new_item.name, + 'added_by': current_user.username if current_user.is_authenticated else 'Gość' + }, to=str(list_id), include_self=True) + +@socketio.on('check_item') +def handle_check_item(data): + item = Item.query.get(data['item_id']) + if item: + item.purchased = True + item.purchased_at = datetime.utcnow() + db.session.commit() + emit('item_checked', {'item_id': item.id}, to=str(item.list_id)) + +@app.cli.command('create_db') +def create_db(): + db.create_all() + print('Database created.') + +if __name__ == '__main__': + socketio.run(app, debug=True) diff --git a/config.py b/config.py new file mode 100644 index 0000000..70f248e --- /dev/null +++ b/config.py @@ -0,0 +1,9 @@ +import os + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY', 'D8pceNZ8q%YR7^7F&9wAC2') + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///shopping.db') + SQLALCHEMY_TRACK_MODIFICATIONS = False + SYSTEM_PASSWORD = os.environ.get('SYSTEM_PASSWORD', 'admin') + DEFAULT_ADMIN_USERNAME = os.environ.get('DEFAULT_ADMIN_USERNAME', 'admin') + DEFAULT_ADMIN_PASSWORD = os.environ.get('DEFAULT_ADMIN_PASSWORD', 'admin123') diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e69de29 diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..d2c9f92 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/sh +flask db upgrade 2>/dev/null || flask create_db +exec python app.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0b8f317 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +Flask +Flask-SQLAlchemy +Flask-Login +Flask-SocketIO +eventlet +Werkzeug diff --git a/static/js/live.js b/static/js/live.js new file mode 100644 index 0000000..421b0e5 --- /dev/null +++ b/static/js/live.js @@ -0,0 +1,109 @@ +const socket = io(); + +function setupList(listId, username) { + socket.emit('join_list', { room: listId, username: username }); + + const newItemInput = document.getElementById('newItem'); + if (newItemInput) { + newItemInput.focus(); + newItemInput.addEventListener('keypress', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + addItem(listId); + } + }); + } + + socket.on('user_joined', data => showToast(`${data.username} dołączył do listy`)); + + socket.on('item_added', data => { + showToast(`${data.added_by} dodał: ${data.name}`); + const list = document.getElementById('items'); + const li = document.createElement('li'); + li.className = 'list-group-item bg-dark text-white d-flex justify-content-between align-items-center'; + li.id = `item-${data.id}`; + li.innerHTML = ` +
+ + ${data.name} +
+
+ + +
+ `; + list.appendChild(li); + }); + + socket.on('item_checked', data => { + const checkbox = document.querySelector(`#item-${data.item_id} input[type='checkbox']`); + if (checkbox) { + checkbox.checked = true; + } + }); + + socket.on('item_deleted', data => { + const li = document.getElementById(`item-${data.item_id}`); + if (li) { + li.remove(); + } + showToast('Usunięto produkt'); + }); + + socket.on('item_edited', data => { + const nameSpan = document.getElementById(`name-${data.item_id}`); + if (nameSpan) { + nameSpan.innerText = data.new_name; + } + showToast(`Zmieniono nazwę na: ${data.new_name}`); + }); +} + +function addItem(listId) { + const name = document.getElementById('newItem').value; + if (name.trim() === '') return; + socket.emit('add_item', { list_id: listId, name: name }); + document.getElementById('newItem').value = ''; + document.getElementById('newItem').focus(); +} + +function checkItem(id) { + socket.emit('check_item', { item_id: id }); +} + +function deleteItem(id) { + if (confirm('Na pewno usunąć produkt?')) { + socket.emit('delete_item', { item_id: id }); + } +} + +function editItem(id, oldName) { + const newName = prompt('Podaj nową nazwę:', oldName); + if (newName && newName.trim() !== '') { + socket.emit('edit_item', { item_id: id, new_name: newName }); + } +} + +function copyLink(link) { + if (navigator.share) { + navigator.share({ + title: 'Lista zakupów', + text: 'Udostępniam Ci moją listę zakupów:', + url: link + }).catch((err) => console.log('Udostępnianie anulowane', err)); + } else { + navigator.clipboard.writeText(link).then(() => { + showToast('Link skopiowany do schowka!'); + }); + } +} + +function showToast(message) { + const toastContainer = document.getElementById('toast-container'); + const toast = document.createElement('div'); + toast.className = 'toast align-items-center text-bg-primary border-0 show'; + toast.setAttribute('role', 'alert'); + toast.innerHTML = `
${message}
`; + toastContainer.appendChild(toast); + setTimeout(() => { toast.remove(); }, 4000); +} diff --git a/templates/admin/add_user.html b/templates/admin/add_user.html new file mode 100644 index 0000000..7322128 --- /dev/null +++ b/templates/admin/add_user.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% block title %}Dodaj użytkownika{% endblock %} +{% block content %} + +
+

➕ Dodaj nowego użytkownika

+ ← Powrót do panelu +
+ +
+
+
+
+ +
+
+ +
+ +
+
+
+ +{% endblock %} diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html new file mode 100644 index 0000000..72b8f99 --- /dev/null +++ b/templates/admin/admin_panel.html @@ -0,0 +1,50 @@ +{% extends 'base.html' %} +{% block title %}Panel administratora{% endblock %} +{% block content %} + +
+

⚙️ Panel administratora

+ ← Powrót do strony głównej +
+ +
+
+

👤 Liczba użytkowników: {{ user_count }}

+

📝 Liczba list: {{ list_count }}

+

🛒 Liczba produktów: {{ item_count }}

+
+
+ + + +

📄 Wszystkie listy zakupowe

+
+ + + + + + + + + + + {% for l in all_lists %} + + + + + + + {% endfor %} + +
IDTytułWłaściciel (ID)Akcje
{{ l.id }}{{ l.title }}{{ l.owner_id }} + 🗑️ Usuń +
+
+ +{% endblock %} diff --git a/templates/admin/list_users.html b/templates/admin/list_users.html new file mode 100644 index 0000000..2d624cd --- /dev/null +++ b/templates/admin/list_users.html @@ -0,0 +1,32 @@ +{% extends 'base.html' %} +{% block title %}Lista użytkowników{% endblock %} +{% block content %} + +
+

👥 Lista użytkowników

+ ← Powrót do panelu +
+ + + + + + + + + + + {% for user in users %} + + + + + + {% endfor %} + +
IDLoginAkcje
{{ user.id }}{{ user.username }} + 🔑 Resetuj + 🗑️ Usuń +
+ +{% endblock %} diff --git a/templates/admin/reset_password.html b/templates/admin/reset_password.html new file mode 100644 index 0000000..c9e1113 --- /dev/null +++ b/templates/admin/reset_password.html @@ -0,0 +1,21 @@ +{% extends 'base.html' %} +{% block title %}Resetuj hasło{% endblock %} +{% block content %} + +
+

🔑 Resetuj hasło: {{ user.username }}

+ ← Powrót do listy użytkowników +
+ +
+
+
+
+ +
+ +
+
+
+ +{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..c00d0ae --- /dev/null +++ b/templates/base.html @@ -0,0 +1,30 @@ + + + + + {% block title %}Live Lista Zakupów{% endblock %} + + + + + + + + +
+ {% block content %}{% endblock %} +
+ + + diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..bf9d2af --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,44 @@ +{% extends 'base.html' %} +{% block title %}Twoje listy{% endblock %} +{% block content %} + +
+

📝 Twoje listy zakupowe

+ +
+ +{% if lists %} +
+ + + + + + + + + + + {% for l in lists %} + + + + + + + {% endfor %} + +
IDTytułData utworzeniaAkcje
{{ l.id }}{{ l.title }}{{ l.created_at.strftime('%Y-%m-%d %H:%M') }} + 📄 Otwórz + 📋 Kopiuj +
+
+{% else %} +

Nie masz jeszcze żadnych list. Kliknij „Utwórz nową listę”, aby dodać pierwszą!

+{% endif %} + +{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..1268042 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,45 @@ +{% extends 'base.html' %} +{% block title %}Twoje listy zakupów{% endblock %} +{% block content %} + +
+

Twoje listy zakupów

+ ← Powrót do panelu +
+ +
+
+
+
+ +
+
+
+ + +
+
+
+ +
+
+
+
+ +{% if lists %} + +{% else %} +

Nie masz jeszcze żadnych list. Utwórz pierwszą, korzystając z formularza powyżej!

+{% endif %} + +{% endblock %} diff --git a/templates/list.html b/templates/list.html new file mode 100644 index 0000000..f80bc2d --- /dev/null +++ b/templates/list.html @@ -0,0 +1,47 @@ +{% extends 'base.html' %} +{% block title %}{{ list.title }}{% endblock %} +{% block content %} + +
+

{{ list.title }}

+ ← Powrót do list +
+ +
+
+
+ Udostępnij link: + {{ request.url_root }}share/{{ list.share_token }} +
+ +
+
+ +
    + {% for item in items %} +
  • +
    + + {{ item.name }} +
    +
    + + +
    +
  • + {% endfor %} +
+ +
+ + +
+ +
+ + +{% endblock %} diff --git a/templates/list_guest.html b/templates/list_guest.html new file mode 100644 index 0000000..84aab8c --- /dev/null +++ b/templates/list_guest.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% block title %}{{ list.title }} (Gość){% endblock %} +{% block content %} +

{{ list.title }} (Gość)

+ +
    + {% for item in items %} +
  • + {{ item.name }} +
  • + {% endfor %} +
+ + + + +
+ +Powrót +{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..68834cf --- /dev/null +++ b/templates/login.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% block title %}Logowanie{% endblock %} +{% block content %} + +
+

🔒 Logowanie

+ ← Powrót do list +
+ +
+
+
+
+ +
+
+ +
+ +
+
+
+ +{% endblock %} diff --git a/templates/system_auth.html b/templates/system_auth.html new file mode 100644 index 0000000..b9a2d66 --- /dev/null +++ b/templates/system_auth.html @@ -0,0 +1,21 @@ +{% extends 'base.html' %} +{% block title %}Hasło do systemu{% endblock %} +{% block content %} + +
+

🔑 Hasło do systemu

+ ← Powrót do list +
+ +
+
+
+
+ +
+ +
+
+
+ +{% endblock %}