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
+112 -250
View File
@@ -1,316 +1,178 @@
import time
import jwt
import json
import httpx
import asyncio
import configparser
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask import Flask, request, jsonify, abort
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, sessionmaker
config = configparser.ConfigParser()
config.read("config.ini")
REGISTER_AUTH = config.get("AUTH", "REGISTER_AUTH", fallback=None)
MANAGE_AUTH = config.get("AUTH", "MANAGE_AUTH", fallback=None)
REGISTER_AUTH = config["AUTH"]["REGISTER_AUTH"]
MANAGE_AUTH = config["AUTH"]["MANAGE_AUTH"]
SQLALCHEMY_DATABASE_URI = config.get(
"DATABASE",
"SQLALCHEMY_DATABASE_URI",
fallback="sqlite:///device_tokens.db"
)
DATABASE_URI = config["DATABASE"]["SQLALCHEMY_DATABASE_URI"]
auth_key_path = config.get(
"APNS",
"auth_key_path",
fallback="./APNSAuthKey.p8"
)
APNS_KEY_PATH = config["APNS"]["auth_key_path"]
APNS_KEY_ID = config["APNS"]["auth_key_id"]
APNS_TEAM_ID = config["APNS"]["team_id"]
APNS_TOPIC = config["APNS"]["topic"]
auth_key_id = config.get("APNS", "auth_key_id", fallback=None)
team_id = config.get("APNS", "team_id", fallback=None)
topic = config.get("APNS", "topic", fallback=None)
APNS_URL = "https://api.push.apple.com/3/device/"
APNS_URL = "https://api.push.apple.com"
if not all([
REGISTER_AUTH,
MANAGE_AUTH,
auth_key_id,
team_id,
topic
]):
raise ValueError("Fehlende Pflichtfelder in der Konfiguration!")
Base = declarative_base()
engine = create_engine(DATABASE_URI, echo=False)
SessionLocal = sessionmaker(bind=engine)
class DeviceToken(Base):
__tablename__ = "device_tokens"
id = Column(Integer, primary_key=True)
token = Column(String, unique=True, nullable=False)
Base.metadata.create_all(engine)
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():
return jwt.encode(
{
"iss": team_id,
"iat": int(time.time())
},
APNS_PRIVATE_KEY,
algorithm="ES256",
headers={
def get_auth_header():
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
return None
return auth.replace("Bearer ", "", 1)
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": auth_key_id
"kid": APNS_KEY_ID
}
)
async def send_notification(
device_token,
title,
body
):
try:
jwt_token = generate_apns_token()
payload = {
"aps": {
"alert": {
"title": title,
"body": body
},
"sound": "default",
"badge": 1
}
"iss": APNS_TEAM_ID,
"iat": int(time.time())
}
headers = {
"authorization": f"bearer {jwt_token}",
"apns-topic": topic,
"apns-push-type": "alert"
}
url = f"{APNS_URL}/3/device/{device_token}"
async with httpx.AsyncClient(
http2=True,
timeout=30.0
) as client:
response = await client.post(
url,
json=payload,
token = jwt.encode(
payload,
load_apns_key(),
algorithm="ES256",
headers=headers
)
if response.status_code != 200:
return token
print(
f"APNS Fehler "
f"{response.status_code} "
f"für {device_token}: "
f"{response.text}"
)
else:
def send_apns_notification(device_token: str, payload: dict):
jwt_token = create_apns_jwt()
print(
f"Push erfolgreich "
f"an {device_token}"
)
headers = {
"authorization": f"bearer {jwt_token}",
"apns-topic": APNS_TOPIC,
"apns-push-type": "alert"
}
url = APNS_URL + device_token
with httpx.Client(http2=True) as client:
response = client.post(url, headers=headers, json=payload)
return response.status_code, response.text
except Exception as e:
print(
f"Fehler beim Senden "
f"an {device_token}: {e}"
)
@app.route("/api/registerDeviceToken", methods=["POST"])
def register_device_token():
if not authenticate(request, REGISTER_AUTH):
return jsonify({
"error": "Unauthorized"
}), 401
try:
def register_device():
require_register_auth()
data = request.get_json()
if not data or "device_token" not in data:
return jsonify({
"error": "device_token fehlt"
}), 400
return jsonify({"error": "device_token required"}), 400
device_token = data["device_token"]
token = data["device_token"]
existing = DeviceToken.query.filter_by(
device_token=device_token
).first()
session = SessionLocal()
exists = session.query(DeviceToken).filter_by(token=token).first()
if existing:
return jsonify({
"message": "Bereits registriert"
}), 200
if not exists:
session.add(DeviceToken(token=token))
session.commit()
db.session.add(
DeviceToken(device_token=device_token)
)
session.close()
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
return jsonify({"status": "registered"})
@app.route("/api/notify", methods=["POST"])
def notify():
if not authenticate(request, MANAGE_AUTH):
return jsonify({
"error": "Unauthorized"
}), 401
try:
require_manage_auth()
data = request.get_json()
if not data:
return jsonify({"error": "invalid json"}), 400
severity = data.get("severity")
notification_text = data.get("notification")
message = data.get("notification")
if not severity or not notification_text:
return jsonify({
"error": (
"severity und notification "
"sind erforderlich"
)
}), 400
if severity not in ["info", "warning", "urgent", "danger"]:
return jsonify({"error": "invalid severity"}), 400
severity_icons = {
"info": "",
"warning": "⚠️ ",
"urgent": "❗️ ",
"danger": ""
if not message:
return jsonify({"error": "notification required"}), 400
payload = {
"aps": {
"alert": message,
"sound": "default"
},
"severity": severity
}
if severity not in severity_icons:
return jsonify({
"error": "Ungültige severity"
}), 400
session = SessionLocal()
tokens = session.query(DeviceToken).all()
session.close()
title = (
f"{severity_icons[severity]}"
f"{notification_text}"
)
results = []
async def send_all():
tasks = []
for token in DeviceToken.query.all():
tasks.append(
send_notification(
device_token=token.device_token,
title=title,
body=notification_text
)
)
await asyncio.gather(*tasks)
asyncio.run(send_all())
for t in tokens:
status, resp = send_apns_notification(t.token, payload)
results.append({
"token": t.token,
"status": status,
"response": resp
})
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({
"device_tokens": [
token.device_token
for token in tokens
]
"sent": len(results),
"results": results
})
@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__":
with app.app_context():
db.create_all()
app.run(
host="0.0.0.0",
port=3000
)
app.run(host="0.0.0.0", port=3000, debug=True)
-2
View File
@@ -11,5 +11,3 @@ auth_key_id=dein_auth_key_id
team_id=dein_team_id
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_sqlalchemy
aiohttp
sqlalchemy
httpx[http2]
pyjwt
cryptography
httpx