diff --git a/.dockerignore b/.dockerignore index 453634df..8ca2e3f8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,7 +11,7 @@ docker/ *.iml # Git folder -.git +# .git .gitea .gitlab .github diff --git a/.forgejo/workflows/release-image.yml b/.forgejo/workflows/release-image.yml index 141bfef9..3eb84223 100644 --- a/.forgejo/workflows/release-image.yml +++ b/.forgejo/workflows/release-image.yml @@ -144,6 +144,9 @@ jobs: file: "docker/Dockerfile" build-args: | CONDUWUIT_VERSION_EXTRA=${{ env.COMMIT_SHORT_SHA }} + COMMIT_SHA=${{ github.sha }}) + REMOTE_URL=${{github.event.repository.html_url }} + REMOTE_COMMIT_URL=${{github.event.head_commit.url }} platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} annotations: ${{ steps.meta.outputs.annotations }} diff --git a/Cargo.lock b/Cargo.lock index 2d8a2d0f..1b6c5d26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,48 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dbc3a507a82b17ba0d98f6ce8fd6954ea0c8152e98009d36a40d8dcc8ce078a" +[[package]] +name = "askama" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash 2.1.1", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "askama_parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow", +] + [[package]] name = "assign" version = "1.1.1" @@ -415,6 +457,15 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bindgen" version = "0.69.5" @@ -904,6 +955,7 @@ dependencies = [ "conduwuit_api", "conduwuit_core", "conduwuit_service", + "conduwuit_web", "const-str", "futures", "http", @@ -961,6 +1013,18 @@ dependencies = [ "webpage", ] +[[package]] +name = "conduwuit_web" +version = "0.5.0-rc.5" +dependencies = [ + "askama", + "axum", + "futures", + "rand 0.8.5", + "thiserror 2.0.12", + "tracing", +] + [[package]] name = "console-api" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 1ce5c1db..44db4c4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -626,6 +626,11 @@ package = "conduwuit_macros" path = "src/macros" default-features = false +[workspace.dependencies.conduwuit-web] +package = "conduwuit_web" +path = "src/web" +default-features = false + ############################################################################### # # Release profiles diff --git a/docker/Dockerfile b/docker/Dockerfile index 536af632..56f2d5fa 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -111,14 +111,112 @@ RUN mkdir /out FROM toolchain AS builder + +# Get source +COPY . . + # Conduwuit version info ARG COMMIT_SHA= +ARG SHORT_COMMIT_SHA= +ARG REMOTE_URL= ARG CONDUWUIT_VERSION_EXTRA= +ENV COMMIT_SHA=$COMMIT_SHA +ENV SHORT_COMMIT_SHA=$SHORT_COMMIT_SHA +ENV REMOTE_URL=$REMOTE_URL ENV CONDUWUIT_VERSION_EXTRA=$CONDUWUIT_VERSION_EXTRA -RUN <> /etc/environment + +# Calculate version info from git if not provided via ARGs +# and write all relevant vars to /etc/environment +RUN <<'EOF' +set -e # Exit on error + +# Use temp variables to store calculated values +calculated_commit_sha="" +calculated_remote_url="" +calculated_version_extra="" + +# --- COMMIT_SHA --- +# Calculate COMMIT_SHA if ENV var (from ARG) is empty +if [ -z "${COMMIT_SHA}" ]; then + # Try to get short commit hash from git + calculated_commit_sha=$(git rev-parse HEAD 2>/dev/null || echo "") + if [ -n "${calculated_commit_sha}" ]; then + echo "COMMIT_SHA='${calculated_commit_sha}'" >> /etc/environment + fi +else + # Ensure ARG-provided value is in /etc/environment + echo "COMMIT_SHA='${COMMIT_SHA}'" >> /etc/environment fi + + +if [ -z "${SHORT_COMMIT_SHA}" ]; then + # Try to get short commit hash from git + calculated_short_commit_sha=$(git rev-parse --short HEAD 2>/dev/null || echo "") + if [ -n "${calculated_short_commit_sha}" ]; then + echo "SHORT_COMMIT_SHA='${calculated_short_commit_sha}'" >> /etc/environment + fi +else + # Ensure ARG-provided value is in /etc/environment + echo "SHORT_COMMIT_SHA='${SHORT_COMMIT_SHA}'" >> /etc/environment +fi + +# --- REMOTE_URL --- +# Calculate REMOTE_URL if ENV var (from ARG) is empty +if [ -z "${REMOTE_URL}" ]; then + # Try to get remote origin URL from git + remote_url_raw=$(git config --get remote.origin.url 2>/dev/null || echo "") + if [ -n "${remote_url_raw}" ]; then + # Transform git URL (SSH or HTTPS) to web URL + if [[ $remote_url_raw == "https://"* ]]; then + # Already HTTPS, just remove .git suffix + calculated_remote_url=$(echo "$remote_url_raw" | sed 's/\.git$//') + else + # Convert SSH URL to HTTPS URL + calculated_remote_url=$(echo "$remote_url_raw" | sed 's/\.git$//' | sed 's/:/\//' | sed 's/^git@/https:\/\//') + fi + + # Write calculated web URL if transformation was successful + if [ -n "${calculated_remote_url}" ]; then + echo "REMOTE_URL='${calculated_remote_url}'" >> /etc/environment + fi + fi +else + # Ensure ARG-provided value is in /etc/environment (assume it's a valid web URL) + echo "REMOTE_URL='${REMOTE_URL}'" >> /etc/environment + # Use provided value for REMOTE_COMMIT_URL calculation below + calculated_remote_url="${REMOTE_URL}" +fi + +# --- Determine effective values for subsequent calculations --- +# Use ENV var value if set (from ARG), otherwise use calculated value +effective_commit_sha="${COMMIT_SHA:-$calculated_commit_sha}" +effective_short_commit_sha="${SHORT_COMMIT_SHA:-$calculated_short_commit_sha}" +effective_remote_url="${REMOTE_URL:-$calculated_remote_url}" + +# --- REMOTE_COMMIT_URL --- +# Calculate and write REMOTE_COMMIT_URL if both components are available +if [ -z "${REMOTE_COMMIT_URL}" ] && [ -n "${effective_remote_url}" ] && [ -n "${effective_commit_sha}" ]; then + echo "REMOTE_COMMIT_URL='${effective_remote_url}/commit/${effective_commit_sha}'" >> /etc/environment +else + # Ensure ARG-provided value is in /etc/environment + echo "REMOTE_COMMIT_URL='${REMOTE_COMMIT_URL}'" >> /etc/environment +fi + +# --- CONDUWUIT_VERSION_EXTRA --- +# Calculate CONDUWUIT_VERSION_EXTRA if ENV var (from ARG) is empty +if [ -z "${CONDUWUIT_VERSION_EXTRA}" ]; then + # Use the effective short commit sha, fallback to "unknown revision" + calculated_version_extra="${effective_short_commit_sha:-unknown revision}" + # Handle case where commit sha calculation failed and ARG wasn't set + if [ -z "${calculated_version_extra}" ]; then + calculated_version_extra="unknown revision" + fi + echo "CONDUWUIT_VERSION_EXTRA='${calculated_version_extra}'" >> /etc/environment +else + # Ensure ARG-provided value is in /etc/environment + echo "CONDUWUIT_VERSION_EXTRA='${CONDUWUIT_VERSION_EXTRA}'" >> /etc/environment +fi + EOF ARG TARGETPLATFORM @@ -127,8 +225,6 @@ ARG TARGETPLATFORM RUN cat /etc/environment RUN xx-cargo --print-target-triple -# Get source -COPY . . # Build the binary RUN --mount=type=cache,target=/usr/local/cargo/registry \ diff --git a/nix/pkgs/main/default.nix b/nix/pkgs/main/default.nix index 9c8038a7..3a43af5a 100644 --- a/nix/pkgs/main/default.nix +++ b/nix/pkgs/main/default.nix @@ -130,6 +130,8 @@ buildDepsOnlyEnv = }); buildPackageEnv = { + COMMIT_SHA = inputs.self.rev or inputs.self.dirtyRev or ""; + SHORT_COMMIT_SHA = inputs.self.shortRev or inputs.self.dirtyShortRev or ""; CONDUWUIT_VERSION_EXTRA = inputs.self.shortRev or inputs.self.dirtyShortRev or ""; } // buildDepsOnlyEnv // { # Only needed in static stdenv because these are transitive dependencies of rocksdb diff --git a/src/router/Cargo.toml b/src/router/Cargo.toml index e4ddcb9b..9fcb8d6a 100644 --- a/src/router/Cargo.toml +++ b/src/router/Cargo.toml @@ -103,6 +103,7 @@ conduwuit-admin.workspace = true conduwuit-api.workspace = true conduwuit-core.workspace = true conduwuit-service.workspace = true +conduwuit-web.workspace = true const-str.workspace = true futures.workspace = true http.workspace = true diff --git a/src/router/router.rs b/src/router/router.rs index 0f95b924..2b9ce2ec 100644 --- a/src/router/router.rs +++ b/src/router/router.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use axum::{Router, response::IntoResponse, routing::get}; +use axum::{Router, response::IntoResponse}; use conduwuit::Error; use conduwuit_api::router::{state, state::Guard}; use conduwuit_service::Services; @@ -11,7 +11,7 @@ pub(crate) fn build(services: &Arc) -> (Router, Guard) { let router = Router::::new(); let (state, guard) = state::create(services.clone()); let router = conduwuit_api::router::build(router, &services.server) - .route("/", get(it_works)) + .merge(conduwuit_web::build::().with_state(())) .fallback(not_found) .with_state(state); @@ -21,5 +21,3 @@ pub(crate) fn build(services: &Arc) -> (Router, Guard) { async fn not_found(_uri: Uri) -> impl IntoResponse { Error::Request(ErrorKind::Unrecognized, "Not Found".into(), StatusCode::NOT_FOUND) } - -async fn it_works() -> &'static str { "hewwo from conduwuit woof!" } diff --git a/src/web/Cargo.toml b/src/web/Cargo.toml new file mode 100644 index 00000000..07dde0f7 --- /dev/null +++ b/src/web/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "conduwuit_web" +categories.workspace = true +description.workspace = true +edition.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +version.workspace = true + +[lib] +path = "mod.rs" +crate-type = [ + "rlib", +# "dylib", +] + +[features] + + +[dependencies] +askama = "0.14.0" + +axum.workspace = true +futures.workspace = true +tracing.workspace = true +rand.workspace = true +thiserror.workspace = true + +[lints] +workspace = true diff --git a/src/web/css/index.css b/src/web/css/index.css new file mode 100644 index 00000000..86cb6d8d --- /dev/null +++ b/src/web/css/index.css @@ -0,0 +1,68 @@ +: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), 100vw); + 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); +} + +.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); +} diff --git a/src/web/mod.rs b/src/web/mod.rs new file mode 100644 index 00000000..ddf13be4 --- /dev/null +++ b/src/web/mod.rs @@ -0,0 +1,61 @@ +use askama::Template; +use axum::{ + Router, + http::{StatusCode, header}, + response::{Html, IntoResponse, Response}, + routing::get, +}; + +pub fn build() -> Router<()> { Router::new().route("/", get(index_handler)) } + +async fn index_handler() -> Result { + #[derive(Debug, Template)] + #[template(path = "index.html.j2")] + struct Tmpl<'a> { + nonce: &'a str, + } + let nonce = rand::random::().to_string(); + + let template = Tmpl { nonce: &nonce }; + Ok(( + [(header::CONTENT_SECURITY_POLICY, format!("default-src 'none' 'nonce-{nonce}';"))], + Html(template.render()?), + )) +} + +#[derive(Debug, thiserror::Error)] +enum WebError { + #[error("Failed to render template: {0}")] + Render(#[from] askama::Error), +} + +impl IntoResponse for WebError { + fn into_response(self) -> Response { + #[derive(Debug, Template)] + #[template(path = "error.html.j2")] + struct Tmpl<'a> { + nonce: &'a str, + err: WebError, + } + + let nonce = rand::random::().to_string(); + + let status = match &self { + | Self::Render(_) => StatusCode::INTERNAL_SERVER_ERROR, + }; + let tmpl = Tmpl { nonce: &nonce, err: self }; + if let Ok(body) = tmpl.render() { + ( + status, + [( + header::CONTENT_SECURITY_POLICY, + format!("default-src 'none' 'nonce-{nonce}';"), + )], + Html(body), + ) + .into_response() + } else { + (status, "Something went wrong").into_response() + } + } +} diff --git a/src/web/templates/_layout.html.j2 b/src/web/templates/_layout.html.j2 new file mode 100644 index 00000000..a38745e1 --- /dev/null +++ b/src/web/templates/_layout.html.j2 @@ -0,0 +1,32 @@ + + + + + + {% block title %}Continuwuity{% endblock %} + + + + + + +
{%~ block content %}{% endblock ~%}
+ {%~ block footer ~%} +
+

Powered by Continuwuity + {%~ if let Some(version_info) = option_env!("CONDUWUIT_VERSION_EXTRA").or(option_env!("SHORT_COMMIT_SHA")) ~%} + {%~ if let Some(url) = option_env!("REMOTE_COMMIT_URL").or(option_env!("REMOTE_URL")) ~%} + ({{ version_info }}) + {%~ else ~%} + ({{ version_info }}) + {%~ endif ~%} + {%~ endif ~%}

+
+ {%~ endblock ~%} + + + diff --git a/src/web/templates/error.html.j2 b/src/web/templates/error.html.j2 new file mode 100644 index 00000000..e320d0ed --- /dev/null +++ b/src/web/templates/error.html.j2 @@ -0,0 +1,20 @@ +{% extends "_layout.html.j2" %} + +{%- block title -%} +Server Error +{%- endblock -%} + +{%- block content -%} +

+ {%- match err -%} + {% else -%} 500: Internal Server Error + {%- endmatch -%} +

+ +{%- match err -%} + {% when WebError::Render(err) -%} +
{{ err }}
+ {% else -%}

An error occurred

+{%- endmatch -%} + +{%- endblock -%} diff --git a/src/web/templates/index.html.j2 b/src/web/templates/index.html.j2 new file mode 100644 index 00000000..51393822 --- /dev/null +++ b/src/web/templates/index.html.j2 @@ -0,0 +1,16 @@ +{% extends "_layout.html.j2" %} +{%- block content -%} +
+
+

Welcome to Continuwuity!

+

Continuwuity is successfully installed and working.

+

To get started, you can:

+ +
+ +{%- endblock content -%}