patx/relay-lang
(examples): add Relay URL shortener with templates, CSRF, and rate limiting
Commit 1ef7ca4 · patx · 2026-02-12T22:46:35-05:00
Comments
No comments yet.
Diff
diff --git a/examples/templates/url_shortener_400.html b/examples/templates/url_shortener_400.html
new file mode 100644
index 0000000..f5581f8
--- /dev/null
+++ b/examples/templates/url_shortener_400.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>400 Bad Request</title>
+ <style>
+ :root {
+ color-scheme: dark;
+ --bg: #0d1117;
+ --card: #161b22;
+ --border: #30363d;
+ --text: #c9d1d9;
+ --muted: #8b949e;
+ }
+
+ * { box-sizing: border-box; }
+
+ body {
+ margin: 0;
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+ background: radial-gradient(circle at top left, #1f2733 0%, var(--bg) 45%);
+ color: var(--text);
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
+ padding: 24px;
+ }
+
+ main {
+ width: 100%;
+ max-width: 640px;
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 20px;
+ }
+
+ h1 { margin: 0 0 8px; }
+ p { color: var(--muted); margin: 6px 0; }
+ a { color: #9ecbff; text-decoration: none; }
+ code {
+ background: #0f141b;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 1px 5px;
+ }
+ </style>
+</head>
+<body>
+ <main>
+ <h1>400 Bad Request</h1>
+ <p>The submitted URL was invalid.</p>
+ <p>URLs must start with <code>http://</code> or <code>https://</code>.</p>
+ <p><a href="/">Back to shortener</a></p>
+ </main>
+</body>
+</html>
diff --git a/examples/templates/url_shortener_404.html b/examples/templates/url_shortener_404.html
new file mode 100644
index 0000000..d369ebd
--- /dev/null
+++ b/examples/templates/url_shortener_404.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>404 Not Found</title>
+ <style>
+ :root {
+ color-scheme: dark;
+ --bg: #0d1117;
+ --card: #161b22;
+ --border: #30363d;
+ --text: #c9d1d9;
+ --muted: #8b949e;
+ }
+
+ * { box-sizing: border-box; }
+
+ body {
+ margin: 0;
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+ background: radial-gradient(circle at top left, #1f2733 0%, var(--bg) 45%);
+ color: var(--text);
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
+ padding: 24px;
+ }
+
+ main {
+ width: 100%;
+ max-width: 640px;
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 20px;
+ }
+
+ h1 { margin: 0 0 8px; }
+ p { color: var(--muted); margin: 6px 0; }
+ a { color: #9ecbff; text-decoration: none; }
+ </style>
+</head>
+<body>
+ <main>
+ <h1>404 Not Found</h1>
+ <p>The requested short URL does not exist.</p>
+ <p><a href="/">Back to shortener</a></p>
+ </main>
+</body>
+</html>
diff --git a/examples/templates/url_shortener_index.html b/examples/templates/url_shortener_index.html
new file mode 100644
index 0000000..86166f1
--- /dev/null
+++ b/examples/templates/url_shortener_index.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Relay URL Shortener</title>
+ <style>
+ :root {
+ color-scheme: dark;
+ --bg: #0d1117;
+ --card: #161b22;
+ --border: #30363d;
+ --text: #c9d1d9;
+ --muted: #8b949e;
+ --accent: #58a6ff;
+ }
+
+ * { box-sizing: border-box; }
+
+ body {
+ margin: 0;
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+ background: radial-gradient(circle at top left, #1f2733 0%, var(--bg) 45%);
+ color: var(--text);
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
+ padding: 24px;
+ }
+
+ main {
+ width: 100%;
+ max-width: 680px;
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 20px;
+ }
+
+ h1 {
+ margin: 0 0 10px;
+ font-size: 1.2rem;
+ }
+
+ p {
+ margin: 0 0 16px;
+ color: var(--muted);
+ font-size: 0.92rem;
+ line-height: 1.4;
+ }
+
+ form {
+ display: flex;
+ gap: 10px;
+ }
+
+ input {
+ flex: 1;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: #0f141b;
+ color: var(--text);
+ padding: 10px 12px;
+ font: inherit;
+ }
+
+ button {
+ border: 1px solid #2f81f7;
+ background: var(--accent);
+ color: #0d1117;
+ border-radius: 8px;
+ font: inherit;
+ font-weight: 700;
+ padding: 10px 14px;
+ cursor: pointer;
+ }
+
+ code {
+ background: #0f141b;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 2px 6px;
+ color: #9ecbff;
+ }
+
+ @media (max-width: 600px) {
+ form { flex-direction: column; }
+ button { width: 100%; }
+ }
+ </style>
+</head>
+<body>
+ <main>
+ <h1>Relay URL Shortener</h1>
+ <p>Paste a URL to create a short link.</p>
+ <form method="post" action="/">
+ <input name="url" type="url" placeholder="https://example.com/some/long/path" required />
+ <input name="csrf_token" type="hidden" value="{{ csrf_token }}" />
+ <button type="submit">Shorten</button>
+ </form>
+ <p style="margin-top: 14px;">
+ API: <code>POST /api/v1/shorten</code> with JSON <code>{"url":"https://example.com"}</code>
+ </p>
+ </main>
+</body>
+</html>
diff --git a/examples/templates/url_shortener_stats.html b/examples/templates/url_shortener_stats.html
new file mode 100644
index 0000000..5561c1c
--- /dev/null
+++ b/examples/templates/url_shortener_stats.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Short URL Stats</title>
+ <style>
+ :root {
+ color-scheme: dark;
+ --bg: #0d1117;
+ --card: #161b22;
+ --border: #30363d;
+ --text: #c9d1d9;
+ --muted: #8b949e;
+ --accent: #58a6ff;
+ }
+
+ * { box-sizing: border-box; }
+
+ body {
+ margin: 0;
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+ background: radial-gradient(circle at top left, #1f2733 0%, var(--bg) 45%);
+ color: var(--text);
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
+ padding: 24px;
+ }
+
+ main {
+ width: 100%;
+ max-width: 760px;
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 20px;
+ }
+
+ h1 { margin-top: 0; font-size: 1.2rem; }
+ p { color: var(--muted); }
+
+ .item {
+ margin: 14px 0;
+ padding: 10px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: #0f141b;
+ overflow-wrap: anywhere;
+ }
+
+ .label {
+ font-size: 0.82rem;
+ color: var(--muted);
+ margin-bottom: 4px;
+ }
+
+ .value {
+ color: var(--text);
+ text-decoration: none;
+ }
+
+ a.value { color: #9ecbff; }
+
+ .clicks {
+ font-size: 1.1rem;
+ font-weight: 700;
+ color: #79c0ff;
+ }
+
+ .actions {
+ margin-top: 18px;
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+ }
+
+ .btn {
+ border: 1px solid #2f81f7;
+ background: var(--accent);
+ color: #0d1117;
+ text-decoration: none;
+ border-radius: 8px;
+ font-weight: 700;
+ padding: 10px 12px;
+ display: inline-block;
+ }
+
+ .btn.secondary {
+ background: transparent;
+ color: var(--text);
+ border-color: var(--border);
+ }
+ </style>
+</head>
+<body>
+ <main>
+ <h1>Link Stats</h1>
+ <p>Usage details for this short URL.</p>
+
+ <div class="item">
+ <div class="label">Short URL</div>
+ <a class="value" href="{{ short_url }}">{{ short_url }}</a>
+ </div>
+
+ <div class="item">
+ <div class="label">Destination URL</div>
+ <a class="value" href="{{ url }}">{{ url }}</a>
+ </div>
+
+ <div class="item">
+ <div class="label">Clicks</div>
+ <span class="clicks">{{ clicks }}</span>
+ </div>
+
+ <div class="actions">
+ <a class="btn" href="/">Shorten Another</a>
+ <a class="btn secondary" href="{{ short_url }}">Open Short URL</a>
+ </div>
+ </main>
+</body>
+</html>
diff --git a/examples/templates/url_shortener_success.html b/examples/templates/url_shortener_success.html
new file mode 100644
index 0000000..302e329
--- /dev/null
+++ b/examples/templates/url_shortener_success.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Short URL Created</title>
+ <style>
+ :root {
+ color-scheme: dark;
+ --bg: #0d1117;
+ --card: #161b22;
+ --border: #30363d;
+ --text: #c9d1d9;
+ --muted: #8b949e;
+ --accent: #58a6ff;
+ }
+
+ * { box-sizing: border-box; }
+
+ body {
+ margin: 0;
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+ background: radial-gradient(circle at top left, #1f2733 0%, var(--bg) 45%);
+ color: var(--text);
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
+ padding: 24px;
+ }
+
+ main {
+ width: 100%;
+ max-width: 760px;
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 20px;
+ }
+
+ h1 { margin-top: 0; font-size: 1.2rem; }
+ p { color: var(--muted); }
+
+ .item {
+ margin: 14px 0;
+ padding: 10px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: #0f141b;
+ overflow-wrap: anywhere;
+ }
+
+ .label {
+ font-size: 0.82rem;
+ color: var(--muted);
+ margin-bottom: 4px;
+ }
+
+ .value {
+ color: var(--text);
+ text-decoration: none;
+ }
+
+ a.value { color: #9ecbff; }
+
+ .actions {
+ margin-top: 18px;
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+ }
+
+ .btn {
+ border: 1px solid #2f81f7;
+ background: var(--accent);
+ color: #0d1117;
+ text-decoration: none;
+ border-radius: 8px;
+ font-weight: 700;
+ padding: 10px 12px;
+ display: inline-block;
+ }
+
+ .btn.secondary {
+ background: transparent;
+ color: var(--text);
+ border-color: var(--border);
+ }
+ </style>
+</head>
+<body>
+ <main>
+ <h1>Short URL Created</h1>
+ <p>Your link is ready.</p>
+
+ <div class="item">
+ <div class="label">Short URL</div>
+ <a class="value" href="{{ short_url }}">{{ short_url }}</a>
+ </div>
+
+ <div class="item">
+ <div class="label">Stats Page</div>
+ <a class="value" href="{{ stats_url }}">{{ stats_url }}</a>
+ </div>
+
+ <div class="item">
+ <div class="label">Destination URL</div>
+ <span class="value">{{ url }}</span>
+ </div>
+
+ <div class="actions">
+ <a class="btn" href="/">Shorten Another</a>
+ <a class="btn secondary" href="{{ short_url }}">Test Redirect</a>
+ </div>
+ </main>
+</body>
+</html>
diff --git a/examples/url_shortener.ry b/examples/url_shortener.ry
new file mode 100644
index 0000000..6f5d6a7
--- /dev/null
+++ b/examples/url_shortener.ry
@@ -0,0 +1,178 @@
+app = WebApp()
+server = WebServer()
+
+mongo = Mongo("mongodb://localhost:27017")
+db = mongo.db("shorty")
+urls = db.collection("urls")
+
+base_url = "http://127.0.0.1:8080"
+
+// Simple per-session abuse guard for write endpoints.
+fn rate_limit(ctx, next)
+ method = ctx["method"]
+ path = ctx["path"]
+ if (method == "POST" && (path == "/" || path == "/api/v1/shorten"))
+ count = session["shorten_count"]
+ if (count == None)
+ count = 0
+
+ if (count >= 50)
+ return HTTPError(429, "rate_limited", "Too many shorten requests in this session")
+
+ session["shorten_count"] = count + 1
+
+ return next()
+
+// CSRF protection for browser form posts; API route is exempt.
+fn csrf_protect(ctx, next)
+ method = ctx["method"]
+ path = ctx["path"]
+
+ if (method == "POST" && path == "/")
+ form = ctx["form"]
+ token = form["csrf_token"]
+ if (token == None || token != session["csrf_token"])
+ return HTTPError(403, "csrf_failed", "Invalid CSRF token")
+
+ return next()
+
+app.use(rate_limit)
+app.use(csrf_protect)
+
+fn is_http_url(url)
+ if (url == None)
+ return False
+
+ if (len(url) < 7)
+ return False
+
+ is_http = (
+ url[0] == "h" &&
+ url[1] == "t" &&
+ url[2] == "t" &&
+ url[3] == "p" &&
+ url[4] == ":" &&
+ url[5] == "/" &&
+ url[6] == "/"
+ )
+
+ if (is_http == True)
+ return True
+
+ if (len(url) < 8)
+ return False
+
+ return (
+ url[0] == "h" &&
+ url[1] == "t" &&
+ url[2] == "t" &&
+ url[3] == "p" &&
+ url[4] == "s" &&
+ url[5] == ":" &&
+ url[6] == "/" &&
+ url[7] == "/"
+ )
+
[email protected]("/")
+fn index()
+ if (session["csrf_token"] == None)
+ session["csrf_token"] = request["request_id"]
+
+ return app.render_template(
+ "examples/templates/url_shortener_index.html",
+ csrf_token=session["csrf_token"]
+ )
+
[email protected]("/")
+fn create_short(url = None)
+ if (is_http_url(url) == False)
+ return Response(
+ app.render_template("examples/templates/url_shortener_400.html"),
+ status=400,
+ content_type="text/html"
+ )
+
+ result = urls.insert_one({
+ "url": url,
+ "clicks": 0
+ })
+
+ short_id = str(result["inserted_id"])
+ short_url = base_url + "/" + short_id
+ stats_url = base_url + "/stats/" + short_id
+
+ return app.render_template(
+ "examples/templates/url_shortener_success.html",
+ short_url=short_url,
+ stats_url=stats_url,
+ url=url
+ )
+
[email protected]("/<short_id>")
+fn redirect_short(short_id)
+ doc = urls.find_one({"_id": short_id})
+ if (doc == None)
+ return Response(
+ app.render_template("examples/templates/url_shortener_404.html"),
+ status=404,
+ content_type="text/html"
+ )
+
+ urls.update_one({"_id": short_id}, {"$inc": {"clicks": 1}})
+ return app.redirect(doc["url"])
+
[email protected]("/stats/<short_id>")
+fn stats_page(short_id)
+ doc = urls.find_one({"_id": short_id})
+ if (doc == None)
+ return Response(
+ app.render_template("examples/templates/url_shortener_404.html"),
+ status=404,
+ content_type="text/html"
+ )
+
+ short_url = base_url + "/" + short_id
+ url = doc["url"]
+ clicks = doc["clicks"]
+
+ return app.render_template(
+ "examples/templates/url_shortener_stats.html",
+ short_url=short_url,
+ url=url,
+ clicks=clicks
+ )
+
[email protected]("/api/v1/shorten")
+fn api_shorten(url = None)
+ if (is_http_url(url) == False)
+ return HTTPError(400, "invalid_url", "URL must start with http:// or https://")
+
+ result = urls.insert_one({
+ "url": url,
+ "clicks": 0
+ })
+
+ short_id = str(result["inserted_id"])
+ return {
+ "status": "success",
+ "long_url": url,
+ "short_id": short_id,
+ "short_url": base_url + "/" + short_id,
+ "stats_url": base_url + "/api/v1/stats/" + short_id
+ }
+
[email protected]("/api/v1/stats/<short_id>")
+fn api_stats(short_id)
+ doc = urls.find_one({"_id": short_id})
+ if (doc == None)
+ return HTTPError(404, "not_found", "Short URL not found")
+
+ return {
+ "status": "success",
+ "short_id": short_id,
+ "short_url": base_url + "/" + short_id,
+ "long_url": doc["url"],
+ "clicks": doc["clicks"]
+ }
+
+server.run(app)