summaryrefslogtreecommitdiff
path: root/src/push/app.rs
blob: 358a8cc5e967cfd830f2b7520156b4deb1744cce (plain)
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
use p256::ecdsa::VerifyingKey;
use sqlx::SqlitePool;
use web_push::SubscriptionInfo;

use super::repo::Provider as _;
use crate::{token::extract::Identity, vapid, vapid::repo::Provider as _};

pub struct Push {
    db: SqlitePool,
}

impl Push {
    pub const fn new(db: SqlitePool) -> Self {
        Self { db }
    }

    pub async fn subscribe(
        &self,
        subscriber: &Identity,
        subscription: &SubscriptionInfo,
        vapid: &VerifyingKey,
    ) -> Result<(), SubscribeError> {
        let mut tx = self.db.begin().await?;

        let current = tx.vapid().current().await?;
        if vapid != &current.key {
            return Err(SubscribeError::StaleVapidKey(current.key));
        }

        match tx.push().create(&subscriber.token, subscription).await {
            Ok(()) => (),
            Err(err) => {
                if let Some(err) = err.as_database_error()
                    && err.is_unique_violation()
                {
                    let current = tx
                        .push()
                        .by_endpoint(&subscriber.login, &subscription.endpoint)
                        .await?;
                    // If we already have a subscription for this endpoint, with _different_
                    // parameters, then this is a client error. They shouldn't reuse endpoint URLs,
                    // per the various RFCs.
                    //
                    // However, if we have a subscription for this endpoint with the same parameters
                    // then we accept it and silently do nothing. This may happen if, for example,
                    // the subscribe request is retried due to a network interruption where it's
                    // not clear whether the original request succeeded.
                    if &current != subscription {
                        return Err(SubscribeError::Duplicate);
                    }
                } else {
                    return Err(SubscribeError::Database(err));
                }
            }
        }

        tx.commit().await?;

        Ok(())
    }
}

#[derive(Debug, thiserror::Error)]
pub enum SubscribeError {
    #[error(transparent)]
    Database(#[from] sqlx::Error),
    #[error(transparent)]
    Vapid(#[from] vapid::repo::Error),
    #[error("subscription created with stale VAPID key")]
    StaleVapidKey(VerifyingKey),
    #[error("subscription already exists for endpoint")]
    // The endpoint URL is not included in the error, as it is a bearer credential in its own right
    // and we want to limit its proliferation. The only intended recipient of this message is the
    // client, which already knows the endpoint anyways and doesn't need us to tell them.
    Duplicate,
}