lesspass_client/
model.rs

1//
2// lesspass-client model.rs
3// Copyright (C) 2021-2025 Óscar García Amor <ogarcia@connectical.com>
4// Distributed under terms of the GNU GPLv3 license.
5//
6
7use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
8use serde::{Deserialize, Deserializer, Serialize};
9
10/// To store the authentication response
11#[derive(Debug, Deserialize)]
12pub struct Token {
13    pub access: String,
14    pub refresh: String
15}
16
17/// To perform authentication and create new users
18#[derive(Debug, Serialize)]
19pub struct Auth {
20    pub email: String,
21    pub password: String
22}
23
24/// To perform the token refresh
25#[derive(Debug, Serialize)]
26pub struct Refresh {
27    pub refresh: String
28}
29
30/// To store the user info
31#[derive(Debug, Deserialize)]
32pub struct User {
33    #[serde(deserialize_with = "id_deserializer")]
34    pub id: String,
35    pub email: String
36}
37
38/// To delete user
39#[derive(Debug, Serialize)]
40pub struct UserPassword {
41    pub current_password: String
42}
43
44/// To change the password for a user
45#[derive(Debug, Serialize)]
46pub struct UserChangePassword {
47    pub current_password: String,
48    pub new_password: String
49}
50
51/// To create a new password entry
52#[derive(Debug, Deserialize, Serialize)]
53pub struct NewPassword {
54    pub site: String,
55    pub login: String,
56    pub lowercase: bool,
57    pub uppercase: bool,
58    pub symbols: bool,
59    pub digits: bool,
60    pub length: u8,
61    pub counter: u32,
62    pub version: u8
63}
64
65/// To store the password list
66#[derive(Clone, Debug, Deserialize, Serialize)]
67pub struct Passwords {
68    pub count: u32,
69    // API implementation does not use (for now) previous and next
70    // so are commented to avoid use memory with garbage
71    //
72    // pub previous: Option<u8>,
73    // pub next: Option<u8>,
74    pub results: Vec<Password>
75}
76
77/// A password item in the password list
78#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
79pub struct Password {
80    pub id: String,
81    pub site: String,
82    pub login: String,
83    pub lowercase: bool,
84    pub uppercase: bool,
85    pub symbols: bool,
86    pub digits: bool,
87    pub length: u8,
88    pub counter: u32,
89    pub version: u8,
90    pub created: DateTime<Utc>,
91    pub modified: DateTime<Utc>
92}
93
94/// The numbers field is deprecated and has been replaced by digits so depending on the
95/// implementation the response may contain the first, the second or even both. We deserialize to
96/// an intermediate structure with both fields and return the final structure with only the digits
97/// field (which takes precedence over numbers).
98impl<'de> Deserialize<'de> for Password {
99    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, {
100        #[derive(Deserialize)]
101        struct RawPassword {
102            #[serde(deserialize_with = "id_deserializer")]
103            id: String,
104            site: String,
105            login: String,
106            lowercase: bool,
107            uppercase: bool,
108            symbols: bool,
109            digits: Option<bool>,
110            numbers: Option<bool>,
111            length: u8,
112            counter: u32,
113            version: u8,
114            #[serde(deserialize_with = "date_deserializer")]
115            created: DateTime<Utc>,
116            #[serde(deserialize_with = "date_deserializer")]
117            modified: DateTime<Utc>
118        }
119        let RawPassword {id, site, login, lowercase, uppercase, symbols, digits, numbers, length, counter, version, created, modified} = RawPassword::deserialize(deserializer)?;
120        let digits = digits.or(numbers).ok_or(serde::de::Error::missing_field("digits or numbers"))?;
121        Ok(Password {id, site, login, lowercase, uppercase, symbols, digits, length, counter, version, created, modified})
122    }
123}
124
125/// Some server implementations (like Rockpass) store IDs in simple integers instead of strings,
126/// this function deserializes unsigned integers or strings.
127fn id_deserializer<'de, D>(deserializer: D) -> Result<String, D::Error> where D: Deserializer<'de>, {
128    #[derive(Deserialize)]
129    #[serde(untagged)]
130    enum StringOrInteger {
131        String(String),
132        Integer(u64)
133    }
134    match StringOrInteger::deserialize(deserializer)? {
135        StringOrInteger::String(string) => Ok(string),
136        StringOrInteger::Integer(integer) => Ok(integer.to_string())
137    }
138}
139
140/// Some server implementations (like Rockpass) store dates in NaiveDateTime,
141/// this function deserializes NaiveDateTime and DateTime with TimeZone.
142fn date_deserializer<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error> where D: Deserializer<'de>, {
143    let s = String::deserialize(deserializer)?;
144    match s.parse::<NaiveDateTime>() {
145        Ok(date) => Ok(Utc.from_utc_datetime(&date)),
146        Err(_) => s.parse::<DateTime<Utc>>().map_err(serde::de::Error::custom)
147    }
148}
149
150/// Turns a NewPassword into a Password using empty string for id and current date for created and
151/// modified fields
152impl From<NewPassword> for Password {
153    fn from(new_password: NewPassword) -> Self {
154        Password {
155            id: String::new(),
156            site: new_password.site,
157            login: new_password.login,
158            lowercase: new_password.lowercase,
159            uppercase: new_password.uppercase,
160            symbols: new_password.symbols,
161            digits: new_password.digits,
162            length: new_password.length,
163            counter: new_password.counter,
164            version: new_password.version,
165            created: Utc::now(),
166            modified: Utc::now()
167        }
168    }
169}