wow
This commit is contained in:
commit
794dce869c
16 changed files with 5430 additions and 0 deletions
146
app.py
Normal file
146
app.py
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
import jwt
|
||||
from flask import Flask, redirect, render_template, request, url_for
|
||||
|
||||
|
||||
JWT_SECRET = os.environ.get("PASSPORT_JWT_SECRET")
|
||||
if not JWT_SECRET:
|
||||
raise RuntimeError("PASSPORT_JWT_SECRET environment variable is required")
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def parse_datetime(value):
|
||||
parsed = datetime.fromisoformat(value)
|
||||
if parsed.tzinfo is None:
|
||||
parsed = parsed.astimezone()
|
||||
return parsed
|
||||
|
||||
|
||||
def format_timestamp(timestamp):
|
||||
return datetime.fromtimestamp(timestamp).astimezone().strftime("%Y.%m.%d %H:%M")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def index():
|
||||
return render_template("index.html", error=None, values={})
|
||||
|
||||
|
||||
@app.post("/issue")
|
||||
def issue_pass():
|
||||
values = {
|
||||
"holder": request.form.get("holder", "").strip(),
|
||||
"location": request.form.get("location", "").strip(),
|
||||
"valid_from": request.form.get("valid_from", "").strip(),
|
||||
"valid_until": request.form.get("valid_until", "").strip(),
|
||||
}
|
||||
|
||||
if not values["holder"] or not values["location"]:
|
||||
return render_template(
|
||||
"index.html", error="Holder and location are required.", values=values
|
||||
), 400
|
||||
|
||||
if len(values["holder"]) > 80 or len(values["location"]) > 120:
|
||||
return render_template(
|
||||
"index.html", error="Holder or location is too long.", values=values
|
||||
), 400
|
||||
|
||||
try:
|
||||
valid_from = parse_datetime(values["valid_from"])
|
||||
valid_until = parse_datetime(values["valid_until"])
|
||||
except (TypeError, ValueError):
|
||||
return render_template(
|
||||
"index.html", error="Enter valid start and end times.", values=values
|
||||
), 400
|
||||
|
||||
if valid_from >= valid_until:
|
||||
return render_template(
|
||||
"index.html", error="Valid From must be earlier than Valid Until.", values=values
|
||||
), 400
|
||||
|
||||
issued_at = int(datetime.now().astimezone().timestamp())
|
||||
payload = {
|
||||
"iss": "passport",
|
||||
"purpose": "access-pass",
|
||||
"holder": values["holder"],
|
||||
"location": values["location"],
|
||||
"iat": issued_at,
|
||||
"nbf": int(valid_from.timestamp()),
|
||||
"exp": int(valid_until.timestamp()),
|
||||
"jti": uuid.uuid4().hex,
|
||||
}
|
||||
token = jwt.encode(payload, JWT_SECRET, algorithm="HS256")
|
||||
return redirect(url_for("show_pass", token=token))
|
||||
|
||||
|
||||
@app.get("/pass/<token>")
|
||||
def show_pass(token):
|
||||
if len(token) > 4096:
|
||||
return render_template("invalid.html"), 400
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
JWT_SECRET,
|
||||
algorithms=["HS256"],
|
||||
issuer="passport",
|
||||
options={
|
||||
"require": ["iss", "purpose", "holder", "location", "iat", "nbf", "exp"],
|
||||
"verify_exp": False,
|
||||
"verify_nbf": False,
|
||||
},
|
||||
)
|
||||
|
||||
if payload.get("purpose") != "access-pass":
|
||||
raise jwt.InvalidTokenError("Invalid purpose")
|
||||
if not isinstance(payload.get("holder"), str) or not payload["holder"].strip():
|
||||
raise jwt.InvalidTokenError("Invalid holder")
|
||||
if not isinstance(payload.get("location"), str) or not payload["location"].strip():
|
||||
raise jwt.InvalidTokenError("Invalid location")
|
||||
if len(payload["holder"]) > 80 or len(payload["location"]) > 120:
|
||||
raise jwt.InvalidTokenError("Claim is too long")
|
||||
|
||||
if any(
|
||||
isinstance(payload[name], bool) or not isinstance(payload[name], (int, float))
|
||||
for name in ("iat", "nbf", "exp")
|
||||
):
|
||||
raise jwt.InvalidTokenError("Invalid timestamp")
|
||||
|
||||
nbf = int(payload["nbf"])
|
||||
exp = int(payload["exp"])
|
||||
if nbf >= exp:
|
||||
raise jwt.InvalidTokenError("Invalid validity period")
|
||||
|
||||
valid_from_text = format_timestamp(nbf)
|
||||
valid_until_text = format_timestamp(exp)
|
||||
except (jwt.InvalidTokenError, TypeError, ValueError, OverflowError, OSError):
|
||||
return render_template("invalid.html"), 400
|
||||
|
||||
now = int(datetime.now().astimezone().timestamp())
|
||||
if now < nbf:
|
||||
status = "NOT ACTIVE"
|
||||
status_class = "pending"
|
||||
elif now >= exp:
|
||||
status = "EXPIRED"
|
||||
status_class = "expired"
|
||||
else:
|
||||
status = "VALID"
|
||||
status_class = "valid"
|
||||
|
||||
credential_id = str(payload.get("jti", "SIGNED-CREDENTIAL"))[:12].upper()
|
||||
return render_template(
|
||||
"pass.html",
|
||||
pass_data=payload,
|
||||
status=status,
|
||||
status_class=status_class,
|
||||
valid_from=valid_from_text,
|
||||
valid_until=valid_until_text,
|
||||
credential_id=credential_id,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
Loading…
Reference in a new issue