1#[cfg(feature = "diesel")]
2use diesel::{prelude::*, sql_types::*};
3
4use std::fmt::Display;
5
6use crate::{
7 app::{App, AppTypes},
8 errors::Error,
9 hashing,
10 mail::{Challenge, Notification, issue_challenge},
11 maybe_auth::Auth,
12 secret::{PasswordHash, Secret},
13};
14
15pub trait UserID<T> {
16 fn id(&self) -> T;
18
19 fn set_id(&mut self, new_id: T);
22
23 fn identifier(&self) -> &str;
25}
26
27#[derive(Debug, Clone, Copy)]
28#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow, sqlx::Type))]
29#[cfg_attr(feature = "diesel", derive(QueryableByName))]
30pub struct UserState {
31 #[cfg_attr(feature = "diesel", diesel(sql_type = Bool))]
32 pub has_password: bool,
33
34 #[cfg_attr(feature = "diesel", diesel(sql_type = Bool))]
35 pub is_suspended: bool,
36
37 #[cfg_attr(feature = "diesel", diesel(sql_type = Bool))]
38 pub require_email_verification: bool,
39
40 #[cfg_attr(feature = "diesel", diesel(sql_type = Bool))]
41 pub require_password_change: bool,
42}
43
44impl UserState {
45 pub fn is_ready(self) -> bool {
49 !self.is_suspended && !self.require_email_verification && !self.require_password_change
50 }
51
52 pub(crate) fn require_ready<ID: Display>(&self, id: ID) -> Result<(), Error> {
55 if self.is_suspended {
56 log::debug!("User {id} is suspended");
57 Err(Error::UserIsSuspended)
58 } else if self.require_email_verification {
59 log::debug!("User {id} requires email verification");
60 Err(Error::EmailNotVerified)
61 } else if self.require_password_change {
62 log::debug!("User {id} requires password change");
63 Err(Error::RequirePasswordChange)
64 } else {
65 Ok(())
66 }
67 }
68}
69
70impl Display for UserState {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 let mut any = false;
73
74 if self.is_suspended {
75 f.write_str("suspended")?;
76 any = true;
77 }
78
79 if self.require_email_verification {
80 if any { f.write_str(", ")?; }
81 f.write_str("unverified")?;
82 any = true;
83 }
84
85 if self.require_password_change {
86 if any { f.write_str(", ")?; }
87 f.write_str("must change password")?;
88 any = true;
89 }
90
91 if !any {
92 f.write_str("ready")?;
93 }
94
95 Ok(())
96 }
97}
98
99#[cfg_attr(feature = "diesel", derive(QueryableByName))]
100pub struct UserData<A: AppTypes> {
101 #[cfg_attr(feature = "diesel", diesel(embed))]
102 pub user: A::User,
103
104 #[cfg_attr(feature = "diesel", diesel(deserialize_as = Option<String>), diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>))]
105 pub password_hash: PasswordHash,
106
107 #[cfg_attr(feature = "diesel", diesel(embed))]
108 pub state: UserState,
109}
110
111impl <A: App> UserData<A> {
112 pub fn has_password(&self) -> bool {
115 self.password_hash.exists()
116 }
117}
118
119pub async fn register_new_user<A: App>(
125 app: &mut A,
126 user: A::User,
127 password: Secret,
128) -> Result<RegistrationOutcome<A>, A::Error> {
129 let len = password.expose().len();
130 let hash = hashing::generate_password_hash(&password)?;
131
132 register(app, user, Some(len), None, hash)
133 .await
134}
135
136pub async fn register_new_user_without_password<A: App>(
142 app: &mut A,
143 user: A::User,
144) -> Result<RegistrationOutcome<A>, A::Error> {
145 register(app, user, None, None, PasswordHash::NONE)
146 .await
147}
148
149pub async fn register_new_user_with_temporary_password<A: App>(
156 app: &mut A,
157 user: A::User,
158) -> Result<RegistrationOutcome<A>, A::Error> {
159 let (password, hash) = hashing::generate_password_and_hash()?;
160
161 register(app, user, None, Some(password), hash)
162 .await
163}
164
165pub enum PasswordChangeOutcome {
166 Success,
168
169 IncorrectPassword,
171
172 NewPasswordTooShort,
175
176 PasswordsNotDifferent,
179}
180
181pub async fn change_password<A: App>(
182 app: &mut A,
183 auth: Auth<A>,
184 old_password: Option<Secret>,
185 new_password: Secret,
186) -> Result<PasswordChangeOutcome, A::Error> {
187 let user = auth.user;
188
189 if matches!(&old_password, Some(old) if old.0 == new_password.0) {
192 return Ok(PasswordChangeOutcome::PasswordsNotDifferent);
193 }
194
195 if new_password.0.len() < app.minimum_password_length() {
197 return Ok(PasswordChangeOutcome::NewPasswordTooShort);
198 }
199
200 let data = app
202 .get_user_data_by_id(user.id())
203 .await
204 .map_err(Into::into)?
205 .ok_or(Error::UserDataQueryFailed {user_id: user.id().into()})?;
206
207 if data.password_hash.exists() {
209 if let Some(old_password) = old_password {
210 let result = hashing::check_password(&data.password_hash, &old_password)?;
212 if !result {
213 return Ok(PasswordChangeOutcome::IncorrectPassword);
214 }
215 } else {
216 return Ok(PasswordChangeOutcome::IncorrectPassword);
218 }
219 }
220
221 let new_hash = hashing::generate_password_hash(&new_password)?;
223 app.update_password(&user, new_hash, false)
224 .await
225 .map_err(Into::into)?;
226
227 app.send_notification(&user, Notification::PasswordChanged)
230 .await
231 .map_err(Into::into)?;
232
233 Ok(PasswordChangeOutcome::Success)
234}
235
236pub async fn request_password_reset<A: App>(app: &mut A, user: &A::User) -> Result<(), A::Error> {
237 issue_challenge(app, user, Challenge::ResetPassword)
238 .await
239}
240
241pub enum RegistrationOutcome<A: App> {
242 Success(A::User),
244
245 IdentifierAlreadyExists,
248
249 PasswordTooShort,
252}
253
254async fn register<A: App>(
255 app: &mut A,
256 mut user: A::User,
257 chosen_password_length: Option<usize>,
258 temporary_password: Option<Secret>,
259 password_hash: PasswordHash,
260) -> Result<RegistrationOutcome<A>, A::Error> {
261 let result = app.user_identifier_exists(user.identifier())
262 .await
263 .map_err(Into::into)?;
264 if result {
265 return Ok(RegistrationOutcome::IdentifierAlreadyExists);
266 }
267
268 if matches!(chosen_password_length, Some(n) if n < app.minimum_password_length()) {
269 return Ok(RegistrationOutcome::PasswordTooShort);
270 }
271
272 let user_data = UserData {
275 user: user.clone(),
276 state: UserState {
277 has_password: password_hash.exists(),
278 is_suspended: false,
279 require_password_change: temporary_password.is_some(),
280 require_email_verification: temporary_password.is_none(),
281 },
282 password_hash,
283 };
284 let user_id = app.insert_user(user_data)
285 .await
286 .map_err(Into::into)?;
287
288 user.set_id(user_id);
290
291 let result = match temporary_password {
293 Some(temporary_password) => {
294 let notification = Notification::UserRegistered {temporary_password};
295 app.send_notification(&user, notification)
296 .await
297 .map_err(Into::into)
298 },
299 None => {
300 issue_challenge(app, &user, Challenge::VerifyNewUser)
301 .await
302 .map_err(Into::into)
303 }
304 };
305
306 if let Err(e) = result {
307 app.delete_user(user_id)
310 .await
311 .map_err(Into::into)?;
312
313 return Err(e);
314 }
315
316 Ok(RegistrationOutcome::Success(user))
317}