#!/usr/bin/env python3 """ Prawicowy Dashboard — Flask backend Port 1234 | Static files + RSS proxy + Admin panel (/admin/) + Auth (/login) """ import os, json, time, threading, socket, subprocess import xml.etree.ElementTree as ET from datetime import datetime from flask import (Flask, request, jsonify, redirect, url_for, render_template_string, send_from_directory, abort, Response, flash) from flask_sqlalchemy import SQLAlchemy from flask_login import (LoginManager, UserMixin, login_user, logout_user, login_required, current_user) from flask_admin import Admin, AdminIndexView, expose from flask_admin.contrib.sqla import ModelView from werkzeug.security import generate_password_hash, check_password_hash ROOT = os.path.dirname(os.path.abspath(__file__)) CACHE: dict = {} CACHE_TTL = 600 FETCH_TIMEOUT = 12 app = Flask(__name__, template_folder=os.path.join(ROOT, 'templates')) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'pd-secret-2024-change-me') app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(ROOT, 'dashboard.db') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) login_manager = LoginManager(app) login_manager.login_view = 'auth_login' login_manager.login_message = 'Wymagane logowanie administratora.' # ══════════════════════════════════════════════════════════ # MODELS # ══════════════════════════════════════════════════════════ class User(db.Model, UserMixin): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) email = db.Column(db.String(200)) password_hash = db.Column(db.String(256), nullable=False, default='') is_admin = db.Column(db.Boolean, default=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) last_login = db.Column(db.DateTime) def set_password(self, pw): self.password_hash = generate_password_hash(pw) def check_password(self, pw): return check_password_hash(self.password_hash, pw) def __str__(self): return self.username class RssSource(db.Model): __tablename__ = 'rss_sources' id = db.Column(db.Integer, primary_key=True) group_id = db.Column(db.String(50), nullable=False, index=True) name = db.Column(db.String(100), nullable=False) url = db.Column(db.String(1000), nullable=False) icon = db.Column(db.String(20), default='📰') website = db.Column(db.String(500)) css_class = db.Column(db.String(50), default='src-republika') dot_id = db.Column(db.String(50)) category = db.Column(db.String(20), default='main') # main | opposition active = db.Column(db.Boolean, default=True) order = db.Column(db.Integer, default=0) def __str__(self): return f'{self.name} ({self.group_id})' class Politician(db.Model): __tablename__ = 'politicians' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) role = db.Column(db.String(200)) img_path = db.Column(db.String(500)) quote = db.Column(db.Text) description = db.Column(db.Text) party = db.Column(db.String(50)) ticker_visible = db.Column(db.Boolean, default=True) sidebar_visible = db.Column(db.Boolean, default=False) popup_key = db.Column(db.String(50)) order = db.Column(db.Integer, default=0) active = db.Column(db.Boolean, default=True) def __str__(self): return self.name class PartyMember(db.Model): __tablename__ = 'party_members' id = db.Column(db.Integer, primary_key=True) party_slug = db.Column(db.String(50), nullable=False, index=True) section = db.Column(db.String(200), default='') name = db.Column(db.String(100), nullable=False) role = db.Column(db.String(200)) img_url = db.Column(db.String(1000)) initials = db.Column(db.String(10)) bg_color = db.Column(db.String(20)) description = db.Column(db.Text) desc_type = db.Column(db.String(10), default='merit') # merit | fail married = db.Column(db.Boolean, default=False) children = db.Column(db.Integer, default=0) religious = db.Column(db.Boolean, default=False) is_leader = db.Column(db.Boolean, default=False) order = db.Column(db.Integer, default=0) active = db.Column(db.Boolean, default=True) def __str__(self): return f'{self.name} ({self.party_slug})' class SiteSetting(db.Model): __tablename__ = 'site_settings' id = db.Column(db.Integer, primary_key=True) key = db.Column(db.String(100), unique=True, nullable=False) value = db.Column(db.Text, default='') description = db.Column(db.String(500)) def __str__(self): return self.key @login_manager.user_loader def load_user(uid): return db.session.get(User, int(uid)) # ══════════════════════════════════════════════════════════ # ADMIN VIEWS # ══════════════════════════════════════════════════════════ class _Auth: def is_accessible(self): return current_user.is_authenticated and current_user.is_admin def inaccessible_callback(self, name, **kwargs): return redirect(url_for('auth_login', next=request.url)) class UserAdmin(_Auth, ModelView): column_labels = dict(username='Użytkownik', email='Email', is_admin='Admin', created_at='Utworzony', last_login='Ostatnie logowanie') column_list = ['username', 'email', 'is_admin', 'created_at', 'last_login'] column_searchable_list = ['username', 'email'] column_filters = ['is_admin'] form_excluded_columns = ['password_hash', 'created_at', 'last_login'] form_widget_args = {'email': {'style': 'width:100%'}} def on_model_change(self, form, model, is_created): pw = (form.new_password.data or '').strip() if hasattr(form, 'new_password') else '' if pw: model.set_password(pw) elif is_created and not model.password_hash: model.set_password('admin123') def scaffold_form(self): from wtforms import PasswordField form_class = super().scaffold_form() form_class.new_password = PasswordField('Nowe hasło (puste = bez zmiany)') return form_class class RssSourceAdmin(_Auth, ModelView): column_labels = dict(group_id='Grupa', name='Nazwa', url='URL feed', icon='Ikona', website='Strona www', css_class='Klasa CSS', dot_id='ID wskaźnika', category='Kategoria', active='Aktywne', order='Kolejność') column_list = ['name', 'group_id', 'url', 'icon', 'website', 'category', 'active', 'order'] column_searchable_list = ['name', 'url', 'group_id'] column_filters = ['group_id', 'active', 'category'] column_editable_list = ['active', 'order', 'name'] form_widget_args = {'url': {'style': 'width:100%'}, 'website': {'style': 'width:100%'}} class PoliticianAdmin(_Auth, ModelView): column_labels = dict(name='Imię i nazwisko', role='Stanowisko', img_path='Zdjęcie', quote='Cytat (ticker)', description='Opis', party='Partia', ticker_visible='Pasek górny', sidebar_visible='Sidebar', popup_key='Klucz popup', order='Kolejność', active='Aktywny') column_list = ['name', 'role', 'party', 'ticker_visible', 'sidebar_visible', 'order', 'active'] column_searchable_list = ['name', 'role', 'party'] column_filters = ['party', 'ticker_visible', 'sidebar_visible', 'active'] column_editable_list = ['ticker_visible', 'sidebar_visible', 'order', 'active'] form_widget_args = {'quote': {'rows': 3}, 'description': {'rows': 5}, 'img_path': {'style': 'width:100%'}} class PartyMemberAdmin(_Auth, ModelView): column_labels = dict(party_slug='Partia', section='Sekcja', name='Imię i nazwisko', role='Funkcja', img_url='URL zdjęcia', description='Opis', desc_type='Typ (merit/fail)', is_leader='Lider', order='Kolejność', active='Aktywny') column_list = ['name', 'party_slug', 'section', 'role', 'desc_type', 'is_leader', 'order', 'active'] column_searchable_list = ['name', 'role', 'party_slug', 'section'] column_filters = ['party_slug', 'desc_type', 'is_leader', 'active'] column_editable_list = ['order', 'active', 'is_leader'] form_widget_args = {'description': {'rows': 6}, 'img_url': {'style': 'width:100%'}} class SettingAdmin(_Auth, ModelView): column_labels = dict(key='Klucz', value='Wartość', description='Opis') column_list = ['key', 'value', 'description'] column_searchable_list = ['key', 'description'] column_editable_list = ['value'] form_widget_args = {'value': {'rows': 3, 'style': 'width:100%'}, 'description': {'style': 'width:100%'}} class HomeAdmin(_Auth, AdminIndexView): @expose('/') def index(self): if not current_user.is_authenticated: return redirect(url_for('auth_login')) stats = dict( users = User.query.count(), sources = RssSource.query.filter_by(active=True).count(), politicians = Politician.query.filter_by(active=True).count(), party_members = PartyMember.query.filter_by(active=True).count(), ) return self.render('admin/index.html', stats=stats) admin_panel = Admin( app, name='🦅 Prawicowy Dashboard — Admin', index_view=HomeAdmin(), ) admin_panel.add_view(RssSourceAdmin( RssSource, db, name='📡 Źródła RSS', category='📰 Treści', endpoint='rss_source')) admin_panel.add_view(PoliticianAdmin(Politician, db, name='🏛 Politycy (ticker)', category='📰 Treści', endpoint='politician')) admin_panel.add_view(PartyMemberAdmin(PartyMember, db, name='⚡ Posłowie partii', category='📰 Treści', endpoint='party_member')) admin_panel.add_view(SettingAdmin( SiteSetting, db, name='⚙️ Ustawienia', category='⚙️ System', endpoint='site_setting')) admin_panel.add_view(UserAdmin( User, db, name='👤 Użytkownicy', category='⚙️ System', endpoint='user')) # ══════════════════════════════════════════════════════════ # AUTH # ══════════════════════════════════════════════════════════ LOGIN_HTML = '''