1use crate::{
2 auth_n::{
3 email::{Message, send_password_reset_email},
4 oidc::{hardcoded_clients::admin_app, utilities::set_user_password},
5 },
6 extract::{
7 csrf_token::CSRFToken,
8 path_tenant::{Project, ProjectIdentifier},
9 },
10 services::ServerState,
11 ui::{
12 components::TenantContext,
13 pages::{self, message::message_html},
14 },
15};
16use axum::{
17 Form,
18 extract::{OriginalUri, Query, State},
19};
20use axum_extra::{extract::Cached, routing::TypedPath};
21use haste_fhir_model::r4::generated::terminology::IssueType;
22use haste_fhir_operation_error::OperationOutcomeError;
23use haste_fhir_search::SearchEngine;
24use haste_fhir_terminology::FHIRTerminology;
25use haste_repository::{
26 Repository,
27 admin::{ProjectModelAdmin, TenantModelAdmin},
28 types::{
29 authorization_code::{AuthorizationCodeKind, CreateAuthorizationCode},
30 user::{AuthMethod, CreateUser, UserSearchClauses},
31 },
32};
33use maud::{Markup, html};
34use serde::Deserialize;
35use std::sync::Arc;
36
37#[derive(TypedPath)]
38#[typed_path("/password-reset")]
39pub struct PasswordResetInitiate;
40
41pub async fn password_reset_initiate_get(
42 _: PasswordResetInitiate,
43 Cached(TenantContext { tenant, branding }): Cached<TenantContext>,
44 Cached(Project(project)): Cached<Project>,
45 CSRFToken(csrf_token): CSRFToken,
46 uri: OriginalUri,
47) -> Result<Markup, OperationOutcomeError> {
48 let response = pages::email_form::email_form_html(
49 &tenant,
50 Some(&project),
51 &csrf_token,
52 &pages::email_form::EmailInformation {
53 continue_url: uri.path().to_string(),
54 },
55 Some(&branding),
56 );
57
58 Ok(response)
59}
60
61#[allow(unused)]
62#[derive(Deserialize)]
63pub struct PasswordResetFormInitiate {
64 pub csrf_token: String,
65 pub email: String,
66}
67
68pub async fn password_reset_initiate_post<
69 Repo: Repository + Send + Sync,
70 Search: SearchEngine + Send + Sync,
71 Terminology: FHIRTerminology + Send + Sync,
72>(
73 _: PasswordResetInitiate,
74 Cached(TenantContext { tenant, branding }): Cached<TenantContext>,
75 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
76 project_resource: Project,
77 State(state): State<Arc<ServerState<Repo, Search, Terminology>>>,
78 CSRFToken(csrf_token): CSRFToken,
79 form: axum::extract::Form<PasswordResetFormInitiate>,
80) -> Result<Markup, OperationOutcomeError> {
81 if form.csrf_token != csrf_token {
82 return Err(OperationOutcomeError::error(
83 IssueType::invalid(),
84 "Invalid CSRF Token".to_string(),
85 ));
86 }
87
88 let user_search_results = TenantModelAdmin::search(
89 &*state.repo,
90 &tenant,
91 &UserSearchClauses {
92 email: Some(form.email.clone()),
93 role: None,
94 method: Some(AuthMethod::EmailPassword),
95 },
96 )
97 .await?;
98
99 if let Some(user) = user_search_results.into_iter().next() {
100 send_password_reset_email(state.as_ref(), &tenant, &project, &user, Message::default())
101 .await?;
102
103 Ok(message_html(
104 Some(&tenant),
105 Some(&project_resource.0),
106 &html! {"An email will arrive in the next few minutes with the next steps to reset your password."},
107 Some(&branding),
108 ))
109 } else {
110 Err(OperationOutcomeError::error(
111 IssueType::not_found(),
112 "No user found with provided email address.".to_string(),
113 ))?
114 }
115}
116
117#[derive(TypedPath)]
118#[typed_path("/password-reset-verify")]
119pub struct PasswordResetVerify;
120
121#[derive(Deserialize)]
122pub struct PasswordResetVerifyQuery {
123 code: String,
124}
125
126pub async fn password_reset_verify_get<
127 Repo: Repository + Send + Sync,
128 Search: SearchEngine + Send + Sync,
129 Terminology: FHIRTerminology + Send + Sync,
130>(
131 _: PasswordResetVerify,
132 uri: OriginalUri,
133 query: Query<PasswordResetVerifyQuery>,
134 Cached(TenantContext { tenant, branding }): Cached<TenantContext>,
135 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
136 Cached(Project(project_resource)): Cached<Project>,
137 CSRFToken(csrf_token): CSRFToken,
138 State(state): State<Arc<ServerState<Repo, Search, Terminology>>>,
139) -> Result<Markup, OperationOutcomeError> {
140 if let Some(code) = ProjectModelAdmin::<CreateAuthorizationCode, _, _, _, _>::read(
141 &*state.repo,
142 &tenant,
143 &project,
144 &query.code,
145 )
146 .await?
147 {
148 if code.kind != AuthorizationCodeKind::PasswordReset {
149 return Err(OperationOutcomeError::error(
150 IssueType::not_found(),
151 "Invalid Password reset code.".to_string(),
152 ));
153 }
154 if code.is_expired.unwrap_or(true) {
155 return Err(OperationOutcomeError::fatal(
156 IssueType::invalid(),
157 "Password reset code has expired.".to_string(),
158 ));
159 }
160 Ok(message_html(
161 Some(&tenant),
162 Some(&project_resource),
163 &html! {
164 div {}
165 h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl "{
166 "Set your password"}
167 form class="space-y-4 md:space-y-6" action=(uri.path().to_string()) method="POST"{
168 input type="hidden" id="code" name="code" value=(query.code) {}
169 input type="hidden" name="csrf_token" value=(csrf_token) {}
170 label for="password" class="block mb-2 text-sm font-medium text-gray-900"{"Enter your Password"}
171 input type="password" id="password" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-brand-600 focus:border-brand-600 block w-full p-2.5" required="" name="password" {}
172 label for="password_confirm" class="block mb-2 text-sm font-medium text-gray-900" {"Confirm your Password"}
173 input type="password" id="password_confirm" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-brand-600 focus:border-brand-600 block w-full p-2.5" required="" name="password_confirm" {}
174 button type="submit" class="cursor-pointer w-full text-white bg-brand-600 hover:bg-brand-500 focus:ring-4 focus:outline-none focus:ring-brand-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center"{"Continue"}
175 }
176 },
177 Some(&branding),
178 ))
179 } else {
180 Err(OperationOutcomeError::error(
181 IssueType::not_found(),
182 "Invalid Password reset code.".to_string(),
183 ))?
184 }
185}
186
187#[derive(Deserialize)]
188pub struct PasswordVerifyPOSTBODY {
189 csrf_token: String,
190 code: String,
191 password: String,
192 password_confirm: String,
193}
194
195pub async fn password_reset_verify_post<
196 Repo: Repository + Send + Sync,
197 Search: SearchEngine + Send + Sync,
198 Terminology: FHIRTerminology + Send + Sync,
199>(
200 _: PasswordResetVerify,
201 Cached(TenantContext { tenant, branding }): Cached<TenantContext>,
202 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
203 Cached(Project(project_resource)): Cached<Project>,
204 State(state): State<Arc<ServerState<Repo, Search, Terminology>>>,
205 CSRFToken(csrf_token): CSRFToken,
206 Form(body): Form<PasswordVerifyPOSTBODY>,
207) -> Result<Markup, OperationOutcomeError> {
208 if body.csrf_token != csrf_token {
209 return Err(OperationOutcomeError::error(
210 IssueType::invalid(),
211 "Invalid CSRF Token".to_string(),
212 ));
213 }
214
215 if body.password != body.password_confirm {
216 return Err(OperationOutcomeError::error(
217 IssueType::invalid(),
218 "Passwords do not match.".to_string(),
219 ));
220 }
221
222 if let Some(code) = ProjectModelAdmin::<CreateAuthorizationCode, _, _, _, _>::read(
223 &*state.repo,
224 &tenant,
225 &project,
226 &body.code,
227 )
228 .await?
229 {
230 if code.kind != AuthorizationCodeKind::PasswordReset {
231 return Err(OperationOutcomeError::error(
232 IssueType::not_found(),
233 "Invalid Password reset code.".to_string(),
234 ));
235 }
236 ProjectModelAdmin::<CreateAuthorizationCode, _, _, _, _>::delete(
237 &*state.repo,
238 &tenant,
239 &project,
240 &body.code,
241 )
242 .await?;
243 if code.is_expired.unwrap_or(true) {
244 return Err(OperationOutcomeError::fatal(
245 IssueType::invalid(),
246 "Password reset code has expired.".to_string(),
247 ));
248 }
249
250 let Some(user) =
251 TenantModelAdmin::<CreateUser, _, _, _, _>::read(&*state.repo, &tenant, &code.user_id)
252 .await?
253 else {
254 return Err(OperationOutcomeError::error(
255 IssueType::not_found(),
256 "User not found.".to_string(),
257 ));
258 };
259
260 let email = user.email.as_ref().ok_or_else(|| {
261 OperationOutcomeError::fatal(
262 IssueType::invalid(),
263 "User does not have an email associated.".to_string(),
264 )
265 })?;
266
267 set_user_password(&*state.repo, &tenant, email, &user.id, &body.password).await?;
268
269 let admin_app_url = admin_app::redirect_url(state.config.as_ref(), &tenant, &project);
270
271 Ok(message_html(
272 Some(&tenant),
273 Some(&project_resource),
274 &html! { span {
275 "Password has been reset successfully. "
276 @if let Some(admin_app_url) = admin_app_url {
277 "Go to the Admin App "
278 a class="hover:underline cursor-pointer text-brand-600" href=(admin_app_url) { "here" }
279 "."
280 }
281 }
282 },
283 Some(&branding),
284 ))
285 } else {
286 Err(OperationOutcomeError::error(
287 IssueType::not_found(),
288 "Invalid Password reset code.".to_string(),
289 ))?
290 }
291}