init commit

This commit is contained in:
2026-03-04 09:52:43 +02:00
commit 88ced77b23
8 changed files with 331 additions and 0 deletions

10
Dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM python:3.12-slim
WORKDIR /app
# Instalăm dependențele
RUN pip install --no-cache-dir flask flask-cors apscheduler
# Copiem fișierele
COPY . .
# Expunem portul
EXPOSE 5000
# Rulăm aplicația
CMD ["python", "server.py"]

BIN
data/aquila_forms.db Normal file

Binary file not shown.

28
docker-compose.yml Normal file
View File

@@ -0,0 +1,28 @@
version: "3.9"
networks:
reverse-proxy:
external: true
volumes:
mailform_db:
mailform_assets:
services:
mailform-app:
build: .
container_name: mailform-aquila
restart: unless-stopped
environment:
- TZ=Europe/Bucharest
- VIRTUAL_HOST=mailform.northdanubesoft.eu
- VIRTUAL_PORT=5000
- LETSENCRYPT_HOST=mailform.northdanubesoft.eu
- LETSENCRYPT_EMAIL=macamete.robert@gmail.com
expose:
- "5000"
volumes:
- mailform_db:/app/data
- mailform_assets:/app/static
networks:
- reverse-proxy

Binary file not shown.

17
helpers/send_email.py Normal file
View File

@@ -0,0 +1,17 @@
import smtplib
# --- HELPER TRIMITE EMAIL (GMAIL) ---
def send_gmail(to_email, subject, body):
# Folosește App Password-ul tău de la Google aici
user = "macamete.robert@gmail.com"
pw = "advx yqlv jkaa czvr"
msg = f"Subject: {subject}\nContent-Type: text/html\n\n{body}"
try:
server = smtplib.SMTP_SSL('smtp.gmail.com', 465)
server.login(user, pw)
server.sendmail(user, to_email, msg)
server.quit()
return True
except Exception as e:
print(f"Eroare mail: {e}")
return False

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
Flask==3.1.3
APScheduler==3.11.2

274
server.py Normal file
View File

@@ -0,0 +1,274 @@
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)

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB