Compare commits

..

17 Commits

Author SHA1 Message Date
Ginger
4e1dac32a5 fix: Don't panic when running startup admin commands 2026-02-15 17:32:26 -05:00
timedout
7b21c3fd9f chore: Update changelog 2026-02-15 20:39:14 +00:00
timedout
f566ca1b93 chore: Release 0.5.5 2026-02-15 20:31:58 +00:00
timedout
debe411e23 fix(ci): Work around LLVM issue & dynamically select clang pkg version 2026-02-15 20:27:55 +00:00
timedout
dc0d6a9220 fix: Install clang-23 specifically
clang (clang-22) is busted
2026-02-15 19:09:33 +00:00
timedout
2efdb6fb0d fix: Work around https://github.com/llvm/llvm-project/issues/153385 2026-02-15 18:55:17 +00:00
Ginger
576348a445 fix: Set default value of allow_registration to true 2026-02-15 18:05:42 +00:00
Ginger
f322b6dca0 chore: News fragment 2026-02-15 18:05:42 +00:00
Ginger
a1ed77a99c feat: Add a link to the clients list on matrix.org 2026-02-15 18:05:42 +00:00
Ginger
01b5dffeee feat: Default index page improvements
- Add project logo to footer and favicon
- Display different messages depending on if first-run mode is active
2026-02-15 18:05:42 +00:00
Ginger
ea3c00da43 chore: Clippy fixes 2026-02-15 18:05:42 +00:00
Ginger
047eba0442 feat: Improve the initial setup experience
- Issue a single-use token for initial account creation
- Disable registration through other methods until the first account is made
- Print helpful instructions to the console on the first run
- Improve the welcome message sent in the admin room on first run
2026-02-15 18:05:42 +00:00
Ginger
11a088be5d feat: Stop logging announcements to the console 2026-02-15 18:05:42 +00:00
Ginger
dc6bd4e541 fix: Silence unnecessary policy server errors in debug builds 2026-02-15 18:05:42 +00:00
Ginger
2bf9207cc4 feat: Add skeleton first-run service 2026-02-15 18:05:42 +00:00
Ginger
b2a87e2fb9 refactor: Add support for multiple static tokens to registration token service 2026-02-15 18:05:42 +00:00
timedout
7d0686f33c fix: Error response can leak appservice token
Reviewed-By: Ginger <ginger@gingershaped.computer>
Reviewed-By: Jade Ellis <jade@ellis.link>
2026-02-15 17:58:48 +00:00
37 changed files with 688 additions and 243 deletions

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
container: ["ubuntu-latest", "ubuntu-previous", "debian-latest", "debian-oldstable"]
container: [ "ubuntu-latest", "ubuntu-previous", "debian-latest", "debian-oldstable" ]
container:
image: "ghcr.io/tcpipuk/act-runner:${{ matrix.container }}"
@@ -30,6 +30,28 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "distribution=$DISTRIBUTION" >> $GITHUB_OUTPUT
echo "Debian distribution: $DISTRIBUTION ($VERSION)"
- name: Work around llvm-project#153385
id: llvm-workaround
run: |
if [ -f /usr/share/apt/default-sequoia.config ]; then
echo "Applying workaround for llvm-project#153385"
mkdir -p /etc/crypto-policies/back-ends/
cp /usr/share/apt/default-sequoia.config /etc/crypto-policies/back-ends/apt-sequoia.config
sed -i 's/\(sha1\.second_preimage_resistance = \)2026-02-01/\12026-06-01/' /etc/crypto-policies/back-ends/apt-sequoia.config
else
echo "No workaround needed for llvm-project#153385"
fi
- name: Pick compatible clang version
id: clang-version
run: |
# both latest need to use clang-23, but oldstable and previous can just use clang
if [[ "${{ matrix.container }}" == "ubuntu-latest" || "${{ matrix.container }}" == "debian-latest" ]]; then
echo "Using clang-23 package for ${{ matrix.container }}"
echo "version=clang-23" >> $GITHUB_OUTPUT
else
echo "Using default clang package for ${{ matrix.container }}"
echo "version=clang" >> $GITHUB_OUTPUT
fi
- name: Checkout repository with full history
uses: actions/checkout@v6
@@ -105,7 +127,7 @@ jobs:
run: |
apt-get update -y
# Build dependencies for rocksdb
apt-get install -y clang liburing-dev
apt-get install -y liburing-dev ${{ steps.clang-version.outputs.version }}
- name: Run cargo-deb
id: cargo-deb

View File

@@ -1,25 +1,65 @@
# Continuwuity v0.5.5 (2026-02-15)
## Features
- Added unstable support for [MSC4406:
`M_SENDER_IGNORED`](https://github.com/matrix-org/matrix-spec-proposals/pull/4406).
Contributed by @nex ([#1308](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1308))
- Introduce a resolver command to allow flushing a server from the cache or to flush the complete cache. Contributed by
@Omar007 ([#1349](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1349))
- Improved the handling of restricted join rules and improved the performance of local-first joins. Contributed by
@nex. ([#1368](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1368))
- You can now set a custom User Agent for URL previews; the default one has been modified to be less likely to be
rejected. Contributed by @trashpanda ([#1372](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1372))
- Improved the first-time setup experience for new homeserver administrators:
- Account registration is disabled on the first run, except for with a new special registration token that is logged
to the console.
- Other helpful information is logged to the console as well, including a giant warning if open registration is
enabled.
- The default index page now says to check the console for setup instructions if no accounts have been created.
- Once the first admin account is created, an improved welcome message is sent to the admin room.
Contributed by @ginger.
## Bugfixes
- Fixed invites sent to other users in the same homeserver not being properly sent down sync. Users with missing or
broken invites should clear their client caches after updating to make them appear. ([#1249](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1249))
- LDAP-enabled servers will no longer have all admins demoted when LDAP-controlled admins are not configured.
Contributed by @Jade ([#1307](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1307))
- Fixed sliding sync not resolving wildcard state key requests, enabling Video/Audio calls in Element X. ([#1370](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1370))
## Misc
- #1344
# Continuwuity v0.5.4 (2026-02-08)
## Features
- The announcement checker will now announce errors it encounters in the first run to the admin room, plus a few other
misc improvements. Contributed by @Jade ([#1288](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1288))
- Drastically improved the performance and reliability of account deactivations. Contributed by @nex ([#1314](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1314))
- Drastically improved the performance and reliability of account deactivations. Contributed by
@nex ([#1314](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1314))
- Refuse to process requests for and events in rooms that we no longer have any local users in (reduces state resets
and improves performance). Contributed by @nex ([#1316](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1316))
and improves performance). Contributed by
@nex ([#1316](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1316))
- Added server-specific admin API routes to ban and unban rooms, for use with moderation bots. Contributed by @nex
([#1301](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1301))
## Bugfixes
- Fix the generated configuration containing uncommented optional sections. Contributed by @Jade ([#1290](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1290))
- Fixed specification non-compliance when handling remote media errors. Contributed by @nex ([#1298](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1298))
- Fix the generated configuration containing uncommented optional sections. Contributed by
@Jade ([#1290](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1290))
- Fixed specification non-compliance when handling remote media errors. Contributed by
@nex ([#1298](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1298))
- UIAA requests which check for out-of-band success (sent by matrix-js-sdk) will no longer create unhelpful errors in
the logs. Contributed by @ginger ([#1305](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1305))
- Use exists instead of contains to save writing to a buffer in `src/service/users/mod.rs`: `is_login_disabled`.
Contributed
by @aprilgrimoire. ([#1340](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1340))
- Fixed backtraces being swallowed during panics. Contributed by @jade ([#1337](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1337))
- Fixed backtraces being swallowed during panics. Contributed by
@jade ([#1337](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1337))
- Fixed a potential vulnerability that could allow an evil remote server to return malicious events during the room join
and knock process. Contributed by @nex, reported by violet & [mat](https://matdoes.dev).
- Fixed a race condition that could result in outlier PDUs being incorrectly marked as visible to a remote server.
@@ -28,25 +68,30 @@ ## Bugfixes
## Docs
- Fixed Fedora install instructions. Contributed by @julian45 ([#1342](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1342))
- Fixed Fedora install instructions. Contributed by
@julian45 ([#1342](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1342))
# Continuwuity 0.5.3 (2026-01-12)
## Features
- Improve the display of nested configuration with the `!admin server show-config` command. Contributed by @Jade ([#1279](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1279))
- Improve the display of nested configuration with the `!admin server show-config` command. Contributed by
@Jade ([#1279](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1279))
## Bugfixes
- Fixed `M_BAD_JSON` error when sending invites to other servers or when providing joins. Contributed by @nex ([#1286](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1286))
- Fixed `M_BAD_JSON` error when sending invites to other servers or when providing joins. Contributed by
@nex ([#1286](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1286))
## Docs
- Improve admin command documentation generation. Contributed by @ginger ([#1280](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1280))
- Improve admin command documentation generation. Contributed by
@ginger ([#1280](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1280))
## Misc
- Improve timeout-related code for federation and URL previews. Contributed by @Jade ([#1278](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1278))
- Improve timeout-related code for federation and URL previews. Contributed by
@Jade ([#1278](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1278))
# Continuwuity 0.5.2 (2026-01-09)
@@ -57,11 +102,14 @@ ## Features
after a certain amount of time has passed. Additionally, the `registration_token_file` configuration option is
superseded by this feature and **has been removed**. Use the new `!admin token` command family to manage registration
tokens. Contributed by @ginger (#783).
- Implemented a configuration defined admin list independent of the admin room. Contributed by @Terryiscool160. ([#1253](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1253))
- Implemented a configuration defined admin list independent of the admin room. Contributed by
@Terryiscool160. ([#1253](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1253))
- Added support for invite and join anti-spam via Draupnir and Meowlnir, similar to that of synapse-http-antispam.
Contributed by @nex. ([#1263](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1263))
- Implemented account locking functionality, to complement user suspension. Contributed by @nex. ([#1266](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1266))
- Added admin command to forcefully log out all of a user's existing sessions. Contributed by @nex. ([#1271](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1271))
- Implemented account locking functionality, to complement user suspension. Contributed by
@nex. ([#1266](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1266))
- Added admin command to forcefully log out all of a user's existing sessions. Contributed by
@nex. ([#1271](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1271))
- Implemented toggling the ability for an account to log in without mutating any of its data. Contributed by @nex. (
[#1272](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1272))
- Add support for custom room create event timestamps, to allow generating custom prefixes in hashed room IDs.
@@ -71,7 +119,8 @@ ## Features
## Bugfixes
- Fixed unreliable room summary fetching and improved error messages. Contributed by @nex. ([#1257](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1257))
- Fixed unreliable room summary fetching and improved error messages. Contributed by
@nex. ([#1257](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1257))
- Client requested timeout parameter is now applied to e2ee key lookups and claims. Related federation requests are now
also concurrent. Contributed by @nex. ([#1261](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1261))
- Fixed the whoami endpoint returning HTTP 404 instead of HTTP 403, which confused some appservices. Contributed by
@@ -90,9 +139,12 @@ # Continuwuity 0.5.0 (2025-12-30)
## Features
- Enabled the OTLP exporter in default builds, and allow configuring the exporter protocol. (@Jade). ([#1251](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1251))
- Enabled the OTLP exporter in default builds, and allow configuring the exporter protocol. (
@Jade). ([#1251](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1251))
## Bug Fixes
- Don't allow admin room upgrades, as this can break the admin room (@timedout) ([#1245](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1245))
- Fix invalid creators in power levels during upgrade to v12 (@timedout) ([#1245](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1245))
- Don't allow admin room upgrades, as this can break the admin room (
@timedout) ([#1245](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1245))
- Fix invalid creators in power levels during upgrade to v12 (
@timedout) ([#1245](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1245))

24
Cargo.lock generated
View File

@@ -1013,7 +1013,7 @@ dependencies = [
[[package]]
name = "conduwuit"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"clap",
"conduwuit_admin",
@@ -1045,7 +1045,7 @@ dependencies = [
[[package]]
name = "conduwuit_admin"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"clap",
"conduwuit_api",
@@ -1066,7 +1066,7 @@ dependencies = [
[[package]]
name = "conduwuit_api"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"async-trait",
"axum 0.7.9",
@@ -1098,14 +1098,14 @@ dependencies = [
[[package]]
name = "conduwuit_build_metadata"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"built",
]
[[package]]
name = "conduwuit_core"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"argon2",
"arrayvec",
@@ -1166,7 +1166,7 @@ dependencies = [
[[package]]
name = "conduwuit_database"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"async-channel",
"conduwuit_core",
@@ -1184,7 +1184,7 @@ dependencies = [
[[package]]
name = "conduwuit_macros"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"itertools 0.14.0",
"proc-macro2",
@@ -1194,7 +1194,7 @@ dependencies = [
[[package]]
name = "conduwuit_router"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"axum 0.7.9",
"axum-client-ip",
@@ -1228,8 +1228,9 @@ dependencies = [
[[package]]
name = "conduwuit_service"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"askama 0.14.0",
"async-trait",
"base64 0.22.1",
"blurhash",
@@ -1264,11 +1265,12 @@ dependencies = [
"tracing",
"url",
"webpage",
"yansi",
]
[[package]]
name = "conduwuit_web"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"askama 0.14.0",
"axum 0.7.9",
@@ -6587,7 +6589,7 @@ dependencies = [
[[package]]
name = "xtask"
version = "0.5.4"
version = "0.5.5"
dependencies = [
"askama 0.15.4",
"cargo_metadata",

View File

@@ -12,7 +12,7 @@ license = "Apache-2.0"
# See also `rust-toolchain.toml`
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
version = "0.5.4"
version = "0.5.5"
[workspace.metadata.crane]
name = "conduwuit"
@@ -549,6 +549,12 @@ features = ["sync", "tls-rustls", "rustls-provider"]
[workspace.dependencies.resolv-conf]
version = "0.7.5"
[workspace.dependencies.yansi]
version = "1.0.1"
[workspace.dependencies.askama]
version = "0.14.0"
#
# Patches
#

View File

@@ -1 +0,0 @@
Fixed invites sent to other users in the same homeserver not being properly sent down sync. Users with missing or broken invites should clear their client caches after updating to make them appear.

View File

@@ -1 +0,0 @@
LDAP-enabled servers will no longer have all admins demoted when LDAP-controlled admins are not configured. Contributed by @Jade

View File

@@ -1,2 +0,0 @@
Added unstable support for [MSC4406: `M_SENDER_IGNORED`](https://github.com/matrix-org/matrix-spec-proposals/pull/4406).
Contributed by @nex

View File

@@ -1 +0,0 @@
Continuwuity will now print information to the console when it detects a deadlock

View File

@@ -1 +0,0 @@
Introduce a resolver command to allow flushing a server from the cache or to flush the complete cache. Contributed by @Omar007

View File

@@ -1 +0,0 @@
Improved the handling of restricted join rules and improved the performance of local-first joins. Contributed by @nex.

View File

@@ -1 +0,0 @@
Fixed sliding sync not resolving wildcard state key requests, enabling Video/Audio calls in Element X.

View File

@@ -1 +0,0 @@
You can now set a custom User Agent for URL previews; the default one has been modified to be less likely to be rejected. Contributed by @trashpanda

View File

@@ -433,7 +433,7 @@
# If you would like registration only via token reg, please configure
# `registration_token`.
#
#allow_registration = false
#allow_registration = true
# If registration is enabled, and this setting is true, new users
# registered after the first admin user will be automatically suspended

View File

@@ -5,7 +5,7 @@
use api::client::{full_user_deactivate, join_room_by_id_helper, leave_room, remote_leave_room};
use conduwuit::{
Err, Result, debug, debug_warn, error, info, is_equal_to,
Err, Result, debug_warn, error, info,
matrix::{Event, pdu::PduBuilder},
utils::{self, ReadyExt},
warn,
@@ -167,27 +167,8 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
// we dont add a device since we're not the user, just the creator
// if this account creation is from the CLI / --execute, invite the first user
// to admin room
if let Ok(admin_room) = self.services.admin.get_admin_room().await {
if self
.services
.rooms
.state_cache
.room_joined_count(&admin_room)
.await
.is_ok_and(is_equal_to!(1))
{
self.services
.admin
.make_user_admin(&user_id)
.boxed()
.await?;
warn!("Granting {user_id} admin privileges as the first user");
}
} else {
debug!("create_user admin command called without an admin room being available");
}
// Make the first user to register an administrator and disable first-run mode.
self.services.firstrun.empower_first_user(&user_id).await?;
self.write_str(&format!("Created user with user_id: {user_id} and password: `{password}`"))
.await

View File

@@ -3,7 +3,7 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Error, Event, Result, debug_info, err, error, info, is_equal_to,
Err, Error, Event, Result, debug_info, err, error, info,
matrix::pdu::PduBuilder,
utils::{self, ReadyExt, stream::BroadbandExt},
warn,
@@ -148,7 +148,12 @@ pub(crate) async fn register_route(
let is_guest = body.kind == RegistrationKind::Guest;
let emergency_mode_enabled = services.config.emergency_password.is_some();
if !services.config.allow_registration && body.appservice_info.is_none() {
// Allow registration if it's enabled in the config file or if this is the first
// run (so the first user account can be created)
let allow_registration =
services.config.allow_registration || services.firstrun.is_first_run();
if !allow_registration && body.appservice_info.is_none() {
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
| (Some(username), Some(device_display_name)) => {
info!(
@@ -185,17 +190,10 @@ pub(crate) async fn register_route(
)));
}
if is_guest
&& (!services.config.allow_guest_registration
|| (services.config.allow_registration
&& services
.registration_tokens
.get_config_file_token()
.is_some()))
{
if is_guest && !services.config.allow_guest_registration {
info!(
"Guest registration disabled / registration enabled with token configured, \
rejecting guest registration attempt, initial device name: \"{}\"",
"Guest registration disabled, rejecting guest registration attempt, initial device \
name: \"{}\"",
body.initial_device_display_name.as_deref().unwrap_or("")
);
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
@@ -309,54 +307,63 @@ pub(crate) async fn register_route(
let skip_auth = body.appservice_info.is_some() || is_guest;
// Populate required UIAA flows
if services
.registration_tokens
.iterate_tokens()
.next()
.await
.is_some()
{
// Registration token required
if services.firstrun.is_first_run() {
// Registration token forced while in first-run mode
uiaainfo.flows.push(AuthFlow {
stages: vec![AuthType::RegistrationToken],
});
}
if services.config.recaptcha_private_site_key.is_some() {
if let Some(pubkey) = &services.config.recaptcha_site_key {
// ReCaptcha required
uiaainfo
.flows
.push(AuthFlow { stages: vec![AuthType::ReCaptcha] });
uiaainfo.params = serde_json::value::to_raw_value(&serde_json::json!({
"m.login.recaptcha": {
"public_key": pubkey,
},
}))
.expect("Failed to serialize recaptcha params");
}
}
if uiaainfo.flows.is_empty() && !skip_auth {
// 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
} else {
if services
.registration_tokens
.iterate_tokens()
.next()
.await
.is_some()
{
return Err!(Request(Forbidden(
"This server is not accepting registrations at this time."
)));
// Registration token required
uiaainfo.flows.push(AuthFlow {
stages: vec![AuthType::RegistrationToken],
});
}
// We have open registration enabled (😧), provide a dummy stage
uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
};
if services.config.recaptcha_private_site_key.is_some() {
if let Some(pubkey) = &services.config.recaptcha_site_key {
// ReCaptcha required
uiaainfo
.flows
.push(AuthFlow { stages: vec![AuthType::ReCaptcha] });
uiaainfo.params = serde_json::value::to_raw_value(&serde_json::json!({
"m.login.recaptcha": {
"public_key": pubkey,
},
}))
.expect("Failed to serialize recaptcha params");
}
}
if uiaainfo.flows.is_empty() && !skip_auth {
// 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 {
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
};
}
}
if !skip_auth {
@@ -514,39 +521,29 @@ pub(crate) async fn register_route(
}
}
// If this is the first real user, grant them admin privileges except for guest
// users
// Note: the server user is generated first
if !is_guest {
if let Ok(admin_room) = services.admin.get_admin_room().await {
if services
.rooms
.state_cache
.room_joined_count(&admin_room)
.await
.is_ok_and(is_equal_to!(1))
{
services.admin.make_user_admin(&user_id).boxed().await?;
warn!("Granting {user_id} admin privileges as the first user");
} else if services.config.suspend_on_register {
// This is not an admin, suspend them.
// Note that we can still do auto joins for suspended users
// Make the first user to register an administrator and disable first-run mode.
let was_first_user = services.firstrun.empower_first_user(&user_id).await?;
// If the registering user was not the first and we're suspending users on
// register, suspend them.
if !was_first_user && services.config.suspend_on_register {
// Note that we can still do auto joins for suspended users
services
.users
.suspend_account(&user_id, &services.globals.server_user)
.await;
// And send an @room notice to the admin room, to prompt admins to review the
// new user and ideally unsuspend them if deemed appropriate.
if services.server.config.admin_room_notices {
services
.users
.suspend_account(&user_id, &services.globals.server_user)
.await;
// And send an @room notice to the admin room, to prompt admins to review the
// new user and ideally unsuspend them if deemed appropriate.
if services.server.config.admin_room_notices {
services
.admin
.send_loud_message(RoomMessageEventContent::text_plain(format!(
"User {user_id} has been suspended as they are not the first user \
on this server. Please review and unsuspend them if appropriate."
)))
.await
.ok();
}
.admin
.send_loud_message(RoomMessageEventContent::text_plain(format!(
"User {user_id} has been suspended as they are not the first user on \
this server. Please review and unsuspend them if appropriate."
)))
.await
.ok();
}
}
}

View File

@@ -2,7 +2,7 @@
use axum_client_ip::InsecureClientIp;
use base64::{Engine as _, engine::general_purpose};
use conduwuit::{
Err, Error, PduEvent, Result, err,
Err, Error, PduEvent, Result, err, error,
matrix::{Event, event::gen_event_id},
utils::{self, hash::sha256},
warn,
@@ -199,20 +199,27 @@ pub(crate) async fn create_invite_route(
for appservice in services.appservice.read().await.values() {
if appservice.is_user_match(&recipient_user) {
let request = ruma::api::appservice::event::push_events::v1::Request {
events: vec![pdu.to_format()],
txn_id: general_purpose::URL_SAFE_NO_PAD
.encode(sha256::hash(pdu.event_id.as_bytes()))
.into(),
ephemeral: Vec::new(),
to_device: Vec::new(),
};
services
.sending
.send_appservice_request(
appservice.registration.clone(),
ruma::api::appservice::event::push_events::v1::Request {
events: vec![pdu.to_format()],
txn_id: general_purpose::URL_SAFE_NO_PAD
.encode(sha256::hash(pdu.event_id.as_bytes()))
.into(),
ephemeral: Vec::new(),
to_device: Vec::new(),
},
)
.await?;
.send_appservice_request(appservice.registration.clone(), request)
.await
.map_err(|e| {
error!(
"failed to notify appservice {} about incoming invite: {e}",
appservice.registration.id
);
err!(BadServerResponse(
"Failed to notify appservice about incoming invite."
))
})?;
}
}
}

View File

@@ -559,7 +559,7 @@ pub struct Config {
///
/// If you would like registration only via token reg, please configure
/// `registration_token`.
#[serde(default)]
#[serde(default = "true_fn")]
pub allow_registration: bool,
/// If registration is enabled, and this setting is true, new users

View File

@@ -39,7 +39,15 @@ pub(crate) async fn run(services: Arc<Services>) -> Result<()> {
.runtime()
.spawn(serve::serve(services.clone(), handle.clone(), tx.subscribe()));
// Focal point
// Run startup admin commands.
// This has to be done after the admin service is initialized otherwise it
// panics.
services.admin.startup_execute().await?;
// Print first-run banner if necessary. This needs to be done after the startup
// admin commands are run in case one of them created the first user.
services.firstrun.print_first_run_banner();
debug!("Running");
let res = tokio::select! {
res = &mut listener => res.map_err(Error::from).unwrap_or_else(Err),

View File

@@ -79,6 +79,7 @@ zstd_compression = [
]
[dependencies]
askama.workspace = true
async-trait.workspace = true
base64.workspace = true
bytes.workspace = true
@@ -118,6 +119,7 @@ webpage.optional = true
blurhash.workspace = true
blurhash.optional = true
recaptcha-verify = { version = "0.1.5", default-features = false }
yansi.workspace = true
[target.'cfg(all(unix, target_os = "linux"))'.dependencies]
sd-notify.workspace = true

View File

@@ -26,7 +26,7 @@ pub(super) async fn console_auto_stop(&self) {
/// Execute admin commands after startup
#[implement(super::Service)]
pub(super) async fn startup_execute(&self) -> Result {
pub async fn startup_execute(&self) -> Result {
// List of commands to execute
let commands = &self.services.server.config.admin_execute;

View File

@@ -9,7 +9,6 @@
RoomAccountDataEventType, StateEventType,
room::{
member::{MembershipState, RoomMemberEventContent},
message::RoomMessageEventContent,
power_levels::RoomPowerLevelsEventContent,
},
tag::{TagEvent, TagEventContent, TagInfo},
@@ -126,23 +125,6 @@ pub async fn make_user_admin(&self, user_id: &UserId) -> Result {
}
}
if self.services.server.config.admin_room_notices {
let welcome_message = String::from(
"## Thank you for trying out Continuwuity!\n\nContinuwuity is a hard fork of conduwuit, which is also a hard fork of Conduit, currently in Beta. The Beta status initially was inherited from Conduit, however overtime this Beta status is rapidly becoming less and less relevant as our codebase significantly diverges more and more. Continuwuity is quite stable and very usable as a daily driver and for a low-medium sized homeserver. There is still a lot of more work to be done, but it is in a far better place than the project was in early 2024.\n\nHelpful links:\n> Source code: https://forgejo.ellis.link/continuwuation/continuwuity\n> Documentation: https://continuwuity.org/\n> Report issues: https://forgejo.ellis.link/continuwuation/continuwuity/issues\n\nFor a list of available commands, send the following message in this room: `!admin --help`\n\nHere are some rooms you can join (by typing the command into your client) -\n\nContinuwuity space: `/join #space:continuwuity.org`\nContinuwuity main room (Ask questions and get notified on updates): `/join #continuwuity:continuwuity.org`\nContinuwuity offtopic room: `/join #offtopic:continuwuity.org`",
);
// Send welcome message
self.services
.timeline
.build_and_append_pdu(
PduBuilder::timeline(&RoomMessageEventContent::text_markdown(welcome_message)),
server_user,
Some(&room_id),
&state_lock,
)
.await?;
}
Ok(())
}

View File

@@ -137,7 +137,6 @@ async fn worker(self: Arc<Self>) -> Result<()> {
let mut signals = self.services.server.signal.subscribe();
let receiver = self.channel.1.clone();
self.startup_execute().await?;
self.console_auto_start().await;
loop {

View File

@@ -18,7 +18,7 @@
use std::{sync::Arc, time::Duration};
use async_trait::async_trait;
use conduwuit::{Result, Server, debug, error, info, warn};
use conduwuit::{Result, Server, debug, error, warn};
use database::{Deserialized, Map};
use rand::Rng;
use ruma::events::{Mentions, room::message::RoomMessageEventContent};
@@ -155,11 +155,6 @@ async fn check(&self) -> Result<()> {
#[tracing::instrument(skip_all)]
async fn handle(&self, announcement: &CheckForAnnouncementsResponseEntry) {
if let Some(date) = &announcement.date {
info!("[announcements] {date} {:#}", announcement.message);
} else {
info!("[announcements] {:#}", announcement.message);
}
let mut message = RoomMessageEventContent::text_markdown(format!(
"### New announcement{}\n\n{}",
announcement

View File

@@ -7,12 +7,25 @@
error, implement,
};
use crate::registration_tokens::{ValidToken, ValidTokenSource};
pub struct Service {
server: Arc<Server>,
}
const SIGNAL: &str = "SIGUSR1";
impl Service {
/// Get the registration token set in the config file, if it exists.
#[must_use]
pub fn get_config_file_token(&self) -> Option<ValidToken> {
self.registration_token.clone().map(|token| ValidToken {
token,
source: ValidTokenSource::ConfigFile,
})
}
}
#[async_trait]
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {

302
src/service/firstrun/mod.rs Normal file
View File

@@ -0,0 +1,302 @@
use std::{
io::IsTerminal,
sync::{Arc, OnceLock},
};
use askama::Template;
use async_trait::async_trait;
use conduwuit::{Result, info, utils::ReadyExt};
use futures::StreamExt;
use ruma::{UserId, events::room::message::RoomMessageEventContent};
use crate::{
Dep, admin, config, globals,
registration_tokens::{self, ValidToken, ValidTokenSource},
users,
};
pub struct Service {
services: Services,
/// Represents the state of first run mode.
///
/// First run mode is either active or inactive at server start. It may
/// transition from active to inactive, but only once, and can never
/// transition the other way. Additionally, whether the server is in first
/// run mode or not can only be determined when all services are
/// constructed. The outer `OnceLock` represents the unknown state of first
/// run mode, and the inner `OnceLock` enforces the one-time transition from
/// active to inactive.
///
/// Consequently, this marker may be in one of three states:
/// 1. OnceLock<uninitialized>, representing the unknown state of first run
/// mode during server startup. Once server startup is complete, the
/// marker transitions to state 2 or directly to state 3.
/// 2. OnceLock<OnceLock<uninitialized>>, representing first run mode being
/// active. The marker may only transition to state 3 from here.
/// 3. OnceLock<OnceLock<()>>, representing first run mode being inactive.
/// The marker may not transition out of this state.
first_run_marker: OnceLock<OnceLock<()>>,
/// A single-use registration token which may be used to create the first
/// account.
first_account_token: String,
}
struct Services {
config: Dep<config::Service>,
users: Dep<users::Service>,
globals: Dep<globals::Service>,
admin: Dep<admin::Service>,
}
#[async_trait]
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
services: Services {
config: args.depend::<config::Service>("config"),
users: args.depend::<users::Service>("users"),
globals: args.depend::<globals::Service>("globals"),
admin: args.depend::<admin::Service>("admin"),
},
// marker starts in an indeterminate state
first_run_marker: OnceLock::new(),
first_account_token: registration_tokens::Service::generate_token_string(),
}))
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
async fn worker(self: Arc<Self>) -> Result {
// first run mode will be enabled if there are no local users
let is_first_run = self
.services
.users
.list_local_users()
.ready_filter(|user| *user != self.services.globals.server_user)
.next()
.await
.is_none();
self.first_run_marker
.set(if is_first_run {
// first run mode is active (empty inner lock)
OnceLock::new()
} else {
// first run mode is inactive (already filled inner lock)
OnceLock::from(())
})
.expect("Service worker should only be called once");
Ok(())
}
}
impl Service {
/// Check if first run mode is active.
pub fn is_first_run(&self) -> bool {
self.first_run_marker
.get()
.expect("First run mode should not be checked during server startup")
.get()
.is_none()
}
/// Disable first run mode and begin normal operation.
///
/// Returns true if first run mode was successfully disabled, and false if
/// first run mode was already disabled.
fn disable_first_run(&self) -> bool {
self.first_run_marker
.get()
.expect("First run mode should not be disabled during server startup")
.set(())
.is_ok()
}
/// If first-run mode is active, grant admin powers to the specified user
/// and disable first-run mode.
///
/// Returns Ok(true) if the specified user was the first user, and Ok(false)
/// if they were not.
pub async fn empower_first_user(&self, user: &UserId) -> Result<bool> {
#[derive(Template)]
#[template(path = "welcome.md.j2")]
struct WelcomeMessage<'a> {
config: &'a Dep<config::Service>,
domain: &'a str,
}
// If first run mode isn't active, do nothing.
if !self.disable_first_run() {
return Ok(false);
}
self.services.admin.make_user_admin(user).await?;
// Send the welcome message
let welcome_message = WelcomeMessage {
config: &self.services.config,
domain: self.services.globals.server_name().as_str(),
}
.render()
.expect("should have been able to render welcome message template");
self.services
.admin
.send_loud_message(RoomMessageEventContent::text_markdown(welcome_message))
.await?;
info!("{user} has been invited to the admin room as the first user.");
Ok(true)
}
/// Get the single-use registration token which may be used to create the
/// first account.
pub fn get_first_account_token(&self) -> Option<ValidToken> {
if self.is_first_run() {
Some(ValidToken {
token: self.first_account_token.clone(),
source: ValidTokenSource::FirstAccount,
})
} else {
None
}
}
pub fn print_first_run_banner(&self) {
use yansi::Paint;
// This function is specially called by the core after all other
// services have started. It runs last to ensure that the banner it
// prints comes after any other logging which may occur on startup.
if !self.is_first_run() {
return;
}
eprintln!();
eprintln!("{}", "============".bold());
eprintln!(
"Welcome to {} {}!",
"Continuwuity".bold().bright_magenta(),
conduwuit::version::version().bold()
);
eprintln!();
eprintln!(
"In order to use your new homeserver, you need to create its first user account."
);
eprintln!(
"Open your Matrix client of choice and register an account on {} using the \
registration token {} . Pick your own username and password!",
self.services.globals.server_name().bold().green(),
self.first_account_token.as_str().bold().green()
);
match (
self.services.config.allow_registration,
self.services.config.get_config_file_token().is_some(),
) {
| (true, true) => {
eprintln!(
"{} until you create an account using the token above.",
"The registration token you set in your configuration will not function"
.red()
);
},
| (true, false) => {
eprintln!(
"{} until you create an account using the token above.",
"Nobody else will be able to register".green()
);
},
| (false, true) => {
eprintln!(
"{} because you have disabled registration in your configuration. If this \
is not desired, set `allow_registration` to true and restart Continuwuity.",
"The registration token you set in your configuration will not be usable"
.yellow()
);
},
| (false, false) => {
eprintln!(
"{} to allow you to create an account. Because registration is not enabled \
in your configuration, it will be disabled again once your account is \
created.",
"Registration has been temporarily enabled".yellow()
);
},
}
eprintln!(
"{} https://matrix.org/ecosystem/clients/",
"Find a list of Matrix clients here:".bold()
);
if self.services.config.suspend_on_register {
eprintln!(
"{} Because you enabled suspend-on-register in your configuration, accounts \
created after yours will be automatically suspended.",
"Your account will not be suspended when you register.".green()
);
}
if self
.services
.config
.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
{
eprintln!();
eprintln!(
"{}",
"You have enabled open registration in your configuration! You almost certainly \
do not want to do this."
.bold()
.on_red()
);
eprintln!(
"{}",
"Servers with open, unrestricted registration are prone to abuse by spammers. \
Users on your server may be unable to join chatrooms which block open \
registration servers."
.red()
);
eprintln!(
"If you enabled it only for the purpose of creating the first account, {} and \
create the first account using the token above.",
"disable it now, restart Continuwuity,".red(),
);
// TODO link to a guide on setting up reCAPTCHA
}
if self.services.config.emergency_password.is_some() {
eprintln!();
eprintln!(
"{}",
"You have set an emergency password for the server user! You almost certainly \
do not want to do this."
.red()
);
eprintln!(
"If you set the password only for the purpose of creating the first account, {} \
and create the first account using the token above.",
"disable it now, restart Continuwuity,".red(),
);
}
eprintln!();
if std::io::stdin().is_terminal() && self.services.config.admin_console_automatic {
eprintln!(
"You may also create the first user through the admin console below using the \
`users create-user` command."
);
} else {
eprintln!(
"If you're running the server interactively, you may also create the first user \
through the admin console using the `users create-user` command. Press Ctrl-C \
to open the console."
);
}
eprintln!("If you need assistance setting up your homeserver, make a Matrix account on another homeserver and join our chatroom: https://matrix.to/#/#continuwuity:continuwuity.org");
eprintln!("{}", "============".bold());
}
}

View File

@@ -18,6 +18,7 @@
pub mod config;
pub mod emergency;
pub mod federation;
pub mod firstrun;
pub mod globals;
pub mod key_backups;
pub mod media;

View File

@@ -1,14 +1,17 @@
mod data;
use std::sync::Arc;
use std::{future::ready, pin::Pin, sync::Arc};
use conduwuit::{Err, Result, utils};
use data::Data;
pub use data::{DatabaseTokenInfo, TokenExpires};
use futures::{Stream, StreamExt, stream};
use futures::{
Stream, StreamExt,
stream::{iter, once},
};
use ruma::OwnedUserId;
use crate::{Dep, config};
use crate::{Dep, config, firstrun};
const RANDOM_TOKEN_LENGTH: usize = 16;
@@ -19,6 +22,7 @@ pub struct Service {
struct Services {
config: Dep<config::Service>,
firstrun: Dep<firstrun::Service>,
}
/// A validated registration token which may be used to create an account.
@@ -46,6 +50,9 @@ pub enum ValidTokenSource {
ConfigFile,
/// A database token which has been checked to be valid.
Database(DatabaseTokenInfo),
/// The single-use token which may be used to create the homeserver's first
/// account.
FirstAccount,
}
impl std::fmt::Display for ValidTokenSource {
@@ -53,6 +60,7 @@ 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),
| Self::FirstAccount => write!(f, "Initial setup token."),
}
}
}
@@ -63,6 +71,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
db: Data::new(args.db),
services: Services {
config: args.depend::<config::Service>("config"),
firstrun: args.depend::<firstrun::Service>("firstrun"),
},
}))
}
@@ -71,45 +80,51 @@ fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
/// Generate a random string suitable to be used as a registration token.
#[must_use]
pub fn generate_token_string() -> String { utils::random_string(RANDOM_TOKEN_LENGTH) }
/// 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 token = Self::generate_token_string();
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,
})
/// Get all the "special" registration tokens that aren't defined in the
/// database.
fn iterate_static_tokens(&self) -> impl Iterator<Item = ValidToken> {
// This does not include the first-account token, because it's special:
// no other registration tokens are valid when it is set.
self.services.config.get_config_file_token().into_iter()
}
/// 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,
});
// Check for the first-account token first
if let Some(first_account_token) = self.services.firstrun.get_first_account_token() {
if first_account_token == *token {
return Some(first_account_token);
}
// If the first-account token is set, no other tokens are valid
return None;
}
// Now check the database
// Then static registration tokens
for static_token in self.iterate_static_tokens() {
if static_token == *token {
return Some(static_token);
}
}
// Then check the database
if let Some(token_info) = self.db.lookup_token_info(&token).await
&& token_info.is_valid()
{
@@ -126,14 +141,14 @@ pub async fn validate_token(&self, token: String) -> Option<ValidToken> {
/// 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);
},
| _ => {
// Do nothing for other token sources.
},
}
}
@@ -144,7 +159,6 @@ pub fn mark_token_as_used(&self, ValidToken { token, source }: ValidToken) {
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."
@@ -154,11 +168,19 @@ pub fn revoke_token(&self, ValidToken { token, source }: ValidToken) -> Result {
self.db.revoke_token(&token);
Ok(())
},
| ValidTokenSource::FirstAccount => {
Err!("The initial setup token cannot be revoked.")
},
}
}
/// Iterate over all valid registration tokens.
pub fn iterate_tokens(&self) -> impl Stream<Item = ValidToken> + Send + '_ {
pub fn iterate_tokens(&self) -> Pin<Box<dyn Stream<Item = ValidToken> + Send + '_>> {
// If the first-account token is set, no other tokens are valid
if let Some(first_account_token) = self.services.firstrun.get_first_account_token() {
return once(ready(first_account_token)).boxed();
}
let db_tokens = self
.db
.iterate_and_clean_tokens()
@@ -167,6 +189,6 @@ pub fn iterate_tokens(&self) -> impl Stream<Item = ValidToken> + Send + '_ {
source: ValidTokenSource::Database(info),
});
stream::iter(self.get_config_file_token()).chain(db_tokens)
iter(self.iterate_static_tokens()).chain(db_tokens).boxed()
}
}

View File

@@ -58,7 +58,11 @@ pub async fn ask_policy_server(
.state_accessor
.room_state_get_content(room_id, &StateEventType::RoomPolicy, "")
.await
.inspect_err(|e| debug_error!("failed to load room policy server state event: {e}"))
.inspect_err(|e| {
if !e.is_not_found() {
debug_error!("failed to load room policy server state event: {e}");
}
})
.map(|c: RoomPolicyEventContent| c)
else {
debug!("room has no policy server configured");

View File

@@ -9,7 +9,7 @@
use crate::{
account_data, admin, announcements, antispam, appservice, client, config, emergency,
federation, globals, key_backups,
federation, firstrun, globals, key_backups,
manager::Manager,
media, moderation, presence, pusher, registration_tokens, resolver, rooms, sending,
server_keys,
@@ -33,6 +33,7 @@ pub struct Services {
pub resolver: Arc<resolver::Service>,
pub rooms: rooms::Service,
pub federation: Arc<federation::Service>,
pub firstrun: Arc<firstrun::Service>,
pub sending: Arc<sending::Service>,
pub server_keys: Arc<server_keys::Service>,
pub sync: Arc<sync::Service>,
@@ -67,6 +68,9 @@ macro_rules! build {
}
Ok(Arc::new(Self {
// firstrun service should be built first so other services
// can check first-run state
firstrun: build!(firstrun::Service),
account_data: build!(account_data::Service),
admin: build!(admin::Service),
appservice: build!(appservice::Service),
@@ -144,6 +148,7 @@ pub async fn start(self: &Arc<Self>) -> Result<Arc<Self>> {
}
debug_info!("Services startup complete.");
Ok(Arc::clone(self))
}

View File

@@ -0,0 +1,29 @@
## Thank you for trying out Continuwuity!
Your new homeserver is ready to use! {%- if config.allow_federation %} To make sure you can federate with the rest of the Matrix network, consider checking your domain (`{{ domain }}`) with a federation tester like [this one](https://connectivity-tester.mtrnord.blog/). {%- endif %}
{% if config.get_config_file_token().is_some() -%}
Users may now create accounts normally using the configured registration token.
{%- else if config.recaptcha_site_key.is_some() -%}
Users may now create accounts normally after solving a CAPTCHA.
{%- else if config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse -%}
**This server has open, unrestricted registration enabled!** Anyone, including spammers, may now create an account with no further steps. If this is not desired behavior, set `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse` to `false` in your configuration and restart the server.
{%- else if config.allow_registration -%}
To allow more users to register, use the `!admin token` admin commands to issue registration tokens, or set a registration token in the configuration.
{%- else -%}
You've disabled registration. To create more accounts, use the `!admin users create-user` admin command.
{%- endif %}
This room is your server's admin room. You can send messages starting with `!admin` in this room to perform a range of administrative actions.
To view a list of available commands, send the following message: `!admin --help`
Project chatrooms:
> Support chatroom: https://matrix.to/#/#continuwuity:continuwuity.org
> Update announcements: https://matrix.to/#/#announcements:continuwuity.org
> Other chatrooms: https://matrix.to/#/#space:continuwuity.org
>
Helpful links:
> Source code: https://forgejo.ellis.link/continuwuation/continuwuity
> Documentation: https://continuwuity.org/
> Report issues: https://forgejo.ellis.link/continuwuation/continuwuity/issues

View File

@@ -187,7 +187,9 @@ pub async fn create(
self.db
.userid_origin
.insert(user_id, origin.unwrap_or("password"));
self.set_password(user_id, password).await
self.set_password(user_id, password).await?;
Ok(())
}
/// Deactivate account

View File

@@ -20,9 +20,7 @@ crate-type = [
[dependencies]
conduwuit-build-metadata.workspace = true
conduwuit-service.workspace = true
askama = "0.14.0"
askama.workspace = true
axum.workspace = true
futures.workspace = true
tracing.workspace = true

View File

@@ -83,3 +83,12 @@ footer {
color: transparent;
filter: brightness(1.2);
}
b {
color: oklch(from var(--c2) var(--name-lightness) c h);
}
.logo {
width: 100%;
height: 64px;
}

View File

@@ -10,8 +10,9 @@
use conduwuit_service::state;
pub fn build() -> Router<state::State> {
let router = Router::<state::State>::new();
router.route("/", get(index_handler))
Router::<state::State>::new()
.route("/", get(index_handler))
.route("/_continuwuity/logo.svg", get(logo_handler))
}
async fn index_handler(
@@ -19,22 +20,34 @@ async fn index_handler(
) -> Result<impl IntoResponse, WebError> {
#[derive(Debug, Template)]
#[template(path = "index.html.j2")]
struct Tmpl<'a> {
struct Index<'a> {
nonce: &'a str,
server_name: &'a str,
first_run: bool,
}
let nonce = rand::random::<u64>().to_string();
let template = Tmpl {
let template = Index {
nonce: &nonce,
server_name: services.config.server_name.as_str(),
first_run: services.firstrun.is_first_run(),
};
Ok((
[(header::CONTENT_SECURITY_POLICY, format!("default-src 'none' 'nonce-{nonce}';"))],
[(
header::CONTENT_SECURITY_POLICY,
format!("default-src 'nonce-{nonce}'; img-src 'self';"),
)],
Html(template.render()?),
))
}
async fn logo_handler() -> impl IntoResponse {
(
[(header::CONTENT_TYPE, "image/svg+xml")],
include_str!("templates/logo.svg").to_owned(),
)
}
#[derive(Debug, thiserror::Error)]
enum WebError {
#[error("Failed to render template: {0}")]
@@ -45,7 +58,7 @@ impl IntoResponse for WebError {
fn into_response(self) -> Response {
#[derive(Debug, Template)]
#[template(path = "error.html.j2")]
struct Tmpl<'a> {
struct Error<'a> {
nonce: &'a str,
err: WebError,
}
@@ -55,7 +68,7 @@ struct Tmpl<'a> {
let status = match &self {
| Self::Render(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
let tmpl = Tmpl { nonce: &nonce, err: self };
let tmpl = Error { nonce: &nonce, err: self };
if let Ok(body) = tmpl.render() {
(
status,

View File

@@ -6,6 +6,7 @@
<title>{% block title %}Continuwuity{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/_continuwuity/logo.svg">
<style type="text/css" nonce="{{ nonce }}">
/*<![CDATA[*/
{{ include_str !("css/index.css") | safe }}
@@ -17,7 +18,8 @@
<main>{%~ block content %}{% endblock ~%}</main>
{%~ block footer ~%}
<footer>
<p>Powered by <a href="https://continuwuity.org">Continuwuity</a>
<img class="logo" src="/_continuwuity/logo.svg">
<p>Powered by <a href="https://continuwuity.org">Continuwuity</a> {{ env!("CARGO_PKG_VERSION") }}
{%~ if let Some(version_info) = self::version_tag() ~%}
{%~ if let Some(url) = GIT_REMOTE_COMMIT_URL.or(GIT_REMOTE_WEB_URL) ~%}
(<a href="{{ url }}">{{ version_info }}</a>)

View File

@@ -1,16 +1,16 @@
{% extends "_layout.html.j2" %}
{%- block content -%}
<div class="orb"></div>
<div class="panel">
<h1>Welcome to <a class="project-name" href="https://continuwuity.org">Continuwuity</a>!</h1>
<p>Continuwuity is successfully installed and working. </p>
<p>To get started, you can:</p>
<ul>
<li>Read the <a href="https://continuwuity.org/introduction">documentation</a></li>
<li>Join the <a href="https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org">Continuwuity Matrix room</a> or <a href="https://matrix.to/#/#space:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org">space</a></li>
<li>Log in with a <a href="https://matrix.org/ecosystem/clients/">client</a></li>
<li>Ensure <a href="https://federationtester.mtrnord.blog/?serverName={{ server_name }}">federation</a> works</li>
</ul>
<h1>
Welcome to <a class="project-name" href="https://continuwuity.org">Continuwuity</a>!
</h1>
<p>Continuwuity is successfully installed and working.</p>
{%- if first_run %}
<p>To get started, <b>check the server logs</b> for instructions on how to create the first account.</p>
<p>For support, take a look at the <a href="https://continuwuity.org/introduction">documentation</a> or join the <a href="https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org">Continuwuity Matrix room</a>.</p>
{%- else %}
<p>To get started, <a href="https://matrix.org/ecosystem/clients">choose a client</a> and connect to <code>{{ server_name }}</code>.</p>
{%- endif %}
</div>
{%- endblock content -%}

1
src/web/templates/logo.svg Symbolic link
View File

@@ -0,0 +1 @@
../../../docs/public/assets/logo.svg