srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/server/src/state.rs
blob: f2a38ade7c739f92a54f02ba170f66209ef33d63 (plain) (blame)
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(),
        }
    }
}