diff --git a/.env_example b/.env_example index 46c3f50..bd139c9 100644 --- a/.env_example +++ b/.env_example @@ -5,5 +5,6 @@ location=localhost invalid_password=qwerty valid_password={{valid_password}} valid_username=clark.kent@dailyplanet.com +valid_name=Clark Kent valid_user_id=00000000 webhook_url=https://webhook.site diff --git a/app.py b/app.py index 6fe5f86..63bbdf7 100644 --- a/app.py +++ b/app.py @@ -51,6 +51,7 @@ def get_default_params(): "invalid_password": os.getenv("invalid_password"), "valid_password": os.getenv("valid_password"), "valid_username": os.getenv("valid_username"), + "valid_name": os.getenv("valid_name") or "Clark Kent", "webhook_url": os.getenv("webhook_url") } @@ -104,6 +105,54 @@ def demo(demo_name): return render_template(template, **params) +################################# +# Risk / Filter (registration) +################################# + +@app.route('/evaluate_signup', methods=['POST']) +def evaluate_signup(): + + name = request.json.get("name") + email = request.json["email"] + request_token = request.json["request_token"] + + castle_type = "$registration" + + # An email that's already taken (the known demo user) is a failed + # registration and goes to /filter; a fresh sign-up is risk-assessed. + if email == os.getenv("valid_username"): + castle_status = "$failed" + castle_api_endpoint = "filter" + else: + castle_status = "$succeeded" + castle_api_endpoint = "risk" + + payload_to_castle = { + 'type': castle_type, + 'status': castle_status, + 'user': { + 'id': os.getenv("valid_user_id"), + 'email': email, + 'name': name, + }, + 'request_token': request_token, + } + + castle = Client.from_request(request) + + if castle_api_endpoint == "risk": + verdict = castle.risk(payload_to_castle) + else: + verdict = castle.filter(payload_to_castle) + + return { + "api_endpoint": castle_api_endpoint, + "payload_to_castle": payload_to_castle, + "result": verdict, + "castle_type": castle_type, + "castle_status": castle_status, + }, 200, {'ContentType': 'application/json'} + ################################# # Risk / Filter (login) ################################# @@ -173,6 +222,44 @@ def evaluate_login(): return r, 200, {'ContentType':'application/json'} +################################# +# Risk (profile update) +################################# + +@app.route('/evaluate_profile_update', methods=['POST']) +def evaluate_profile_update(): + + name = request.json.get("name") + email = request.json.get("email") or os.getenv("valid_username") + request_token = request.json["request_token"] + + castle_type = "$profile_update" + castle_status = "$succeeded" + + payload_to_castle = { + 'type': castle_type, + 'status': castle_status, + 'user': { + 'id': os.getenv("valid_user_id"), + 'email': email, + 'name': name, + 'registered_at': registered_at, + }, + 'request_token': request_token, + } + + # A profile change is a sensitive action, so evaluate it with /risk. + castle = Client.from_request(request) + verdict = castle.risk(payload_to_castle) + + return { + "api_endpoint": "risk", + "payload_to_castle": payload_to_castle, + "result": verdict, + "castle_type": castle_type, + "castle_status": castle_status, + }, 200, {'ContentType': 'application/json'} + ################################# # Log (password reset) ################################# @@ -218,6 +305,39 @@ def evaluate_new_password(): return r, 200, {'ContentType':'application/json'} +################################# +# Log (logout) +################################# + +@app.route('/evaluate_logout', methods=['POST']) +def evaluate_logout(): + + request_token = request.json["request_token"] + + castle_type = "$logout" + castle_status = "$succeeded" + + payload_to_castle = { + 'type': castle_type, + 'status': castle_status, + 'user': { + 'id': os.getenv("valid_user_id"), + 'email': os.getenv("valid_username"), + }, + 'request_token': request_token, + } + + # Logout is recorded with the non-blocking log endpoint as well. + castle = Client.from_request(request) + castle.log(payload_to_castle) + + return { + "api_endpoint": "log", + "payload_to_castle": payload_to_castle, + "castle_type": castle_type, + "castle_status": castle_status, + }, 200, {'ContentType': 'application/json'} + ################################# # Lists API ################################# @@ -283,51 +403,3 @@ def privacy_user_data(): "result": result, }, 200, {'ContentType': 'application/json'} -################################# -# Events API -################################# - -@app.route('/events_schema', methods=['POST']) -def events_schema(): - - castle = castle_client() - - try: - result = castle.events_schema() - except CastleError as error: - result = {"error": str(error)} - - return { - "api_endpoint": "events/schema", - "payload_to_castle": {}, - "result": result, - }, 200, {'ContentType': 'application/json'} - -@app.route('/query_events', methods=['POST']) -def query_events(): - - print(request.json) - - payload = { - 'filters': [ - { - 'field': request.json.get('field') or 'name', - 'op': request.json.get('op') or '$eq', - 'value': request.json.get('value') or '$login', - } - ], - 'sort': {'field': 'created_at', 'order': 'desc'}, - } - - castle = castle_client() - - try: - result = castle.query_events(payload) - except CastleError as error: - result = {"error": str(error)} - - return { - "api_endpoint": "events/query", - "payload_to_castle": payload, - "result": result, - }, 200, {'ContentType': 'application/json'} diff --git a/demo_config.py b/demo_config.py index a8b2506..1a8524a 100644 --- a/demo_config.py +++ b/demo_config.py @@ -4,11 +4,19 @@ ################################# demos = { + "signup": { + "friendly_name": "sign up", + "blurb": "Evaluate a registration ($registration) with the risk endpoint." + }, "login": { "friendly_name": "login", "blurb": "Evaluate a login with the risk and filter endpoints.", "wsd": "https://www.websequencediagrams.com/files/render?link=Q9WYp8rNThVZhA1inf2FSLfjChYZTdHXyGB9zqvMNpsaAvKvJPARgo5LI5fM5K4D" }, + "account": { + "friendly_name": "account", + "blurb": "Update your profile, send a custom event, and log out." + }, "password_reset": { "friendly_name": "password reset", "blurb": "Record a password-reset event with the non-blocking log endpoint." @@ -20,10 +28,6 @@ "privacy": { "friendly_name": "privacy", "blurb": "Request or delete a user's data with the Privacy API." - }, - "events": { - "friendly_name": "events", - "blurb": "Inspect your event schema and query events." } } diff --git a/readme.md b/readme.md index 024681c..44c20cd 100644 --- a/readme.md +++ b/readme.md @@ -4,11 +4,16 @@ This project demonstrates key components of several essential Castle workflows. ## What's demonstrated -- **login** – `risk` (successful login) and `filter` (failed login) endpoints -- **password reset** – the non-blocking `log` endpoint +The app walks through a full user lifecycle. Every action mints a fresh Castle +request token in the browser (`Castle.createRequestToken()`) and forwards it to +the backend. + +- **sign up** – `$registration` to `risk` (a new email) or `filter` (an email that already exists) +- **login** – `$login` to `risk` (successful) or `filter` (failed) +- **account** – post-login actions: profile update (`$profile_update` to `risk`), a custom event (`Castle.custom()`), and logout (`$logout` via the non-blocking `log` endpoint) +- **password reset** – `$password_reset` via the non-blocking `log` endpoint - **lists** – the Lists API (`create_list`, `get_all_lists`) - **privacy** – the Privacy API (`request_user_data`, `delete_user_data`) -- **events** – the Events API (`events_schema`, `query_events`) ## Screenshots diff --git a/templates/account.html b/templates/account.html new file mode 100644 index 0000000..8f9ddc8 --- /dev/null +++ b/templates/account.html @@ -0,0 +1,76 @@ +{% extends 'demo.html' %} + +{% block ui %} + +
Signed in as {{ valid_username }}. These actions run once a user is authenticated.
The post-login actions each mint a fresh request token from castle.js:
$profile_update sent to /risk.Castle.custom() in the browser.$logout via the non-blocking /log endpoint.The Events API (added in castle 7.1) lets you inspect your event schema (/events/schema) and query recorded events (/events/query).
A valid Castle API secret is required for these calls to succeed.
- -{% endblock %} - -{% block scripts %} - -{% endblock %} diff --git a/templates/login.html b/templates/login.html index ecfb525..cdea33f 100644 --- a/templates/login.html +++ b/templates/login.html @@ -20,6 +20,10 @@A registration is a sensitive action, so it is risk-assessed:
+$registration / $succeeded sent to /risk; act on the verdict (allow, challenge, deny).$registration / $failed sent to /filter.