commit
This commit is contained in:
commit
2a065aba3b
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
# Ignoruj pliki związane z Pythonem
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Pliki virtual environment (np. venv)
|
||||
venv/
|
||||
ENV/
|
||||
.venv/
|
||||
env/
|
||||
|
||||
# Konfiguracje prywatne / klucze
|
||||
.env
|
||||
*.env
|
||||
|
||||
# Logi i pliki tymczasowe
|
||||
*.log
|
||||
*.tmp
|
||||
|
||||
# Ignorowanie bazy danych
|
||||
ssl_monitor.db
|
||||
|
||||
# Jeżeli używasz instance folder w Flask
|
||||
instance/
|
||||
|
||||
# Mac OS pliki systemowe
|
||||
.DS_Store
|
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
FROM python:3.13-slim
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN apt-get update && apt-get install -y build-essential && \
|
||||
pip install --upgrade pip && pip install -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
RUN mkdir -p /app/instance
|
||||
|
||||
EXPOSE 5583
|
||||
|
||||
CMD ["python", "run_waitress.py"]
|
787
app.py
Normal file
787
app.py
Normal file
@ -0,0 +1,787 @@
|
||||
import os
|
||||
import ssl
|
||||
import socket
|
||||
from datetime import datetime, timedelta
|
||||
import requests
|
||||
from flask import Flask, render_template, request, redirect, url_for, session, jsonify, flash, current_app
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from flask_bcrypt import Bcrypt
|
||||
import smtplib
|
||||
import imaplib
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
# Globalna referencja do aplikacji – przydatna dla scheduler'a
|
||||
app_instance = None
|
||||
|
||||
BASEDIR = os.path.abspath(os.path.dirname(__file__))
|
||||
DATABASE_FILE = os.path.join(BASEDIR, 'ssl_monitor.db')
|
||||
|
||||
# Globalna flaga, by operację utworzenia domyślnego użytkownika wykonać tylko raz
|
||||
default_user_created = False
|
||||
|
||||
db = SQLAlchemy()
|
||||
bcrypt = Bcrypt()
|
||||
|
||||
def create_app():
|
||||
global app_instance
|
||||
app = Flask(__name__)
|
||||
# W produkcji ustaw SECRET_KEY przez zmienną środowiskową!
|
||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'moj-klucz-session')
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{DATABASE_FILE}'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
# Konfiguracja ciasteczek sesyjnych (zalecane przy HTTPS)
|
||||
app.config['SESSION_COOKIE_SECURE'] = False
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||
|
||||
db.init_app(app)
|
||||
bcrypt.init_app(app)
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
app_instance = app
|
||||
|
||||
init_scheduler(app)
|
||||
register_routes(app)
|
||||
|
||||
return app
|
||||
|
||||
###############################################################################
|
||||
# MODELE
|
||||
###############################################################################
|
||||
|
||||
class User(db.Model):
|
||||
"""
|
||||
Model użytkownika – hasło przechowywane jako hash przy użyciu bcrypt.
|
||||
"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(50), unique=True, nullable=False)
|
||||
password = db.Column(db.String(200), nullable=False)
|
||||
|
||||
class MonitoredService(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
host = db.Column(db.String(256), nullable=False)
|
||||
port = db.Column(db.Integer, default=443)
|
||||
protocol = db.Column(db.String(50), default='https')
|
||||
region = db.Column(db.String(100), nullable=True)
|
||||
certificate_type = db.Column(db.String(50), nullable=True)
|
||||
last_check = db.Column(db.DateTime)
|
||||
expiry_date = db.Column(db.DateTime)
|
||||
status = db.Column(db.String(20))
|
||||
|
||||
class Settings(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
check_interval_minutes = db.Column(db.Integer, default=60)
|
||||
pushover_enabled = db.Column(db.Boolean, default=False)
|
||||
pushover_token = db.Column(db.String(200), nullable=True)
|
||||
pushover_userkey = db.Column(db.String(200), nullable=True)
|
||||
alert_threshold_30 = db.Column(db.Integer, default=30)
|
||||
alert_threshold_14 = db.Column(db.Integer, default=14)
|
||||
alert_threshold_7 = db.Column(db.Integer, default=7)
|
||||
alert_repeat = db.Column(db.Boolean, default=False)
|
||||
logs_retention_days = db.Column(db.Integer, default=30)
|
||||
|
||||
class History(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
service_id = db.Column(db.Integer, db.ForeignKey('monitored_service.id'), nullable=False)
|
||||
# Dodaj relację, by mieć dostęp do obiektu MonitoredService
|
||||
service = db.relationship("MonitoredService", backref=db.backref("history_entries", lazy=True))
|
||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
status = db.Column(db.String(20))
|
||||
expiry_date = db.Column(db.DateTime, nullable=True)
|
||||
message = db.Column(db.String(500))
|
||||
|
||||
class AuditLog(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||
operation = db.Column(db.String(50))
|
||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
details = db.Column(db.Text)
|
||||
|
||||
class UserPreferences(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), unique=True)
|
||||
default_grouping = db.Column(db.String(50), default='protocol') # lub "none", "host", "region", itp.
|
||||
# Możesz dodać więcej pól, np. zapisanych filtrów w formie JSON:
|
||||
filters = db.Column(db.Text, default='{}')
|
||||
|
||||
###############################################################################
|
||||
# FUNKCJE POMOCNICZE – SPRAWDZANIE CERTYFIKATU
|
||||
###############################################################################
|
||||
|
||||
def check_https_cert(host, port=443):
|
||||
"""
|
||||
Pobiera datę wygaśnięcia certyfikatu HTTPS.
|
||||
"""
|
||||
context = ssl.create_default_context()
|
||||
with socket.create_connection((host, port), timeout=10) as sock:
|
||||
with context.wrap_socket(sock, server_hostname=host) as ssock:
|
||||
cert = ssock.getpeercert()
|
||||
not_after = cert["notAfter"]
|
||||
expiry_date = datetime.strptime(not_after, "%b %d %H:%M:%S %Y %Z")
|
||||
return expiry_date
|
||||
|
||||
def update_certs():
|
||||
with app_instance.app_context():
|
||||
services = MonitoredService.query.all()
|
||||
settings = Settings.query.first()
|
||||
for service in services:
|
||||
try:
|
||||
proto = service.protocol.lower().strip().replace(" ", "_")
|
||||
if proto == 'https':
|
||||
exp_date = check_https_cert(service.host, service.port)
|
||||
elif proto == 'smtp_starttls':
|
||||
exp_date = check_smtp_starttls_cert(service.host, service.port)
|
||||
elif proto == 'smtp_ssl':
|
||||
exp_date = check_smtp_ssl_cert(service.host, service.port)
|
||||
elif proto == 'imap_starttls':
|
||||
exp_date = check_imap_starttls_cert(service.host, service.port)
|
||||
elif proto == 'imap_ssl':
|
||||
exp_date = check_imap_ssl_cert(service.host, service.port)
|
||||
else:
|
||||
service.status = 'ProtocolNotImplemented'
|
||||
db.session.commit()
|
||||
continue
|
||||
|
||||
service.expiry_date = exp_date
|
||||
service.last_check = datetime.now()
|
||||
if exp_date < datetime.now():
|
||||
new_status = 'Expired'
|
||||
elif exp_date < datetime.now() + timedelta(days=7):
|
||||
new_status = 'ExpiringSoon'
|
||||
else:
|
||||
new_status = 'OK'
|
||||
old_status = service.status
|
||||
service.status = new_status
|
||||
db.session.commit()
|
||||
# Zapis do historii
|
||||
history = History(service_id=service.id, status=new_status, expiry_date=exp_date,
|
||||
message=f"Sprawdzenie. Poprzedni status: {old_status}")
|
||||
db.session.add(history)
|
||||
db.session.commit()
|
||||
|
||||
# Sprawdź progi alertów (dla certyfikatów, które nie wygasły)
|
||||
if new_status in ['ExpiringSoon', 'OK']:
|
||||
days_left = (exp_date - datetime.now()).days
|
||||
alert_sent = False # Możesz rozwinąć logikę, by zapamiętać wysłane alerty
|
||||
if days_left <= settings.alert_threshold_7:
|
||||
# Wysyłamy alert o 7-dniowym progu
|
||||
send_pushover_message("user_key_placeholder", "api_token_placeholder",
|
||||
f"Certyfikat dla {service.host} wygasa za {days_left} dni (7-dniowy próg).",
|
||||
"Alert SSL Monitor")
|
||||
alert_sent = True
|
||||
elif days_left <= settings.alert_threshold_14:
|
||||
send_pushover_message("user_key_placeholder", "api_token_placeholder",
|
||||
f"Certyfikat dla {service.host} wygasa za {days_left} dni (14-dniowy próg).",
|
||||
"Alert SSL Monitor")
|
||||
alert_sent = True
|
||||
elif days_left <= settings.alert_threshold_30:
|
||||
send_pushover_message("user_key_placeholder", "api_token_placeholder",
|
||||
f"Certyfikat dla {service.host} wygasa za {days_left} dni (30-dniowy próg).",
|
||||
"Alert SSL Monitor")
|
||||
alert_sent = True
|
||||
# Jeśli alert_repeat jest False, możesz zapisać, że alert dla tego progu został wysłany,
|
||||
# aby nie wysyłać go ponownie.
|
||||
except Exception as e:
|
||||
service.status = 'Error'
|
||||
db.session.commit()
|
||||
history = History(service_id=service.id, status="Error", expiry_date=None, message=str(e))
|
||||
db.session.add(history)
|
||||
db.session.commit()
|
||||
|
||||
def check_smtp_starttls_cert(host, port=587):
|
||||
context = ssl.create_default_context()
|
||||
server = smtplib.SMTP(host, port, timeout=10)
|
||||
server.ehlo()
|
||||
server.starttls(context=context)
|
||||
cert = server.sock.getpeercert()
|
||||
server.quit()
|
||||
not_after = cert["notAfter"]
|
||||
expiry_date = datetime.strptime(not_after, "%b %d %H:%M:%S %Y %Z")
|
||||
return expiry_date
|
||||
|
||||
def check_smtp_ssl_cert(host, port=465):
|
||||
context = ssl.create_default_context()
|
||||
server = smtplib.SMTP_SSL(host, port, timeout=10, context=context)
|
||||
cert = server.sock.getpeercert()
|
||||
server.quit()
|
||||
not_after = cert["notAfter"]
|
||||
expiry_date = datetime.strptime(not_after, "%b %d %H:%M:%S %Y %Z")
|
||||
return expiry_date
|
||||
|
||||
def check_imap_starttls_cert(host, port=143):
|
||||
context = ssl.create_default_context()
|
||||
imap = imaplib.IMAP4(host, port)
|
||||
imap.starttls(ssl_context=context)
|
||||
cert = imap.sock.getpeercert()
|
||||
imap.logout()
|
||||
not_after = cert["notAfter"]
|
||||
expiry_date = datetime.strptime(not_after, "%b %d %H:%M:%S %Y %Z")
|
||||
return expiry_date
|
||||
|
||||
def check_imap_ssl_cert(host, port=993):
|
||||
context = ssl.create_default_context()
|
||||
imap = imaplib.IMAP4_SSL(host, port, ssl_context=context)
|
||||
cert = imap.sock.getpeercert()
|
||||
imap.logout()
|
||||
not_after = cert["notAfter"]
|
||||
expiry_date = datetime.strptime(not_after, "%b %d %H:%M:%S %Y %Z")
|
||||
return expiry_date
|
||||
|
||||
def get_service_response(service):
|
||||
"""
|
||||
Nawiązuje połączenie z serwerem dla danej usługi i zwraca pierwsze 1024 bajty odpowiedzi.
|
||||
Dla HTTPS wysyła żądanie HEAD, dla pozostałych protokołów pobiera banner.
|
||||
"""
|
||||
host = service.host
|
||||
port = service.port
|
||||
proto = service.protocol.lower().strip().replace(" ", "_")
|
||||
timeout = 10
|
||||
if proto == "https":
|
||||
context = ssl.create_default_context()
|
||||
with socket.create_connection((host, port), timeout=timeout) as sock:
|
||||
with context.wrap_socket(sock, server_hostname=host) as ssock:
|
||||
# Wysyłamy żądanie HEAD, aby uzyskać nagłówki
|
||||
request = f"HEAD / HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n"
|
||||
ssock.send(request.encode())
|
||||
data = ssock.recv(1024)
|
||||
return data.decode(errors='replace')
|
||||
elif proto in ["smtp_starttls", "smtp_ssl"]:
|
||||
if proto == "smtp_ssl":
|
||||
context = ssl.create_default_context()
|
||||
with socket.create_connection((host, port), timeout=timeout) as sock:
|
||||
with context.wrap_socket(sock, server_hostname=host) as ssock:
|
||||
data = ssock.recv(1024)
|
||||
return data.decode(errors='replace')
|
||||
else:
|
||||
# smtp_starttls: banner jest wysyłany w wersji niezaszyfrowanej
|
||||
with socket.create_connection((host, port), timeout=timeout) as sock:
|
||||
data = sock.recv(1024)
|
||||
return data.decode(errors='replace')
|
||||
elif proto in ["imap_starttls", "imap_ssl"]:
|
||||
if proto == "imap_ssl":
|
||||
context = ssl.create_default_context()
|
||||
with socket.create_connection((host, port), timeout=timeout) as sock:
|
||||
with context.wrap_socket(sock, server_hostname=host) as ssock:
|
||||
data = ssock.recv(1024)
|
||||
return data.decode(errors='replace')
|
||||
else:
|
||||
# imap_starttls: banner wysyłany jest bez szyfrowania
|
||||
with socket.create_connection((host, port), timeout=timeout) as sock:
|
||||
data = sock.recv(1024)
|
||||
return data.decode(errors='replace')
|
||||
else:
|
||||
raise Exception("Protocol not supported for response retrieval")
|
||||
|
||||
|
||||
def send_pushover_message(user_key, api_token, message, title="SSL Monitor Alert"):
|
||||
"""
|
||||
Wysyła powiadomienie przez Pushover przy użyciu requests.
|
||||
"""
|
||||
url = "https://api.pushover.net/1/messages.json"
|
||||
data = {
|
||||
"token": api_token,
|
||||
"user": user_key,
|
||||
"message": message,
|
||||
"title": title
|
||||
}
|
||||
response = requests.post(url, data=data)
|
||||
return response.json()
|
||||
|
||||
def get_cert_details(service):
|
||||
"""
|
||||
Pobiera szczegółowe informacje o certyfikacie (HTTPS).
|
||||
"""
|
||||
host = service.host
|
||||
port = service.port
|
||||
context = ssl.create_default_context()
|
||||
with socket.create_connection((host, port), timeout=10) as sock:
|
||||
with context.wrap_socket(sock, server_hostname=host) as ssock:
|
||||
cert = ssock.getpeercert()
|
||||
return cert # Zwracamy cały słownik certyfikatu
|
||||
|
||||
def get_cert_details(svc):
|
||||
"""
|
||||
Pobiera szczegółowe informacje o certyfikacie dla danej usługi HTTPS.
|
||||
Zwraca certyfikat w formie słownika.
|
||||
"""
|
||||
host = svc.host
|
||||
port = svc.port
|
||||
context = ssl.create_default_context()
|
||||
with socket.create_connection((host, port), timeout=10) as sock:
|
||||
with context.wrap_socket(sock, server_hostname=host) as ssock:
|
||||
cert = ssock.getpeercert()
|
||||
return cert
|
||||
|
||||
def format_cert_details_as_table(cert_details):
|
||||
def flatten_pairs(value):
|
||||
"""Rekurencyjnie przetwarza zagnieżdżone struktury i zwraca listę napisów 'klucz: wartość'."""
|
||||
result = []
|
||||
if isinstance(value, (list, tuple)):
|
||||
for item in value:
|
||||
# Jeżeli mamy krotkę o długości 2, a pierwszy element to napis – traktuj ją jako parę
|
||||
if isinstance(item, (list, tuple)) and len(item) == 2 and isinstance(item[0], str):
|
||||
result.append(f"{item[0]}: {item[1]}")
|
||||
else:
|
||||
result.extend(flatten_pairs(item))
|
||||
else:
|
||||
result.append(str(value))
|
||||
return result
|
||||
|
||||
table = '<table class="table table-sm table-dark table-bordered">'
|
||||
table += '<thead><tr><th>Klucz</th><th>Wartość</th></tr></thead><tbody>'
|
||||
for key, value in cert_details.items():
|
||||
if isinstance(value, (list, tuple)):
|
||||
flat = flatten_pairs(value)
|
||||
formatted_value = "<br>".join(flat) if flat else str(value)
|
||||
else:
|
||||
formatted_value = str(value)
|
||||
table += f'<tr><th>{key}</th><td>{formatted_value}</td></tr>'
|
||||
table += '</tbody></table>'
|
||||
return table
|
||||
|
||||
|
||||
def get_cert_chain_details(host, port=443):
|
||||
context = ssl.create_default_context()
|
||||
with socket.create_connection((host, port), timeout=10) as sock:
|
||||
with context.wrap_socket(sock, server_hostname=host) as ssock:
|
||||
der_cert = ssock.getpeercert(binary_form=True)
|
||||
cert = x509.load_der_x509_certificate(der_cert, default_backend())
|
||||
# Możesz wyekstrahować informacje o certyfikacie:
|
||||
details = {
|
||||
"subject": cert.subject.rfc4514_string(),
|
||||
"issuer": cert.issuer.rfc4514_string(),
|
||||
"not_before": cert.not_valid_before.isoformat(),
|
||||
"not_after": cert.not_valid_after.isoformat(),
|
||||
"serial_number": str(cert.serial_number),
|
||||
"signature_algorithm": cert.signature_hash_algorithm.name if cert.signature_hash_algorithm else "Unknown",
|
||||
# Dla SAN: cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
||||
}
|
||||
return details
|
||||
|
||||
def get_cert_chain_html(host, port=443):
|
||||
context = ssl.create_default_context()
|
||||
with socket.create_connection((host, port), timeout=10) as sock:
|
||||
with context.wrap_socket(sock, server_hostname=host) as ssock:
|
||||
der_cert = ssock.getpeercert(binary_form=True)
|
||||
cert = x509.load_der_x509_certificate(der_cert, default_backend())
|
||||
|
||||
# Pobranie Subject Alternative Names (SAN) jeśli są dostępne
|
||||
try:
|
||||
san_extension = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
||||
san = ", ".join(san_extension.value.get_values_for_type(x509.DNSName))
|
||||
except Exception:
|
||||
san = "N/A"
|
||||
|
||||
# Można dodać inne rozszerzenia, np. Key Usage, Extended Key Usage, itd.
|
||||
try:
|
||||
key_usage = cert.extensions.get_extension_for_class(x509.KeyUsage).value
|
||||
key_usage_str = ", ".join([
|
||||
"digitalSignature" if key_usage.digital_signature else "",
|
||||
"contentCommitment" if key_usage.content_commitment else "",
|
||||
"keyEncipherment" if key_usage.key_encipherment else "",
|
||||
"dataEncipherment" if key_usage.data_encipherment else "",
|
||||
"keyAgreement" if key_usage.key_agreement else "",
|
||||
"keyCertSign" if key_usage.key_cert_sign else "",
|
||||
"crlSign" if key_usage.crl_sign else "",
|
||||
"encipherOnly" if key_usage.encipher_only else "",
|
||||
"decipherOnly" if key_usage.decipher_only else ""
|
||||
]).strip(", ")
|
||||
except Exception:
|
||||
key_usage_str = "N/A"
|
||||
|
||||
details_html = f"""
|
||||
<h5 class="mb-3">Szczegółowe informacje o certyfikacie</h5>
|
||||
<table class="table table-sm table-dark">
|
||||
<tr>
|
||||
<th>Subject</th>
|
||||
<td>{cert.subject.rfc4514_string()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Issuer</th>
|
||||
<td>{cert.issuer.rfc4514_string()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Not Before</th>
|
||||
<td>{cert.not_valid_before.strftime('%Y-%m-%d %H:%M:%S')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Not After</th>
|
||||
<td>{cert.not_valid_after.strftime('%Y-%m-%d %H:%M:%S')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Serial Number</th>
|
||||
<td>{cert.serial_number}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Signature Algorithm</th>
|
||||
<td>{cert.signature_hash_algorithm.name if cert.signature_hash_algorithm else 'Unknown'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Subject Alternative Names</th>
|
||||
<td>{san}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Key Usage</th>
|
||||
<td>{key_usage_str}</td>
|
||||
</tr>
|
||||
</table>
|
||||
"""
|
||||
return details_html
|
||||
|
||||
|
||||
###############################################################################
|
||||
# KONFIGURACJA SCHEDULERA
|
||||
###############################################################################
|
||||
|
||||
def init_scheduler(flask_app):
|
||||
global scheduler
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
with flask_app.app_context():
|
||||
settings = Settings.query.first()
|
||||
if not settings:
|
||||
settings = Settings(check_interval_minutes=60)
|
||||
db.session.add(settings)
|
||||
db.session.commit()
|
||||
interval = settings.check_interval_minutes
|
||||
|
||||
scheduler.add_job(func=update_certs, trigger="interval", minutes=interval)
|
||||
scheduler.start()
|
||||
|
||||
###############################################################################
|
||||
# TRASY I WIDOKI
|
||||
###############################################################################
|
||||
|
||||
def register_routes(app):
|
||||
# @app.before_request
|
||||
# def ensure_default_user():
|
||||
# """
|
||||
# Przy pierwszym żądaniu tworzymy domyślnego użytkownika 'admin' z zahashowanym hasłem.
|
||||
# """
|
||||
# global default_user_created
|
||||
# if not default_user_created:
|
||||
# if not User.query.filter_by(username='admin').first():
|
||||
# hashed_pw = bcrypt.generate_password_hash("admin").decode('utf-8')
|
||||
# user = User(username='admin', password=hashed_pw)
|
||||
# db.session.add(user)
|
||||
# db.session.commit()
|
||||
# default_user_created = True
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
if 'logged_in' in session and session['logged_in']:
|
||||
return redirect(url_for('dashboard'))
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
password = request.form.get('password')
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user and bcrypt.check_password_hash(user.password, password):
|
||||
session['logged_in'] = True
|
||||
session['username'] = user.username
|
||||
session['user_id'] = user.id # << Dodaj tę linię
|
||||
flash("Zalogowano poprawnie.", "success")
|
||||
# Log operacji logowania
|
||||
log = AuditLog(user_id=user.id, operation="login", details="Użytkownik zalogowany.")
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
return redirect(url_for('dashboard'))
|
||||
else:
|
||||
flash("Błędne dane logowania.", "danger")
|
||||
return redirect(url_for('login'))
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
flash("Wylogowano.", "success")
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@app.route('/dashboard')
|
||||
def dashboard():
|
||||
if 'logged_in' not in session or not session['logged_in']:
|
||||
flash("Musisz się zalogować.", "warning")
|
||||
return redirect(url_for('login'))
|
||||
|
||||
services = MonitoredService.query.all()
|
||||
total = len(services)
|
||||
expired = sum(1 for s in services if s.status == 'Expired')
|
||||
exp_soon = sum(1 for s in services if s.status == 'ExpiringSoon')
|
||||
ok = sum(1 for s in services if s.status == 'OK')
|
||||
error = sum(1 for s in services if s.status == 'Error')
|
||||
|
||||
# Pobranie preferencji użytkownika
|
||||
user = User.query.filter_by(username=session.get('username')).first()
|
||||
prefs = UserPreferences.query.filter_by(user_id=user.id).first() if user else None
|
||||
default_grouping = prefs.default_grouping if prefs else "protocol"
|
||||
|
||||
return render_template('dashboard.html',
|
||||
services=services,
|
||||
total=total, expired=expired,
|
||||
exp_soon=exp_soon, ok=ok, error=error,
|
||||
default_grouping=default_grouping)
|
||||
|
||||
@app.route('/api/services', methods=['GET'])
|
||||
def api_get_services():
|
||||
services = MonitoredService.query.all()
|
||||
data = []
|
||||
for s in services:
|
||||
data.append({
|
||||
'id': s.id,
|
||||
'host': s.host,
|
||||
'port': s.port,
|
||||
'protocol': s.protocol,
|
||||
'region': s.region, # Dodane pole region
|
||||
'last_check': s.last_check.strftime('%Y-%m-%d %H:%M:%S') if s.last_check else None,
|
||||
'expiry_date': s.expiry_date.strftime('%Y-%m-%d %H:%M:%S') if s.expiry_date else None,
|
||||
'status': s.status
|
||||
})
|
||||
return jsonify(data)
|
||||
|
||||
@app.route('/api/services/add', methods=['POST'])
|
||||
def api_add_service():
|
||||
req = request.json
|
||||
host = req.get('host')
|
||||
port = req.get('port', 443)
|
||||
protocol = req.get('protocol', 'https')
|
||||
region = req.get('region', '') # Pobranie regionu z żądania
|
||||
new_svc = MonitoredService(
|
||||
host=host,
|
||||
port=port,
|
||||
protocol=protocol,
|
||||
region=region, # Zapis regionu
|
||||
status='Unknown'
|
||||
)
|
||||
db.session.add(new_svc)
|
||||
db.session.commit()
|
||||
return jsonify({'message': 'Service added'}), 200
|
||||
|
||||
@app.route('/api/services/delete/<int:service_id>', methods=['DELETE'])
|
||||
def api_delete_service(service_id):
|
||||
svc = MonitoredService.query.get_or_404(service_id)
|
||||
db.session.delete(svc)
|
||||
db.session.commit()
|
||||
return jsonify({'message': 'Service deleted'}), 200
|
||||
|
||||
@app.route('/api/services/edit/<int:service_id>', methods=['POST'])
|
||||
def api_edit_service(service_id):
|
||||
req = request.json
|
||||
svc = MonitoredService.query.get_or_404(service_id)
|
||||
svc.host = req.get('host', svc.host)
|
||||
svc.port = req.get('port', svc.port)
|
||||
svc.protocol = req.get('protocol', svc.protocol)
|
||||
# Pobierz region – jeśli nie został podany, ustaw wartość domyślną (np. "default")
|
||||
region = req.get('region', '').strip()
|
||||
if not region:
|
||||
region = "default"
|
||||
svc.region = region
|
||||
db.session.commit()
|
||||
return jsonify({'message': 'Service updated'}), 200
|
||||
|
||||
@app.route('/api/services/update/<int:service_id>', methods=['POST'])
|
||||
def api_update_service(service_id):
|
||||
svc = MonitoredService.query.get_or_404(service_id)
|
||||
try:
|
||||
# Ujednolicenie protokołu
|
||||
proto = svc.protocol.lower().strip().replace(" ", "_")
|
||||
if proto == 'https':
|
||||
exp_date = check_https_cert(svc.host, svc.port)
|
||||
elif proto == 'smtp_starttls':
|
||||
exp_date = check_smtp_starttls_cert(svc.host, svc.port)
|
||||
elif proto == 'smtp_ssl':
|
||||
exp_date = check_smtp_ssl_cert(svc.host, svc.port)
|
||||
elif proto == 'imap_starttls':
|
||||
exp_date = check_imap_starttls_cert(svc.host, svc.port)
|
||||
elif proto == 'imap_ssl':
|
||||
exp_date = check_imap_ssl_cert(svc.host, svc.port)
|
||||
else:
|
||||
svc.status = 'ProtocolNotImplemented'
|
||||
db.session.commit()
|
||||
return jsonify({'message': 'Protocol not implemented'}), 200
|
||||
|
||||
svc.expiry_date = exp_date
|
||||
svc.last_check = datetime.now()
|
||||
if exp_date < datetime.now():
|
||||
svc.status = 'Expired'
|
||||
elif exp_date < datetime.now() + timedelta(days=7):
|
||||
svc.status = 'ExpiringSoon'
|
||||
else:
|
||||
svc.status = 'OK'
|
||||
db.session.commit()
|
||||
return jsonify({'message': 'Service updated'}), 200
|
||||
except Exception as e:
|
||||
svc.status = 'Error'
|
||||
db.session.commit()
|
||||
return jsonify({'message': 'Error updating service', 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/services/bulk_update', methods=['POST'])
|
||||
def api_bulk_update():
|
||||
update_certs()
|
||||
return jsonify({'message': 'Bulk update completed'}), 200
|
||||
|
||||
@app.route('/settings', methods=['GET', 'POST'])
|
||||
def app_settings():
|
||||
if 'logged_in' not in session or not session['logged_in']:
|
||||
flash("Musisz się zalogować.", "warning")
|
||||
return redirect(url_for('login'))
|
||||
|
||||
s = Settings.query.first()
|
||||
if not s:
|
||||
s = Settings()
|
||||
db.session.add(s)
|
||||
db.session.commit()
|
||||
|
||||
if request.method == 'POST':
|
||||
s.check_interval_minutes = int(request.form.get('check_interval_minutes', 60))
|
||||
s.pushover_enabled = bool(request.form.get('pushover_enabled', False))
|
||||
s.pushover_token = request.form.get('pushover_token', '')
|
||||
s.pushover_userkey = request.form.get('pushover_userkey', '')
|
||||
s.alert_threshold_30 = int(request.form.get('alert_threshold_info', 30))
|
||||
s.alert_threshold_14 = int(request.form.get('alert_threshold_warning', 14))
|
||||
s.alert_threshold_7 = int(request.form.get('alert_threshold_critical', 7))
|
||||
s.alert_repeat = bool(request.form.get('alert_repeat', False))
|
||||
# Pobierz wartość z nowego pola, domyślnie 30 dni
|
||||
s.logs_retention_days = int(request.form.get('logs_retention_days', 30))
|
||||
db.session.commit()
|
||||
|
||||
global scheduler
|
||||
if scheduler:
|
||||
scheduler.remove_all_jobs()
|
||||
scheduler.add_job(func=update_certs,
|
||||
trigger="interval",
|
||||
minutes=s.check_interval_minutes)
|
||||
|
||||
# Usuwanie logów starszych niż podana liczba dni
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=s.logs_retention_days)
|
||||
deleted_count = History.query.filter(History.timestamp < cutoff_date).delete()
|
||||
db.session.commit()
|
||||
|
||||
flash(f"Ustawienia zapisane. Usunięto {deleted_count} logów starszych niż {s.logs_retention_days} dni.", "success")
|
||||
return redirect(url_for('app_settings'))
|
||||
|
||||
return render_template('settings.html', settings=s)
|
||||
|
||||
@app.route('/api/service/response/<int:service_id>', methods=['GET'])
|
||||
def api_service_response(service_id):
|
||||
svc = MonitoredService.query.get_or_404(service_id)
|
||||
try:
|
||||
response_data = get_service_response(svc)
|
||||
return jsonify({'response': response_data})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
password = request.form.get('password')
|
||||
confirm = request.form.get('confirm')
|
||||
if password != confirm:
|
||||
flash("Hasła nie są zgodne.", "danger")
|
||||
return redirect(url_for('register'))
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash("Użytkownik o tej nazwie już istnieje.", "danger")
|
||||
return redirect(url_for('register'))
|
||||
hashed_pw = bcrypt.generate_password_hash(password).decode('utf-8')
|
||||
new_user = User(username=username, password=hashed_pw)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
# Rejestracja operacji w logu audytu
|
||||
log = AuditLog(user_id=new_user.id, operation="register", details="Nowy użytkownik zarejestrowany.")
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
flash("Rejestracja zakończona powodzeniem. Zaloguj się.", "success")
|
||||
return redirect(url_for('login'))
|
||||
return render_template('register.html')
|
||||
|
||||
@app.route('/change_password', methods=['GET', 'POST'])
|
||||
def change_password():
|
||||
if 'logged_in' not in session or not session['logged_in']:
|
||||
flash("Musisz być zalogowany.", "warning")
|
||||
return redirect(url_for('login'))
|
||||
if request.method == 'POST':
|
||||
current_password = request.form.get('current_password')
|
||||
new_password = request.form.get('new_password')
|
||||
confirm = request.form.get('confirm')
|
||||
user = User.query.filter_by(username=session.get('username')).first()
|
||||
if not user or not bcrypt.check_password_hash(user.password, current_password):
|
||||
flash("Błędne aktualne hasło.", "danger")
|
||||
return redirect(url_for('change_password'))
|
||||
if new_password != confirm:
|
||||
flash("Nowe hasła nie są zgodne.", "danger")
|
||||
return redirect(url_for('change_password'))
|
||||
user.password = bcrypt.generate_password_hash(new_password).decode('utf-8')
|
||||
db.session.commit()
|
||||
# Log operacji zmiany hasła
|
||||
log = AuditLog(user_id=user.id, operation="change_password", details="Zmiana hasła.")
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
flash("Hasło zostało zmienione.", "success")
|
||||
return redirect(url_for('dashboard'))
|
||||
return render_template('change_password.html')
|
||||
|
||||
@app.route('/api/cert_details/<int:service_id>', methods=['GET'])
|
||||
def api_cert_details(service_id):
|
||||
svc = MonitoredService.query.get_or_404(service_id)
|
||||
try:
|
||||
proto = svc.protocol.lower().strip().replace(" ", "_")
|
||||
if proto != "https":
|
||||
return jsonify({"error": "Szczegółowe informacje dostępne tylko dla HTTPS."}), 400
|
||||
details = get_cert_details(svc)
|
||||
html_table = format_cert_details_as_table(details)
|
||||
return jsonify({"html": html_table})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/history')
|
||||
def history():
|
||||
history_records = History.query.order_by(History.timestamp.desc()).all()
|
||||
return render_template('history.html', history=history_records)
|
||||
|
||||
@app.route('/preferences', methods=['GET', 'POST'])
|
||||
def preferences():
|
||||
if 'logged_in' not in session or not session['logged_in']:
|
||||
flash("Musisz się zalogować.", "warning")
|
||||
return redirect(url_for('login'))
|
||||
user = User.query.filter_by(username=session.get('username')).first()
|
||||
prefs = UserPreferences.query.filter_by(user_id=user.id).first()
|
||||
if not prefs:
|
||||
prefs = UserPreferences(user_id=user.id)
|
||||
db.session.add(prefs)
|
||||
db.session.commit()
|
||||
if request.method == 'POST':
|
||||
prefs.default_grouping = request.form.get('default_grouping', 'protocol')
|
||||
# Przyjmijmy, że filtrujemy jako JSON – np. {"region": "EU", "certificate_type": "DV"}
|
||||
prefs.filters = request.form.get('filters', '{}')
|
||||
db.session.commit()
|
||||
flash("Preferencje widoku zapisane.", "success")
|
||||
return redirect(url_for('dashboard'))
|
||||
return render_template('preferences.html', prefs=prefs)
|
||||
|
||||
@app.route('/api/cert_chain/<int:service_id>', methods=['GET'])
|
||||
def api_cert_chain(service_id):
|
||||
svc = MonitoredService.query.get_or_404(service_id)
|
||||
try:
|
||||
proto = svc.protocol.lower().strip().replace(" ", "_")
|
||||
if proto != "https":
|
||||
return jsonify({"error": "Łańcuch certyfikatów dostępny tylko dla HTTPS."}), 400
|
||||
html_details = get_cert_chain_html(svc.host, svc.port)
|
||||
return jsonify({"html": html_details})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
###############################################################################
|
||||
# URUCHAMIANIE APLIKACJI (tryb deweloperski)
|
||||
###############################################################################
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = create_app()
|
||||
app.run(debug=True)
|
9
config.py
Normal file
9
config.py
Normal file
@ -0,0 +1,9 @@
|
||||
# config.py
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
class Config:
|
||||
SECRET_KEY = 'secret'
|
||||
SQLALCHEMY_DATABASE_URI = f'sqlite:///{os.path.join(BASE_DIR, "ssl_monitor.db")}'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
@ -0,0 +1,12 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "5583:5583"
|
||||
volumes:
|
||||
- ./instance:/app/instance
|
||||
restart: unless-stopped
|
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -0,0 +1,7 @@
|
||||
Flask
|
||||
flask-sqlalchemy
|
||||
APScheduler
|
||||
waitress
|
||||
requests
|
||||
flask-bcrypt
|
||||
cryptography
|
7
run_waitress.py
Normal file
7
run_waitress.py
Normal file
@ -0,0 +1,7 @@
|
||||
from waitress import serve
|
||||
from app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
serve(app, host='0.0.0.0', port=5583)
|
13
ssl_monitor.service
Normal file
13
ssl_monitor.service
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
[Unit]
|
||||
Description=ssl_monitor Waitress Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/opt/ssl_monitor
|
||||
ExecStart=/opt/ssl_monitor/venv/bin/python3 /opt/ssl_monitor/run_waitress.py
|
||||
Restart=always
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
0
static/css/custom.css
Normal file
0
static/css/custom.css
Normal file
89
templates/base.html
Normal file
89
templates/base.html
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pl" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}SSL Monitor{% endblock %}</title>
|
||||
<!-- Bootstrap CSS (Bootstrap 5.3.0) -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Własny plik CSS -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body class="dark-mode">
|
||||
<!-- Pasek nawigacyjny -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('dashboard') }}">SSL Monitor</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown"
|
||||
aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Przełącz nawigację">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNavDropdown">
|
||||
{% if session.get('user_id') %}
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('dashboard') }}">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('history') }}">Historia</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('preferences') }}">Preferencje widoku</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('app_settings') }}">Ustawienia</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
{% endif %}
|
||||
<ul class="navbar-nav ms-auto align-items-center">
|
||||
|
||||
{% if session.get('user_id') %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('change_password') }}">Zmiana hasła</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link btn btn-danger ms-2" href="{{ url_for('logout') }}">Wyloguj</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link btn btn-success ms-2" href="{{ url_for('login') }}">Zaloguj</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link btn btn-info ms-2" href="{{ url_for('register') }}">Rejestracja</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Kontener alertów dla dynamicznych komunikatów -->
|
||||
<div id="alert-container" class="container mt-3"></div>
|
||||
<!-- Główny kontener treści -->
|
||||
<div class="container mt-4">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Zamknij"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<!-- Stopka -->
|
||||
<footer class="bg-dark text-center text-lg-start mt-5 py-3">
|
||||
<div class="container">
|
||||
<span class="text-light">© 2025 SSL Monitor, @linuxiarz.pl</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS Bundle (Bootstrap 5.3.0) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
15
templates/cert_details.html
Normal file
15
templates/cert_details.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Szczegóły certyfikatu - SSL Monitor{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>Szczegóły certyfikatu</h2>
|
||||
{% if details.error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ details.error }}
|
||||
</div>
|
||||
{% else %}
|
||||
<pre>{{ details | tojson(indent=2) }}</pre>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">Powrót</a>
|
||||
</div>
|
||||
{% endblock %}
|
22
templates/change_password.html
Normal file
22
templates/change_password.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Zmiana hasła - SSL Monitor{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>Zmiana hasła</h2>
|
||||
<form method="POST" action="{{ url_for('change_password') }}">
|
||||
<div class="mb-3">
|
||||
<label for="current_password" class="form-label">Aktualne hasło</label>
|
||||
<input type="password" class="form-control" id="current_password" name="current_password" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_password" class="form-label">Nowe hasło</label>
|
||||
<input type="password" class="form-control" id="new_password" name="new_password" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirm" class="form-label">Potwierdź nowe hasło</label>
|
||||
<input type="password" class="form-control" id="confirm" name="confirm" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Zmień hasło</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
453
templates/dashboard.html
Normal file
453
templates/dashboard.html
Normal file
@ -0,0 +1,453 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard - SSL Monitor{% endblock %}
|
||||
{% block content %}
|
||||
<h2 class="mb-4 text-center">Dashboard</h2>
|
||||
|
||||
<!-- Przyciski akcji na górze -->
|
||||
<div class="mb-4 text-center">
|
||||
<button class="btn btn-success me-2" onclick="showAddServiceModal()">Dodaj Serwis</button>
|
||||
<button class="btn btn-primary me-2" onclick="bulkUpdate()">Bulk Update</button>
|
||||
</div>
|
||||
|
||||
<!-- Opcje grupowania -->
|
||||
<div class="mb-4 d-flex justify-content-center">
|
||||
<div style="max-width:300px;">
|
||||
<label for="groupingCriteria" class="form-label">Grupuj usługi:</label>
|
||||
<select id="groupingCriteria" class="form-select">
|
||||
<option value="none" {% if default_grouping == 'none' %}selected{% endif %}>Brak grupowania</option>
|
||||
<option value="protocol" {% if default_grouping == 'protocol' %}selected{% endif %}>Grupuj według protokołu</option>
|
||||
<option value="host" {% if default_grouping == 'host' %}selected{% endif %}>Grupuj według hosta</option>
|
||||
<option value="region" {% if default_grouping == 'region' %}selected{% endif %}>Grupuj według regionu</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cardsContainer" class="row">
|
||||
{% for s in services %}
|
||||
{% set card_class = "" %}
|
||||
{% if s.status == "Expired" %}
|
||||
{% set card_class = "bg-danger text-white" %}
|
||||
{% elif s.status == "ExpiringSoon" %}
|
||||
{% set card_class = "bg-warning text-dark" %}
|
||||
{% elif s.status == "OK" %}
|
||||
{% set card_class = "bg-success text-white" %}
|
||||
{% elif s.status == "Error" %}
|
||||
{% set card_class = "bg-secondary text-white" %}
|
||||
{% else %}
|
||||
{% set card_class = "bg-light" %}
|
||||
{% endif %}
|
||||
<div class="col-md-4 mb-4 service-card" data-protocol="{{ s.protocol|lower|trim }}" data-host="{{ s.host }}" data-region="{{ s.region }}">
|
||||
|
||||
<div class="card {{ card_class }} shadow-sm rounded">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ s.host }}:{{ s.port }} ({{ s.protocol }})</h5>
|
||||
<p class="card-text">
|
||||
Status: <strong>{{ s.status }}</strong><br>
|
||||
Last Check: {{ s.last_check if s.last_check else '---' }}<br>
|
||||
Expiry: {{ s.expiry_date if s.expiry_date else '---' }}
|
||||
</p>
|
||||
<div class="d-flex justify-content-center gap-2 flex-wrap">
|
||||
<button class="btn btn-sm btn-light" onclick="showEditServiceModal({{ s.id }})">Edytuj</button>
|
||||
<button class="btn btn-sm btn-info" onclick="updateService({{ s.id }})">Aktualizuj</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteService({{ s.id }})">Usuń</button>
|
||||
{% if s.protocol|lower == 'https' %}
|
||||
<button class="btn btn-sm btn-secondary" onclick="showCertDetails({{ s.id }})">Szczegóły certyfikatu</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick="showCertChain({{ s.id }})">Łańcuch certyfikatów</button>
|
||||
{% else %}
|
||||
<button class="btn btn-sm btn-secondary" onclick="showResponse({{ s.id }})">Odpowiedź serwera</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Modale -->
|
||||
<!-- Modal dodawania serwisu -->
|
||||
<div class="modal" tabindex="-1" id="addServiceModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content bg-dark text-white">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Dodaj nowy serwis</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close" onclick="hideAddServiceModal()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="addServiceForm">
|
||||
<div class="mb-3">
|
||||
<label for="host" class="form-label">Host</label>
|
||||
<input type="text" class="form-control" id="host" name="host" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="protocol" class="form-label">Protocol</label>
|
||||
<select class="form-select" id="protocol" name="protocol">
|
||||
<option value="https">HTTPS (443)</option>
|
||||
<option value="smtp_starttls">SMTP STARTTLS (587)</option>
|
||||
<option value="smtp_ssl">SMTP SSL (465)</option>
|
||||
<option value="imap_starttls">IMAP STARTTLS (143)</option>
|
||||
<option value="imap_ssl">IMAP SSL (993)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="port" class="form-label">Port</label>
|
||||
<input type="number" class="form-control" id="port" name="port" value="443">
|
||||
</div>
|
||||
<!-- Nowe pole dla regionu -->
|
||||
<div class="mb-3">
|
||||
<label for="region" class="form-label">Region</label>
|
||||
<input type="text" class="form-control" id="region" name="region">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="hideAddServiceModal()">Anuluj</button>
|
||||
<button class="btn btn-primary" onclick="addService()">Dodaj</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal edycji serwisu -->
|
||||
<!-- Modal edycji serwisu -->
|
||||
<div class="modal" tabindex="-1" id="editServiceModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content bg-dark text-white">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Edytuj serwis</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close" onclick="hideEditServiceModal()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editServiceForm">
|
||||
<input type="hidden" id="editServiceId">
|
||||
<div class="mb-3">
|
||||
<label for="editHost" class="form-label">Host</label>
|
||||
<input type="text" class="form-control" id="editHost" name="host" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editProtocol" class="form-label">Protocol</label>
|
||||
<select class="form-select" id="editProtocol" name="protocol">
|
||||
<option value="https">HTTPS (443)</option>
|
||||
<option value="smtp_starttls">SMTP STARTTLS (587)</option>
|
||||
<option value="smtp_ssl">SMTP SSL (465)</option>
|
||||
<option value="imap_starttls">IMAP STARTTLS (143)</option>
|
||||
<option value="imap_ssl">IMAP SSL (993)</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Nowe pole dla regionu -->
|
||||
<div class="mb-3">
|
||||
<label for="editRegion" class="form-label">Region</label>
|
||||
<input type="text" class="form-control" id="editRegion" name="region">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editPort" class="form-label">Port</label>
|
||||
<input type="number" class="form-control" id="editPort" name="port" value="443">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="hideEditServiceModal()">Anuluj</button>
|
||||
<button class="btn btn-primary" onclick="editService()">Zapisz zmiany</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal wyświetlania certyfikatu / łańcucha certyfikatów (dla HTTPS) -->
|
||||
<div class="modal" tabindex="-1" id="certDetailsModal">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content bg-dark text-white">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Szczegóły certyfikatu / Łańcuch certyfikatów</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close" onclick="hideCertDetailsModal()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="certDetails"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="hideCertDetailsModal()">Zamknij</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal wyświetlania odpowiedzi serwera (dla nie-HTTPS) -->
|
||||
<div class="modal" tabindex="-1" id="responseModal">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content bg-dark text-white">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Odpowiedź serwera</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close" onclick="hideResponseModal()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="serverResponse" style="white-space: pre-wrap;"></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="hideResponseModal()">Zamknij</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% block extra_js %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
let addServiceModal = null;
|
||||
let editServiceModal = null;
|
||||
let certDetailsModal = null;
|
||||
let responseModal = null;
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
addServiceModal = new bootstrap.Modal(document.getElementById('addServiceModal'), {});
|
||||
editServiceModal = new bootstrap.Modal(document.getElementById('editServiceModal'), {});
|
||||
certDetailsModal = new bootstrap.Modal(document.getElementById('certDetailsModal'), {});
|
||||
responseModal = new bootstrap.Modal(document.getElementById('responseModal'), {});
|
||||
|
||||
// Aktualizacja domyślnej wartości portu w formularzu dodawania
|
||||
document.getElementById('protocol').addEventListener('change', function() {
|
||||
let portField = document.getElementById('port');
|
||||
switch(this.value) {
|
||||
case 'https': portField.value = 443; break;
|
||||
case 'smtp_starttls': portField.value = 587; break;
|
||||
case 'smtp_ssl': portField.value = 465; break;
|
||||
case 'imap_starttls': portField.value = 143; break;
|
||||
case 'imap_ssl': portField.value = 993; break;
|
||||
default: portField.value = 443;
|
||||
}
|
||||
});
|
||||
|
||||
// Aktualizacja domyślnej wartości portu w formularzu edycji
|
||||
document.getElementById('editProtocol').addEventListener('change', function() {
|
||||
let portField = document.getElementById('editPort');
|
||||
switch(this.value) {
|
||||
case 'https': portField.value = 443; break;
|
||||
case 'smtp_starttls': portField.value = 587; break;
|
||||
case 'smtp_ssl': portField.value = 465; break;
|
||||
case 'imap_starttls': portField.value = 143; break;
|
||||
case 'imap_ssl': portField.value = 993; break;
|
||||
default: portField.value = 443;
|
||||
}
|
||||
});
|
||||
|
||||
// Grupowanie kart przy zmianie kryterium
|
||||
document.getElementById('groupingCriteria').addEventListener('change', function() {
|
||||
groupCards(this.value);
|
||||
});
|
||||
groupCards(document.getElementById('groupingCriteria').value);
|
||||
});
|
||||
|
||||
function showAlert(message, type) {
|
||||
const alertContainer = document.getElementById('alert-container');
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
|
||||
alertDiv.setAttribute('role', 'alert');
|
||||
alertDiv.innerHTML = message +
|
||||
'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Zamknij"></button>';
|
||||
alertContainer.appendChild(alertDiv);
|
||||
setTimeout(function(){
|
||||
alertDiv.classList.remove('show');
|
||||
alertDiv.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function groupCards(criteria) {
|
||||
const container = document.getElementById('cardsContainer');
|
||||
const cards = Array.from(container.getElementsByClassName('service-card'));
|
||||
container.innerHTML = "";
|
||||
|
||||
if (criteria === "none") {
|
||||
cards.forEach(card => container.appendChild(card));
|
||||
} else {
|
||||
const groups = {};
|
||||
cards.forEach(card => {
|
||||
const key = card.getAttribute("data-" + criteria);
|
||||
if (!groups[key]) {
|
||||
groups[key] = [];
|
||||
}
|
||||
groups[key].push(card);
|
||||
});
|
||||
for (let group in groups) {
|
||||
const header = document.createElement("h4");
|
||||
header.className = "mt-4";
|
||||
header.innerText = criteria === "protocol" ? group.toUpperCase() : group;
|
||||
container.appendChild(header);
|
||||
const rowDiv = document.createElement("div");
|
||||
rowDiv.className = "row";
|
||||
groups[group].forEach(card => rowDiv.appendChild(card));
|
||||
container.appendChild(rowDiv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showAddServiceModal() { addServiceModal.show(); }
|
||||
function hideAddServiceModal() { addServiceModal.hide(); }
|
||||
|
||||
function showEditServiceModal(serviceId) {
|
||||
fetch("{{ url_for('api_get_services') }}")
|
||||
.then(r => r.json())
|
||||
.then(services => {
|
||||
const svc = services.find(s => s.id === serviceId);
|
||||
if (svc) {
|
||||
document.getElementById("editServiceId").value = svc.id;
|
||||
document.getElementById("editHost").value = svc.host;
|
||||
document.getElementById("editPort").value = svc.port;
|
||||
document.getElementById("editProtocol").value = svc.protocol;
|
||||
document.getElementById("editRegion").value = svc.region ? svc.region : "default";
|
||||
editServiceModal.show();
|
||||
}
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
||||
function showCertDetails(serviceId) {
|
||||
fetch(`{{ url_for('api_cert_details', service_id=0) }}`.replace("0", serviceId))
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
let htmlContent = "";
|
||||
if (data.error) {
|
||||
htmlContent = `<p class="text-danger">Błąd: ${data.error}</p>`;
|
||||
} else {
|
||||
htmlContent = data.html;
|
||||
}
|
||||
document.getElementById("certDetails").innerHTML = htmlContent;
|
||||
certDetailsModal.show();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
document.getElementById("certDetails").innerHTML = `<p class="text-danger">Błąd przy pobieraniu szczegółów certyfikatu.</p>`;
|
||||
certDetailsModal.show();
|
||||
});
|
||||
}
|
||||
|
||||
function showCertChain(serviceId) {
|
||||
fetch(`{{ url_for('api_cert_chain', service_id=0) }}`.replace("0", serviceId))
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
let htmlContent = "";
|
||||
if (data.error) {
|
||||
htmlContent = `<p class="text-danger">Błąd: ${data.error}</p>`;
|
||||
} else {
|
||||
htmlContent = data.html;
|
||||
}
|
||||
document.getElementById("certDetails").innerHTML = htmlContent;
|
||||
certDetailsModal.show();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
document.getElementById("certDetails").innerHTML = `<p class="text-danger">Błąd przy pobieraniu łańcucha certyfikatów.</p>`;
|
||||
certDetailsModal.show();
|
||||
});
|
||||
}
|
||||
|
||||
function showResponse(serviceId) {
|
||||
fetch(`{{ url_for('api_service_response', service_id=0) }}`.replace("0", serviceId))
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
let responseText = "";
|
||||
if (data.error) {
|
||||
responseText = "Błąd: " + data.error;
|
||||
} else {
|
||||
responseText = data.response;
|
||||
}
|
||||
document.getElementById("serverResponse").innerText = responseText;
|
||||
responseModal.show();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
document.getElementById("serverResponse").innerText = "Błąd przy pobieraniu odpowiedzi serwera.";
|
||||
responseModal.show();
|
||||
});
|
||||
}
|
||||
|
||||
function hideCertDetailsModal() { certDetailsModal.hide(); }
|
||||
function hideResponseModal() { responseModal.hide(); }
|
||||
|
||||
function addService() {
|
||||
const host = document.getElementById("host").value;
|
||||
const port = document.getElementById("port").value;
|
||||
const protocol = document.getElementById("protocol").value;
|
||||
const region = document.getElementById("region").value; // Pobranie wartości regionu
|
||||
|
||||
fetch("{{ url_for('api_add_service') }}", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({host, port, protocol, region})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(resp => {
|
||||
showAlert("Serwis został dodany.", "success");
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showAlert("Błąd przy dodawaniu serwisu.", "danger");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function deleteService(serviceId) {
|
||||
if (!confirm("Czy na pewno chcesz usunąć ten serwis?")) return;
|
||||
const url = "{{ url_for('api_delete_service', service_id=0) }}".replace("0", serviceId);
|
||||
|
||||
fetch(url, { method: "DELETE" })
|
||||
.then(r => r.json())
|
||||
.then(resp => {
|
||||
showAlert("Serwis został usunięty.", "success");
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showAlert("Błąd przy usuwaniu serwisu.", "danger");
|
||||
});
|
||||
}
|
||||
|
||||
function editService() {
|
||||
const serviceId = document.getElementById("editServiceId").value;
|
||||
const host = document.getElementById("editHost").value;
|
||||
const port = document.getElementById("editPort").value;
|
||||
const protocol = document.getElementById("editProtocol").value;
|
||||
const region = document.getElementById("editRegion").value; // Pobranie wartości regionu
|
||||
|
||||
fetch(`{{ url_for('api_edit_service', service_id=0) }}`.replace("0", serviceId), {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({host, port, protocol, region})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(resp => {
|
||||
showAlert("Serwis został zaktualizowany.", "success");
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showAlert("Błąd przy aktualizacji serwisu.", "danger");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function updateService(serviceId) {
|
||||
fetch(`{{ url_for('api_update_service', service_id=0) }}`.replace("0", serviceId), { method: "POST" })
|
||||
.then(r => r.json())
|
||||
.then(resp => {
|
||||
showAlert("Serwis został zaktualizowany.", "success");
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showAlert("Błąd przy aktualizacji serwisu.", "danger");
|
||||
});
|
||||
}
|
||||
|
||||
function bulkUpdate() {
|
||||
fetch("{{ url_for('api_bulk_update') }}", { method: "POST" })
|
||||
.then(r => r.json())
|
||||
.then(resp => {
|
||||
showAlert("Masowa aktualizacja wykonana.", "success");
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showAlert("Błąd przy masowej aktualizacji.", "danger");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
31
templates/history.html
Normal file
31
templates/history.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Historia - SSL Monitor{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>Historia sprawdzeń certyfikatów</h2>
|
||||
<table class="table table-dark table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Usługa</th>
|
||||
<th>Status</th>
|
||||
<th>Data sprawdzenia</th>
|
||||
<th>Data wygaśnięcia</th>
|
||||
<th>Komunikat</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for record in history %}
|
||||
<tr>
|
||||
<td>{{ record.id }}</td>
|
||||
<td>{{ record.service.host }}:{{ record.service.port }} ({{ record.service.protocol }})</td>
|
||||
<td>{{ record.status }}</td>
|
||||
<td>{{ record.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
||||
<td>{{ record.expiry_date.strftime('%Y-%m-%d %H:%M:%S') if record.expiry_date else '---' }}</td>
|
||||
<td>{{ record.message }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
27
templates/login.html
Normal file
27
templates/login.html
Normal file
@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Logowanie - SSL Monitor{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h3 class="mb-3 text-center">Logowanie</h3>
|
||||
{% if error %}
|
||||
<div class="alert alert-danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
<form method="POST" action="{{ url_for('login') }}">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Użytkownik</label>
|
||||
<input type="text" class="form-control" name="username" id="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Hasło</label>
|
||||
<input type="password" class="form-control" name="password" id="password" required>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Zaloguj</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
24
templates/preferences.html
Normal file
24
templates/preferences.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Preferencje Widoku - SSL Monitor{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>Preferencje Widoku</h2>
|
||||
<form method="POST" action="{{ url_for('preferences') }}">
|
||||
<div class="mb-3">
|
||||
<label for="default_grouping" class="form-label">Domyślne kryterium grupowania:</label>
|
||||
<select id="default_grouping" name="default_grouping" class="form-select">
|
||||
<option value="none" {% if prefs.default_grouping == 'none' %}selected{% endif %}>Brak grupowania</option>
|
||||
<option value="protocol" {% if prefs.default_grouping == 'protocol' %}selected{% endif %}>Grupuj według protokołu</option>
|
||||
<option value="host" {% if prefs.default_grouping == 'host' %}selected{% endif %}>Grupuj według hosta</option>
|
||||
<option value="region" {% if prefs.default_grouping == 'region' %}selected{% endif %}>Grupuj według regionu</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="filters" class="form-label">Filtry (JSON):</label>
|
||||
<textarea id="filters" name="filters" class="form-control" rows="3">{{ prefs.filters }}</textarea>
|
||||
<div class="form-text">Np. {"region": "EU", "certificate_type": "DV"}</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Zapisz preferencje</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
29
templates/register.html
Normal file
29
templates/register.html
Normal file
@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Rejestracja - SSL Monitor{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2 class="mb-3 text-center">Rejestracja</h2>
|
||||
<form method="POST" action="{{ url_for('register') }}">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Nazwa użytkownika</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Hasło</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirm" class="form-label">Potwierdź hasło</label>
|
||||
<input type="password" class="form-control" id="confirm" name="confirm" required>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Zarejestruj się</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="mt-2 text-center">Masz już konto? <a href="{{ url_for('login') }}">Zaloguj się</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
48
templates/settings.html
Normal file
48
templates/settings.html
Normal file
@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Ustawienia - SSL Monitor{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>Ustawienia</h2>
|
||||
<form method="POST" action="{{ url_for('app_settings') }}">
|
||||
<div class="mb-3">
|
||||
<label for="check_interval_minutes" class="form-label">Częstotliwość sprawdzania (minuty)</label>
|
||||
<input type="number" class="form-control" id="check_interval_minutes" name="check_interval_minutes" value="{{ settings.check_interval_minutes }}">
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" class="form-check-input" id="pushover_enabled" name="pushover_enabled" {% if settings.pushover_enabled %}checked{% endif %}>
|
||||
<label class="form-check-label" for="pushover_enabled">Włącz powiadomienia Pushover</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pushover_token" class="form-label">Pushover Token</label>
|
||||
<input type="text" class="form-control" id="pushover_token" name="pushover_token" value="{{ settings.pushover_token }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pushover_userkey" class="form-label">Pushover User Key</label>
|
||||
<input type="text" class="form-control" id="pushover_userkey" name="pushover_userkey" value="{{ settings.pushover_userkey }}">
|
||||
</div>
|
||||
<!-- Nowe pola dla konfiguracji alertów -->
|
||||
<div class="mb-3">
|
||||
<label for="alert_threshold_info" class="form-label">Informacyjne: Ilość dni przed wygaśnięciem dla powiadomienia</label>
|
||||
<input type="number" class="form-control" id="alert_threshold_info" name="alert_threshold_info" value="{{ settings.alert_threshold_30 }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="alert_threshold_warning" class="form-label">Ostrzegawcze: Ilość dni przed wygaśnięciem dla powiadomienia</label>
|
||||
<input type="number" class="form-control" id="alert_threshold_warning" name="alert_threshold_warning" value="{{ settings.alert_threshold_14 }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="alert_threshold_critical" class="form-label">Krytyczne: Ilość dni przed wygaśnięciem dla powiadomienia</label>
|
||||
<input type="number" class="form-control" id="alert_threshold_critical" name="alert_threshold_critical" value="{{ settings.alert_threshold_7 }}">
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" class="form-check-input" id="alert_repeat" name="alert_repeat" {% if settings.alert_repeat %}checked{% endif %}>
|
||||
<label class="form-check-label" for="alert_repeat">Wysyłaj powiadomienia wielokrotnie</label>
|
||||
</div>
|
||||
<!-- Nowe pole do kasowania logów -->
|
||||
<div class="mb-3">
|
||||
<label for="logs_retention_days" class="form-label">Kasuj logi starsze niż (dni)</label>
|
||||
<input type="number" class="form-control" id="logs_retention_days" name="logs_retention_days" value="{{ settings.logs_retention_days }}">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Zapisz ustawienia</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user