1use crate::config::AuthModule;
8use crate::error::AuthError;
9use crate::state::try_global;
10use doido_model::sea_orm::{ConnectionTrait, DatabaseConnection, DbBackend, Statement, Value};
11
12struct Settings {
13 maximum_attempts: u32,
14 unlock_in: i64,
15}
16
17fn settings() -> Option<Settings> {
19 let state = try_global()?;
20 if !state.config.has_module(AuthModule::Lockable) {
21 return None;
22 }
23 Some(Settings {
24 maximum_attempts: state.config.maximum_attempts,
25 unlock_in: state.config.unlock_in,
26 })
27}
28
29async fn locked_at(db: &DatabaseConnection, email: &str) -> Result<Option<String>, AuthError> {
30 let row = db
31 .query_one_raw(Statement::from_sql_and_values(
32 DbBackend::Sqlite,
33 "SELECT locked_at FROM users WHERE email = ?",
34 [Value::from(email.to_string())],
35 ))
36 .await
37 .map_err(|e| AuthError::Internal(e.to_string()))?;
38 match row {
39 Some(row) => Ok(row
40 .try_get::<Option<String>>("", "locked_at")
41 .map_err(|e| AuthError::Internal(e.to_string()))?),
42 None => Ok(None),
43 }
44}
45
46async fn exec(db: &DatabaseConnection, sql: &str, email: &str) -> Result<(), AuthError> {
47 db.execute_raw(Statement::from_sql_and_values(
48 DbBackend::Sqlite,
49 sql,
50 [Value::from(email.to_string())],
51 ))
52 .await
53 .map_err(|e| AuthError::Internal(e.to_string()))?;
54 Ok(())
55}
56
57pub async fn ensure_not_locked(db: &DatabaseConnection, email: &str) -> Result<(), AuthError> {
61 let settings = match settings() {
62 Some(s) => s,
63 None => return Ok(()),
64 };
65 let locked = match locked_at(db, email).await? {
66 Some(ts) => ts,
67 None => return Ok(()),
68 };
69 let locked_time = chrono::DateTime::parse_from_rfc3339(&locked)
70 .map(|t| t.with_timezone(&chrono::Utc))
71 .map_err(|e| AuthError::Internal(e.to_string()))?;
72 if (chrono::Utc::now() - locked_time).num_seconds() >= settings.unlock_in {
73 unlock(db, email).await?;
75 Ok(())
76 } else {
77 Err(AuthError::AccountLocked)
78 }
79}
80
81pub async fn record_failure(db: &DatabaseConnection, email: &str) -> Result<(), AuthError> {
84 let settings = match settings() {
85 Some(s) => s,
86 None => return Ok(()),
87 };
88 exec(
89 db,
90 "UPDATE users SET failed_attempts = failed_attempts + 1 WHERE email = ?",
91 email,
92 )
93 .await?;
94
95 let row = db
96 .query_one_raw(Statement::from_sql_and_values(
97 DbBackend::Sqlite,
98 "SELECT failed_attempts FROM users WHERE email = ?",
99 [Value::from(email.to_string())],
100 ))
101 .await
102 .map_err(|e| AuthError::Internal(e.to_string()))?;
103 let attempts = match row {
104 Some(row) => row
105 .try_get::<i32>("", "failed_attempts")
106 .map_err(|e| AuthError::Internal(e.to_string()))?,
107 None => return Ok(()),
108 };
109
110 if attempts as u32 >= settings.maximum_attempts {
111 db.execute_raw(Statement::from_sql_and_values(
112 DbBackend::Sqlite,
113 "UPDATE users SET locked_at = ? WHERE email = ?",
114 [
115 Value::from(chrono::Utc::now().to_rfc3339()),
116 Value::from(email.to_string()),
117 ],
118 ))
119 .await
120 .map_err(|e| AuthError::Internal(e.to_string()))?;
121 }
122 Ok(())
123}
124
125pub async fn reset_attempts(db: &DatabaseConnection, email: &str) -> Result<(), AuthError> {
128 if settings().is_none() {
129 return Ok(());
130 }
131 unlock(db, email).await
132}
133
134async fn unlock(db: &DatabaseConnection, email: &str) -> Result<(), AuthError> {
135 exec(
136 db,
137 "UPDATE users SET failed_attempts = 0, locked_at = NULL WHERE email = ?",
138 email,
139 )
140 .await
141}