274 lines
13 KiB
Python
274 lines
13 KiB
Python
import sqlite3, uuid, random
|
|
import atexit
|
|
from datetime import datetime, timedelta
|
|
from flask import Flask, request, jsonify, render_template_string, session, redirect, send_from_directory
|
|
from apscheduler.schedulers.background import BackgroundScheduler
|
|
from helpers.send_email import send_gmail
|
|
|
|
app = Flask(__name__, static_folder='static', static_url_path='/static')
|
|
app.secret_key = "cheie_ultra_secreta_aquila"
|
|
DB_FILE = "data/aquila_forms.db"
|
|
ADMIN_EMAIL = "macamete.robert@gmail.com"
|
|
|
|
# --- LOGICA BAZĂ DE DATE ---
|
|
def init_db():
|
|
with sqlite3.connect(DB_FILE) as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''CREATE TABLE IF NOT EXISTS clients
|
|
(id TEXT PRIMARY KEY,
|
|
nume TEXT,
|
|
email_destinatie TEXT,
|
|
creat_la DATETIME,
|
|
data_expirare DATETIME)''')
|
|
cursor.execute('''CREATE TABLE IF NOT EXISTS otps (cod TEXT, expira_la DATETIME)''')
|
|
conn.commit()
|
|
|
|
# --- RUTE ADMIN ---
|
|
@app.route('/admin')
|
|
def admin_home():
|
|
if not session.get('logged_in'):
|
|
return render_template_string(LOGIN_HTML)
|
|
|
|
with sqlite3.connect(DB_FILE) as conn:
|
|
conn.row_factory = sqlite3.Row
|
|
clients = conn.execute("SELECT * FROM clients").fetchall()
|
|
return render_template_string(DASHBOARD_HTML, clients=clients)
|
|
|
|
@app.route('/admin/login', methods=['POST'])
|
|
def do_login():
|
|
email = request.form.get('email')
|
|
if email == ADMIN_EMAIL:
|
|
otp = str(random.randint(100000, 999999))
|
|
expira = datetime.now() + timedelta(minutes=10)
|
|
with sqlite3.connect(DB_FILE) as conn:
|
|
conn.execute("INSERT INTO otps VALUES (?, ?)", (otp, expira))
|
|
send_gmail(ADMIN_EMAIL, "Cod Acces Panou", f"Codul tau: <b>{otp}</b>")
|
|
return render_template_string(VERIFY_HTML)
|
|
return "Acces interzis", 403
|
|
|
|
@app.route('/admin/verify', methods=['POST'])
|
|
def do_verify():
|
|
cod = request.form.get('cod')
|
|
with sqlite3.connect(DB_FILE) as conn:
|
|
res = conn.execute("SELECT * FROM otps WHERE cod=? AND expira_la > ?", (cod, datetime.now())).fetchone()
|
|
if res:
|
|
session['logged_in'] = True
|
|
conn.execute("DELETE FROM otps") # Curățăm codurile folosite
|
|
return redirect('/admin')
|
|
return "Cod invalid", 401
|
|
|
|
@app.route('/admin/add', methods=['POST'])
|
|
def add_client():
|
|
if not session.get('logged_in'): return "No", 401
|
|
uid = str(uuid.uuid4())[:8]
|
|
nume = request.form.get('nume')
|
|
email = request.form.get('email')
|
|
acum = datetime.now().isoformat()
|
|
# Calculăm data de expirare (peste 365 zile)
|
|
data_expirare = (datetime.now() + timedelta(days=365)).isoformat()
|
|
|
|
with sqlite3.connect(DB_FILE) as conn:
|
|
conn.execute("INSERT INTO clients VALUES (?, ?, ?, ?, ?)",
|
|
(uid, nume, email, acum, data_expirare))
|
|
return redirect('/admin')
|
|
|
|
@app.route('/admin/delete/<id>')
|
|
def delete_client(id):
|
|
if not session.get('logged_in'): return "No", 401
|
|
with sqlite3.connect(DB_FILE) as conn:
|
|
conn.execute("DELETE FROM clients WHERE id=?", (id,))
|
|
return redirect('/admin')
|
|
|
|
# --- ENDPOINT PUBLIC PENTRU SITE-URI ---
|
|
@app.route('/api/v1/send', methods=['POST'])
|
|
def public_api():
|
|
cid = request.form.get('client_id')
|
|
with sqlite3.connect(DB_FILE) as conn:
|
|
conn.row_factory = sqlite3.Row
|
|
client = conn.execute("SELECT * FROM clients WHERE id=?", (cid,)).fetchone()
|
|
|
|
if client:
|
|
body = f"Mesaj nou de la <b>{request.form.get('nume')}</b><br>Email: {request.form.get('email')}<br><br>{request.form.get('mesaj')}"
|
|
send_gmail(client['email_destinatie'], f"Contact: {client['nume']}", body)
|
|
return jsonify({"status": "ok"}), 200
|
|
return jsonify({"status": "error"}), 404
|
|
|
|
# --- LOGICA DE VERIFICARE BILLING ---
|
|
def check_billing_reminders():
|
|
print(f"[{datetime.now()}] Se verifică termenele de facturare...")
|
|
|
|
# Calculăm data de peste fix 30 de zile (doar data, fără oră)
|
|
tinta = (datetime.now() + timedelta(days=30)).strftime('%Y-%m-%d')
|
|
|
|
try:
|
|
with sqlite3.connect(DB_FILE) as conn:
|
|
conn.row_factory = sqlite3.Row
|
|
# Verificăm cine expiră în fereastra de 30 de zile
|
|
query = "SELECT * FROM clients WHERE date(data_expirare) = ?"
|
|
clienti = conn.execute(query, (tinta,)).fetchall()
|
|
|
|
for client in clienti:
|
|
subiect = f"💰 Facturare: {client['nume']}"
|
|
corp = f"""
|
|
<h3>Reminder Facturare</h3>
|
|
<p>Clientul <b>{client['nume']}</b> are data de expirare pe <b>{client['data_expirare']}</b>.</p>
|
|
<p>Trebuie emisă factura pentru reînnoire.</p>
|
|
<br>
|
|
<small>ID Intern: {client['id']}</small>
|
|
"""
|
|
send_gmail(ADMIN_EMAIL, subiect, corp)
|
|
print(f"Notificare trimisă pentru {client['nume']}")
|
|
|
|
except Exception as e:
|
|
print(f"Eroare la verificarea billing-ului: {e}")
|
|
|
|
# --- CONFIGURARE SCHEDULER ---
|
|
scheduler = BackgroundScheduler()
|
|
# Setează ora la care vrei să primești mail-ul (ex: ora 09:00 dimineața)
|
|
scheduler.add_job(func=check_billing_reminders, trigger="cron", hour=9, minute=0)
|
|
scheduler.start()
|
|
|
|
# Închide scheduler-ul când aplicația se oprește
|
|
atexit.register(lambda: scheduler.shutdown())
|
|
|
|
@app.route('/logo.png')
|
|
def serve_logo():
|
|
return send_from_directory(app.static_folder, 'logo.png')
|
|
|
|
# --- TEMPLATE-URI HTML (Simplificate) ---
|
|
LOGIN_HTML = """
|
|
<!DOCTYPE html>
|
|
<html lang="ro">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<title>Login Admin - AquilaSoft</title>
|
|
</head>
|
|
<body class="bg-gray-100 h-screen flex items-center justify-center p-4">
|
|
<div class="bg-white p-8 rounded-xl shadow-md w-full max-w-md">
|
|
<h2 class="text-2xl font-bold mb-6 text-gray-800 text-center">Admin Access</h2>
|
|
<form action="/admin/login" method="post" class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">Email Admin</label>
|
|
<input type="email" name="email" required class="mt-1 w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none">
|
|
</div>
|
|
<button class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition">Trimite Cod</button>
|
|
</form>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
VERIFY_HTML = """
|
|
<!DOCTYPE html>
|
|
<html lang="ro">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<title>Verificare Cod - AquilaSoft</title>
|
|
</head>
|
|
<body class="bg-gray-100 h-screen flex items-center justify-center p-4">
|
|
<div class="bg-white p-8 rounded-xl shadow-md w-full max-w-md">
|
|
<h2 class="text-2xl font-bold mb-2 text-gray-800 text-center">Verificare Cod</h2>
|
|
<p class="text-sm text-gray-600 mb-6 text-center">Introdu codul primit pe email.</p>
|
|
<form action="/admin/verify" method="post" class="space-y-4">
|
|
<input type="text" name="cod" placeholder="000000" required class="w-full p-4 border rounded-lg text-center text-2xl tracking-widest focus:ring-2 focus:ring-blue-500 outline-none">
|
|
<button class="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-3 rounded-lg transition">Verifică</button>
|
|
</form>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
DASHBOARD_HTML = """
|
|
<!DOCTYPE html>
|
|
<html lang="ro">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<title>Dashboard - AquilaSoft</title>
|
|
</head>
|
|
<body class="bg-gray-50 min-h-screen">
|
|
<nav class="bg-blue-700 text-white p-3 shadow-lg">
|
|
<div class="container mx-auto flex justify-between items-center">
|
|
<div class="flex items-center gap-3">
|
|
<img src="/logo.png" alt="Logo AquilaSoft" class="h-10 w-auto rounded">
|
|
<h1 class="text-xl font-bold italic tracking-tight">Forms Management</h1>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-xs text-blue-200">Admin</span>
|
|
<a href="/admin" class="text-sm bg-blue-800 hover:bg-blue-900 px-4 py-2 rounded-lg transition">Refresh</a>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="container mx-auto p-4 md:p-8">
|
|
<div class="bg-white p-6 rounded-xl shadow-sm mb-8 border border-gray-100">
|
|
<h2 class="text-lg font-semibold mb-5 text-gray-700 flex items-center gap-2">
|
|
<svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"></path></svg>
|
|
Adaugă Client Nou
|
|
</h2>
|
|
<form action="/admin/add" method="post" class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<input name="nume" placeholder="Nume Client (ex: Avocat Popescu)" required class="p-3 border rounded-lg focus:ring-2 focus:ring-blue-200 outline-none transition">
|
|
<input name="email" type="email" placeholder="Email Destinație Reînnoire" required class="p-3 border rounded-lg focus:ring-2 focus:ring-blue-200 outline-none transition">
|
|
<button class="bg-blue-600 text-white font-bold py-3 rounded-lg hover:bg-blue-700 transition flex items-center justify-center gap-2">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
|
Creează Client
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-xl shadow-sm overflow-hidden border border-gray-100">
|
|
<div class="p-5 border-b border-gray-100 bg-gray-50 flex justify-between items-center">
|
|
<h3 class="font-semibold text-gray-800">Clienți Activi</h3>
|
|
<span class="text-sm text-gray-500">{{ clients|length }} înregistrați</span>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-left border-collapse">
|
|
<thead>
|
|
<tr class="bg-gray-100/50 text-gray-600 uppercase text-xs tracking-wider">
|
|
<th class="p-4 border-b">ID (client_id)</th>
|
|
<th class="p-4 border-b">Nume Site</th>
|
|
<th class="p-4 border-b">Email Notificări</th>
|
|
<th class="p-4 border-b">Expirare</th>
|
|
<th class="p-4 border-b text-center">Acțiuni</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="text-gray-700">
|
|
{% for c in clients %}
|
|
<tr class="hover:bg-blue-50/50 transition border-b border-gray-100">
|
|
<td class="p-4"><code class="bg-blue-50 px-3 py-1 rounded-md text-blue-800 font-mono text-xs border border-blue-100">{{ c['id'] }}</code></td>
|
|
<td class="p-4 font-medium text-gray-900">{{ c['nume'] }}</td>
|
|
<td class="p-4 text-sm">{{ c['email_destinatie'] }}</td>
|
|
<td class="p-4 text-sm text-gray-600">{{ c['data_expirare'] }}</td>
|
|
<td class="p-4 text-center">
|
|
<a href="/admin/delete/{{ c['id'] }}" onclick="return confirm('Ești sigur că vrei să ștergi clientul {{ c['nume'] }}? Toate formularele lui vor înceta să funcționeze.')" class="text-red-500 hover:text-red-700 font-semibold text-sm bg-red-50 px-3 py-1 rounded-full hover:bg-red-100 transition">Şterge</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% if not clients %}
|
|
<div class="p-12 text-center text-gray-500 italic flex flex-col items-center gap-3">
|
|
<svg class="w-12 h-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 00-2 2H6a2 2 0 00-2 2v-5m16 0h-3m-3 0h-3m-3 0H4m0 0h3m-3 0h3m-3 0H4"></path></svg>
|
|
Nu există clienți înregistrați. Adaugă primul client folosind formularul de mai sus.
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<footer class="mt-12 text-center text-xs text-gray-400 p-4">
|
|
AquilaSoft Forms v1.0 | © 2026
|
|
</footer>
|
|
</main>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
if __name__ == '__main__':
|
|
init_db()
|
|
app.run(host='0.0.0.0', port=5000) |