mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5b11af3e8 | |||
| 71a26e433f | |||
| d353446488 | |||
| 77e8fd1744 | |||
| 7fa7b129c0 | |||
| 247bc15659 | |||
| 88a35e139d | |||
| 37574ef5cc | |||
| 1c816850ed | |||
| 3483059e1c | |||
| d865dd4454 | |||
| adc7c5ac49 | |||
| 112403e470 | |||
| ea0a124981 | |||
| bf205fb13c | |||
| 9a6408f98f | |||
| ca77970ff3 | |||
| 42f4ec34cd | |||
| ecf74bb31f | |||
| 8c716befdc | |||
| a8209d1dd9 | |||
| 9552dd7485 | |||
| 88c84f221f | |||
| a10bd71945 |
+4
-11
@@ -1,26 +1,17 @@
|
|||||||
#cargo-features = ["profile-rustflags"]
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["src/*", "xtask/*"]
|
members = ["src/*", "xtask/*"]
|
||||||
default-members = ["src/*"]
|
default-members = ["src/*"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
authors = [
|
authors = ["Continuwuity Team and contributors <team@continuwuity.org>"]
|
||||||
"June Clementine Strawberry <june@girlboss.ceo>",
|
description = "A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver."
|
||||||
"strawberry <strawberry@puppygock.gay>", # woof
|
|
||||||
"Jason Volk <jason@zemos.net>",
|
|
||||||
]
|
|
||||||
categories = ["network-programming"]
|
|
||||||
description = "a very cool Matrix chat homeserver written in Rust"
|
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
homepage = "https://continuwuity.org/"
|
homepage = "https://continuwuity.org/"
|
||||||
keywords = ["chat", "matrix", "networking", "server", "uwu"]
|
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
# See also `rust-toolchain.toml`
|
# See also `rust-toolchain.toml`
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
||||||
rust-version = "1.86.0"
|
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|
||||||
[workspace.metadata.crane]
|
[workspace.metadata.crane]
|
||||||
@@ -848,6 +839,8 @@ unknown_lints = "allow"
|
|||||||
|
|
||||||
###################
|
###################
|
||||||
cargo = { level = "warn", priority = -1 }
|
cargo = { level = "warn", priority = -1 }
|
||||||
|
# Nobody except for us should be consuming these crates, they don't need metadata
|
||||||
|
cargo_common_metadata = { level = "allow"}
|
||||||
|
|
||||||
## some sadness
|
## some sadness
|
||||||
multiple_crate_versions = { level = "allow", priority = 1 }
|
multiple_crate_versions = { level = "allow", priority = 1 }
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Added admin command to forcefully log out all of a user's existing sessions. Contributed by @nex.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Implemented toggling the ability for an account to log in without mutating any of its data. Contributed by @nex.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Added support for issuing additional registration tokens, stored in the database, which supplement the existing registration token hardcoded in the config file. These tokens may optionally expire after a certain number of uses or after a certain amount of time has passed. Additionally, the `registration_token_file` configuration option is superseded by this feature and **has been removed**.
|
||||||
+3
-12
@@ -421,7 +421,7 @@
|
|||||||
# `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
|
# `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
|
||||||
#
|
#
|
||||||
# If you would like registration only via token reg, please configure
|
# If you would like registration only via token reg, please configure
|
||||||
# `registration_token` or `registration_token_file`.
|
# `registration_token`.
|
||||||
#
|
#
|
||||||
#allow_registration = false
|
#allow_registration = false
|
||||||
|
|
||||||
@@ -452,22 +452,13 @@
|
|||||||
# `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
|
# `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
|
||||||
# to true to allow open registration without any conditions.
|
# to true to allow open registration without any conditions.
|
||||||
#
|
#
|
||||||
# YOU NEED TO EDIT THIS OR USE registration_token_file.
|
# If you do not want to set a static token, the `!admin token` commands
|
||||||
|
# may also be used to manage registration tokens.
|
||||||
#
|
#
|
||||||
# example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
|
# example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
|
||||||
#
|
#
|
||||||
#registration_token =
|
#registration_token =
|
||||||
|
|
||||||
# Path to a file on the system that gets read for additional registration
|
|
||||||
# tokens. Multiple tokens can be added if you separate them with
|
|
||||||
# whitespace
|
|
||||||
#
|
|
||||||
# continuwuity must be able to access the file, and it must not be empty
|
|
||||||
#
|
|
||||||
# example: "/etc/continuwuity/.reg_token"
|
|
||||||
#
|
|
||||||
#registration_token_file =
|
|
||||||
|
|
||||||
# The public site key for reCaptcha. If this is provided, reCaptcha
|
# The public site key for reCaptcha. If this is provided, reCaptcha
|
||||||
# becomes required during registration. If both captcha *and*
|
# becomes required during registration. If both captcha *and*
|
||||||
# registration token are enabled, both will be required during
|
# registration token are enabled, both will be required during
|
||||||
|
|||||||
+1
-1
@@ -52,7 +52,7 @@ ENV BINSTALL_VERSION=1.16.6
|
|||||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||||
ENV CARGO_SBOM_VERSION=0.9.1
|
ENV CARGO_SBOM_VERSION=0.9.1
|
||||||
# renovate: datasource=crate depName=lddtree
|
# renovate: datasource=crate depName=lddtree
|
||||||
ENV LDDTREE_VERSION=0.3.7
|
ENV LDDTREE_VERSION=0.4.0
|
||||||
# renovate: datasource=crate depName=timelord-cli
|
# renovate: datasource=crate depName=timelord-cli
|
||||||
ENV TIMELORD_VERSION=3.0.1
|
ENV TIMELORD_VERSION=3.0.1
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ ENV BINSTALL_VERSION=1.16.6
|
|||||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||||
ENV CARGO_SBOM_VERSION=0.9.1
|
ENV CARGO_SBOM_VERSION=0.9.1
|
||||||
# renovate: datasource=crate depName=lddtree
|
# renovate: datasource=crate depName=lddtree
|
||||||
ENV LDDTREE_VERSION=0.3.7
|
ENV LDDTREE_VERSION=0.4.0
|
||||||
|
|
||||||
# Install unpackaged tools
|
# Install unpackaged tools
|
||||||
RUN <<EOF
|
RUN <<EOF
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ This document contains the help content for the `continuwuity` command-line prog
|
|||||||
|
|
||||||
## `continuwuity`
|
## `continuwuity`
|
||||||
|
|
||||||
a very cool Matrix chat homeserver written in Rust
|
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver.
|
||||||
|
|
||||||
**Usage:** `continuwuity [OPTIONS]`
|
**Usage:** `continuwuity [OPTIONS]`
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
Name: continuwuity
|
Name: continuwuity
|
||||||
Version: {{{ git_repo_version }}}
|
Version: {{{ git_repo_version }}}
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: Very cool Matrix chat homeserver written in Rust
|
Summary: A Matrix homeserver written in Rust.
|
||||||
|
|
||||||
License: Apache-2.0 AND MIT
|
License: Apache-2.0 AND MIT
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ Requires: glibc
|
|||||||
Requires: libstdc++
|
Requires: libstdc++
|
||||||
|
|
||||||
%global _description %{expand:
|
%global _description %{expand:
|
||||||
A cool hard fork of Conduit, a Matrix homeserver written in Rust}
|
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver.}
|
||||||
|
|
||||||
%description %{_description}
|
%description %{_description}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "conduwuit_admin"
|
name = "conduwuit_admin"
|
||||||
categories.workspace = true
|
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
keywords.workspace = true
|
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|||||||
+20
-4
@@ -2,10 +2,17 @@ use clap::Parser;
|
|||||||
use conduwuit::Result;
|
use conduwuit::Result;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
appservice, appservice::AppserviceCommand, check, check::CheckCommand, context::Context,
|
appservice::{self, AppserviceCommand},
|
||||||
debug, debug::DebugCommand, federation, federation::FederationCommand, media,
|
check::{self, CheckCommand},
|
||||||
media::MediaCommand, query, query::QueryCommand, room, room::RoomCommand, server,
|
context::Context,
|
||||||
server::ServerCommand, user, user::UserCommand,
|
debug::{self, DebugCommand},
|
||||||
|
federation::{self, FederationCommand},
|
||||||
|
media::{self, MediaCommand},
|
||||||
|
query::{self, QueryCommand},
|
||||||
|
room::{self, RoomCommand},
|
||||||
|
server::{self, ServerCommand},
|
||||||
|
token::{self, TokenCommand},
|
||||||
|
user::{self, UserCommand},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
@@ -19,6 +26,10 @@ pub enum AdminCommand {
|
|||||||
/// - Commands for managing local users
|
/// - Commands for managing local users
|
||||||
Users(UserCommand),
|
Users(UserCommand),
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
/// - Commands for managing registration tokens
|
||||||
|
Token(TokenCommand),
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
/// - Commands for managing rooms
|
/// - Commands for managing rooms
|
||||||
Rooms(RoomCommand),
|
Rooms(RoomCommand),
|
||||||
@@ -64,6 +75,11 @@ pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Res
|
|||||||
context.bail_restricted()?;
|
context.bail_restricted()?;
|
||||||
user::process(command, context).await
|
user::process(command, context).await
|
||||||
},
|
},
|
||||||
|
| Token(command) => {
|
||||||
|
// token commands are all restricted
|
||||||
|
context.bail_restricted()?;
|
||||||
|
token::process(command, context).await
|
||||||
|
},
|
||||||
| Rooms(command) => room::process(command, context).await,
|
| Rooms(command) => room::process(command, context).await,
|
||||||
| Federation(command) => federation::process(command, context).await,
|
| Federation(command) => federation::process(command, context).await,
|
||||||
| Server(command) => server::process(command, context).await,
|
| Server(command) => server::process(command, context).await,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub(crate) mod media;
|
|||||||
pub(crate) mod query;
|
pub(crate) mod query;
|
||||||
pub(crate) mod room;
|
pub(crate) mod room;
|
||||||
pub(crate) mod server;
|
pub(crate) mod server;
|
||||||
|
pub(crate) mod token;
|
||||||
pub(crate) mod user;
|
pub(crate) mod user;
|
||||||
|
|
||||||
extern crate conduwuit_api as api;
|
extern crate conduwuit_api as api;
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
use conduwuit::{Err, Result, utils};
|
||||||
|
use conduwuit_macros::admin_command;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use service::registration_tokens::TokenExpires;
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result {
|
||||||
|
let expires = {
|
||||||
|
if expires.immortal {
|
||||||
|
None
|
||||||
|
} else if let Some(max_uses) = expires.max_uses {
|
||||||
|
Some(TokenExpires::AfterUses(max_uses))
|
||||||
|
} else if expires.once {
|
||||||
|
Some(TokenExpires::AfterUses(1))
|
||||||
|
} else if let Some(max_age) = expires
|
||||||
|
.max_age
|
||||||
|
.as_deref()
|
||||||
|
.map(|max_age| utils::time::timepoint_from_now(utils::time::parse_duration(max_age)?))
|
||||||
|
.transpose()?
|
||||||
|
{
|
||||||
|
Some(TokenExpires::AfterTime(max_age))
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (token, info) = self
|
||||||
|
.services
|
||||||
|
.registration_tokens
|
||||||
|
.issue_token(self.sender_or_service_user().into(), expires);
|
||||||
|
|
||||||
|
self.write_str(&format!(
|
||||||
|
"New registration token issued: `{token}`. {}.",
|
||||||
|
if let Some(expires) = info.expires {
|
||||||
|
format!("{expires}")
|
||||||
|
} else {
|
||||||
|
"Never expires".to_owned()
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
pub(super) async fn revoke_token(&self, token: String) -> Result {
|
||||||
|
let Some(token) = self
|
||||||
|
.services
|
||||||
|
.registration_tokens
|
||||||
|
.validate_token(token)
|
||||||
|
.await
|
||||||
|
else {
|
||||||
|
return Err!("This token does not exist or has already expired.");
|
||||||
|
};
|
||||||
|
|
||||||
|
self.services.registration_tokens.revoke_token(token)?;
|
||||||
|
|
||||||
|
self.write_str("Token revoked successfully.").await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
pub(super) async fn list_tokens(&self) -> Result {
|
||||||
|
let tokens: Vec<_> = self
|
||||||
|
.services
|
||||||
|
.registration_tokens
|
||||||
|
.iterate_tokens()
|
||||||
|
.collect()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self.write_str(&format!("Found {} registration tokens:\n", tokens.len()))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for token in tokens {
|
||||||
|
self.write_str(&format!("- {token}\n")).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
mod commands;
|
||||||
|
|
||||||
|
use clap::{Args, Subcommand};
|
||||||
|
use conduwuit::Result;
|
||||||
|
|
||||||
|
use crate::admin_command_dispatch;
|
||||||
|
|
||||||
|
#[admin_command_dispatch]
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum TokenCommand {
|
||||||
|
/// - Issue a new registration token
|
||||||
|
#[clap(name = "issue")]
|
||||||
|
IssueToken {
|
||||||
|
/// When this token will expire.
|
||||||
|
#[command(flatten)]
|
||||||
|
expires: TokenExpires,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// - Revoke a registration token
|
||||||
|
#[clap(name = "revoke")]
|
||||||
|
RevokeToken {
|
||||||
|
/// The token to revoke.
|
||||||
|
token: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// - List all registration tokens
|
||||||
|
#[clap(name = "list")]
|
||||||
|
ListTokens,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
#[group(required = true, multiple = false)]
|
||||||
|
pub struct TokenExpires {
|
||||||
|
/// The maximum number of times this token is allowed to be used before it
|
||||||
|
/// expires.
|
||||||
|
#[arg(long)]
|
||||||
|
max_uses: Option<u64>,
|
||||||
|
|
||||||
|
/// The maximum age of this token (e.g. 30s, 5m, 7d). It will expire after
|
||||||
|
/// this much time has passed.
|
||||||
|
#[arg(long)]
|
||||||
|
max_age: Option<String>,
|
||||||
|
|
||||||
|
/// This token will never expire.
|
||||||
|
#[arg(long)]
|
||||||
|
immortal: bool,
|
||||||
|
|
||||||
|
/// A shortcut for `--max-uses 1`.
|
||||||
|
#[arg(long)]
|
||||||
|
once: bool,
|
||||||
|
}
|
||||||
@@ -280,7 +280,12 @@ pub(super) async fn unsuspend(&self, user_id: String) -> Result {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn reset_password(&self, username: String, password: Option<String>) -> Result {
|
pub(super) async fn reset_password(
|
||||||
|
&self,
|
||||||
|
logout: bool,
|
||||||
|
username: String,
|
||||||
|
password: Option<String>,
|
||||||
|
) -> Result {
|
||||||
let user_id = parse_local_user_id(self.services, &username)?;
|
let user_id = parse_local_user_id(self.services, &username)?;
|
||||||
|
|
||||||
if user_id == self.services.globals.server_user {
|
if user_id == self.services.globals.server_user {
|
||||||
@@ -303,7 +308,18 @@ pub(super) async fn reset_password(&self, username: String, password: Option<Str
|
|||||||
write!(self, "Successfully reset the password for user {user_id}: `{new_password}`")
|
write!(self, "Successfully reset the password for user {user_id}: `{new_password}`")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
.await
|
.await?;
|
||||||
|
|
||||||
|
if logout {
|
||||||
|
self.services
|
||||||
|
.users
|
||||||
|
.all_device_ids(&user_id)
|
||||||
|
.for_each(|device_id| self.services.users.remove_device(&user_id, device_id))
|
||||||
|
.await;
|
||||||
|
write!(self, "\nAll existing sessions have been logged out.").await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
@@ -1017,3 +1033,72 @@ pub(super) async fn unlock(&self, user_id: String) -> Result {
|
|||||||
self.write_str(&format!("User {user_id} has been unlocked."))
|
self.write_str(&format!("User {user_id} has been unlocked."))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
pub(super) async fn logout(&self, user_id: String) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||||
|
assert!(
|
||||||
|
self.services.globals.user_is_local(&user_id),
|
||||||
|
"Parsed user_id must be a local user"
|
||||||
|
);
|
||||||
|
if user_id == self.services.globals.server_user {
|
||||||
|
return Err!("Not allowed to log out the server service account.",);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.services.users.exists(&user_id).await {
|
||||||
|
return Err!("User {user_id} does not exist.");
|
||||||
|
}
|
||||||
|
if self.services.users.is_admin(&user_id).await {
|
||||||
|
return Err!("You cannot forcefully log out admin users.");
|
||||||
|
}
|
||||||
|
self.services
|
||||||
|
.users
|
||||||
|
.all_device_ids(&user_id)
|
||||||
|
.for_each(|device_id| self.services.users.remove_device(&user_id, device_id))
|
||||||
|
.await;
|
||||||
|
self.write_str(&format!("User {user_id} has been logged out from all devices."))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
pub(super) async fn disable_login(&self, user_id: String) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||||
|
assert!(
|
||||||
|
self.services.globals.user_is_local(&user_id),
|
||||||
|
"Parsed user_id must be a local user"
|
||||||
|
);
|
||||||
|
if user_id == self.services.globals.server_user {
|
||||||
|
return Err!("Not allowed to disable login for the server service account.",);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.services.users.exists(&user_id).await {
|
||||||
|
return Err!("User {user_id} does not exist.");
|
||||||
|
}
|
||||||
|
if self.services.users.is_admin(&user_id).await {
|
||||||
|
return Err!("Admin users cannot have their login disallowed.");
|
||||||
|
}
|
||||||
|
self.services.users.disable_login(&user_id);
|
||||||
|
|
||||||
|
self.write_str(&format!(
|
||||||
|
"{user_id} can no longer log in. Their existing sessions remain unaffected."
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
pub(super) async fn enable_login(&self, user_id: String) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||||
|
assert!(
|
||||||
|
self.services.globals.user_is_local(&user_id),
|
||||||
|
"Parsed user_id must be a local user"
|
||||||
|
);
|
||||||
|
if !self.services.users.exists(&user_id).await {
|
||||||
|
return Err!("User {user_id} does not exist.");
|
||||||
|
}
|
||||||
|
self.services.users.enable_login(&user_id);
|
||||||
|
|
||||||
|
self.write_str(&format!("{user_id} can now log in.")).await
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ pub enum UserCommand {
|
|||||||
|
|
||||||
/// - Reset user password
|
/// - Reset user password
|
||||||
ResetPassword {
|
ResetPassword {
|
||||||
|
/// Log out existing sessions
|
||||||
|
#[arg(short, long)]
|
||||||
|
logout: bool,
|
||||||
/// Username of the user for whom the password should be reset
|
/// Username of the user for whom the password should be reset
|
||||||
username: String,
|
username: String,
|
||||||
/// New password for the user, if unspecified one is generated
|
/// New password for the user, if unspecified one is generated
|
||||||
@@ -59,6 +62,18 @@ pub enum UserCommand {
|
|||||||
force: bool,
|
force: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// - Forcefully log a user out of all of their devices.
|
||||||
|
///
|
||||||
|
/// This will invalidate all access tokens for the specified user,
|
||||||
|
/// effectively logging them out from all sessions.
|
||||||
|
/// Note that this is destructive and may result in data loss for the user,
|
||||||
|
/// such as encryption keys. Use with caution. Can only be used in the admin
|
||||||
|
/// room.
|
||||||
|
Logout {
|
||||||
|
/// Username of the user to log out
|
||||||
|
user_id: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// - Suspend a user
|
/// - Suspend a user
|
||||||
///
|
///
|
||||||
/// Suspended users are able to log in, sync, and read messages, but are not
|
/// Suspended users are able to log in, sync, and read messages, but are not
|
||||||
@@ -101,6 +116,22 @@ pub enum UserCommand {
|
|||||||
user_id: String,
|
user_id: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// - Enable login for a user
|
||||||
|
EnableLogin {
|
||||||
|
/// Username of the user to enable login for
|
||||||
|
user_id: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// - Disable login for a user
|
||||||
|
///
|
||||||
|
/// Disables login for the specified user without deactivating or locking
|
||||||
|
/// their account. This prevents the user from obtaining new access tokens,
|
||||||
|
/// but does not invalidate existing sessions.
|
||||||
|
DisableLogin {
|
||||||
|
/// Username of the user to disable login for
|
||||||
|
user_id: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// - List local users in the database
|
/// - List local users in the database
|
||||||
#[clap(alias = "list")]
|
#[clap(alias = "list")]
|
||||||
ListUsers,
|
ListUsers,
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "conduwuit_api"
|
name = "conduwuit_api"
|
||||||
categories.workspace = true
|
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
keywords.workspace = true
|
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|||||||
+39
-13
@@ -179,13 +179,18 @@ pub(crate) async fn register_route(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err!(Request(Forbidden("Registration has been disabled.")));
|
return Err!(Request(Forbidden(
|
||||||
|
"This server is not accepting registrations at this time."
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_guest
|
if is_guest
|
||||||
&& (!services.config.allow_guest_registration
|
&& (!services.config.allow_guest_registration
|
||||||
|| (services.config.allow_registration
|
|| (services.config.allow_registration
|
||||||
&& services.globals.registration_token.is_some()))
|
&& services
|
||||||
|
.registration_tokens
|
||||||
|
.get_config_file_token()
|
||||||
|
.is_some()))
|
||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
"Guest registration disabled / registration enabled with token configured, \
|
"Guest registration disabled / registration enabled with token configured, \
|
||||||
@@ -203,7 +208,9 @@ pub(crate) async fn register_route(
|
|||||||
rejecting registration. Guest's initial device name: \"{}\"",
|
rejecting registration. Guest's initial device name: \"{}\"",
|
||||||
body.initial_device_display_name.as_deref().unwrap_or("")
|
body.initial_device_display_name.as_deref().unwrap_or("")
|
||||||
);
|
);
|
||||||
return Err!(Request(Forbidden("Registration is temporarily disabled.")));
|
return Err!(Request(Forbidden(
|
||||||
|
"This server is not accepting registrations at this time."
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = match (body.username.as_ref(), is_guest) {
|
let user_id = match (body.username.as_ref(), is_guest) {
|
||||||
@@ -301,7 +308,13 @@ pub(crate) async fn register_route(
|
|||||||
let skip_auth = body.appservice_info.is_some() || is_guest;
|
let skip_auth = body.appservice_info.is_some() || is_guest;
|
||||||
|
|
||||||
// Populate required UIAA flows
|
// Populate required UIAA flows
|
||||||
if services.globals.registration_token.is_some() {
|
if services
|
||||||
|
.registration_tokens
|
||||||
|
.iterate_tokens()
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
// Registration token required
|
// Registration token required
|
||||||
uiaainfo.flows.push(AuthFlow {
|
uiaainfo.flows.push(AuthFlow {
|
||||||
stages: vec![AuthType::RegistrationToken],
|
stages: vec![AuthType::RegistrationToken],
|
||||||
@@ -323,7 +336,19 @@ pub(crate) async fn register_route(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if uiaainfo.flows.is_empty() && !skip_auth {
|
if uiaainfo.flows.is_empty() && !skip_auth {
|
||||||
// No registration token necessary, but clients must still go through the flow
|
// Registration isn't _disabled_, but there's no captcha configured and no
|
||||||
|
// registration tokens currently set. Bail out by default unless open
|
||||||
|
// registration was explicitly enabled.
|
||||||
|
if !services
|
||||||
|
.config
|
||||||
|
.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
|
||||||
|
{
|
||||||
|
return Err!(Request(Forbidden(
|
||||||
|
"This server is not accepting registrations at this time."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have open registration enabled (😧), provide a dummy stage
|
||||||
uiaainfo = UiaaInfo {
|
uiaainfo = UiaaInfo {
|
||||||
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
|
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
|
||||||
completed: Vec::new(),
|
completed: Vec::new(),
|
||||||
@@ -846,19 +871,20 @@ pub(crate) async fn request_3pid_management_token_via_msisdn_route(
|
|||||||
|
|
||||||
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
|
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
|
||||||
///
|
///
|
||||||
/// Checks if the provided registration token is valid at the time of checking
|
/// Checks if the provided registration token is valid at the time of checking.
|
||||||
///
|
|
||||||
/// Currently does not have any ratelimiting, and this isn't very practical as
|
|
||||||
/// there is only one registration token allowed.
|
|
||||||
pub(crate) async fn check_registration_token_validity(
|
pub(crate) async fn check_registration_token_validity(
|
||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
body: Ruma<check_registration_token_validity::v1::Request>,
|
body: Ruma<check_registration_token_validity::v1::Request>,
|
||||||
) -> Result<check_registration_token_validity::v1::Response> {
|
) -> Result<check_registration_token_validity::v1::Response> {
|
||||||
let Some(reg_token) = services.globals.registration_token.clone() else {
|
// TODO: ratelimit this pretty heavily
|
||||||
return Err!(Request(Forbidden("Server does not allow token registration")));
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(check_registration_token_validity::v1::Response { valid: reg_token == body.token })
|
let valid = services
|
||||||
|
.registration_tokens
|
||||||
|
.validate_token(body.token.clone())
|
||||||
|
.await
|
||||||
|
.is_some();
|
||||||
|
|
||||||
|
Ok(check_registration_token_validity::v1::Response { valid })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs through all the deactivation steps:
|
/// Runs through all the deactivation steps:
|
||||||
|
|||||||
@@ -178,7 +178,20 @@ pub async fn leave_room(
|
|||||||
.rooms
|
.rooms
|
||||||
.state_cache
|
.state_cache
|
||||||
.left_state(user_id, room_id)
|
.left_state(user_id, room_id)
|
||||||
.await?
|
.await
|
||||||
|
.inspect_err(|err| {
|
||||||
|
// `left_state` may return an Err if the user _is_ in the room they're
|
||||||
|
// trying to leave, but the membership cache is incorrect and
|
||||||
|
// they're cached as being joined. In this situation
|
||||||
|
// we save a `None` to the `roomuserid_leftcount` table, which generates
|
||||||
|
// and sends a dummy leave to the client.
|
||||||
|
warn!(
|
||||||
|
?err,
|
||||||
|
"Trying to leave room not cached as leave, sending dummy leave \
|
||||||
|
event to client"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use axum_client_ip::InsecureClientIp;
|
|||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Error, Result, debug, err, info,
|
Err, Error, Result, debug, err, info,
|
||||||
utils::{self, ReadyExt, hash},
|
utils::{self, ReadyExt, hash},
|
||||||
|
warn,
|
||||||
};
|
};
|
||||||
use conduwuit_core::{debug_error, debug_warn};
|
use conduwuit_core::{debug_error, debug_warn};
|
||||||
use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH};
|
use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH};
|
||||||
@@ -12,6 +13,7 @@ use futures::StreamExt;
|
|||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedUserId, UserId,
|
OwnedUserId, UserId,
|
||||||
api::client::{
|
api::client::{
|
||||||
|
error::ErrorKind,
|
||||||
session::{
|
session::{
|
||||||
get_login_token,
|
get_login_token,
|
||||||
get_login_types::{
|
get_login_types::{
|
||||||
@@ -184,6 +186,15 @@ pub(crate) async fn handle_login(
|
|||||||
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
|
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if services.users.is_locked(&user_id).await? {
|
||||||
|
return Err(Error::BadRequest(ErrorKind::UserLocked, "This account has been locked."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if services.users.is_login_disabled(&user_id).await {
|
||||||
|
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.")));
|
||||||
|
}
|
||||||
|
|
||||||
if cfg!(feature = "ldap") && services.config.ldap.enable {
|
if cfg!(feature = "ldap") && services.config.ldap.enable {
|
||||||
match Box::pin(ldap_login(services, &user_id, &lowercased_user_id, password)).await {
|
match Box::pin(ldap_login(services, &user_id, &lowercased_user_id, password)).await {
|
||||||
| Ok(user_id) => Ok(user_id),
|
| Ok(user_id) => Ok(user_id),
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "conduwuit_build_metadata"
|
name = "conduwuit_build_metadata"
|
||||||
categories.workspace = true
|
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
keywords.workspace = true
|
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "conduwuit_core"
|
name = "conduwuit_core"
|
||||||
categories.workspace = true
|
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
keywords.workspace = true
|
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|||||||
@@ -146,22 +146,6 @@ pub fn check(config: &Config) -> Result {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if we can read the token file path, and check if the file is empty
|
|
||||||
if config.registration_token_file.as_ref().is_some_and(|path| {
|
|
||||||
let Ok(token) = std::fs::read_to_string(path).inspect_err(|e| {
|
|
||||||
error!("Failed to read the registration token file: {e}");
|
|
||||||
}) else {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
token == String::new()
|
|
||||||
}) {
|
|
||||||
return Err!(Config(
|
|
||||||
"registration_token_file",
|
|
||||||
"Registration token file was specified but is empty or failed to be read"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.max_request_size < 10_000_000 {
|
if config.max_request_size < 10_000_000 {
|
||||||
return Err!(Config(
|
return Err!(Config(
|
||||||
"max_request_size",
|
"max_request_size",
|
||||||
@@ -187,29 +171,9 @@ pub fn check(config: &Config) -> Result {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.allow_registration
|
|
||||||
&& !config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
|
|
||||||
&& config.registration_token.is_none()
|
|
||||||
&& config.registration_token_file.is_none()
|
|
||||||
&& config.recaptcha_site_key.is_none()
|
|
||||||
{
|
|
||||||
return Err!(Config(
|
|
||||||
"registration_token",
|
|
||||||
"!! You have `allow_registration` enabled without a token or captcha configured \
|
|
||||||
which means you are allowing ANYONE to register on your continuwuity instance \
|
|
||||||
without any 2nd-step (e.g. registration token, captcha), which is FREQUENTLY \
|
|
||||||
abused by malicious actors. If this is not the intended behaviour, please set a \
|
|
||||||
registration token. For security and safety reasons, continuwuity will shut down. \
|
|
||||||
If you are extra sure this is the desired behaviour you want, please set the \
|
|
||||||
following config option to true:
|
|
||||||
`yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.allow_registration
|
if config.allow_registration
|
||||||
&& config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
|
&& config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
|
||||||
&& config.registration_token.is_none()
|
&& config.registration_token.is_none()
|
||||||
&& config.registration_token_file.is_none()
|
|
||||||
{
|
{
|
||||||
warn!(
|
warn!(
|
||||||
"Open registration is enabled via setting \
|
"Open registration is enabled via setting \
|
||||||
|
|||||||
+5
-12
@@ -545,7 +545,7 @@ pub struct Config {
|
|||||||
/// `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
|
/// `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
|
||||||
///
|
///
|
||||||
/// If you would like registration only via token reg, please configure
|
/// If you would like registration only via token reg, please configure
|
||||||
/// `registration_token` or `registration_token_file`.
|
/// `registration_token`.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub allow_registration: bool,
|
pub allow_registration: bool,
|
||||||
|
|
||||||
@@ -576,22 +576,14 @@ pub struct Config {
|
|||||||
/// `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
|
/// `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
|
||||||
/// to true to allow open registration without any conditions.
|
/// to true to allow open registration without any conditions.
|
||||||
///
|
///
|
||||||
/// YOU NEED TO EDIT THIS OR USE registration_token_file.
|
/// If you do not want to set a static token, the `!admin token` commands
|
||||||
|
/// may also be used to manage registration tokens.
|
||||||
///
|
///
|
||||||
/// example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
|
/// example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
|
||||||
///
|
///
|
||||||
/// display: sensitive
|
/// display: sensitive
|
||||||
pub registration_token: Option<String>,
|
pub registration_token: Option<String>,
|
||||||
|
|
||||||
/// Path to a file on the system that gets read for additional registration
|
|
||||||
/// tokens. Multiple tokens can be added if you separate them with
|
|
||||||
/// whitespace
|
|
||||||
///
|
|
||||||
/// continuwuity must be able to access the file, and it must not be empty
|
|
||||||
///
|
|
||||||
/// example: "/etc/continuwuity/.reg_token"
|
|
||||||
pub registration_token_file: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// The public site key for reCaptcha. If this is provided, reCaptcha
|
/// The public site key for reCaptcha. If this is provided, reCaptcha
|
||||||
/// becomes required during registration. If both captcha *and*
|
/// becomes required during registration. If both captcha *and*
|
||||||
/// registration token are enabled, both will be required during
|
/// registration token are enabled, both will be required during
|
||||||
@@ -2296,7 +2288,7 @@ pub struct DraupnirConfig {
|
|||||||
pub secret: String,
|
pub secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEPRECATED_KEYS: &[&str; 9] = &[
|
const DEPRECATED_KEYS: &[&str] = &[
|
||||||
"cache_capacity",
|
"cache_capacity",
|
||||||
"conduit_cache_capacity_modifier",
|
"conduit_cache_capacity_modifier",
|
||||||
"max_concurrent_requests",
|
"max_concurrent_requests",
|
||||||
@@ -2306,6 +2298,7 @@ const DEPRECATED_KEYS: &[&str; 9] = &[
|
|||||||
"well_known_support_role",
|
"well_known_support_role",
|
||||||
"well_known_support_email",
|
"well_known_support_email",
|
||||||
"well_known_support_mxid",
|
"well_known_support_mxid",
|
||||||
|
"registration_token_file",
|
||||||
];
|
];
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "conduwuit_database"
|
name = "conduwuit_database"
|
||||||
categories.workspace = true
|
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
keywords.workspace = true
|
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ pub(super) static MAPS: &[Descriptor] = &[
|
|||||||
name: "referencedevents",
|
name: "referencedevents",
|
||||||
..descriptor::RANDOM
|
..descriptor::RANDOM
|
||||||
},
|
},
|
||||||
|
Descriptor {
|
||||||
|
name: "registrationtoken_info",
|
||||||
|
..descriptor::RANDOM_SMALL
|
||||||
|
},
|
||||||
Descriptor {
|
Descriptor {
|
||||||
name: "roomid_invitedcount",
|
name: "roomid_invitedcount",
|
||||||
..descriptor::RANDOM_SMALL
|
..descriptor::RANDOM_SMALL
|
||||||
@@ -390,6 +394,10 @@ pub(super) static MAPS: &[Descriptor] = &[
|
|||||||
name: "userid_lock",
|
name: "userid_lock",
|
||||||
..descriptor::RANDOM_SMALL
|
..descriptor::RANDOM_SMALL
|
||||||
},
|
},
|
||||||
|
Descriptor {
|
||||||
|
name: "userid_logindisabled",
|
||||||
|
..descriptor::RANDOM_SMALL
|
||||||
|
},
|
||||||
Descriptor {
|
Descriptor {
|
||||||
name: "userid_presenceid",
|
name: "userid_presenceid",
|
||||||
..descriptor::RANDOM_SMALL
|
..descriptor::RANDOM_SMALL
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "conduwuit_macros"
|
name = "conduwuit_macros"
|
||||||
categories.workspace = true
|
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
keywords.workspace = true
|
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|||||||
+2
-6
@@ -2,15 +2,12 @@
|
|||||||
name = "conduwuit"
|
name = "conduwuit"
|
||||||
default-run = "conduwuit"
|
default-run = "conduwuit"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
categories.workspace = true
|
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
keywords.workspace = true
|
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
rust-version.workspace = true
|
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
metadata.crane.workspace = true
|
metadata.crane.workspace = true
|
||||||
|
|
||||||
@@ -23,14 +20,13 @@ crate-type = [
|
|||||||
|
|
||||||
[package.metadata.deb]
|
[package.metadata.deb]
|
||||||
name = "continuwuity"
|
name = "continuwuity"
|
||||||
maintainer = "continuwuity developers <contact@continuwuity.org>"
|
maintainer = "Continuwuity Team and contributors <team@continuwuity.org>"
|
||||||
copyright = "2024, continuwuity developers"
|
|
||||||
license-file = ["../../LICENSE", "3"]
|
license-file = ["../../LICENSE", "3"]
|
||||||
depends = "$auto, ca-certificates"
|
depends = "$auto, ca-certificates"
|
||||||
breaks = ["conduwuit (<<0.5.0)"]
|
breaks = ["conduwuit (<<0.5.0)"]
|
||||||
replaces = ["conduwuit (<<0.5.0)"]
|
replaces = ["conduwuit (<<0.5.0)"]
|
||||||
extended-description = """\
|
extended-description = """\
|
||||||
a cool hard fork of Conduit, a Matrix homeserver written in Rust"""
|
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver."""
|
||||||
section = "net"
|
section = "net"
|
||||||
priority = "optional"
|
priority = "optional"
|
||||||
conf-files = ["/etc/conduwuit/conduwuit.toml"]
|
conf-files = ["/etc/conduwuit/conduwuit.toml"]
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "conduwuit_router"
|
name = "conduwuit_router"
|
||||||
categories.workspace = true
|
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
keywords.workspace = true
|
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "conduwuit_service"
|
name = "conduwuit_service"
|
||||||
categories.workspace = true
|
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
keywords.workspace = true
|
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ pub struct Service {
|
|||||||
pub server_user: OwnedUserId,
|
pub server_user: OwnedUserId,
|
||||||
pub admin_alias: OwnedRoomAliasId,
|
pub admin_alias: OwnedRoomAliasId,
|
||||||
pub turn_secret: String,
|
pub turn_secret: String,
|
||||||
pub registration_token: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RateLimitState = (Instant, u32); // Time if last failed try, number of failed tries
|
type RateLimitState = (Instant, u32); // Time if last failed try, number of failed tries
|
||||||
@@ -41,19 +40,6 @@ impl crate::Service for Service {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let registration_token = config.registration_token_file.as_ref().map_or_else(
|
|
||||||
|| config.registration_token.clone(),
|
|
||||||
|path| {
|
|
||||||
let Ok(token) = std::fs::read_to_string(path).inspect_err(|e| {
|
|
||||||
error!("Failed to read the registration token file: {e}");
|
|
||||||
}) else {
|
|
||||||
return config.registration_token.clone();
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(token.trim().to_owned())
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(Arc::new(Self {
|
Ok(Arc::new(Self {
|
||||||
db,
|
db,
|
||||||
server: args.server.clone(),
|
server: args.server.clone(),
|
||||||
@@ -66,7 +52,6 @@ impl crate::Service for Service {
|
|||||||
)
|
)
|
||||||
.expect("@conduit:server_name is valid"),
|
.expect("@conduit:server_name is valid"),
|
||||||
turn_secret,
|
turn_secret,
|
||||||
registration_token,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ pub mod media;
|
|||||||
pub mod moderation;
|
pub mod moderation;
|
||||||
pub mod presence;
|
pub mod presence;
|
||||||
pub mod pusher;
|
pub mod pusher;
|
||||||
|
pub mod registration_tokens;
|
||||||
pub mod resolver;
|
pub mod resolver;
|
||||||
pub mod rooms;
|
pub mod rooms;
|
||||||
pub mod sending;
|
pub mod sending;
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
use std::{sync::Arc, time::SystemTime};
|
||||||
|
|
||||||
|
use conduwuit::utils::{
|
||||||
|
self,
|
||||||
|
stream::{ReadyExt, TryIgnore},
|
||||||
|
};
|
||||||
|
use database::{Database, Deserialized, Json, Map};
|
||||||
|
use futures::Stream;
|
||||||
|
use ruma::OwnedUserId;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub(super) struct Data {
|
||||||
|
registrationtoken_info: Arc<Map>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Metadata of a registration token.
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct DatabaseTokenInfo {
|
||||||
|
/// The admin user who created this token.
|
||||||
|
pub creator: OwnedUserId,
|
||||||
|
/// The number of times this token has been used to create an account.
|
||||||
|
pub uses: u64,
|
||||||
|
/// When this token will expire, if it expires.
|
||||||
|
pub expires: Option<TokenExpires>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DatabaseTokenInfo {
|
||||||
|
pub(super) fn new(creator: OwnedUserId, expires: Option<TokenExpires>) -> Self {
|
||||||
|
Self { creator, uses: 0, expires }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine whether this token info represents a valid token, i.e. one
|
||||||
|
/// that has not expired according to its [`Self::expires`] property. If
|
||||||
|
/// [`Self::expires`] is [`None`], this function will always return `true`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
match self.expires {
|
||||||
|
| Some(TokenExpires::AfterUses(max_uses)) => self.uses < max_uses,
|
||||||
|
| Some(TokenExpires::AfterTime(expiry_time)) => {
|
||||||
|
let now = SystemTime::now();
|
||||||
|
|
||||||
|
expiry_time >= now
|
||||||
|
},
|
||||||
|
| None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for DatabaseTokenInfo {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Token created by {} and used {} times. ", &self.creator, self.uses)?;
|
||||||
|
if let Some(expires) = &self.expires {
|
||||||
|
write!(f, "{expires}.")?;
|
||||||
|
} else {
|
||||||
|
write!(f, "Never expires.")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum TokenExpires {
|
||||||
|
AfterUses(u64),
|
||||||
|
AfterTime(SystemTime),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for TokenExpires {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
| Self::AfterUses(max_uses) => write!(f, "Expires after {max_uses} uses"),
|
||||||
|
| Self::AfterTime(max_age) => {
|
||||||
|
let now = SystemTime::now();
|
||||||
|
let formatted_expiry = utils::time::format(*max_age, "%+");
|
||||||
|
|
||||||
|
match max_age.duration_since(now) {
|
||||||
|
| Ok(duration) => write!(
|
||||||
|
f,
|
||||||
|
"Expires in {} ({formatted_expiry})",
|
||||||
|
utils::time::pretty(duration)
|
||||||
|
),
|
||||||
|
| Err(_) => write!(f, "Expired at {formatted_expiry}"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Data {
|
||||||
|
pub(super) fn new(db: &Arc<Database>) -> Self {
|
||||||
|
Self {
|
||||||
|
registrationtoken_info: db["registrationtoken_info"].clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Associate a registration token with its metadata in the database.
|
||||||
|
pub(super) fn save_token(&self, token: &str, info: &DatabaseTokenInfo) {
|
||||||
|
self.registrationtoken_info.raw_put(token, Json(info));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a registration token.
|
||||||
|
pub(super) fn revoke_token(&self, token: &str) { self.registrationtoken_info.remove(token); }
|
||||||
|
|
||||||
|
/// Look up a registration token's metadata.
|
||||||
|
pub(super) async fn lookup_token_info(&self, token: &str) -> Option<DatabaseTokenInfo> {
|
||||||
|
self.registrationtoken_info
|
||||||
|
.get(token)
|
||||||
|
.await
|
||||||
|
.deserialized()
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over all valid tokens and delete expired ones.
|
||||||
|
pub(super) fn iterate_and_clean_tokens(
|
||||||
|
&self,
|
||||||
|
) -> impl Stream<Item = (&str, DatabaseTokenInfo)> + Send + '_ {
|
||||||
|
self.registrationtoken_info
|
||||||
|
.stream()
|
||||||
|
.ignore_err()
|
||||||
|
.ready_filter_map(|item: (&str, DatabaseTokenInfo)| {
|
||||||
|
if item.1.is_valid() {
|
||||||
|
Some(item)
|
||||||
|
} else {
|
||||||
|
self.registrationtoken_info.remove(item.0);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
mod data;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use conduwuit::{Err, Result, utils};
|
||||||
|
use data::Data;
|
||||||
|
pub use data::{DatabaseTokenInfo, TokenExpires};
|
||||||
|
use futures::{Stream, StreamExt, stream};
|
||||||
|
use ruma::OwnedUserId;
|
||||||
|
|
||||||
|
use crate::{Dep, config};
|
||||||
|
|
||||||
|
const RANDOM_TOKEN_LENGTH: usize = 16;
|
||||||
|
|
||||||
|
pub struct Service {
|
||||||
|
db: Data,
|
||||||
|
services: Services,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Services {
|
||||||
|
config: Dep<config::Service>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A validated registration token which may be used to create an account.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ValidToken {
|
||||||
|
pub token: String,
|
||||||
|
pub source: ValidTokenSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ValidToken {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "`{}` --- {}", self.token, &self.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<str> for ValidToken {
|
||||||
|
fn eq(&self, other: &str) -> bool { self.token == other }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The source of a valid database token.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ValidTokenSource {
|
||||||
|
/// The static token set in the homeserver's config file, which is
|
||||||
|
/// always valid.
|
||||||
|
ConfigFile,
|
||||||
|
/// A database token which has been checked to be valid.
|
||||||
|
Database(DatabaseTokenInfo),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ValidTokenSource {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
| Self::ConfigFile => write!(f, "Token defined in config."),
|
||||||
|
| Self::Database(info) => info.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::Service for Service {
|
||||||
|
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||||
|
Ok(Arc::new(Self {
|
||||||
|
db: Data::new(args.db),
|
||||||
|
services: Services {
|
||||||
|
config: args.depend::<config::Service>("config"),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Service {
|
||||||
|
/// Issue a new registration token and save it in the database.
|
||||||
|
pub fn issue_token(
|
||||||
|
&self,
|
||||||
|
creator: OwnedUserId,
|
||||||
|
expires: Option<TokenExpires>,
|
||||||
|
) -> (String, DatabaseTokenInfo) {
|
||||||
|
let token = utils::random_string(RANDOM_TOKEN_LENGTH);
|
||||||
|
let info = DatabaseTokenInfo::new(creator, expires);
|
||||||
|
|
||||||
|
self.db.save_token(&token, &info);
|
||||||
|
(token, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the registration token set in the config file, if it exists.
|
||||||
|
pub fn get_config_file_token(&self) -> Option<ValidToken> {
|
||||||
|
self.services
|
||||||
|
.config
|
||||||
|
.registration_token
|
||||||
|
.clone()
|
||||||
|
.map(|token| ValidToken {
|
||||||
|
token,
|
||||||
|
source: ValidTokenSource::ConfigFile,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate a registration token.
|
||||||
|
pub async fn validate_token(&self, token: String) -> Option<ValidToken> {
|
||||||
|
// Check the registration token in the config first
|
||||||
|
if self
|
||||||
|
.get_config_file_token()
|
||||||
|
.is_some_and(|valid_token| valid_token == *token)
|
||||||
|
{
|
||||||
|
return Some(ValidToken {
|
||||||
|
token,
|
||||||
|
source: ValidTokenSource::ConfigFile,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now check the database
|
||||||
|
if let Some(token_info) = self.db.lookup_token_info(&token).await
|
||||||
|
&& token_info.is_valid()
|
||||||
|
{
|
||||||
|
return Some(ValidToken {
|
||||||
|
token,
|
||||||
|
source: ValidTokenSource::Database(token_info),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise it's not valid
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark a valid token as having been used to create a new account.
|
||||||
|
pub fn mark_token_as_used(&self, ValidToken { token, source }: ValidToken) {
|
||||||
|
match source {
|
||||||
|
| ValidTokenSource::ConfigFile => {
|
||||||
|
// we don't track uses of the config file token, do nothing
|
||||||
|
},
|
||||||
|
| ValidTokenSource::Database(mut info) => {
|
||||||
|
info.uses = info.uses.saturating_add(1);
|
||||||
|
|
||||||
|
self.db.save_token(&token, &info);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to revoke a valid token.
|
||||||
|
///
|
||||||
|
/// Note that some tokens (like the one set in the config file) cannot be
|
||||||
|
/// revoked.
|
||||||
|
pub fn revoke_token(&self, ValidToken { token, source }: ValidToken) -> Result {
|
||||||
|
match source {
|
||||||
|
| ValidTokenSource::ConfigFile => {
|
||||||
|
// the config file token cannot be revoked
|
||||||
|
Err!(
|
||||||
|
"The token set in the config file cannot be revoked. Edit the config file \
|
||||||
|
to change it."
|
||||||
|
)
|
||||||
|
},
|
||||||
|
| ValidTokenSource::Database(_) => {
|
||||||
|
self.db.revoke_token(&token);
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over all valid registration tokens.
|
||||||
|
pub fn iterate_tokens(&self) -> impl Stream<Item = ValidToken> + Send + '_ {
|
||||||
|
let db_tokens = self
|
||||||
|
.db
|
||||||
|
.iterate_and_clean_tokens()
|
||||||
|
.map(|(token, info)| ValidToken {
|
||||||
|
token: token.to_owned(),
|
||||||
|
source: ValidTokenSource::Database(info),
|
||||||
|
});
|
||||||
|
|
||||||
|
stream::iter(self.get_config_file_token()).chain(db_tokens)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,8 +11,9 @@ use crate::{
|
|||||||
account_data, admin, announcements, antispam, appservice, client, config, emergency,
|
account_data, admin, announcements, antispam, appservice, client, config, emergency,
|
||||||
federation, globals, key_backups,
|
federation, globals, key_backups,
|
||||||
manager::Manager,
|
manager::Manager,
|
||||||
media, moderation, presence, pusher, resolver, rooms, sending, server_keys, service,
|
media, moderation, presence, pusher, registration_tokens, resolver, rooms, sending,
|
||||||
service::{Args, Map, Service},
|
server_keys,
|
||||||
|
service::{self, Args, Map, Service},
|
||||||
sync, transaction_ids, uiaa, users,
|
sync, transaction_ids, uiaa, users,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ pub struct Services {
|
|||||||
pub media: Arc<media::Service>,
|
pub media: Arc<media::Service>,
|
||||||
pub presence: Arc<presence::Service>,
|
pub presence: Arc<presence::Service>,
|
||||||
pub pusher: Arc<pusher::Service>,
|
pub pusher: Arc<pusher::Service>,
|
||||||
|
pub registration_tokens: Arc<registration_tokens::Service>,
|
||||||
pub resolver: Arc<resolver::Service>,
|
pub resolver: Arc<resolver::Service>,
|
||||||
pub rooms: rooms::Service,
|
pub rooms: rooms::Service,
|
||||||
pub federation: Arc<federation::Service>,
|
pub federation: Arc<federation::Service>,
|
||||||
@@ -77,6 +79,7 @@ impl Services {
|
|||||||
media: build!(media::Service),
|
media: build!(media::Service),
|
||||||
presence: build!(presence::Service),
|
presence: build!(presence::Service),
|
||||||
pusher: build!(pusher::Service),
|
pusher: build!(pusher::Service),
|
||||||
|
registration_tokens: build!(registration_tokens::Service),
|
||||||
rooms: rooms::Service {
|
rooms: rooms::Service {
|
||||||
alias: build!(rooms::alias::Service),
|
alias: build!(rooms::alias::Service),
|
||||||
auth_chain: build!(rooms::auth_chain::Service),
|
auth_chain: build!(rooms::auth_chain::Service),
|
||||||
|
|||||||
+17
-27
@@ -1,7 +1,4 @@
|
|||||||
use std::{
|
use std::{collections::BTreeMap, sync::Arc};
|
||||||
collections::{BTreeMap, HashSet},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Error, Result, SyncRwLock, err, error, implement, utils,
|
Err, Error, Result, SyncRwLock, err, error, implement, utils,
|
||||||
@@ -16,7 +13,7 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{Dep, config, globals, users};
|
use crate::{Dep, config, globals, registration_tokens, users};
|
||||||
|
|
||||||
pub struct Service {
|
pub struct Service {
|
||||||
userdevicesessionid_uiaarequest: SyncRwLock<RequestMap>,
|
userdevicesessionid_uiaarequest: SyncRwLock<RequestMap>,
|
||||||
@@ -28,6 +25,7 @@ struct Services {
|
|||||||
globals: Dep<globals::Service>,
|
globals: Dep<globals::Service>,
|
||||||
users: Dep<users::Service>,
|
users: Dep<users::Service>,
|
||||||
config: Dep<config::Service>,
|
config: Dep<config::Service>,
|
||||||
|
registration_tokens: Dep<registration_tokens::Service>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Data {
|
struct Data {
|
||||||
@@ -50,6 +48,8 @@ impl crate::Service for Service {
|
|||||||
globals: args.depend::<globals::Service>("globals"),
|
globals: args.depend::<globals::Service>("globals"),
|
||||||
users: args.depend::<users::Service>("users"),
|
users: args.depend::<users::Service>("users"),
|
||||||
config: args.depend::<config::Service>("config"),
|
config: args.depend::<config::Service>("config"),
|
||||||
|
registration_tokens: args
|
||||||
|
.depend::<registration_tokens::Service>("registration_tokens"),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -57,26 +57,6 @@ impl crate::Service for Service {
|
|||||||
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[implement(Service)]
|
|
||||||
pub async fn read_tokens(&self) -> Result<HashSet<String>> {
|
|
||||||
let mut tokens = HashSet::new();
|
|
||||||
if let Some(file) = &self.services.config.registration_token_file.as_ref() {
|
|
||||||
match std::fs::read_to_string(file) {
|
|
||||||
| Ok(text) => {
|
|
||||||
text.split_ascii_whitespace().for_each(|token| {
|
|
||||||
tokens.insert(token.to_owned());
|
|
||||||
});
|
|
||||||
},
|
|
||||||
| Err(e) => error!("Failed to read the registration token file: {e}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(token) = &self.services.config.registration_token {
|
|
||||||
tokens.insert(token.to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(tokens)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new Uiaa session. Make sure the session token is unique.
|
/// Creates a new Uiaa session. Make sure the session token is unique.
|
||||||
#[implement(Service)]
|
#[implement(Service)]
|
||||||
pub fn create(
|
pub fn create(
|
||||||
@@ -229,8 +209,18 @@ pub async fn try_auth(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
| AuthData::RegistrationToken(t) => {
|
| AuthData::RegistrationToken(t) => {
|
||||||
let tokens = self.read_tokens().await?;
|
let token = t.token.trim().to_owned();
|
||||||
if tokens.contains(t.token.trim()) {
|
|
||||||
|
if let Some(valid_token) = self
|
||||||
|
.services
|
||||||
|
.registration_tokens
|
||||||
|
.validate_token(token)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
self.services
|
||||||
|
.registration_tokens
|
||||||
|
.mark_token_as_used(valid_token);
|
||||||
|
|
||||||
uiaainfo.completed.push(AuthType::RegistrationToken);
|
uiaainfo.completed.push(AuthType::RegistrationToken);
|
||||||
} else {
|
} else {
|
||||||
uiaainfo.auth_error = Some(StandardErrorBody {
|
uiaainfo.auth_error = Some(StandardErrorBody {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ struct Data {
|
|||||||
userid_password: Arc<Map>,
|
userid_password: Arc<Map>,
|
||||||
userid_suspension: Arc<Map>,
|
userid_suspension: Arc<Map>,
|
||||||
userid_lock: Arc<Map>,
|
userid_lock: Arc<Map>,
|
||||||
|
userid_logindisabled: Arc<Map>,
|
||||||
userid_selfsigningkeyid: Arc<Map>,
|
userid_selfsigningkeyid: Arc<Map>,
|
||||||
userid_usersigningkeyid: Arc<Map>,
|
userid_usersigningkeyid: Arc<Map>,
|
||||||
useridprofilekey_value: Arc<Map>,
|
useridprofilekey_value: Arc<Map>,
|
||||||
@@ -117,6 +118,7 @@ impl crate::Service for Service {
|
|||||||
userid_password: args.db["userid_password"].clone(),
|
userid_password: args.db["userid_password"].clone(),
|
||||||
userid_suspension: args.db["userid_suspension"].clone(),
|
userid_suspension: args.db["userid_suspension"].clone(),
|
||||||
userid_lock: args.db["userid_lock"].clone(),
|
userid_lock: args.db["userid_lock"].clone(),
|
||||||
|
userid_logindisabled: args.db["userid_logindisabled"].clone(),
|
||||||
userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(),
|
userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(),
|
||||||
userid_usersigningkeyid: args.db["userid_usersigningkeyid"].clone(),
|
userid_usersigningkeyid: args.db["userid_usersigningkeyid"].clone(),
|
||||||
useridprofilekey_value: args.db["useridprofilekey_value"].clone(),
|
useridprofilekey_value: args.db["useridprofilekey_value"].clone(),
|
||||||
@@ -295,6 +297,16 @@ impl Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn disable_login(&self, user_id: &UserId) {
|
||||||
|
self.db.userid_logindisabled.insert(user_id, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_login(&self, user_id: &UserId) { self.db.userid_logindisabled.remove(user_id); }
|
||||||
|
|
||||||
|
pub async fn is_login_disabled(&self, user_id: &UserId) -> bool {
|
||||||
|
self.db.userid_logindisabled.contains(user_id).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if account is active, infallible
|
/// Check if account is active, infallible
|
||||||
pub async fn is_active(&self, user_id: &UserId) -> bool {
|
pub async fn is_active(&self, user_id: &UserId) -> bool {
|
||||||
!self.is_deactivated(user_id).await.unwrap_or(true)
|
!self.is_deactivated(user_id).await.unwrap_or(true)
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "conduwuit_web"
|
name = "conduwuit_web"
|
||||||
categories.workspace = true
|
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
keywords.workspace = true
|
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "xtask-generate-commands"
|
name = "xtask-generate-commands"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
categories.workspace = true
|
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
keywords.workspace = true
|
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
rust-version.workspace = true
|
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "xtask"
|
name = "xtask"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
categories.workspace = true
|
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
keywords.workspace = true
|
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
rust-version.workspace = true
|
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
Reference in New Issue
Block a user