main
tag: launch-2026-06-01
tag: magicmirror-v0.1.1
tag: magicmirror-v0.3.0
tag: mnw-cli-v0.1.2
tag: mnw-cli-v0.1.3
tag: mnw-cli-v0.1.4
tag: pom-v0.4.1
tag: pom-v0.4.2
tag: pom-v0.4.3
tag: pom-v0.4.4
tag: pom-v0.4.5
tag: wam-v0.3.0
tag: wam-v0.3.1
Files
Commits
Tags
Notes
Issues
Fix auth rate limiter hitting GET /login, add unique test IPs
GET /login was incorrectly rate-limited because route_layer applied
the governor to both GET and POST on /login. Move rate limiter to
per-handler .layer() so only POST /login, passkey start, and passkey
finish are rate-limited. Also assign unique IPs per TestClient via
atomic counter to prevent cross-test rate limit interference. Fix
UUID type in sandbox subscription test.
Co-Authored-By
Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3 files changed,
+19 insertions,
-10 deletions
2
2
3
3
use axum::{
4
4
extract::State,
5
+
handler::Handler,
5
6
http::{header::HeaderMap, StatusCode},
6
7
response::{Html, IntoResponse, Redirect, Response},
7
8
routing::{get, post},
35
36
let validate_rate_limit = rate_limiter_per_sec(constants::VALIDATE_RATE_LIMIT_PER_SEC, constants::VALIDATE_RATE_LIMIT_BURST);
36
37
37
38
Router::new()
38
-
// Auth routes with rate limiting
39
-
.route("/login", post(login_handler))
40
-
.route("/auth/passkey/start", post(passkey_auth_start))
41
-
.route("/auth/passkey/finish", post(passkey_auth_finish))
42
-
.route_layer(GovernorLayer {
43
-
config: auth_rate_limit,
44
-
})
39
+
// GET /login is NOT rate-limited (page render for CSRF tokens).
40
+
// POST /login and passkey routes ARE rate-limited.
41
+
.route("/login", get(crate::routes::pages::public::landing::login_page)
42
+
.post(login_handler.layer(GovernorLayer { config: auth_rate_limit.clone() })))
43
+
.route("/auth/passkey/start", post(passkey_auth_start)
44
+
.layer(GovernorLayer { config: auth_rate_limit.clone() }))
45
+
.route("/auth/passkey/finish", post(passkey_auth_finish)
46
+
.layer(GovernorLayer { config: auth_rate_limit }))
45
47
// Routes without auth rate limiting
46
48
.route("/logout", post(logout_handler))
47
49
.route("/auth/me", get(me_handler))
8
8
use axum::Router;
9
9
use http_body_util::BodyExt;
10
10
use std::collections::HashMap;
11
+
use std::sync::atomic::{AtomicU32, Ordering};
11
12
use tower::ServiceExt;
12
13
14
+
/// Monotonic counter for unique per-test IPs (10.1.x.y).
15
+
static IP_COUNTER: AtomicU32 = AtomicU32::new(1);
16
+
13
17
/// A test HTTP client that talks to the app in-process.
14
18
pub struct TestClient {
15
19
app: Router,
21
25
22
26
impl TestClient {
23
27
pub fn new(app: Router) -> Self {
28
+
let n = IP_COUNTER.fetch_add(1, Ordering::Relaxed);
29
+
let octet3 = (n / 256) % 256;
30
+
let octet4 = n % 256;
24
31
TestClient {
25
32
app,
26
33
cookies: HashMap::new(),
27
34
csrf_token: None,
28
-
forwarded_ip: "127.0.0.1".to_string(),
35
+
forwarded_ip: format!("10.1.{}.{}", octet3, octet4),
29
36
bearer_token: None,
30
37
}
31
38
}
496
496
h.client.post_form("/logout", "").await;
497
497
498
498
// ── Visit the public project page as anonymous user ──
499
-
let resp = h.client.get("/@tiervis/tier-vis").await;
499
+
let resp = h.client.get("/p/tier-vis").await;
500
500
assert_eq!(resp.status, 200, "Public project page should render, got {}", resp.status);
501
501
// The project page template should include the tier name
502
502
assert!(
522
522
h.client.fetch_csrf_token().await;
523
523
524
524
// Find the sandbox user
525
-
let (sandbox_user_id, is_sandbox): (i32, bool) = sqlx::query_as(
525
+
let (sandbox_user_id, is_sandbox): (uuid::Uuid, bool) = sqlx::query_as(
526
526
"SELECT id, is_sandbox FROM users WHERE username LIKE 'sandbox_%' ORDER BY created_at DESC LIMIT 1",
527
527
)
528
528
.fetch_one(&h.db)