Merge branch 'deployment' into feat/space-permission-cascading
Some checks failed
Documentation / Build and Deploy Documentation (pull_request) Has been skipped
Checks / Prek / Pre-commit & Formatting (pull_request) Failing after 5s
Checks / Prek / Clippy and Cargo Tests (pull_request) Failing after 5s

This commit is contained in:
ember 2026-03-19 21:38:38 +00:00
commit 4a439801b5
49 changed files with 1527 additions and 338 deletions

View file

@ -62,10 +62,6 @@ sync:
target: registry.gitlab.com/continuwuity/continuwuity target: registry.gitlab.com/continuwuity/continuwuity
type: repository type: repository
<<: *tags-main <<: *tags-main
- source: *source
target: git.nexy7574.co.uk/mirrored/continuwuity
type: repository
<<: *tags-releases
- source: *source - source: *source
target: ghcr.io/continuwuity/continuwuity target: ghcr.io/continuwuity/continuwuity
type: repository type: repository

4
.github/FUNDING.yml vendored
View file

@ -1,4 +1,4 @@
github: [JadedBlueEyes, nexy7574, gingershaped] github: [JadedBlueEyes, nexy7574, gingershaped]
custom: custom:
- https://ko-fi.com/nexy7574 - https://timedout.uk/donate.html
- https://ko-fi.com/JadedBlueEyes - https://jade.ellis.link/sponsors

371
Cargo.lock generated
View file

@ -94,9 +94,9 @@ dependencies = [
[[package]] [[package]]
name = "anstyle" name = "anstyle"
version = "1.0.13" version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]] [[package]]
name = "anyhow" name = "anyhow"
@ -221,7 +221,7 @@ dependencies = [
"serde", "serde",
"serde_derive", "serde_derive",
"unicode-ident", "unicode-ident",
"winnow", "winnow 0.7.15",
] ]
[[package]] [[package]]
@ -461,6 +461,7 @@ dependencies = [
"axum", "axum",
"axum-core", "axum-core",
"bytes", "bytes",
"cookie",
"futures-core", "futures-core",
"futures-util", "futures-util",
"headers", "headers",
@ -750,9 +751,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.56" version = "1.2.57"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
@ -823,9 +824,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.60" version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -833,9 +834,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.60" version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"clap_lex", "clap_lex",
@ -843,9 +844,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.55" version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@ -855,9 +856,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "1.0.0" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]] [[package]]
name = "cmake" name = "cmake"
@ -905,7 +906,7 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit" name = "conduwuit"
version = "0.5.6" version = "0.5.7-alpha.1"
dependencies = [ dependencies = [
"clap", "clap",
"conduwuit_admin", "conduwuit_admin",
@ -937,7 +938,7 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit_admin" name = "conduwuit_admin"
version = "0.5.6" version = "0.5.7-alpha.1"
dependencies = [ dependencies = [
"clap", "clap",
"conduwuit_api", "conduwuit_api",
@ -958,7 +959,7 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit_api" name = "conduwuit_api"
version = "0.5.6" version = "0.5.7-alpha.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@ -990,14 +991,14 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit_build_metadata" name = "conduwuit_build_metadata"
version = "0.5.6" version = "0.5.7-alpha.1"
dependencies = [ dependencies = [
"built", "built",
] ]
[[package]] [[package]]
name = "conduwuit_core" name = "conduwuit_core"
version = "0.5.6" version = "0.5.7-alpha.1"
dependencies = [ dependencies = [
"argon2", "argon2",
"arrayvec", "arrayvec",
@ -1059,7 +1060,7 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit_database" name = "conduwuit_database"
version = "0.5.6" version = "0.5.7-alpha.1"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"conduwuit_core", "conduwuit_core",
@ -1077,7 +1078,7 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit_macros" name = "conduwuit_macros"
version = "0.5.6" version = "0.5.7-alpha.1"
dependencies = [ dependencies = [
"itertools 0.14.0", "itertools 0.14.0",
"proc-macro2", "proc-macro2",
@ -1087,7 +1088,7 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit_router" name = "conduwuit_router"
version = "0.5.6" version = "0.5.7-alpha.1"
dependencies = [ dependencies = [
"axum", "axum",
"axum-client-ip", "axum-client-ip",
@ -1121,7 +1122,7 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit_service" name = "conduwuit_service"
version = "0.5.6" version = "0.5.7-alpha.1"
dependencies = [ dependencies = [
"askama", "askama",
"async-trait", "async-trait",
@ -1163,16 +1164,26 @@ dependencies = [
[[package]] [[package]]
name = "conduwuit_web" name = "conduwuit_web"
version = "0.5.6" version = "0.5.7-alpha.1"
dependencies = [ dependencies = [
"askama", "askama",
"async-trait",
"axum", "axum",
"axum-extra",
"base64 0.22.1",
"conduwuit_build_metadata", "conduwuit_build_metadata",
"conduwuit_core",
"conduwuit_service", "conduwuit_service",
"futures", "futures",
"memory-serve",
"rand 0.10.0", "rand 0.10.0",
"ruma",
"serde",
"thiserror 2.0.18", "thiserror 2.0.18",
"tower-http",
"tower-sec-fetch",
"tracing", "tracing",
"validator",
] ]
[[package]] [[package]]
@ -1255,6 +1266,17 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]] [[package]]
name = "coolor" name = "coolor"
version = "1.1.0" version = "1.1.0"
@ -1507,6 +1529,41 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "817fa642fb0ee7fe42e95783e00e0969927b96091bdd4b9b1af082acd943913b" checksum = "817fa642fb0ee7fe42e95783e00e0969927b96091bdd4b9b1af082acd943913b"
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]] [[package]]
name = "data-encoding" name = "data-encoding"
version = "2.10.0" version = "2.10.0"
@ -2538,6 +2595,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "1.1.0" version = "1.1.0"
@ -2561,9 +2624,9 @@ dependencies = [
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.9" version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"byteorder-lite", "byteorder-lite",
@ -2579,8 +2642,8 @@ dependencies = [
"rayon", "rayon",
"rgb", "rgb",
"tiff", "tiff",
"zune-core 0.5.1", "zune-core",
"zune-jpeg 0.5.12", "zune-jpeg",
] ]
[[package]] [[package]]
@ -2866,9 +2929,9 @@ dependencies = [
[[package]] [[package]]
name = "libz-sys" name = "libz-sys"
version = "1.1.24" version = "1.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839" checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1"
dependencies = [ dependencies = [
"cc", "cc",
"pkg-config", "pkg-config",
@ -3027,6 +3090,22 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "memory-serve"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81b5bbad2035f57b1e95f66da606832edd935b47d82312e38e1ccffbcfb8a427"
dependencies = [
"axum",
"brotli",
"flate2",
"mime_guess",
"sha256",
"tracing",
"urlencoding",
"walkdir",
]
[[package]] [[package]]
name = "meowlnir-antispam" name = "meowlnir-antispam"
version = "0.1.0" version = "0.1.0"
@ -3043,6 +3122,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]] [[package]]
name = "minicbor" name = "minicbor"
version = "2.2.1" version = "2.2.1"
@ -3115,9 +3204,9 @@ dependencies = [
[[package]] [[package]]
name = "moxcms" name = "moxcms"
version = "0.7.11" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [ dependencies = [
"num-traits", "num-traits",
"pxfm", "pxfm",
@ -3478,9 +3567,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
dependencies = [ dependencies = [
"critical-section", "critical-section",
"portable-atomic", "portable-atomic",
@ -3828,7 +3917,29 @@ version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [ dependencies = [
"toml_edit 0.25.4+spec-1.1.0", "toml_edit 0.25.5+spec-1.1.0",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn",
] ]
[[package]] [[package]]
@ -3966,9 +4077,9 @@ dependencies = [
[[package]] [[package]]
name = "quinn-proto" name = "quinn-proto"
version = "0.11.13" version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
dependencies = [ dependencies = [
"bytes", "bytes",
"getrandom 0.3.4", "getrandom 0.3.4",
@ -4121,9 +4232,9 @@ dependencies = [
[[package]] [[package]]
name = "ravif" name = "ravif"
version = "0.12.0" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
dependencies = [ dependencies = [
"avif-serialize", "avif-serialize",
"imgref", "imgref",
@ -4156,9 +4267,9 @@ dependencies = [
[[package]] [[package]]
name = "recaptcha-verify" name = "recaptcha-verify"
version = "0.1.6" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d694033c2b0abdbb8893edfb367f16270e790be4a67e618206d811dbe4efee4" checksum = "409bf11a93fe93093f3c0254aab67576524f1e0524692615b5b63091dbc88a79"
dependencies = [ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
@ -4632,6 +4743,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "sanitize-filename" name = "sanitize-filename"
version = "0.6.0" version = "0.6.0"
@ -4654,9 +4774,9 @@ dependencies = [
[[package]] [[package]]
name = "schannel" name = "schannel"
version = "0.1.28" version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@ -4993,6 +5113,19 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "sha256"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6"
dependencies = [
"async-trait",
"bytes",
"hex",
"sha2",
"tokio",
]
[[package]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.7" version = "0.1.7"
@ -5161,6 +5294,12 @@ dependencies = [
"quote", "quote",
] ]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "subslice" name = "subslice"
version = "0.2.3" version = "0.2.3"
@ -5301,16 +5440,16 @@ dependencies = [
[[package]] [[package]]
name = "tiff" name = "tiff"
version = "0.10.3" version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
dependencies = [ dependencies = [
"fax", "fax",
"flate2", "flate2",
"half", "half",
"quick-error", "quick-error",
"weezl", "weezl",
"zune-jpeg 0.4.21", "zune-jpeg",
] ]
[[package]] [[package]]
@ -5384,9 +5523,9 @@ dependencies = [
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.10.0" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
dependencies = [ dependencies = [
"tinyvec_macros", "tinyvec_macros",
] ]
@ -5495,7 +5634,7 @@ dependencies = [
"toml_datetime 0.7.5+spec-1.1.0", "toml_datetime 0.7.5+spec-1.1.0",
"toml_parser", "toml_parser",
"toml_writer", "toml_writer",
"winnow", "winnow 0.7.15",
] ]
[[package]] [[package]]
@ -5518,9 +5657,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "1.0.0+spec-1.1.0" version = "1.0.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]
@ -5536,28 +5675,28 @@ dependencies = [
"serde_spanned 0.6.9", "serde_spanned 0.6.9",
"toml_datetime 0.6.11", "toml_datetime 0.6.11",
"toml_write", "toml_write",
"winnow", "winnow 0.7.15",
] ]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.25.4+spec-1.1.0" version = "0.25.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"toml_datetime 1.0.0+spec-1.1.0", "toml_datetime 1.0.1+spec-1.1.0",
"toml_parser", "toml_parser",
"winnow", "winnow 1.0.0",
] ]
[[package]] [[package]]
name = "toml_parser" name = "toml_parser"
version = "1.0.9+spec-1.1.0" version = "1.0.10+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
dependencies = [ dependencies = [
"winnow", "winnow 1.0.0",
] ]
[[package]] [[package]]
@ -5568,9 +5707,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]] [[package]]
name = "toml_writer" name = "toml_writer"
version = "1.0.6+spec-1.1.0" version = "1.0.7+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d"
[[package]] [[package]]
name = "tonic" name = "tonic"
@ -5661,6 +5800,18 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-sec-fetch"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff1e78d241de2527d3ef67e49d65d8cb08468c644c3aafac7a988c4accd76547"
dependencies = [
"futures",
"http",
"tower",
"tracing",
]
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.3" version = "0.3.3"
@ -5673,6 +5824,7 @@ version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [ dependencies = [
"log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
"tracing-core", "tracing-core",
@ -5750,9 +5902,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-subscriber" name = "tracing-subscriber"
version = "0.3.22" version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [ dependencies = [
"matchers", "matchers",
"nu-ansi-term", "nu-ansi-term",
@ -5894,6 +6046,12 @@ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]] [[package]]
name = "utf-8" name = "utf-8"
version = "0.7.6" version = "0.7.6"
@ -5929,6 +6087,36 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "validator"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa"
dependencies = [
"idna",
"once_cell",
"regex",
"serde",
"serde_derive",
"serde_json",
"url",
"validator_derive",
]
[[package]]
name = "validator_derive"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca"
dependencies = [
"darling",
"once_cell",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.1" version = "0.1.1"
@ -5947,6 +6135,16 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@ -6161,6 +6359,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
@ -6404,6 +6611,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winnow"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.50.0" version = "0.50.0"
@ -6538,7 +6754,7 @@ dependencies = [
[[package]] [[package]]
name = "xtask" name = "xtask"
version = "0.5.6" version = "0.5.7-alpha.1"
dependencies = [ dependencies = [
"askama", "askama",
"cargo_metadata", "cargo_metadata",
@ -6584,18 +6800,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.41" version = "0.8.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96e13bc581734df6250836c59a5f44f3c57db9f9acb9dc8e3eaabdaf6170254d" checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.41" version = "0.8.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3545ea9e86d12ab9bba9fcd99b54c1556fd3199007def5a03c375623d05fac1c" checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -6696,12 +6912,6 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]] [[package]]
name = "zune-core" name = "zune-core"
version = "0.5.1" version = "0.5.1"
@ -6719,18 +6929,9 @@ dependencies = [
[[package]] [[package]]
name = "zune-jpeg" name = "zune-jpeg"
version = "0.4.21" version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c"
dependencies = [ dependencies = [
"zune-core 0.4.12", "zune-core",
]
[[package]]
name = "zune-jpeg"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe"
dependencies = [
"zune-core 0.5.1",
] ]

View file

@ -12,7 +12,7 @@ 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"
version = "0.5.6" version = "0.5.7-alpha.1"
[workspace.metadata.crane] [workspace.metadata.crane]
name = "conduwuit" name = "conduwuit"
@ -99,7 +99,7 @@ features = [
[workspace.dependencies.axum-extra] [workspace.dependencies.axum-extra]
version = "0.12.0" version = "0.12.0"
default-features = false default-features = false
features = ["typed-header", "tracing"] features = ["typed-header", "tracing", "cookie"]
[workspace.dependencies.axum-server] [workspace.dependencies.axum-server]
version = "0.7.2" version = "0.7.2"
@ -969,3 +969,6 @@ needless_raw_string_hashes = "allow"
# TODO: Enable this lint & fix all instances # TODO: Enable this lint & fix all instances
collapsible_if = "allow" collapsible_if = "allow"
# TODO: break these apart
cognitive_complexity = "allow"

View file

@ -0,0 +1 @@
Added support for using an admin command to issue self-service password reset links.

View file

@ -0,0 +1 @@
Add new config option to allow or disallow search engine indexing through a `<meta ../>` tag. Defaults to blocking indexing (`content="noindex"`). Contributed by @s1lv3r and @ginger.

View file

@ -25,6 +25,10 @@
# #
# Also see the `[global.well_known]` config section at the very bottom. # Also see the `[global.well_known]` config section at the very bottom.
# #
# If `client` is not set under `[global.well_known]`, the server name will
# be used as the base domain for user-facing links (such as password
# reset links) created by Continuwuity.
#
# Examples of delegation: # Examples of delegation:
# - https://continuwuity.org/.well-known/matrix/server # - https://continuwuity.org/.well-known/matrix/server
# - https://continuwuity.org/.well-known/matrix/client # - https://continuwuity.org/.well-known/matrix/client
@ -1807,6 +1811,11 @@
# #
#config_reload_signal = true #config_reload_signal = true
# Allow search engines and crawlers to index Continuwuity's built-in
# webpages served under the `/_continuwuity/` prefix.
#
#allow_web_indexing = false
[global.tls] [global.tls]
# Path to a valid TLS certificate file. # Path to a valid TLS certificate file.

36
flake.lock generated
View file

@ -3,11 +3,11 @@
"advisory-db": { "advisory-db": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1772776993, "lastModified": 1773786698,
"narHash": "sha256-CpBa+UpogN0Xn1gMmgqQrzKGee+E8TCkgHar8/w6CRk=", "narHash": "sha256-o/J7ZculgwSs1L4H4UFlFZENOXTJzq1X0n71x6oNNvY=",
"owner": "rustsec", "owner": "rustsec",
"repo": "advisory-db", "repo": "advisory-db",
"rev": "b3472341e37cbd4b8c27b052b2abb34792f4d3c4", "rev": "99e9de91bb8b61f06ef234ff84e11f758ecd5384",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -18,11 +18,11 @@
}, },
"crane": { "crane": {
"locked": { "locked": {
"lastModified": 1772560058, "lastModified": 1773189535,
"narHash": "sha256-NuVKdMBJldwUXgghYpzIWJdfeB7ccsu1CC7B+NfSoZ8=", "narHash": "sha256-E1G/Or6MWeP+L6mpQ0iTFLpzSzlpGrITfU2220Gq47g=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "db590d9286ed5ce22017541e36132eab4e8b3045", "rev": "6fa2fb4cf4a89ba49fc9dd5a3eb6cde99d388269",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -39,11 +39,11 @@
"rust-analyzer-src": "rust-analyzer-src" "rust-analyzer-src": "rust-analyzer-src"
}, },
"locked": { "locked": {
"lastModified": 1772953398, "lastModified": 1773732206,
"narHash": "sha256-fTTHCaEvPLzWyZFxPud/G9HM3pNYmW/64Kj58hdH4+k=", "narHash": "sha256-HKibxaUXyWd4Hs+ZUnwo6XslvaFqFqJh66uL9tphU4Q=",
"owner": "nix-community", "owner": "nix-community",
"repo": "fenix", "repo": "fenix",
"rev": "fc4863887d98fd879cf5f11af1d23d44d9bdd8ae", "rev": "0aa13c1b54063a8d8679b28a5cd357ba98f4a56b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -89,11 +89,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1772773019, "lastModified": 1773734432,
"narHash": "sha256-E1bxHxNKfDoQUuvriG71+f+s/NT0qWkImXsYZNFFfCs=", "narHash": "sha256-IF5ppUWh6gHGHYDbtVUyhwy/i7D261P7fWD1bPefOsw=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "aca4d95fce4914b3892661bcb80b8087293536c6", "rev": "cda48547b432e8d3b18b4180ba07473762ec8558",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -132,11 +132,11 @@
"rust-analyzer-src": { "rust-analyzer-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1772877513, "lastModified": 1773697963,
"narHash": "sha256-RcRGv2Bng5I9y75XwFX7oK2l6mLH1dtbTTG9U8qun0c=", "narHash": "sha256-xdKI77It9PM6eNrCcDZsnP4SKulZwk8VkDgBRVMnCb8=",
"owner": "rust-lang", "owner": "rust-lang",
"repo": "rust-analyzer", "repo": "rust-analyzer",
"rev": "a1b86d600f88be98643e5dd61d6ed26eda17c09e", "rev": "2993637174252ff60a582fd1f55b9ab52c39db6d",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -153,11 +153,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1772660329, "lastModified": 1773297127,
"narHash": "sha256-IjU1FxYqm+VDe5qIOxoW+pISBlGvVApRjiw/Y/ttJzY=", "narHash": "sha256-6E/yhXP7Oy/NbXtf1ktzmU8SdVqJQ09HC/48ebEGBpk=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "3710e0e1218041bbad640352a0440114b1e10428", "rev": "71b125cd05fbfd78cab3e070b73544abe24c5016",
"type": "github" "type": "github"
}, },
"original": { "original": {

61
package-lock.json generated
View file

@ -16,21 +16,21 @@
} }
}, },
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.8.1", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
"integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@emnapi/wasi-threads": "1.1.0", "@emnapi/wasi-threads": "1.2.0",
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
"version": "1.8.1", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@ -39,9 +39,9 @@
} }
}, },
"node_modules/@emnapi/wasi-threads": { "node_modules/@emnapi/wasi-threads": {
"version": "1.1.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@ -144,17 +144,22 @@
} }
}, },
"node_modules/@rsbuild/plugin-react": { "node_modules/@rsbuild/plugin-react": {
"version": "1.4.5", "version": "1.4.6",
"resolved": "https://registry.npmjs.org/@rsbuild/plugin-react/-/plugin-react-1.4.5.tgz", "resolved": "https://registry.npmjs.org/@rsbuild/plugin-react/-/plugin-react-1.4.6.tgz",
"integrity": "sha512-eS2sXCedgGA/7bLu8yVtn48eE/GyPbXx4Q7OcutB01IQ1D2y8WSMBys4nwfrecy19utvw4NPn4gYDy52316+vg==", "integrity": "sha512-LAT6xHlEyZKA0VjF/ph5d50iyG+WSmBx+7g98HNZUwb94VeeTMZFB8qVptTkbIRMss3BNKOXmHOu71Lhsh9oEw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@rspack/plugin-react-refresh": "^1.6.0", "@rspack/plugin-react-refresh": "^1.6.1",
"react-refresh": "^0.18.0" "react-refresh": "^0.18.0"
}, },
"peerDependencies": { "peerDependencies": {
"@rsbuild/core": "^1.0.0 || ^2.0.0-0" "@rsbuild/core": "^1.0.0 || ^2.0.0-0"
},
"peerDependenciesMeta": {
"@rsbuild/core": {
"optional": true
}
} }
}, },
"node_modules/@rspack/binding": { "node_modules/@rspack/binding": {
@ -695,13 +700,13 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/@unhead/react": { "node_modules/@unhead/react": {
"version": "2.1.10", "version": "2.1.12",
"resolved": "https://registry.npmjs.org/@unhead/react/-/react-2.1.10.tgz", "resolved": "https://registry.npmjs.org/@unhead/react/-/react-2.1.12.tgz",
"integrity": "sha512-z9IzzkaCI1GyiBwVRMt4dGc2mOvsj9drbAdXGMy6DWpu9FwTR37ZTmAi7UeCVyIkpVdIaNalz7vkbvGG8afFng==", "integrity": "sha512-1xXFrxyw29f+kScXfEb0GxjlgtnHxoYau0qpW9k8sgWhQUNnE5gNaH3u+rNhd5IqhyvbdDRJpQ25zoz0HIyGaw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"unhead": "2.1.10" "unhead": "2.1.12"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/harlan-zw" "url": "https://github.com/sponsors/harlan-zw"
@ -1573,9 +1578,9 @@
} }
}, },
"node_modules/hookable": { "node_modules/hookable": {
"version": "6.0.1", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-6.0.1.tgz", "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.0.tgz",
"integrity": "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==", "integrity": "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -2978,14 +2983,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/oniguruma-to-es": { "node_modules/oniguruma-to-es": {
"version": "4.3.4", "version": "4.3.5",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz",
"integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", "integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"oniguruma-parser": "^0.12.1", "oniguruma-parser": "^0.12.1",
"regex": "^6.0.1", "regex": "^6.1.0",
"regex-recursion": "^6.0.2" "regex-recursion": "^6.0.2"
} }
}, },
@ -3720,9 +3725,9 @@
} }
}, },
"node_modules/unhead": { "node_modules/unhead": {
"version": "2.1.10", "version": "2.1.12",
"resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.10.tgz", "resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.12.tgz",
"integrity": "sha512-We8l9uNF8zz6U8lfQaVG70+R/QBfQx1oPIgXin4BtZnK2IQpz6yazQ0qjMNVBDw2ADgF2ea58BtvSK+XX5AS7g==", "integrity": "sha512-iTHdWD9ztTunOErtfUFk6Wr11BxvzumcYJ0CzaSCBUOEtg+DUZ9+gnE99i8QkLFT2q1rZD48BYYGXpOZVDLYkA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View file

@ -296,6 +296,31 @@ pub(super) async fn reset_password(
Ok(()) Ok(())
} }
#[admin_command]
pub(super) async fn issue_password_reset_link(&self, username: String) -> Result {
use conduwuit_service::password_reset::{PASSWORD_RESET_PATH, RESET_TOKEN_QUERY_PARAM};
self.bail_restricted()?;
let mut reset_url = self
.services
.config
.get_client_domain()
.join(PASSWORD_RESET_PATH)
.unwrap();
let user_id = parse_local_user_id(self.services, &username)?;
let token = self.services.password_reset.issue_token(user_id).await?;
reset_url
.query_pairs_mut()
.append_pair(RESET_TOKEN_QUERY_PARAM, &token.token);
self.write_str(&format!("Password reset link issued for {username}: {reset_url}"))
.await?;
Ok(())
}
#[admin_command] #[admin_command]
pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) -> Result { pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) -> Result {
if self.body.len() < 2 if self.body.len() < 2

View file

@ -29,6 +29,12 @@ pub enum UserCommand {
password: Option<String>, password: Option<String>,
}, },
/// Issue a self-service password reset link for a user.
IssuePasswordResetLink {
/// Username of the user who may use the link
username: String,
},
/// Deactivate a user /// Deactivate a user
/// ///
/// User will be removed from all rooms by default. /// User will be removed from all rooms by default.

View file

@ -68,6 +68,10 @@ pub struct Config {
/// ///
/// Also see the `[global.well_known]` config section at the very bottom. /// Also see the `[global.well_known]` config section at the very bottom.
/// ///
/// If `client` is not set under `[global.well_known]`, the server name will
/// be used as the base domain for user-facing links (such as password
/// reset links) created by Continuwuity.
///
/// Examples of delegation: /// Examples of delegation:
/// - https://continuwuity.org/.well-known/matrix/server /// - https://continuwuity.org/.well-known/matrix/server
/// - https://continuwuity.org/.well-known/matrix/client /// - https://continuwuity.org/.well-known/matrix/client
@ -2105,6 +2109,13 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub force_disable_first_run_mode: bool, pub force_disable_first_run_mode: bool,
/// Allow search engines and crawlers to index Continuwuity's built-in
/// webpages served under the `/_continuwuity/` prefix.
///
/// default: false
#[serde(default)]
pub allow_web_indexing: bool,
/// display: nested /// display: nested
#[serde(default)] #[serde(default)]
pub ldap: LdapConfig, pub ldap: LdapConfig,

View file

@ -1224,6 +1224,7 @@ fn can_send_event(event: &impl Event, ple: Option<&impl Event>, user_level: Int)
} }
/// Confirm that the event sender has the required power levels. /// Confirm that the event sender has the required power levels.
#[allow(clippy::cognitive_complexity)]
fn check_power_levels( fn check_power_levels(
room_version: &RoomVersion, room_version: &RoomVersion,
power_event: &impl Event, power_event: &impl Event,

View file

@ -75,6 +75,7 @@ type Result<T, E = Error> = crate::Result<T, E>;
/// event is part of the same room. /// event is part of the same room.
//#[tracing::instrument(level = "debug", skip(state_sets, auth_chain_sets, //#[tracing::instrument(level = "debug", skip(state_sets, auth_chain_sets,
//#[tracing::instrument(level event_fetch))] //#[tracing::instrument(level event_fetch))]
#[allow(clippy::cognitive_complexity)]
pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, ExistsFut>( pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, ExistsFut>(
room_version: &RoomVersionId, room_version: &RoomVersionId,
state_sets: Sets, state_sets: Sets,

View file

@ -112,6 +112,10 @@ pub(super) static MAPS: &[Descriptor] = &[
name: "onetimekeyid_onetimekeys", name: "onetimekeyid_onetimekeys",
..descriptor::RANDOM_SMALL ..descriptor::RANDOM_SMALL
}, },
Descriptor {
name: "passwordresettoken_info",
..descriptor::RANDOM_SMALL
},
Descriptor { Descriptor {
name: "pduid_pdu", name: "pduid_pdu",
cache_disp: CacheDisp::SharedWith("eventid_outlierpdu"), cache_disp: CacheDisp::SharedWith("eventid_outlierpdu"),

View file

@ -18,5 +18,5 @@ pub(crate) fn build(services: &Arc<Services>) -> (Router, Guard) {
} }
async fn not_found(_uri: Uri) -> impl IntoResponse { async fn not_found(_uri: Uri) -> impl IntoResponse {
Error::Request(ErrorKind::Unrecognized, "Not Found".into(), StatusCode::NOT_FOUND) Error::Request(ErrorKind::Unrecognized, "not found :(".into(), StatusCode::NOT_FOUND)
} }

View file

@ -121,7 +121,7 @@ webpage.workspace = true
webpage.optional = true webpage.optional = true
blurhash.workspace = true blurhash.workspace = true
blurhash.optional = true blurhash.optional = true
recaptcha-verify = { version = "0.1.5", default-features = false } recaptcha-verify = { version = "0.2.0", default-features = false }
yansi.workspace = true yansi.workspace = true
[target.'cfg(all(unix, target_os = "linux"))'.dependencies] [target.'cfg(all(unix, target_os = "linux"))'.dependencies]

View file

@ -6,6 +6,7 @@ use conduwuit::{
config::{Config, check}, config::{Config, check},
error, implement, error, implement,
}; };
use url::Url;
use crate::registration_tokens::{ValidToken, ValidTokenSource}; use crate::registration_tokens::{ValidToken, ValidTokenSource};
@ -23,6 +24,18 @@ impl Service {
.clone() .clone()
.map(|token| ValidToken { token, source: ValidTokenSource::Config }) .map(|token| ValidToken { token, source: ValidTokenSource::Config })
} }
/// Get the base domain to use for user-facing URLs.
#[must_use]
pub fn get_client_domain(&self) -> Url {
self.well_known.client.clone().unwrap_or_else(|| {
let host = self.server_name.host();
format!("https://{host}")
.as_str()
.try_into()
.expect("server name should be a valid host")
})
}
} }
#[async_trait] #[async_trait]

View file

@ -23,6 +23,7 @@ pub mod globals;
pub mod key_backups; pub mod key_backups;
pub mod media; pub mod media;
pub mod moderation; pub mod moderation;
pub mod password_reset;
pub mod presence; pub mod presence;
pub mod pusher; pub mod pusher;
pub mod registration_tokens; pub mod registration_tokens;

View file

@ -0,0 +1,68 @@
use std::{
sync::Arc,
time::{Duration, SystemTime},
};
use conduwuit::utils::{ReadyExt, stream::TryExpect};
use database::{Database, Deserialized, Json, Map};
use ruma::{OwnedUserId, UserId};
use serde::{Deserialize, Serialize};
pub(super) struct Data {
passwordresettoken_info: Arc<Map>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ResetTokenInfo {
pub user: OwnedUserId,
pub issued_at: SystemTime,
}
impl ResetTokenInfo {
// one hour
const MAX_TOKEN_AGE: Duration = Duration::from_secs(60 * 60);
pub fn is_valid(&self) -> bool {
let now = SystemTime::now();
now.duration_since(self.issued_at)
.is_ok_and(|duration| duration < Self::MAX_TOKEN_AGE)
}
}
impl Data {
pub(super) fn new(db: &Arc<Database>) -> Self {
Self {
passwordresettoken_info: db["passwordresettoken_info"].clone(),
}
}
/// Associate a reset token with its info in the database.
pub(super) fn save_token(&self, token: &str, info: &ResetTokenInfo) {
self.passwordresettoken_info.raw_put(token, Json(info));
}
/// Lookup the info for a reset token.
pub(super) async fn lookup_token_info(&self, token: &str) -> Option<ResetTokenInfo> {
self.passwordresettoken_info
.get(token)
.await
.deserialized()
.ok()
}
/// Find a user's existing reset token, if any.
pub(super) async fn find_token_for_user(
&self,
user: &UserId,
) -> Option<(String, ResetTokenInfo)> {
self.passwordresettoken_info
.stream::<'_, String, ResetTokenInfo>()
.expect_ok()
.ready_find(|(_, info)| info.user == user)
.await
}
/// Remove a reset token.
pub(super) fn remove_token(&self, token: &str) { self.passwordresettoken_info.remove(token); }
}

View file

@ -0,0 +1,120 @@
mod data;
use std::{sync::Arc, time::SystemTime};
use conduwuit::{Err, Result, utils};
use data::{Data, ResetTokenInfo};
use ruma::OwnedUserId;
use crate::{Dep, globals, users};
pub const PASSWORD_RESET_PATH: &str = "/_continuwuity/account/reset_password";
pub const RESET_TOKEN_QUERY_PARAM: &str = "token";
const RESET_TOKEN_LENGTH: usize = 32;
pub struct Service {
db: Data,
services: Services,
}
struct Services {
users: Dep<users::Service>,
globals: Dep<globals::Service>,
}
#[derive(Debug)]
pub struct ValidResetToken {
pub token: String,
pub info: ResetTokenInfo,
}
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
db: Data::new(args.db),
services: Services {
users: args.depend::<users::Service>("users"),
globals: args.depend::<globals::Service>("globals"),
},
}))
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
/// Generate a random string suitable to be used as a password reset token.
#[must_use]
pub fn generate_token_string() -> String { utils::random_string(RESET_TOKEN_LENGTH) }
/// Issue a password reset token for `user`, who must be a local user with
/// the `password` origin.
pub async fn issue_token(&self, user_id: OwnedUserId) -> Result<ValidResetToken> {
if !self.services.globals.user_is_local(&user_id) {
return Err!("Cannot issue a password reset token for remote user {user_id}");
}
if user_id == self.services.globals.server_user {
return Err!("Cannot issue a password reset token for the server user");
}
if self
.services
.users
.origin(&user_id)
.await
.unwrap_or_else(|_| "password".to_owned())
!= "password"
{
return Err!("Cannot issue a password reset token for non-internal user {user_id}");
}
if self.services.users.is_deactivated(&user_id).await? {
return Err!("Cannot issue a password reset token for deactivated user {user_id}");
}
if let Some((existing_token, _)) = self.db.find_token_for_user(&user_id).await {
self.db.remove_token(&existing_token);
}
let token = Self::generate_token_string();
let info = ResetTokenInfo {
user: user_id,
issued_at: SystemTime::now(),
};
self.db.save_token(&token, &info);
Ok(ValidResetToken { token, info })
}
/// Check if `token` represents a valid, non-expired password reset token.
pub async fn check_token(&self, token: &str) -> Option<ValidResetToken> {
self.db.lookup_token_info(token).await.and_then(|info| {
if info.is_valid() {
Some(ValidResetToken { token: token.to_owned(), info })
} else {
self.db.remove_token(token);
None
}
})
}
/// Consume the supplied valid token, using it to change its user's password
/// to `new_password`.
pub async fn consume_token(
&self,
ValidResetToken { token, info }: ValidResetToken,
new_password: &str,
) -> Result<()> {
if info.is_valid() {
self.db.remove_token(&token);
self.services
.users
.set_password(&info.user, Some(new_password))
.await?;
}
Ok(())
}
}

View file

@ -11,8 +11,8 @@ use crate::{
account_data, admin, announcements, antispam, appservice, client, config, emergency, account_data, admin, announcements, antispam, appservice, client, config, emergency,
federation, firstrun, globals, key_backups, federation, firstrun, globals, key_backups,
manager::Manager, manager::Manager,
media, moderation, presence, pusher, registration_tokens, resolver, rooms, sending, media, moderation, password_reset, presence, pusher, registration_tokens, resolver, rooms,
server_keys, sending, server_keys,
service::{self, Args, Map, Service}, service::{self, Args, Map, Service},
sync, transactions, uiaa, users, sync, transactions, uiaa, users,
}; };
@ -27,6 +27,7 @@ pub struct Services {
pub globals: Arc<globals::Service>, pub globals: Arc<globals::Service>,
pub key_backups: Arc<key_backups::Service>, pub key_backups: Arc<key_backups::Service>,
pub media: Arc<media::Service>, pub media: Arc<media::Service>,
pub password_reset: Arc<password_reset::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 registration_tokens: Arc<registration_tokens::Service>,
@ -81,6 +82,7 @@ impl Services {
globals: build!(globals::Service), globals: build!(globals::Service),
key_backups: build!(key_backups::Service), key_backups: build!(key_backups::Service),
media: build!(media::Service), media: build!(media::Service),
password_reset: build!(password_reset::Service),
presence: build!(presence::Service), presence: build!(presence::Service),
pusher: build!(pusher::Service), pusher: build!(pusher::Service),
registration_tokens: build!(registration_tokens::Service), registration_tokens: build!(registration_tokens::Service),

View file

@ -181,20 +181,11 @@ pub async fn try_auth(
uiaainfo.completed.push(AuthType::Password); uiaainfo.completed.push(AuthType::Password);
}, },
| AuthData::ReCaptcha(r) => { | AuthData::ReCaptcha(r) => {
if self.services.config.recaptcha_private_site_key.is_none() { let Some(ref private_site_key) = self.services.config.recaptcha_private_site_key
else {
return Err!(Request(Forbidden("ReCaptcha is not configured."))); return Err!(Request(Forbidden("ReCaptcha is not configured.")));
} };
match recaptcha_verify::verify( match recaptcha_verify::verify_v3(private_site_key, r.response.as_str(), None).await {
self.services
.config
.recaptcha_private_site_key
.as_ref()
.unwrap(),
r.response.as_str(),
None,
)
.await
{
| Ok(()) => { | Ok(()) => {
uiaainfo.completed.push(AuthType::ReCaptcha); uiaainfo.completed.push(AuthType::ReCaptcha);
}, },

View file

@ -20,12 +20,25 @@ crate-type = [
[dependencies] [dependencies]
conduwuit-build-metadata.workspace = true conduwuit-build-metadata.workspace = true
conduwuit-service.workspace = true conduwuit-service.workspace = true
conduwuit-core.workspace = true
async-trait.workspace = true
askama.workspace = true askama.workspace = true
axum.workspace = true axum.workspace = true
axum-extra.workspace = true
base64.workspace = true
futures.workspace = true futures.workspace = true
tracing.workspace = true tracing.workspace = true
rand.workspace = true rand.workspace = true
ruma.workspace = true
thiserror.workspace = true thiserror.workspace = true
tower-http.workspace = true
serde.workspace = true
memory-serve = "2.1.0"
validator = { version = "0.20.0", features = ["derive"] }
tower-sec-fetch = { version = "0.1.2", features = ["tracing"] }
[build-dependencies]
memory-serve = "2.1.0"
[lints] [lints]
workspace = true workspace = true

2
src/web/askama.toml Normal file
View file

@ -0,0 +1,2 @@
[general]
dirs = ["pages/templates"]

1
src/web/build.rs Normal file
View file

@ -0,0 +1 @@
fn main() { memory_serve::load_directory("./pages/resources"); }

View file

@ -1,94 +0,0 @@
:root {
color-scheme: light;
--font-stack: sans-serif;
--background-color: #fff;
--text-color: #000;
--bg: oklch(0.76 0.0854 317.27);
--panel-bg: oklch(0.91 0.042 317.27);
--name-lightness: 0.45;
@media (prefers-color-scheme: dark) {
color-scheme: dark;
--text-color: #fff;
--bg: oklch(0.15 0.042 317.27);
--panel-bg: oklch(0.24 0.03 317.27);
--name-lightness: 0.8;
}
--c1: oklch(0.44 0.177 353.06);
--c2: oklch(0.59 0.158 150.88);
--normal-font-size: 1rem;
--small-font-size: 0.8rem;
}
body {
color: var(--text-color);
font-family: var(--font-stack);
margin: 0;
padding: 0;
display: grid;
place-items: center;
min-height: 100vh;
}
html {
background-color: var(--bg);
background-image: linear-gradient(
70deg,
oklch(from var(--bg) l + 0.2 c h),
oklch(from var(--bg) l - 0.2 c h)
);
font-size: 16px;
}
.panel {
width: min(clamp(24rem, 12rem + 40vw, 48rem), calc(100vw - 3rem));
border-radius: 15px;
background-color: var(--panel-bg);
padding-inline: 1.5rem;
padding-block: 1rem;
box-shadow: 0 0.25em 0.375em hsla(0, 0%, 0%, 0.1);
}
@media (max-width: 24rem) {
.panel {
padding-inline: 0.25rem;
width: calc(100vw - 0.5rem);
border-radius: 0;
margin-block-start: 0.2rem;
}
main {
height: 100%;
}
}
footer {
padding-inline: 0.25rem;
height: max(fit-content, 2rem);
}
.project-name {
text-decoration: none;
background: linear-gradient(
130deg,
oklch(from var(--c1) var(--name-lightness) c h),
oklch(from var(--c2) var(--name-lightness) c h)
);
background-clip: text;
color: transparent;
filter: brightness(1.2);
}
b {
color: oklch(from var(--c2) var(--name-lightness) c h);
}
.logo {
width: 100%;
height: 64px;
}

View file

@ -1,86 +1,113 @@
use std::any::Any;
use askama::Template; use askama::Template;
use axum::{ use axum::{
Router, Router,
extract::State, extract::rejection::{FormRejection, QueryRejection},
http::{StatusCode, header}, http::{HeaderValue, StatusCode, header},
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
routing::get,
}; };
use conduwuit_build_metadata::{GIT_REMOTE_COMMIT_URL, GIT_REMOTE_WEB_URL, version_tag};
use conduwuit_service::state; use conduwuit_service::state;
use tower_http::{catch_panic::CatchPanicLayer, set_header::SetResponseHeaderLayer};
use tower_sec_fetch::SecFetchLayer;
pub fn build() -> Router<state::State> { use crate::pages::TemplateContext;
Router::<state::State>::new()
.route("/", get(index_handler))
.route("/_continuwuity/logo.svg", get(logo_handler))
}
async fn index_handler( mod pages;
State(services): State<state::State>,
) -> Result<impl IntoResponse, WebError> {
#[derive(Debug, Template)]
#[template(path = "index.html.j2")]
struct Index<'a> {
nonce: &'a str,
server_name: &'a str,
first_run: bool,
}
let nonce = rand::random::<u64>().to_string();
let template = Index { type State = state::State;
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 'nonce-{nonce}'; img-src 'self';"),
)],
Html(template.render()?),
))
}
async fn logo_handler() -> impl IntoResponse { const CATASTROPHIC_FAILURE: &str = "cat-astrophic failure! we couldn't even render the error template. \
( please contact the team @ https://continuwuity.org";
[(header::CONTENT_TYPE, "image/svg+xml")],
include_str!("templates/logo.svg").to_owned(),
)
}
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
enum WebError { enum WebError {
#[error("Failed to validate form body: {0}")]
ValidationError(#[from] validator::ValidationErrors),
#[error("{0}")]
QueryRejection(#[from] QueryRejection),
#[error("{0}")]
FormRejection(#[from] FormRejection),
#[error("{0}")]
BadRequest(String),
#[error("This page does not exist.")]
NotFound,
#[error("Failed to render template: {0}")] #[error("Failed to render template: {0}")]
Render(#[from] askama::Error), Render(#[from] askama::Error),
#[error("{0}")]
InternalError(#[from] conduwuit_core::Error),
#[error("Request handler panicked! {0}")]
Panic(String),
} }
impl IntoResponse for WebError { impl IntoResponse for WebError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
#[derive(Debug, Template)] #[derive(Debug, Template)]
#[template(path = "error.html.j2")] #[template(path = "error.html.j2")]
struct Error<'a> { struct Error {
nonce: &'a str, error: WebError,
err: WebError, status: StatusCode,
context: TemplateContext,
} }
let nonce = rand::random::<u64>().to_string();
let status = match &self { let status = match &self {
| Self::Render(_) => StatusCode::INTERNAL_SERVER_ERROR, | Self::ValidationError(_)
| Self::BadRequest(_)
| Self::QueryRejection(_)
| Self::FormRejection(_) => StatusCode::BAD_REQUEST,
| Self::NotFound => StatusCode::NOT_FOUND,
| _ => StatusCode::INTERNAL_SERVER_ERROR,
}; };
let tmpl = Error { nonce: &nonce, err: self };
if let Ok(body) = tmpl.render() { let template = Error {
( error: self,
status, status,
[( context: TemplateContext {
header::CONTENT_SECURITY_POLICY, // Statically set false to prevent error pages from being indexed.
format!("default-src 'none' 'nonce-{nonce}';"), allow_indexing: false,
)], },
Html(body), };
)
.into_response() if let Ok(body) = template.render() {
(status, Html(body)).into_response()
} else { } else {
(status, "Something went wrong").into_response() (status, CATASTROPHIC_FAILURE).into_response()
} }
} }
} }
pub fn build() -> Router<state::State> {
#[allow(clippy::wildcard_imports)]
use pages::*;
Router::new()
.merge(index::build())
.nest(
"/_continuwuity/",
Router::new()
.merge(resources::build())
.merge(password_reset::build())
.merge(debug::build())
.fallback(async || WebError::NotFound),
)
.layer(CatchPanicLayer::custom(|panic: Box<dyn Any + Send + 'static>| {
let details = if let Some(s) = panic.downcast_ref::<String>() {
s.clone()
} else if let Some(s) = panic.downcast_ref::<&str>() {
(*s).to_owned()
} else {
"(opaque panic payload)".to_owned()
};
WebError::Panic(details).into_response()
}))
.layer(SetResponseHeaderLayer::if_not_present(
header::CONTENT_SECURITY_POLICY,
HeaderValue::from_static("default-src 'self'; img-src 'self' data:;"),
))
.layer(SecFetchLayer::new(|policy| {
policy.allow_safe_methods().reject_missing_metadata();
}))
}

View file

@ -0,0 +1,98 @@
use askama::{Template, filters::HtmlSafe};
use validator::ValidationErrors;
/// A reusable form component with field validation.
#[derive(Debug, Template)]
#[template(path = "_components/form.html.j2", print = "code")]
pub(crate) struct Form<'a> {
pub inputs: Vec<FormInput<'a>>,
pub validation_errors: Option<ValidationErrors>,
pub submit_label: &'a str,
}
impl HtmlSafe for Form<'_> {}
/// An input element in a form component.
#[derive(Debug, Clone, Copy)]
pub(crate) struct FormInput<'a> {
/// The field name of the input.
pub id: &'static str,
/// The `type` property of the input.
pub input_type: &'a str,
/// The contents of the input's label.
pub label: &'a str,
/// Whether the input is required. Defaults to `true`.
pub required: bool,
/// The autocomplete mode for the input. Defaults to `on`.
pub autocomplete: &'a str,
// This is a hack to make the form! macro's support for client-only fields
// work properly. Client-only fields are specified in the macro without a type and aren't
// included in the POST body or as a field in the generated struct.
// To keep the field from being included in the POST body, its `name` property needs not to
// be set in the template. Because of limitations of macro_rules!'s repetition feature, this
// field needs to exist to allow the template to check if the field is client-only.
#[doc(hidden)]
pub type_name: Option<&'static str>,
}
impl Default for FormInput<'_> {
fn default() -> Self {
Self {
id: "",
input_type: "text",
label: "",
required: true,
autocomplete: "",
type_name: None,
}
}
}
/// Generate a deserializable struct which may be turned into a [`Form`]
/// for inclusion in another template.
#[macro_export]
macro_rules! form {
(
$(#[$struct_meta:meta])*
struct $struct_name:ident {
$(
$(#[$field_meta:meta])*
$name:ident$(: $type:ty)? where { $($prop:ident: $value:expr),* }
),*
submit: $submit_label:expr
}
) => {
#[derive(Debug, serde::Deserialize, validator::Validate)]
$(#[$struct_meta])*
struct $struct_name {
$(
$(#[$field_meta])*
$(pub $name: $type,)?
)*
}
impl $struct_name {
/// Generate a [`Form`] which matches the shape of this struct.
#[allow(clippy::needless_update)]
fn build(validation_errors: Option<validator::ValidationErrors>) -> $crate::pages::components::form::Form<'static> {
$crate::pages::components::form::Form {
inputs: vec![
$(
$crate::pages::components::form::FormInput {
id: stringify!($name),
$(type_name: Some(stringify!($type)),)?
$($prop: $value),*,
..Default::default()
},
)*
],
validation_errors,
submit_label: $submit_label,
}
}
}
};
}

View file

@ -0,0 +1,71 @@
use askama::{Template, filters::HtmlSafe};
use base64::Engine;
use conduwuit_core::result::FlatOk;
use conduwuit_service::Services;
use ruma::UserId;
pub(super) mod form;
#[derive(Debug)]
pub(super) enum AvatarType<'a> {
Initial(char),
Image(&'a str),
}
#[derive(Debug, Template)]
#[template(path = "_components/avatar.html.j2")]
pub(super) struct Avatar<'a> {
pub(super) avatar_type: AvatarType<'a>,
}
impl HtmlSafe for Avatar<'_> {}
#[derive(Debug, Template)]
#[template(path = "_components/user_card.html.j2")]
pub(super) struct UserCard<'a> {
pub user_id: &'a UserId,
pub display_name: Option<String>,
pub avatar_src: Option<String>,
}
impl HtmlSafe for UserCard<'_> {}
impl<'a> UserCard<'a> {
pub(super) async fn for_local_user(services: &Services, user_id: &'a UserId) -> Self {
let display_name = services.users.displayname(user_id).await.ok();
let avatar_src = async {
let avatar_url = services.users.avatar_url(user_id).await.ok()?;
let avatar_mxc = avatar_url.parts().ok()?;
let file = services.media.get(&avatar_mxc).await.flat_ok()?;
Some(format!(
"data:{};base64,{}",
file.content_type
.unwrap_or_else(|| "application/octet-stream".to_owned()),
file.content
.map(|content| base64::prelude::BASE64_STANDARD.encode(content))
.unwrap_or_default(),
))
}
.await;
Self { user_id, display_name, avatar_src }
}
fn avatar(&'a self) -> Avatar<'a> {
let avatar_type = if let Some(ref avatar_src) = self.avatar_src {
AvatarType::Image(avatar_src)
} else if let Some(initial) = self
.display_name
.as_ref()
.and_then(|display_name| display_name.chars().next())
{
AvatarType::Initial(initial)
} else {
AvatarType::Initial(self.user_id.localpart().chars().next().unwrap())
};
Avatar { avatar_type }
}
}

17
src/web/pages/debug.rs Normal file
View file

@ -0,0 +1,17 @@
use std::convert::Infallible;
use axum::{Router, routing::get};
use conduwuit_core::Error;
use crate::WebError;
pub(crate) fn build() -> Router<crate::State> {
Router::new()
.route("/_debug/panic", get(async || -> Infallible { panic!("Guru meditation error") }))
.route(
"/_debug/error",
get(async || -> WebError {
Error::Err(std::borrow::Cow::Borrowed("Guru meditation error")).into()
}),
)
}

28
src/web/pages/index.rs Normal file
View file

@ -0,0 +1,28 @@
use askama::Template;
use axum::{Router, extract::State, response::IntoResponse, routing::get};
use crate::{WebError, template};
pub(crate) fn build() -> Router<crate::State> {
Router::new()
.route("/", get(index_handler))
.route("/_continuwuity/", get(index_handler))
}
async fn index_handler(
State(services): State<crate::State>,
) -> Result<impl IntoResponse, WebError> {
template! {
struct Index<'a> use "index.html.j2" {
server_name: &'a str,
first_run: bool
}
}
Ok(Index::new(
&services,
services.globals.server_name().as_str(),
services.firstrun.is_first_run(),
)
.into_response())
}

53
src/web/pages/mod.rs Normal file
View file

@ -0,0 +1,53 @@
mod components;
pub(super) mod debug;
pub(super) mod index;
pub(super) mod password_reset;
pub(super) mod resources;
#[derive(Debug)]
pub(crate) struct TemplateContext {
pub allow_indexing: bool,
}
impl From<&crate::State> for TemplateContext {
fn from(state: &crate::State) -> Self {
Self {
allow_indexing: state.config.allow_web_indexing,
}
}
}
#[macro_export]
macro_rules! template {
(
struct $name:ident $(<$lifetime:lifetime>)? use $path:literal {
$($field_name:ident: $field_type:ty),*
}
) => {
#[derive(Debug, askama::Template)]
#[template(path = $path)]
struct $name$(<$lifetime>)? {
context: $crate::pages::TemplateContext,
$($field_name: $field_type,)*
}
impl$(<$lifetime>)? $name$(<$lifetime>)? {
fn new(state: &$crate::State, $($field_name: $field_type,)*) -> Self {
Self {
context: state.into(),
$($field_name,)*
}
}
}
#[allow(single_use_lifetimes)]
impl$(<$lifetime>)? axum::response::IntoResponse for $name$(<$lifetime>)? {
fn into_response(self) -> axum::response::Response {
match self.render() {
Ok(rendered) => axum::response::Html(rendered).into_response(),
Err(err) => $crate::WebError::from(err).into_response()
}
}
}
};
}

View file

@ -0,0 +1,120 @@
use askama::Template;
use axum::{
Router,
extract::{
Query, State,
rejection::{FormRejection, QueryRejection},
},
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
};
use serde::Deserialize;
use validator::Validate;
use crate::{
WebError, form,
pages::components::{UserCard, form::Form},
template,
};
const INVALID_TOKEN_ERROR: &str = "Invalid reset token. Your reset link may have expired.";
#[derive(Deserialize)]
struct PasswordResetQuery {
token: String,
}
template! {
struct PasswordReset<'a> use "password_reset.html.j2" {
user_card: UserCard<'a>,
body: PasswordResetBody
}
}
#[derive(Debug)]
enum PasswordResetBody {
Form(Form<'static>),
Success,
}
form! {
struct PasswordResetForm {
#[validate(length(min = 1, message = "Password cannot be empty"))]
new_password: String where {
input_type: "password",
label: "New password",
autocomplete: "new-password"
},
#[validate(must_match(other = "new_password", message = "Passwords must match"))]
confirm_new_password: String where {
input_type: "password",
label: "Confirm new password",
autocomplete: "new-password"
}
submit: "Reset Password"
}
}
pub(crate) fn build() -> Router<crate::State> {
Router::new()
.route("/account/reset_password", get(get_password_reset).post(post_password_reset))
}
async fn password_reset_form(
services: crate::State,
query: PasswordResetQuery,
reset_form: Form<'static>,
) -> Result<impl IntoResponse, WebError> {
let Some(token) = services.password_reset.check_token(&query.token).await else {
return Err(WebError::BadRequest(INVALID_TOKEN_ERROR.to_owned()));
};
let user_card = UserCard::for_local_user(&services, &token.info.user).await;
Ok(PasswordReset::new(&services, user_card, PasswordResetBody::Form(reset_form))
.into_response())
}
async fn get_password_reset(
State(services): State<crate::State>,
query: Result<Query<PasswordResetQuery>, QueryRejection>,
) -> Result<impl IntoResponse, WebError> {
let Query(query) = query?;
password_reset_form(services, query, PasswordResetForm::build(None)).await
}
async fn post_password_reset(
State(services): State<crate::State>,
query: Result<Query<PasswordResetQuery>, QueryRejection>,
form: Result<axum::Form<PasswordResetForm>, FormRejection>,
) -> Result<Response, WebError> {
let Query(query) = query?;
let axum::Form(form) = form?;
match form.validate() {
| Ok(()) => {
let Some(token) = services.password_reset.check_token(&query.token).await else {
return Err(WebError::BadRequest(INVALID_TOKEN_ERROR.to_owned()));
};
let user_id = token.info.user.clone();
services
.password_reset
.consume_token(token, &form.new_password)
.await?;
let user_card = UserCard::for_local_user(&services, &user_id).await;
Ok(PasswordReset::new(&services, user_card, PasswordResetBody::Success)
.into_response())
},
| Err(err) => Ok((
StatusCode::BAD_REQUEST,
password_reset_form(services, query, PasswordResetForm::build(Some(err))).await,
)
.into_response()),
}
}

View file

@ -0,0 +1,9 @@
use axum::Router;
pub(crate) fn build() -> Router<crate::State> {
Router::new().nest(
"/resources/",
#[allow(unused_qualifications)]
memory_serve::load!().index_file(None).into_router(),
)
}

View file

@ -0,0 +1,185 @@
:root {
color-scheme: light;
--font-stack: sans-serif;
--background-color: #fff;
--text-color: #000;
--secondary: #666;
--bg: oklch(0.76 0.0854 317.27);
--panel-bg: oklch(0.91 0.042 317.27);
--c1: oklch(0.44 0.177 353.06);
--c2: oklch(0.59 0.158 150.88);
--name-lightness: 0.45;
--background-lightness: 0.9;
--background-gradient:
radial-gradient(42.12% 56.13% at 100% 0%, oklch(from var(--c2) var(--background-lightness) c h) 0%, #fff0 100%),
radial-gradient(42.01% 79.63% at 52.86% 0%, #d9ff5333 0%, #fff0 100%),
radial-gradient(79.67% 58.09% at 0% 0%, oklch(from var(--c1) var(--background-lightness) c h) 0%, #fff0 100%);
--normal-font-size: 1rem;
--small-font-size: 0.8rem;
--border-radius-sm: 5px;
--border-radius-lg: 15px;
@media (prefers-color-scheme: dark) {
color-scheme: dark;
--text-color: #fff;
--secondary: #888;
--bg: oklch(0.15 0.042 317.27);
--panel-bg: oklch(0.24 0.03 317.27);
--name-lightness: 0.8;
--background-lightness: 0.2;
--background-gradient:
radial-gradient(
42.12% 56.13% at 100% 0%,
oklch(from var(--c2) var(--background-lightness) c h) 0%,
#12121200 100%
),
radial-gradient(55.81% 87.78% at 48.37% 0%, #000 0%, #12121200 89.55%),
radial-gradient(
122.65% 88.24% at 0% 0%,
oklch(from var(--c1) var(--background-lightness) c h) 0%,
#12121200 100%
);
}
}
* {
box-sizing: border-box;
}
body {
display: grid;
margin: 0;
padding: 0;
place-items: center;
min-height: 100vh;
color: var(--text-color);
font-family: var(--font-stack);
line-height: 1.5;
}
html {
background-color: var(--bg);
background-image: var(--background-gradient);
font-size: var(--normal-font-size);
}
footer {
padding-inline: 0.25rem;
height: max(fit-content, 2rem);
.logo {
width: 100%;
height: 64px;
}
}
p {
margin: 1rem 0;
}
em {
color: oklch(from var(--c2) var(--name-lightness) c h);
font-weight: bold;
font-style: normal;
}
small {
color: var(--secondary);
}
small.error {
display: block;
color: red;
font-size: small;
font-style: italic;
margin-bottom: 0.5rem;
}
.panel {
--preferred-width: 12rem + 40dvw;
--maximum-width: 48rem;
width: min(clamp(24rem, var(--preferred-width), var(--maximum-width)), calc(100dvw - 3rem));
border-radius: var(--border-radius-lg);
background-color: var(--panel-bg);
padding-inline: 1.5rem;
padding-block: 1rem;
box-shadow: 0 0.25em 0.375em hsla(0, 0%, 0%, 0.1);
&.narrow {
--preferred-width: 12rem + 20dvw;
--maximum-width: 36rem;
input, button {
width: 100%;
}
}
}
label {
display: block;
}
input, button {
display: inline-block;
padding: 0.5em;
margin-bottom: 0.5em;
font-size: inherit;
font-family: inherit;
color: white;
background-color: transparent;
border: none;
border-radius: var(--border-radius-sm);
}
input {
border: 2px solid var(--secondary);
&:focus-visible {
outline: 2px solid var(--c1);
border-color: transparent;
}
}
button {
background-color: var(--c1);
transition: opacity .2s;
&:enabled:hover {
opacity: 0.8;
cursor: pointer;
}
}
h1 {
margin-top: 0;
margin-bottom: 0.67em;
}
@media (max-width: 425px) {
main {
padding-block-start: 2rem;
width: 100%;
}
.panel {
border-radius: 0;
width: 100%;
}
}
@media (max-width: 799px) {
input, button {
width: 100%;
}
}

View file

@ -0,0 +1,44 @@
.avatar {
--avatar-size: 56px;
display: inline-block;
aspect-ratio: 1 / 1;
inline-size: var(--avatar-size);
border-radius: 50%;
text-align: center;
text-transform: uppercase;
font-size: calc(var(--avatar-size) * 0.5);
font-weight: 700;
line-height: calc(var(--avatar-size) - 2px);
color: oklch(from var(--c1) calc(l + 0.2) c h);
background-color: var(--c1);
}
.user-card {
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
background-color: oklch(from var(--panel-bg) calc(l - 0.05) c h);
border-radius: var(--border-radius-lg);
padding: 16px;
.info {
flex: 1 1;
p {
margin: 0;
&.display-name {
font-weight: 700;
}
&:nth-of-type(2) {
color: var(--secondary);
}
}
}
}

View file

@ -0,0 +1,13 @@
.k10y {
font-family: monospace;
font-size: x-small;
font-weight: 700;
transform: translate(1rem, 1.6rem);
color: var(--secondary);
user-select: none;
}
h1 {
display: flex;
align-items: center;
}

View file

@ -0,0 +1,11 @@
.project-name {
text-decoration: none;
background: linear-gradient(
130deg,
oklch(from var(--c1) var(--name-lightness) c h),
oklch(from var(--c2) var(--name-lightness) c h)
);
background-clip: text;
color: transparent;
filter: brightness(1.2);
}

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="447.99823"
height="447.99823"
viewBox="0 0 447.99823 447.99823"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"
transform="translate(-32.000893,-32.000893)"><circle
style="fill:#9b4bd4;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-dasharray:none;stroke-opacity:1"
id="path1"
cy="256"
cx="256"
r="176" /><path
style="fill:#de6cd3;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 41,174 69,36 C 135,126 175,102 226,94 l -12,31 62,-44 -69,-44 15,30 C 128,69 84,109 41,172 Z"
id="path7" /><path
style="fill:#de6cd3;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 338,41 -36,69 c 84,25 108,65 116,116 l -31,-12 44,62 44,-69 -30,15 C 443,128 403,84 340,41 Z"
id="path6" /><path
style="fill:#de6cd3;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 471,338 -69,-36 c -25,84 -65,108 -116,116 l 12,-31 -62,44 69,44 -15,-30 c 94,-2 138,-42 181,-105 z"
id="path8" /><path
style="fill:#de6cd3;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 174,471 36,-69 C 126,377 102,337 94,286 l 31,12 -44,-62 -44,69 30,-15 c 2,94 42,138 105,181 z"
id="path9" /><g
id="g15"
transform="translate(-5.4157688e-4)"><path
style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 155.45977,224.65379 c -7.25909,13.49567 -7.25909,26.09161 -6.35171,39.58729 0.90737,11.69626 12.7034,24.29222 24.49943,26.09164 21.77727,3.59884 28.12898,-20.69338 28.12898,-20.69338 0,0 4.53693,-15.29508 5.4443,-40.48699"
id="path11" /><path
style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 218.96706,278.05399 c 3.00446,17.12023 7.52704,24.88918 19.22704,28.48918 9,2.7 22.5,-4.5 22.5,-16.2 0.9,21.6 17.1,17.1 19.8,17.1 11.7,-1.8 18.9,-14.4 16.2,-30.6"
id="path12" /><path
style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 305.6941,230.94317 c 1.8,27 6.3,40.5 6.3,40.5 8.1,27 28.8,19.8 28.8,19.8 18.9,-7.2 22.5,-24.3 22.5,-30.6 0,-25.2 -6.3,-35.1 -6.3,-35.1"
id="path13" /></g></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,6 @@
{% match avatar_type %}
{% when AvatarType::Initial with (initial) %}
<span class="avatar" role="img">{{ initial }}</span>
{% when AvatarType::Image with (src) %}
<img class="avatar" src="{{ src }}">
{% endmatch %}

View file

@ -0,0 +1,30 @@
<form method="post">
{% let validation_errors = validation_errors.clone().unwrap_or_default() %}
{% let field_errors = validation_errors.field_errors() %}
{% for input in inputs %}
<p>
<label for="{{ input.id }}">{{ input.label }}</label>
{% let name = std::borrow::Cow::from(*input.id) %}
{% if let Some(errors) = field_errors.get(name) %}
{% for error in errors %}
<small class="error">
{% if let Some(message) = error.message %}
{{ message }}
{% else %}
Mysterious validation error <code>{{ error.code }}</code>!
{% endif %}
</small>
{% endfor %}
{% endif %}
<input
type="{{ input.input_type }}"
id="{{ input.id }}"
autocomplete="{{ input.autocomplete }}"
{% if input.type_name.is_some() %}name="{{ input.id }}"{% endif %}
{% if input.required %}required{% endif %}
>
</p>
{% endfor %}
<button type="submit">{{ submit_label }}</button>
</form>

View file

@ -0,0 +1,9 @@
<div class="user-card">
{{ avatar() }}
<div class="info">
{% if let Some(display_name) = display_name %}
<p class="display-name">{{ display_name }}</p>
{% endif %}
<p class="user_id">{{ user_id }}</p>
</div>
</div>

View file

@ -5,23 +5,24 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>{% block title %}Continuwuity{% endblock %}</title> <title>{% block title %}Continuwuity{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
{%- if !context.allow_indexing %}
<meta name="robots" content="noindex" />
{%- endif %}
<link rel="icon" href="/_continuwuity/logo.svg"> <link rel="icon" href="/_continuwuity/resources/logo.svg">
<style type="text/css" nonce="{{ nonce }}"> <link rel="stylesheet" href="/_continuwuity/resources/common.css">
/*<![CDATA[*/ <link rel="stylesheet" href="/_continuwuity/resources/components.css">
{{ include_str !("css/index.css") | safe }} {% block head %}{% endblock %}
/*]]>*/
</style>
</head> </head>
<body> <body>
<main>{%~ block content %}{% endblock ~%}</main> <main>{%~ block content %}{% endblock ~%}</main>
{%~ block footer ~%} {%~ block footer ~%}
<footer> <footer>
<img class="logo" src="/_continuwuity/logo.svg"> <img class="logo" src="/_continuwuity/resources/logo.svg">
<p>Powered by <a href="https://continuwuity.org">Continuwuity</a> {{ env!("CARGO_PKG_VERSION") }} <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(version_info) = conduwuit_build_metadata::version_tag() ~%}
{%~ if let Some(url) = GIT_REMOTE_COMMIT_URL.or(GIT_REMOTE_WEB_URL) ~%} {%~ if let Some(url) = conduwuit_build_metadata::GIT_REMOTE_COMMIT_URL.or(conduwuit_build_metadata::GIT_REMOTE_WEB_URL) ~%}
(<a href="{{ url }}">{{ version_info }}</a>) (<a href="{{ url }}">{{ version_info }}</a>)
{%~ else ~%} {%~ else ~%}
({{ version_info }}) ({{ version_info }})

View file

@ -0,0 +1,41 @@
{% extends "_layout.html.j2" %}
{%- block head -%}
<link rel="stylesheet" href="/_continuwuity/resources/error.css">
{%- endblock -%}
{%- block title -%}
🐈 Request Error
{%- endblock -%}
{%- block content -%}
<pre class="k10y" aria-hidden>
        
      |  _  _|
     ` ミ_x
     /      |
    /  ヽ   ノ
    │  | | |
 / ̄|   | | |
 | ( ̄ヽ__ヽ_)__)
 \二つ
</pre>
<div class="panel">
<h1>
{% if status == StatusCode::NOT_FOUND %}
Not found
{% else if status == StatusCode::INTERNAL_SERVER_ERROR %}
Internal server error
{% else %}
Bad request
{% endif %}
</h1>
{% if status == StatusCode::INTERNAL_SERVER_ERROR %}
<p>Please <a href="https://forgejo.ellis.link/continuwuation/continuwuity/issues/new">submit a bug report</a> 🥺</p>
{% endif %}
<pre><code>{{ error }}</code></pre>
</div>
{%- endblock -%}

View file

@ -1,4 +1,9 @@
{% extends "_layout.html.j2" %} {% extends "_layout.html.j2" %}
{%- block head -%}
<link rel="stylesheet" href="/_continuwuity/resources/index.css">
{%- endblock -%}
{%- block content -%} {%- block content -%}
<div class="panel"> <div class="panel">
<h1> <h1>
@ -6,7 +11,7 @@
</h1> </h1>
<p>Continuwuity is successfully installed and working.</p> <p>Continuwuity is successfully installed and working.</p>
{%- if first_run %} {%- if first_run %}
<p>To get started, <b>check the server logs</b> for instructions on how to create the first account.</p> <p>To get started, <em>check the server logs</em> 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> <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 %} {%- else %}
<p>To get started, <a href="https://matrix.org/ecosystem/clients">choose a client</a> and connect to <code>{{ server_name }}</code>.</p> <p>To get started, <a href="https://matrix.org/ecosystem/clients">choose a client</a> and connect to <code>{{ server_name }}</code>.</p>

View file

@ -0,0 +1,18 @@
{% extends "_layout.html.j2" %}
{%- block title -%}
Reset Password
{%- endblock -%}
{%- block content -%}
<div class="panel narrow">
<h1>Reset Password</h1>
{{ user_card }}
{% match body %}
{% when PasswordResetBody::Form(reset_form) %}
{{ reset_form }}
{% when PasswordResetBody::Success %}
<p>Your password has been reset successfully.</p>
{% endmatch %}
</div>
{%- endblock -%}

View file

@ -1,20 +0,0 @@
{% extends "_layout.html.j2" %}
{%- block title -%}
Server Error
{%- endblock -%}
{%- block content -%}
<h1>
{%- match err -%}
{% else -%} 500: Internal Server Error
{%- endmatch -%}
</h1>
{%- match err -%}
{% when WebError::Render(err) -%}
<pre>{{ err }}</pre>
{% else -%} <p>An error occurred</p>
{%- endmatch -%}
{%- endblock -%}

View file

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