1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
|
use crate::multiplayer::{new_registry, RoomRegistry};
use crate::rate_limit::RateLimiter;
use reqwest::Client;
use sqlx::PgPool;
use std::time::Duration;
#[derive(Clone, Default)]
pub struct SpotifyConfig {
pub client_id: String,
pub client_secret: String,
pub redirect_uri: String,
}
impl SpotifyConfig {
pub fn is_configured(&self) -> bool {
!self.client_id.is_empty() && !self.client_secret.is_empty()
}
}
/// A passage a race can be run on, with whoever wrote it. The attribution
/// travels with the text so every racer can see where the passage came from,
/// the same way single player does.
#[derive(Debug, Clone)]
pub struct RaceText {
pub text: String,
pub attribution: Option<String>,
/// The pack it came from. Shown when a passage has no attribution of its
/// own, so the results screen says where the text is from either way.
pub category: Option<String>,
}
#[derive(Clone)]
pub struct AppState {
pub db: PgPool,
pub auth_rate_limiter: RateLimiter,
/// Keyed by user id, not IP - this guards an authenticated endpoint
/// (stats submission) against a single compromised/scripted account
/// hammering it, which an IP-keyed limiter wouldn't catch behind NAT or
/// a VPN and would over-punish for a shared IP.
pub stats_rate_limiter: RateLimiter<String>,
/// /api/lyrics forwards to a third party on the caller's behalf, so it is
/// an open proxy unless it is bounded. Keyed by IP, since the endpoint is
/// reachable without an account.
pub lyrics_rate_limiter: RateLimiter,
/// Keyed by user: enough for someone contributing in a sitting, not
/// enough for a script to fill the moderation queue.
pub submission_rate_limiter: RateLimiter<String>,
/// Set from COOKIE_SECURE. Off for plain-HTTP local dev, must be on
/// behind TLS in production or browsers silently drop the cookie.
pub cookie_secure: bool,
pub rooms: RoomRegistry,
/// Race passages every multiplayer room draws from, so every player in
/// a room types the identical text - loaded once at startup rather
/// than per-room, since the pool itself never changes at runtime.
pub race_texts: Vec<RaceText>,
pub spotify: SpotifyConfig,
pub stripe: crate::billing::StripeConfig,
pub frontend_origin: String,
pub http: Client,
}
impl AppState {
pub fn new(
db: PgPool,
cookie_secure: bool,
race_texts: Vec<RaceText>,
spotify: SpotifyConfig,
stripe: crate::billing::StripeConfig,
frontend_origin: String,
) -> Self {
Self {
db,
stripe,
auth_rate_limiter: RateLimiter::new(10, Duration::from_secs(5 * 60)),
// A genuine player finishes a test at most every several
// seconds; 60 submissions in 5 minutes is generous headroom for
// rapid Words-10 sessions while still capping scripted spam.
stats_rate_limiter: RateLimiter::new(60, Duration::from_secs(5 * 60)),
lyrics_rate_limiter: RateLimiter::new(30, Duration::from_secs(60)),
submission_rate_limiter: RateLimiter::new(20, Duration::from_secs(60 * 60)),
cookie_secure,
rooms: new_registry(),
race_texts,
spotify,
frontend_origin,
http: Client::new(),
}
}
}
|