1use std::fmt::Display;
2
3use crate::{
4 app::{App, AppTypes},
5 errors::Error,
6 hashing,
7 mail::{Challenge, Notification, issue_challenge},
8 maybe_auth::Auth,
9 secret::{PasswordHash, Secret},
10};
11
12pub trait UserID<T> {
13 fn id(&self) -> T;
15
16 fn set_id(&mut self, new_id: T);
19}
20
21#[cfg_attr(feature = "diesel", derive(diesel::prelude::QueryableByName))]
22pub struct UserData<A: AppTypes> {
23 #[cfg_attr(feature = "diesel", diesel(embed))]
24 pub user: A::User,
25
26 #[cfg_attr(feature = "diesel", diesel(deserialize_as = String), diesel(sql_type = diesel::sql_types::Text))]
27 pub password_hash: PasswordHash,
28
29 #[cfg_attr(feature = "diesel", diesel(embed))]
30 pub state: UserState,
31}
32
33#[derive(Debug, Clone, Copy)]
34#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow, sqlx::Type))]
35#[cfg_attr(feature = "diesel", derive(diesel::prelude::QueryableByName))]
36pub struct UserState {
37 #[cfg_attr(feature = "diesel", diesel(sql_type = diesel::sql_types::Bool))]
38 pub is_suspended: bool,
39 #[cfg_attr(feature = "diesel", diesel(sql_type = diesel::sql_types::Bool))]
40 pub require_email_verification: bool,
41 #[cfg_attr(feature = "diesel", diesel(sql_type = diesel::sql_types::Bool))]
42 pub require_password_change: bool,
43}
44
45impl <A: App> UserData<A> {
46 pub fn has_password(&self) -> bool {
49 self.password_hash.exists()
50 }
51}
52
53impl UserState {
54 pub(crate) fn require_ready<ID: Display>(self, user_id: ID) -> Result<(), Error> {
58 if self.is_suspended {
59 log::info!("User #{user_id} is suspended");
60 Err(Error::UserIsSuspended)
61 } else if self.require_email_verification {
62 log::info!("User #{user_id} requires email verification");
63 Err(Error::EmailNotVerified)
64 } else if self.require_password_change {
65 log::info!("User #{user_id} requires password change");
66 Err(Error::RequirePasswordChange)
67 } else {
68 Ok(())
69 }
70 }
71}
72
73pub async fn register_new_user<A: App>(
79 app: &mut A,
80 user: A::User,
81 password: Secret,
82) -> Result<A::User, A::Error> {
83 check_password_strength(app, &password)?;
84 let hash = hashing::generate_password_hash(&password)?;
85
86 register(app, user, None, hash)
87 .await
88}
89
90pub async fn register_new_user_without_password<A: App>(
96 app: &mut A,
97 user: A::User,
98) -> Result<A::User, A::Error> {
99 register(app, user, None, PasswordHash::NONE)
100 .await
101}
102
103pub async fn register_new_user_with_temporary_password<A: App>(
110 app: &mut A,
111 user: A::User,
112) -> Result<A::User, A::Error> {
113 let (password, hash) = hashing::generate_password_and_hash()?;
114
115 register(app, user, Some(password), hash)
116 .await
117}
118
119pub async fn change_password<A: App>(
120 app: &mut A,
121 auth: Auth<A>,
122 old_password: Option<Secret>,
123 new_password: Secret,
124) -> Result<(), A::Error> {
125 let user = auth.user;
126
127 if matches!(&old_password, Some(old) if old.0 == new_password.0) {
130 return Error::PasswordsNotDifferent.as_app_err();
131 }
132
133 check_password_strength(app, &new_password)?;
134
135 let data = app
137 .get_user_data_by_id(user.id())
138 .await
139 .map_err(Into::into)?
140 .ok_or(Error::UserDataQueryFailed {user_id: user.id().into()})?;
141
142 if data.password_hash.exists() {
144 if let Some(old_password) = old_password {
145 hashing::verify_password(&data.password_hash, &old_password)?;
147 } else {
148 return Error::IncorrectPassword.as_app_err();
150 }
151 }
152
153 let new_hash = hashing::generate_password_hash(&new_password)?;
155 app.update_password(&user, new_hash, false)
156 .await
157 .map_err(Into::into)?;
158
159 app.send_notification(&user, Notification::PasswordChanged)
162 .await
163 .map_err(Into::into)?;
164
165 Ok(())
166}
167
168pub async fn request_password_reset<A: App>(app: &mut A, user: &A::User) -> Result<(), A::Error> {
169 issue_challenge(app, user, Challenge::ResetPassword)
170 .await
171}
172
173fn check_password_strength<A: App>(app: &mut A, password: &Secret) -> Result<(), Error> {
174 if password.0.len() < app.minimum_password_length() {
175 return Err(Error::PasswordTooShort);
176 }
177 Ok(())
178}
179
180async fn register<A: App>(
181 app: &mut A,
182 mut user: A::User,
183 temporary_password: Option<Secret>,
184 password_hash: PasswordHash,
185) -> Result<A::User, A::Error> {
186 let user_data = UserData {
189 user: user.clone(),
190 password_hash,
191 state: UserState {
192 is_suspended: false,
193 require_password_change: temporary_password.is_some(),
194 require_email_verification: temporary_password.is_none(),
195 },
196 };
197 let user_id = app.insert_user(user_data)
198 .await
199 .map_err(Into::into)?;
200
201 user.set_id(user_id);
203
204 let result = match temporary_password {
206 Some(temporary_password) => {
207 let notification = Notification::UserRegistered {temporary_password};
208 app.send_notification(&user, notification)
209 .await
210 .map_err(Into::into)
211 },
212 None => {
213 issue_challenge(app, &user, Challenge::VerifyNewUser)
214 .await
215 .map_err(Into::into)
216 }
217 };
218
219 if let Err(e) = result {
220 app.delete_user(user_id)
223 .await
224 .map_err(Into::into)?;
225
226 return Err(e);
227 }
228
229 Ok(user)
230}