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
18 changes: 8 additions & 10 deletions .env_example
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
castle_pk={{castle_pk}}
castle_api_secret={{castle_api_secret}}
FLASK_APP=app.py
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
# Copy this file to .env and fill in your Castle credentials.
# Grab them from the Castle dashboard: https://dashboard.castle.io (Settings -> API).

# Publishable key, used by the browser SDK to mint request tokens.
castle_pk=

# Server-side API secret, used by the Castle Python SDK.
castle_api_secret=
8 changes: 2 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,8 @@ COPY --from=frontend /app/node_modules ./node_modules
ENV location=docker
ENV PORT=80

# Non-secret demo defaults. Supply castle_pk, castle_api_secret and
# valid_password at runtime (e.g. docker run -e ...).
ENV invalid_password=qwerty
ENV valid_username=clark.kent@dailyplanet.com
ENV valid_user_id=00000000
ENV webhook_url=https://webhook.site
# Only the Castle credentials are needed at runtime (e.g. docker run -e ...);
# the simulated demo user values are baked in as code defaults.

EXPOSE 80

Expand Down
14 changes: 14 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@

load_dotenv()

# Demo fixture defaults. Only castle_pk and castle_api_secret need to be set in
# .env; the simulated "valid user" the demo logs in falls back to these values.
DEMO_DEFAULTS = {
"location": "localhost",
"valid_username": "clark.kent@dailyplanet.com",
"valid_name": "Clark Kent",
"valid_user_id": "00000000",
"valid_password": "1234",
"invalid_password": "qwerty",
"webhook_url": "https://webhook.site",
}
for _key, _value in DEMO_DEFAULTS.items():
os.environ.setdefault(_key, _value)

app = Flask(__name__)

# Serve the Castle browser SDK straight from the npm install (node_modules)
Expand Down
Binary file modified docs/screenshots/home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/login.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 25 additions & 50 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Castle demo application: Python

This project demonstrates key components of several essential Castle workflows. It is built in Python on Flask/gunicorn and uses the [castle](https://github.com/castle/castle-python) SDK (7.1).
This project demonstrates key Castle workflows in a Python / Flask app built on
the [castle](https://github.com/castle/castle-python) SDK (7.1).

## What's demonstrated

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.
the backend, which calls Castle and acts on the verdict.

- **sign up** – `$registration` to `risk` (a new email) or `filter` (an email that already exists)
- **login** – `$login` to `risk` (successful) or `filter` (failed)
Expand All @@ -23,45 +24,41 @@ the backend.

## Prerequisites

You'll need a Castle tenant to run this app against. If you don't already have one, you can start a free trial at https://castle.io.
You'll need a Castle account. If you don't have one, start a free trial at
https://castle.io. For local development, use a **sandbox** environment so demo
traffic from `localhost` stays separate from production data — from the Castle
dashboard (Settings → API) grab the sandbox keys:

From your Castle dashboard you'll need two values:
- your **publishable key** (`castle_pk`) – used by the browser SDK
- your **API secret** (`castle_api_secret`) – used by the backend SDK

- your **publishable key** (`pk`) – used by the browser SDK
- your **API secret** – used by the backend SDK
These are the only two values you need to configure.

## Running locally

This is a Python app. The castle 7.1 SDK requires **Python 3.9 or newer**; this demo is tested with Python 3.13.

Clone the repo and change into it:
The castle 7.1 SDK requires **Python 3.9 or newer** (tested with Python 3.13).

```bash
git clone https://github.com/castle/castle-python-example.git
cd castle-python-example
```

Create and activate a virtual environment:
Create and activate a virtual environment, then install dependencies:

```bash
python -m venv venv
. venv/bin/activate
```

Install the Python dependencies:

```bash
pip install -r requirements.txt
```

Install the Castle browser SDK from npm. It is served at runtime straight from
`node_modules` (at `/vendor/castle-js/...`), so there's no file to copy or commit:
The Castle browser SDK is served at runtime straight from `node_modules`, so
install it too:

```bash
npm install
```

Create your `.env` from the example and fill in your Castle publishable key (`castle_pk`), API secret (`castle_api_secret`) and a `valid_password`:
Create your `.env` from the example and fill in your two Castle keys:

```bash
cp .env_example .env
Expand All @@ -70,53 +67,31 @@ cp .env_example .env
Run the app:

```bash
flask run
# Running on http://127.0.0.1:5000/
```

The app also runs under gunicorn:

```bash
gunicorn app:app
flask --app app run --port 4007
# Running on http://127.0.0.1:4007
```

## Styling (Tailwind CSS)

The UI is styled with [Tailwind CSS](https://tailwindcss.com). The source lives in
`src/tailwind.css` (design tokens are configured in `tailwind.config.js`) and is
compiled to `static/styles.css`, which is committed so the app and the Docker
image work without a build step.

If you change the templates (`templates/`) or `src/tailwind.css`, regenerate the
stylesheet (`tailwindcss` is installed by `npm install`):

```bash
npm run build:css # one-off, minified build
npm run watch:css # rebuild on change during development
```
It also runs under gunicorn: `gunicorn app:app`.

## Running with Docker

The bundled `Dockerfile` builds from local source and serves the app with gunicorn on port 80. It uses a multi-stage build that runs `npm ci` to fetch the Castle browser SDK, so no pre-build steps are needed.

Build the image:
The bundled `Dockerfile` builds from local source and serves the app with
gunicorn on port 80.

```bash
docker build -t castle-demo-python .
```

Run a container. The non-secret demo values (`valid_username`, `valid_user_id`, etc.) are baked into the image, so you only need to pass your secrets:

```bash
docker run -d -p 4005:80 \
-e castle_pk=YOUR_PUBLISHABLE_KEY \
-e castle_api_secret=YOUR_API_SECRET \
-e valid_password=YOUR_VALID_PASSWORD \
castle-demo-python
```

The app will be available at http://127.0.0.1:4005.
The app will be available at http://127.0.0.1:4005. Point it at a Castle sandbox
environment when running locally.

## Disclaimer

I’m sharing this sample app with the hope that other developers find it valuable. Although it is not an officially supported sample, we welcome questions and suggestions at `support@castle.io`.
We're sharing this sample app in the hope that other developers find it
valuable. Although it is not an officially supported sample, we welcome
questions and suggestions at `support@castle.io`.
28 changes: 17 additions & 11 deletions src/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
@apply min-h-screen bg-bg font-sans text-[15px] leading-relaxed text-ink antialiased;
background-image: radial-gradient(
1200px 600px at 80% -10%,
rgba(124, 92, 255, 0.12),
rgba(54, 94, 237, 0.12),
transparent 60%
);
}
Expand Down Expand Up @@ -39,17 +39,21 @@

.navbar {
@apply sticky top-0 z-50 flex flex-wrap items-center gap-6 border-b border-border px-6 py-3.5;
background: rgba(13, 16, 22, 0.8);
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
}

.brand {
@apply flex items-center gap-2 text-[1.05rem] font-bold text-ink hover:no-underline;
}

.brand-dot {
@apply h-2.5 w-2.5 rounded-full bg-accent;
box-shadow: 0 0 12px #7c5cff;
.brand-logo {
@apply h-[1.4rem] w-[1.4rem] shrink-0 text-accent;
filter: drop-shadow(0 0 8px rgba(54, 94, 237, 0.35));
}

.brand-logo-lg {
@apply h-12 w-12;
}

.nav-links {
Expand Down Expand Up @@ -106,7 +110,7 @@

.input:focus {
@apply border-accent outline-none;
box-shadow: 0 0 0 3px rgba(124, 92, 255, 0.14);
box-shadow: 0 0 0 3px rgba(54, 94, 237, 0.14);
}

.checkbox {
Expand Down Expand Up @@ -166,15 +170,15 @@ pre.json {
}

.json .k {
color: #7ee0c8;
color: #0550ae;
}

.json .s {
color: #ffd479;
color: #0a7d33;
}

.json .n {
color: #84c1ff;
color: #b25000;
}

.json .b {
Expand Down Expand Up @@ -210,15 +214,17 @@ pre.json {
}

.verdict-allow .verdict-action {
@apply bg-success text-bg;
@apply bg-success;
color: #0b1020;
}

.verdict-challenge {
@apply border-challenge/40 bg-challenge/10;
}

.verdict-challenge .verdict-action {
@apply bg-challenge text-bg;
@apply bg-challenge;
color: #0b1020;
}

.verdict-deny {
Expand Down
2 changes: 1 addition & 1 deletion static/styles.css

Large diffs are not rendered by default.

28 changes: 14 additions & 14 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ module.exports = {
theme: {
extend: {
colors: {
bg: '#0b0e14',
'bg-soft': '#11151f',
surface: '#151a23',
'surface-2': '#1b2230',
border: '#232b39',
'border-soft': '#1c2330',
ink: '#e6e9ef',
muted: '#9aa4b2',
accent: '#7c5cff',
'accent-hover': '#6b4cf0',
success: '#2ecc71',
challenge: '#ffbf47',
danger: '#ff5c7c',
bg: '#f6f8fc',
'bg-soft': '#eef2f9',
surface: '#ffffff',
'surface-2': '#eef2fb',
border: '#dde3ee',
'border-soft': '#e9edf5',
ink: '#0f1729',
muted: '#5b6678',
accent: '#365eed',
'accent-hover': '#2a4ed1',
success: '#16a34a',
challenge: '#f59e0b',
danger: '#dc2626',
},
fontFamily: {
sans: ['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'],
Expand All @@ -29,7 +29,7 @@ module.exports = {
lg: '9px',
},
boxShadow: {
card: '0 10px 30px rgba(0, 0, 0, 0.35)',
card: '0 1px 3px rgba(16, 24, 40, 0.06), 0 8px 24px rgba(16, 24, 40, 0.06)',
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<body>

<nav class="navbar">
<a class="brand" href="/"><span class="brand-dot"></span> Castle <span class="text-muted" style="font-weight:400">demo</span></a>
<a class="brand" href="/"><svg class="brand-logo" viewBox="0 0 158 158" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M79 158c43.63 0 79-35.37 79-79S122.63 0 79 0 0 35.37 0 79s35.37 79 79 79ZM31 57h24v12h12V57h24v12h12V57h24v24c-6.627 0-12 5.373-12 12v12H43V93c0-6.627-5.373-12-12-12V57Z"/></svg> Castle <span class="text-muted" style="font-weight:400">demo</span></a>
<div class="nav-links">
{% for demo in demo_list %}
<a href="/{{ demo['url'] }}">{{ demo['friendly_name'] }}</a>
Expand Down
Loading