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
Binary file added 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 added 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.
1,632 changes: 1,630 additions & 2 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"description": "This project demonstrates key components of several essential Castle workflows, including login and reviewing a suspicious device. The application is built in Python on Flask/gunicorn.",
"main": "index.js",
"scripts": {
"build:css": "tailwindcss -i ./src/tailwind.css -o ./static/styles.css --minify",
"watch:css": "tailwindcss -i ./src/tailwind.css -o ./static/styles.css --watch",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
Expand All @@ -18,5 +20,8 @@
"homepage": "https://github.com/castle/castle-python-example#readme",
"dependencies": {
"@castleio/castle-js": "^2.8.4"
},
"devDependencies": {
"tailwindcss": "^3.4.19"
}
}
21 changes: 21 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ This project demonstrates key components of several essential Castle workflows.
- **privacy** – the Privacy API (`request_user_data`, `delete_user_data`)
- **events** – the Events API (`events_schema`, `query_events`)

## Screenshots

| Home | Login |
| ---- | ----- |
| ![Home](docs/screenshots/home.png) | ![Login](docs/screenshots/login.png) |

## 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.
Expand Down Expand Up @@ -69,6 +75,21 @@ The app also runs under gunicorn:
gunicorn app:app
```

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

## 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.
Expand Down
238 changes: 238 additions & 0 deletions src/tailwind.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
body {
@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),
transparent 60%
);
}

a {
@apply text-accent no-underline hover:underline;
}

h1,
h2,
h3,
h4 {
@apply font-semibold leading-tight;
}

p {
@apply mb-3;
}

code {
@apply rounded border border-border bg-surface-2 px-1.5 py-0.5 font-mono text-[0.86em];
}
}

/*
* Component classes. Authored outside @layer so they are always emitted even
* when the selector only appears in JS-generated markup (badge/json).
*/

.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);
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;
}

.nav-links {
@apply ml-auto flex flex-wrap items-center gap-5;
}

.nav-links a {
@apply text-[0.92rem] text-muted hover:text-ink hover:no-underline;
}

.tag {
@apply rounded-full border border-accent/40 bg-accent/10 px-2 py-0.5 text-xs font-semibold text-accent;
}

.container-page {
@apply mx-auto max-w-[1120px] px-6 pb-16 pt-8;
}

.card {
@apply rounded-xl border border-border bg-surface p-6 shadow-card;
}

.eyebrow {
@apply mb-1.5 text-xs font-bold uppercase tracking-wider text-muted;
}

.hero {
@apply px-4 py-12 text-center;
}

.feature {
@apply block rounded-xl border border-border bg-surface p-5 text-left transition hover:-translate-y-0.5 hover:border-accent hover:no-underline;
}

.feature h3 {
@apply mb-1 text-ink;
}

.feature p {
@apply m-0 text-sm text-muted;
}

.field {
@apply mb-3.5;
}

.field label {
@apply mb-1.5 block text-[0.82rem] font-semibold text-muted;
}

.input {
@apply w-full rounded-lg border border-border bg-bg-soft px-3 py-2.5 font-sans text-[0.95rem] text-ink transition;
}

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

.checkbox {
@apply mt-1 flex items-center gap-2 text-[0.85rem] text-muted;
}

.checkbox input {
@apply m-0 w-auto;
}

.form-links {
@apply mt-4 flex justify-between gap-4 text-[0.85rem];
}

.btn {
@apply cursor-pointer rounded-lg border border-border bg-surface-2 px-4 py-2.5 font-sans text-[0.92rem] font-semibold text-ink transition hover:border-accent active:translate-y-px;
}

.btn-primary {
@apply border-accent bg-accent text-white hover:bg-accent-hover;
}

.btn-ghost {
@apply bg-transparent;
}

.btn-row {
@apply mt-4 flex flex-wrap gap-2.5;
}

.meta-list {
@apply m-0 list-none p-0;
}

.meta-list li {
@apply flex justify-between gap-4 border-b border-border-soft py-2 text-[0.9rem] last:border-b-0;
}

.meta-list .k {
@apply text-muted;
}

.meta-list .v {
@apply break-all text-right font-mono text-ink;
}

.result-block {
@apply mt-4;
}

.result-block .label {
@apply mb-1.5 text-[0.78rem] font-bold uppercase tracking-wide text-muted;
}

pre.json {
@apply m-0 overflow-auto rounded-lg border border-border bg-bg-soft p-4 font-mono text-[0.85rem] leading-normal;
}

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

.json .s {
color: #ffd479;
}

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

.json .b {
@apply text-accent;
}

.json .z {
@apply text-muted;
}

.badge {
@apply inline-block rounded-full border border-border px-2 py-0.5 text-xs font-semibold;
}

.badge.endpoint {
@apply border-accent/40 bg-accent/10 font-mono text-accent;
}

.verdict {
@apply flex items-center gap-3.5 rounded-lg border border-border bg-surface-2 px-4 py-2.5;
}

.verdict-action {
@apply rounded-full px-2.5 py-1 text-[0.85rem] font-bold uppercase tracking-wider;
}

.verdict-score {
@apply text-[0.9rem] text-muted;
}

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

.verdict-allow .verdict-action {
@apply bg-success text-bg;
}

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

.verdict-challenge .verdict-action {
@apply bg-challenge text-bg;
}

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

.verdict-deny .verdict-action {
@apply bg-danger text-white;
}

.signals {
@apply mt-2.5 flex flex-wrap gap-1.5;
}

.signals .chip {
@apply rounded-full border border-border bg-bg-soft px-2 py-0.5 font-mono text-xs text-muted;
}
Loading
Loading