mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
feat: Improve account panel UI for locked and suspended accounts
This commit is contained in:
@@ -92,10 +92,6 @@ pub async fn handle_login(
|
|||||||
return Err!(Request(InvalidParam("User ID does not belong to this homeserver")));
|
return Err!(Request(InvalidParam("User ID does not belong to this homeserver")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if services.users.is_locked(&user_id).await? {
|
|
||||||
return Err!(Request(UserLocked("This account has been locked.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
if services.users.is_login_disabled(&user_id).await {
|
if services.users.is_login_disabled(&user_id).await {
|
||||||
warn!(%user_id, "user attempted to log in with a login-disabled account");
|
warn!(%user_id, "user attempted to log in with a login-disabled account");
|
||||||
return Err!(Request(Forbidden("This account is not permitted to log in.")));
|
return Err!(Request(Forbidden("This account is not permitted to log in.")));
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ async fn route_login(
|
|||||||
Extension(context): Extension<TemplateContext>,
|
Extension(context): Extension<TemplateContext>,
|
||||||
Expect(Query(LoginQuery { next, reauthenticate })): Expect<Query<LoginQuery>>,
|
Expect(Query(LoginQuery { next, reauthenticate })): Expect<Query<LoginQuery>>,
|
||||||
session_store: Session,
|
session_store: Session,
|
||||||
user: User,
|
user: User<true>,
|
||||||
PostForm(form): PostForm<LoginForm>,
|
PostForm(form): PostForm<LoginForm>,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
let user_id = user.into_session().map(|session| session.user_id);
|
let user_id = user.into_session().map(|session| session.user_id);
|
||||||
|
|||||||
@@ -58,19 +58,34 @@ struct ThreepidQuery {
|
|||||||
template! {
|
template! {
|
||||||
struct Account use "account.html.j2" {
|
struct Account use "account.html.j2" {
|
||||||
user_card: UserCard,
|
user_card: UserCard,
|
||||||
|
body: AccountBody
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum AccountBody {
|
||||||
|
Unlocked {
|
||||||
|
suspended: bool,
|
||||||
email_requirement: EmailRequirement,
|
email_requirement: EmailRequirement,
|
||||||
email: Option<String>,
|
email: Option<String>,
|
||||||
devices: Vec<DeviceCard>
|
devices: Vec<DeviceCard>,
|
||||||
}
|
},
|
||||||
|
Locked,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_account(
|
async fn get_account(
|
||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
Extension(context): Extension<TemplateContext>,
|
Extension(context): Extension<TemplateContext>,
|
||||||
user: User,
|
user: User<true>,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
let user_id = user.expect(LoginTarget::Account)?;
|
let user_id = user.expect(LoginTarget::Account)?;
|
||||||
|
|
||||||
|
let user_card = UserCard::for_local_user(&services, user_id.clone()).await;
|
||||||
|
|
||||||
|
if services.users.is_locked(&user_id).await.unwrap() {
|
||||||
|
return response!(Account::new(context, user_card, AccountBody::Locked));
|
||||||
|
}
|
||||||
|
|
||||||
let email_requirement = services.threepid.email_requirement();
|
let email_requirement = services.threepid.email_requirement();
|
||||||
let email = services
|
let email = services
|
||||||
.threepid
|
.threepid
|
||||||
@@ -78,8 +93,6 @@ async fn get_account(
|
|||||||
.await
|
.await
|
||||||
.map(|address| address.to_string());
|
.map(|address| address.to_string());
|
||||||
|
|
||||||
let user_card = UserCard::for_local_user(&services, user_id.clone()).await;
|
|
||||||
|
|
||||||
let dehydrated_device_id = services.users.get_dehydrated_device_id(&user_id).await.ok();
|
let dehydrated_device_id = services.users.get_dehydrated_device_id(&user_id).await.ok();
|
||||||
|
|
||||||
let mut devices: Vec<_> = services
|
let mut devices: Vec<_> = services
|
||||||
@@ -111,7 +124,14 @@ async fn get_account(
|
|||||||
.collect()
|
.collect()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
response!(Account::new(context, user_card, email_requirement, email, device_cards))
|
let suspended = services.users.is_suspended(&user_id).await.unwrap();
|
||||||
|
|
||||||
|
response!(Account::new(context, user_card, AccountBody::Unlocked {
|
||||||
|
suspended,
|
||||||
|
email_requirement,
|
||||||
|
email,
|
||||||
|
devices: device_cards
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|||||||
@@ -197,7 +197,9 @@ async fn route_reset_password_validate(
|
|||||||
UserId::parse(format!("@{localpart}:{}", services.globals.server_name()))
|
UserId::parse(format!("@{localpart}:{}", services.globals.server_name()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
require_active(&services, &user_id).await?;
|
if let Err(response) = require_active(&services, &user_id, true).await {
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
let user_card = UserCard::for_local_user(&services, user_id.clone()).await;
|
let user_card = UserCard::for_local_user(&services, user_id.clone()).await;
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ template! {
|
|||||||
async fn route_authorization_code(
|
async fn route_authorization_code(
|
||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
Extension(context): Extension<TemplateContext>,
|
Extension(context): Extension<TemplateContext>,
|
||||||
user: User,
|
user: User<true>,
|
||||||
Expect(Query(query)): Expect<Query<AuthorizationCodeQuery>>,
|
Expect(Query(query)): Expect<Query<AuthorizationCodeQuery>>,
|
||||||
PostForm(form): PostForm<()>,
|
PostForm(form): PostForm<()>,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
|
|||||||
@@ -47,6 +47,12 @@
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
display: block;
|
||||||
|
background-color: oklch(from red 0.2 c h);
|
||||||
|
border: 1px dashed red;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-list {
|
.card-list {
|
||||||
|
|||||||
@@ -27,7 +27,8 @@
|
|||||||
{%~ else ~%}
|
{%~ else ~%}
|
||||||
({{ version_info }})
|
({{ version_info }})
|
||||||
{%~ endif ~%}
|
{%~ endif ~%}
|
||||||
{%~ endif ~%}</p>
|
{%~ endif ~%}
|
||||||
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
{%~ endblock ~%}
|
{%~ endblock ~%}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -8,46 +8,67 @@ Your account
|
|||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h1>Manage your account</h1>
|
<h1>Manage your account</h1>
|
||||||
{{ user_card }}
|
{{ user_card }}
|
||||||
<section>
|
{% match body %}
|
||||||
{% if email_requirement.may_change() %}
|
{% when AccountBody::Unlocked { suspended, email_requirement, email, devices } %}
|
||||||
<p>
|
{% if suspended %}
|
||||||
{% if let Some(email) = email %}
|
<p class="card danger">
|
||||||
Your account's associated email address is <code>{{ email }}</code>.
|
⚠️ Your account has been suspended by your homeserver's administrator.
|
||||||
{% else %}
|
Some functionality may be restricted.
|
||||||
Your account has no associated email address.
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<section>
|
||||||
|
{% if email_requirement.may_change() %}
|
||||||
|
<p>
|
||||||
|
{% if let Some(email) = email %}
|
||||||
|
Your account's associated email address is <code>{{ email }}</code>.
|
||||||
|
{% else %}
|
||||||
|
Your account has no associated email address.
|
||||||
|
{% endif %}
|
||||||
|
<a href="email/change/">Change your email</a>
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="email/change/">Change your email</a>
|
<p>
|
||||||
|
<a href="password/change">Change your password</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<a class="button fullwidth" href="logout">Log out</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<details>
|
||||||
|
<summary>Your devices ({{ devices.len() }})</summary>
|
||||||
|
<div class="card-list" id="devices">
|
||||||
|
{% for device in devices %}
|
||||||
|
{{ device }}
|
||||||
|
{% else %}
|
||||||
|
<span>
|
||||||
|
Your account has no devices. <a href="https://matrix.org/ecosystem/clients">Choose a client</a>
|
||||||
|
and sign in to start chatting on Matrix.
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<details>
|
||||||
|
<summary>Danger zone</summary>
|
||||||
|
<p>
|
||||||
|
Settings here <em class="negative">may affect the integrity of your account</em>.
|
||||||
|
</p>
|
||||||
|
<a href="cross_signing_reset">Reset your digital identity</a> •
|
||||||
|
<a href="deactivate">Deactivate your account</a>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
{% when AccountBody::Locked %}
|
||||||
|
<p class="card danger">
|
||||||
|
⚠️ Your account has been locked by your homeserver's administrator.
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
<section>
|
||||||
<p>
|
<a class="button fullwidth" href="logout">Log out</a>
|
||||||
<a href="password/change">Change your password</a>
|
</section>
|
||||||
</p>
|
{% endmatch %}
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<a class="button fullwidth" href="logout">Log out</a>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<details>
|
|
||||||
<summary>Your devices ({{ devices.len() }})</summary>
|
|
||||||
<div class="card-list" id="devices"car>
|
|
||||||
{% for device in devices %}
|
|
||||||
{{ device }}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<details>
|
|
||||||
<summary>Danger zone</summary>
|
|
||||||
<p>
|
|
||||||
Settings here <em class="negative">may affect the integrity of your account</em>.
|
|
||||||
</p>
|
|
||||||
<a href="cross_signing_reset">Reset your digital identity</a> •
|
|
||||||
<a href="deactivate">Deactivate your account</a>
|
|
||||||
</details>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
{%- endblock -%}
|
{%- endblock -%}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Device information
|
|||||||
{% else %}
|
{% else %}
|
||||||
This device can access and control all features of your Matrix account.
|
This device can access and control all features of your Matrix account.
|
||||||
<br>
|
<br>
|
||||||
<small>❖ <i>This is a legacy device. Legacy devices always have full access to your account.</i></small>
|
<small>❖ This is a legacy device. Legacy devices always have full access to your account.</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+24
-14
@@ -4,7 +4,11 @@ use std::{
|
|||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
use axum::{extract::FromRequestParts, http::request::Parts};
|
use axum::{
|
||||||
|
extract::FromRequestParts,
|
||||||
|
http::request::Parts,
|
||||||
|
response::{IntoResponse, Redirect, Response},
|
||||||
|
};
|
||||||
use conduwuit_service::oauth::grant::AuthorizationCodeQuery;
|
use conduwuit_service::oauth::grant::AuthorizationCodeQuery;
|
||||||
use ruma::{OwnedUserId, UserId};
|
use ruma::{OwnedUserId, UserId};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -62,7 +66,7 @@ impl LoginTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// An extractor that fetches the authenticated user.
|
/// An extractor that fetches the authenticated user.
|
||||||
pub(crate) struct User(Option<UserSession>);
|
pub(crate) struct User<const ALLOW_LOCKED: bool = false>(Option<UserSession>);
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub(crate) struct UserSession {
|
pub(crate) struct UserSession {
|
||||||
@@ -89,7 +93,9 @@ impl UserSession {
|
|||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
pub(crate) const KEY: &str = "session";
|
pub(crate) const KEY: &str = "session";
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const ALLOW_LOCKED: bool> User<ALLOW_LOCKED> {
|
||||||
/// Consume this extractor and return the user's session information.
|
/// Consume this extractor and return the user's session information.
|
||||||
pub(crate) fn into_session(self) -> Option<UserSession> { self.0 }
|
pub(crate) fn into_session(self) -> Option<UserSession> { self.0 }
|
||||||
|
|
||||||
@@ -127,8 +133,8 @@ impl User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromRequestParts<crate::State> for User {
|
impl<const ALLOW_LOCKED: bool> FromRequestParts<crate::State> for User<ALLOW_LOCKED> {
|
||||||
type Rejection = WebError;
|
type Rejection = Response;
|
||||||
|
|
||||||
async fn from_request_parts(
|
async fn from_request_parts(
|
||||||
parts: &mut Parts,
|
parts: &mut Parts,
|
||||||
@@ -139,12 +145,12 @@ impl FromRequestParts<crate::State> for User {
|
|||||||
.expect("should be able to extract session");
|
.expect("should be able to extract session");
|
||||||
|
|
||||||
let session = session_store
|
let session = session_store
|
||||||
.get::<UserSession>(Self::KEY)
|
.get::<UserSession>(User::KEY)
|
||||||
.await
|
.await
|
||||||
.expect("should be able to deserialize session");
|
.expect("should be able to deserialize session");
|
||||||
|
|
||||||
if let Some(session) = &session {
|
if let Some(session) = &session {
|
||||||
require_active(services, &session.user_id).await?;
|
require_active(services, &session.user_id, ALLOW_LOCKED).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self(session))
|
Ok(Self(session))
|
||||||
@@ -154,18 +160,22 @@ impl FromRequestParts<crate::State> for User {
|
|||||||
pub(crate) async fn require_active(
|
pub(crate) async fn require_active(
|
||||||
services: &crate::State,
|
services: &crate::State,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
) -> Result<(), WebError> {
|
allow_locked: bool,
|
||||||
|
) -> Result<(), Response> {
|
||||||
if !services.users.is_active(user_id).await {
|
if !services.users.is_active(user_id).await {
|
||||||
return Err(WebError::Forbidden("Your account is deactivated.".to_owned()));
|
return Err(
|
||||||
|
WebError::Forbidden("Your account is deactivated.".to_owned()).into_response()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if services
|
if !allow_locked
|
||||||
.users
|
&& services
|
||||||
.is_locked(user_id)
|
.users
|
||||||
.await
|
.is_locked(user_id)
|
||||||
.expect("should be able to check lock state")
|
.await
|
||||||
|
.expect("should be able to check lock state")
|
||||||
{
|
{
|
||||||
return Err(WebError::Forbidden("Your account is locked.".to_owned()));
|
return Err(Redirect::to(&format!("{ROUTE_PREFIX}/account/")).into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Reference in New Issue
Block a user