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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
use std::{error::Error as StdError, sync::Arc};

use async_trait::async_trait;
use chrono::Duration;
use futures::TryStreamExt;
use mongodb::{
    bson::{doc, DateTime, Document},
    Database,
};
use serde::{Deserialize, Serialize};

use super::super::authorization_code::{
    AuthorizationCode, AuthorizationCodeModel, QueryCond, EXPIRES,
};

/// Model instance.
pub struct Model {
    /// The associated database connection.
    conn: Arc<Database>,
}

/// MongoDB schema.
#[derive(Deserialize, Serialize)]
struct Schema {
    code: String,
    #[serde(rename = "expiresAt")]
    expires_at: DateTime,
    #[serde(rename = "redirectUri")]
    redirect_uri: String,
    scope: Option<String>,
    #[serde(rename = "clientId")]
    client_id: String,
    #[serde(rename = "userId")]
    user_id: String,
    #[serde(rename = "createdAt")]
    created_at: DateTime,
}

const COL_NAME: &'static str = "authorizationCode";

impl Model {
    /// To create the model instance with a database connection.
    pub async fn new(conn: Arc<Database>) -> Result<Self, Box<dyn StdError>> {
        let model = Model { conn };
        model.init().await?;
        Ok(model)
    }
}

#[async_trait]
impl AuthorizationCodeModel for Model {
    async fn init(&self) -> Result<(), Box<dyn StdError>> {
        let indexes = vec![
            doc! {"name": "code_1", "key": {"code": 1}, "unique": true},
            doc! {"name": "clientId_1", "key": {"clientId": 1}},
            doc! {"name": "userId_1", "key": {"userId": 1}},
            doc! {"name": "ttl_1", "key": {"createdAt": 1}, "expireAfterSeconds": EXPIRES + 60},
        ];
        let command = doc! {
            "createIndexes": COL_NAME,
            "indexes": indexes,
        };
        self.conn.run_command(command, None).await?;
        Ok(())
    }

    async fn get(&self, code: &str) -> Result<Option<AuthorizationCode>, Box<dyn StdError>> {
        let mut cursor = self
            .conn
            .collection::<Schema>(COL_NAME)
            .find(doc! {"code": code}, None)
            .await?;
        if let Some(item) = cursor.try_next().await? {
            return Ok(Some(AuthorizationCode {
                code: item.code,
                expires_at: item.expires_at.into(),
                redirect_uri: item.redirect_uri,
                scope: item.scope,
                client_id: item.client_id,
                user_id: item.user_id,
            }));
        }
        Ok(None)
    }

    async fn add(&self, code: &AuthorizationCode) -> Result<(), Box<dyn StdError>> {
        let item = Schema {
            code: code.code.clone(),
            expires_at: code.expires_at.into(),
            redirect_uri: code.redirect_uri.clone(),
            scope: code.scope.clone(),
            client_id: code.client_id.clone(),
            user_id: code.user_id.clone(),
            created_at: (code.expires_at - Duration::seconds(EXPIRES)).into(),
        };
        self.conn
            .collection::<Schema>(COL_NAME)
            .insert_one(item, None)
            .await?;
        Ok(())
    }

    async fn del(&self, cond: &QueryCond) -> Result<(), Box<dyn StdError>> {
        let filter = get_query_filter(cond);
        self.conn
            .collection::<Schema>(COL_NAME)
            .delete_many(filter, None)
            .await?;
        Ok(())
    }
}

/// Transforms query conditions to the MongoDB document.
fn get_query_filter(cond: &QueryCond) -> Document {
    let mut filter = Document::new();
    if let Some(value) = cond.code {
        filter.insert("code", value);
    }
    if let Some(value) = cond.client_id {
        filter.insert("clientId", value);
    }
    if let Some(value) = cond.user_id {
        filter.insert("userId", value);
    }
    filter
}