tuwunel_api/client/account/3pid/
email_validate.rs1use std::net::IpAddr;
2
3use axum::{
4 extract::{Form, Request, State},
5 response::{Html, IntoResponse, Response},
6};
7use const_str::format as const_format;
8use http::{
9 StatusCode,
10 header::{CACHE_CONTROL, REFERRER_POLICY},
11};
12use serde::Deserialize;
13use tuwunel_core::utils::html::escape as html_escape;
14
15use crate::ClientIp;
16
17static VALIDATE_HEAD: &str = r#"
18 <meta charset="UTF-8">
19 <link rel="stylesheet" href="/_tuwunel/oidc/account.css">
20"#;
21
22static GENERIC_FAILURE: &str =
23 "This verification link is invalid or has expired. Request a new one from your client.";
24
25#[derive(Debug, Default, Deserialize)]
26pub(crate) struct ValidateParams {
27 sid: Option<String>,
28 client_secret: Option<String>,
29 token: Option<String>,
30}
31
32pub(crate) async fn get_email_validate_route(
38 State(services): State<crate::State>,
39 ClientIp(client): ClientIp,
40 request: Request,
41) -> Response {
42 if let Some(limited) = rate_limited(services, client) {
43 return limited;
44 }
45
46 let params: ValidateParams =
47 serde_html_form::from_str(request.uri().query().unwrap_or_default()).unwrap_or_default();
48
49 validate_html(StatusCode::OK, confirm_html(¶ms))
50}
51
52pub(crate) async fn post_email_validate_route(
58 State(services): State<crate::State>,
59 ClientIp(client): ClientIp,
60 Form(params): Form<ValidateParams>,
61) -> Response {
62 if let Some(limited) = rate_limited(services, client) {
63 return limited;
64 }
65
66 let (Some(sid), Some(client_secret), Some(token)) =
67 (¶ms.sid, ¶ms.client_secret, ¶ms.token)
68 else {
69 return validate_html(StatusCode::OK, error_html(GENERIC_FAILURE));
70 };
71
72 match services
73 .threepid
74 .validate_pending_token(sid, client_secret, token)
75 .await
76 {
77 | Ok(()) => validate_html(
78 StatusCode::OK,
79 result_html(
80 "Email verified",
81 "Your email address has been verified. Return to your client to continue.",
82 ),
83 ),
84 | Err(_) => validate_html(StatusCode::OK, error_html(GENERIC_FAILURE)),
85 }
86}
87
88fn rate_limited(services: crate::State, client: IpAddr) -> Option<Response> {
89 services
90 .threepid
91 .check_ip_rate_limit(client)
92 .is_err()
93 .then(|| {
94 validate_html(
95 StatusCode::TOO_MANY_REQUESTS,
96 error_html("Too many requests. Please wait and try again."),
97 )
98 })
99}
100
101fn confirm_html(params: &ValidateParams) -> String {
102 let escape = |value: &Option<String>| html_escape(value.as_deref().unwrap_or_default());
103
104 CONFIRM_HTML
106 .replace("{token}", &escape(¶ms.token))
107 .replace("{client_secret}", &escape(¶ms.client_secret))
108 .replace("{sid}", &escape(¶ms.sid))
109}
110
111static CONFIRM_HTML: &str = const_format!(
112 r#"
113<!DOCTYPE html>
114<html lang="en">
115 <head>
116 {VALIDATE_HEAD}
117 <title>Verify your email address</title>
118 </head>
119 <body>
120 <h1>Verify your email address</h1>
121 <p>Confirm that you want to verify this email address.</p>
122 <form method="POST" action="/_tuwunel/3pid/email/validate">
123 <input type="hidden" name="sid" value="{{sid}}">
124 <input type="hidden" name="client_secret" value="{{client_secret}}">
125 <input type="hidden" name="token" value="{{token}}">
126 <button type="submit" class="primary">Verify</button>
127 </form>
128 </body>
129</html>"#
130);
131
132fn result_html(title: &str, message: &str) -> String {
133 RESULT_HTML
134 .replace("{title}", &html_escape(title))
135 .replace("{message}", &html_escape(message))
136}
137
138static RESULT_HTML: &str = const_format!(
139 r#"
140<!DOCTYPE html>
141<html lang="en">
142 <head>
143 {VALIDATE_HEAD}
144 <title>{{title}}</title>
145 </head>
146 <body>
147 <h1>{{title}}</h1>
148 <p>{{message}}</p>
149 </body>
150</html>"#
151);
152
153fn error_html(message: &str) -> String { ERROR_HTML.replace("{msg}", &html_escape(message)) }
154
155static ERROR_HTML: &str = const_format!(
156 r#"
157<!DOCTYPE html>
158<html lang="en">
159 <head>
160 {VALIDATE_HEAD}
161 <title>Verification failed</title>
162 </head>
163 <body>
164 <h1 class="err">Verification failed</h1>
165 <p>{{msg}}</p>
166 </body>
167</html>"#
168);
169
170fn validate_html(status: StatusCode, html: String) -> Response {
171 let headers = [(CACHE_CONTROL, "no-store"), (REFERRER_POLICY, "no-referrer")];
172
173 (status, headers, Html(html)).into_response()
174}