This commit is contained in:
Thies Mueller
2026-06-19 21:29:24 +02:00
parent a25a5efd9b
commit cb2c95b542
+112 -250
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,
algorithm="ES256", def require_register_auth():
headers={ 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", "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 = { payload = {
"aps": { "iss": APNS_TEAM_ID,
"alert": { "iat": int(time.time())
"title": title,
"body": body
},
"sound": "default",
"badge": 1
}
} }
headers = { token = jwt.encode(
"authorization": f"bearer {jwt_token}", payload,
"apns-topic": topic, load_apns_key(),
"apns-push-type": "alert" algorithm="ES256",
}
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,
headers=headers 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( headers = {
f"Push erfolgreich " "authorization": f"bearer {jwt_token}",
f"an {device_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"]) @app.route("/api/registerDeviceToken", methods=["POST"])
def register_device_token(): def register_device():
require_register_auth()
if not authenticate(request, REGISTER_AUTH):
return jsonify({
"error": "Unauthorized"
}), 401
try:
data = request.get_json() data = request.get_json()
if not data or "device_token" not in data: if not data or "device_token" not in data:
return jsonify({ return jsonify({"error": "device_token required"}), 400
"error": "device_token fehlt"
}), 400
device_token = data["device_token"] token = data["device_token"]
existing = DeviceToken.query.filter_by( session = SessionLocal()
device_token=device_token exists = session.query(DeviceToken).filter_by(token=token).first()
).first()
if existing: if not exists:
return jsonify({ session.add(DeviceToken(token=token))
"message": "Bereits registriert" session.commit()
}), 200
db.session.add( session.close()
DeviceToken(device_token=device_token)
)
db.session.commit() return jsonify({"status": "registered"})
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):
return jsonify({
"error": "Unauthorized"
}), 401
try:
data = request.get_json() data = request.get_json()
if not data:
return jsonify({"error": "invalid json"}), 400
severity = data.get("severity") severity = data.get("severity")
notification_text = data.get("notification") message = data.get("notification")
if not severity or not notification_text: if severity not in ["info", "warning", "urgent", "danger"]:
return jsonify({ return jsonify({"error": "invalid severity"}), 400
"error": (
"severity und notification "
"sind erforderlich"
)
}), 400
severity_icons = { if not message:
"info": "", return jsonify({"error": "notification required"}), 400
"warning": "⚠️ ",
"urgent": "❗️ ", payload = {
"danger": "" "aps": {
"alert": message,
"sound": "default"
},
"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({ return jsonify({
"message": "Benachrichtigungen gesendet" "sent": len(results),
}), 200 "results": results
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
]
}) })
@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
)