| 16 |
16 |
|
db,
|
| 17 |
17 |
|
error::{AppError, Result, ResultExt},
|
| 18 |
18 |
|
helpers::hx_toast,
|
| 19 |
|
- |
templates::{TotpSetupTemplate, TotpStatusTemplate},
|
|
19 |
+ |
templates::{SaveStatusTemplate, TotpSetupTemplate, TotpStatusTemplate},
|
| 20 |
20 |
|
};
|
| 21 |
21 |
|
|
| 22 |
22 |
|
/// Generate a TOTP secret, QR code, and backup codes (does not enable 2FA yet).
|
| 103 |
103 |
|
// accepted step, and that step is then recorded. `setup` cleared the step to
|
| 104 |
104 |
|
// NULL (defaults to 0), so a genuine first code (step ~= now/30) always wins.
|
| 105 |
105 |
|
let now = chrono::Utc::now().timestamp() as u64;
|
| 106 |
|
- |
let invalid = || {
|
| 107 |
|
- |
(
|
|
106 |
+ |
let invalid = || -> Result<Response> {
|
|
107 |
+ |
Ok((
|
| 108 |
108 |
|
[
|
| 109 |
109 |
|
("HX-Retarget", "#totp-confirm-status"),
|
| 110 |
110 |
|
("HX-Reswap", "innerHTML"),
|
| 111 |
111 |
|
],
|
| 112 |
|
- |
Html("<span class=\"save-error\">Invalid code. Please try again.</span>"),
|
|
112 |
+ |
Html(
|
|
113 |
+ |
SaveStatusTemplate {
|
|
114 |
+ |
success: false,
|
|
115 |
+ |
message: "Invalid code. Please try again.".to_string(),
|
|
116 |
+ |
}
|
|
117 |
+ |
.render_string()?,
|
|
118 |
+ |
),
|
| 113 |
119 |
|
)
|
| 114 |
|
- |
.into_response()
|
|
120 |
+ |
.into_response())
|
| 115 |
121 |
|
};
|
| 116 |
122 |
|
|
| 117 |
123 |
|
let Some(step) = find_matching_step(&totp, &form.code, now) else {
|
| 118 |
|
- |
return Ok(invalid());
|
|
124 |
+ |
return invalid();
|
| 119 |
125 |
|
};
|
| 120 |
126 |
|
|
| 121 |
127 |
|
// Record the matched step atomically; the guarded write is the authoritative
|
| 123 |
129 |
|
// the step loses here and is rejected, the step monotonicity is enforced by
|
| 124 |
130 |
|
// the DB, not by a separate read-then-write.
|
| 125 |
131 |
|
if !db::totp::set_totp_last_used_step(&db, user.id, step).await? {
|
| 126 |
|
- |
return Ok(invalid());
|
|
132 |
+ |
return invalid();
|
| 127 |
133 |
|
}
|
| 128 |
134 |
|
|
| 129 |
135 |
|
db::totp::enable_totp(&db, user.id).await?;
|
| 161 |
167 |
|
("HX-Retarget", "#totp-disable-status"),
|
| 162 |
168 |
|
("HX-Reswap", "innerHTML"),
|
| 163 |
169 |
|
],
|
| 164 |
|
- |
Html("<span class=\"save-error\">Incorrect password.</span>"),
|
|
170 |
+ |
Html(
|
|
171 |
+ |
SaveStatusTemplate {
|
|
172 |
+ |
success: false,
|
|
173 |
+ |
message: "Incorrect password.".to_string(),
|
|
174 |
+ |
}
|
|
175 |
+ |
.render_string()?,
|
|
176 |
+ |
),
|
| 165 |
177 |
|
)
|
| 166 |
178 |
|
.into_response());
|
| 167 |
179 |
|
}
|
| 202 |
214 |
|
("HX-Retarget", "#backup-regen-status"),
|
| 203 |
215 |
|
("HX-Reswap", "innerHTML"),
|
| 204 |
216 |
|
],
|
| 205 |
|
- |
Html("<span class=\"save-error\">Incorrect password.</span>"),
|
|
217 |
+ |
Html(
|
|
218 |
+ |
SaveStatusTemplate {
|
|
219 |
+ |
success: false,
|
|
220 |
+ |
message: "Incorrect password.".to_string(),
|
|
221 |
+ |
}
|
|
222 |
+ |
.render_string()?,
|
|
223 |
+ |
),
|
| 206 |
224 |
|
)
|
| 207 |
225 |
|
.into_response());
|
| 208 |
226 |
|
}
|