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
22 changes: 18 additions & 4 deletions crates/modelrelay-cloud/src/routes/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ fn client_ip(headers: &HeaderMap) -> IpAddr {
}

/// Render a 429 Too Many Requests page.
fn rate_limit_response() -> Response {
///
/// `path` is the canonical URL path of the request that triggered the rate limit
/// (e.g. `/signup` or `/login`), used for per-page og:url and canonical.
fn rate_limit_response(path: &str) -> Response {
let body = "\
<div class=\"card\" style=\"border-left: 3px solid #fbbf24;\">\
<h2 style=\"display:flex;align-items:center;gap:10px;\">\
Expand All @@ -38,7 +41,7 @@ fn rate_limit_response() -> Response {
<p style=\"margin-top:12px;color:#8b949e;font-size:0.9rem;\">This limit protects your account from unauthorized access attempts.</p>\
<a href=\"/\" class=\"btn\" style=\"margin-top:20px;\">Back to Home</a>\
</div>";
let html = modelrelay_web::templates::page_shell("Too Many Requests", body, false);
let html = modelrelay_web::templates::page_shell("Too Many Requests", path, body, false);
(StatusCode::TOO_MANY_REQUESTS, Html(html)).into_response()
}

Expand Down Expand Up @@ -67,6 +70,7 @@ pub async fn signup_page(session: Session) -> Response {
let csrf_field = csrf::hidden_field(&session).await;
Html(modelrelay_web::templates::page_shell(
"Sign Up",
"/signup",
&signup_form_html(None, &csrf_field),
false,
))
Expand All @@ -83,14 +87,15 @@ pub async fn signup_submit(
) -> Response {
let ip = client_ip(&headers);
if state.rate_limiter.is_limited(ip) {
return rate_limit_response();
return rate_limit_response("/signup");
}

let csrf_field = csrf::hidden_field(&session).await;

let Some(ref pool) = state.db else {
return Html(modelrelay_web::templates::page_shell(
"Sign Up",
"/signup",
"<div class=\"card\"><h2>Error</h2><p>Database not available.</p></div>",
false,
))
Expand All @@ -104,6 +109,7 @@ pub async fn signup_submit(
if email.is_empty() || !email.contains('@') {
return Html(modelrelay_web::templates::page_shell(
"Sign Up",
"/signup",
&signup_form_html(Some("Please enter a valid email address."), &csrf_field),
false,
))
Expand All @@ -112,6 +118,7 @@ pub async fn signup_submit(
if password.len() < 8 {
return Html(modelrelay_web::templates::page_shell(
"Sign Up",
"/signup",
&signup_form_html(Some("Password must be at least 8 characters."), &csrf_field),
false,
))
Expand All @@ -130,6 +137,7 @@ pub async fn signup_submit(
state.rate_limiter.record_attempt(ip);
return Html(modelrelay_web::templates::page_shell(
"Sign Up",
"/signup",
&signup_form_html(
Some("An account with this email already exists. <a href=\"/login\">Log in instead</a>."),
&csrf_field,
Expand All @@ -146,6 +154,7 @@ pub async fn signup_submit(
tracing::error!("password hash error: {e}");
return Html(modelrelay_web::templates::page_shell(
"Sign Up",
"/signup",
&signup_form_html(Some("Internal error. Please try again."), &csrf_field),
false,
))
Expand All @@ -171,6 +180,7 @@ pub async fn signup_submit(
tracing::error!("user insert error: {e}");
return Html(modelrelay_web::templates::page_shell(
"Sign Up",
"/signup",
&signup_form_html(
Some("Could not create account. Please try again."),
&csrf_field,
Expand Down Expand Up @@ -210,6 +220,7 @@ pub async fn login_page(session: Session) -> Response {
let csrf_field = csrf::hidden_field(&session).await;
Html(modelrelay_web::templates::page_shell(
"Log In",
"/login",
&login_form_html(None, &csrf_field),
false,
))
Expand All @@ -225,14 +236,15 @@ pub async fn login_submit(
) -> Response {
let ip = client_ip(&headers);
if state.rate_limiter.is_limited(ip) {
return rate_limit_response();
return rate_limit_response("/login");
}

let csrf_field = csrf::hidden_field(&session).await;

let Some(ref pool) = state.db else {
return Html(modelrelay_web::templates::page_shell(
"Log In",
"/login",
"<div class=\"card\"><h2>Error</h2><p>Database not available.</p></div>",
false,
))
Expand All @@ -252,6 +264,7 @@ pub async fn login_submit(
state.rate_limiter.record_attempt(ip);
return Html(modelrelay_web::templates::page_shell(
"Log In",
"/login",
&login_form_html(Some("Invalid email or password."), &csrf_field),
false,
))
Expand All @@ -262,6 +275,7 @@ pub async fn login_submit(
state.rate_limiter.record_attempt(ip);
return Html(modelrelay_web::templates::page_shell(
"Log In",
"/login",
&login_form_html(Some("Invalid email or password."), &csrf_field),
false,
))
Expand Down
20 changes: 18 additions & 2 deletions crates/modelrelay-cloud/src/routes/checkout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,21 @@ static CANCEL_HTML: &str = include_str!("../../templates/checkout_cancel.html");
static SUCCESS_HTML: &str = include_str!("../../templates/checkout_success.html");

fn success_page() -> String {
modelrelay_web::templates::page_shell("Subscription Active", SUCCESS_HTML, true)
modelrelay_web::templates::page_shell(
"Subscription Active",
"/checkout/success",
SUCCESS_HTML,
true,
)
}

fn cancel_page() -> String {
modelrelay_web::templates::page_shell("Checkout Cancelled", CANCEL_HTML, false)
modelrelay_web::templates::page_shell(
"Checkout Cancelled",
"/checkout/cancel",
CANCEL_HTML,
false,
)
}

/// POST /checkout — create a Stripe Checkout Session and redirect to Stripe.
Expand All @@ -26,6 +36,7 @@ pub async fn create(session: Session, State(state): State<Arc<CloudState>>) -> R
let Some(ref key) = state.stripe_key else {
return Html(modelrelay_web::templates::page_shell(
"Billing Not Configured",
"/checkout",
"<div class=\"card\" style=\"border-left: 3px solid #fbbf24;\">\
<h2 style=\"display:flex;align-items:center;gap:10px;\">\
<svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#fbbf24\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\
Expand All @@ -42,6 +53,7 @@ pub async fn create(session: Session, State(state): State<Arc<CloudState>>) -> R
if price_id.is_empty() {
return Html(modelrelay_web::templates::page_shell(
"Billing Not Configured",
"/checkout",
"<div class=\"card\" style=\"border-left: 3px solid #fbbf24;\">\
<h2 style=\"display:flex;align-items:center;gap:10px;\">\
<svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#fbbf24\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\
Expand Down Expand Up @@ -104,6 +116,7 @@ pub async fn create(session: Session, State(state): State<Arc<CloudState>>) -> R
} else {
Html(modelrelay_web::templates::page_shell(
"Checkout Error",
"/checkout",
"<p>Stripe did not return a checkout URL.</p>\
<p class=\"back-link\"><a href=\"/pricing\">&larr; Back to pricing</a></p>",
false,
Expand All @@ -115,6 +128,7 @@ pub async fn create(session: Session, State(state): State<Arc<CloudState>>) -> R
tracing::error!("stripe response parse error: {e}");
Html(modelrelay_web::templates::page_shell(
"Checkout Error",
"/checkout",
"<p>Could not process Stripe response.</p>\
<p class=\"back-link\"><a href=\"/pricing\">&larr; Back to pricing</a></p>",
false,
Expand All @@ -128,6 +142,7 @@ pub async fn create(session: Session, State(state): State<Arc<CloudState>>) -> R
tracing::error!("stripe API error: {status} — {body}");
Html(modelrelay_web::templates::page_shell(
"Checkout Error",
"/checkout",
"<p>Could not create checkout session. Please try again later.</p>\
<p class=\"back-link\"><a href=\"/pricing\">&larr; Back to pricing</a></p>",
false,
Expand All @@ -138,6 +153,7 @@ pub async fn create(session: Session, State(state): State<Arc<CloudState>>) -> R
tracing::error!("stripe request error: {e}");
Html(modelrelay_web::templates::page_shell(
"Checkout Error",
"/checkout",
"<p>Could not reach payment provider. Please try again later.</p>\
<p class=\"back-link\"><a href=\"/pricing\">&larr; Back to pricing</a></p>",
false,
Expand Down
Loading