Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
e402156
middle
Yashb404 Feb 10, 2026
f7cb3e6
Rewrite project.rs
Yashb404 Feb 10, 2026
f973039
Enhance EmbedModal and ViewPage components to support VIP links and w…
Yashb404 Feb 10, 2026
c777433
Initial plan
Copilot Feb 11, 2026
9dca279
Update client/src/components/modal.rs
Yashb404 Feb 11, 2026
5d48617
Initial plan
Copilot Feb 11, 2026
373a5cb
Update client/src/pages/embed.rs
Yashb404 Feb 11, 2026
15b0a71
Initial plan
Copilot Feb 11, 2026
35b4365
Initial plan
Copilot Feb 11, 2026
de4acb3
Initial plan
Copilot Feb 11, 2026
7d337df
Initial plan
Copilot Feb 11, 2026
3c3c96d
Implement URL normalization for Referer header validation to prevent …
Copilot Feb 11, 2026
dbf9c7a
Initial plan
Copilot Feb 11, 2026
5f6574f
Update client/src/components/modal.rs
Yashb404 Feb 11, 2026
d870e3b
Improve error handling for whitelist add/remove operations
Copilot Feb 11, 2026
1f36148
Enhance URL normalization security: restrict to http/https, require v…
Copilot Feb 11, 2026
9c3bd6e
Fix resize divider memory leak by adding mounted signal guard
Copilot Feb 11, 2026
c361986
Fix race condition by setting mounted flag before scheduling callback
Copilot Feb 11, 2026
5f08671
Improve code readability by destructuring mounted signal
Copilot Feb 11, 2026
127525c
Generate embed_key at project creation time in publish_handler
Copilot Feb 11, 2026
beca6bd
Use console::error_1 instead of console::log_1 for error messages
Copilot Feb 11, 2026
ad406be
Add logging and improve error messages for URL validation
Copilot Feb 11, 2026
4e25442
Merge pull request #117 from TryCli-Studio/copilot/sub-pr-116
Yashb404 Feb 11, 2026
f32a538
Add VIP key query parameter support to embed pages
Copilot Feb 11, 2026
f8cd379
Merge pull request #118 from TryCli-Studio/copilot/sub-pr-116-again
Yashb404 Feb 11, 2026
fb51be2
Merge pull request #119 from TryCli-Studio/copilot/sub-pr-116-another…
Yashb404 Feb 11, 2026
350dd58
Show helpful loading message when VIP link is unavailable
Copilot Feb 11, 2026
b87746a
Merge pull request #120 from TryCli-Studio/copilot/sub-pr-116-yet-again
Yashb404 Feb 11, 2026
3e820db
Run cargo fmt to fix formatting
Copilot Feb 11, 2026
23f1ad6
Add rate limiting and max entries limit for whitelist endpoints
Copilot Feb 11, 2026
a36cd10
Initial plan
Copilot Feb 11, 2026
26b847f
Initial plan
Copilot Feb 11, 2026
7ba4492
Update server/migrations/20260210000000_secure_embed_schema.up.sql
Yashb404 Feb 11, 2026
dc3c3bc
Initial plan
Copilot Feb 11, 2026
f190fee
Initial plan
Copilot Feb 11, 2026
be66fb4
Address code review feedback: fix race condition and improve code qua…
Copilot Feb 11, 2026
fdb09a3
Fix race condition using PostgreSQL advisory locks in transaction
Copilot Feb 11, 2026
d8023cd
Update server/src/handlers/project.rs
Yashb404 Feb 11, 2026
8298271
Address final code review comments
Copilot Feb 11, 2026
08f4e6a
Fix whitelist fetch to only execute for project owners
Copilot Feb 11, 2026
f4f10ac
Implement CSRF protection for whitelist management endpoints
Copilot Feb 11, 2026
e745d42
Implement separate endpoint for embed_key retrieval to prevent exposure
Copilot Feb 11, 2026
b7c8984
Add security documentation for embed key protection
Copilot Feb 11, 2026
e63ff19
Fix closure scope and improve error handling based on code review
Copilot Feb 11, 2026
f3799d8
Add error logging and improve error handling for embed_key fetch
Copilot Feb 11, 2026
d89d045
Reorder query fields to add 'id' at the end for maintainability
Copilot Feb 11, 2026
d484331
Fix information disclosure by returning NOT_FOUND for non-owners
Copilot Feb 11, 2026
3036331
Fix closure capture of origin variable and improve error messages
Copilot Feb 11, 2026
14ea5ed
Merge pull request #125 from TryCli-Studio/copilot/sub-pr-116-again
Yashb404 Feb 11, 2026
3b95b6e
Merge pull request #124 from TryCli-Studio/copilot/sub-pr-116
Yashb404 Feb 11, 2026
f297755
Merge pull request #123 from TryCli-Studio/copilot/sub-pr-116-6b4d3b0…
Yashb404 Feb 11, 2026
b6458a2
Merge pull request #126 from TryCli-Studio/copilot/sub-pr-116-9da4b21…
Yashb404 Feb 11, 2026
f149003
Fix error handling in dashboard and create pages
Copilot Feb 11, 2026
705a0bf
Run cargo fmt
Copilot Feb 11, 2026
33a77cb
Use CSS variables for consistent styling in error messages
Copilot Feb 11, 2026
cc87040
Merge pull request #127 from TryCli-Studio/copilot/sub-pr-116-9718e6b…
Yashb404 Feb 11, 2026
02cb40f
Merge branch 'feat/keys-fresh' into copilot/sub-pr-116-one-more-time
Yashb404 Feb 11, 2026
0046611
Merge pull request #121 from TryCli-Studio/copilot/sub-pr-116-one-mor…
Yashb404 Feb 11, 2026
d8d4863
Merge branch 'feat/keys-fresh' into copilot/sub-pr-116-please-work
Yashb404 Feb 11, 2026
eb0424e
Merge pull request #122 from TryCli-Studio/copilot/sub-pr-116-please-…
Yashb404 Feb 11, 2026
5e104d4
Initial plan
Copilot Feb 11, 2026
b477d42
Restore VIP link loading state that was lost during merges
Copilot Feb 11, 2026
f91245b
Update modal.rs
Yashb404 Feb 11, 2026
0ca6fb4
Merge pull request #128 from TryCli-Studio/copilot/sub-pr-116-please-…
Yashb404 Feb 11, 2026
bb873bb
remove analytics event struct
Yashb404 Feb 12, 2026
b976e89
Merge branch 'main' into feat/keys-fresh
Yashb404 Feb 12, 2026
2847c5c
remove unnecessary mut
Yashb404 Feb 12, 2026
d74315a
reset allowed url to prevent issues in prod
Yashb404 Feb 12, 2026
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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,29 @@ trunk serve --open
* Copy the URL (e.g., `http://localhost:8080/project/my-cool-tool`).
* Send it to users. They will get a fresh clone of the environment you set up!

## Security Architecture

### Embed Authorization

TryCli Studio implements a dual-layer security model for embedded projects:

1. **VIP Pass (embed_key):** A private key that grants unrestricted access to embedded projects. Only project owners have access to this key.
2. **Guest List (whitelist):** A list of authorized URLs that can embed the project publicly.

#### Embed Key Protection

To prevent accidental exposure of the `embed_key` through browser dev tools or network inspection:

* The `embed_key` is **not** included in the main project response (`/api/project/:username/:slug`)
* Instead, a dedicated authenticated endpoint (`/api/project/:slug/embed-key`) is used to retrieve the key
* This endpoint requires authentication and verifies project ownership
* The key is only fetched when the user explicitly clicks the "Share / Embed" button

This separation ensures that:
* Screen sharing during project viewing won't expose the key
* Browser extensions or network logs won't capture the key during normal browsing
* The key is only retrieved when intentionally needed for sharing purposes

## Troubleshooting

* **"Container ID not found":** Ensure you wait for the terminal to initialize before clicking Publish.
Expand Down
2 changes: 1 addition & 1 deletion client/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// CONFIGURATION HELPERS
// CONFIGURATION HELPERS
pub fn api_base() -> &'static str {
option_env!("API_URL").unwrap_or("http://localhost:3000")
}
Expand Down
12 changes: 8 additions & 4 deletions client/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use crate::components::protected::ProtectedRoute;
use crate::pages::{
admin::AdminPage, analytics::AnalyticsPage, blogs::BlogsPage, create::CreatePage,
dashboard::DashboardPage, docs::DocsPage, embed::EmbedPage, home::LandingPage,
policy::PolicyPage, view::ViewPage,
};
use leptos::*;
use leptos_router::*;
use crate::components::protected::ProtectedRoute;
use crate::pages::{home::LandingPage, dashboard::DashboardPage, create::CreatePage, view::ViewPage, embed::EmbedPage, docs::DocsPage, blogs::BlogsPage, analytics::AnalyticsPage, admin::AdminPage, policy::PolicyPage};

#[component]
pub fn App() -> impl IntoView {
Expand All @@ -23,8 +27,8 @@ pub fn App() -> impl IntoView {
<AnalyticsPage />
</ProtectedRoute>
} />


<Route path="/new" view=move || view! {
<ProtectedRoute>
<CreatePage />
Expand Down
10 changes: 5 additions & 5 deletions client/src/components/limit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@ pub fn LimitReached() -> impl IntoView {
<br/>
"Please try again later or contact the owner."
</p>

<div style="display: flex; flex-direction: column; gap: 12px; align-items: center;">
<p style="color: var(--text-main); font-size: 0.9rem; margin-bottom: 0.5rem; font-weight: 600;">
"Are you the owner?"
</p>
<a href="mailto:tryclistudio@gmail.com"
<a href="mailto:tryclistudio@gmail.com"
class="btn-secondary btn-action"
style="width: 100%; max-width: 300px; text-decoration: none; justify-content: center">
"Request More Compute"
</a>
<a href="https://ko-fi.com/V7V21TRPL5"
target="_blank"
<a href="https://ko-fi.com/V7V21TRPL5"
target="_blank"
rel="noopener noreferrer"
class="btn-secondary"
style="width: 100%; max-width: 300px; justify-content: center; color: #ffdd00; border-color: #FFDD00; font-weight: 200;">
Expand All @@ -34,4 +34,4 @@ pub fn LimitReached() -> impl IntoView {
</div>
</div>
}
}
}
151 changes: 145 additions & 6 deletions client/src/components/modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,24 +69,36 @@ pub fn EmbedModal(
title: MaybeSignal<String>,
iframe_code: MaybeSignal<String>,
smart_link: MaybeSignal<String>,
vip_link: MaybeSignal<String>,
whitelist: MaybeSignal<Vec<String>>,
on_add_url: Callback<String>,
on_remove_url: Callback<String>,
on_close: Callback<()>,
) -> impl IntoView {
let (copied_iframe, set_copied_iframe) = create_signal(false);
let (copied_link, set_copied_link) = create_signal(false);

let (copied_vip, set_copied_vip) = create_signal(false);
let (new_url, set_new_url) = create_signal(String::new());

let iframe_ref = create_node_ref::<leptos::html::Textarea>();
let link_ref = create_node_ref::<leptos::html::Input>();
let vip_ref = create_node_ref::<leptos::html::Input>();

view! {
{move || {
let show = show.clone();
let title = title.clone();
let iframe_code = iframe_code.clone();
let smart_link = smart_link.clone();
let vip_link = vip_link.clone();
let whitelist = whitelist.clone();
let on_close = on_close.clone();
let on_add_url = on_add_url.clone();
let on_remove_url = on_remove_url.clone();

let iframe_code_for_click = iframe_code.clone();
let smart_link_for_click = smart_link.clone();
let vip_link_for_click = vip_link.clone();

if show.get() {
view! {
Expand All @@ -96,7 +108,7 @@ pub fn EmbedModal(
<h3 class="modal-title" style="margin: 0; font-size: 1.25rem;">{move || title.get()}</h3>
<button class="btn-nav" on:click=move |_| on_close.call(()) style="font-size: 1.5rem; line-height: 1;">"×"</button>
</div>

// --- SECTION 1: IFRAME ---
<div style="margin-bottom: 24px; position: relative;">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
Expand Down Expand Up @@ -137,11 +149,79 @@ pub fn EmbedModal(
</div>
</div>

// --- SECTION 2: SMART LINK ---
// --- SECTION 2: PRIVATE VIP LINK ---
<div style="margin-bottom: 24px; position: relative;">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<label style="color:var(--text-main); font-weight:600; font-size: 0.9rem;">
"Option 2: Private VIP Link"
</label>
{move || if copied_vip.get() {
view! { <span style="color: #22c55e; font-size: 0.8rem; font-weight: 600; animation: fadeIn 0.2s;">"✓ Copied!"</span> }.into_view()
} else {
view! { <span style="opacity: 0;">"Placeholder"</span> }.into_view()
}}
</div>

{move || {
let link = vip_link.get();
let vip_link_for_input = vip_link.clone();
let vip_link_for_button = vip_link_for_click.clone();
if link.is_empty() {
view! {
<div style="background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; padding: 16px; text-align: center;">
<p style="color: var(--text-muted); font-size: 0.85rem; margin: 0;">
"VIP link is being generated..."
</p>
<p style="color: var(--text-muted); font-size: 0.75rem; margin-top: 8px;">
"This may take a moment. The link will appear here once ready."
</p>
</div>
}.into_view()
} else {
view! {
<>
<div class="input-hero-wrapper" style="display: flex; gap: 0;">
<input
type="text"
class="input-slug"
style="flex: 1; font-family: var(--font-mono); font-size: 0.85rem; border-top-right-radius: 0; border-bottom-right-radius: 0; padding: 10px;"
readonly
node_ref=vip_ref
prop:value=move || vip_link_for_input.get()
/>
<button
class="btn-secondary"
style="border-top-left-radius: 0; border-bottom-left-radius: 0; border-left: none; width: 50px; display: flex; align-items: center; justify-content: center;"
aria-label="Copy VIP link"
on:click=move |_| {
let text = vip_link_for_button.get();
let _ = window().navigator().clipboard().write_text(&text);
if let Some(el) = vip_ref.get() { el.select(); }
set_copied_vip.set(true);
set_timeout(move || set_copied_vip.set(false), std::time::Duration::from_millis(2000));
}
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</div>
<p style="font-size: 0.8rem; color: var(--text-muted); margin-top: 8px;">
<strong style="color: #dc2626;">"Security warning:"</strong>
" This VIP link bypasses the Guest List and must only be shared privately. Do NOT embed it on public websites, iframes, or forums; anyone with this link can access your terminal."
</p>
</>
}.into_view()
}
}}
</div>

// --- SECTION 3: SMART LINK ---
<div style="margin-bottom: 32px; position: relative;">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<label style="color:var(--text-main); font-weight:600; font-size: 0.9rem;">
"Option 2: Smart Link (Medium, Reddit)"
"Option 3: Smart Link (Medium, Reddit)"
</label>
{move || if copied_link.get() {
view! { <span style="color: #22c55e; font-size: 0.8rem; font-weight: 600; animation: fadeIn 0.2s;">"✓ Copied!"</span> }.into_view()
Expand All @@ -152,7 +232,7 @@ pub fn EmbedModal(
<div class="input-hero-wrapper" style="display: flex; gap: 0;">
<input
type="text"
class="input-slug"
class="input-slug"
style="flex: 1; font-family: var(--font-mono); font-size: 0.85rem; border-top-right-radius: 0; border-bottom-right-radius: 0; padding: 10px;"
readonly
node_ref=link_ref
Expand Down Expand Up @@ -181,6 +261,65 @@ pub fn EmbedModal(
</p>
</div>

// --- SECTION 4: Guest List / Whitelist ---
<div style="margin-bottom: 24px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<label style="color:var(--text-main); font-weight:600; font-size: 0.9rem;">
"Guest List (Authorized URLs)"
</label>
<span style="font-size: 0.8rem; color: var(--text-muted);">
"Only these pages can auto-launch your terminal."
</span>
</div>
<div style="display: flex; gap: 10px; margin-bottom: 12px;">
<input
type="text"
class="input-slug"
style="flex: 1;"
placeholder="https://medium.com/@user/article-slug"
prop:value=new_url
on:input=move |ev| set_new_url.set(event_target_value(&ev))
/>
<button
class="btn-primary"
on:click=move |_| {
let url = new_url.get();
if !url.is_empty() {
on_add_url.call(url);
set_new_url.set(String::new());
}
}
prop:disabled=move || new_url.get().is_empty()
>
"Add URL"
</button>
</div>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<For
each=move || whitelist.get()
key=|u| u.clone()
children=move |url| {
let on_remove_url = on_remove_url.clone();
view! {
<span class="badge" style="margin: 0; display: flex; align-items: center; gap: 8px;">
{url.clone()}
<button
class="btn-nav"
style="padding: 0; color: #ef4444; font-weight: bold; font-size: 0.9rem;"
aria-label="Remove URL"
on:click=move |_| {
on_remove_url.call(url.clone());
}
>
"×"
</button>
</span>
}
}
/>
</div>
</div>

<div class="modal-actions">
<button class="btn-secondary btn-action" on:click=move |_| on_close.call(())>
"Done"
Expand Down Expand Up @@ -266,4 +405,4 @@ pub fn ConfirmModal(
}
}}
}
}
}
Loading