aboutsummaryrefslogtreecommitdiff
path: root/src/db/models/two_factor_incomplete.rs
blob: 49f7691f1a550b019104a5bc2c465cce3ca9da07 (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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
use chrono::{NaiveDateTime, Utc};

use crate::{api::EmptyResult, auth::ClientIp, db::DbConn, error::MapResult, CONFIG};

db_object! {
    #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
    #[diesel(table_name = twofactor_incomplete)]
    #[diesel(primary_key(user_uuid, device_uuid))]
    pub struct TwoFactorIncomplete {
        pub user_uuid: String,
        // This device UUID is simply what's claimed by the device. It doesn't
        // necessarily correspond to any UUID in the devices table, since a device
        // must complete 2FA login before being added into the devices table.
        pub device_uuid: String,
        pub device_name: String,
        pub login_time: NaiveDateTime,
        pub ip_address: String,
    }
}

impl TwoFactorIncomplete {
    pub async fn mark_incomplete(
        user_uuid: &str,
        device_uuid: &str,
        device_name: &str,
        ip: &ClientIp,
        conn: &mut DbConn,
    ) -> EmptyResult {
        if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
            return Ok(());
        }

        // Don't update the data for an existing user/device pair, since that
        // would allow an attacker to arbitrarily delay notifications by
        // sending repeated 2FA attempts to reset the timer.
        let existing = Self::find_by_user_and_device(user_uuid, device_uuid, conn).await;
        if existing.is_some() {
            return Ok(());
        }

        db_run! { conn: {
            diesel::insert_into(twofactor_incomplete::table)
                .values((
                    twofactor_incomplete::user_uuid.eq(user_uuid),
                    twofactor_incomplete::device_uuid.eq(device_uuid),
                    twofactor_incomplete::device_name.eq(device_name),
                    twofactor_incomplete::login_time.eq(Utc::now().naive_utc()),
                    twofactor_incomplete::ip_address.eq(ip.ip.to_string()),
                ))
                .execute(conn)
                .map_res("Error adding twofactor_incomplete record")
        }}
    }

    pub async fn mark_complete(user_uuid: &str, device_uuid: &str, conn: &mut DbConn) -> EmptyResult {
        if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
            return Ok(());
        }

        Self::delete_by_user_and_device(user_uuid, device_uuid, conn).await
    }

    pub async fn find_by_user_and_device(user_uuid: &str, device_uuid: &str, conn: &mut DbConn) -> Option<Self> {
        db_run! { conn: {
            twofactor_incomplete::table
                .filter(twofactor_incomplete::user_uuid.eq(user_uuid))
                .filter(twofactor_incomplete::device_uuid.eq(device_uuid))
                .first::<TwoFactorIncompleteDb>(conn)
                .ok()
                .from_db()
        }}
    }

    pub async fn find_logins_before(dt: &NaiveDateTime, conn: &mut DbConn) -> Vec<Self> {
        db_run! {conn: {
            twofactor_incomplete::table
                .filter(twofactor_incomplete::login_time.lt(dt))
                .load::<TwoFactorIncompleteDb>(conn)
                .expect("Error loading twofactor_incomplete")
                .from_db()
        }}
    }

    pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
        Self::delete_by_user_and_device(&self.user_uuid, &self.device_uuid, conn).await
    }

    pub async fn delete_by_user_and_device(user_uuid: &str, device_uuid: &str, conn: &mut DbConn) -> EmptyResult {
        db_run! { conn: {
            diesel::delete(twofactor_incomplete::table
                           .filter(twofactor_incomplete::user_uuid.eq(user_uuid))
                           .filter(twofactor_incomplete::device_uuid.eq(device_uuid)))
                .execute(conn)
                .map_res("Error in twofactor_incomplete::delete_by_user_and_device()")
        }}
    }

    pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
        db_run! { conn: {
            diesel::delete(twofactor_incomplete::table.filter(twofactor_incomplete::user_uuid.eq(user_uuid)))
                .execute(conn)
                .map_res("Error in twofactor_incomplete::delete_all_by_user()")
        }}
    }
}