Compare commits

..

3 Commits

Author SHA1 Message Date
cc4a8a962b User can delete his own 2FA login history
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-11-12 11:51:24 +01:00
7887ccaa41 Show 2FA successful login on 2FA user page 2022-11-12 11:37:15 +01:00
1fa36c0aff Automatically remove outdated 2FA successful entries 2022-11-12 11:27:19 +01:00
5 changed files with 91 additions and 0 deletions

View File

@@ -55,6 +55,10 @@ pub struct ChangePasswordResult(pub bool);
#[rtype(result = "bool")]
pub struct AddSuccessful2FALogin(pub UserID, pub IpAddr);
#[derive(Message)]
#[rtype(result = "bool")]
pub struct Clear2FALoginHistory(pub UserID);
#[derive(Debug)]
pub struct UpdateUserResult(pub bool);
@@ -125,6 +129,13 @@ impl Handler<AddSuccessful2FALogin> for UsersActor {
}
}
impl Handler<Clear2FALoginHistory> for UsersActor {
type Result = <Clear2FALoginHistory as actix::Message>::Result;
fn handle(&mut self, msg: Clear2FALoginHistory, _ctx: &mut Self::Context) -> Self::Result {
self.manager.clear_2fa_login_history(&msg.0)
}
}
impl Handler<GetUserRequest> for UsersActor {
type Result = MessageResult<GetUserRequest>;

View File

@@ -120,3 +120,15 @@ pub async fn delete_factor(
HttpResponse::Ok().body("Removed factor!")
}
}
pub async fn clear_login_history(
user: CurrentUser,
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
users
.send(users_actor::Clear2FALoginHistory(user.uid.clone()))
.await
.unwrap();
HttpResponse::Ok().body("History successfully cleared")
}

View File

@@ -190,6 +190,11 @@ impl User {
.collect::<Vec<_>>()
}
pub fn remove_outdated_successful_2fa_attempts(&mut self) {
self.last_successful_2fa
.retain(|_, t| *t + SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN > time());
}
pub fn get_formatted_2fa_successful_logins(&self) -> Vec<Successful2FALogin> {
self.last_successful_2fa
.iter()
@@ -301,6 +306,17 @@ impl EntityManager<User> {
pub fn save_new_successful_2fa_authentication(&mut self, id: &UserID, ip: IpAddr) -> bool {
self.update_user(id, |mut user| {
user.last_successful_2fa.insert(ip, time());
// Remove outdated successful attempts
user.remove_outdated_successful_2fa_attempts();
user
})
}
pub fn clear_2fa_login_history(&mut self, id: &UserID) -> bool {
self.update_user(id, |mut user| {
user.last_successful_2fa = Default::default();
user
})
}

View File

@@ -192,6 +192,11 @@ async fn main() -> std::io::Result<()> {
"/settings/api/two_factor/delete_factor",
web::post().to(two_factor_api::delete_factor),
)
.route(
"/settings/api/two_factor/clear_login_history",
// Use POST to prevent CSRF
web::post().to(two_factor_api::clear_login_history),
)
// Admin routes
.route(
"/admin",

View File

@@ -34,6 +34,33 @@
</tbody>
</table>
{% if !user.last_successful_2fa.is_empty() %}
<div id="2fa_history_container">
<h5 style="margin-top: 50px">Successful 2FA login history</h5>
<p>
<a type="button" class="btn btn-danger btn-sm" onclick="clear_login_history()">Clear history</a>
</p>
<table class="table table-hover" style="max-width: 800px;" aria-describedby="Factors list">
<thead>
<tr>
<th scope="col">IP address</th>
<th scope="col">Date</th>
<th scope="col">Bypass 2FA</th>
</tr>
</thead>
<tbody>
{% for e in user.get_formatted_2fa_successful_logins() %}
<tr>
<td>{{ e.ip }}</td>
<td>{{ e.fmt_time() }}</td>
<td>{% if e.can_bypass_2fa %}YES{% else %}NO{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
<script>
async function delete_factor(id) {
if (!confirm("Do you really want to remove this factor?"))
@@ -61,5 +88,25 @@
}
}
async function clear_login_history() {
if (!confirm("Do you really want to clear your 2FA login history?"))
return;
try {
const res = await fetch("/settings/api/two_factor/clear_login_history", {
method: "post"
});
let text = await res.text();
alert(text);
if (res.status == 200)
document.getElementById("2fa_history_container").remove();
} catch(e) {
console.error(e);
alert("Failed to clear 2FA history!");
}
}
</script>
{% endblock content %}