Skip to main content

haste_server/auth_n/oidc/routes/interactions/
password_reset.rs

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