1mod pages;
6
7use std::{
8 any::{Any, TypeId},
9 future::Future,
10 marker::PhantomData,
11 time::Duration,
12};
13
14use topcoat::{
15 context::Cx,
16 router::{
17 Body, RouteFuture,
18 error::{see_other, too_many_requests},
19 response::IntoResponse,
20 },
21};
22
23pub(super) use self::pages::invalid_link;
24use self::pages::notice;
25use super::{
26 LoginThrottle, PanelUser, infrastructure_failure,
27 login::login_url,
28 mail::{after_response, private, token_from_query},
29 panel_root, resolved,
30 session::resolve,
31 token::{self, TokenPurpose, run_serializable},
32 unauthenticated_error,
33};
34use crate::{
35 BoxFuture,
36 auth::realm::{Realm, current_realm, hosted, panel_prefix},
37 flash::set_notification,
38 outcome::Notification,
39};
40
41pub const VERIFICATION_LIFETIME: Duration = Duration::from_hours(24);
43
44pub trait EmailVerifier: Send + Sync + 'static {
98 type User: PanelUser;
101
102 fn is_verified(&self, user: &Self::User) -> bool;
105
106 fn store_verified_email(
113 &self,
114 cx: &Cx,
115 user_id: &str,
116 email: &str,
117 ex: &mut dyn toasty::Executor,
118 ) -> impl Future<Output = topcoat::Result<()>> + Send;
119}
120
121pub(crate) trait DynEmailVerifier: Send + Sync {
123 fn is_verified(&self, user: &dyn PanelUser) -> bool;
125 fn store_verified_email<'a>(
126 &'a self,
127 cx: &'a Cx,
128 user_id: &'a str,
129 email: &'a str,
130 ex: &'a mut dyn toasty::Executor,
131 ) -> BoxFuture<'a, topcoat::Result<()>>;
132 fn user_type(&self) -> TypeId;
134}
135
136impl<V: EmailVerifier> DynEmailVerifier for V {
137 fn is_verified(&self, user: &dyn PanelUser) -> bool {
138 let user: &dyn Any = user;
139 user.downcast_ref::<V::User>()
140 .is_some_and(|user| EmailVerifier::is_verified(self, user))
141 }
142
143 fn store_verified_email<'a>(
144 &'a self,
145 cx: &'a Cx,
146 user_id: &'a str,
147 email: &'a str,
148 ex: &'a mut dyn toasty::Executor,
149 ) -> BoxFuture<'a, topcoat::Result<()>> {
150 Box::pin(EmailVerifier::store_verified_email(
151 self, cx, user_id, email, ex,
152 ))
153 }
154
155 fn user_type(&self) -> TypeId {
156 TypeId::of::<V::User>()
157 }
158}
159
160pub struct EmailVerification<U> {
185 verifier: Box<dyn DynEmailVerifier>,
186 throttle: LoginThrottle,
187 user: PhantomData<fn() -> U>,
188}
189
190impl<U: PanelUser> EmailVerification<U> {
191 #[must_use]
193 pub fn new(verifier: impl EmailVerifier<User = U>) -> Self {
194 Self {
195 verifier: Box::new(verifier),
196 throttle: LoginThrottle::new(3, Duration::from_mins(15)),
197 user: PhantomData,
198 }
199 }
200
201 #[must_use]
204 pub fn throttle(mut self, throttle: LoginThrottle) -> Self {
205 self.throttle = throttle;
206 self
207 }
208
209 pub(crate) fn erase(self) -> EmailVerification<()> {
211 EmailVerification {
212 verifier: self.verifier,
213 throttle: self.throttle,
214 user: PhantomData,
215 }
216 }
217}
218
219impl EmailVerification<()> {
220 pub(crate) fn verifier(&self) -> &dyn DynEmailVerifier {
221 &*self.verifier
222 }
223}
224
225impl<U> std::fmt::Debug for EmailVerification<U> {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 f.debug_struct("EmailVerification")
228 .field("throttle", &self.throttle)
229 .finish_non_exhaustive()
230 }
231}
232
233pub(crate) fn notice_url(cx: &Cx) -> String {
235 format!("{}/verify-email", panel_prefix(cx))
236}
237
238pub(crate) fn confirm_url(cx: &Cx) -> String {
240 format!("{}/verify-email/confirm", panel_prefix(cx))
241}
242
243pub(crate) fn pending(panel: &Realm, user: &dyn PanelUser) -> bool {
246 user.email().is_some()
247 && panel
248 .auth
249 .verification()
250 .is_some_and(|verification| !verification.verifier().is_verified(user))
251}
252
253pub(super) fn mail_after_sign_up(cx: &Cx, user: &dyn PanelUser) {
256 let Some(panel) = current_realm(cx) else {
257 return;
258 };
259 let Some(email) = user.email().filter(|_| pending(panel, user)) else {
260 return;
261 };
262 let (email, user_id) = (email.to_string(), user.user_id());
263 let locale = user.locale();
264 after_response(cx, "verification link", move |cx, panel| async move {
265 let cx = crate::i18n::addressed_to(&cx, locale.as_ref());
266 mail_link(&cx, &panel, &user_id, &email).await
267 });
268}
269
270async fn mail_link(cx: &Cx, panel: &Realm, user_id: &str, email: &str) -> topcoat::Result<()> {
273 let mail = panel
274 .auth
275 .auth_mail()
276 .ok_or_else(topcoat::router::error::not_found)?;
277 let token = token::issue_alone(
278 cx,
279 TokenPurpose::EmailVerification,
280 user_id,
281 Some(email.to_string()),
282 VERIFICATION_LIFETIME,
283 )
284 .await?;
285 let link = mail.link(&confirm_url(cx), &token);
286 let brand = hosted(cx)?.brand_name(cx);
287 let message = mail.compose(
288 email,
289 crate::t!(cx, "tablo-verify-mail-subject", brand = brand.clone()),
290 crate::t!(
291 cx,
292 "tablo-verify-mail-body",
293 email = email,
294 brand = brand,
295 link = link
296 ),
297 )?;
298 topcoat::mail::send(cx, message).await?;
299 Ok(())
300}
301
302pub(super) fn notice_page(cx: &Cx, _body: Body) -> RouteFuture<'_> {
305 Box::pin(async move {
306 let signed = resolved(cx).ok_or_else(|| unauthenticated_error(cx))?;
307 if !pending(&signed.realm, signed.user.as_ref()) {
308 return see_other(panel_root(cx)).into_response(cx);
309 }
310 notice(cx, signed.user.email().unwrap_or_default()).await
311 })
312}
313
314pub(super) fn notice_post(cx: &Cx, body: Body) -> RouteFuture<'_> {
317 Box::pin(async move {
318 let signed = resolved(cx).ok_or_else(|| unauthenticated_error(cx))?;
319 let verification = signed
320 .realm
321 .auth
322 .verification()
323 .ok_or_else(topcoat::router::error::not_found)?;
324 let form = crate::auth::input::read_form(cx, body).await?;
325 crate::csrf::verify(cx, form.last(crate::csrf::FIELD_NAME))?;
326 if !pending(&signed.realm, signed.user.as_ref()) {
327 return see_other(panel_root(cx)).into_response(cx);
328 }
329 let user_id = signed.user.user_id();
330 if !verification.throttle.attempt(&user_id, None) {
331 tracing::warn!(panel = %signed.realm.prefix, "verification link request throttled");
332 return Err(too_many_requests(verification.throttle.window_secs()).into());
333 }
334 if let Some(email) = signed.user.email() {
335 let email = email.to_string();
336 let locale = signed.user.locale();
337 after_response(cx, "verification link", move |cx, panel| async move {
338 let cx = crate::i18n::addressed_to(&cx, locale.as_ref());
339 mail_link(&cx, &panel, &user_id, &email).await
340 });
341 }
342 set_notification(
343 cx,
344 Notification::success(crate::t!(cx, "tablo-reset-sent"))
345 .description(crate::t!(cx, "tablo-verify-sent-description")),
346 );
347 see_other(notice_url(cx)).into_response(cx)
348 })
349}
350
351pub(super) fn confirm_page(cx: &Cx, _body: Body) -> RouteFuture<'_> {
357 Box::pin(async move {
358 let panel = current_realm(cx).ok_or_else(topcoat::router::error::not_found)?;
359 let verification = panel
360 .auth
361 .verification()
362 .ok_or_else(topcoat::router::error::not_found)?;
363 let encoded = token_from_query(cx);
364 let mut db = crate::db::db(cx)?;
365 let Some(row) = token::live(cx, &mut db, TokenPurpose::EmailVerification, &encoded).await?
366 else {
367 return invalid_link(cx).await;
368 };
369 if !stores(cx, panel, &row).await? {
372 return invalid_link(cx).await;
373 }
374 let email = row.email.clone().unwrap_or_default();
375 let verified = run_serializable!(cx, |tx| async {
376 if token::live(cx, tx, TokenPurpose::EmailVerification, &encoded)
378 .await?
379 .is_none()
380 {
381 return Ok(false);
382 }
383 verification
384 .verifier()
385 .store_verified_email(cx, &row.user_id, &email, tx)
386 .await
387 .map_err(infrastructure_failure)?;
388 token::spend(tx, &row).await?;
389 Ok(true)
390 })?;
391 if !verified {
392 return invalid_link(cx).await;
393 }
394 set_notification(
395 cx,
396 Notification::success(crate::t!(cx, "tablo-verify-done")),
397 );
398 let signed_in_here = match panel.auth.authenticator() {
400 Some(authenticator) => resolve(cx, panel, authenticator)
401 .await?
402 .is_some_and(|signed| signed.user.user_id() == row.user_id),
403 None => false,
404 };
405 let target = if signed_in_here {
406 panel_root(cx)
407 } else {
408 login_url(cx)
409 };
410 private(see_other(target).into_response(cx)?)
411 })
412}
413
414async fn stores(cx: &Cx, panel: &Realm, row: &token::AuthToken) -> topcoat::Result<bool> {
417 let Some(authenticator) = panel.auth.authenticator() else {
418 return Ok(false);
419 };
420 let user = authenticator
421 .find_by_id(cx, &row.user_id)
422 .await
423 .map_err(infrastructure_failure)?;
424 Ok(user.is_some_and(|user| {
425 user.can_access_panel() && user.email().is_some() && user.email() == row.email.as_deref()
426 }))
427}
428
429pub(super) fn declaration_errors(auth: &super::AuthConfig) -> Vec<crate::DeclarationErrorKind> {
432 use crate::{DeclarationErrorKind, declaration::VerificationFault};
433
434 if auth.verification.is_some() && auth.authenticator().is_none() {
435 return vec![DeclarationErrorKind::EmailVerification(
436 VerificationFault::AuthDisabled,
437 )];
438 }
439 Vec::new()
440}