from flask import Flask, render_template, request, redirect, url_for, flash from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager, login_user, login_required, logout_user, current_user, UserMixin from werkzeug.security import generate_password_hash, check_password_hash from datetime import datetime from markupsafe import Markup import markdown as md from flask import request, flash, abort import os import re import socket app = Flask(__name__) # Ładujemy konfigurację z pliku config.py app.config.from_object('config.Config') db = SQLAlchemy(app) login_manager = LoginManager(app) login_manager.login_view = 'login' # MODELE class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) is_admin = db.Column(db.Boolean, default=False) # Flaga głównego administratora def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) class Zbiorka(db.Model): id = db.Column(db.Integer, primary_key=True) nazwa = db.Column(db.String(100), nullable=False) opis = db.Column(db.Text, nullable=False) numer_konta = db.Column(db.String(50), nullable=False) numer_telefonu_blik = db.Column(db.String(50), nullable=False) cel = db.Column(db.Float, nullable=False, default=0.0) stan = db.Column(db.Float, default=0.0) ukryta = db.Column(db.Boolean, default=False) ukryj_kwote = db.Column(db.Boolean, default=False) wplaty = db.relationship('Wplata', backref='zbiorka', lazy=True, order_by='Wplata.data.desc()') zrealizowana = db.Column(db.Boolean, default=False) class Wplata(db.Model): id = db.Column(db.Integer, primary_key=True) zbiorka_id = db.Column(db.Integer, db.ForeignKey('zbiorka.id'), nullable=False) kwota = db.Column(db.Float, nullable=False) data = db.Column(db.DateTime, default=datetime.utcnow) opis = db.Column(db.Text, nullable=True) # Opis wpłaty class GlobalSettings(db.Model): id = db.Column(db.Integer, primary_key=True) numer_konta = db.Column(db.String(50), nullable=False) numer_telefonu_blik = db.Column(db.String(50), nullable=False) allowed_login_hosts = db.Column(db.Text, nullable=True) @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) def is_allowed_ip(remote_ip, allowed_hosts_str): # Jeśli istnieje plik awaryjny, zawsze zezwalamy na dostęp if os.path.exists("emergency_access.txt"): return True # Rozdzielamy wpisy – mogą być oddzielone przecinkami lub znakami nowej linii allowed_hosts = re.split(r'[\n,]+', allowed_hosts_str.strip()) allowed_ips = set() for host in allowed_hosts: host = host.strip() if not host: continue try: # Rozwiązywanie nazwy domeny do adresu IP. resolved_ip = socket.gethostbyname(host) allowed_ips.add(resolved_ip) except Exception: # Jeśli rozwiązywanie nazwy nie powiedzie się, pomijamy ten wpis. continue return remote_ip in allowed_ips # Dodaj filtr Markdown – pozwala na zagnieżdżanie linków i obrazków w opisie @app.template_filter('markdown') def markdown_filter(text): return Markup(md.markdown(text)) # TRASY PUBLICZNE @app.route('/') def index(): zbiorki = Zbiorka.query.filter_by(ukryta=False, zrealizowana=False).all() return render_template('index.html', zbiorki=zbiorki) @app.route('/zbiorki_zrealizowane') def zbiorki_zrealizowane(): zbiorki = Zbiorka.query.filter_by(zrealizowana=True).all() return render_template('index.html', zbiorki=zbiorki) @app.errorhandler(404) def page_not_found(e): return redirect(url_for('index')) @app.route('/zbiorka/') def zbiorka(zbiorka_id): zb = Zbiorka.query.get_or_404(zbiorka_id) # Jeżeli zbiórka jest ukryta i użytkownik nie jest administratorem, zwróć 404 if zb.ukryta and (not current_user.is_authenticated or not current_user.is_admin): abort(404) return render_template('zbiorka.html', zbiorka=zb) def get_real_ip(): # Sprawdź, czy żądanie pochodzi przez Cloudflare if "CF-Connecting-IP" in request.headers: return request.headers.get("CF-Connecting-IP") # Następnie sprawdź nagłówek X-Real-IP elif "X-Real-IP" in request.headers: return request.headers.get("X-Real-IP") # Jeśli jest nagłówek X-Forwarded-For, pobierz pierwszy adres na liście elif "X-Forwarded-For" in request.headers: forwarded_for = request.headers.get("X-Forwarded-For").split(",") return forwarded_for[0].strip() # W przeciwnym wypadku użyj standardowego remote_addr return request.remote_addr # TRASY LOGOWANIA I REJESTRACJI @app.route('/login', methods=['GET', 'POST']) def login(): # Pobierz ustawienia globalne, w tym dozwolone hosty settings = GlobalSettings.query.first() allowed_hosts_str = "" if settings and settings.allowed_login_hosts: allowed_hosts_str = settings.allowed_login_hosts # Sprawdzenie, czy adres IP klienta jest dozwolony client_ip = get_real_ip() if not is_allowed_ip(client_ip, allowed_hosts_str): flash('Dostęp do endpointu /login jest zablokowany dla Twojego adresu IP', 'danger') return redirect(url_for('index')) if request.method == 'POST': username = request.form['username'] password = request.form['password'] user = User.query.filter_by(username=username).first() if user and user.check_password(password): login_user(user) flash('Zalogowano pomyślnie', 'success') next_page = request.args.get('next') return redirect(next_page) if next_page else redirect(url_for('admin_dashboard')) else: flash('Nieprawidłowe dane logowania', 'danger') return render_template('login.html') @app.route('/logout') @login_required def logout(): logout_user() flash('Wylogowano', 'success') return redirect(url_for('login')) @app.route('/register', methods=['GET', 'POST']) def register(): if not app.config.get('ALLOW_REGISTRATION', False): flash('Rejestracja została wyłączona przez administratora', 'danger') return redirect(url_for('login')) if request.method == 'POST': username = request.form['username'] password = request.form['password'] if User.query.filter_by(username=username).first(): flash('Użytkownik już istnieje', 'danger') return redirect(url_for('register')) new_user = User(username=username) new_user.set_password(password) db.session.add(new_user) db.session.commit() flash('Konto utworzone, możesz się zalogować', 'success') return redirect(url_for('login')) return render_template('register.html') # PANEL ADMINISTRACYJNY @app.route('/admin') @login_required def admin_dashboard(): if not current_user.is_admin: flash('Brak uprawnień do panelu administracyjnego', 'danger') return redirect(url_for('index')) active_zbiorki = Zbiorka.query.filter_by(zrealizowana=False).all() completed_zbiorki = Zbiorka.query.filter_by(zrealizowana=True).all() return render_template('admin/dashboard.html', active_zbiorki=active_zbiorki, completed_zbiorki=completed_zbiorki) @app.route('/admin/zbiorka/dodaj', methods=['GET', 'POST']) @login_required def dodaj_zbiorka(): if not current_user.is_admin: flash('Brak uprawnień', 'danger') return redirect(url_for('index')) global_settings = GlobalSettings.query.first() # Pobieramy globalne ustawienia if request.method == 'POST': nazwa = request.form['nazwa'] opis = request.form['opis'] # Pozyskujemy numer konta i telefon z formularza (mogą być nadpisane ręcznie) numer_konta = request.form['numer_konta'] numer_telefonu_blik = request.form['numer_telefonu_blik'] cel = float(request.form['cel']) ukryj_kwote = 'ukryj_kwote' in request.form nowa_zbiorka = Zbiorka( nazwa=nazwa, opis=opis, numer_konta=numer_konta, numer_telefonu_blik=numer_telefonu_blik, cel=cel, ukryj_kwote=ukryj_kwote ) db.session.add(nowa_zbiorka) db.session.commit() flash('Zbiórka została dodana', 'success') return redirect(url_for('admin_dashboard')) return render_template('admin/add_zbiorka.html', global_settings=global_settings) @app.route('/admin/zbiorka/edytuj/', methods=['GET', 'POST']) @login_required def edytuj_zbiorka(zbiorka_id): if not current_user.is_admin: flash('Brak uprawnień', 'danger') return redirect(url_for('index')) zb = Zbiorka.query.get_or_404(zbiorka_id) global_settings = GlobalSettings.query.first() # Pobieramy globalne ustawienia if request.method == 'POST': zb.nazwa = request.form['nazwa'] zb.opis = request.form['opis'] zb.numer_konta = request.form['numer_konta'] zb.numer_telefonu_blik = request.form['numer_telefonu_blik'] try: zb.cel = float(request.form['cel']) except ValueError: flash('Podano nieprawidłową wartość dla celu zbiórki', 'danger') return render_template('admin/edit_zbiorka.html', zbiorka=zb, global_settings=global_settings) zb.ukryj_kwote = 'ukryj_kwote' in request.form db.session.commit() flash('Zbiórka została zaktualizowana', 'success') return redirect(url_for('admin_dashboard')) return render_template('admin/edit_zbiorka.html', zbiorka=zb, global_settings=global_settings) # TRASA DODAWANIA WPŁATY Z OPISEM # TRASA DODAWANIA WPŁATY W PANELU ADMINA @app.route('/admin/zbiorka//wplata/dodaj', methods=['GET', 'POST']) @login_required def admin_dodaj_wplate(zbiorka_id): if not current_user.is_admin: flash('Brak uprawnień', 'danger') return redirect(url_for('index')) zb = Zbiorka.query.get_or_404(zbiorka_id) if request.method == 'POST': kwota = float(request.form['kwota']) opis = request.form.get('opis', '') nowa_wplata = Wplata(zbiorka_id=zb.id, kwota=kwota, opis=opis) zb.stan += kwota # Aktualizacja stanu zbiórki db.session.add(nowa_wplata) db.session.commit() flash('Wpłata została dodana', 'success') return redirect(url_for('admin_dashboard')) return render_template('admin/add_wplata.html', zbiorka=zb) @app.route('/admin/zbiorka/usun/', methods=['POST']) @login_required def usun_zbiorka(zbiorka_id): if not current_user.is_admin: flash('Brak uprawnień', 'danger') return redirect(url_for('index')) zb = Zbiorka.query.get_or_404(zbiorka_id) db.session.delete(zb) db.session.commit() flash('Zbiórka została usunięta', 'success') return redirect(url_for('admin_dashboard')) @app.route('/admin/zbiorka/edytuj_stan/', methods=['GET', 'POST']) @login_required def edytuj_stan(zbiorka_id): if not current_user.is_admin: flash('Brak uprawnień', 'danger') return redirect(url_for('index')) zb = Zbiorka.query.get_or_404(zbiorka_id) if request.method == 'POST': try: nowy_stan = float(request.form['stan']) except ValueError: flash('Nieprawidłowa wartość kwoty', 'danger') return redirect(url_for('edytuj_stan', zbiorka_id=zbiorka_id)) zb.stan = nowy_stan db.session.commit() flash('Stan zbiórki został zaktualizowany', 'success') return redirect(url_for('admin_dashboard')) return render_template('admin/edytuj_stan.html', zbiorka=zb) @app.route('/admin/zbiorka/toggle_visibility/', methods=['POST']) @login_required def toggle_visibility(zbiorka_id): if not current_user.is_admin: flash('Brak uprawnień', 'danger') return redirect(url_for('index')) zb = Zbiorka.query.get_or_404(zbiorka_id) zb.ukryta = not zb.ukryta db.session.commit() flash('Zbiórka została ' + ('ukryta' if zb.ukryta else 'przywrócona'), 'success') return redirect(url_for('admin_dashboard')) def create_admin_account(): admin = User.query.filter_by(is_admin=True).first() if not admin: main_admin = User(username=app.config['MAIN_ADMIN_USERNAME'], is_admin=True) main_admin.set_password(app.config['MAIN_ADMIN_PASSWORD']) db.session.add(main_admin) db.session.commit() @app.after_request def add_security_headers(response): if app.config.get("BLOCK_BOTS", False): cache_control = app.config.get("CACHE_CONTROL_HEADER") if cache_control: response.headers["Cache-Control"] = cache_control # Jeśli Cache-Control jest ustawiony, usuwamy Pragma response.headers.pop("Pragma", None) else: response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0" response.headers["Pragma"] = app.config.get("PRAGMA_HEADER", "no-cache") response.headers["X-Robots-Tag"] = app.config.get("ROBOTS_TAG", "noindex, nofollow, nosnippet, noarchive") return response @app.route('/admin/settings', methods=['GET', 'POST']) @login_required def admin_settings(): if not current_user.is_admin: flash('Brak uprawnień do panelu administracyjnego', 'danger') return redirect(url_for('index')) settings = GlobalSettings.query.first() if request.method == 'POST': numer_konta = request.form.get('numer_konta') numer_telefonu_blik = request.form.get('numer_telefonu_blik') allowed_login_hosts = request.form.get('allowed_login_hosts') if settings is None: settings = GlobalSettings( numer_konta=numer_konta, numer_telefonu_blik=numer_telefonu_blik, allowed_login_hosts=allowed_login_hosts ) db.session.add(settings) else: settings.numer_konta = numer_konta settings.numer_telefonu_blik = numer_telefonu_blik settings.allowed_login_hosts = allowed_login_hosts db.session.commit() flash('Ustawienia globalne zostały zaktualizowane', 'success') return redirect(url_for('admin_dashboard')) return render_template('admin/settings.html', settings=settings) @app.route('/admin/zbiorka/oznacz/', methods=['POST']) @login_required def oznacz_zbiorka(zbiorka_id): if not current_user.is_admin: flash('Brak uprawnień do wykonania tej operacji', 'danger') return redirect(url_for('index')) zb = Zbiorka.query.get_or_404(zbiorka_id) zb.zrealizowana = True db.session.commit() flash('Zbiórka została oznaczona jako zrealizowana', 'success') return redirect(url_for('admin_dashboard')) @app.route('/robots.txt') def robots(): if app.config.get("BLOCK_BOTS", False): # Instrukcje dla robotów – blokujemy indeksowanie całej witryny robots_txt = "User-agent: *\nDisallow: /" else: # Jeśli blokowanie botów wyłączone, można zwrócić pusty plik lub inne ustawienia robots_txt = "User-agent: *\nAllow: /" return robots_txt, 200, {'Content-Type': 'text/plain'} if __name__ == '__main__': with app.app_context(): db.create_all() # Tworzenie konta głównego admina, jeśli nie istnieje if not User.query.filter_by(is_admin=True).first(): main_admin = User(username=app.config['MAIN_ADMIN_USERNAME'], is_admin=True) main_admin.set_password(app.config['MAIN_ADMIN_PASSWORD']) db.session.add(main_admin) db.session.commit() app.run(debug=True)