Skip to content
This repository was archived by the owner on Sep 30, 2025. It is now read-only.
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ RUST_LOG=info # https://docs.rs/env_logger/latest/env_logger/#enabling-logging
SUBSCRIPTIONS__PORT=1337
SUBSCRIPTIONS__HOST=0.0.0.0
SUBSCRIPTIONS__BASE_URL=http://localhost:1337
SUBSCRIPTIONS__HMAC_SECRET=secret

# Subscriptions Database
SUBSCRIPTIONS__DATABASE__BASE_URL=ws://database:4000
Expand Down
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ RUST_LOG=debug # https://docs.rs/env_logger/latest/env_logger/#enabling-logging
SUBSCRIPTIONS__PORT=1337 # not used by tests
SUBSCRIPTIONS__HOST=0.0.0.0 # not used by tests
SUBSCRIPTIONS__BASE_URL=http://localhost:1337 # not used by tests
SUBSCRIPTIONS__HMAC_SECRET=secret

# Subscriptions Database
SUBSCRIPTIONS__DATABASE__BASE_URL=mem://
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ jobs:
SUBSCRIPTIONS__EMAIL_CLIENT__BASE_URL=${{ vars.SUBSCRIPTIONS__EMAIL_CLIENT__BASE_URL }}
SUBSCRIPTIONS__EMAIL_CLIENT__AUTH_TOKEN=${{ secrets.SUBSCRIPTIONS__EMAIL_CLIENT__AUTH_TOKEN }}
SUBSCRIPTIONS__EMAIL_CLIENT__TIMEOUT=${{ vars.SUBSCRIPTIONS__EMAIL_CLIENT__TIMEOUT }}
SUBSCRIPTIONS__HMAC_SECRET=${{ vars.SUBSCRIPTIONS__HMAC_SECRET }}

- name: Deployment URL
run: 'echo "${{ steps.deploy.outputs.url }}"'
115 changes: 115 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ validator = "0.20.0"
serde_json = "1.0.143"
rand = { version = "0.9.2", features = ["std_rng"] }
include_dir = "0.7.4"
base64 = "0.22.1"
urlencoding = "2.1.3"
htmlescape = "0.3.1"
hmac = { version = "0.12.1", features = ["std"] }
sha2 = "0.10.9"
hex = "0.4.3"
tower-cookies = "0.11.0"
axum-messages = "0.8.0"
tower-sessions = "0.14.0"
tower-sessions-surrealdb-store = "0.7.0"
Comment on lines +41 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Version compatibility and toolchain pinning.

The added auth/session deps look consistent, but please sanity-check cross-crate compatibility (axum 0.8.x, tower-http 0.6.x, tower-sessions 0.14.x, store 0.7.x) and ensure CI uses a toolchain that supports edition = "2024".

  • Consider adding rust-version to Cargo.toml and pinning toolchain via rust-toolchain.toml.
  • Consider caret ranges for non-security-critical crates to ease patch updates.

🏁 Script executed:

#!/bin/bash
# Verify toolchain and compatibility hints in-repo.
echo "rust-toolchain files:"; fd -HI --glob 'rust-toolchain{,.toml}' -t f
echo "workflows referencing toolchains:"; fd -HI .github/workflows -t f | xargs -r rg -n "toolchain|rust-version|rustup"

Length of output: 650


🌐 Web query:

Is `tower-sessions-surrealdb-store` 0.7.x compatible with `tower-sessions` 0.14.x and SurrealDB 2.x? Any known breaking changes with Axum 0.8.x?

💡 Result:

Short answer: probably not without changes.

  • tower-sessions-surrealdb-store appears to be an unmaintained beta (latest published ~0.5.x) and its repo/docs show no explicit support for tower-sessions 0.14 or recent SurrealDB 2.x. [1][2].
  • tower-sessions 0.8+ → 0.14 introduced multiple breaking changes that stores must follow (async/lazy sessions, the Record intermediary, new concrete error type, and later a required create method for session creation/id-collision mitigation). A store written for older tower-sessions versions will need updates to implement these new SessionStore APIs. [3][4].
  • SurrealDB 2.x made breaking changes (storage/ID semantics and SurrealQL differences); a store depending on the pre-2.x surrealdb crate or older query/ID semantics will likely need code changes to work with SurrealDB 2.x. See the SurrealDB 2.x migration notes. [5].
  • Axum 0.8 also contains breaking changes (path parameter syntax and extractor/trait changes). Since tower-sessions exposes an axum extractor and depends on axum-core, you should verify the store + app use axum-compatible versions; tower-sessions 0.14 bumped axum-core previously, but you may still need to adjust routes/extractors when moving to axum 0.8. [3][6].

Recommendation: if you need to run tower-sessions 0.14 + SurrealDB 2.x + axum 0.8, either

  • update/patch tower-sessions-surrealdb-store to implement the current SessionStore trait and use a SurrealDB 2.x-compatible client; or
  • look for/author a maintained store crate targeting tower-sessions ≥0.12 and surrealdb 2.x.

References:

  1. tower-sessions-surrealdb-store (repo/docs). [GitHub / docs.rs]. [2]
  2. tower-sessions release notes / changelog (breaking changes across 0.7→0.8→...→0.14). [3][4]
  3. tower-sessions crate docs (0.14). [3]
  4. tower-sessions changelog (notes about async/lazy, Record, create API). [4]
  5. SurrealDB 2.x upgrade / breaking changes. [5]
  6. axum 0.8 announcement (breaking changes). [6]

If you want, I can check the exact tower-sessions-surrealdb-store Cargo.toml and open-source code to list the specific API mismatches and a minimal patch.


🏁 Script executed:

#!/bin/bash
echo "== rust-toolchain.toml =="
sed -n '1,200p' rust-toolchain.toml

echo "== CI workflows referencing toolchains =="
grep -RInE "rust-toolchain|rust-version|toolchain|rustup" .github/workflows || echo "⟂ none found"

echo "== Cargo.toml editions =="
grep -RIn "^edition" Cargo.toml

Length of output: 576


Patch session store compatibility & confirm toolchain pinning

  • tower-sessions-surrealdb-store v0.7.x isn’t compatible with tower-sessions v0.14.x and SurrealDB 2.x; update or replace it to implement the current SessionStore trait and SurrealDB v2 client.
  • rust-toolchain.toml already pins channel 1.89.0 and CI’s setup-toolchain uses it; Cargo.toml is on edition "2024". Optionally add rust-version = "1.89" to Cargo.toml.
  • Consider using caret ranges for non-security deps (e.g. base64 = "^0.22") to allow automatic patch upgrades.


[dev-dependencies]
mime = "0.3.17"
Expand Down
1 change: 1 addition & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub struct Config {
pub port: u16,
pub host: String,
pub base_url: Url,
pub hmac_secret: SecretString,
pub database: DatabaseConfig,
pub email_client: EmailClientConfig,
}
Expand Down
24 changes: 20 additions & 4 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use axum::{http::StatusCode, response::IntoResponse};
use axum::{
body::Body,
http::StatusCode,
response::{IntoResponse, Response},
};

pub type Result<T = ()> = std::result::Result<T, Error>;

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
SurrealDb(Box<surrealdb::Error>),
SurrealDb(#[from] Box<surrealdb::Error>),
#[error("{0:?}")]
Migrations(String),
#[error(transparent)]
Expand All @@ -20,6 +24,10 @@ pub enum Error {
ValidationError(#[from] validator::ValidationError),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error(transparent)]
Session(#[from] tower_sessions::session::Error),
#[error("{0:?}")]
Auth(String),

#[error("{0:?}")]
Custom(String),
Expand All @@ -32,12 +40,20 @@ impl From<surrealdb::Error> for Error {
}

impl IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
fn into_response(self) -> Response {
match self {
Self::ValidationErrors(_) | Self::ValidationError(_) => {
tracing::info!("Bad request: - {self:?}");
tracing::warn!("Bad request: - {self:?}");
StatusCode::BAD_REQUEST.into_response()
}
Self::Auth(_) => {
tracing::warn!("Unauthorized : - {self:?}");
Response::builder()
.status(StatusCode::UNAUTHORIZED)
.header("WWW-Authenticate", r#"Basic realm="publish""#)
.body(Body::empty())
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR.into_response())
}
Comment on lines +49 to +56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid logging secrets; add charset to WWW-Authenticate.

Do not log Auth(String) contents; it risks leaking credentials. Also include charset="UTF-8" per RFC 7617.

Apply:

-            Self::Auth(_) => {
-                tracing::warn!("Unauthorized : - {self:?}");
-                Response::builder()
-                    .status(StatusCode::UNAUTHORIZED)
-                    .header("WWW-Authenticate", r#"Basic realm="publish""#)
-                    .body(Body::empty())
-                    .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR.into_response())
-            }
+            Self::Auth(_) => {
+                // Avoid logging credential material.
+                tracing::warn!("Unauthorized");
+                Response::builder()
+                    .status(StatusCode::UNAUTHORIZED)
+                    .header(axum::http::header::WWW_AUTHENTICATE, r#"Basic realm="publish", charset="UTF-8""#)
+                    .body(Body::empty())
+                    .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR.into_response())
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Self::Auth(_) => {
tracing::warn!("Unauthorized : - {self:?}");
Response::builder()
.status(StatusCode::UNAUTHORIZED)
.header("WWW-Authenticate", r#"Basic realm="publish""#)
.body(Body::empty())
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR.into_response())
}
Self::Auth(_) => {
// Avoid logging credential material.
tracing::warn!("Unauthorized");
Response::builder()
.status(StatusCode::UNAUTHORIZED)
.header(
axum::http::header::WWW_AUTHENTICATE,
r#"Basic realm="publish", charset="UTF-8""#,
)
.body(Body::empty())
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR.into_response())
}
🤖 Prompt for AI Agents
In src/errors.rs around lines 49 to 56, the current branch logs the Auth(String)
contents and sets the WWW-Authenticate header without a charset; remove or
sanitize the logged secret by replacing the tracing::warn! call with a generic
message that does not include the auth string (e.g. "Unauthorized access
attempt"), and update the header value to include charset per RFC 7617 (e.g.
Basic realm="publish", charset="UTF-8"); keep the rest of the response
construction identical and do not expose the inner String anywhere in logs or
responses.

_ => {
tracing::error!("Internal Server: - {self:?}");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
Expand Down
37 changes: 37 additions & 0 deletions src/handlers/admin/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use crate::{Result, model::ModelManager, session_state::TypedSession};
use axum::{
extract::State,
response::{Html, IntoResponse, Redirect},
};
use reqwest::StatusCode;
use std::sync::Arc;

pub async fn admin_dashboard(
State(mm): State<Arc<ModelManager>>,
session: TypedSession,
) -> Result<impl IntoResponse> {
let username = match session.get_user_id().await {
Ok(Some(user_id)) => mm.get_username(user_id).await?,
reason => {
tracing::error!("Failed to authenticate: {reason:?}");
return Ok(Redirect::to("/login").into_response());
}
};
Comment on lines +13 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Escape username before embedding in HTML (stored XSS risk).

username comes from the DB and is rendered unescaped into HTML. Escape it before formatting.

 use std::sync::Arc;
+use askama_escape::escape;
@@
     };
 
+    let username = escape(&username).to_string();
+
     let body = format!(
         r#"
         <!DOCTYPE html>
         <html>
             <head>
                 <meta http-equiv="content-type" content="text/html; charset=utf-8">
                 <title>Admin dashboard</title>
             </head>
             <body>
                 <p>Welcome {username}</p>
             </body>
         </html>
         "#
     );

Add dependency (Cargo.toml):

askama_escape = "0.10"

Also applies to: 21-36

🤖 Prompt for AI Agents
In src/handlers/admin/mod.rs around lines 13-19 (and also apply to 21-36), the
username retrieved from the DB is embedded into HTML without escaping (stored
XSS risk); add the askama_escape = "0.10" dependency to Cargo.toml, import and
use the Askama HTML escape utility (or equivalent escape function) to escape
username before inserting it into any HTML/template or string formatting, and
replace direct uses of username in those lines with the escaped value (ensure
all occurrences in 21-36 are updated similarly).


let body = format!(
r#"
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Admin dashboard</title>
</head>
<body>
<p>Welcome {username}</p>
</body>
</html>
"#
);

Ok((StatusCode::OK, Html(body)).into_response())
}
10 changes: 10 additions & 0 deletions src/handlers/home/home.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Use modern charset meta

Prefer the shorter, HTML5-standard <meta charset="utf-8">.

-        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+        <meta charset="utf-8">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta charset="utf-8">
🤖 Prompt for AI Agents
In src/handlers/home/home.html around line 4 the document uses the legacy meta
tag `<meta http-equiv="content-type" content="text/html; charset=utf-8" />`;
replace it with the HTML5-standard shorter form `<meta charset="utf-8">` on that
line, removing the http-equiv/content-type attributes and the self-closing slash
so the page declares charset in the modern, preferred way.

<title>Home</title>
</head>
<body>
<p>Welcome to our newsletter!</p>
</body>
</html>
7 changes: 7 additions & 0 deletions src/handlers/home/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use crate::Result;
use axum::response::{Html, IntoResponse};
use reqwest::StatusCode;

pub async fn home() -> Result<impl IntoResponse> {
Ok((StatusCode::OK, Html(include_str!("home.html"))))
}
49 changes: 49 additions & 0 deletions src/handlers/login/get.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use crate::Result;
use axum::response::{Html, IntoResponse};
use axum_messages::Messages;
use reqwest::StatusCode;

pub async fn login(
messages: Messages,
// Query(query): Option<Query<QueryParams>>,
) -> Result<impl IntoResponse> {
Comment on lines +8 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Remove dead, commented parameter.

-    // Query(query): Option<Query<QueryParams>>,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Query(query): Option<Query<QueryParams>>,
) -> Result<impl IntoResponse> {
) -> Result<impl IntoResponse> {
🤖 Prompt for AI Agents
In src/handlers/login/get.rs around lines 8 to 9, remove the dead commented
parameter "// Query(query): Option<Query<QueryParams>>," from the function
signature so the signature no longer contains commented-out parameters; update
any trailing commas/whitespace to keep formatting consistent and run cargo fmt.

let error_message = messages
.into_iter()
.map(|message| format!("<p><i>{}</i></p>", message.message))
.collect::<Vec<_>>()
.join("");

Comment on lines +10 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Escape flash messages to prevent HTML injection.

message.message is injected into HTML without escaping. Use an HTML escaper to avoid XSS if any message ever contains user-controlled data.

 use axum_messages::Messages;
 use reqwest::StatusCode;
+use askama_escape::escape;
@@
-    let error_message = messages
+    let error_message = messages
         .into_iter()
-        .map(|message| format!("<p><i>{}</i></p>", message.message))
+        .map(|message| format!("<p><i>{}</i></p>", escape(&message.message)))
         .collect::<Vec<_>>()
         .join("");

Add dependency (Cargo.toml):

askama_escape = "0.10"

Also applies to: 16-46

🤖 Prompt for AI Agents
In src/handlers/login/get.rs around lines 10 to 15 (also apply to 16-46): flash
message text is being injected directly into HTML; add the askama_escape
dependency to Cargo.toml (askama_escape = "0.10") and use its escape function
when formatting messages, e.g. replace message.message with
askama_escape::escape(&message.message).to_string() (and import
askama_escape::escape) so the map/format call becomes
format!("<p><i>{}</i></p>", escape(&message.message)); apply the same escaping
to all other places in lines 16-46 where message.message is interpolated.

let body = format!(
r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Login</title>
</head>
<body>
{error_message}
<form action="/login" method="post">
<label>Username
<input
type="text"
placeholder="Enter Username"
name="username"
>
</label>
<label>Password
<input
type="password"
placeholder="Enter Password"
name="password"
>
</label>
<button type="submit">Login</button>
</form>
</body>
</html>
"#
);

Ok((StatusCode::OK, Html(body)))
}
Loading
Loading