diff --git a/server/core/hook_engine.py b/server/core/hook_engine.py index 42c4172..9699ad7 100644 --- a/server/core/hook_engine.py +++ b/server/core/hook_engine.py @@ -5,10 +5,8 @@ from models.area import Area from services import get_all_services from core.reaction_executor import reaction_executor -from services.notification_service import send_push_notification - -logger = logging.getLogger("AREA-App") +logger = logging.getLogger("AREA-API") def check_hooks(app=None): ctx = app.app_context() if app else current_app.app_context() @@ -34,22 +32,18 @@ def check_hooks(app=None): last_run = area.last_run.replace(tzinfo=timezone.utc) if (now - last_run) < timedelta(seconds=area.frequency): continue - + params = area.params or {} data = act_srv.check_action(user, area.action, params=params) if data: logger.info(f"[check_hooks] AREA {area.id} triggered") + logger.info(params) + area.set_params(params) area.last_run = now db.session.commit() - + reaction_executor(user, rea_srv, area.reaction, params=params, data=data) - titre = "Workflows from area is running" - description = area.name - send_push_notification(user, description, titre) except Exception as e: - logger.exception(f"[check_hooks] Error AREA {area.id}: {e}") - - - + logger.exception(f"[check_hooks] Error AREA {area.id}: {e}") \ No newline at end of file diff --git a/server/models/area.py b/server/models/area.py index 233672f..24399bd 100644 --- a/server/models/area.py +++ b/server/models/area.py @@ -1,5 +1,6 @@ from extensions import db from sqlalchemy.dialects.postgresql import JSON +from sqlalchemy.ext.mutable import MutableDict from utils.crypto_manager import crypto class Area(db.Model): @@ -14,7 +15,7 @@ class Area(db.Model): reaction = db.Column(db.String(80), nullable=False) frequency = db.Column(db.Integer, default=3600) last_run = db.Column(db.DateTime, default=None) - params = db.Column(db.JSON, nullable=True) + params = db.Column(MutableDict.as_mutable(db.JSON), nullable=True) enabled = db.Column(db.Boolean, default=True) public = db.Column(db.Boolean, default=False) @@ -25,12 +26,17 @@ def set_params(self, params: dict): safe = {} for k, v in (params or {}).items(): - if "password" in k.lower(): + if isinstance(v, dict): + safe[k] = {sub_k: crypto.encrypt(sub_v) if "password" in sub_k.lower() else sub_v + for sub_k, sub_v in v.items()} + elif "password" in k.lower(): safe[k] = crypto.encrypt(v) else: safe[k] = v + self.params = safe + def get_params(self) -> dict: safe = {} diff --git a/server/oui.txt b/server/oui.txt deleted file mode 100644 index 6d669fd..0000000 --- a/server/oui.txt +++ /dev/null @@ -1 +0,0 @@ -https://avowedly-uncomputed-velvet.ngrok-free.dev/git/callback diff --git a/server/requirements.txt b/server/requirements.txt deleted file mode 100644 index 01eda9f..0000000 --- a/server/requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -Flask==3.0.3 -Flask-SQLAlchemy==3.1.1 -flask_migrate==4.0.7 -flask_jwt_extended==4.6.0 -pymysql==1.1.1 -python-dotenv==1.0.0 -requests==2.31.0 -flask-cors==3.0.10 -APScheduler==3.10.4 -flasgger -discord -cryptography -pytest -pytest-flask -pytest-mock -exponent-server-sdk diff --git a/server/routes/__init__.py b/server/routes/__init__.py index 11638cc..deabedb 100644 --- a/server/routes/__init__.py +++ b/server/routes/__init__.py @@ -6,6 +6,7 @@ from .service import bp as service_bp from .spotify import bp as spotify_bp from .github_service import bp as github_service_bp +from .ip_manager import bp as ip_manager_bp def register_routes(app): app.register_blueprint(users_bp) @@ -15,4 +16,5 @@ def register_routes(app): app.register_blueprint(area_bp) app.register_blueprint(json_bp) app.register_blueprint(spotify_bp) - app.register_blueprint(github_service_bp) \ No newline at end of file + app.register_blueprint(github_service_bp) + app.register_blueprint(ip_manager_bp) \ No newline at end of file diff --git a/server/routes/github_service.py b/server/routes/github_service.py index c85bbf6..98dc491 100644 --- a/server/routes/github_service.py +++ b/server/routes/github_service.py @@ -1,5 +1,7 @@ from flask import Blueprint, jsonify, redirect, request import requests, urllib.parse, os +from .ip_manager import decode_ip +import json bp = Blueprint("github_service", __name__, url_prefix="/git") @@ -15,11 +17,20 @@ def github_login(): """ Redirige l'utilisateur vers GitHub pour autorisation OAuth """ - # with open("oui.txt", "w") as f: - # print(REDIRECT_URI, file=f) + frontend = request.args.get("frontend", "web") + encoded_ip = request.args.get("ip", None) + + if not encoded_ip: + return jsonify({"error": "Missing 'ip' parameter"}), 400 + + ip = decode_ip(encoded_ip) + scope = "repo user" - state = f"frontend:{frontend}" + state = json.dumps({ + "frontend": frontend, + "ip": ip + }) params = { "client_id": CLIENT_ID, @@ -39,15 +50,20 @@ def github_callback(): return jsonify({"error": request.args["error"]}), 400 code = request.args.get("code") - state = request.args.get("state", "frontend:web") + state_str = request.args.get("state", '{"frontend":"web"}') + + clean = state_str.replace('+', '') + data = json.loads(clean) + + frontend = data.get("frontend") + ip = data.get("ip") if not code: return jsonify({"error": "Missing authorization code"}), 400 - frontend = state.split(":")[1] if frontend == "mobile": - mobile_redirect_uri = "exp://10.18.208.5:8081" + mobile_redirect_uri = f"exp://{ip}:8083" return redirect(f"{mobile_redirect_uri}?code={code}") data = { diff --git a/server/routes/ip_manager.py b/server/routes/ip_manager.py new file mode 100644 index 0000000..857dd88 --- /dev/null +++ b/server/routes/ip_manager.py @@ -0,0 +1,67 @@ +from flask import request, jsonify, Blueprint +from itsdangerous import URLSafeSerializer, BadSignature +import os +import re +import ipaddress + +bp = Blueprint("ip_manager", __name__, url_prefix="/ip") + +SECRET_KEY = os.getenv("IP_SECRET_KEY", "default_secret_key") +SALT = os.getenv("IP_SALT", "ip_salt") + +serializer = URLSafeSerializer(SECRET_KEY, salt=SALT) + +def extract_ip(exp_url: str) -> str | None: + """ + Extrait l'IP d'une URL Expo (exp://IP:PORT) + """ + pattern = r"exp://([\d\.]+):\d+" + match = re.match(pattern, exp_url) + if match: + return match.group(1) + return None + +def validate_ip(ip_str): + try: + ipaddress.ip_address(ip_str) + return True + except ValueError: + return False + + + +def decode_ip(ip_encoded): + """ + Attendu JSON: { "encoded": "" } + Réponse JSON: { "ip": "1.2.3.4" } ou erreur 400/401 + """ + data = ip_encoded + if not ip_encoded: + return None + + try: + ip = serializer.loads(data) + except BadSignature: + return None + except Exception: + return None + + if not validate_ip(ip): + return None + + return ip + +@bp.route("/encode", methods=["POST"]) +def encode_ip(): + data = request.get_json(force=True, silent=True) + if not data or "ip" not in data: + return jsonify({"error": "Missing 'ip' in request body"}), 400 + + url = str(data["ip"]).strip() + ip = extract_ip(url) + with open("oui.txt", "w") as f: + print(f"Encoding IP: {ip}", file=f) + if not validate_ip(ip): + return jsonify({"error": "Invalid IP address format"}), 400 + token = serializer.dumps(ip) + return jsonify({"encoded": token}), 200 \ No newline at end of file diff --git a/server/routes/spotify.py b/server/routes/spotify.py index ef001fb..2292e38 100644 --- a/server/routes/spotify.py +++ b/server/routes/spotify.py @@ -3,6 +3,10 @@ import urllib.parse import os from datetime import datetime +from .ip_manager import decode_ip +import json +import re + bp = Blueprint("spotify_auth", __name__) @@ -15,8 +19,19 @@ @bp.route("/spotify/login", methods=["GET"]) def spotify_login(): frontend = request.args.get("frontend", "web") + encoded_ip = request.args.get("ip", None) + + if not encoded_ip: + return jsonify({"error": "Missing 'ip' parameter"}), 400 + + ip = decode_ip(encoded_ip) + if not ip: + return jsonify({"error": "Invalid or tampered 'ip' parameter"}), 400 scope = "user-read-currently-playing user-read-playback-state" - state = f"frontend:{frontend}" + state = json.dumps({ + "frontend": frontend, + "ip": ip + }) params = { "client_id": CLIENT_ID, @@ -34,15 +49,19 @@ def spotify_login(): @bp.route("/spotify/callback") def spotify_callback(): code = request.args.get("code") - state = request.args.get("state", "frontend:web") + state_str = request.args.get("state", '{"frontend":"web"}') - frontend = state.split(":")[1] + clean = state_str.replace('+', '') + data = json.loads(clean) + + frontend = data.get("frontend") + ip = data.get("ip") if not code: return jsonify({"error": "Missing authorization code"}), 400 if frontend == "mobile": - mobile_redirect_uri = f"exp://10.18.208.5:8081?code={code}" + mobile_redirect_uri = f"exp://{ip}:8083?code={code}" print(f"Redirecting to mobile app: {mobile_redirect_uri}") return redirect(mobile_redirect_uri) diff --git a/server/services/spotify_service.py b/server/services/spotify_service.py index 8ec2ee6..0d0efc2 100644 --- a/server/services/spotify_service.py +++ b/server/services/spotify_service.py @@ -2,7 +2,7 @@ import requests import re import os -from datetime import datetime +from datetime import datetime, timezone class SpotifyService(BaseService): @@ -42,67 +42,66 @@ def get_reactions(self): {"name": "previous", "description": "Retourne à la musique précédente"} ] - def get_reactions_params(self, reaction_name): - if reaction_name == "play": - return [ - {"name": "track_uri", "type": "string", "description": "URI de la piste à jouer"}, - {"name": "tokens", "type": "object", "description": "Tokens d'authentification de l'utilisateur (access_token et refresh_token)"}, - {"name": "refresh_token", "type": "string", "description": "Token de rafraîchissement de l'utilisateur"} - ] - return [ - {"name": "tokens", "type": "object", "description": "Tokens d'authentification de l'utilisateur (access_token et refresh_token)"}, - {"name": "refresh_token", "type": "string", "description": "Token de rafraîchissement de l'utilisateur"} - ] - def check_action(self, user, action, params=None): - tokens = params.get("tokens", {}) - access_token = tokens.get("access_token") - refresh_token = tokens.get("refresh_token") - - if not access_token or not refresh_token: - return None - - headers = {"Authorization": f"Bearer {access_token}"} - url = f"{self.api_base}/me/player/currently-playing" - - res = requests.get(url, headers=headers) - - if res.status_code == 401: - refreshed = self.refresh_token(refresh_token) - if not refreshed: + params = params or {} + access_token = params.get("access_token") + refresh_token = params.get("refresh_token") + + if not access_token or not refresh_token: + print("[Spotify] Tokens manquants") return None - - access_token = refreshed.get("access_token") - headers["Authorization"] = f"Bearer {access_token}" + + headers = {"Authorization": f"Bearer {access_token}"} + url = f"{self.api_base}/me/player/currently-playing" + res = requests.get(url, headers=headers) - - if res.status_code != 200: - print(f"[Spotify] Erreur API ({res.status_code}): {res.text}") - return None - data = res.json() - item = data.get("item") - if not item: - return None + if res.status_code == 401: + refreshed = self.refresh_token(refresh_token) + if not refreshed: + print("[Spotify] Refresh token échoué") + return None - track_name = item.get("name") - artist_name = item["artists"][0]["name"] if item.get("artists") else "Inconnu" - album_name = item["album"]["name"] - url = item["external_urls"]["spotify"] + access_token = refreshed.get("access_token") + headers["Authorization"] = f"Bearer {access_token}" + res = requests.get(url, headers=headers) - if action == "new_title": - last_title = params.get("last_title") - if last_title == track_name: + if res.status_code != 200: + print(f"[Spotify] Erreur API ({res.status_code}): {res.text}") return None - params["last_title"] = track_name - return { - "track": track_name, - "artist": artist_name, - "album": album_name, - "url": url - } + data = res.json() + item = data.get("item") + if not item: + return None + track_name = item.get("name") + artist_name = item["artists"][0]["name"] if item.get("artists") else "Inconnu" + album_name = item["album"]["name"] + track_url = item["external_urls"]["spotify"] + + if action == "currently_playing": + return { + "track": track_name, + "artist": artist_name, + "album": album_name, + "url": track_url + } + + elif action == "new_title": + last_title = params.get("last_title") + if last_title == track_name: + return None + params["last_title"] = track_name + params["last_updated"] = datetime.now(timezone.utc).isoformat() + return { + "track": track_name, + "artist": artist_name, + "album": album_name, + "url": track_url + } + + return None def execute_reaction(self, user, reaction, params=None, data=None): tokens = params.get("tokens", {}) @@ -154,5 +153,6 @@ def execute_reaction(self, user, reaction, params=None, data=None): return True - + def get_actions_params(self, action_name): + pass