Skip to content

Commit db2e7b9

Browse files
author
EL BADOURI Youssef
committed
added the notification and report features
1 parent 7cd227a commit db2e7b9

File tree

15 files changed

+1000
-207
lines changed

15 files changed

+1000
-207
lines changed

backend/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from routes.full_scan_routes import full_scan_bp
1818
from routes.t5_base import t5_base_bp
1919
from routes.admin import admin_bp
20+
from routes.report_routes import report_bp
2021

2122
# Load environment variables
2223
load_dotenv()
@@ -49,6 +50,7 @@
4950
app.register_blueprint(full_scan_bp, url_prefix="/")
5051
app.register_blueprint(t5_base_bp, url_prefix="/")
5152
app.register_blueprint(admin_bp, url_prefix="/")
53+
app.register_blueprint(report_bp, url_prefix="/")
5254

5355
if __name__ == "__main__" :
5456
app.run(debug=True, port=5000)

backend/models/notification.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from utils.db import db
2+
from datetime import datetime, timezone
3+
4+
class Notification(db.Model):
5+
__tablename__ = "notifications"
6+
7+
id = db.Column(db.Integer, primary_key=True)
8+
user_id = db.Column(db.Integer, nullable=False, index=True)
9+
scan_id = db.Column(db.Integer, nullable=False, index=True)
10+
repo = db.Column(db.String(255), nullable=False)
11+
status = db.Column(db.String(32), nullable=False) # SUCCESS / FAILED
12+
findings_count = db.Column(db.Integer, nullable=False, default=0)
13+
email = db.Column(db.String(255), nullable=False)
14+
subject = db.Column(db.String(255), nullable=False)
15+
sent = db.Column(db.Boolean, default=False)
16+
error_text = db.Column(db.Text, nullable=True)
17+
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
18+
sent_at = db.Column(db.DateTime, nullable=True)
19+
20+
__table_args__ = (
21+
db.UniqueConstraint('scan_id', 'user_id', name='uq_notifications_scan_user'),
22+
)

backend/models/user_preference.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from utils.db import db
2+
3+
class UserPreference(db.Model):
4+
__tablename__ = "user_preferences"
5+
6+
id = db.Column(db.Integer, primary_key=True)
7+
user_id = db.Column(db.Integer, unique=True, index=True, nullable=False)
8+
email_notifications_enabled = db.Column(db.Boolean, default=True)

backend/routes/notifications.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from flask import Blueprint, request, jsonify
2+
from flask_jwt_extended import jwt_required, get_jwt_identity
3+
from utils.db import db
4+
from models.notification import Notification
5+
from models.user_preference import UserPreference
6+
7+
bp_notifications = Blueprint("notifications", __name__, url_prefix="/api/notifications")
8+
9+
@bp_notifications.get("/")
10+
@jwt_required()
11+
def list_notifications():
12+
user_id = int(get_jwt_identity())
13+
rows = (Notification.query
14+
.filter_by(user_id=user_id)
15+
.order_by(Notification.created_at.desc())
16+
.limit(100).all())
17+
data = [{
18+
"id": r.id, "scan_id": r.scan_id, "repo": r.repo, "status": r.status,
19+
"findings_count": r.findings_count, "email": r.email, "subject": r.subject,
20+
"sent": r.sent, "error_text": r.error_text,
21+
"created_at": r.created_at.isoformat() if r.created_at else None,
22+
"sent_at": r.sent_at.isoformat() if r.sent_at else None,
23+
} for r in rows]
24+
return jsonify(data)
25+
26+
@bp_notifications.patch("/preferences/email")
27+
@jwt_required()
28+
def toggle_email_pref():
29+
user_id = int(get_jwt_identity())
30+
body = request.get_json(force=True) or {}
31+
enabled = bool(body.get("enabled", True))
32+
pref = UserPreference.query.filter_by(user_id=user_id).first()
33+
if not pref:
34+
pref = UserPreference(user_id=user_id, email_notifications_enabled=enabled)
35+
db.session.add(pref)
36+
else:
37+
pref.email_notifications_enabled = enabled
38+
db.session.commit()
39+
return jsonify({"email_notifications_enabled": pref.email_notifications_enabled})

backend/routes/report_routes.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from flask import Blueprint, send_file, request, jsonify
2+
from flask_jwt_extended import jwt_required, get_jwt_identity
3+
from utils.db import db
4+
from models.scan_history import ScanHistory
5+
from models.user import User
6+
from services.report_service import generate_csv_for_scan, send_csv_report_email
7+
8+
report_bp = Blueprint("reports", __name__)
9+
10+
@report_bp.route("/reports/<int:scan_id>/csv", methods=["GET"])
11+
@jwt_required()
12+
def download_report_csv(scan_id: int):
13+
user_id = int(get_jwt_identity())
14+
scan: ScanHistory = db.session.get(ScanHistory, scan_id)
15+
if not scan:
16+
return jsonify({"error": "scan not found"}), 404
17+
18+
user = db.session.get(User, user_id)
19+
if (scan.user_id != user_id) and (user.role != "admin"):
20+
return jsonify({"error": "forbidden"}), 403
21+
22+
try:
23+
csv_path, csv_filename = generate_csv_for_scan(scan_id)
24+
25+
if request.args.get("email", "false").lower() == "true":
26+
subject = f"SafeOps — Rapport CSV ({scan.scan_type})"
27+
executed_at = ((scan.scan_result or {}).get("meta", {}) or {}).get("executed_at", scan.created_at.isoformat() if getattr(scan, "created_at", None) else "")
28+
body = f"Bonjour {user.name},\n\nVeuillez trouver ci-joint le rapport CSV de votre scan.\nExécuté le : {executed_at}\n"
29+
send_csv_report_email(user.email, subject, body, csv_path, csv_filename)
30+
31+
return send_file(csv_path, mimetype="text/csv", as_attachment=True, download_name=csv_filename)
32+
except Exception as e:
33+
return jsonify({"error": str(e)}), 500

0 commit comments

Comments
 (0)