init commit
This commit is contained in:
10
Dockerfile
Normal file
10
Dockerfile
Normal 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
BIN
data/aquila_forms.db
Normal file
Binary file not shown.
28
docker-compose.yml
Normal file
28
docker-compose.yml
Normal 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
|
||||||
BIN
helpers/__pycache__/send_email.cpython-313.pyc
Normal file
BIN
helpers/__pycache__/send_email.cpython-313.pyc
Normal file
Binary file not shown.
17
helpers/send_email.py
Normal file
17
helpers/send_email.py
Normal 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
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Flask==3.1.3
|
||||||
|
APScheduler==3.11.2
|
||||||
274
server.py
Normal file
274
server.py
Normal 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
BIN
static/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
Reference in New Issue
Block a user