Compare commits

..

3 Commits

Author SHA1 Message Date
Thies Mueller cb2c95b542 rewrite 2026-06-19 21:29:24 +02:00
Thies Mueller a25a5efd9b new requirements 2026-06-19 21:29:17 +02:00
Thies Mueller 181824b460 remove resultsapi 2026-06-19 21:29:11 +02:00
3 changed files with 121 additions and 265 deletions
+118 -256
View File
@@ -1,316 +1,178 @@
import time import time
import jwt import jwt
import json
import httpx import httpx
import asyncio
import configparser import configparser
from flask import Flask, request, jsonify from flask import Flask, request, jsonify, abort
from flask_sqlalchemy import SQLAlchemy from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, sessionmaker
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read("config.ini") config.read("config.ini")
REGISTER_AUTH = config.get("AUTH", "REGISTER_AUTH", fallback=None) REGISTER_AUTH = config["AUTH"]["REGISTER_AUTH"]
MANAGE_AUTH = config.get("AUTH", "MANAGE_AUTH", fallback=None) MANAGE_AUTH = config["AUTH"]["MANAGE_AUTH"]
SQLALCHEMY_DATABASE_URI = config.get( DATABASE_URI = config["DATABASE"]["SQLALCHEMY_DATABASE_URI"]
"DATABASE",
"SQLALCHEMY_DATABASE_URI",
fallback="sqlite:///device_tokens.db"
)
auth_key_path = config.get( APNS_KEY_PATH = config["APNS"]["auth_key_path"]
"APNS", APNS_KEY_ID = config["APNS"]["auth_key_id"]
"auth_key_path", APNS_TEAM_ID = config["APNS"]["team_id"]
fallback="./APNSAuthKey.p8" APNS_TOPIC = config["APNS"]["topic"]
)
auth_key_id = config.get("APNS", "auth_key_id", fallback=None) APNS_URL = "https://api.push.apple.com/3/device/"
team_id = config.get("APNS", "team_id", fallback=None)
topic = config.get("APNS", "topic", fallback=None)
APNS_URL = "https://api.push.apple.com"
if not all([ Base = declarative_base()
REGISTER_AUTH, engine = create_engine(DATABASE_URI, echo=False)
MANAGE_AUTH, SessionLocal = sessionmaker(bind=engine)
auth_key_id,
team_id,
topic class DeviceToken(Base):
]): __tablename__ = "device_tokens"
raise ValueError("Fehlende Pflichtfelder in der Konfiguration!")
id = Column(Integer, primary_key=True)
token = Column(String, unique=True, nullable=False)
Base.metadata.create_all(engine)
app = Flask(__name__) app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
class DeviceToken(db.Model):
id = db.Column(db.Integer, primary_key=True)
device_token = db.Column(
db.String(256),
nullable=False,
unique=True
)
def authenticate(req, required_token):
auth_header = req.headers.get("Authorization")
return auth_header == f"Bearer {required_token}"
with open(auth_key_path, "r") as f:
APNS_PRIVATE_KEY = f.read()
def generate_apns_token(): def get_auth_header():
return jwt.encode( auth = request.headers.get("Authorization", "")
{ if not auth.startswith("Bearer "):
"iss": team_id, return None
"iat": int(time.time()) return auth.replace("Bearer ", "", 1)
},
APNS_PRIVATE_KEY,
def require_register_auth():
if get_auth_header() != REGISTER_AUTH:
abort(401)
def require_manage_auth():
if get_auth_header() != MANAGE_AUTH:
abort(401)
def load_apns_key():
with open(APNS_KEY_PATH, "r") as f:
return f.read()
def create_apns_jwt():
headers = {
"alg": "ES256",
"kid": APNS_KEY_ID
}
payload = {
"iss": APNS_TEAM_ID,
"iat": int(time.time())
}
token = jwt.encode(
payload,
load_apns_key(),
algorithm="ES256", algorithm="ES256",
headers={ headers=headers
"alg": "ES256",
"kid": auth_key_id
}
) )
return token
async def send_notification(
device_token,
title,
body
):
try:
jwt_token = generate_apns_token() def send_apns_notification(device_token: str, payload: dict):
jwt_token = create_apns_jwt()
payload = { headers = {
"aps": { "authorization": f"bearer {jwt_token}",
"alert": { "apns-topic": APNS_TOPIC,
"title": title, "apns-push-type": "alert"
"body": body }
},
"sound": "default",
"badge": 1
}
}
headers = { url = APNS_URL + device_token
"authorization": f"bearer {jwt_token}",
"apns-topic": topic,
"apns-push-type": "alert"
}
url = f"{APNS_URL}/3/device/{device_token}" with httpx.Client(http2=True) as client:
response = client.post(url, headers=headers, json=payload)
async with httpx.AsyncClient( return response.status_code, response.text
http2=True,
timeout=30.0
) as client:
response = await client.post(
url,
json=payload,
headers=headers
)
if response.status_code != 200:
print(
f"APNS Fehler "
f"{response.status_code} "
f"für {device_token}: "
f"{response.text}"
)
else:
print(
f"Push erfolgreich "
f"an {device_token}"
)
except Exception as e:
print(
f"Fehler beim Senden "
f"an {device_token}: {e}"
)
@app.route("/api/registerDeviceToken", methods=["POST"]) @app.route("/api/registerDeviceToken", methods=["POST"])
def register_device_token(): def register_device():
require_register_auth()
if not authenticate(request, REGISTER_AUTH): data = request.get_json()
return jsonify({ if not data or "device_token" not in data:
"error": "Unauthorized" return jsonify({"error": "device_token required"}), 400
}), 401
try: token = data["device_token"]
data = request.get_json() session = SessionLocal()
exists = session.query(DeviceToken).filter_by(token=token).first()
if not data or "device_token" not in data: if not exists:
return jsonify({ session.add(DeviceToken(token=token))
"error": "device_token fehlt" session.commit()
}), 400
device_token = data["device_token"] session.close()
existing = DeviceToken.query.filter_by( return jsonify({"status": "registered"})
device_token=device_token
).first()
if existing:
return jsonify({
"message": "Bereits registriert"
}), 200
db.session.add(
DeviceToken(device_token=device_token)
)
db.session.commit()
return jsonify({
"message": "Device Token registriert"
}), 200
except Exception as e:
db.session.rollback()
return jsonify({
"error": "Interner Serverfehler",
"details": str(e)
}), 500
@app.route("/api/notify", methods=["POST"]) @app.route("/api/notify", methods=["POST"])
def notify(): def notify():
require_manage_auth()
if not authenticate(request, MANAGE_AUTH): data = request.get_json()
return jsonify({
"error": "Unauthorized"
}), 401
try: if not data:
return jsonify({"error": "invalid json"}), 400
data = request.get_json() severity = data.get("severity")
message = data.get("notification")
severity = data.get("severity") if severity not in ["info", "warning", "urgent", "danger"]:
notification_text = data.get("notification") return jsonify({"error": "invalid severity"}), 400
if not severity or not notification_text: if not message:
return jsonify({ return jsonify({"error": "notification required"}), 400
"error": (
"severity und notification "
"sind erforderlich"
)
}), 400
severity_icons = { payload = {
"info": "", "aps": {
"warning": "⚠️ ", "alert": message,
"urgent": "❗️ ", "sound": "default"
"danger": "" },
} "severity": severity
}
if severity not in severity_icons: session = SessionLocal()
return jsonify({ tokens = session.query(DeviceToken).all()
"error": "Ungültige severity" session.close()
}), 400
title = ( results = []
f"{severity_icons[severity]}"
f"{notification_text}"
)
async def send_all(): for t in tokens:
status, resp = send_apns_notification(t.token, payload)
tasks = [] results.append({
"token": t.token,
for token in DeviceToken.query.all(): "status": status,
"response": resp
tasks.append( })
send_notification(
device_token=token.device_token,
title=title,
body=notification_text
)
)
await asyncio.gather(*tasks)
asyncio.run(send_all())
return jsonify({
"message": "Benachrichtigungen gesendet"
}), 200
except Exception as e:
return jsonify({
"error": "Interner Serverfehler",
"details": str(e)
}), 500
@app.route("/showdevicetokens", methods=["GET"])
def show_device_tokens():
tokens = DeviceToken.query.all()
return jsonify({ return jsonify({
"device_tokens": [ "sent": len(results),
token.device_token "results": results
for token in tokens
]
}) })
@app.route(
"/api/deleteAllDeviceTokens",
methods=["POST"]
)
def delete_all_device_tokens():
if not authenticate(request, MANAGE_AUTH):
return jsonify({
"error": "Unauthorized"
}), 401
try:
db.session.query(DeviceToken).delete()
db.session.commit()
return jsonify({
"message": "Alle Device Tokens gelöscht"
}), 200
except Exception as e:
db.session.rollback()
return jsonify({
"error": "Interner Serverfehler",
"details": str(e)
}), 500
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0", port=3000, debug=True)
with app.app_context():
db.create_all()
app.run(
host="0.0.0.0",
port=3000
)
-2
View File
@@ -11,5 +11,3 @@ auth_key_id=dein_auth_key_id
team_id=dein_team_id team_id=dein_team_id
topic=dein_topic topic=dein_topic
[API]
resultsapi=https://foobar.regattatech.de/api/v1/results
+2 -6
View File
@@ -1,9 +1,5 @@
asyncio
apns2
configparser
flask flask
flask_sqlalchemy sqlalchemy
aiohttp httpx[http2]
pyjwt pyjwt
cryptography cryptography
httpx