Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions server/core/hook_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from services import get_all_services
from core.reaction_executor import reaction_executor

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()
Expand All @@ -32,19 +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)

except Exception as e:
logger.exception(f"[check_hooks] Error AREA {area.id}: {e}")



logger.exception(f"[check_hooks] Error AREA {area.id}: {e}")
10 changes: 8 additions & 2 deletions server/models/area.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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)

Expand All @@ -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 = {}
Expand Down
4 changes: 2 additions & 2 deletions server/models/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def seed_services():
"name": "Spotify",
"description": "Spotify is a digital music platform where you can stream millions of songs and podcasts. It lets you create and share playlists for any mood or moment. Discover new artists and listen to your favorite albums anytime, anywhere. You can connect with friends and see what they’re listening to. Perfect for anyone who loves music and wants instant access to it.",
"image": image_to_binary("spotify"),
"auth_url": "https://accounts.spotify.com/authorize?client_id=a63f13819159493eb695b3c16785aa55&response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A8080%2Fspotify%2Fcallback&scope=user-read-currently-playing+user-read-playback-state&show_dialog=true"
"auth_url": "http://127.0.0.1:8080/spotify/login"
},
{
"name": "OpenWeather",
Expand All @@ -42,7 +42,7 @@ def seed_services():
"name": "GitHub",
"description": "GitHub is a platform where developers store and share their code online. It allows teams to collaborate on software projects from anywhere. You can track changes, review contributions, and manage updates easily. Developers use it to build, test, and improve their applications together. It’s the world’s largest community for open-source and coding projects.",
"image": image_to_binary("github"),
"auth_url": "https://github.com/login/oauth/authorize?client_id=Ov23liHHgFODT3eZrD2N&scope=repo+user",
"auth_url": "http://127.0.0.1:8080/git/login",
},
{
"name": "NASA",
Expand Down
3 changes: 2 additions & 1 deletion server/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class User(db.Model):
password_hash = db.Column(db.Text, nullable=False)
pictures = db.Column(db.LargeBinary(length=(2**24)-1), nullable=True)
expo_push_token = db.Column(db.String(255), nullable=True)
allow_notifications = db.Column(db.Boolean, default=False)

def set_password(self, password):
self.password_hash = generate_password_hash(password)
Expand All @@ -16,4 +17,4 @@ def check_password(self, password):
return check_password_hash(self.password_hash, password)

def to_dict(self):
return {"id": self.id, "email": self.email, "pictures": binascii.b2a_base64(self.pictures).decode('utf-8') if self.pictures else None}
return {"id": self.id, "email": self.email, "pictures": binascii.b2a_base64(self.pictures).decode('utf-8') if self.pictures else None, "allow_notifications": self.allow_notifications}
1 change: 0 additions & 1 deletion server/oui.txt

This file was deleted.

6 changes: 3 additions & 3 deletions server/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ APScheduler==3.10.4
flasgger
discord
cryptography
pytest
pytest-flask
pytest
pytest-flask
pytest-mock
exponent-server-sdk
exponent-server-sdk
4 changes: 3 additions & 1 deletion server/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
app.register_blueprint(github_service_bp)
app.register_blueprint(ip_manager_bp)
38 changes: 29 additions & 9 deletions server/routes/github_service.py
Original file line number Diff line number Diff line change
@@ -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")

Expand All @@ -15,11 +17,25 @@ 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")

if frontend == "mobile":
encoded_ip = request.args.get("ip", None)
port = request.args.get("port", None)

if encoded_ip:
ip = decode_ip(encoded_ip)
if not ip:
return jsonify({"error": "Invalid or tampered 'ip' parameter"}), 400


scope = "repo user"
state = f"frontend:{frontend}"
state = json.dumps({
"frontend": frontend,
"ip": ip if frontend == "mobile" else None,
"port": port if frontend == "mobile" else None
})

params = {
"client_id": CLIENT_ID,
Expand All @@ -30,7 +46,6 @@ def github_login():
url = f"{AUTH_URL}?{urllib.parse.urlencode(params)}"
return redirect(url)

# --- 🟡 Étape 2 : callback GitHub après autorisation
@bp.route("/callback", methods=["GET"])
def github_callback():
"""
Expand All @@ -40,18 +55,23 @@ 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")
port = data.get("port")

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}:{port}"
return redirect(f"{mobile_redirect_uri}?code={code}")

# --- 🌐 Sinon : flow classique web
data = {
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
Expand All @@ -66,12 +86,12 @@ def github_callback():
if "access_token" not in tokens:
return jsonify({"error": "GitHub OAuth failed", "details": tokens}), 400

return redirect(f"http://localhost:8081/services?tokens={tokens}")
return jsonify({
"message": "GitHub connected successfully!",
"tokens": tokens
})

# --- 🔵 Étape 3 : échange du code (flow mobile)
@bp.route("/exchange_token", methods=["POST"])
def github_exchange_token():
"""
Expand Down
67 changes: 67 additions & 0 deletions server/routes/ip_manager.py
Original file line number Diff line number Diff line change
@@ -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": "<string>" }
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
7 changes: 6 additions & 1 deletion server/routes/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,9 @@ def service_fetch_by_name(service_name):
), 200

except Exception as e:
return jsonify({"error": str(e)}), 500
return jsonify({"error": str(e)}), 500


@bp.route("/db")
def db():
return redirect("http://localhost:8082", code=302)
32 changes: 28 additions & 4 deletions server/routes/spotify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -15,8 +19,22 @@
@bp.route("/spotify/login", methods=["GET"])
def spotify_login():
frontend = request.args.get("frontend", "web")

if frontend == "mobile":
encoded_ip = request.args.get("ip", None)
port = request.args.get("port", "8081")

if encoded_ip:
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 if frontend == "mobile" else None,
"port": port if frontend == "mobile" else None
})

params = {
"client_id": CLIENT_ID,
Expand All @@ -34,15 +52,20 @@ 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")
port = data.get("port")

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}:{port}?code={code}"
print(f"Redirecting to mobile app: {mobile_redirect_uri}")
return redirect(mobile_redirect_uri)

Expand All @@ -59,6 +82,7 @@ def spotify_callback():
return jsonify({"error": res.text}), 400

tokens = res.json()
return redirect(f"http://localhost:8081/services?tokens={tokens}")
return jsonify({
"message": "Spotify connected successfully!",
"tokens": tokens
Expand Down
Loading