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}}
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
PORT=4006
# 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 Node SDK.
castle_api_secret=
8 changes: 2 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,8 @@ COPY . .
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
15 changes: 15 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

require('dotenv').config({ quiet: true });

// 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.
const demoDefaults = {
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 (const [key, value] of Object.entries(demoDefaults)) {
if (!process.env[key]) process.env[key] = value;
}

const fs = require('fs');
const path = require('path');
const express = require('express');
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.
10 changes: 6 additions & 4 deletions react/src/index.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 @@ -47,7 +47,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);
}

.btn {
Expand Down Expand Up @@ -82,14 +82,16 @@ pre.json {
@apply border-success/40 bg-success/10;
}
.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
28 changes: 14 additions & 14 deletions react/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,26 @@ export default {
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'],
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Consolas', 'monospace'],
},
borderRadius: { xl: '14px', lg: '9px' },
boxShadow: { card: '0 10px 30px rgba(0, 0, 0, 0.35)' },
boxShadow: { card: '0 1px 3px rgba(16, 24, 40, 0.06), 0 8px 24px rgba(16, 24, 40, 0.06)' },
},
},
plugins: [],
Expand Down
104 changes: 26 additions & 78 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
# Castle demo application: Node

This project demonstrates key components of several essential Castle workflows. It is built in Node.js on Express and uses the [Castle Node SDK](https://github.com/castle/castle-node) (3.0).
This project demonstrates key Castle workflows in a small Node.js / Express app
built on the [Castle Node SDK](https://github.com/castle/castle-node) (3.0).

## What's demonstrated

The app walks through a full user lifecycle. Every request 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.

Server-rendered (Pug) pages:
Server-rendered pages:

- **sign up** – `$registration` to `risk` (a new email) or `filter` (an email that already exists)
- **login** – `$login` to `risk` (successful) or `filter` (failed), with the verdict (allow / challenge / deny), risk score and signals surfaced in the UI
- **password reset** – `$password_reset` via the non-blocking `log` endpoint
- **lists** – the Lists API (`createList`, `fetchAllLists`)
- **privacy** – the Privacy API (`requestUserData`, `deleteUserData`)

Post-login React `/account` page (see [React integration](#react-integration)):
Post-login `/account` page:

- **profile update** – `$profile_update` to `risk`
- **custom event** – `Castle.custom()` (only available once signed in)
Expand All @@ -28,41 +29,25 @@ Post-login React `/account` page (see [React integration](#react-integration)):
| ---- | ----- |
| ![Home](docs/screenshots/home.png) | ![Login](docs/screenshots/login.png) |

## React integration

The post-login `/account` page is a **React + Vite + TypeScript** app
([`react/`](react/)) served by Express. It integrates `@castleio/castle-js`
through a `CastleProvider` / `useCastle()` hook (configured once) and mints a
request token for each action — profile update, custom event and logout. Build
it with `npm install --prefix react && npm run build --prefix react`; see
[`react/README.md`](react/README.md) for details.

## 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 Node.js app. The Castle Node SDK 3.0 requires **Node.js 20 or newer**.

Clone the repo and change into it:
The Castle Node SDK 3.0 requires **Node.js 20 or newer**.

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

Install the dependencies. This also installs the browser SDK
(`@castleio/castle-js`), which is served at runtime straight from
`node_modules` (at `/vendor/castle-js/...`), so there's no file to copy or
commit:

```bash
npm install
```

Expand All @@ -71,7 +56,7 @@ npm install
> (`castleio-sdk-3.0.0.tgz`). Once `3.0` is on npm, change the dependency to
> `"@castleio/sdk": "^3.0.0"` and delete the tarball.

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 @@ -84,69 +69,32 @@ npm start
# Castle Node demo listening on http://localhost:4006
```

For development with auto-reload:

```bash
npm run dev
```

## 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 `npm start` and the Docker
image work without a build step.

If you change the templates (`views/`) or `src/tailwind.css`, regenerate the
stylesheet:

```bash
npm run build:css # one-off, minified build
npm run watch:css # rebuild on change during development
```

## Running the tests

The app is covered by a Jest + Supertest suite (no network access or API secret
needed):

```bash
npm test
```

It includes three layers:

- **route tests** (`test/app.test.js`) — the endpoint logic with the Castle
client stubbed (e.g. login routing to `risk` vs `filter`).
- **SDK integration tests** (`test/sdk-integration.test.js`) — the *real* Castle
SDK driven through its `overrideFetch` hook, asserting the request URL,
method, auth header and JSON body, plus response parsing, error mapping and
failover behaviour.
- **front-end tests** (`test/frontend.test.js`) — the verdict banner rendering,
run against `static/app.js` in jsdom.
For development with auto-reload, use `npm run dev`.

## Running with Docker

The bundled `Dockerfile` builds from local source and serves the app on port 80.

Build the image:

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

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 4006:80 \
-e castle_pk=YOUR_PUBLISHABLE_KEY \
-e castle_api_secret=YOUR_API_SECRET \
-e valid_password=YOUR_VALID_PASSWORD \
castle-demo-node
```

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

## Running the tests

```bash
npm test
```

## 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.

Loading
Loading