Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env_example
Original file line number Diff line number Diff line change
Expand Up @@ -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
168 changes: 120 additions & 48 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down Expand Up @@ -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)
#################################
Expand Down Expand Up @@ -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)
#################################
Expand Down Expand Up @@ -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
#################################
Expand Down Expand Up @@ -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'}
12 changes: 8 additions & 4 deletions demo_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand All @@ -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."
}
}

Expand Down
11 changes: 8 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
76 changes: 76 additions & 0 deletions templates/account.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{% extends 'demo.html' %}

{% block ui %}

<p class="text-muted" style="margin-top:0;">Signed in as <code>{{ valid_username }}</code>. These actions run once a user is authenticated.</p>

<div style="margin-top:1.2rem;">
<div class="field">
<label for="name">name</label>
<input class="input" type="text" id="name" value="{{ valid_name }}">
</div>
<div class="field">
<label for="email">email</label>
<input class="input" type="text" id="email" value="{{ valid_username }}">
</div>
<div class="btn-row">
<button class="btn btn-primary" onclick="updateProfile()">Save changes</button>
<button class="btn btn-ghost" onclick="sendCustomEvent()">Send a custom event</button>
<button class="btn" onclick="logout()">Log out</button>
</div>
</div>

{% endblock %}

{% block desc %}

<p>The post-login actions each mint a fresh request token from <code>castle.js</code>:</p>
<ol class="list-decimal pl-5 space-y-1">
<li><strong>profile update</strong> &rarr; <code>$profile_update</code> sent to <code>/risk</code>.</li>
<li><strong>custom event</strong> &rarr; <code>Castle.custom()</code> in the browser.</li>
<li><strong>logout</strong> &rarr; <code>$logout</code> via the non-blocking <code>/log</code> endpoint.</li>
</ol>

{% endblock %}

{% block scripts %}
<script>
const VALID_USER = "{{ valid_username }}";
const VALID_USER_ID = "{{ valid_user_id }}";

function updateProfile() {
withRequestToken(function (requestToken) {
postJSON("/evaluate_profile_update", {
name: document.getElementById("name").value,
email: document.getElementById("email").value,
request_token: requestToken,
}).then(renderCastleResponse);
});
}

function sendCustomEvent() {
if (window.Castle && typeof Castle.custom === "function") {
try {
Castle.custom({
name: "$custom",
user: { id: VALID_USER_ID || VALID_USER, email: VALID_USER },
properties: { source: "account-page" },
});
} catch (e) {
console.error("Castle.custom failed", e);
}
}
clearResults();
addJSONBlock("Custom event sent (browser)", { name: "$custom", user: { email: VALID_USER } });
showResultsCard();
}

function logout() {
withRequestToken(function (requestToken) {
postJSON("/evaluate_logout", {
request_token: requestToken,
}).then(renderCastleResponse);
});
}
</script>
{% endblock %}
50 changes: 0 additions & 50 deletions templates/events.html

This file was deleted.

Loading
Loading