Initial commit: Flask app translating Discourse User API Key flow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
d8f5a7f77c
|
|
@ -0,0 +1 @@
|
|||
config.yaml
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["python", "app.py", "/config/config.yaml"]
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import secrets
|
||||
|
||||
import yaml
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
||||
from flask import Flask, redirect, render_template, request, session, url_for
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("config", help="Path to configuration file")
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.config) as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = config["secret_key"]
|
||||
|
||||
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html", application_name=config["application_name"])
|
||||
|
||||
|
||||
@app.route("/authorize")
|
||||
def authorize():
|
||||
public_key = private_key.public_key().public_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
).decode()
|
||||
|
||||
nonce = secrets.token_hex(16)
|
||||
client_id = secrets.token_hex(48)
|
||||
session["nonce"] = nonce
|
||||
session["client_id"] = client_id
|
||||
|
||||
params = {
|
||||
"auth_redirect": config["redirect_url"],
|
||||
"application_name": config["application_name"],
|
||||
"scopes": "read",
|
||||
"client_id": client_id,
|
||||
"nonce": nonce,
|
||||
"public_key": public_key,
|
||||
}
|
||||
query = "&".join(f"{k}={v}" for k, v in params.items())
|
||||
return redirect(f"{config['forum_url']}/user-api-key/new?{query}")
|
||||
|
||||
|
||||
@app.route("/callback")
|
||||
def callback():
|
||||
payload_b64 = request.args.get("payload")
|
||||
if not payload_b64:
|
||||
return "Missing payload", 400
|
||||
|
||||
try:
|
||||
payload = base64.b64decode(payload_b64)
|
||||
data = private_key.decrypt(payload, padding.PKCS1v15())
|
||||
response = json.loads(data)
|
||||
except Exception:
|
||||
return "Failed to decrypt or decode payload", 400
|
||||
|
||||
if response.get("nonce") != session.get("nonce"):
|
||||
return "Invalid nonce", 400
|
||||
|
||||
key = response["key"]
|
||||
calendar_url = f"{config['forum_url']}/discourse-post-event/events.ics?order=desc&api_key={key}"
|
||||
|
||||
return render_template(
|
||||
"result.html",
|
||||
application_name=config["application_name"],
|
||||
calendar_url=calendar_url,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
forum_url: "https://forum.example.com"
|
||||
redirect_url: "https://myapp.example.com/callback"
|
||||
application_name: "My Calendar App"
|
||||
secret_key: "change-me-to-a-long-random-string"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
flask
|
||||
cryptography
|
||||
pyyaml
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ application_name }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ application_name }}</h1>
|
||||
<p>Click the button below to authorize access and generate your personal calendar link.</p>
|
||||
<a href="{{ url_for('authorize') }}"><button>Generate Calendar Link</button></a>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ application_name }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ application_name }}</h1>
|
||||
<p>Your calendar URL — save it, it won't be displayed again:</p>
|
||||
<pre><a href="{{ calendar_url }}">{{ calendar_url }}</a></pre>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue