Skip to content

Commit c344fed

Browse files
committed
add sentry
1 parent ed7ce05 commit c344fed

41 files changed

Lines changed: 701 additions & 398 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ jobs:
4949
EMAIL_NEWSLETTER_REPLY_TO: ${{ secrets.EMAIL_NEWSLETTER_REPLY_TO }}
5050
CF_TURNSTILE_SITE_KEY: ${{ secrets.CF_TURNSTILE_SITE_KEY }}
5151
CF_TURNSTILE_SECRET_KEY: ${{ secrets.CF_TURNSTILE_SECRET_KEY }}
52+
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
5253

5354
- name: Generate content hashes
5455
run: |

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ tower = "0.5"
2121
tower-http = { version = "0.6", features = ["fs", "set-header", "compression-br", "request-id", "trace", "util"] }
2222
tower-serve-static = "0.1"
2323
reqwest = { version = "0.12", default-features = false, features = ["http2", "charset", "json", "multipart", "rustls-tls"] }
24+
sentry = { version = "0.46", default-features = false, features = ["tracing", "backtrace", "contexts", "debug-images", "panic", "release-health", "tower-axum-matched-path", "reqwest", "rustls"]}
25+
backtrace = "0.3"
2426

25-
anyhow = "1"
2627
thiserror = "2"
2728
chrono-tz = { version = "0.10", features = ["serde"] }
2829
chrono = { version = "0.4", features = ["serde"] }

config/prod.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,6 @@ webhook_key = "$STRIPE_WEBHOOK_KEY"
3434
[cloudflare]
3535
turnstile_site_key = "$CF_TURNSTILE_SITE_KEY"
3636
turnstile_secret_key = "$CF_TURNSTILE_SECRET_KEY"
37+
38+
[sentry]
39+
dsn = "$SENTRY_DSN"

frontend/styles/error.css

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#error {
2+
h1 {
3+
@apply mb-2 text-2xl italic;
4+
}
5+
p {
6+
@apply text-lsd-gray text-sm;
7+
}
8+
a {
9+
@apply decoration-lsd-blue hover:text-lsd-blue underline;
10+
}
11+
}
12+
13+
#error-extra {
14+
max-width: 800px;
15+
.section {
16+
@apply mb-6;
17+
}
18+
pre {
19+
font-size: 10px;
20+
}
21+
}

frontend/styles/events/rsvp_selection.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
}
2626
svg {
2727
@apply cursor-pointer touch-manipulation select-none;
28-
transition: color 0.1s linear;
28+
transition: color 0.05s linear;
2929
&.disabled {
3030
@apply cursor-default;
3131
color: var(--color-lsd-charcoal);

frontend/styles/main.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
/* Home */
1010
@import "./home.css";
1111
@import "./dashboard.css";
12+
@import "./error.css";
1213

1314
/* Events */
1415
@import "./events/edit.css";

frontend/templates/error.html

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,25 @@
22

33
{% block title %}light and sound{% endblock title %}
44
{% block content %}
5-
<section class="ext/layout thin">
6-
<header>
7-
<h1>Error</h1>
8-
</header>
9-
<p>{{ message }}</p>
5+
<section id="error" class="ext/layout thin">
6+
<h1>{{ title }}</h1>
7+
<p>{{ message | safe }}</p>
108
</section>
9+
{% if context.is_some() || backtrace.is_some() %}
10+
<section id="error-extra">
11+
{% if let Some(context) = context %}
12+
<div class="section">
13+
<h2>Context</h2>
14+
<pre>{{ context }}</pre>
15+
</div>
16+
{% endif %}
17+
18+
{% if let Some(backtrace) = backtrace %}
19+
<div class="section">
20+
<h2>Backtrace</h2>
21+
<pre>{{ backtrace }}</pre>
22+
</div>
23+
{% endif %}
24+
</section>
25+
{% endif %}
1126
{% endblock content %}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{% extends "layout.html" %}
2+
3+
{% block title %}light and sound{% endblock title %}
4+
{% block content %}
5+
<section id="error" class="ext/layout thin">
6+
<h1>Error</h1>
7+
<p>{{ message | safe }}</p>
8+
</section>
9+
{% endblock content %}

src/app/auth.rs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ pub fn add_middleware(router: AxumRouter, state: SharedAppState) -> AxumRouter {
3737
/// Middleware layer to lookup add a `User` to the request if a session token is present.
3838
pub async fn session_middleware(
3939
State(state): State<SharedAppState>, mut cookies: CookieJar, mut request: Request, next: Next,
40-
) -> AppResult<(CookieJar, Response)> {
40+
) -> HtmlResult {
4141
if let Some(token) = cookies.get("session") {
4242
match User::lookup_by_session_token(&state.db, token.value()).await? {
4343
Some(user) => {
@@ -47,7 +47,7 @@ pub fn add_middleware(router: AxumRouter, state: SharedAppState) -> AxumRouter {
4747
}
4848
}
4949
let response = next.run(request).await;
50-
Ok((cookies, response))
50+
Ok((cookies, response).into_response())
5151
}
5252
router.layer(axum::middleware::from_fn_with_state(state, session_middleware))
5353
}
@@ -60,22 +60,20 @@ impl<S: Send + Sync> axum::extract::OptionalFromRequestParts<S> for User {
6060
}
6161
/// Enable extracting a `User` in a handler, returning UNAUTHORIZED if not logged in.
6262
impl<S: Send + Sync> axum::extract::FromRequestParts<S> for User {
63-
type Rejection = AppError;
63+
type Rejection = HtmlError;
6464
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
6565
match parts.extensions.get::<User>().cloned() {
6666
Some(user) => Ok(user),
67-
None => Err(AppError::Unauthorized),
67+
None => bail_unauthorized!(),
6868
}
6969
}
7070
}
7171

7272
/// Process a login form and send either a login or registration link via email.
73-
async fn login_form(
74-
State(state): State<SharedAppState>, Form(form): Form<LoginForm>,
75-
) -> AppResult<impl IntoResponse> {
73+
async fn login_form(State(state): State<SharedAppState>, Form(form): Form<LoginForm>) -> HtmlResult {
7674
let email = form.email.email.to_string();
7775
let Some(user) = User::lookup_by_email(&state.db, &email).await? else {
78-
return Err(AppError::Unauthorized);
76+
bail_unauthorized!()
7977
};
8078

8179
let email_id = Email::create_login(&state.db, &user).await?;
@@ -99,11 +97,11 @@ async fn login_form(
9997
match state.mailer.send(&msg).await {
10098
Ok(_) => {
10199
Email::mark_sent(&state.db, email_id).await?;
102-
Ok("Check your email!")
100+
Ok("Check your email!".into_response())
103101
}
104102
Err(e) => {
105-
Email::mark_error(&state.db, email_id, &e.to_string()).await?;
106-
Err(e)
103+
Email::mark_error(&state.db, email_id, e.message()).await?;
104+
Err(e.into())
107105
}
108106
}
109107
}
@@ -129,7 +127,7 @@ impl LoginQuery {
129127
}
130128
async fn login_link(
131129
user: Option<User>, State(state): State<SharedAppState>, Query(query): Query<LoginQuery>,
132-
) -> AppResult<Response> {
130+
) -> HtmlResult {
133131
// If user is already logged in for some reason, just follow the redirect
134132
if user.is_some() {
135133
return Ok(query.redirect().into_response());
@@ -149,7 +147,7 @@ async fn login_link(
149147

150148
// Otherwise we're handling a login link. Valdiate the login token and create a new session.
151149
let Some(user) = User::lookup_by_login_token(&state.db, token).await? else {
152-
return Err(AppError::Unauthorized);
150+
bail_unauthorized!();
153151
};
154152
let token = SessionToken::create(&state.db, &user).await?;
155153
let cookie = session_cookie(&state.config, token);

src/app/contact.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ pub fn add_routes(router: AppRouter) -> AppRouter {
99
router.public_routes(|r| r.route("/contact", get(contact_page).post(contact_form)))
1010
}
1111

12-
async fn contact_page(
13-
user: Option<User>, State(state): State<SharedAppState>,
14-
) -> AppResult<impl IntoResponse> {
12+
async fn contact_page(user: Option<User>, State(state): State<SharedAppState>) -> HtmlResult {
1513
#[derive(Template, WebTemplate)]
1614
#[template(path = "contact/send.html")]
1715
struct Html {
@@ -21,7 +19,8 @@ async fn contact_page(
2119
Ok(Html {
2220
user,
2321
turnstile_site_key: state.config.cloudflare.turnstile_site_key.clone(),
24-
})
22+
}
23+
.into_response())
2524
}
2625

2726
#[derive(serde::Deserialize, Debug)]
@@ -36,9 +35,9 @@ struct ContactForm {
3635
async fn contact_form(
3736
user: Option<User>, State(state): State<SharedAppState>, ConnectInfo(client): ConnectInfo<SocketAddr>,
3837
Form(form): Form<ContactForm>,
39-
) -> AppResult<impl IntoResponse> {
38+
) -> HtmlResult {
4039
if !state.cloudflare.validate_turnstile(client.ip(), &form.turnstile_token).await? {
41-
return Err(AppError::BadRequest);
40+
bail_invalid!();
4241
}
4342

4443
let name = Some(form.name).filter(|n| !n.is_empty());
@@ -51,7 +50,7 @@ async fn contact_form(
5150
None => format!("[Anonymous]: {}", form.subject),
5251
};
5352
let reply_to = match email {
54-
Some(e) => Some(Mailbox::new(name, e.parse().map_err(|_| AppError::BadRequest)?)),
53+
Some(e) => Some(Mailbox::new(name, e.parse().map_err(|_| invalid())?)),
5554
None => None,
5655
};
5756

@@ -68,5 +67,5 @@ async fn contact_form(
6867
struct Html {
6968
user: Option<User>,
7069
};
71-
Ok(Html { user })
70+
Ok(Html { user }.into_response())
7271
}

0 commit comments

Comments
 (0)