diff --git a/.cargo/config.toml b/.cargo/config.toml index df7470bc5..bc6123a6a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -17,7 +17,7 @@ rustflags = ["--cfg", "tokio_unstable"] [alias] -dev = "run --release --package charted-devtools --" -internals = "run --release --package charted-internals --" -cli = "dev cli" -server = "dev server" +internals = "run --release --bin internals --" +server = "cli server" +cli-rl = "run --release --bin charted --" +cli = "run --bin charted --" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index b17c9a3c2..000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "charted-server", - "remoteUser": "noel", - "dockerComposeFile": "./docker-compose.yml", - "containerUser": "noel", - "workspaceFolder": "/workspaces/charted", - "service": "workspace", - "runServices": ["postgres", "redis"], - "forwardPorts": [6379, 5432], - "customizations": { - "vscode": { - "extensions": [ - "me-dutour-mathieu.vscode-github-actions", - "ms-azuretools.vscode-docker", - "tamasfe.even-better-toml", - "rust-lang.rust-analyzer", - "redhat.vscode-yaml", - "hashicorp.hcl", - "Vue.volar" - ] - } - }, - "features": { - "ghcr.io/devcontainers/features/sshd:1": { - "version": "latest" - } - } -} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml deleted file mode 100644 index da38ac64f..000000000 --- a/.devcontainer/docker-compose.yml +++ /dev/null @@ -1,36 +0,0 @@ -# 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -# Copyright 2022-2025 Noelware, LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -version: '3.8' -services: - workspace: - user: noel - command: sleep infinity - depends_on: [postgres, redis] - image: ghcr.io/auguwu/coder-images/rust - volumes: - - ..:/workspaces/charted:cached - redis: - image: bitnami/redis:7.4.2 - restart: unless-stopped - environment: - - ALLOW_EMPTY_PASSWORD=yes - postgres: - image: bitnami/postgresql:15.10.0 - restart: unless-stopped - environment: - - POSTGRESQL_DATABASE=charted - - POSTGRESQL_USERNAME=charted - - POSTGRESQL_PASSWORD=charted diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 8fe3a09a7..8b20950eb 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -name: CI +name: CI Pipeline on: workflow_dispatch: {} push: @@ -21,99 +21,103 @@ on: - 'issues/gh-**' - 'feat/**' - main + paths-ignore: - '.github/**' - - '.coder/**' - - '.devcontainer/**' - '.vscode/**' + - 'assets/**' + - 'distribution/**' - 'docs/**' - - 'scripts/**' - '.*ignore' - '**.md' - - renovate.json + + - .envrc + - .noeldoc + - .prettierrc.json + - rustfmt.toml + - taplo.toml - LICENSE + - renovate.json pull_request: types: [opened, synchronize] branches: + - 'issues/gh-**' + - 'feat/**' - main - paths-ignore: - - '.coder/**' - - '.devcontainer/**' - - '.vscode/**' - - 'docs/**' - - '.*ignore' - - '**.md' - - renovate.json - - LICENSE + paths: + - '**' +permissions: + contents: read + checks: write +concurrency: + group: ${{github.workflow}}-${{github.ref}} + cancel-in-progress: true jobs: - ci: - name: Rust CI (${{matrix.runner == 'ubuntu-latest' && 'Linux (x86_64)' || matrix.runner == 'self-hosted' && 'Linux (arm64)' || matrix.runner == 'macos-latest' && 'macOS (x86_64)' || matrix.runner == 'macos-14-arm64' && 'macOS (M1)' || matrix.runner == 'windows-latest' && 'Windows' || 'Unknown'}}, Rust ${{matrix.rust-version}}) + rust: + name: "Rust CI / ${{matrix.runner == 'ubuntu-24.04' && 'Linux (x64)' || matrix.runner == 'linux-aarch64' && 'Linux (aarch64)' || matrix.runner == 'macos-latest' && 'macOS (x64)' || matrix.runner == 'macos-14-xlarge' && 'macOS (aarch64)' || matrix.runner == 'windows-latest' && 'Windows (x64)'}}" runs-on: ${{matrix.runner}} strategy: fail-fast: true matrix: - runner: [ubuntu-latest, macos-latest, windows-latest] - rust-version: [nightly] + runner: + [ + ubuntu-24.04, + linux-aarch64, + macos-latest, + macos-14-xlarge, + windows-latest + ] steps: - uses: actions/checkout@v4 - # remove the `rust-toolchain.toml` since it'll be overwritten. + # Ensure that we test on the recent Nightly compiler toolchain - run: rm ${GITHUB_WORKSPACE}/rust-toolchain.toml shell: bash - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{matrix.rust-version}} - components: clippy, rustfmt - + - uses: dtolnay/rust-toolchain@nightly - uses: Swatinem/rust-cache@v2 - - name: Setup `VCPKG_ROOT` environment variable - if: matrix.runner == 'windows-latest' - id: vcpkg - run: | - echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append - echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + - name: 'Windows: Setup vcpkg' + if: ${{matrix.runner == 'windows-latest'}} + run: ./src/ci/windows/Setup-Vcpkg.ps1 - - name: Setup vcpkg cache - if: matrix.runner == 'windows-latest' + - name: 'Windows: Setup vcpkg cache' + if: ${{matrix.runner == 'windows-latest'}} uses: actions/cache@v4 with: key: vcpkg-cache path: | - ${{steps.vcpkg.outputs.VCPKG_ROOT}} - - - name: Install libraries via `vcpkg` - run: vcpkg --triplet x64-windows-static-md install openssl sqlite3 libpq - if: matrix.runner == 'windows-latest' + ${{env.VCPKG_ROOT}} - # On Windows, we'll also need nasm for use with BoringSSL via aws-lc-sys. - - uses: ilammy/setup-nasm@v1 - if: matrix.runner == 'windows-latest' + - name: 'Windows: Install libraries' + if: ${{matrix.runner == 'windows-latest'}} + run: ./src/ci/windows/Install-Libraries.ps1 - # on macOS, `libpq` is not avaliable - - if: matrix.runner == 'macos-latest' - run: brew install postgresql + - name: 'Windows: Install `nasm` for `aws-lc-sys`' + if: ${{matrix.runner == 'windows-latest'}} + uses: ilammy/setup-nasm@v1 - uses: taiki-e/cache-cargo-install-action@v2 with: tool: cargo-nextest - - run: cargo build --all-features --workspace - - run: cargo nextest run --all-features --workspace - - run: cargo test --doc --all-features --workspace + - run: cargo build --workspace --all-features + - run: cargo nextest run --workspace --all-features + - run: cargo test --doc --workspace --all-features + cargo-deny: - name: '`cargo deny`' - runs-on: ubuntu-latest + name: 'Rust / `cargo deny`' + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: taiki-e/cache-cargo-install-action@v2 with: tool: cargo-deny + - run: cargo deny check all - report-missing-deps: - name: Report Missing Dependencies + report-missing-dependencies: + name: 'Rust / Report Missing Dependencies' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -122,66 +126,52 @@ jobs: tool: cargo-machete - run: cargo machete - # we perform Cachix builds on each commit and each release, and Noel uses - # NixOS, so this is probably a must! - # - # useful for users who don't pin to a specific Git tag and pointed to `main` branch + # Perform Cachix builds on each commit (for bleeding edge cases) + # and each release (for `Noelware/nixpkgs-noelware`). nix-build: - name: Nix Build + name: 'Nix / Build' runs-on: ubuntu-latest - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: ${{github.event_name == 'push' && github.ref == 'refs/heads/main'}} steps: - - name: Checkout source code - uses: actions/checkout@v4 - - - name: Install `nix` binary - uses: cachix/install-nix-action@v30 - with: - nix_path: nixpkgs=channel:nixos-unstable - - - name: Setup Cachix - uses: cachix/cachix-action@v15 + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v30 + + # TODO(@auguwu): switch to https://nix.noelware.org, + # which uses Attic as the binary cache + # service. + # - uses: ryanccn/attic-action@v0 + # with: + # endpoint: https://nix.noelware.org + # cache: noelware + # token: ${{secrets.NIX_BINARY_CACHE_TOKEN}} + + - uses: cachix/cachix-action@v15 with: name: noelware authToken: ${{secrets.CACHIX_AUTH_TOKEN}} - - name: Build `charted` binary + - name: 'Build :: `charted`' run: nix build .#charted - - name: Build `charted-helm-plugin` binary + - name: 'Build :: `charted-helm-plugin`' run: nix build .#helm-plugin clippy: - name: Clippy! - runs-on: ubuntu-latest - strategy: - matrix: - rust-version: [nightly] + name: 'Rust / Clippy' + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - # remove the `rust-toolchain.toml` since it'll be overwritten. + # Ensure that we test on the recent Nightly compiler toolchain - run: rm ${GITHUB_WORKSPACE}/rust-toolchain.toml - - uses: dtolnay/rust-toolchain@master + shell: bash + + - uses: dtolnay/rust-toolchain@nightly with: - toolchain: ${{matrix.rust-version}} - components: clippy, rustfmt + components: clippy + - uses: Swatinem/rust-cache@v2 - uses: auguwu/clippy-action@1.4.0 with: all-features: true check-args: --workspace --locked token: ${{github.token}} - hadolint: - name: Hadolint [${{matrix.dockerfile}}] - runs-on: ubuntu-latest - strategy: - matrix: - dockerfile: - - ./distribution/charted/docker/alpine.Dockerfile - - ./distribution/charted/docker/debian.Dockerfile - - ./distribution/helm-plugin/docker/Dockerfile - steps: - - uses: actions/checkout@v4 - - uses: hadolint/hadolint-action@v3.1.0 - with: - dockerfile: ${{matrix.dockerfile}} diff --git a/.gitignore b/.gitignore index e22f887e2..5a899e0dd 100644 --- a/.gitignore +++ b/.gitignore @@ -686,29 +686,11 @@ FodyWeavers.xsd !crates/helm-plugin/src/bin/ !crates/devtools/src/bin/ -!crates/helm-charts/__fixtures__/*.tgz -crates/database/data.db +!crates/helm/charts/__fixtures__/*.tgz !nix/packages/*.nix -!hack/release/ !crates/bin/ !Cargo.lock -config.hcl +config.toml .direnv/ data/ .env - -# !server/src/routing/**/repository/releases/ -# !crates/charts/tests/__fixtures__/*.tgz -# !src/charts/__fixtures__/*.tgz -# !distribution/bin/ -# !scripts/publish/ -# !crates/bin/ -# web/.output -# !Cargo.lock -# config.hcl -# !src/bin/ -# .direnv/ -# .cache/ -# .data/ -# data/ -# .env diff --git a/crates/helm-plugin/src/cmds/login.rs b/.noeldoc similarity index 100% rename from crates/helm-plugin/src/cmds/login.rs rename to .noeldoc diff --git a/.vscode/settings.json b/.vscode/settings.json index d8774688d..12bc50943 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,11 @@ "editor.formatOnPaste": true, "rust-analyzer.check.command": "clippy", "rust-analyzer.showUnlinkedFileNotification": false, + "rust-analyzer.cargo.extraEnv": { + // We use Tokio's unstable cfg flag so we can collect metrics + // so it is easier for us to track. + "RUSTFLAGS": "--cfg tokio_unstable" + }, // Ensure that we don't lock up `cargo check` (i.e, for `cargo expand` to debug macros) // but this comes at the cost of duplicate artifacts, which I think we can sacrifice. @@ -24,8 +29,5 @@ }, "[toml]": { "editor.defaultFormatter": "tamasfe.even-better-toml" - }, - "yaml.schemas": { - "https://json.schemastore.org/github-workflow.json": "/.github/actions/*" } } diff --git a/Cargo.lock b/Cargo.lock index e4e33985b..8d4829489 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "RustyXML" @@ -39,7 +39,7 @@ dependencies = [ "getrandom 0.2.15", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -51,6 +51,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + [[package]] name = "allocator-api2" version = "0.2.21" @@ -113,11 +119,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", + "once_cell", "windows-sys 0.59.0", ] @@ -145,45 +152,6 @@ dependencies = [ "password-hash", ] -[[package]] -name = "asn1-rs" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" -dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", - "displaydoc", - "nom", - "num-traits", - "rusticata-macros", - "thiserror 1.0.69", - "time", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "synstructure 0.12.6", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "async-channel" version = "1.9.0" @@ -196,38 +164,56 @@ dependencies = [ ] [[package]] -name = "async-compression" -version = "0.4.18" +name = "async-lock" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ - "flate2", - "futures-core", - "memchr", + "event-listener 5.4.0", + "event-listener-strategy", "pin-project-lite", - "tokio", ] [[package]] -name = "async-lock" -version = "3.4.0" +name = "async-stream" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ - "event-listener 5.4.0", - "event-listener-strategy", + "async-stream-impl", + "futures-core", "pin-project-lite", ] +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "async-trait" -version = "0.1.85" +version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", ] [[package]] @@ -244,9 +230,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-config" -version = "1.5.13" +version = "1.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a50b30228d3af8865ce83376b4e99e1ffa34728220fe2860e4df0bb5278d6" +checksum = "50236e4d60fe8458de90a71c0922c761e41755adf091b1b03de1cef537179915" dependencies = [ "aws-credential-types", "aws-runtime", @@ -264,7 +250,7 @@ dependencies = [ "fastrand 2.3.0", "hex", "http 0.2.12", - "ring 0.17.8", + "ring", "time", "tokio", "tracing", @@ -286,9 +272,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.12.0" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f409eb70b561706bf8abba8ca9c112729c481595893fd06a2dd9af8ed8441148" +checksum = "4c2b7ddaa2c56a367ad27a094ad8ef4faacf8a617c2575acb2ba88949df999ca" dependencies = [ "aws-lc-sys", "paste", @@ -297,9 +283,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.24.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923ded50f602b3007e5e63e3f094c479d9c8a9b42d7f4034e4afe456aa48bfd2" +checksum = "71b2ddd3ada61a305e1d8bb6c005d1eaa7d14d903681edfc400406d523a9b491" dependencies = [ "bindgen", "cc", @@ -311,9 +297,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.3" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16d1aa50accc11a4b4d5c50f7fb81cc0cf60328259c587d0e6b0f11385bde46" +checksum = "76dd04d39cc12844c0994f2c9c5a6f5184c22e9188ec1ff723de41910a21dcad" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -337,9 +323,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.68.0" +version = "1.74.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5ddf1dc70287dc9a2f953766a1fe15e3e74aef02fd1335f2afa475c9b4f4fc" +checksum = "f551566d462b47c3e49b330f1b86e69e7dc6e4d4efb1959e28c5c82d22e79f7c" dependencies = [ "aws-credential-types", "aws-runtime", @@ -371,9 +357,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.53.0" +version = "1.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1605dc0bf9f0a4b05b451441a17fcb0bda229db384f23bf5cead3adbab0664ac" +checksum = "16ff718c9ee45cc1ebd4774a0e086bb80a6ab752b4902edf1c9f56b86ee1f770" dependencies = [ "aws-credential-types", "aws-runtime", @@ -393,9 +379,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.54.0" +version = "1.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59f3f73466ff24f6ad109095e0f3f2c830bfb4cd6c8b12f744c8e61ebf4d3ba1" +checksum = "5183e088715cc135d8d396fdd3bc02f018f0da4c511f53cb8d795b6a31c55809" dependencies = [ "aws-credential-types", "aws-runtime", @@ -415,9 +401,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.54.1" +version = "1.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "861d324ef69247c6f3c6823755f408a68877ffb1a9afaff6dd8b0057c760de60" +checksum = "c9f944ef032717596639cea4a2118a3a457268ef51bbb5fde9637e54c465da00" dependencies = [ "aws-credential-types", "aws-runtime", @@ -438,9 +424,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.2.6" +version = "1.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3820e0c08d0737872ff3c7c1f21ebbb6693d832312d6152bf18ef50a5471c2" +checksum = "0bc5bbd1e4a2648fd8c5982af03935972c24a2f9846b396de661d351ee3ce837" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -457,7 +443,7 @@ dependencies = [ "once_cell", "p256", "percent-encoding", - "ring 0.17.8", + "ring", "sha2", "subtle", "time", @@ -467,9 +453,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427cb637d15d63d6f9aae26358e1c9a9c09d5aa490d64b09354c8217cfef0f28" +checksum = "fa59d1327d8b5053c54bf2eaae63bf629ba9e904434d0835a28ed3c0ed0a614e" dependencies = [ "futures-util", "pin-project-lite", @@ -478,15 +464,16 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.60.13" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1a71073fca26775c8b5189175ea8863afb1c9ea2cceb02a5de5ad9dfbaa795" +checksum = "f2f45a1c384d7a393026bc5f5c177105aa9fa68e4749653b985707ac27d77295" dependencies = [ "aws-smithy-http", "aws-smithy-types", "bytes", "crc32c", "crc32fast", + "crc64fast-nvme", "hex", "http 0.2.12", "http-body 0.4.6", @@ -499,9 +486,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.5" +version = "0.60.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90" +checksum = "8b18559a41e0c909b77625adf2b8c50de480a8041e5e4a3f5f7d177db70abc5a" dependencies = [ "aws-smithy-types", "bytes", @@ -510,9 +497,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.60.11" +version = "0.60.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6" +checksum = "7809c27ad8da6a6a68c454e651d4962479e81472aa19ae99e59f9aba1f9713cc" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -531,9 +518,9 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.61.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4e69cc50921eb913c6b662f8d909131bb3e6ad6cb6090d3a39b66fc5c52095" +checksum = "623a51127f24c30776c8b374295f2df78d92517386f77ba30773f15a30ce1422" dependencies = [ "aws-smithy-types", ] @@ -550,9 +537,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.7.6" +version = "1.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a05dd41a70fc74051758ee75b5c4db2c0ca070ed9229c3df50e9475cda1cb985" +checksum = "d526a12d9ed61fadefda24abe2e682892ba288c2018bcb38b1b4c111d13f6d92" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -594,9 +581,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.2.11" +version = "1.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ddc9bd6c28aeb303477170ddd183760a956a03e083b3902a990238a7e3792d" +checksum = "c7b8a53819e42f10d0821f56da995e1470b199686a1809168db6ca485665f042" dependencies = [ "base64-simd", "bytes", @@ -629,9 +616,9 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.3" +version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5221b91b3e441e6675310829fd8984801b772cb1546ef6c0e54dec9f1ac13fef" +checksum = "dfbd0a668309ec1f66c0f6bda4840dd6d4796ae26d699ebc266d7cc95c6d040f" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -641,47 +628,13 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core 0.4.5", - "bytes", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.5.2", - "hyper-util", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower 0.5.2", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "axum" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" dependencies = [ - "axum-core 0.5.0", + "axum-core", "axum-macros", "bytes", "form_urlencoded", @@ -689,10 +642,10 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.2", + "hyper 1.6.0", "hyper-util", "itoa", - "matchit 0.8.4", + "matchit", "memchr", "mime", "percent-encoding", @@ -712,11 +665,10 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.5" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" dependencies = [ - "async-trait", "bytes", "futures-util", "http 1.2.0", @@ -732,23 +684,25 @@ dependencies = [ ] [[package]] -name = "axum-core" -version = "0.5.0" +name = "axum-extra" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" +checksum = "460fc6f625a1f7705c6cf62d0d070794e94668988b1c38111baeec177c715f7b" dependencies = [ + "axum", + "axum-core", "bytes", "futures-util", + "headers", "http 1.2.0", "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", - "rustversion", - "sync_wrapper", + "serde", + "tower 0.5.2", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -759,7 +713,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -774,10 +728,10 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.2", + "hyper 1.6.0", "hyper-util", "pin-project-lite", - "rustls 0.23.21", + "rustls 0.23.22", "rustls-pemfile 2.2.0", "rustls-pki-types", "tokio", @@ -789,7 +743,7 @@ dependencies = [ [[package]] name = "azalia" version = "0.1.0" -source = "git+https://github.com/Noelware/azalia?rev=46037bbe850082e833ff01068355180c771621db#46037bbe850082e833ff01068355180c771621db" +source = "git+https://github.com/Noelware/azalia?rev=f4600130658cfe523350222717b4530ce4d30123#f4600130658cfe523350222717b4530ce4d30123" dependencies = [ "azalia-config", "azalia-log", @@ -802,39 +756,39 @@ dependencies = [ [[package]] name = "azalia-config" version = "0.1.0" -source = "git+https://github.com/Noelware/azalia?rev=46037bbe850082e833ff01068355180c771621db#46037bbe850082e833ff01068355180c771621db" +source = "git+https://github.com/Noelware/azalia?rev=f4600130658cfe523350222717b4530ce4d30123#f4600130658cfe523350222717b4530ce4d30123" dependencies = [ "azalia-config-derive", + "url", ] [[package]] name = "azalia-config-derive" version = "0.1.0" -source = "git+https://github.com/Noelware/azalia?rev=46037bbe850082e833ff01068355180c771621db#46037bbe850082e833ff01068355180c771621db" +source = "git+https://github.com/Noelware/azalia?rev=f4600130658cfe523350222717b4530ce4d30123#f4600130658cfe523350222717b4530ce4d30123" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "azalia-log" version = "0.1.0" -source = "git+https://github.com/Noelware/azalia?rev=46037bbe850082e833ff01068355180c771621db#46037bbe850082e833ff01068355180c771621db" +source = "git+https://github.com/Noelware/azalia?rev=f4600130658cfe523350222717b4530ce4d30123#f4600130658cfe523350222717b4530ce4d30123" dependencies = [ "cfg-if", "chrono", "owo-colors 4.1.0", "serde_json", "tracing", - "tracing-log", "tracing-subscriber", ] [[package]] name = "azalia-remi" version = "0.1.0" -source = "git+https://github.com/Noelware/azalia?rev=46037bbe850082e833ff01068355180c771621db#46037bbe850082e833ff01068355180c771621db" +source = "git+https://github.com/Noelware/azalia?rev=f4600130658cfe523350222717b4530ce4d30123#f4600130658cfe523350222717b4530ce4d30123" dependencies = [ "remi", "remi-azure", @@ -847,7 +801,7 @@ dependencies = [ [[package]] name = "azalia-serde" version = "0.1.0" -source = "git+https://github.com/Noelware/azalia?rev=46037bbe850082e833ff01068355180c771621db#46037bbe850082e833ff01068355180c771621db" +source = "git+https://github.com/Noelware/azalia?rev=f4600130658cfe523350222717b4530ce4d30123#f4600130658cfe523350222717b4530ce4d30123" dependencies = [ "serde", "tracing", @@ -1000,7 +954,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.8.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -1011,9 +965,9 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 1.1.0", + "rustc-hash", "shlex", - "syn 2.0.96", + "syn 2.0.98", "which 4.4.2", ] @@ -1025,9 +979,12 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +dependencies = [ + "serde", +] [[package]] name = "bitvec" @@ -1059,56 +1016,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bollard" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d41711ad46fda47cd701f6908e59d1bd6b9a2b7464c0d0aeab95c6d37096ff8a" -dependencies = [ - "base64 0.22.1", - "bollard-stubs", - "bytes", - "futures-core", - "futures-util", - "hex", - "home", - "http 1.2.0", - "http-body-util", - "hyper 1.5.2", - "hyper-named-pipe", - "hyper-rustls 0.27.5", - "hyper-util", - "hyperlocal", - "log", - "pin-project-lite", - "rustls 0.23.21", - "rustls-native-certs 0.7.3", - "rustls-pemfile 2.2.0", - "rustls-pki-types", - "serde", - "serde_derive", - "serde_json", - "serde_repr", - "serde_urlencoded", - "thiserror 1.0.69", - "tokio", - "tokio-util", - "tower-service", - "url", - "winapi", -] - -[[package]] -name = "bollard-stubs" -version = "1.45.0-rc.26.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7c5415e3a6bc6d3e99eff6268e488fd4ee25e7b28c10f08fa6760bd9de16e4" -dependencies = [ - "serde", - "serde_repr", - "serde_with", -] - [[package]] name = "bson" version = "2.13.0" @@ -1119,7 +1026,7 @@ dependencies = [ "base64 0.13.1", "bitvec", "hex", - "indexmap 2.7.0", + "indexmap 2.7.1", "js-sys", "once_cell", "rand 0.8.5", @@ -1132,9 +1039,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byteorder" @@ -1144,9 +1051,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "bytes-utils" @@ -1158,11 +1065,30 @@ dependencies = [ "either", ] +[[package]] +name = "cbindgen" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb" +dependencies = [ + "clap", + "heck 0.4.1", + "indexmap 2.7.1", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.98", + "tempfile", + "toml", +] + [[package]] name = "cc" -version = "1.2.8" +version = "1.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0cf6e91fde44c773c6ee7ec6bba798504641a8bc2eb7e37a04ffbf4dfaa55a" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" dependencies = [ "jobserver", "libc", @@ -1184,82 +1110,55 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "charted" version = "0.1.0" dependencies = [ "charted-cli", - "charted-core", "clap", "color-eyre", "dotenvy", "eyre", - "libsqlite3-sys", "mimalloc", "num_cpus", - "pq-sys", "tokio", ] [[package]] -name = "charted-app" +name = "charted-authz" version = "0.1.0" dependencies = [ "azalia", - "charted-authz", - "charted-authz-ldap", - "charted-authz-local", - "charted-config", "charted-core", "charted-database", + "charted-types", + "derive_more 2.0.0", "eyre", - "tracing", ] [[package]] -name = "charted-authz" +name = "charted-authz-ldap" version = "0.1.0" -dependencies = [ - "azalia", - "charted-core", - "charted-types", - "derive_more 1.0.0", - "eyre", - "tokio", -] [[package]] -name = "charted-authz-ldap" +name = "charted-authz-local" version = "0.1.0" dependencies = [ + "argon2", "charted-authz", - "charted-config", "charted-core", - "charted-types", "eyre", - "ldap3", - "sentry", - "tokio", "tracing", - "url", ] [[package]] -name = "charted-authz-local" +name = "charted-authz-static" version = "0.1.0" dependencies = [ "argon2", "charted-authz", "charted-core", "charted-types", - "eyre", - "tracing", ] [[package]] @@ -1267,45 +1166,26 @@ name = "charted-cli" version = "0.1.0" dependencies = [ "azalia", - "charted-app", - "charted-config", - "charted-core", - "charted-database", - "charted-helm-charts", - "charted-server", - "charted-types", "clap", "clap_complete", - "cli-table", - "diesel", - "diesel_migrations", "eyre", "num_cpus", - "owo-colors 4.1.0", - "rayon", - "reqwest", - "sentry", - "sentry-tracing", - "serde_yaml_ng", - "tokio", + "sea-orm-migration", "tracing", - "tracing-error", "tracing-subscriber", - "url", ] [[package]] name = "charted-client" version = "0.1.0" dependencies = [ - "futures", - "prettyplease", - "progenitor", - "progenitor-client", + "charted-core", + "charted-helm-types", + "charted-types", + "derive_more 2.0.0", "reqwest", - "serde", - "serde_json", - "syn 2.0.96", + "tracing", + "url", ] [[package]] @@ -1314,13 +1194,11 @@ version = "0.1.0" dependencies = [ "azalia", "charted-core", + "derive_more 2.0.0", "eyre", - "hcl-rs", - "remi-azure", - "remi-fs", - "remi-s3", - "sentry-types 0.36.0", + "sentry-types", "serde", + "toml", "tracing", "url", ] @@ -1330,20 +1208,16 @@ name = "charted-core" version = "0.1.0" dependencies = [ "argon2", - "axum 0.8.1", - "azalia", - "charted-testkit", + "axum", "chrono", - "eyre", + "derive_more 2.0.0", "humantime", - "rand 0.8.5", + "rand 0.9.0", "rustc_version", "schemars", "serde", "serde_json", "serde_repr", - "testcontainers-modules", - "ulid", "utoipa", "which 7.0.1", ] @@ -1352,57 +1226,32 @@ dependencies = [ name = "charted-database" version = "0.1.0" dependencies = [ + "async-trait", "charted-config", - "diesel", - "diesel_migrations", - "eyre", - "sentry", - "serde", - "tracing", -] - -[[package]] -name = "charted-devtools" -version = "0.0.0-devel.0" -dependencies = [ - "azalia", - "clap", - "color-eyre", + "charted-core", + "charted-types", "eyre", + "sea-orm", + "sea-orm-migration", + "sqlx", "tracing", - "tracing-subscriber", - "which 7.0.1", + "url", ] [[package]] name = "charted-feature-totp" version = "0.1.0" -dependencies = [ - "charted-config", - "charted-core", - "charted-database", - "charted-features", - "eyre", -] [[package]] name = "charted-features" version = "0.1.0" -dependencies = [ - "axum 0.8.1", - "azalia", - "charted-app", - "charted-core", - "charted-database", - "eyre", - "utoipa", -] [[package]] name = "charted-helm-charts" version = "0.1.0" dependencies = [ "azalia", + "charted-helm-types", "charted-types", "eyre", "flate2", @@ -1421,124 +1270,59 @@ dependencies = [ [[package]] name = "charted-helm-plugin" version = "0.1.0" + +[[package]] +name = "charted-helm-types" +version = "0.1.0" dependencies = [ - "azalia", - "charted-core", "charted-types", - "clap", - "clap_complete", - "color-eyre", - "comfy-table", - "etcetera", - "eyre", - "http 1.2.0", - "reqwest", - "reqwest-middleware", - "schemars", + "chrono", "serde", - "serde_json", - "tokio", - "toml", - "tracing", - "tracing-subscriber", - "url", - "which 7.0.1", + "utoipa", ] [[package]] name = "charted-internals" version = "0.0.0-devel.0" -dependencies = [ - "azalia", - "charted-helm-plugin", - "charted-server", - "color-eyre", - "eyre", - "tracing", - "tracing-subscriber", - "utoipa", -] [[package]] name = "charted-server" version = "0.1.0" dependencies = [ - "argon2", - "axum 0.8.1", + "axum", + "axum-extra", "axum-server", - "azalia", "base64 0.22.1", - "charted-app", "charted-authz", "charted-authz-local", "charted-config", "charted-core", "charted-database", - "charted-features", "charted-helm-charts", - "charted-testkit", + "charted-helm-types", "charted-types", - "chrono", - "derive_more 1.0.0", - "diesel", + "derive_more 2.0.0", "eyre", "jsonwebtoken", "mime", - "multer", + "sea-orm", "sentry", - "sentry-eyre", - "sentry-tower", "serde", "serde_json", "serde_path_to_error", - "serde_yaml_ng", - "tempfile", - "tokio", - "tower 0.5.2", - "tower-http", "tracing", - "utoipa", - "validator", -] - -[[package]] -name = "charted-testkit" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36414d97dbd4280db97141efa1e08b149f2ce22cb82cd6e78c4d023841e5f459" -dependencies = [ - "axum 0.7.9", - "charted-testkit-macros", - "http-body-util", - "hyper 1.5.2", - "hyper-util", - "tokio", - "tower 0.4.13", -] - -[[package]] -name = "charted-testkit-macros" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f88c53739bdba7a0978f33888308c6cdf55c66ea51db97464f6a3298af43bba0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", ] [[package]] name = "charted-types" version = "0.1.0" dependencies = [ - "azalia", "charted-core", - "charted-database", "chrono", - "derive_more 1.0.0", - "diesel", + "derive_more 2.0.0", "paste", "schemars", + "sea-orm", "semver", "serde", "serde_json", @@ -1574,9 +1358,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.26" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" dependencies = [ "clap_builder", "clap_derive", @@ -1584,9 +1368,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.26" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -1596,9 +1380,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.42" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a7e468e750fa4b6be660e8b5651ad47372e8fb114030b594c2d75d48c5ffd0" +checksum = "375f9d8255adeeedd51053574fd8d4ba875ea5fa558e86617b07f09f1680c8b6" dependencies = [ "clap", ] @@ -1609,10 +1393,10 @@ version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1621,29 +1405,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" -[[package]] -name = "cli-table" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b53f9241f288a7b12c56565f04aaeaeeab6b8923d42d99255d4ca428b4d97f89" -dependencies = [ - "cli-table-derive", - "csv", - "termcolor", - "unicode-width 0.1.14", -] - -[[package]] -name = "cli-table-derive" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e83a93253aaae7c74eb7428ce4faa6e219ba94886908048888701819f82fb94" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "cmake" version = "0.1.52" @@ -1687,18 +1448,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" -[[package]] -name = "comfy-table" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24f165e7b643266ea80cb858aed492ad9280e3e05ce24d4a99d7d7b889b6a4d9" -dependencies = [ - "crossterm", - "strum", - "strum_macros", - "unicode-width 0.2.0", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1714,6 +1463,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -1738,13 +1507,28 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32c" version = "0.6.8" @@ -1764,20 +1548,20 @@ dependencies = [ ] [[package]] -name = "crossbeam-deque" -version = "0.8.6" +name = "crc64fast-nvme" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "d5e2ee08013e3f228d6d2394116c4549a6df77708442c62d887d83f68ef2ee37" dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", + "cbindgen", + "crc", ] [[package]] -name = "crossbeam-epoch" -version = "0.9.18" +name = "crossbeam-queue" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] @@ -1789,26 +1573,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "crossterm" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" -dependencies = [ - "bitflags 2.7.0", - "crossterm_winapi", - "parking_lot", - "rustix", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" +name = "crunchy" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "crypto-bigint" @@ -1842,27 +1610,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "csv" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "csv-core" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" -dependencies = [ - "memchr", -] - [[package]] name = "darling" version = "0.20.10" @@ -1884,7 +1631,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1895,14 +1642,14 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" [[package]] name = "debugid" @@ -1925,17 +1672,14 @@ dependencies = [ ] [[package]] -name = "der-parser" -version = "8.2.0" +name = "der" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ - "asn1-rs", - "displaydoc", - "nom", - "num-bigint", - "num-traits", - "rusticata-macros", + "const-oid", + "pem-rfc7468", + "zeroize", ] [[package]] @@ -1948,6 +1692,17 @@ dependencies = [ "serde", ] +[[package]] +name = "derive-syn-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "derive-where" version = "1.2.7" @@ -1956,94 +1711,43 @@ checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "derive_more" -version = "0.99.18" +version = "0.99.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "derive_more" -version = "1.0.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +checksum = "71158d5e914dec8a242751a3fc516b03ed3e6772ce9de79e1aeea6420663cad4" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "1.0.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +checksum = "9e04e066e440d7973a852a3acdc25b0ae712bb6d311755fbf773d6a4518b2226" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "unicode-xid", ] -[[package]] -name = "diesel" -version = "2.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf1bedf64cdb9643204a36dd15b19a6ce8e7aa7f7b105868e9f1fad5ffa7d12" -dependencies = [ - "bitflags 2.7.0", - "byteorder", - "chrono", - "diesel_derives", - "itoa", - "libsqlite3-sys", - "pq-sys", - "r2d2", - "time", - "uuid", -] - -[[package]] -name = "diesel_derives" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f2c3de51e2ba6bf2a648285696137aaf0f5f487bcbea93972fe8a364e131a4" -dependencies = [ - "diesel_table_macro_syntax", - "dsl_auto_type", - "proc-macro2", - "quote", - "syn 2.0.96", -] - -[[package]] -name = "diesel_migrations" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a73ce704bad4231f001bff3314d91dce4aba0770cee8b233991859abc15c1f6" -dependencies = [ - "diesel", - "migrations_internals", - "migrations_macros", -] - -[[package]] -name = "diesel_table_macro_syntax" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" -dependencies = [ - "syn 2.0.96", -] - [[package]] name = "digest" version = "0.10.7" @@ -2051,6 +1755,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -2063,18 +1768,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", -] - -[[package]] -name = "docker_credential" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31951f49556e34d90ed28342e1df7e1cb7a229c4cab0aecc627b5d91edd41d07" -dependencies = [ - "base64 0.21.7", - "serde", - "serde_json", + "syn 2.0.98", ] [[package]] @@ -2083,20 +1777,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "dsl_auto_type" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5d9abe6314103864cc2d8901b7ae224e0ab1a103a0a416661b4097b0779b607" -dependencies = [ - "darling", - "either", - "heck", - "proc-macro2", - "quote", - "syn 2.0.96", -] - [[package]] name = "dunce" version = "1.0.5" @@ -2105,9 +1785,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "dyn-clone" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" [[package]] name = "ecdsa" @@ -2115,10 +1795,10 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ - "der", + "der 0.6.1", "elliptic-curve", "rfc6979", - "signature", + "signature 1.6.4", ] [[package]] @@ -2126,6 +1806,9 @@ name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] [[package]] name = "elliptic-curve" @@ -2135,12 +1818,12 @@ checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ "base16ct", "crypto-bigint 0.4.9", - "der", + "der 0.6.1", "digest", "ff", "generic-array", "group", - "pkcs8", + "pkcs8 0.9.0", "rand_core 0.6.4", "sec1", "subtle", @@ -2162,10 +1845,10 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2180,16 +1863,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "erased-serde" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" -dependencies = [ - "serde", - "typeid", -] - [[package]] name = "errno" version = "0.3.10" @@ -2310,7 +1983,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", - "miniz_oxide 0.8.2", + "miniz_oxide 0.8.3", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", ] [[package]] @@ -2403,6 +2087,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.31" @@ -2432,7 +2127,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2499,6 +2194,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + [[package]] name = "gimli" version = "0.28.1" @@ -2534,7 +2241,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.7.0", + "indexmap 2.7.1", "slab", "tokio", "tokio-util", @@ -2553,7 +2260,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.2.0", - "indexmap 2.7.0", + "indexmap 2.7.1", "slab", "tokio", "tokio-util", @@ -2566,16 +2273,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] - [[package]] name = "hashbrown" version = "0.15.2" @@ -2588,44 +2285,44 @@ dependencies = [ ] [[package]] -name = "hcl-edit" -version = "0.8.4" +name = "hashlink" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0579545680893e2204fbf2d44ee806eba33f78d15ae77f0185811643f43aa5" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "fnv", - "hcl-primitives", - "vecmap-rs", - "winnow", + "hashbrown 0.15.2", ] [[package]] -name = "hcl-primitives" -version = "0.1.8" +name = "headers" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f23f2a1344e7879898b90b8ea39f301b15dde450b315f628f36402d4566823" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ - "itoa", - "kstring", - "ryu", - "serde", - "unicode-ident", + "base64 0.21.7", + "bytes", + "headers-core", + "http 1.2.0", + "httpdate", + "mime", + "sha1", ] [[package]] -name = "hcl-rs" -version = "0.18.3" +name = "headers-core" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf13bca2491da7ac04443ff88836f769ee56fe043c4f7bafcfc51b432f2b5831" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "hcl-edit", - "hcl-primitives", - "indexmap 2.7.0", - "itoa", - "serde", - "vecmap-rs", + "http 1.2.0", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -2652,9 +2349,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hickory-proto" -version = "0.24.2" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447afdcdb8afb9d0a852af6dc65d9b285ce720ed7a59e42a8bf2e931c67bc1b5" +checksum = "2ad3d6d98c648ed628df039541a5577bee1a7c83e9e16fe3dbedeea4cdfeb971" dependencies = [ "async-trait", "cfg-if", @@ -2676,9 +2373,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.24.2" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2e2aba9c389ce5267d31cf1e4dace82390ae276b0b364ea55630b1fa1b44b4" +checksum = "dcf287bde7b776e85d7188e6e5db7cf410a2f9531fe82817eb87feed034c8d14" dependencies = [ "cfg-if", "futures-util", @@ -2695,6 +2392,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -2813,9 +2519,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" [[package]] name = "httpdate" @@ -2855,9 +2561,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", @@ -2874,21 +2580,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-named-pipe" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" -dependencies = [ - "hex", - "hyper 1.5.2", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", - "winapi", -] - [[package]] name = "hyper-rustls" version = "0.24.2" @@ -2900,7 +2591,7 @@ dependencies = [ "hyper 0.14.32", "log", "rustls 0.21.12", - "rustls-native-certs 0.6.3", + "rustls-native-certs", "tokio", "tokio-rustls 0.24.1", ] @@ -2913,14 +2604,13 @@ checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", "http 1.2.0", - "hyper 1.5.2", + "hyper 1.6.0", "hyper-util", - "rustls 0.23.21", + "rustls 0.23.22", "rustls-pki-types", "tokio", "tokio-rustls 0.26.1", "tower-service", - "webpki-roots 0.26.7", ] [[package]] @@ -2931,7 +2621,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.5.2", + "hyper 1.6.0", "hyper-util", "native-tls", "tokio", @@ -2950,7 +2640,7 @@ dependencies = [ "futures-util", "http 1.2.0", "http-body 1.0.1", - "hyper 1.5.2", + "hyper 1.6.0", "pin-project-lite", "socket2", "tokio", @@ -2958,21 +2648,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "hyperlocal" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" -dependencies = [ - "hex", - "http-body-util", - "hyper 1.5.2", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - [[package]] name = "iana-time-zone" version = "0.1.61" @@ -3111,7 +2786,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -3160,9 +2835,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -3181,6 +2856,17 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847" +[[package]] +name = "inherent" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "instant" version = "0.1.13" @@ -3204,19 +2890,19 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is-terminal" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi 0.4.0", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3266,9 +2952,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", @@ -3276,34 +2962,27 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "9.3.0" +version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "js-sys", "pem", - "ring 0.17.8", + "ring", "serde", "serde_json", "simple_asn1", ] -[[package]] -name = "kstring" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" -dependencies = [ - "serde", - "static_assertions", -] - [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "lazycell" @@ -3311,43 +2990,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" -[[package]] -name = "lber" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df7f9fd9f64cf8f59e1a4a0753fe7d575a5b38d3d7ac5758dcee9357d83ef0a" -dependencies = [ - "bytes", - "nom", -] - -[[package]] -name = "ldap3" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "166199a8207874a275144c8a94ff6eed5fcbf5c52303e4d9b4d53a0c7ac76554" -dependencies = [ - "async-trait", - "bytes", - "futures", - "futures-util", - "lazy_static", - "lber", - "log", - "nom", - "percent-encoding", - "ring 0.16.20", - "rustls 0.21.12", - "rustls-native-certs 0.6.3", - "thiserror 1.0.69", - "tokio", - "tokio-rustls 0.24.1", - "tokio-stream", - "tokio-util", - "url", - "x509-parser", -] - [[package]] name = "libc" version = "0.2.169" @@ -3364,6 +3006,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "libmimalloc-sys" version = "0.1.39" @@ -3380,9 +3028,9 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.8.0", "libc", - "redox_syscall 0.5.8", + "redox_syscall", ] [[package]] @@ -3426,12 +3074,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.24" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6ea2a48c204030ee31a7d7fc72c93294c92fe87ecb1789881c9543516e1a0d" -dependencies = [ - "value-bag", -] +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "lru" @@ -3451,6 +3096,54 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "macro_magic" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc33f9f0351468d26fbc53d9ce00a096c8522ecb42f19b50f34f2c422f76d21d" +dependencies = [ + "macro_magic_core", + "macro_magic_macros", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "macro_magic_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1687dc887e42f352865a393acae7cf79d98fab6351cde1f58e9e057da89bf150" +dependencies = [ + "const-random", + "derive-syn-parse", + "macro_magic_core_macros", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "macro_magic_core_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "macro_magic_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" +dependencies = [ + "macro_magic_core", + "quote", + "syn 2.0.98", +] + [[package]] name = "match_cfg" version = "0.1.0" @@ -3466,12 +3159,6 @@ dependencies = [ "regex-automata 0.1.10", ] -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "matchit" version = "0.8.4" @@ -3494,27 +3181,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "migrations_internals" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd01039851e82f8799046eabbb354056283fb265c8ec0996af940f4e85a380ff" -dependencies = [ - "serde", - "toml", -] - -[[package]] -name = "migrations_macros" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb161cc72176cb37aa47f1fc520d3ef02263d67d661f44f05d05a079e1237fd" -dependencies = [ - "migrations_internals", - "proc-macro2", - "quote", -] - [[package]] name = "mimalloc" version = "0.1.43" @@ -3530,6 +3196,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "minimal-lexical" version = "0.2.1" @@ -3547,9 +3223,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", ] @@ -3567,9 +3243,9 @@ dependencies = [ [[package]] name = "mongodb" -version = "3.1.1" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff1f6edf7fe8828429647a2200f684681ca6d5a33b45edc3140c81390d852301" +checksum = "9a93560fa3ec754ed9aa0954ae8307c5997150dbba7aa735173b514660088475" dependencies = [ "async-trait", "base64 0.13.1", @@ -3577,7 +3253,7 @@ dependencies = [ "bson", "chrono", "derive-where", - "derive_more 0.99.18", + "derive_more 0.99.19", "futures-core", "futures-executor", "futures-io", @@ -3586,6 +3262,7 @@ dependencies = [ "hickory-proto", "hickory-resolver", "hmac", + "macro_magic", "md-5", "mongodb-internal-macros", "once_cell", @@ -3615,13 +3292,14 @@ dependencies = [ [[package]] name = "mongodb-internal-macros" -version = "3.1.1" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b07bfd601af78e39384707a8e80041946c98260e3e0190e294ee7435823e6bf" +checksum = "79b3dace6c4f33db61d492b3d3b02f4358687a1eb59457ffef6f6cfe461cdb54" dependencies = [ + "macro_magic", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -3637,15 +3315,15 @@ dependencies = [ "httparse", "memchr", "mime", - "spin 0.9.8", + "spin", "version_check", ] [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" dependencies = [ "libc", "log", @@ -3688,6 +3366,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -3703,6 +3398,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3710,6 +3416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3731,39 +3438,19 @@ dependencies = [ "memchr", ] -[[package]] -name = "oid-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" -dependencies = [ - "asn1-rs", -] - [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" -[[package]] -name = "openapiv3" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc02deea53ffe807708244e5914f6b099ad7015a207ee24317c22112e17d9c5c" -dependencies = [ - "indexmap 2.7.0", - "serde", - "serde_json", -] - [[package]] name = "openssl" -version = "0.10.68" +version = "0.10.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.8.0", "cfg-if", "foreign-types", "libc", @@ -3780,20 +3467,20 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.104" +version = "0.9.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" dependencies = [ "cc", "libc", @@ -3801,6 +3488,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-float" +version = "3.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" +dependencies = [ + "num-traits", +] + [[package]] name = "os_info" version = "3.9.2" @@ -3812,11 +3508,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.98", +] + [[package]] name = "outref" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "overload" @@ -3875,36 +3595,11 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.8", + "redox_syscall", "smallvec", "windows-targets 0.52.6", ] -[[package]] -name = "parse-display" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" -dependencies = [ - "parse-display-derive", - "regex", - "regex-syntax 0.8.5", -] - -[[package]] -name = "parse-display-derive" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281" -dependencies = [ - "proc-macro2", - "quote", - "regex", - "regex-syntax 0.8.5", - "structmeta", - "syn 2.0.96", -] - [[package]] name = "password-hash" version = "0.5.0" @@ -3941,6 +3636,15 @@ dependencies = [ "serde", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -3949,22 +3653,22 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -3979,14 +3683,35 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.9", + "pkcs8 0.10.2", + "spki 0.7.3", +] + [[package]] name = "pkcs8" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ - "der", - "spki", + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.9", + "spki 0.7.3", ] [[package]] @@ -4007,37 +3732,39 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] -name = "pq-src" -version = "0.3.2" +name = "prettyplease" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28fbf44fbf1d3e50d0ca0c6e6253f65d1bf01bb004a6ea553efb32e7081948c7" +checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" dependencies = [ - "cc", - "openssl-sys", + "proc-macro2", + "syn 2.0.98", ] [[package]] -name = "pq-sys" -version = "0.6.3" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cc05d7ea95200187117196eee9edd0644424911821aeb28a18ce60ea0b8793" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ - "pq-src", - "vcpkg", + "proc-macro2", + "quote", ] [[package]] -name = "prettyplease" -version = "0.2.27" +name = "proc-macro-error2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483f8c21f64f3ea09fe0f30f5d48c3e8eefe5dac9129f0075f76593b4c1da705" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" dependencies = [ + "proc-macro-error-attr2", "proc-macro2", - "syn 2.0.96", + "quote", + "syn 2.0.98", ] [[package]] @@ -4050,69 +3777,16 @@ dependencies = [ ] [[package]] -name = "progenitor" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88f54bd2506c3e7b6e45b6ab16500abef551689021264f3be260ef7e295ac327" -dependencies = [ - "progenitor-client", - "progenitor-impl", - "progenitor-macro", -] - -[[package]] -name = "progenitor-client" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdae8df95f0b2a7d6159a9c43b7380016b8d3b0fc1ece46871ecd2e0087cfaf6" -dependencies = [ - "bytes", - "futures-core", - "percent-encoding", - "reqwest", - "serde", - "serde_json", - "serde_urlencoded", -] - -[[package]] -name = "progenitor-impl" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37adc80a94c9cae890e82deeeecc9d8f2a5cb153256caaf1bf0f03611e537214" -dependencies = [ - "heck", - "http 1.2.0", - "indexmap 2.7.0", - "openapiv3", - "proc-macro2", - "quote", - "regex", - "schemars", - "serde", - "serde_json", - "syn 2.0.96", - "thiserror 2.0.11", - "typify", - "unicode-ident", -] - -[[package]] -name = "progenitor-macro" -version = "0.9.1" +name = "proc-macro2-diagnostics" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3b2b9f0d5ba58375c5e8e89d5dff949108e234c1d9f22a3336d2be4daaf292" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ - "openapiv3", "proc-macro2", - "progenitor-impl", "quote", - "schemars", - "serde", - "serde_json", - "serde_tokenstream", - "serde_yaml", - "syn 2.0.96", + "syn 2.0.98", + "version_check", + "yansi", ] [[package]] @@ -4131,58 +3805,6 @@ dependencies = [ "serde", ] -[[package]] -name = "quinn" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" -dependencies = [ - "bytes", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.0", - "rustls 0.23.21", - "socket2", - "thiserror 2.0.11", - "tokio", - "tracing", -] - -[[package]] -name = "quinn-proto" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" -dependencies = [ - "bytes", - "getrandom 0.2.15", - "rand 0.8.5", - "ring 0.17.8", - "rustc-hash 2.1.0", - "rustls 0.23.21", - "rustls-pki-types", - "slab", - "thiserror 2.0.11", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "quote" version = "1.0.38" @@ -4192,17 +3814,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r2d2" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" -dependencies = [ - "log", - "parking_lot", - "scheduled-thread-pool", -] - [[package]] name = "radium" version = "0.7.0" @@ -4233,6 +3844,17 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.0", + "zerocopy 0.8.14", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -4253,6 +3875,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.0", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -4272,41 +3904,22 @@ dependencies = [ ] [[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" +name = "rand_core" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" dependencies = [ - "crossbeam-deque", - "crossbeam-utils", + "getrandom 0.3.1", + "zerocopy 0.8.14", ] [[package]] -name = "redox_syscall" -version = "0.3.5" +name = "rand_hc" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "bitflags 1.3.2", + "rand_core 0.5.1", ] [[package]] @@ -4315,7 +3928,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.8.0", ] [[package]] @@ -4368,21 +3981,11 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "regress" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1541daf4e4ed43a0922b7969bdc2170178bcacc5dabf7e39bc508a9fa3953a7a" -dependencies = [ - "hashbrown 0.14.5", - "memchr", -] - [[package]] name = "remi" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe3c79b3a6c88c780d8fedc9d0fa83512757d50545a65bb5f1b6ec4e504daea0" +checksum = "60941a428437426330589644d4d39c9f722dbdf11a2f657b35088ebe586cce7f" dependencies = [ "async-trait", "bytes", @@ -4390,9 +3993,9 @@ dependencies = [ [[package]] name = "remi-azure" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115571fce4169d992999acd1fc0171cef7a0f57399e906a2bfa38f8c6361138f" +checksum = "a9139ecb62fbf0e4297e4df3dfdacaf0431069f02aa038195a587532b884ae10" dependencies = [ "async-trait", "azure_core", @@ -4402,14 +4005,13 @@ dependencies = [ "futures-util", "remi", "serde", - "tracing", ] [[package]] name = "remi-fs" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99621b113276ba096c8ee56e09781a1ce209393b9f29dc22c9307c4111936791" +checksum = "ebb456a861f3093c9d3c19bccd4de78f52d0eab12ff33d0c22d74cd84858ceae" dependencies = [ "etcetera", "file-format", @@ -4424,9 +4026,9 @@ dependencies = [ [[package]] name = "remi-gridfs" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "141a4e406f67d9154eb740b7bc018edaf143424e5aaf41505c1641309df19125" +checksum = "8ad28621d06fdd33ccc618a6a5c3f8303bd6ef6e4618ad1f8131e6522bb5ba7a" dependencies = [ "async-trait", "bytes", @@ -4435,14 +4037,13 @@ dependencies = [ "remi", "serde", "tokio-util", - "tracing", ] [[package]] name = "remi-s3" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca42e6c3097c5e2791f882fa530f0478063390009b54b0116d11f5f1c5e7991" +checksum = "f200c93ebff3f9568135d550e0bdd168e52605891d50a1973303cc231a19e1df" dependencies = [ "aws-config", "aws-credential-types", @@ -4450,7 +4051,6 @@ dependencies = [ "aws-smithy-runtime-api", "remi", "serde", - "tracing", ] [[package]] @@ -4469,7 +4069,7 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.2", + "hyper 1.6.0", "hyper-rustls 0.27.5", "hyper-tls", "hyper-util", @@ -4477,14 +4077,12 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", "pin-project-lite", - "quinn", - "rustls 0.23.21", "rustls-pemfile 2.2.0", - "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", @@ -4492,7 +4090,6 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", - "tokio-rustls 0.26.1", "tokio-util", "tower 0.5.2", "tower-service", @@ -4501,25 +4098,9 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.26.7", "windows-registry", ] -[[package]] -name = "reqwest-middleware" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ccd3b55e711f91a9885a2fa6fbbb2e39db1776420b062efc058c6410f7e5e3" -dependencies = [ - "anyhow", - "async-trait", - "http 1.2.0", - "reqwest", - "serde", - "thiserror 1.0.69", - "tower-service", -] - [[package]] name = "resolv-conf" version = "0.7.0" @@ -4541,21 +4122,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - [[package]] name = "ring" version = "0.17.8" @@ -4566,11 +4132,31 @@ dependencies = [ "cfg-if", "getrandom 0.2.15", "libc", - "spin 0.9.8", - "untrusted 0.9.0", + "spin", + "untrusted", "windows-sys 0.52.0", ] +[[package]] +name = "rsa" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "signature 2.2.0", + "spki 0.7.3", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -4583,12 +4169,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc-hash" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" - [[package]] name = "rustc_version" version = "0.4.1" @@ -4608,22 +4188,13 @@ dependencies = [ "semver", ] -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom", -] - [[package]] name = "rustix" -version = "0.38.43" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", @@ -4637,20 +4208,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", - "ring 0.17.8", + "ring", "rustls-webpki 0.101.7", "sct", ] [[package]] name = "rustls" -version = "0.23.21" +version = "0.23.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7" dependencies = [ "aws-lc-rs", "once_cell", - "ring 0.17.8", + "ring", "rustls-pki-types", "rustls-webpki 0.102.8", "subtle", @@ -4669,19 +4240,6 @@ dependencies = [ "security-framework", ] -[[package]] -name = "rustls-native-certs" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" -dependencies = [ - "openssl-probe", - "rustls-pemfile 2.2.0", - "rustls-pki-types", - "schannel", - "security-framework", -] - [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -4702,12 +4260,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" -dependencies = [ - "web-time", -] +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" [[package]] name = "rustls-webpki" @@ -4715,8 +4270,8 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", + "ring", + "untrusted", ] [[package]] @@ -4726,9 +4281,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "aws-lc-rs", - "ring 0.17.8", + "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -4739,9 +4294,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "schannel" @@ -4752,15 +4307,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "scheduled-thread-pool" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" -dependencies = [ - "parking_lot", -] - [[package]] name = "schemars" version = "0.8.21" @@ -4774,7 +4320,6 @@ dependencies = [ "serde", "serde_json", "url", - "uuid", ] [[package]] @@ -4786,7 +4331,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -4801,35 +4346,159 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", + "ring", + "untrusted", ] [[package]] -name = "sec1" -version = "0.3.0" +name = "sea-bae" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.98", ] [[package]] -name = "security-framework" -version = "2.11.1" +name = "sea-orm" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "1a93194430b419da0801f404baf3b986399d6a2a4f43bc79bc96dea83f92ca43" dependencies = [ - "bitflags 2.7.0", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", + "async-stream", + "async-trait", + "chrono", + "futures", + "log", + "ouroboros", + "sea-orm-macros", + "sea-query", + "sea-query-binder", + "serde", + "sqlx", + "strum", + "thiserror 1.0.69", + "tracing", + "url", +] + +[[package]] +name = "sea-orm-macros" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19e8f22fb474a8a622eb516c46885a080535d8d559386188f525977eaad32b3" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.98", + "unicode-ident", +] + +[[package]] +name = "sea-orm-migration" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0bb76ba314552ce15e3a24778cf9c116fc1225fa406e48b0a36e5a3cdbc1e21" +dependencies = [ + "async-trait", + "futures", + "sea-orm", + "sea-schema", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sea-query" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085e94f7d7271c0393ac2d164a39994b1dff1b06bc40cd9a0da04f3d672b0fee" +dependencies = [ + "chrono", + "inherent", + "ordered-float", + "sea-query-derive", +] + +[[package]] +name = "sea-query-binder" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" +dependencies = [ + "chrono", + "sea-query", + "sqlx", +] + +[[package]] +name = "sea-query-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9834af2c4bd8c5162f00c89f1701fb6886119a88062cf76fe842ea9e232b9839" +dependencies = [ + "darling", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.98", + "thiserror 1.0.69", +] + +[[package]] +name = "sea-schema" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef5dd7848c993f3789d09a2616484c72c9330cae2b048df59d8c9b8c0343e95" +dependencies = [ + "futures", + "sea-query", + "sea-schema-derive", +] + +[[package]] +name = "sea-schema-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der 0.6.1", + "generic-array", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.8.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", ] [[package]] @@ -4844,9 +4513,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" dependencies = [ "serde", ] @@ -4862,7 +4531,7 @@ dependencies = [ "reqwest", "sentry-backtrace", "sentry-contexts", - "sentry-core 0.36.0", + "sentry-core", "sentry-debug-images", "sentry-panic", "sentry-tracing", @@ -4879,7 +4548,7 @@ dependencies = [ "backtrace", "once_cell", "regex", - "sentry-core 0.36.0", + "sentry-core", ] [[package]] @@ -4892,22 +4561,10 @@ dependencies = [ "libc", "os_info", "rustc_version", - "sentry-core 0.36.0", + "sentry-core", "uname", ] -[[package]] -name = "sentry-core" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "161283cfe8e99c8f6f236a402b9ccf726b201f365988b5bb637ebca0abbd4a30" -dependencies = [ - "once_cell", - "sentry-types 0.34.0", - "serde", - "serde_json", -] - [[package]] name = "sentry-core" version = "0.36.0" @@ -4916,7 +4573,7 @@ checksum = "653942e6141f16651273159f4b8b1eaeedf37a7554c00cd798953e64b8a9bf72" dependencies = [ "once_cell", "rand 0.8.5", - "sentry-types 0.36.0", + "sentry-types", "serde", "serde_json", ] @@ -4929,17 +4586,7 @@ checksum = "2a60bc2154e6df59beed0ac13d58f8dfaf5ad20a88548a53e29e4d92e8e835c2" dependencies = [ "findshlibs", "once_cell", - "sentry-core 0.36.0", -] - -[[package]] -name = "sentry-eyre" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ba221106f53abf085db8871c53cd325ba14f50b2ba18819d5e6db0f782ddc3f" -dependencies = [ - "eyre", - "sentry-core 0.34.0", + "sentry-core", ] [[package]] @@ -4949,22 +4596,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "105e3a956c8aa9dab1e4087b1657b03271bfc49d838c6ae9bfc7c58c802fd0ef" dependencies = [ "sentry-backtrace", - "sentry-core 0.36.0", -] - -[[package]] -name = "sentry-tower" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "082f781dfc504d984e16d99f8dbf94d6ee4762dd0fc28de25713d0f900a8164d" -dependencies = [ - "axum 0.8.1", - "http 1.2.0", - "pin-project", - "sentry-core 0.36.0", - "tower-layer", - "tower-service", - "url", + "sentry-core", ] [[package]] @@ -4974,28 +4606,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64e75c831b4d8b34a5aec1f65f67c5d46a26c7c5d3c7abd8b5ef430796900cf8" dependencies = [ "sentry-backtrace", - "sentry-core 0.36.0", + "sentry-core", "tracing-core", "tracing-subscriber", ] -[[package]] -name = "sentry-types" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d68cdf6bc41b8ff3ae2a9c4671e97426dcdd154cc1d4b6b72813f285d6b163f" -dependencies = [ - "debugid", - "hex", - "rand 0.8.5", - "serde", - "serde_json", - "thiserror 1.0.69", - "time", - "url", - "uuid", -] - [[package]] name = "sentry-types" version = "0.36.0" @@ -5039,7 +4654,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -5050,25 +4665,16 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", -] - -[[package]] -name = "serde_fmt" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" -dependencies = [ - "serde", + "syn 2.0.98", ] [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ - "indexmap 2.7.0", + "indexmap 2.7.1", "itoa", "memchr", "ryu", @@ -5104,7 +4710,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -5116,18 +4722,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_tokenstream" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64060d864397305347a78851c51588fd283767e7e7589829e8121d65512340f1" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 2.0.96", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5150,7 +4744,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.7.0", + "indexmap 2.7.1", "serde", "serde_derive", "serde_json", @@ -5167,20 +4761,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.96", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.7.0", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", + "syn 2.0.98", ] [[package]] @@ -5189,7 +4770,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" dependencies = [ - "indexmap 2.7.0", + "indexmap 2.7.1", "itoa", "ryu", "serde", @@ -5263,15 +4844,25 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simple_asn1" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 1.0.69", + "thiserror 2.0.11", "time", ] @@ -5289,6 +4880,9 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -5300,17 +4894,14 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "spki" @@ -5319,181 +4910,271 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", - "der", + "der 0.6.1", ] [[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "stringprep" -version = "0.1.5" +name = "spki" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", + "base64ct", + "der 0.7.9", ] [[package]] -name = "strsim" -version = "0.11.1" +name = "sqlx" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] [[package]] -name = "structmeta" -version = "0.3.0" +name = "sqlx-core" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" dependencies = [ - "proc-macro2", - "quote", - "structmeta-derive", - "syn 2.0.96", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener 5.4.0", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.2", + "hashlink", + "indexmap 2.7.1", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls 0.23.22", + "rustls-pemfile 2.2.0", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.11", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots 0.26.8", ] [[package]] -name = "structmeta-derive" -version = "0.3.0" +name = "sqlx-macros" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.98", ] [[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" - -[[package]] -name = "strum_macros" -version = "0.26.4" +name = "sqlx-macros-core" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad" dependencies = [ - "heck", + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", "proc-macro2", "quote", - "rustversion", - "syn 2.0.96", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.98", + "tempfile", + "tokio", + "url", ] [[package]] -name = "subtle" -version = "2.6.1" +name = "sqlx-mysql" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.8.0", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.11", + "tracing", + "whoami", +] [[package]] -name = "supports-color" -version = "2.1.0" +name = "sqlx-postgres" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" +checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" dependencies = [ - "is-terminal", - "is_ci", + "atoi", + "base64 0.22.1", + "bitflags 2.8.0", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.11", + "tracing", + "whoami", ] [[package]] -name = "supports-color" -version = "3.0.2" +name = "sqlx-sqlite" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540" dependencies = [ - "is_ci", + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "tracing", + "url", ] [[package]] -name = "sval" -version = "2.13.2" +name = "stable_deref_trait" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6dc0f9830c49db20e73273ffae9b5240f63c42e515af1da1fceefb69fceafd8" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] -name = "sval_buffer" -version = "2.13.2" +name = "static_assertions" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "429922f7ad43c0ef8fd7309e14d750e38899e32eb7e8da656ea169dd28ee212f" -dependencies = [ - "sval", - "sval_ref", -] +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "sval_dynamic" -version = "2.13.2" +name = "stringprep" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f16ff5d839396c11a30019b659b0976348f3803db0626f736764c473b50ff4" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "sval", + "unicode-bidi", + "unicode-normalization", + "unicode-properties", ] [[package]] -name = "sval_fmt" -version = "2.13.2" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c01c27a80b6151b0557f9ccbe89c11db571dc5f68113690c1e028d7e974bae94" -dependencies = [ - "itoa", - "ryu", - "sval", -] +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "sval_json" -version = "2.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0deef63c70da622b2a8069d8600cf4b05396459e665862e7bdb290fd6cf3f155" -dependencies = [ - "itoa", - "ryu", - "sval", -] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" [[package]] -name = "sval_nested" -version = "2.13.2" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a39ce5976ae1feb814c35d290cf7cf8cd4f045782fe1548d6bc32e21f6156e9f" -dependencies = [ - "sval", - "sval_buffer", - "sval_ref", -] +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "sval_ref" -version = "2.13.2" +name = "supports-color" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7c6ee3751795a728bc9316a092023529ffea1783499afbc5c66f5fabebb1fa" +checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" dependencies = [ - "sval", + "is-terminal", + "is_ci", ] [[package]] -name = "sval_serde" -version = "2.13.2" +name = "supports-color" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a5572d0321b68109a343634e3a5d576bf131b82180c6c442dee06349dfc652a" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" dependencies = [ - "serde", - "sval", - "sval_nested", + "is_ci", ] [[package]] @@ -5509,9 +5190,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.96" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -5527,18 +5208,6 @@ dependencies = [ "futures-core", ] -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-xid", -] - [[package]] name = "synstructure" version = "0.13.1" @@ -5547,7 +5216,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -5556,7 +5225,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.8.0", "core-foundation", "system-configuration-sys", ] @@ -5596,65 +5265,18 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.15.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if", "fastrand 2.3.0", - "getrandom 0.2.15", + "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.59.0", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "testcontainers" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f40cc2bd72e17f328faf8ca7687fe337e61bccd8acf9674fa78dd3792b045e1" -dependencies = [ - "async-trait", - "bollard", - "bollard-stubs", - "bytes", - "docker_credential", - "either", - "etcetera", - "futures", - "log", - "memchr", - "parse-display", - "pin-project-lite", - "serde", - "serde_json", - "serde_with", - "thiserror 1.0.69", - "tokio", - "tokio-stream", - "tokio-tar", - "tokio-util", - "url", -] - -[[package]] -name = "testcontainers-modules" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4a531f812dedf1f0d844e69884d76fd7dd9b1967abc89550c6d270b2b55b9c" -dependencies = [ - "testcontainers", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -5681,7 +5303,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -5692,7 +5314,7 @@ checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -5737,6 +5359,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -5787,7 +5418,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -5816,7 +5447,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls 0.23.21", + "rustls 0.23.22", "tokio", ] @@ -5831,21 +5462,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-tar" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75" -dependencies = [ - "filetime", - "futures-core", - "libc", - "redox_syscall 0.3.5", - "tokio", - "tokio-stream", - "xattr", -] - [[package]] name = "tokio-util" version = "0.7.13" @@ -5862,9 +5478,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ "serde", "serde_spanned", @@ -5883,11 +5499,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" dependencies = [ - "indexmap 2.7.0", + "indexmap 2.7.1", "serde", "serde_spanned", "toml_datetime", @@ -5925,30 +5541,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tower-http" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" -dependencies = [ - "async-compression", - "base64 0.22.1", - "bitflags 2.7.0", - "bytes", - "futures-core", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "http-body-util", - "mime", - "pin-project-lite", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower-layer" version = "0.3.3" @@ -5981,7 +5573,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -6050,65 +5642,12 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "typeid" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" - [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "typify" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e03ba3643450cfd95a1aca2e1938fef63c1c1994489337998aff4ad771f21ef8" -dependencies = [ - "typify-impl", - "typify-macro", -] - -[[package]] -name = "typify-impl" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bce48219a2f3154aaa2c56cbf027728b24a3c8fe0a47ed6399781de2b3f3eeaf" -dependencies = [ - "heck", - "log", - "proc-macro2", - "quote", - "regress", - "schemars", - "semver", - "serde", - "serde_json", - "syn 2.0.96", - "thiserror 2.0.11", - "unicode-ident", -] - -[[package]] -name = "typify-macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b5780d745920ed73c5b7447496a9b5c42ed2681a9b70859377aec423ecf02b" -dependencies = [ - "proc-macro2", - "quote", - "schemars", - "semver", - "serde", - "serde_json", - "serde_tokenstream", - "syn 2.0.96", - "typify-impl", -] - [[package]] name = "ulid" version = "1.1.4" @@ -6129,6 +5668,12 @@ dependencies = [ "libc", ] +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -6137,9 +5682,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-normalization" @@ -6156,18 +5701,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" - [[package]] name = "unicode-xid" version = "0.2.6" @@ -6180,12 +5713,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -6247,7 +5774,7 @@ version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" dependencies = [ - "indexmap 2.7.0", + "indexmap 2.7.1", "serde", "serde_json", "utoipa-gen", @@ -6261,75 +5788,25 @@ checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", + "ulid", ] [[package]] name = "uuid" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" -dependencies = [ - "getrandom 0.2.15", - "serde", -] - -[[package]] -name = "validator" -version = "0.20.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" dependencies = [ - "idna", - "once_cell", - "regex", + "getrandom 0.3.1", "serde", - "serde_derive", - "serde_json", - "url", ] [[package]] name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - -[[package]] -name = "value-bag" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" -dependencies = [ - "value-bag-serde1", - "value-bag-sval2", -] - -[[package]] -name = "value-bag-serde1" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb773bd36fd59c7ca6e336c94454d9c66386416734817927ac93d81cb3c5b0b" -dependencies = [ - "erased-serde", - "serde", - "serde_fmt", -] - -[[package]] -name = "value-bag-sval2" -version = "1.10.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a916a702cac43a88694c97657d449775667bcd14b70419441d05b7fea4a83a" -dependencies = [ - "sval", - "sval_buffer", - "sval_dynamic", - "sval_fmt", - "sval_json", - "sval_ref", - "sval_serde", -] +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" @@ -6337,15 +5814,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vecmap-rs" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78fc839a22ab6c4e2f48cf5b935064188148258d467f49323134d503dd08294" -dependencies = [ - "serde", -] - [[package]] name = "version_check" version = "0.9.5" @@ -6385,36 +5853,52 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.49" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", @@ -6425,9 +5909,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6435,22 +5919,25 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-streams" @@ -6467,9 +5954,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -6493,9 +5980,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.7" +version = "0.26.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" dependencies = [ "rustls-pki-types", ] @@ -6521,10 +6008,19 @@ dependencies = [ "either", "env_home", "rustix", - "tracing", "winsafe", ] +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall", + "wasite", +] + [[package]] name = "widestring" version = "1.1.0" @@ -6547,15 +6043,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -6761,9 +6248,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.24" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" dependencies = [ "memchr", ] @@ -6784,6 +6271,15 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -6805,23 +6301,6 @@ dependencies = [ "tap", ] -[[package]] -name = "x509-parser" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da" -dependencies = [ - "asn1-rs", - "data-encoding", - "der-parser", - "lazy_static", - "nom", - "oid-registry", - "rusticata-macros", - "thiserror 1.0.69", - "time", -] - [[package]] name = "xattr" version = "1.4.0" @@ -6839,6 +6318,12 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.7.5" @@ -6859,8 +6344,8 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", - "synstructure 0.13.1", + "syn 2.0.98", + "synstructure", ] [[package]] @@ -6870,7 +6355,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" +dependencies = [ + "zerocopy-derive 0.8.14", ] [[package]] @@ -6881,7 +6375,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", ] [[package]] @@ -6901,8 +6406,8 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", - "synstructure 0.13.1", + "syn 2.0.98", + "synstructure", ] [[package]] @@ -6930,5 +6435,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] diff --git a/Cargo.toml b/Cargo.toml index e509edbcf..743bb400d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,24 +16,24 @@ [workspace] resolver = "2" members = [ - "crates/app", "crates/authz", "crates/authz/*", - "crates/bin", "crates/cli", "crates/client", - "crates/config", + "crates/configuration", "crates/core", "crates/database", - "crates/devtools", "crates/features", - "crates/features/totp", - "crates/helm-charts", - "crates/helm-plugin", + "crates/helm/charts", + "crates/helm/plugin", + "crates/helm/types", "crates/server", "crates/types", - "internals", + "src/charted", + "src/internals", + + "features/totp", ] [workspace.package] @@ -49,54 +49,77 @@ homepage = "https://charts.noelware.org" license = "Apache-2.0" publish = false repository = "https://github.com/charted-dev/charted" +rust-version = "1.80" [workspace.dependencies] +charted-authz = { path = "./crates/authz" } +charted-authz-local = { path = "./crates/authz/local" } +charted-authz-ldap = { path = "./crates/authz/ldap" } +charted-authz-static = { path = "./crates/authz/static" } +charted-cli = { path = "./crates/cli" } +charted-client = { path = "./crates/client", version = "0.1.0" } +charted-config = { path = "./crates/configuration" } +charted-core = { path = "./crates/core", version = "0.1.0" } +charted-database = { path = "./crates/database" } +charted-helm-charts = { path = "./crates/helm/charts" } +charted-helm-plugin = { path = "./crates/helm/plugin" } +charted-helm-types = { path = "./crates/helm/types" } +charted-metrics = { path = "./crates/metrics" } +charted-search = { path = "./crates/search" } +charted-server = { path = "./crates/server" } +charted-types = { path = "./crates/types", version = "0.1.0" } + argon2 = "0.5.3" axum = { version = "0.8.1", features = ["macros", "http2"] } -charted-core = { version = "0.1.0", path = "./crates/core", default-features = false } -charted-database = { version = "0.1.0", path = "./crates/database" } -charted-testkit = "0.1.2" -charted-types = { version = "0.1.0", path = "./crates/types", default-features = false } -chrono = { version = "0.4.23", features = ["serde"] } -clap = { version = "4.5.20", features = ["derive", "env"] } -clap_complete = "4.5.33" -derive_more = "1.0.0" -diesel = { version = "2.2.4", features = ["postgres", "sqlite", "chrono"] } -diesel_migrations = { version = "2.2.0", features = ["postgres", "sqlite"] } +chrono = { version = "0.4.39", features = ["serde"] } +clap = { version = "4.5.27", features = ["derive", "env"] } +clap_complete = "4.5.44" +color-eyre = { version = "0.6.3", features = ["issue-url"] } +derive_more = "2.0.0" eyre = "0.6.12" multer = "3.1.0" -opentelemetry = "0.27.0" -rayon = "1.10.0" -remi = "0.10.0" -remi-azure = { version = "0.10.0", features = ["tracing", "export-azure"] } +num_cpus = "1.16.0" +rand = "0.9.0" remi-fs = { version = "0.10.0", features = ["tracing"] } -remi-s3 = { version = "0.10.0", features = ["tracing", "export-crates"] } -reqwest = { version = "0.12.9", default-features = false, features = [ - "http2", - "macos-system-configuration", - "charset", - "rustls-tls", +rustc_version = "0.4.1" +schemars = { version = "0.8.21", features = ["chrono", "semver", "url"] } +sea-orm = { version = "1.1.4", default-features = false, features = [ + "macros", + "with-chrono", + "sqlx-sqlite", + "sqlx-postgres", +] } +sea-orm-migration = { version = "1.1.4", default-features = false, features = [ + "with-chrono", + "sqlx-sqlite", + "sqlx-postgres", ] } -schemars = "0.8.21" -semver = { version = "1.0.23", features = ["serde"] } +semver = { version = "1.0.25", features = ["serde"] } sentry = "0.36.0" -serde = { version = "1.0.204", features = ["derive"] } -serde_json = "1.0.122" +serde = { version = "1.0.217", features = ["derive", "rc"] } +serde_json = "1.0.138" serde_yaml_ng = "0.10.0" +sqlx = { version = "0.8.3", features = [ + "sqlite", + "postgres", + "runtime-tokio", + "runtime-tokio-rustls", +] } tempfile = "3.12.0" -utoipa = { version = "5.1.1", features = ["chrono", "non_strict_integers"] } -testcontainers = "0.23.0" -testcontainers-modules = "0.11.0" -tokio = "1.39.3" -tracing = "0.1.40" -tracing-subscriber = "0.3.18" -url = "2.5.2" -which = "7.0.0" +tracing = "0.1.41" +tracing-subscriber = "0.3.19" +tokio = "1.43.0" +utoipa = { version = "5.3.1", features = [ + "chrono", + "non_strict_integers", + "ulid", +] } +url = "2.5.4" [workspace.dependencies.azalia] version = "0.1.0" git = "https://github.com/Noelware/azalia" -rev = "46037bbe850082e833ff01068355180c771621db" +rev = "f4600130658cfe523350222717b4530ce4d30123" [profile.release] codegen-units = 1 # use a single codegen unit diff --git a/ci/windows/Install-Libraries.ps1 b/ci/windows/Install-Libraries.ps1 deleted file mode 100644 index bcbd065d8..000000000 --- a/ci/windows/Install-Libraries.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -# 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -# Copyright 2022-2025 Noelware, LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/ci/windows/Release.ps1 b/ci/windows/Release.ps1 deleted file mode 100644 index bcbd065d8..000000000 --- a/ci/windows/Release.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -# 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -# Copyright 2022-2025 Noelware, LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs deleted file mode 100644 index 1dd39edf6..000000000 --- a/crates/app/src/lib.rs +++ /dev/null @@ -1,117 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use azalia::remi::StorageService; -use charted_authz::Authenticator; -use charted_config::{sessions::Backend, storage, Config}; -use charted_core::ulid::AtomicGenerator; -use charted_database::DbPool; -use eyre::Context as _; -use std::sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, -}; -use tracing::info; - -/// Represents the application context that runs through the whole lifetime -/// of **charted-server**. -pub struct Context { - /// Generator that atomically generates monotonic ULIDs. - pub ulid_gen: AtomicGenerator, - - /// How many requests the server has served. - pub requests: AtomicUsize, - - /// Data storage. - pub storage: StorageService, - - /// Parsed configuration from the `charted.hcl` file or system environment variables. - pub config: Config, - - /// Authenticator that allows authenticating users. - pub authz: Arc, - - /// Database pool. - pub pool: DbPool, -} - -impl Clone for Context { - fn clone(&self) -> Self { - Context { - ulid_gen: self.ulid_gen.clone(), - requests: AtomicUsize::new(self.requests.load(Ordering::SeqCst)), - storage: self.storage.clone(), - config: self.config.clone(), - authz: self.authz.clone(), - pool: self.pool.clone(), - } - } -} - -/// Creates a [`StorageService`][azalia::remi::StorageService] that is idempotent from a [`Context`]. -pub async fn create_data_storage(config: &Config) -> eyre::Result { - let storage = match config.storage.clone() { - storage::Config::Filesystem(fs) => { - StorageService::Filesystem(azalia::remi::fs::StorageService::with_config(fs)) - } - - storage::Config::Azure(azure) => StorageService::Azure(azalia::remi::azure::StorageService::new(azure)?), - storage::Config::S3(s3) => StorageService::S3(azalia::remi::s3::StorageService::new(s3)), - }; - - ::init(&storage) - .await - .map(|()| storage) - .context("failed to build data storage") -} - -/// Creates a [`DbPool`] that is idempotent from a [`Context`]. -pub fn create_db_pool(config: &Config) -> eyre::Result { - let pool = charted_database::create_pool(&config.database)?; - let version = charted_database::version(&pool)?; - - info!("received database version [{version}]: connection succeeded."); - if config.database.can_run_migrations() { - info!("performing data migration!"); - charted_database::migrations::migrate(&pool)?; - } - - Ok(pool) -} - -impl Context { - /// Creates a new [`Context`] object with the given configuration. - pub async fn new(config: Config) -> eyre::Result { - let pool = create_db_pool(&config)?; - - info!("initializing data storage!"); - let storage = create_data_storage(&config).await?; - - info!("initializing authentication backend"); - let authz: Arc = match config.sessions.backend.clone() { - Backend::Local => Arc::new(charted_authz_local::Backend), - Backend::Ldap(ldap) => Arc::new(charted_authz_ldap::Backend::new(ldap)), - }; - - Ok(Context { - ulid_gen: AtomicGenerator::new(), - requests: AtomicUsize::new(0), - storage, - config, - authz, - pool, - }) - } -} diff --git a/crates/authz/Cargo.toml b/crates/authz/Cargo.toml index 7b9548f4a..8dd710e04 100644 --- a/crates/authz/Cargo.toml +++ b/crates/authz/Cargo.toml @@ -15,7 +15,6 @@ [package] name = "charted-authz" -description = "🐻‍❄️📦 Abstraction on how to authenticate a user" version.workspace = true documentation.workspace = true edition.workspace = true @@ -30,10 +29,8 @@ path = "lib.rs" [dependencies] azalia.workspace = true -charted-core = { workspace = true, default-features = false } -charted-types = { workspace = true, default-features = false } +charted-core.workspace = true +charted-database.workspace = true +charted-types.workspace = true derive_more = { workspace = true, features = ["display"] } eyre.workspace = true - -[dev-dependencies] -tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/crates/authz/ldap/Cargo.toml b/crates/authz/ldap/Cargo.toml index d5b6d3131..51df0ba96 100644 --- a/crates/authz/ldap/Cargo.toml +++ b/crates/authz/ldap/Cargo.toml @@ -15,7 +15,6 @@ [package] name = "charted-authz-ldap" -description = "🐻‍❄️📦 Allow authentication with OpenLDAP or Active Directory" version.workspace = true documentation.workspace = true edition.workspace = true @@ -27,17 +26,3 @@ authors.workspace = true [lib] path = "lib.rs" - -[dependencies] -charted-authz = { version = "0.1.0", path = ".." } -charted-config = { version = "0.1.0", path = "../../config" } -charted-core = { version = "0.1.0", path = "../../core", default-features = false } -charted-types = { version = "0.1.0", path = "../../types", default-features = false } -eyre.workspace = true -ldap3 = { version = "0.11.5", features = [ - "tls-rustls", -], default-features = false } -sentry.workspace = true -tokio.workspace = true -tracing.workspace = true -url.workspace = true diff --git a/crates/authz/ldap/lib.rs b/crates/authz/ldap/lib.rs index f02515707..4f2e6a0e6 100644 --- a/crates/authz/ldap/lib.rs +++ b/crates/authz/ldap/lib.rs @@ -13,15 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use charted_authz::{Authenticator, InvalidPassword}; -use charted_config::sessions::ldap::Config; -use charted_core::BoxedFuture; -use charted_types::User; -use eyre::eyre; -use ldap3::{Ldap, LdapConnAsync, LdapConnSettings}; -use std::future::Future; -use tracing::error; -use url::Url; +/* #[derive(Clone)] pub struct Backend { @@ -81,4 +73,4 @@ impl Authenticator for Backend { .await }) } -} +*/ diff --git a/crates/authz/lib.rs b/crates/authz/lib.rs index 9e31256fa..75023741b 100644 --- a/crates/authz/lib.rs +++ b/crates/authz/lib.rs @@ -13,61 +13,81 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! # 🐻‍❄️📦 `charted_authz` +//! This crate holds the `Authenticator` trait, which other implementations +//! in `crates/authz` can use to safely authenticate a user. + use azalia::rust::AsArcAny; use charted_core::BoxedFuture; use charted_types::User; use std::{ any::{Any, TypeId}, - error::Error, + borrow::Cow, }; -/// [`Error`] that represents that the password given is invalid. +/// Request object for the [`Authenticator::authenticate`] method. +#[derive(Debug, Clone)] +pub struct Request<'a> { + pub user: User, + pub password: Cow<'a, str>, + pub model: charted_database::entities::user::Model, +} + +/// Error type to safely throw in a [`Authenticator`] implementation +/// when a invalid password is given. #[derive(Debug, derive_more::Display)] -#[display("invalid password given")] +#[display("invalid password")] pub struct InvalidPassword; -impl Error for InvalidPassword {} +impl std::error::Error for InvalidPassword {} -/// Trait that allows to build an authenticator that allows to authenticate users. +/// Safely authenticate a user from any source. pub trait Authenticator: AsArcAny + Send + Sync { - /// Authenticate a given [`User`] with the password given. - fn authenticate<'u>(&'u self, user: &'u User, password: String) -> BoxedFuture<'u, eyre::Result<()>>; + fn authenticate<'a>(&'a self, request: Request<'a>) -> BoxedFuture<'a, eyre::Result<()>>; } impl dyn Authenticator { - /// Compares if [`self`] is `T`, similar to [`Any::is`]. - /// - /// This method might fail (as in, returns `false`) if `T` doesn't implement [`Authenticator`]. + /// Compares if [`self`] is a instance of `T`. Similar implementation + /// of [`Any::is`]. /// /// [`Any::is`]: https://doc.rust-lang.org/std/any/trait.Any.html#method.is pub fn is(&self) -> bool { - // get the `TypeId` of the concrete type (`self` being whatever authenticator is avaliable) - let t = self.type_id(); - - // get the `TypeId` of `T`. - let other = TypeId::of::(); - - t == other + self.type_id() == TypeId::of::() } - /// Attempts to downcast `T` from this dyn [`Authenticator`]. + /// Downcasts `self` to `T`. Returns `None` if `T` is + /// not comparable to `self`. pub fn downcast(&self) -> Option<&T> { - if self.is::() { - // Safety: we checked if `T` is `dyn Registry`. - Some(unsafe { self.downcast_unchecked() }) - } else { - None + self.is::().then_some( + // Safety: we already checked if `self` is `T`. + unsafe { &*(self as *const dyn Authenticator as *const T) }, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct Dummy; + impl Authenticator for Dummy { + fn authenticate<'a>(&'a self, _: Request<'a>) -> BoxedFuture<'a, eyre::Result<()>> { + Box::pin(async { Ok(()) }) } } - /// This method is the same as [`Any::downcast_ref_unchecked`] but uses `dyn Registry` - /// instead of [`dyn Any`]. - /// - /// Since the purpose of this is for the `downcast` method, this is not public - /// and probably never will be. - unsafe fn downcast_unchecked(&self) -> &T { - debug_assert!(self.is::()); + #[test] + fn dyn_authenticator_is() { + let me = Dummy; + + assert!(::is::(&me)); + assert!(!(::is::(&me))); + } + + #[test] + fn dyn_authenticator_downcast() { + let me: Box = Box::new(Dummy); - // SAFETY: caller has ensured that `self` is `dyn Registry`. - unsafe { &*(self as *const dyn Authenticator as *const T) } + assert!(me.downcast::().is_some()); + assert!(me.downcast::().is_none()); } } diff --git a/crates/authz/local/Cargo.toml b/crates/authz/local/Cargo.toml index dbca4cfb8..91b309443 100644 --- a/crates/authz/local/Cargo.toml +++ b/crates/authz/local/Cargo.toml @@ -15,7 +15,6 @@ [package] name = "charted-authz-local" -description = "🐻‍❄️📦 Local implementation that uses charted's database to authorize users" version.workspace = true documentation.workspace = true edition.workspace = true @@ -30,8 +29,7 @@ path = "lib.rs" [dependencies] argon2.workspace = true -charted-authz = { version = "0.1.0", path = ".." } -charted-core = { version = "0.1.0", path = "../../core", default-features = false } -charted-types = { version = "0.1.0", path = "../../types", default-features = false } +charted-authz.workspace = true +charted-core.workspace = true eyre.workspace = true tracing.workspace = true diff --git a/crates/authz/local/lib.rs b/crates/authz/local/lib.rs index 88df22d0b..e771dbbca 100644 --- a/crates/authz/local/lib.rs +++ b/crates/authz/local/lib.rs @@ -13,12 +13,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -use argon2::{PasswordHash, PasswordVerifier}; -use charted_core::ARGON2; -use charted_types::User; -use eyre::eyre; -use tracing::error; +use charted_authz::{Authenticator, Request}; +use charted_core::BoxedFuture; +/// Main implementation of the **local** session management +#[derive(Debug, Clone, Copy, Default)] +pub struct Backend { + _priv: (), +} + +impl Authenticator for Backend { + fn authenticate<'a>( + &'a self, + Request { + user: _, + password: _, + model: _, + }: Request<'a>, + ) -> BoxedFuture<'a, eyre::Result<()>> { + Box::pin(async move { Ok(()) }) + } +} + +/* pub struct Backend; impl charted_authz::Authenticator for Backend { fn authenticate<'u>(&'u self, user: &'u User, password: String) -> charted_core::BoxedFuture<'u, eyre::Result<()>> { @@ -44,3 +61,4 @@ impl charted_authz::Authenticator for Backend { }) } } +*/ diff --git a/crates/features/totp/Cargo.toml b/crates/authz/static/Cargo.toml similarity index 78% rename from crates/features/totp/Cargo.toml rename to crates/authz/static/Cargo.toml index 69ec5b74c..bdd6f415b 100644 --- a/crates/features/totp/Cargo.toml +++ b/crates/authz/static/Cargo.toml @@ -14,7 +14,7 @@ # limitations under the License. [package] -name = "charted-feature-totp" +name = "charted-authz-static" version.workspace = true documentation.workspace = true edition.workspace = true @@ -24,12 +24,11 @@ publish.workspace = true repository.workspace = true authors.workspace = true +[lib] +path = "lib.rs" + [dependencies] -charted-config = { version = "0.1.0", path = "../../config" } +argon2.workspace = true +charted-authz.workspace = true charted-core.workspace = true -charted-database.workspace = true -charted-features = { version = "0.1.0", path = "..", features = [ - "extends-db", - "extends-openapi", -] } -eyre.workspace = true +charted-types.workspace = true diff --git a/crates/helm-plugin/src/main.rs b/crates/authz/static/lib.rs similarity index 71% rename from crates/helm-plugin/src/main.rs rename to crates/authz/static/lib.rs index 0aa1c3dbb..a40148e1d 100644 --- a/crates/helm-plugin/src/main.rs +++ b/crates/authz/static/lib.rs @@ -13,15 +13,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use charted_helm_plugin::Program; -use clap::Parser; +use argon2::PasswordHash; +use std::collections::HashMap; -#[tokio::main(flavor = "current_thread")] -async fn main() -> eyre::Result<()> { - color_eyre::install()?; +#[derive(Debug, Clone)] +pub struct Backend<'a>(#[allow(dead_code)] HashMap>); - let program = Program::parse(); - program.init_logger(); - - program.cmd.run().await +impl Backend<'_> { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Backend(HashMap::new()) + } } diff --git a/crates/bin/Cargo.toml b/crates/bin/Cargo.toml deleted file mode 100644 index 6334e041f..000000000 --- a/crates/bin/Cargo.toml +++ /dev/null @@ -1,50 +0,0 @@ -# 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -# Copyright 2022-2025 Noelware, LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -[package] -name = "charted" -description = "🐻‍❄️📦 Free, open, reliable Helm chart registry built in Rust" -version.workspace = true -documentation.workspace = true -edition.workspace = true -homepage.workspace = true -license.workspace = true -publish.workspace = true -repository.workspace = true -authors.workspace = true - -[features] -default = [] - -bundled-pq = ["dep:pq-sys", "pq-sys/bundled"] -bundled-sqlite = ["dep:libsqlite3-sys", "libsqlite3-sys/bundled"] - -[dependencies] -charted-cli = { version = "0.1.0", path = "../cli" } -charted-core.workspace = true -clap.workspace = true -color-eyre = { version = "0.6.3", features = ["issue-url"] } -dotenvy = "0.15.7" -eyre.workspace = true -libsqlite3-sys = { version = "0.30.1", optional = true } -mimalloc = "0.1.43" -num_cpus = "1.16.0" -pq-sys = { version = "0.6.3", optional = true } -tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } - -[package.metadata.cargo-machete] -# The drivers here aren't actually used in the crate, they're mainly here -# to statically link them if their respected features is enabled -ignored = ["libsqlite3-sys", "pq-sys"] diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 77339cffc..e0e2e5aef 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -15,7 +15,6 @@ [package] name = "charted-cli" -description = "🐻‍❄️📦 Implementation of the `charted` CLI" version.workspace = true documentation.workspace = true edition.workspace = true @@ -26,33 +25,11 @@ repository.workspace = true authors.workspace = true [dependencies] -azalia = { workspace = true, features = [ - "log", - "log-writers", - "log-tracing-log", -] } -charted-app = { version = "0.1.0", path = "../app" } -charted-config = { version = "0.1.0", path = "../config" } -charted-core = { workspace = true, default-features = false } -charted-database.workspace = true -charted-helm-charts = { version = "0.1.0", path = "../helm-charts" } -charted-server = { version = "0.1.0", path = "../server" } -charted-types.workspace = true -clap = { workspace = true, features = ["derive", "env"] } +azalia = { workspace = true, features = ["log", "log-writers"] } +clap.workspace = true clap_complete.workspace = true -cli-table = "0.4.9" -diesel.workspace = true -diesel_migrations.workspace = true eyre.workspace = true -num_cpus = "1.16.0" -owo-colors = { version = "4.1.0", features = ["supports-colors"] } -rayon.workspace = true -reqwest.workspace = true -sentry.workspace = true -sentry-tracing = "0.36.0" -serde_yaml_ng.workspace = true -tokio = { workspace = true, features = ["rt"] } +num_cpus.workspace = true +sea-orm-migration.workspace = true tracing.workspace = true -tracing-error = "0.2.0" tracing-subscriber.workspace = true -url.workspace = true diff --git a/crates/cli/src/cmds/migrations.rs b/crates/cli/src/cmds/migrations.rs deleted file mode 100644 index 241aa24b7..000000000 --- a/crates/cli/src/cmds/migrations.rs +++ /dev/null @@ -1,39 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -mod chart; -mod list; -mod revert; -mod run; - -/// Allows doing database migrations or Helm chart index migrations. -#[derive(Debug, Clone, clap::Subcommand)] -pub enum Cmd { - Chart(chart::Args), - List(list::Args), - Revert(revert::Args), - Run(run::Args), -} - -impl Cmd { - pub async fn execute(self) -> eyre::Result<()> { - match self { - Cmd::Run(args) => run::run(args), - Cmd::List(args) => list::run(args), - Cmd::Chart(args) => chart::run(args).await, - Cmd::Revert(args) => revert::run(args), - } - } -} diff --git a/crates/cli/src/cmds/migrations/chart.rs b/crates/cli/src/cmds/migrations/chart.rs deleted file mode 100644 index 7154d9ef3..000000000 --- a/crates/cli/src/cmds/migrations/chart.rs +++ /dev/null @@ -1,244 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![allow(unused)] - -use azalia::remi::{core::StorageService as _, StorageService}; -use charted_config::Config; -use charted_types::{ - helm::{ChartIndex, ChartIndexSpec}, - name::Name, - Ulid, -}; -use clap::ValueEnum; -use eyre::{eyre, Context}; -use rayon::ThreadPoolBuilder; -use std::{fmt::Display, fs::File, path::PathBuf, process::exit}; -use tokio::runtime::Handle; -use tracing::{error, info, instrument, trace, warn}; -use url::Url; - -use crate::util; - -#[derive(Debug, Clone, Copy, Default, PartialEq, ValueEnum)] -pub enum RepoOwnerKind { - #[default] - User, - Organization, -} - -impl Display for RepoOwnerKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - RepoOwnerKind::User => f.write_str("user"), - RepoOwnerKind::Organization => f.write_str("organization"), - } - } -} - -/// Migrate a chart index from the internet into charted-server. -/// -/// Since charts are repositories, this will create a user/organization account -/// by the name given and will store the `index.yaml` downloaded into its metadata -/// with all URLs to point to us. -#[derive(Debug, Clone, clap::Parser)] -pub struct Args { - /// URL that points to a valid index. - /// - /// To use a file, use the `file://` scheme. - url: Url, - - /// The name of the user/organization that will be created - /// that owns the charts. - /// - /// By default, it'll use the hostname as the user or organization name. - name: Option, - - /// Owner type, which can be a `User` or `Organization` - #[arg(short = 'k', long, default_value_t = RepoOwnerKind::User)] - kind: RepoOwnerKind, - - /// location to a relative/absolute path to a configuration file. by default, this will locate - /// in `./config/charted.yml`/`./config.yml` if found. - #[arg(short = 'c', long, env = "CHARTED_CONFIG_FILE")] - config: Option, - - /// Amount of workers to use when doing filesystem operations. - /// - /// This is used to spawn multiple threads if the Helm index is really large, but it might - /// be useful to set this to `1` if it is a relatively small index. - #[clap(long, short = 'w', env = "CHARTED_RUNTIME_WORKERS", default_value_t = num_cpus::get())] - workers: usize, -} - -// Credit for the `spawn_handler` code: -// https://users.rust-lang.org/t/can-rayon-and-tokio-cooperate/85022/3 -/// This method is invoked by [`run`] to build a global Rayon pool to perform -/// concurrent Tokio tasks. -fn build_rayon_pool(workers: usize) -> eyre::Result<()> { - ThreadPoolBuilder::new() - .num_threads(workers) - .panic_handler(|msg| { - let msg = azalia::message_from_panic(msg); - - error!(%msg, "rayon thread panicked"); - }) - .thread_name(|idx| format!("charted-rayon-pool[#{idx}]")) - .spawn_handler(|thread| { - let rt = Handle::current(); - let mut b = std::thread::Builder::new(); - if let Some(name) = thread.name() { - b = b.name(name.to_owned()); - } - - if let Some(stack_size) = thread.stack_size() { - b = b.stack_size(stack_size); - } - - b.spawn(move || { - let _guard = rt.enter(); - thread.run() - })?; - - Ok(()) - }) - .build_global() - .context("failed to build global rayon pool") -} - -fn build_http_client() -> eyre::Result { - reqwest::ClientBuilder::new() - .user_agent(format!( - "Noelware/charted CLI (https://github.com/charted-dev/charted; {})", - charted_core::version() - )) - .use_rustls_tls() - .build() - .context("failed to build HTTP client") -} - -pub async fn run( - Args { - mut url, - workers, - config, - kind, - name, - }: Args, -) -> eyre::Result<()> { - build_rayon_pool(workers)?; - - trace!("building server configuration..."); - let config = util::load_config(config)?; - - trace!("building data storage..."); - let app = charted_app::Context::new(config).await?; - charted_helm_charts::init(&app.storage).await?; - - let name = match name { - Some(name) => name, - None => match url.host_str() { - Some(host) => host.parse()?, - None => { - error!("unable to infer name from URI, please specify it as the second argument"); - exit(1); - } - }, - }; - - let id = create_account(&app, kind, &name).await?; - let chart: ChartIndex = match url.scheme() { - "https" | "http" => { - let http = build_http_client()?; - - // If it doesn't end with `index.yaml`, then append it - if !url.path().ends_with("index.yaml") { - if url.path().ends_with('/') { - url = url.join("index.yaml")?; - } else { - url.set_path(&format!("{}/index.yaml", url.path())); - } - } - - info!(%url, "attempting to get chart index"); - let resp = http - .execute(http.get(url.clone()).build().context("failed to build HTTP request")?) - .await?; - - if !resp.status().is_success() { - error!("received status code {} with URL {}", resp.status(), url); - exit(1); - } - - let bytes = resp.bytes().await?; - serde_yaml_ng::from_slice(&bytes)? - } - - "file" => { - let path = url.path(); - let file = File::open(path).with_context(|| format!("failed to open file {path}"))?; - - serde_yaml_ng::from_reader(file)? - } - - scheme => return Err(eyre!("unsupported scheme: {scheme}")), - }; - - info!("collected {} charts to dump!", chart.entries.len()); - - let base_url = app.config.base_url.as_ref().unwrap(); - for (name, specs) in chart.entries { - if let Err(e) = dump_chart(&app.storage, base_url, &name, &specs).await { - error!(error = %e, %name, "failed to dump chart, skipping"); - } - } - - Ok(()) -} - -#[instrument(name = "charted.helm.createOwner", skip(ctx))] -async fn create_account(ctx: &charted_app::Context, kind: RepoOwnerKind, name: &Name) -> eyre::Result { - // If this is a single user registry AND there is not a user, then we will - // create it and not allow any other users to be created. - if ctx.config.single_user { - warn!("this instance is a single user registry, so I will perform a check if any users exist."); - warn!("if not, then the migration will perform as normal and the {name} user will be created with 'changeme' as the password. otherwise, this operation will fail."); - } - - // Otherwise, if this is a single organization registry AND there is not an organization, - // then we will create the organization - if ctx.config.single_org && kind == RepoOwnerKind::Organization { - warn!("this instance is a single organization registry, so I will perform a check if any organizations already exist"); - warn!("if not, then the migration will perform as normal and the {name} organization will be created. Otherwise, this operation will fail."); - } - - todo!() -} - -#[instrument( - name = "charted.helm.migrate", - skip_all, - fields(index.name = name) -)] -async fn dump_chart(storage: &StorageService, base: &Url, name: &str, specs: &[ChartIndexSpec]) -> eyre::Result<()> { - // First, we need to create the repository. - - info!("performing dump with {} index specifications", specs.len()); - for mut spec in specs { - dbg!(&spec.urls); - } - - Ok(()) -} diff --git a/crates/cli/src/cmds/migrations/list.rs b/crates/cli/src/cmds/migrations/list.rs deleted file mode 100644 index d8fb6d6f3..000000000 --- a/crates/cli/src/cmds/migrations/list.rs +++ /dev/null @@ -1,132 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::util; -use charted_config::database::Config; -use cli_table::{format::Justify, Cell, Style, Table}; -use diesel::{ - backend::Backend, - migration::{Migration, MigrationSource, MigrationVersion}, - pg::Pg, - sqlite::Sqlite, -}; -use diesel_migrations::MigrationHarness; -use eyre::{eyre, Context}; -use std::path::PathBuf; -use tracing::warn; - -#[derive(Table)] -struct CliTable { - #[table(title = "Name", justify = "Justify::Left")] - name: String, - - #[table(title = "Applied")] - applied: &'static str, -} - -/// Lists all database migrations -#[derive(Debug, Clone, clap::Parser)] -pub struct Args { - /// location to a relative/absolute path to a configuration file. by default, this will locate - /// in `./config/charted.hcl`/`./config.hcl` if found. - #[arg(short = 'c', long, env = "CHARTED_CONFIG_FILE")] - config: Option, -} - -// Code for this is from diesel's implementation of `diesel migration list`: -// https://github.com/diesel-rs/diesel/blob/2a3e7757af05fda4f3cb56f41008171a151cc223/diesel_cli/src/migrations/mod.rs#L257-L283 - -pub fn run(Args { config }: Args) -> eyre::Result<()> { - let mut config = util::load_config(config)?; - - // Don't run pending migrations even if there is some already pending! - config.database = match config.database { - Config::PostgreSQL(mut cfg) => { - cfg.run_migrations = false; - Config::PostgreSQL(cfg) - } - - Config::SQLite(mut cfg) => { - cfg.run_migrations = false; - Config::SQLite(cfg) - } - }; - - let pool = charted_app::create_db_pool(&config)?; - let mut conn = pool.get().context("failed to grab db connection")?; - let applied = charted_database::connection!(@raw conn { - PostgreSQL(conn) => conn - .applied_migrations() - .map_err(|e| eyre!("failed to get migrations from db: {e}")) - .into_iter() - .flatten() - .collect::>(); - - SQLite(conn) => conn - .applied_migrations() - .map_err(|e| eyre!("failed to get migrations from db: {e}")) - .into_iter() - .flatten() - .collect::>(); - }); - - match config.database { - Config::PostgreSQL(_) => { - let mut migrations = - MigrationSource::::migrations(&charted_database::migrations::POSTGRESQL_MIGRATIONS) - .map_err(|e| eyre!("failed to collect migrations: {e}"))?; - - print_migrations(&mut migrations, applied); - } - - Config::SQLite(_) => { - let mut migrations = - MigrationSource::::migrations(&charted_database::migrations::SQLITE_MIGRATIONS) - .map_err(|e| eyre!("failed to collect migrations: {e}"))?; - - print_migrations(&mut migrations, applied); - } - } - - Ok(()) -} - -fn print_migrations(migrations: &mut [Box>], applied: Vec>) { - migrations.sort_unstable_by(|a, b| a.name().version().cmp(&b.name().version())); - let mut has_pending = false; - let mut cells = Vec::with_capacity(migrations.len()); - - for migration in migrations { - let name = migration.name(); - if !applied.contains(&name.version()) { - has_pending = true; - } - - cells.push(CliTable { - name: name.to_string(), - applied: if applied.contains(&name.version()) { "Yes" } else { "No" }, - }); - } - - if has_pending { - warn!("you have pending migrations! please run `charted migrate run` to run them!"); - } - - let _ = cli_table::print_stdout( - cells - .table() - .title(["Name".cell().bold(true), "Applied?".cell().bold(true)]), - ); -} diff --git a/crates/cli/src/cmds/migrations/revert.rs b/crates/cli/src/cmds/migrations/revert.rs deleted file mode 100644 index b66ddf79e..000000000 --- a/crates/cli/src/cmds/migrations/revert.rs +++ /dev/null @@ -1,96 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::util; -use diesel::{backend::Backend, migration::MigrationSource}; -use diesel_migrations::{MigrationError, MigrationHarness}; -use eyre::{eyre, Context}; -use std::path::PathBuf; -use tracing::info; - -/// Reverts `N` or all database migrations. -#[derive(Debug, Clone, clap::Parser)] -pub struct Args { - /// Reverts `n` amount of migrations. - #[arg(default_value_t = 1, conflicts_with = "all")] - amount: u64, - - /// location to a relative/absolute path to a configuration file. by default, this will locate - /// in `./config/charted.hcl`/`./config.hcl` if found. - #[arg(short = 'c', long, env = "CHARTED_CONFIG_FILE")] - config: Option, - - /// If all migrations should be reverted. - #[arg(short = 'a', long, default_value_t = false, conflicts_with = "amount")] - all: bool, -} - -// Code for this is from diesel's implementation of `diesel migration revert`: -// https://github.com/diesel-rs/diesel/blob/2a3e7757af05fda4f3cb56f41008171a151cc223/diesel_cli/src/migrations/mod.rs#L34-L62 - -pub fn run(Args { config, amount, all }: Args) -> eyre::Result<()> { - let config = util::load_config(config)?; - let pool = charted_app::create_db_pool(&config)?; - let mut conn = pool.get().context("failed to get db connection")?; - - if all { - info!("reverting all database migrations to a fresh state!"); - - charted_database::connection!(@raw conn { - PostgreSQL(conn) => revert_all(conn, charted_database::migrations::POSTGRESQL_MIGRATIONS); - SQLite(conn) => revert_all(conn, charted_database::migrations::SQLITE_MIGRATIONS); - })?; - - return Ok(()); - } - - for _ in 0..amount { - match charted_database::connection!(@raw conn { - PostgreSQL(conn) => revert_last(conn, charted_database::migrations::POSTGRESQL_MIGRATIONS); - SQLite(conn) => revert_last(conn, charted_database::migrations::SQLITE_MIGRATIONS); - }) { - Ok(()) => {} - Err(e) if e.is::() => { - match e.downcast_ref::() { - // If `amount` was higher than the migrations that were - // reverted, then break out of the loop. - Some(MigrationError::NoMigrationRun) => break, - _ => return Err(eyre!("failed to revert last migration: {e}")), - } - } - - Err(e) => return Err(eyre!("failed to revert last migration: {e}")), - } - } - - Ok(()) -} - -fn revert_all, S: MigrationSource, DB: Backend>( - harness: &mut H, - source: S, -) -> eyre::Result<()> { - harness - .revert_all_migrations(source) - .map(|_| ()) - .map_err(|e| eyre!("failed to revert all migrations: {e}")) -} - -fn revert_last, S: MigrationSource, DB: Backend>( - harness: &mut H, - source: S, -) -> Result<(), Box> { - harness.revert_last_migration(source).map(|_| ()) -} diff --git a/crates/cli/src/cmds/migrations/run.rs b/crates/cli/src/cmds/migrations/run.rs deleted file mode 100644 index 750cb0158..000000000 --- a/crates/cli/src/cmds/migrations/run.rs +++ /dev/null @@ -1,70 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::util; -use charted_config::database::Config; -use diesel::{backend::Backend, migration::MigrationSource}; -use diesel_migrations::MigrationHarness; -use eyre::{eyre, Context}; -use std::path::PathBuf; -use tracing::info; - -/// Runs all the pending migrations. -#[derive(Debug, Clone, clap::Parser)] -pub struct Args { - /// location to a relative/absolute path to a configuration file. by default, this will locate - /// in `./config/charted.hcl`/`./config.hcl` if found. - #[arg(short = 'c', long, env = "CHARTED_CONFIG_FILE")] - config: Option, -} - -pub fn run(Args { config }: Args) -> eyre::Result<()> { - info!("running all pending migrations!"); - - let mut config = util::load_config(config)?; - - // Don't run pending migrations as this command will run pending migrations. - config.database = match config.database { - Config::PostgreSQL(mut cfg) => { - cfg.run_migrations = false; - Config::PostgreSQL(cfg) - } - - Config::SQLite(mut cfg) => { - cfg.run_migrations = false; - Config::SQLite(cfg) - } - }; - - let pool = charted_app::create_db_pool(&config)?; - let mut conn = pool.get().context("failed to get db connection")?; - - charted_database::connection!(@raw conn { - PostgreSQL(conn) => run_all_migrations(conn, charted_database::migrations::POSTGRESQL_MIGRATIONS); - SQLite(conn) => run_all_migrations(conn, charted_database::migrations::SQLITE_MIGRATIONS); - })?; - - Ok(()) -} - -fn run_all_migrations, H: MigrationHarness>( - harness: &mut H, - source: S, -) -> eyre::Result<()> { - harness - .run_pending_migrations(source) - .map(|_| ()) - .map_err(|e| eyre!("failed to run pending migrations: {e}")) -} diff --git a/crates/cli/src/cmds/server.rs b/crates/cli/src/cmds/server.rs deleted file mode 100644 index 86ff9ee17..000000000 --- a/crates/cli/src/cmds/server.rs +++ /dev/null @@ -1,161 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use azalia::log::{ - writers::{self, default::Writer}, - WriteLayer, -}; -use charted_config::Config; -use charted_core::Distribution; -use owo_colors::{OwoColorize, Stream::Stdout}; -use std::{ - borrow::Cow, - io::{self, Write as _}, - path::PathBuf, -}; -use tracing::info; -use tracing::level_filters::LevelFilter; -use tracing_subscriber::prelude::*; - -use crate::util; - -/// Runs the API server. -#[derive(Debug, Clone, clap::Parser)] -pub struct Args { - /// location to a relative/absolute path to a configuration file. by default, this will locate - /// in `./config/charted.hcl`/`./config.hcl` if found. - #[arg(short = 'c', long, env = "CHARTED_CONFIG_FILE")] - config: Option, - - /// amount of workers to spawn for the Tokio runtime. This cannot exceeded - /// the amount of CPU cores you have. - #[arg(short = 'w', long, env = "CHARTED_RUNTIME_WORKERS", default_value_t = num_cpus::get())] - pub workers: usize, -} - -pub async fn run(Args { config, .. }: Args) -> eyre::Result<()> { - print_banner(); - - let config = util::load_config(config)?; - let _guard = sentry::init(sentry::ClientOptions { - attach_stacktrace: true, - server_name: Some(Cow::Borrowed("charted-server")), - release: Some(Cow::Borrowed(charted_core::version())), - dsn: config.sentry_dsn.clone(), - - ..Default::default() - }); - - init_logger(&config); - info!("initializing systems..."); - - let cx = charted_app::Context::new(config).await?; - charted_helm_charts::init(&cx.storage).await?; - - charted_server::start(cx).await -} - -fn init_logger(config: &Config) { - tracing_subscriber::registry() - .with( - match config.logging.json { - false => WriteLayer::new_with(io::stdout(), Writer::default()), - true => WriteLayer::new_with(io::stdout(), writers::json), - } - .with_filter(LevelFilter::from_level(config.logging.level)) - .with_filter(tracing_subscriber::filter::filter_fn(|meta| { - // disallow from getting logs from `tokio` since it doesn't contain anything - // useful to us - !meta.target().starts_with("tokio::") - })), - ) - .with(sentry_tracing::layer()) - .with(tracing_error::ErrorLayer::default()) - .init(); -} - -fn print_banner() { - let mut stdout = io::stdout().lock(); - let _ = writeln!( - stdout, - "{}", - "«~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~»" - .if_supports_color(Stdout, |x| x.fg_rgb::<134, 134, 134>()) - ); - - let _ = writeln!( - stdout, - "{} {} {} {} {}", - "«".if_supports_color(Stdout, |x| x.fg_rgb::<134, 134, 134>()), - "_".if_supports_color(Stdout, |x| x.fg_rgb::<212, 171, 216>()), - "_".if_supports_color(Stdout, |x| x.fg_rgb::<212, 171, 216>()), - "_".if_supports_color(Stdout, |x| x.fg_rgb::<212, 171, 216>()), - "»".if_supports_color(Stdout, |x| x.fg_rgb::<134, 134, 134>()) - ); - - let _ = writeln!( - stdout, - "{} {} {}", - "«".if_supports_color(Stdout, |x| x.fg_rgb::<134, 134, 134>()), - "___| |__ __ _ _ __| |_ ___ __| | ___ ___ _ ____ _____ _ __" - .if_supports_color(Stdout, |x| x.fg_rgb::<212, 171, 216>()), - "»".if_supports_color(Stdout, |x| x.fg_rgb::<134, 134, 134>()) - ); - - let _ = writeln!( - stdout, - "{} {} {}", - "«".if_supports_color(Stdout, |x| x.fg_rgb::<134, 134, 134>()), - "/ __| '_ \\ / _` | '__| __/ _ \\/ _` |_____/ __|/ _ \\ '__\\ \\ / / _ \\ '__|" - .if_supports_color(Stdout, |x| x.fg_rgb::<212, 171, 216>()), - "»".if_supports_color(Stdout, |x| x.fg_rgb::<134, 134, 134>()) - ); - - let _ = writeln!( - stdout, - "{} {} {}", - "«".if_supports_color(Stdout, |x| x.fg_rgb::<134, 134, 134>()), - "| (__| | | | (_| | | | || __/ (_| |_____\\__ \\ __/ | \\ V / __/ |" - .if_supports_color(Stdout, |x| x.fg_rgb::<212, 171, 216>()), - "»".if_supports_color(Stdout, |x| x.fg_rgb::<134, 134, 134>()) - ); - - let _ = writeln!( - stdout, - "{} {} {}", - "«".if_supports_color(Stdout, |x| x.fg_rgb::<134, 134, 134>()), - "\\___|_| |_|\\__,_|_| \\__\\___|\\__,_| |___/\\___|_| \\_/ \\___|_|" - .if_supports_color(Stdout, |x| x.fg_rgb::<212, 171, 216>()), - "»".if_supports_color(Stdout, |x| x.fg_rgb::<134, 134, 134>()) - ); - - let _ = writeln!( - stdout, - "{}", - "«~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~»" - .if_supports_color(Stdout, |x| x.fg_rgb::<134, 134, 134>()) - ); - - let _ = writeln!(stdout); - let distribution = Distribution::detect(); - - let _ = writeln!( - stdout, - "» Booting up {} {}, compiled with Rust {} on {distribution}", - "charted-server".if_supports_color(Stdout, |x| x.bold()), - charted_core::version().if_supports_color(Stdout, |x| x.bold()), - charted_core::RUSTC_VERSION.if_supports_color(Stdout, |x| x.bold()) - ); -} diff --git a/crates/cli/src/commands/admin.rs b/crates/cli/src/commands/admin.rs new file mode 100644 index 000000000..e1fc3c71f --- /dev/null +++ b/crates/cli/src/commands/admin.rs @@ -0,0 +1,29 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod user; + +/// Administrative commands. +#[derive(Debug, clap::Subcommand)] +pub enum Subcommand { + #[command(subcommand)] + User(user::Subcommand), +} + +pub async fn run(subcmd: Subcommand) -> eyre::Result<()> { + match subcmd { + Subcommand::User(subcmd) => user::run(subcmd).await, + } +} diff --git a/crates/devtools/src/main.rs b/crates/cli/src/commands/admin/user.rs similarity index 75% rename from crates/devtools/src/main.rs rename to crates/cli/src/commands/admin/user.rs index eeed137df..10ea37516 100644 --- a/crates/devtools/src/main.rs +++ b/crates/cli/src/commands/admin/user.rs @@ -13,14 +13,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -use charted_devtools::Program; -use clap::Parser; +mod create; +mod delete; +mod list; +mod make_admin; +mod make_vp; -fn main() -> eyre::Result<()> { - color_eyre::install()?; +/// User administrative commands. +#[derive(Debug, clap::Subcommand)] +pub enum Subcommand {} - let program = Program::parse(); - program.init_logging(); - - charted_devtools::commands::run(program.command) +pub async fn run(subcmd: Subcommand) -> eyre::Result<()> { + match subcmd {} } diff --git a/crates/helm-plugin/src/cmds/logout.rs b/crates/cli/src/commands/admin/user/create.rs similarity index 100% rename from crates/helm-plugin/src/cmds/logout.rs rename to crates/cli/src/commands/admin/user/create.rs diff --git a/crates/server/src/routing/v1/organization/avatar.rs b/crates/cli/src/commands/admin/user/delete.rs similarity index 100% rename from crates/server/src/routing/v1/organization/avatar.rs rename to crates/cli/src/commands/admin/user/delete.rs diff --git a/crates/server/src/routing/v1/repository/icons.rs b/crates/cli/src/commands/admin/user/list.rs similarity index 100% rename from crates/server/src/routing/v1/repository/icons.rs rename to crates/cli/src/commands/admin/user/list.rs diff --git a/crates/cli/src/commands/admin/user/make-admin.rs b/crates/cli/src/commands/admin/user/make-admin.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/cli/src/commands/admin/user/make-vp.rs b/crates/cli/src/commands/admin/user/make-vp.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/cli/src/commands/admin/user/make_admin.rs b/crates/cli/src/commands/admin/user/make_admin.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/cli/src/commands/admin/user/make_vp.rs b/crates/cli/src/commands/admin/user/make_vp.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/cli/src/cmds/completions.rs b/crates/cli/src/commands/completions.rs similarity index 82% rename from crates/cli/src/cmds/completions.rs rename to crates/cli/src/commands/completions.rs index 0dddc2472..60c1c8f58 100644 --- a/crates/cli/src/cmds/completions.rs +++ b/crates/cli/src/commands/completions.rs @@ -16,12 +16,14 @@ use crate::Program; use clap::CommandFactory; use clap_complete::{generate, Shell}; -use eyre::eyre; +use eyre::bail; use std::io; use tracing::trace; -#[derive(Debug, Clone, clap::Parser)] +/// Generates shell completions for a specified shell or from the `$SHELL` environment variable. +#[derive(Debug, clap::Parser)] pub struct Args { + /// Specified shell to generate completions from. shell: Option, } @@ -31,7 +33,7 @@ pub fn run(Args { shell }: Args) -> eyre::Result<()> { let Some(shell) = Shell::from_env() else { trace!("...it wasn't found or included invalid unicode"); - return Err(eyre!("tried to detect shell based off the `$SHELL` environment variable but wasn't found or included invalid unicode")); + bail!("cannot detect current shell based off `$SHELL` environment variable"); }; // re-run the command with a shell set diff --git a/crates/cli/src/commands/generate-config.rs b/crates/cli/src/commands/generate-config.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/server/src/routing/v1/repository/mod.rs b/crates/cli/src/commands/migrations.rs similarity index 78% rename from crates/server/src/routing/v1/repository/mod.rs rename to crates/cli/src/commands/migrations.rs index 8d64db898..4402a90ee 100644 --- a/crates/server/src/routing/v1/repository/mod.rs +++ b/crates/cli/src/commands/migrations.rs @@ -13,11 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::ServerContext; -use axum::Router; +mod list; +mod rollback; +mod run; -crate::macros::impl_list_response!(RepositoryListResponse as "Repository"); +/// Database migrations. +#[derive(Debug, clap::Subcommand)] +pub enum Subcommand {} -pub fn create_router() -> Router { - Router::new() +pub async fn run(subcmd: Subcommand) -> eyre::Result<()> { + match subcmd {} } diff --git a/crates/cli/src/cmds/admin/user/list.rs b/crates/cli/src/commands/migrations/list.rs similarity index 100% rename from crates/cli/src/cmds/admin/user/list.rs rename to crates/cli/src/commands/migrations/list.rs diff --git a/crates/cli/src/cmds/admin/user.rs b/crates/cli/src/commands/migrations/rollback.rs similarity index 100% rename from crates/cli/src/cmds/admin/user.rs rename to crates/cli/src/commands/migrations/rollback.rs diff --git a/crates/cli/src/cmds/admin/user/create.rs b/crates/cli/src/commands/migrations/run.rs similarity index 100% rename from crates/cli/src/cmds/admin/user/create.rs rename to crates/cli/src/commands/migrations/run.rs diff --git a/crates/cli/src/cmds/mod.rs b/crates/cli/src/commands/mod.rs similarity index 62% rename from crates/cli/src/cmds/mod.rs rename to crates/cli/src/commands/mod.rs index 2c78d0cac..b6ba09948 100644 --- a/crates/cli/src/cmds/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -16,23 +16,25 @@ mod admin; mod completions; mod migrations; -mod server; +pub mod server; -#[derive(Debug, Clone, clap::Subcommand)] -pub enum Cmd { +#[derive(Debug, clap::Subcommand)] +pub enum Subcommand { Completions(completions::Args), Server(server::Args), #[command(subcommand)] - Migrate(migrations::Cmd), + Admin(admin::Subcommand), + + #[command(subcommand)] + Migrations(migrations::Subcommand), } -impl Cmd { - pub async fn run(self) -> eyre::Result<()> { - match self { - Cmd::Server(args) => server::run(args).await, - Cmd::Migrate(cmd) => cmd.execute().await, - Cmd::Completions(args) => completions::run(args), - } +pub async fn execute(subcmd: Subcommand) -> eyre::Result<()> { + match subcmd { + Subcommand::Server(args) => server::run(args).await, + Subcommand::Migrations(subcmd) => migrations::run(subcmd).await, + Subcommand::Admin(subcmd) => admin::run(subcmd).await, + Subcommand::Completions(args) => completions::run(args), } } diff --git a/crates/cli/src/util.rs b/crates/cli/src/commands/server.rs similarity index 57% rename from crates/cli/src/util.rs rename to crates/cli/src/commands/server.rs index 18ddb1f79..c58834c05 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/commands/server.rs @@ -13,14 +13,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -use charted_config::Config; use std::path::PathBuf; -pub fn load_config(config: Option) -> eyre::Result { - config - .map(|path| Config::new(Some(path))) - .unwrap_or(match Config::get_default_conf_location_if_any() { - Ok(Some(path)) => Config::new(Some(path)), - _ => Config::new::<&str>(None), - }) +/// Runs the API server. +#[derive(Debug, clap::Parser)] +pub struct Args { + /// Path to a charted `config.toml` configuration file. + #[arg(long, short = 'c', env = "CHARTED_CONFIG_FILE")] + config: Option, + + /// Number of Tokio workers to use. + /// + /// By default, this will use the number of avaliable CPU cores on the system + /// itself. + #[arg(long, short = 'w', env = "TOKIO_WORKER_THREADS", default_value_t = num_cpus::get())] + pub workers: usize, +} + +pub(crate) async fn run(_: Args) -> eyre::Result<()> { + Ok(()) } diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 88e476702..c387f1c12 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -13,39 +13,48 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod cmds; -pub(crate) mod util; +pub mod commands; use azalia::log::{writers::default::Writer, WriteLayer}; -use clap::Parser; -use std::io; +use commands::Subcommand; +use std::{future::Future, io}; use tracing::{level_filters::LevelFilter, Level}; -use tracing_subscriber::prelude::*; -#[derive(Debug, Clone, Parser)] +#[derive(Debug, clap::Parser)] #[clap( bin_name = "charted", - about = "🐻‍❄️📦 charted-server is a free, open source, and reliable Helm Chart registry made in Rust", + about = "🐻‍❄️📦 Free, open source, and reliable Helm Chart registry made in Rust", author = "Noelware, LLC. ", - override_usage = "charted [...ARGS...]", - arg_required_else_help = true + override_usage = "charted [ARGS]", + arg_required_else_help = true, + disable_version_flag = true )] pub struct Program { - /// Configures the log level for the logs that are transmitted by the CLI. This will not configure - /// the logger level for the `charted server` command. - #[arg(global(true), short = 'l', long = "log-level", default_value_t = Level::INFO)] + /// Configures the log level for all CLI commands. + /// + /// This will not configure the log level for the `server` subcommand. + #[arg( + global = true, + short = 'l', + long = "log-level", + default_value_t = Level::INFO, + env = "CHARTED_LOG_LEVEL" + )] pub level: Level, #[command(subcommand)] - pub command: cmds::Cmd, + pub command: Subcommand, } impl Program { + #[doc(hidden)] pub fn init_logger(&self) { + use tracing_subscriber::prelude::*; + tracing_subscriber::registry() .with( WriteLayer::new_with( - io::stdout(), + io::stderr(), Writer { print_module: false, print_thread: false, @@ -57,6 +66,11 @@ impl Program { ) .init(); } + + /// Runs the subcommand that was selected by the consumer. + pub fn execute(self) -> impl Future> { + commands::execute(self.command) + } } #[cfg(test)] diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 4f759c84d..78f7fe3f5 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -15,28 +15,28 @@ [package] name = "charted-client" -description = "🐻‍❄️📦 Fully fledged client-based library to interact with charted-server" +description = "🐻‍❄️📦 REST client for `charted-server`" version.workspace = true documentation.workspace = true edition.workspace = true homepage.workspace = true license.workspace = true -publish.workspace = true +publish = true repository.workspace = true authors.workspace = true +rust-version.workspace = true -[dependencies] -futures = "0.3.31" -progenitor-client = "0.9.0" -reqwest = { version = "0.12.8", features = ["json", "stream"] } -serde.workspace = true -serde_json.workspace = true +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(noeldoc)'] } -[build-dependencies] -prettyplease = "0.2.22" -progenitor = "0.9.0" -serde_json.workspace = true -syn = "2.0.79" +[features] +default = [] -[package.metadata.cargo-machete] -ignored = ["futures", "progenitor-client", "reqwest", "serde", "serde_json"] +[dependencies] +charted-core.workspace = true +charted-types.workspace = true +charted-helm-types.workspace = true +derive_more = { workspace = true, features = ["from", "display"] } +reqwest = { version = "0.12.12", features = ["json", "multipart"] } +url.workspace = true +tracing = { workspace = true, optional = true } diff --git a/crates/client/README.md b/crates/client/README.md index 7019b861d..d62856570 100644 --- a/crates/client/README.md +++ b/crates/client/README.md @@ -1,2 +1 @@ -# 🐻‍❄️📦 `charted_client` -The **charted_client** crate contains a full HTTP client to interact with **charted-server**'s HTTP service. +# 🐻‍❄️📦 `charted-client` diff --git a/crates/client/build.rs b/crates/client/build.rs deleted file mode 100644 index 76d578169..000000000 --- a/crates/client/build.rs +++ /dev/null @@ -1,36 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// use std::{ -// fs::{self, File}, -// path::PathBuf, -// }; - -const OPENAPI: &str = "../../assets/openapi.json"; - -fn main() { - println!("cargo::rerun-if-changed={OPENAPI}"); - println!("cargo::rerun-if-changed=build.rs"); - - // let file = File::open(OPENAPI).unwrap(); - // let spec = serde_json::from_reader(file).unwrap(); - - // let tokens = progenitor::Generator::default().generate_tokens(&spec).unwrap(); - // let ast = syn::parse2(tokens).unwrap(); - // let content = prettyplease::unparse(&ast); - - // let out = PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "src/generated.rs")); - // fs::write(out, content).unwrap(); -} diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs new file mode 100644 index 000000000..4122fe2a3 --- /dev/null +++ b/crates/client/src/client.rs @@ -0,0 +1,94 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{Error, Result}; +use charted_core::api; +use reqwest::{header::HeaderMap, Body, ClientBuilder, Method, Response}; +use url::Url; + +/// The default API endpoint. +pub const DEFAULT_API_ENDPOINT: &str = "https://charts.noelware.org/api/"; + +/// The default API version. +pub const DEFAULT_API_VERSION: api::Version = api::Version::V1; + +/// The actual client to use when sending requests. +#[derive(Debug, Clone)] +pub struct Client { + inner: reqwest::Client, + base: Url, +} + +impl Client { + /// Creates a new [`Client`] instance with a base URL. + pub fn new>(base: U, version: api::Version) -> Result { + let url: Url = base.try_into()?; + let base = url.join(&format!("/{version}"))?; + + Ok(Client { + inner: ClientBuilder::new().build()?, + base, + }) + } + + /// Replaces the default [`reqwest::Client`] with your own. + pub fn with_client>(self, client: C) -> Self { + Self { + inner: client.into(), + ..self + } + } + + #[cfg_attr(feature = "tracing", tracing::instrument(name = "charted.client.request", skip_all))] + pub async fn send>>( + &self, + method: Method, + endpoint: impl AsRef, + headers: Option, + body: B, + ) -> Result { + let endpoint = endpoint.as_ref(); + + #[cfg(feature = "tracing")] + ::tracing::debug!("<- {} {}", method, endpoint); + + let mut builder = self.inner.request(method, self.base.join(endpoint)?); + if let Some(headers) = headers { + builder = builder.headers(headers); + } + + if let Some(body) = body.into() { + builder = builder.body(body); + } + + builder.send().await.map_err(Error::Reqwest) + } +} + +/// The default implementation will use [`DEFAULT_API_ENDPOINT`] as the base. +impl Default for Client { + fn default() -> Self { + Self::new(DEFAULT_API_ENDPOINT, DEFAULT_API_VERSION).unwrap() + } +} + +impl From for Client { + fn from(value: reqwest::Client) -> Self { + Self { + inner: value, + ..Default::default() + } + } +} diff --git a/crates/helm-plugin/src/cmds/auth/mod.rs b/crates/client/src/error.rs similarity index 58% rename from crates/helm-plugin/src/cmds/auth/mod.rs rename to crates/client/src/error.rs index 043f43405..81bd79ee8 100644 --- a/crates/helm-plugin/src/cmds/auth/mod.rs +++ b/crates/client/src/error.rs @@ -13,26 +13,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod delete; -mod list; -mod switch; -mod token; +use std::io; -/// Subcommand to perform auth-related actions. -#[derive(Debug, Clone, clap::Subcommand)] -pub enum Cmd { - /// Switch to a different context when pulling or pushing Helm charts onto a registry. - Switch(switch::Args), +pub type Result = std::result::Result; - /// Lists all the different authentications avaliable - List(list::Args), +/// Representation of any error that could've occurred in this crate. +#[derive(Debug, derive_more::From, derive_more::Display)] +#[non_exhaustive] +pub enum Error { + ParseUrl(url::ParseError), + Reqwest(reqwest::Error), + Io(io::Error), } -impl Cmd { - pub fn run(self) -> eyre::Result<()> { +impl std::error::Error for Error { + fn cause(&self) -> Option<&dyn std::error::Error> { match self { - Cmd::List(args) => list::run(args), - Cmd::Switch(args) => switch::run(args), + Error::ParseUrl(err) => Some(err), + Error::Reqwest(err) => Some(err), + Error::Io(err) => Some(err), } } } diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index e69de29bb..91de0461f 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -0,0 +1,73 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # 🐻‍❄️📦 `charted_client` + +#![cfg_attr(any(noeldoc, docsrs), feature(doc_cfg))] +#![doc(html_logo_url = "https://cdn.floofy.dev/images/trans.png")] +#![doc(html_favicon_url = "https://cdn.floofy.dev/images/trans.png")] + +mod client; +mod error; +pub mod types; + +pub use client::*; +pub use error::*; + +// use charted_core::api; +// use reqwest::Url; + +// #[derive(Debug, Clone)] +// pub struct Client { +// version: api::Version, +// inner: reqwest::Client, +// base: Url, +// } + +// impl Client { +// /// Creates a new client instance. +// pub fn new(base: Url) -> Client { +// Client { +// version: api::Version::V1, +// inner: reqwest::ClientBuilder::new().build().unwrap(), +// base, +// } +// } + +// /// Sets the API version. +// pub fn with_api_version>(self, version: V) -> Self { +// Self { +// version: version.into(), +// ..self +// } +// } + +// pub fn with_base>(self, url: U) -> +// } + +// impl Default for Client { +// fn default() -> Self { +// Self::new(Url::parse("https://charts.noelware.org/api").unwrap()) +// } +// } + +// impl From for Client { +// fn from(value: reqwest::Client) -> Self { +// Self { +// inner: value, +// ..Default::default() +// } +// } +// } diff --git a/crates/client/src/types.rs b/crates/client/src/types.rs new file mode 100644 index 000000000..1aaf19c35 --- /dev/null +++ b/crates/client/src/types.rs @@ -0,0 +1,25 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! All of the avaliable types that the REST client uses. + +pub use charted_types::{ + name, payloads, ApiKey, ChartType, DateTime, Organization, OrganizationMember, Repository, RepositoryMember, + RepositoryRelease, Session, Ulid, User, UserConnections, Version, VersionReq, +}; + +pub use charted_core::api; +pub use charted_core::serde::Duration; +pub use charted_helm_types::*; diff --git a/crates/config/src/database.rs b/crates/config/src/database.rs deleted file mode 100644 index cce4b779c..000000000 --- a/crates/config/src/database.rs +++ /dev/null @@ -1,123 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod postgresql; -pub mod sqlite; - -use azalia::config::{env, merge::Merge, TryFromEnv}; -use eyre::eyre; -use serde::{Deserialize, Serialize}; -use std::{env::VarError, fmt::Display}; - -/// The `database {}` block allows to configure the database that charted-server -/// uses to store persistent data like users, repositories, and more. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum Config { - /// Uses [PostgreSQL] as the database driver. This is recommended - /// for production use. - /// - /// [PostgreSQL]: https://postgresql.org - PostgreSQL(postgresql::Config), - - /// Uses [SQLite] as the database driver. This is the recommended - /// driver for development and evaluation use or don't need - /// PostgreSQL running. - /// - /// [SQLite]: https://sqlite.org - SQLite(sqlite::Config), -} - -impl Merge for Config { - fn merge(&mut self, other: Self) { - match (self, other) { - (Self::PostgreSQL(psql1), Self::PostgreSQL(psql2)) => { - psql1.merge(psql2); - } - - (Self::SQLite(sqlite1), Self::SQLite(sqlite2)) => { - sqlite1.merge(sqlite2); - } - - (me, other) => { - *me = other; - } - } - } -} - -impl Default for Config { - fn default() -> Self { - Self::SQLite(sqlite::Config::default()) - } -} - -impl Display for Config { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Config::PostgreSQL(psql) => { - f.write_str("postgresql://")?; - - match (psql.username.as_ref(), psql.password.as_ref()) { - (Some(user), Some(pass)) => write!(f, "{user}:{pass}@")?, - (Some(user), None) => write!(f, "{user}:@")?, - (None, Some(pass)) => write!(f, "postgres:{pass}@")?, - _ => {} - } - - write!(f, "{}:{}/{}", psql.host, psql.port, psql.database) - } - - Config::SQLite(sqlite) => write!(f, "sqlite://{}", sqlite.db_path.display()), - } - } -} - -impl TryFromEnv for Config { - type Output = Config; - type Error = eyre::Report; - - fn try_from_env() -> Result { - match env!("CHARTED_DATABASE_DRIVER") { - Ok(s) => match &*s.to_ascii_lowercase() { - "postgresql" | "postgres" => Ok(Config::PostgreSQL(postgresql::Config::try_from_env()?)), - "sqlite" => Ok(Config::SQLite(sqlite::Config::try_from_env()?)), - s => Err(eyre!("unknown variant for `$CHARTED_DATABASE_DRIVER`: {s}")), - }, - - Err(VarError::NotPresent) => Ok(Config::SQLite(sqlite::Config::try_from_env()?)), - Err(VarError::NotUnicode(_)) => Err(eyre!( - "received non-unicode in `$CHARTED_DATABASE_DRIVER` environment variable" - )), - } - } -} - -impl Config { - /// Returns the amount of maximum connections the database pool can hold. - pub fn max_connections(&self) -> u32 { - match self { - Config::PostgreSQL(psql) => psql.max_connections, - Config::SQLite(sqlite) => sqlite.max_connections, - } - } - - pub fn can_run_migrations(&self) -> bool { - match self { - Config::PostgreSQL(psql) => psql.run_migrations, - Config::SQLite(sqlite) => sqlite.run_migrations, - } - } -} diff --git a/crates/config/src/database/postgresql.rs b/crates/config/src/database/postgresql.rs deleted file mode 100644 index e31c878c3..000000000 --- a/crates/config/src/database/postgresql.rs +++ /dev/null @@ -1,100 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::helpers; -use azalia::config::{env, merge::Merge, TryFromEnv}; -use serde::{Deserialize, Serialize}; - -/// ## `database "postgresql" {}` -/// -/// This database driver will use [PostgreSQL](https://postgresql.org). This driver -/// is recommended to be used for production use cases for better reliability. -#[derive(Debug, Clone, Merge, Serialize, Deserialize)] -pub struct Config { - /// Maximum amount of connections that the database pool can hold. - #[serde(default = "__max_connections")] - pub max_connections: u32, - - /// whether if migrations should be ran on startup. By default, this is `false`. - /// - /// You can use the `charted migrations run` command to run all migrations. - #[serde(default)] - #[merge(strategy = azalia::config::merge::strategy::bool::only_if_falsy)] - pub run_migrations: bool, - - /// The password to use for authentication. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub password: Option, - - /// The username to use for authentication - #[serde(default, skip_serializing_if = "Option::is_none")] - pub username: Option, - - /// Database name to use when connecting. - #[serde(default = "__database")] - pub database: String, - - /// Database schema to select when querying objects. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub schema: Option, - - /// Database host to connect to. - #[serde(default = "__host")] - pub host: String, - - /// Database port to connect to. - #[serde(default = "__port")] - pub port: u16, -} - -impl TryFromEnv for Config { - type Output = Config; - type Error = eyre::Report; - - fn try_from_env() -> Result { - Ok(Config { - max_connections: helpers::env_from_str("CHARTED_DATABASE_MAX_CONNECTIONS", __max_connections())?, - run_migrations: helpers::env_from_result( - env!("CHARTED_DATABASE_RUN_MIGRATIONS").map(|x| azalia::TRUTHY_REGEX.is_match(&x)), - false, - )?, - - password: env!("CHARTED_DATABASE_PASSWORD").ok(), - username: env!("CHARTED_DATABASE_USERNAME").ok(), - database: helpers::env_from_result(env!("CHARTED_DATABASE_NAME"), String::from("charted"))?, - schema: env!("CHARTED_DATABASE_SCHEMA").ok(), - host: helpers::env_from_result(env!("CHARTED_DATABASE_HOST"), __host())?, - port: helpers::env_from_str("CHARTED_DATABASE_PORT", __port())?, - }) - } -} - -const fn __max_connections() -> u32 { - 10 -} - -#[inline] -fn __database() -> String { - String::from("charted") -} - -#[inline] -fn __host() -> String { - String::from("localhost") -} - -const fn __port() -> u16 { - 5432 -} diff --git a/crates/config/src/features.rs b/crates/config/src/features.rs deleted file mode 100644 index aa088d2c5..000000000 --- a/crates/config/src/features.rs +++ /dev/null @@ -1,74 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod auditlog; -pub mod gc; -pub mod oci; -pub mod totp; -pub mod webhooks; - -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum Config { - /// Enables the garbage collection feature. - /// - /// The **garbage collection** feature allows the API server to collect objects - /// from the database and delete objects based off conditions that are defined - /// in the configuration. - #[serde(rename = "gc")] - GarbageCollection, - - /// Enables the audit logging feature. - /// - /// **NOTE** — This will require a configured [ClickHouse] connection as all - /// audit logs will be written to ClickHouse. - /// - /// The **audit logging** feature allows introspection of API server requests - /// from authenticated users, repository members, and organization members. - /// - /// [ClickHouse]: https://clickhouse.com - AuditLogging, - - /// Enables the HTTP webhooks feature. - /// - /// **NOTE** — This will require a configured [ClickHouse] connection as all - /// audit logs will be written to ClickHouse. - /// - /// The **webhooks** feature implements the [HTTP Standard Webhooks Specification] to allow - /// receiving events from data that has been modified by the API server. - /// - /// [HTTP Standard Webhooks Specification]: https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md - /// [ClickHouse]: https://clickhouse.com - Webhooks, - - /// Enables the TOTP (time-based one time password) feature. - /// - /// This feature is similar to two-factor authentication. - Totp, - - /// Enables the OCI Registry feature. - /// - /// **NOTE** — If this feature is enabled, you can still use the REST API to download - /// Helm charts as well. The Helm plugin supports both OCI and REST API. - /// - /// This feature will ensure that **charted-server** will act like a OCI Registry - /// based off the [Registry v1.1.0 Specification]. Techincally, if this feature - /// is enabled, **charted-server** can host both Helm charts and Docker containers. - /// - /// [Registry v1.1.0 Specification]: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md - OCI, -} diff --git a/crates/config/src/features/totp.rs b/crates/config/src/features/totp.rs deleted file mode 100644 index c55c364dd..000000000 --- a/crates/config/src/features/totp.rs +++ /dev/null @@ -1,31 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use serde::{Deserialize, Serialize}; - -/// Represents the configuration for the TOTP feature. -/// -/// A different secret is used than the JWT signing key is for security reasons. -/// -/// ## Example -/// ```hcl -/// feature "totp" { -/// secret = "" -/// } -/// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - pub secret: String, -} diff --git a/crates/config/src/helpers.rs b/crates/config/src/helpers.rs deleted file mode 100644 index a75b9cf7a..000000000 --- a/crates/config/src/helpers.rs +++ /dev/null @@ -1,51 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use eyre::eyre; -use std::{env::VarError, fmt::Display, str::FromStr}; - -pub fn env_from_result(res: Result, default: T) -> eyre::Result { - match res { - Ok(value) => Ok(value), - Err(VarError::NotPresent) => Ok(default), - Err(VarError::NotUnicode(_)) => Err(eyre!("received non-unicode in environment variable")), - } -} - -pub fn env_from_str(key: &str, default: F) -> eyre::Result { - match azalia::config::env!(key) { - Ok(value) => value - .parse::() - .map_err(|_| eyre!("failed to parse environment variable `${key}`")), - - Err(VarError::NotPresent) => Ok(default), - Err(VarError::NotUnicode(_)) => Err(eyre!("received non-unicode in `${}` environment variable", key)), - } -} - -pub fn env_optional_from_str(key: &str, default: Option) -> eyre::Result> -where - F::Err: Display, -{ - match azalia::config::env!(key) { - Ok(value) => value - .parse::() - .map(Some) - .map_err(|e| eyre!("failed to parse environment variable `${key}`: {e}")), - - Err(VarError::NotPresent) => Ok(default), - Err(VarError::NotUnicode(_)) => Err(eyre!("received non-unicode in `${}` environment variable", key)), - } -} diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs deleted file mode 100644 index 99a7ae460..000000000 --- a/crates/config/src/lib.rs +++ /dev/null @@ -1,194 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use azalia::{ - config::{env, merge::Merge, FromEnv, TryFromEnv}, - TRUTHY_REGEX, -}; -use sentry_types::Dsn; -use serde::{Deserialize, Serialize}; -use std::{ - env::VarError, - fs::File, - io::Read, - path::{Path, PathBuf}, -}; -use url::Url; - -pub(crate) mod helpers; - -pub mod database; -pub mod features; -pub mod logging; -pub mod metrics; -pub mod server; -pub mod sessions; -pub mod storage; - -#[derive(Debug, Clone, Default, Serialize, Deserialize, Merge)] -pub struct Config { - /// Base URL to point Helm chart objects to. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub base_url: Option, - - /// whether or not if users can be registered on this instance - #[serde(default = "__truthy")] - #[merge(strategy = azalia::config::merge::strategy::bool::only_if_falsy)] - pub registrations: bool, - - /// Secret key for encoding JWT tokens. This must be set once and never touched again. - #[serde(default)] - #[merge(skip)] // don't even attempt to merge jwt secret keys - pub jwt_secret_key: String, - - #[serde(skip_serializing_if = "Option::is_none")] - pub sentry_dsn: Option, - - /// whether or not if the API server should act like a single organization, where most features - /// are disabled like repository/organization members and audit logging. - #[serde(default)] - #[merge(strategy = azalia::config::merge::strategy::bool::only_if_falsy)] - pub single_org: bool, - - /// whether or not if the API server should act like a single user, where *most* features - /// are disabled and only one user is allowed to roam. - /// - /// all publically available features like Audit Logging can be enabled but repository and - /// organization members are disabled. most endpoints will be also disabled. - #[serde(default)] - #[merge(strategy = azalia::config::merge::strategy::bool::only_if_falsy)] - pub single_user: bool, - - #[serde(default, serialize_with = "hcl::ser::labeled_block")] - pub database: database::Config, - - #[serde(default, serialize_with = "hcl::ser::labeled_block")] - pub storage: storage::Config, - - #[serde(default, serialize_with = "hcl::ser::block")] - pub logging: logging::Config, - - #[serde(default, serialize_with = "hcl::ser::block")] - pub server: server::Config, - - #[serde(default, serialize_with = "hcl::ser::block")] - pub sessions: sessions::Config, -} - -impl TryFromEnv for Config { - type Output = Config; - type Error = eyre::Report; - - fn try_from_env() -> Result { - Ok(Config { - jwt_secret_key: helpers::env_from_result(env!("CHARTED_JWT_SECRET_KEY"), __generated_secret_key())?, - registrations: env!("CHARTED_ENABLE_REGISTRATIONS", |val| TRUTHY_REGEX.is_match(&val); or true), - single_user: env!("CHARTED_SINGLE_USER", |val| TRUTHY_REGEX.is_match(&val); or false), - single_org: env!("CHARTED_SINGLE_ORG", |val| TRUTHY_REGEX.is_match(&val); or false), - sentry_dsn: helpers::env_optional_from_str("CHARTED_SENTRY_DSN", None)?, - base_url: helpers::env_optional_from_str("CHARTED_BASE_URL", None)?, - - database: database::Config::try_from_env()?, - sessions: sessions::Config::try_from_env()?, - logging: logging::Config::from_env(), - storage: storage::Config::try_from_env()?, - server: server::Config::try_from_env()?, - }) - } -} - -impl Config { - pub fn get_default_conf_location_if_any() -> eyre::Result> { - let config_dir = PathBuf::from("./config"); - if config_dir.is_dir() && config_dir.try_exists()? { - let hcl = config_dir.join("charted.hcl"); - if hcl.is_file() && hcl.try_exists()? { - return Ok(Some(hcl)); - } - } - - match azalia::config::env!("CHARTED_CONFIG_FILE").map(PathBuf::from) { - Ok(path) if path.try_exists()? && path.is_file() => Ok(Some(path)), - Ok(_) | Err(VarError::NotPresent) => { - let last_resort = PathBuf::from("./config.hcl"); - if last_resort.is_file() && last_resort.is_file() { - return Ok(Some(last_resort)); - } - - Ok(None) - } - - Err(e) => Err(eyre::eyre!(e)), - } - } - - pub fn new>(path: Option

) -> eyre::Result { - // priority: config file > env variables - let Some(path) = path.as_ref() else { - return Config::try_from_env(); - }; - - let path = path.as_ref(); - if !path.try_exists()? { - eprintln!( - "[charted :: WARN] file '{}' doesn't exist; using system environment variable instead", - path.display() - ); - return Config::try_from_env(); - } - - let mut config = Config::try_from_env()?; - let mut contents = String::new(); - - { - let mut file = File::open(path)?; - file.read_to_string(&mut contents)?; - } - - let file: Config = hcl::from_str(&contents)?; - config.merge(file); - - if config.jwt_secret_key.is_empty() { - let key = __generated_secret_key(); - eprintln!("[charted WARN] Missing a secret key for encoding JWT tokens, but I have generated one for you: {key} \ - Set this in the `CHARTED_JWT_SECRET_KEY` environment variable when loading the API server or in the `jwt_secret_key` in your `config.hcl` file. \ - If any other key replaces this, then all JWT tokens will no longer be able to be verified, so it is recommended to keep this safe somewhere"); - - config.jwt_secret_key = key; - } - - if config.base_url.is_none() { - let scheme = match config.server.ssl { - Some(_) => "https", - None => "http", - }; - - let url = Url::parse(&format!("{scheme}://{}", config.server.addr()))?; - eprintln!("[charted WARN] `base_url` was not configured properly! All URLs will be mapped to {url}."); - - config.base_url = Some(url); - } - - Ok(config) - } -} - -fn __generated_secret_key() -> String { - charted_core::rand_string(16) -} - -const fn __truthy() -> bool { - true -} diff --git a/crates/config/src/server.rs b/crates/config/src/server.rs deleted file mode 100644 index 983b01ad4..000000000 --- a/crates/config/src/server.rs +++ /dev/null @@ -1,102 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod ratelimits; -pub mod ssl; - -use crate::helpers; -use azalia::{ - config::{env, merge::Merge, TryFromEnv}, - TRUTHY_REGEX, -}; -use eyre::eyre; -use serde::{Deserialize, Serialize}; -use std::{env::VarError, net::SocketAddr}; - -#[derive(Debug, Clone, Merge, Serialize, Deserialize)] -pub struct Config { - /// Host to bind onto. `127.0.0.1` is for internal, `0.0.0.0` is for public. - #[serde(default = "__default_host")] - pub host: String, - - /// Port to listen on. - #[serde(default = "__default_port")] - pub port: u16, - - /// Configures the use of HTTPS on the server. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub ssl: Option, -} - -impl Default for Config { - fn default() -> Self { - Config { - ssl: None, - host: __default_host(), - port: __default_port(), - } - } -} - -impl Config { - pub fn addr(&self) -> SocketAddr { - format!("{}:{}", self.host, self.port).parse().unwrap() - } -} - -impl TryFromEnv for Config { - type Output = Config; - type Error = eyre::Report; - - fn try_from_env() -> Result { - Ok(Config { - host: helpers::env_from_result( - env!("CHARTED_SERVER_HOST"), - helpers::env_from_result(env!("HOST"), __default_host())?, - )?, - - port: match env!("CHARTED_SERVER_PORT") { - Ok(value) => value.parse::()?, - Err(VarError::NotPresent) => match env!("PORT") { - Ok(value) => value.parse::()?, - Err(VarError::NotPresent) => __default_port(), - Err(VarError::NotUnicode(_)) => Err(eyre!("received non-unicode in environment variable"))?, - }, - - Err(VarError::NotUnicode(_)) => Err(eyre!("received non-unicode in environment variable"))?, - }, - - ssl: { - let value = env!("CHARTED_SERVER_ENABLE_SSL", |val| TRUTHY_REGEX.is_match(&val); or false) - .then(ssl::Config::try_from_env); - - if let Some(res) = value { - Some(res?) - } else { - None - } - }, - }) - } -} - -#[inline] -fn __default_host() -> String { - String::from("0.0.0.0") -} - -const fn __default_port() -> u16 { - 3651 -} diff --git a/crates/config/src/storage.rs b/crates/config/src/storage.rs deleted file mode 100644 index 59c588522..000000000 --- a/crates/config/src/storage.rs +++ /dev/null @@ -1,294 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::helpers; -use azalia::{ - config::{env, merge::Merge, TryFromEnv}, - TRUTHY_REGEX, -}; -use eyre::{eyre, Context, Report}; -use remi_azure::{CloudLocation, Credential}; -use remi_s3::aws::s3::{ - config::Region, - types::{BucketCannedAcl, ObjectCannedAcl}, -}; -use serde::{Deserialize, Serialize}; -use std::{borrow::Cow, path::PathBuf, str::FromStr}; - -/// Configures the storage for holding external media and chart indexes. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum Config { - /// Uses the local filesystem to store external media and chart indexes. - Filesystem(azalia::remi::fs::StorageConfig), - - /// Uses Microsoft's [Azure Blob Storage](https://azure.microsoft.com/en-us/products/storage/blobs) to store - /// external media and chart indexes. - Azure(azalia::remi::azure::StorageConfig), - - /// Uses Amazon's Simple Storage Service (S3) service or a S3-compatible server to store - /// external media and chart indexes. - S3(azalia::remi::s3::StorageConfig), -} - -macro_rules! merge_tuple { - ($first:expr, $second:expr, copyable) => { - match ($first, $second) { - (Some(obj1), Some(obj2)) if obj1 != obj2 => { - $first = Some(obj2); - } - - (None, Some(obj)) => { - $first = Some(obj); - } - - _ => {} - } - }; - - ($first:expr, $second:expr) => { - match (&($first), &($second)) { - (Some(obj1), Some(obj2)) if obj1 != obj2 => { - $first = Some(obj2.clone()); - } - - (None, Some(obj)) => { - $first = Some(obj.clone()); - } - - _ => {} - } - }; -} - -impl Merge for Config { - fn merge(&mut self, other: Self) { - match (self, other) { - (Self::Filesystem(fs1), Self::Filesystem(fs2)) => { - fs1.directory.merge(fs2.directory); - } - - (Self::Azure(me), Self::Azure(other)) => { - me.container.merge(other.container); - - match (&me.location, &other.location) { - (CloudLocation::Public(acc1), CloudLocation::Public(acc2)) if acc1 != acc2 => { - me.location = CloudLocation::Public(acc2.clone()); - } - - (CloudLocation::China(acc1), CloudLocation::China(acc2)) if acc1 != acc2 => { - me.location = CloudLocation::China(acc2.clone()); - } - - ( - CloudLocation::Emulator { - address: addr1, - port: port1, - }, - CloudLocation::Emulator { - address: addr2, - port: port2, - }, - ) if addr1 != addr2 || port1 != port2 => { - me.location = CloudLocation::Emulator { - address: addr2.clone(), - port: *port2, - }; - } - - (_, other) => { - me.location = other.clone(); - } - } - - match (&me.credentials, &other.credentials) { - ( - Credential::AccessKey { - account: acc1, - access_key: ak1, - }, - Credential::AccessKey { account, access_key }, - ) if acc1 != account || access_key != ak1 => { - me.credentials = Credential::AccessKey { - account: account.clone(), - access_key: access_key.clone(), - }; - } - - (Credential::SASToken(token1), Credential::SASToken(token2)) if token1 != token2 => { - me.credentials = Credential::SASToken(token2.to_owned()); - } - - (Credential::Bearer(token1), Credential::Bearer(token2)) if token1 != token2 => { - me.credentials = Credential::SASToken(token2.to_owned()); - } - - (Credential::Anonymous, Credential::Anonymous) => {} - - // overwrite if they aren't the same at all - (_, other) => { - me.credentials = other.clone(); - } - }; - } - - (Self::S3(me), Self::S3(other)) => { - azalia::config::merge::strategy::bool::only_if_falsy( - &mut me.enable_signer_v4_requests, - other.enable_signer_v4_requests, - ); - - azalia::config::merge::strategy::bool::only_if_falsy( - &mut me.enforce_path_access_style, - other.enforce_path_access_style, - ); - - merge_tuple!(me.default_bucket_acl, other.default_bucket_acl); - merge_tuple!(me.default_object_acl, other.default_object_acl); - - me.secret_access_key.merge(other.secret_access_key); - me.access_key_id.merge(other.access_key_id); - - merge_tuple!(me.app_name, other.app_name); - merge_tuple!(me.endpoint, other.endpoint); - merge_tuple!(me.region, other.region); - - me.bucket.merge(other.bucket); - } - - (me, other) => { - *me = other; - } - } - } -} - -impl Default for Config { - fn default() -> Config { - Config::Filesystem(remi_fs::StorageConfig { - directory: PathBuf::from("./data"), - }) - } -} - -impl TryFromEnv for Config { - type Output = Config; - type Error = Report; - - fn try_from_env() -> Result { - match env!("CHARTED_STORAGE_SERVICE") { - Ok(res) => match res.to_lowercase().as_str() { - "filesystem" | "fs" => Ok(Config::Filesystem(remi_fs::StorageConfig { - directory: helpers::env_from_str("CHARTED_STORAGE_FILESYSTEM_DIRECTORY", PathBuf::from("./data"))?, - })), - - "azure" => Ok(Config::Azure(remi_azure::StorageConfig { - credentials: to_env_credentials()?, - location: to_env_location()?, - container: env!("CHARTED_STORAGE_AZURE_CONTAINER", optional).unwrap_or("charted".into()), - })), - - "s3" => Ok(Config::S3(remi_s3::StorageConfig { - enable_signer_v4_requests: env!("CHARTED_STORAGE_S3_ENABLE_SIGNER_V4_REQUESTS", |val| TRUTHY_REGEX.is_match(&val); or false), - enforce_path_access_style: env!("CHARTED_STORAGE_S3_ENFORCE_PATH_ACCESS_STYLE", |val| TRUTHY_REGEX.is_match(&val); or false), - default_object_acl: env!("CHARTED_STORAGE_S3_DEFAULT_OBJECT_ACL", |val| ObjectCannedAcl::from_str(val.as_str()).ok(); or Some(ObjectCannedAcl::BucketOwnerFullControl)), - default_bucket_acl: env!("CHARTED_STORAGE_S3_DEFAULT_OBJECT_ACL", |val| BucketCannedAcl::from_str(val.as_str()).ok(); or Some(BucketCannedAcl::AuthenticatedRead)), - - secret_access_key: env!("CHARTED_STORAGE_S3_SECRET_ACCESS_KEY") - .context("required env variable [CHARTED_STORAGE_S3_SECRET_ACCESS_KEY]")?, - - access_key_id: env!("CHARTED_STORAGE_S3_ACCESS_KEY_ID") - .context("required env variable [CHARTED_STORAGE_S3_ACCESS_KEY_ID]")?, - - app_name: env!("CHARTED_STORAGE_S3_APP_NAME", optional), - endpoint: env!("CHARTED_STORAGE_S3_ENDPOINT", optional), - prefix: env!("CHARTED_STORAGE_S3_PREFIX", optional), - region: env!("CHARTED_STORAGE_S3_REGION", |val| Some(Region::new(Cow::Owned(val))); or Some(Region::new(Cow::Owned("us-east-1".to_owned())))), - bucket: env!("CHARTED_STORAGE_S3_BUCKET", optional).unwrap_or("charted".into()), - })), - - loc => Err(eyre!("expected [filesystem/fs, azure, s3]; received '{loc}'")), - }, - Err(_) => Ok(Default::default()), - } - } -} - -fn to_env_credentials() -> eyre::Result { - match env!("CHARTED_STORAGE_AZURE_CREDENTIAL") { - Ok(res) => match res.as_str() { - "anonymous" | "anon" => Ok(Credential::Anonymous), - "accesskey" | "access_key" => Ok(Credential::AccessKey { - account: env!("CHARTED_STORAGE_AZURE_CREDENTIAL_ACCESSKEY_ACCOUNT") - .context("missing required env variable [CHARTED_STORAGE_AZURE_CREDENTIAL_ACCESSKEY_ACCOUNT]")?, - access_key: env!("CHARTED_STORAGE_AZURE_CREDENTIAL_ACCESSKEY") - .context("missing required env variable [CHARTED_STORAGE_AZURE_CREDENTIAL_ACCESSKEY]")?, - }), - - "sastoken" | "sas_token" => Ok(Credential::SASToken( - env!("CHARTED_STORAGE_AZURE_CREDENTIAL_SAS_TOKEN") - .context("missing required env variable [CHARTED_STORAGE_AZURE_CREDENTIAL_SAS_TOKEN]")?, - )), - - "bearer" => Ok(Credential::SASToken( - env!("CHARTED_STORAGE_AZURE_CREDENTIAL_BEARER") - .context("missing required env variable [CHARTED_STORAGE_AZURE_CREDENTIAL_BEARER]")?, - )), - - res => Err(eyre!( - "expected [anonymous/anon, accesskey/access_key, sastoken/sas_token, bearer]; received '{res}'" - )), - }, - Err(_) => Err(eyre!( - "missing required `CHARTED_STORAGE_AZURE_CREDENTIAL` env or was invalid utf-8" - )), - } -} - -fn to_env_location() -> eyre::Result { - match env!("CHARTED_STORAGE_AZURE_LOCATION") { - Ok(res) => match res.as_str() { - "public" => Ok(CloudLocation::Public( - env!("CHARTED_STORAGE_AZURE_ACCOUNT") - .context("missing required env [CHARTED_STORAGE_AZURE_ACCOUNT]")?, - )), - - "china" => Ok(CloudLocation::China( - env!("CHARTED_STORAGE_AZURE_ACCOUNT") - .context("missing required env [CHARTED_STORAGE_AZURE_ACCOUNT]")?, - )), - - "emulator" => Ok(CloudLocation::Emulator { - address: env!("CHARTED_STORAGE_AZURE_EMULATOR_ADDRESS") - .context("missing required env [CHARTED_STORAGE_AZURE_EMULATOR_ADDRESS]")?, - - port: helpers::env_from_str("CHARTED_STORAGE_AZURE_EMULATOR_PORT", 10000u16)?, - }), - - "custom" => Ok(CloudLocation::Custom { - account: env!("CHARTED_STORAGE_AZURE_ACCOUNT") - .context("missing required env [CHARTED_STORAGE_AZURE_ACCOUNT]")?, - - uri: env!("CHARTED_STORAGE_AZURE_URI").context("missing required env [CHARTED_STORAGE_AZURE_URI]")?, - }), - - loc => Err(eyre!("expected [public, china, emulator, custom]; received '{loc}'")), - }, - - Err(_) => Err(eyre!( - "missing required `CHARTED_STORAGE_AZURE_LOCATION` env or was invalid utf-8" - )), - } -} diff --git a/crates/config/Cargo.toml b/crates/configuration/Cargo.toml similarity index 75% rename from crates/config/Cargo.toml rename to crates/configuration/Cargo.toml index 8095e1e0b..fbdebf3e6 100644 --- a/crates/config/Cargo.toml +++ b/crates/configuration/Cargo.toml @@ -28,28 +28,28 @@ authors.workspace = true azalia = { workspace = true, features = [ "config", "config-derive", + "config-url", + + "serde", + "serde-tracing", + "remi", "remi-fs", "remi-s3", "remi-azure", - "serde", - "serde-tracing", -] } -charted-core = { version = "0.1.0", path = "../core", default-features = false, features = [ - "merge", + "remi-serde", + + "regex", + "lazy", ] } +charted-core.workspace = true +derive_more = { workspace = true, features = ["display", "deref"] } eyre.workspace = true -hcl-rs = "0.18.2" -remi-azure = { workspace = true, features = ["serde"] } -remi-fs = { workspace = true, features = ["serde"] } -remi-s3 = { workspace = true, features = ["serde"] } sentry-types = "0.36.0" serde.workspace = true +toml = "0.8.20" tracing.workspace = true url = { workspace = true, features = ["serde"] } -[package.metadata.cargo-machete] -ignored = [ - # the crate name is `hcl` but the name on crates.io is `hcl-rs`. - "hcl-rs", -] +[dev-dependencies] +derive_more = { workspace = true, features = ["deref"] } diff --git a/crates/configuration/src/database.rs b/crates/configuration/src/database.rs new file mode 100644 index 000000000..bcf33eea5 --- /dev/null +++ b/crates/configuration/src/database.rs @@ -0,0 +1,346 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod common; +pub mod postgresql; +pub mod sqlite; + +use azalia::config::{env, merge::Merge, TryFromEnv}; +use eyre::eyre; +use serde::{Deserialize, Serialize}; +use std::{env::VarError, fmt::Display, str::FromStr}; + +/// The `database` table allows to configure the database that charted-server +/// uses to store persistent data like users, repositories, and more. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Config { + /// Uses [PostgreSQL] as the database driver. This is recommended + /// for production use. + /// + /// [PostgreSQL]: https://postgresql.org + PostgreSQL(postgresql::Config), + + /// Uses [SQLite] as the database driver. This is the recommended + /// driver for development and evaluation use or don't need + /// PostgreSQL running. + /// + /// [SQLite]: https://sqlite.org + SQLite(sqlite::Config), +} + +impl Default for Config { + fn default() -> Self { + Config::SQLite(sqlite::Config::default()) + } +} + +impl Merge for Config { + fn merge(&mut self, other: Self) { + match (self, other) { + (Self::PostgreSQL(p1), Self::PostgreSQL(p2)) => { + p1.merge(p2); + } + + (Self::SQLite(s1), Self::SQLite(s2)) => { + s1.merge(s2); + } + + (me, other) => { + *me = other; + } + } + } +} + +impl Display for Config { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Config::PostgreSQL(psql) => Display::fmt(psql, f), + Config::SQLite(sqlite) => Display::fmt(sqlite, f), + } + } +} + +pub const DRIVER: &str = "CHARTED_DATABASE_DRIVER"; + +impl TryFromEnv for Config { + type Output = Config; + type Error = eyre::Report; + + fn try_from_env() -> Result { + match env!(DRIVER) { + Ok(s) => match &*s.to_ascii_lowercase() { + "postgresql" | "postgres" => Ok(Config::PostgreSQL(postgresql::Config::try_from_env()?)), + "sqlite" => Ok(Config::SQLite(sqlite::Config::try_from_env()?)), + s => Err(eyre!("unknown variant for `$CHARTED_DATABASE_DRIVER`: {s}")), + }, + + Err(VarError::NotPresent) => Ok(Config::SQLite(sqlite::Config::try_from_env()?)), + Err(VarError::NotUnicode(_)) => Err(eyre!("received non-unicode in `${}` environment variable", DRIVER)), + } + } +} + +impl Config { + pub fn common(&self) -> &common::Config { + match self { + Config::PostgreSQL(c) => &c.common, + Config::SQLite(c) => &c.common, + } + } + + #[cfg(test)] + pub(crate) fn as_sqlite(&self) -> Option<&sqlite::Config> { + match self { + Config::SQLite(c) => Some(c), + _ => None, + } + } + + #[cfg(test)] + pub(crate) fn as_postgresql(&self) -> Option<&postgresql::Config> { + match self { + Config::PostgreSQL(c) => Some(c), + _ => None, + } + } +} + +// another newtype wrapper around a newtype wrapper to implement `Merge` +// since Azalia is not avaliable in crates.io +// +// shouldn't be used outside of `charted-database`. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, derive_more::Display, derive_more::Deref)] +pub struct Duration(charted_core::serde::Duration); +impl Duration { + pub(crate) const fn from_secs(secs: u64) -> Self { + Self(charted_core::serde::Duration::from_secs(secs)) + } +} + +impl FromStr for Duration { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + charted_core::serde::Duration::from_str(s).map(Self) + } +} + +impl Merge for Duration { + fn merge(&mut self, other: Self) { + // Don't attempt to merge if both `me_std` and `other_std` are zero duration + if self.0.is_zero() && other.0.is_zero() { + return; + } + + // If we are a non-zero duration and `other` is a zero duration + // (i.e, from `Default::default`), then don't merge. + if !self.0.is_zero() && other.0.is_zero() { + return; + } + + *self = Duration(other.0); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use azalia::config::MultipleEnvGuard; + + // A test that is similar to `merge sqlite -> sqlite` but uses + // the actual system environment variables via `MultipleEnvGuard` + #[test] + fn merge_sqlite_to_sqlite_via_environment_variables() { + let _guard = MultipleEnvGuard::enter([ + (DRIVER, "sqlite"), + (common::MAX_CONNECTIONS, "30"), + (common::RUN_PENDING_MIGRATIONS, "yes"), + ]); + + let mut c1 = Config::default(); + let c2 = Config::try_from_env().expect("failed to parse configuration from system environment variables"); + + let old = c1.clone(); + + c1.merge(c2); + let Some(c1) = c1.as_sqlite() else { unreachable!() }; + let Some(old_c1) = old.as_sqlite() else { unreachable!() }; + + assert_eq!(c1.max_connections, 30); + assert!(c1.run_migrations); + assert_eq!(c1.path, old_c1.path); + } + + // Similar to the below test but loads it via the TOML configuration + // to see if it works correctly. + #[test] + #[ignore = "toml parsing is wack at the moment but it should work once this attr is removed"] + fn merge_sqlite_to_sqlite_via_configuration_file() { + #[derive(Deserialize, Default, derive_more::Deref)] + struct Config { + database: super::Config, + } + + const CONFIG: &str = r#""" + [database.sqlite] + max_connections = 30 + run_migrations = true + """#; + + let mut c1 = Config::default(); + let c2: Config = toml::from_str(CONFIG).unwrap(); + + let old = c1.clone(); + + c1.database.merge(c2.database); + let Some(c1) = c1.as_sqlite() else { unreachable!() }; + let Some(old_c1) = old.as_sqlite() else { unreachable!() }; + + assert_eq!(c1.max_connections, 30); + assert!(c1.run_migrations); + assert_eq!(c1.path, old_c1.path); + } + + // given configuration: + // + // $CHARTED_DATABASE_DRIVER = sqlite + // $CHARTED_DATABASE_MAX_CONNECTIONS = 30 + // $CHARTED_DATABASE_RUN_PENDING_MIGRATIONS = true + #[test] + fn merge_sqlite_to_sqlite() { + let mut c1 = Config::default(); + let c2 = Config::SQLite(sqlite::Config { + common: common::Config { + max_connections: 30, + run_migrations: true, + + ..Default::default() + }, + + path: c1.as_sqlite().unwrap().path.clone(), + }); + + let old = c1.clone(); + + c1.merge(c2); + let Some(c1) = c1.as_sqlite() else { unreachable!() }; + let Some(old_c1) = old.as_sqlite() else { unreachable!() }; + + assert_eq!(c1.max_connections, 30); + assert!(c1.run_migrations); + assert_eq!(c1.path, old_c1.path); + } + + // given configuration: + // $CHARTED_DATABASE_DRIVER = postgresql + // $CHARTED_DATABASE_MAX_CONNECTIONS = 100 + // $CHARTED_DATABASE_RUN_PENDING_MIGRATIONS = true + // $CHARTED_DATABASE_USERNAME = noel + #[test] + fn merge_psql_to_psql() { + let mut c1 = Config::PostgreSQL(postgresql::Config::default()); + let c2 = Config::PostgreSQL(postgresql::Config { + common: common::Config { + max_connections: 100, + run_migrations: true, + + ..Default::default() + }, + + username: Some(String::from("noel")), + ..Default::default() + }); + + let old = c1.clone(); + c1.merge(c2); + + let Some(c1) = c1.as_postgresql() else { unreachable!() }; + let Some(old_c1) = old.as_postgresql() else { + unreachable!() + }; + + assert_eq!(c1.max_connections, 100); + assert!(c1.run_migrations); + assert_eq!(c1.username, Some(String::from("noel"))); + assert_eq!(c1.password, old_c1.password); + assert_eq!(c1.database, old_c1.database); + assert_eq!(c1.schema, old_c1.schema); + assert_eq!(c1.url, old_c1.url); + } + + // A test that is similar to `merge sqlite -> sqlite` but uses + // the actual system environment variables via `MultipleEnvGuard` + #[test] + fn merge_psql_to_psql_via_environment_variables() { + let _guard = MultipleEnvGuard::enter([ + (DRIVER, "postgres"), + (common::MAX_CONNECTIONS, "100"), + (common::RUN_PENDING_MIGRATIONS, "yes"), + (postgresql::USERNAME, "noel"), + ]); + + let mut c1 = Config::PostgreSQL(postgresql::Config::default()); + let c2 = Config::try_from_env().expect("failed to parse configuration from system environment variables"); + + let old = c1.clone(); + c1.merge(c2); + + let Some(c1) = c1.as_postgresql() else { unreachable!() }; + let Some(old_c1) = old.as_postgresql() else { + unreachable!() + }; + + assert_eq!(c1.max_connections, 100); + assert!(c1.run_migrations); + assert_eq!(c1.username, Some(String::from("noel"))); + assert_eq!(c1.password, old_c1.password); + assert_eq!(c1.database, old_c1.database); + assert_eq!(c1.schema, old_c1.schema); + assert_eq!(c1.url, old_c1.url); + } + + // Similar to the below test but loads it via the TOML configuration + // to see if it works correctly. + #[test] + #[ignore = "toml parsing is wack at the moment but it should work once this attr is removed"] + fn merge_psql_to_psql_via_configuration_file() { + #[derive(Deserialize, Default, derive_more::Deref)] + struct Config { + database: super::Config, + } + + const _: &str = r#""" + [database.postgresql] + max_connections = 30 + run_migrations = true + url = "postgres://noel@noeliscutieuwu:localhost:5432/charted" + """#; + + // let mut c1 = Config::default(); + // let c2: Config = toml::from_str(CONFIG).unwrap(); + + // let old = c1.clone(); + + // c1.database.merge(c2.database); + // let Some(c1) = c1.as_sqlite() else { unreachable!() }; + // let Some(old_c1) = old.as_sqlite() else { unreachable!() }; + + // assert_eq!(c1.max_connections, 30); + // assert!(c1.run_migrations); + // assert_eq!(c1.db_path, old_c1.db_path); + } +} diff --git a/crates/configuration/src/database/common.rs b/crates/configuration/src/database/common.rs new file mode 100644 index 000000000..8f03895c7 --- /dev/null +++ b/crates/configuration/src/database/common.rs @@ -0,0 +1,108 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::Duration; +use crate::util; +use azalia::config::{merge::Merge, TryFromEnv}; +use serde::{Deserialize, Serialize}; + +pub const MAX_CONNECTIONS: &str = "CHARTED_DATABASE_MAX_CONNECTIONS"; +pub const RUN_PENDING_MIGRATIONS: &str = "CHARTED_DATABASE_RUN_PENDING_MIGRATIONS"; +pub const DATABASE: &str = "CHARTED_DATABASE_NAME"; +pub const ACQUIRE_TIMEOUT: &str = "CHARTED_DATABASE_ACQUIRE_TIMEOUT"; +pub const CONNECT_TIMEOUT: &str = "CHARTED_DATABASE_CONNECT_TIMEOUT"; +pub const IDLE_TIMEOUT: &str = "CHARTED_DATABASE_IDLE_TIMEOUT"; +pub const URL: &str = "CHARTED_DATABASE_URL"; + +/// Common configuration shared within each database. +#[derive(Debug, Clone, Serialize, Deserialize, Merge)] +pub struct Config { + /// Maximum amount of connections that the database pool can hold. + #[serde(default = "__max_connections")] + pub max_connections: u32, + + /// whether if migrations should be ran on startup. By default, this is `false`. + /// + /// You can use the `charted migrations run` command to run all migrations. + #[serde(default)] + #[merge(strategy = azalia::config::merge::strategy::bool::only_if_falsy)] + pub run_migrations: bool, + + /// Maximum amount of time to spend waiting when acquiring a new connection. + #[serde(default = "__acquire_timeout")] + pub acquire_timeout: Duration, + + /// Maximum amount of time to spend for connecting to the database. + #[serde(default = "__connect_timeout")] + pub connect_timeout: Duration, + + /// Maximum amount of time to idle until it is relinquished and can be re-used. + #[serde(default = "__idle_timeout")] + pub idle_timeout: Duration, + + /// The database name. + #[serde(default = "__database")] + pub database: String, +} + +impl Default for Config { + fn default() -> Self { + Config { + max_connections: __max_connections(), + acquire_timeout: __acquire_timeout(), + connect_timeout: __connect_timeout(), + run_migrations: false, + idle_timeout: __idle_timeout(), + database: __database(), + } + } +} + +impl TryFromEnv for Config { + type Output = Config; + type Error = eyre::Report; + + fn try_from_env() -> Result { + Ok(Config { + max_connections: util::env_from_str(MAX_CONNECTIONS, __max_connections())?, + acquire_timeout: util::env_from_str(ACQUIRE_TIMEOUT, __acquire_timeout())?, + connect_timeout: util::env_from_str(CONNECT_TIMEOUT, __connect_timeout())?, + run_migrations: util::bool_env(RUN_PENDING_MIGRATIONS)?, + idle_timeout: util::env_from_str(IDLE_TIMEOUT, __idle_timeout())?, + database: util::env_from_str(DATABASE, __database())?, + }) + } +} + +const fn __max_connections() -> u32 { + 10 +} + +const fn __acquire_timeout() -> Duration { + Duration::from_secs(30) +} + +const fn __connect_timeout() -> Duration { + Duration::from_secs(15) +} + +const fn __idle_timeout() -> Duration { + Duration::from_secs(120) +} + +#[inline] +fn __database() -> String { + String::from("charted") +} diff --git a/crates/configuration/src/database/postgresql.rs b/crates/configuration/src/database/postgresql.rs new file mode 100644 index 000000000..c69be0d4f --- /dev/null +++ b/crates/configuration/src/database/postgresql.rs @@ -0,0 +1,84 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::util; + +use super::common; +use azalia::config::{env, merge::Merge, TryFromEnv}; +use serde::{Deserialize, Serialize}; +use url::Url; + +/// ## `[database.postgresql]` +/// +/// This database driver will use [PostgreSQL](https://postgresql.org). This driver +/// is recommended to be used for production use cases and better reliability. +#[derive(Debug, Clone, Merge, Serialize, Deserialize, derive_more::Display, derive_more::Deref)] +#[display("{}", self.url)] +pub struct Config { + #[serde(flatten)] + #[deref] + pub common: common::Config, + + /// The password to use for authentication. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub password: Option, + + /// The username to use for authentication + #[serde(default, skip_serializing_if = "Option::is_none")] + pub username: Option, + + /// Database schema to select when querying objects. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub schema: Option, + + /// Database URL to connect to. + pub url: Url, +} + +impl Default for Config { + fn default() -> Self { + Config { + username: Default::default(), + password: Default::default(), + common: Default::default(), + schema: Default::default(), + url: Url::parse("postgresql://localhost:5432/charted?application_name=charted-server").unwrap(), + } + } +} + +pub const PASSWORD: &str = "CHARTED_DATABASE_PASSWORD"; +pub const USERNAME: &str = "CHARTED_DATABASE_USERNAME"; +pub const SCHEMA: &str = "CHARTED_DATABASE_SCHEMA"; +pub const HOST: &str = "CHARTED_DATABASE_HOST"; +pub const PORT: &str = "CHARTED_DATABASE_PORT"; + +impl TryFromEnv for Config { + type Output = Config; + type Error = eyre::Report; + + fn try_from_env() -> Result { + Ok(Config { + password: env!(PASSWORD).ok(), + username: env!(USERNAME).ok(), + schema: env!(SCHEMA).ok(), + common: common::Config::try_from_env()?, + url: util::env_from_str( + common::URL, + Url::parse("postgresql://localhost:5432/charted?application_name=charted-server").unwrap(), + )?, + }) + } +} diff --git a/crates/config/src/database/sqlite.rs b/crates/configuration/src/database/sqlite.rs similarity index 59% rename from crates/config/src/database/sqlite.rs rename to crates/configuration/src/database/sqlite.rs index 8626f62f5..f186b97f6 100644 --- a/crates/config/src/database/sqlite.rs +++ b/crates/configuration/src/database/sqlite.rs @@ -13,66 +13,52 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::helpers; +use super::common; use azalia::config::{env, merge::Merge, TryFromEnv}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -/// ## `database "sqlite" {}` +/// ## `[database.sqlite]` /// /// This database driver uses the almighty, holy [SQLite](https://sqlite.org). This is mainly used /// for development, evaluation purposes, or if PostgreSQL is too heavy for your use-cases. -#[derive(Debug, Clone, Merge, Serialize, Deserialize)] +#[derive(Debug, Clone, Merge, Serialize, Deserialize, derive_more::Deref, derive_more::Display)] +#[display("sqlite://{}", self.path.display())] pub struct Config { - /// Maximum amount of connections that the database pool can hold. - #[serde(default = "__max_connections")] - pub max_connections: u32, - - /// whether if migrations should be ran on startup. By default, this is `false`. - /// - /// You can use the `charted migrations run` command to run all migrations. - #[serde(default)] - #[merge(strategy = azalia::config::merge::strategy::bool::only_if_falsy)] - pub run_migrations: bool, + #[serde(flatten)] + #[deref] + pub common: common::Config, /// Path to the SQLite database. By default, this will be in `./data/charted.db`. /// /// The [official Docker image](https://docker.noelware.org/~/charted/server) will overwrite this path to `/var/lib/noelware/charted/data/charted.db`. #[serde(default = "__db_path")] - pub db_path: PathBuf, + pub path: PathBuf, } impl Default for Config { fn default() -> Self { Config { - max_connections: __max_connections(), - run_migrations: false, - db_path: __db_path(), + common: common::Config::default(), + path: __db_path(), } } } +const PATH: &str = "CHARTED_DATABASE_PATH"; + impl TryFromEnv for Config { type Output = Config; type Error = eyre::Report; fn try_from_env() -> Result { Ok(Config { - max_connections: helpers::env_from_str("CHARTED_DATABASE_MAX_CONNECTIONS", __max_connections())?, - run_migrations: helpers::env_from_result( - env!("CHARTED_DATABASE_RUN_MIGRATIONS").map(|x| azalia::TRUTHY_REGEX.is_match(&x)), - false, - )?, - - db_path: env!("CHARTED_DATABASE_PATH", as PathBuf).unwrap_or(__db_path()), + common: common::Config::try_from_env()?, + path: env!(PATH).map(PathBuf::from).unwrap_or(__db_path()), }) } } -const fn __max_connections() -> u32 { - 10 -} - #[inline] fn __db_path() -> PathBuf { PathBuf::from("./data/charted.db") diff --git a/crates/helm-plugin/src/cmds/version.rs b/crates/configuration/src/features.rs similarity index 97% rename from crates/helm-plugin/src/cmds/version.rs rename to crates/configuration/src/features.rs index 6bffe91d7..7a32026c1 100644 --- a/crates/helm-plugin/src/cmds/version.rs +++ b/crates/configuration/src/features.rs @@ -12,3 +12,5 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + +pub mod totp; diff --git a/crates/config/src/features/auditlog.rs b/crates/configuration/src/features/auditlog.rs similarity index 100% rename from crates/config/src/features/auditlog.rs rename to crates/configuration/src/features/auditlog.rs diff --git a/crates/config/src/features/gc.rs b/crates/configuration/src/features/gc.rs similarity index 100% rename from crates/config/src/features/gc.rs rename to crates/configuration/src/features/gc.rs diff --git a/crates/config/src/features/oci.rs b/crates/configuration/src/features/oci.rs similarity index 100% rename from crates/config/src/features/oci.rs rename to crates/configuration/src/features/oci.rs diff --git a/crates/cli/src/cmds/admin/user/delete.rs b/crates/configuration/src/features/totp.rs similarity index 100% rename from crates/cli/src/cmds/admin/user/delete.rs rename to crates/configuration/src/features/totp.rs diff --git a/crates/config/src/features/webhooks.rs b/crates/configuration/src/features/webhooks.rs similarity index 100% rename from crates/config/src/features/webhooks.rs rename to crates/configuration/src/features/webhooks.rs diff --git a/crates/configuration/src/lib.rs b/crates/configuration/src/lib.rs new file mode 100644 index 000000000..a45ae8bf0 --- /dev/null +++ b/crates/configuration/src/lib.rs @@ -0,0 +1,210 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod database; +pub mod features; +pub mod logging; +pub mod metrics; +pub mod server; +pub mod sessions; +pub mod storage; +pub mod tracing; +pub(crate) mod util; + +use azalia::config::{env, merge::Merge, FromEnv, TryFromEnv}; +use sentry_types::Dsn; +use serde::{Deserialize, Serialize}; +use std::{ + fs, + path::{Path, PathBuf}, +}; +use url::Url; + +/// The root configuration for the API server. +/// +/// **charted-server** uses a TOML-based configuration format for easy accessiblity. +/// +/// **charted-server** also supports environment variables that can be overwritten when the configuration is +/// being loaded. The priority is **Environment Variables > Configuration File**. +#[derive(Debug, Clone, Serialize, Deserialize, Merge)] +pub struct Config { + /// A secret key for generating JWT tokens for session-based authentication. + /// + /// It is recommended to set this as the `CHARTED_JWT_SECRET_KEY` environment + /// variable and **charted-server** will load it into this property. + /// + /// If this is ever messed with, sessions that are on-going will be permanently corrupted. + #[serde(default)] + #[merge(strategy = azalia::config::merge::strategy::strings::overwrite_empty)] + pub jwt_secret_key: String, + + /// Whether if this instance accepts user registrations. + #[serde(default = "crate::util::truthy")] + pub registrations: bool, + + /// whether if this instance should like a single user registry. + /// + /// If this is the case, most features are disabled like organizations, + /// repository/organization members, user creation, etc. + /// + /// You can use either the `charted admin user new ` + /// if you're going to use the local session backend or use the static backend with + /// `user = "password"`. + #[serde(default)] + pub single_user: bool, + + /// whether if this instance should act like a single organization registry. + /// + /// If so, most features are disabled like user creation, repository members, etc. + #[serde(default)] + pub single_org: bool, + + /// opt into reporting errors to a [Sentry] server. + /// + /// [Sentry]: https://sentry.io + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sentry_dsn: Option, + + /// URI that will redirect all API requests and Helm chart downloads towards. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub base_url: Option, + + #[serde(default)] + pub database: database::Config, + + #[serde(default)] + pub logging: logging::Config, + + #[serde(default)] + pub server: server::Config, + + #[serde(default)] + pub storage: storage::Config, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tracing: Option, + + #[serde(default)] + pub sessions: sessions::Config, +} + +impl Config { + pub fn find_default_location() -> eyre::Result> { + const VALID_FILE_NAMES: &[&str; 2] = &["charted.toml", "config.toml"]; + + let config_dir = PathBuf::from("./config"); + if config_dir.is_dir() && config_dir.try_exists()? { + let charted_toml = config_dir.join(VALID_FILE_NAMES[0]); + if charted_toml.try_exists()? && charted_toml.is_file() { + return Ok(Some(charted_toml)); + } + } + + for file in VALID_FILE_NAMES { + let path = PathBuf::from(format!("./{file}")); + if path.try_exists()? && path.is_file() { + return Ok(Some(path)); + } + } + + Ok(None) + } + + pub fn load>(path: Option

) -> eyre::Result { + let Some(path) = path.as_ref() else { + return Config::try_from_env(); + }; + + let path = path.as_ref(); + if !path.try_exists()? { + eprintln!( + "[charted :: WARN] file [{}] doesn't exist; using system environment variables instead", + path.display() + ); + + return Config::try_from_env(); + } + + let mut config = Config::try_from_env()?; + let contents = fs::read_to_string(path)?; + + let file: Config = toml::from_str(&contents)?; + config.merge(file); + + if config.jwt_secret_key.is_empty() { + let key = charted_core::rand_string(16); + eprintln!( + r#"""[charted :: WARN] You are missing a JWT secret key either from +the `${env}` environment variable or from the `jwt_secret_key` configuration property in your `config.toml` + +I generated one for you here: `{secret}` + +!! DO NOT LOSE IT !!"""#, + env = JWT_SECRET_KEY, + secret = key + ); + + config.jwt_secret_key = key; + } + + if config.base_url.is_none() { + let scheme = match config.server.ssl { + Some(_) => "https", + None => "http", + }; + + let url = Url::parse(&format!("{scheme}://{}", config.server.to_socket_addr()))?; + eprintln!("[charted :: WARN] `base_url` was not configured properly! All URLs will be mapped to {url}"); + + config.base_url = Some(url); + } + + Ok(config) + } +} + +pub const JWT_SECRET_KEY: &str = "CHARTED_JWT_SECRET_KEY"; +pub const REGISTRATIONS: &str = "CHARTED_ENABLE_REGISTRATIONS"; +pub const SINGLE_USER: &str = "CHARTED_SINGLE_USER"; +pub const SINGLE_ORG: &str = "CHARTED_SINGLE_ORGANIZATION"; +pub const SENTRY_DSN: &str = "CHARTED_SENTRY_DSN"; +pub const BASE_URL: &str = "CHARTED_BASE_URL"; + +impl TryFromEnv for Config { + type Output = Self; + type Error = eyre::Report; + + fn try_from_env() -> Result { + Ok(Self { + jwt_secret_key: util::env_from_result_lazy(env!(JWT_SECRET_KEY), || Ok(String::default()))?, + registrations: util::bool_env(REGISTRATIONS)?, + single_user: util::bool_env(SINGLE_USER)?, + single_org: util::bool_env(SINGLE_ORG)?, + sentry_dsn: util::env_optional_from_str(SENTRY_DSN, None)?, + base_url: util::env_optional_from_str(BASE_URL, None)?, + database: database::Config::try_from_env()?, + sessions: sessions::Config::try_from_env()?, + logging: logging::Config::from_env(), + storage: storage::Config::try_from_env()?, + server: server::Config::try_from_env()?, + + tracing: match util::bool_env(tracing::ENABLED) { + Ok(true) => Some(tracing::Config::try_from_env()?), + Ok(false) => None, + Err(e) => return Err(e), + }, + }) + } +} diff --git a/crates/config/src/logging.rs b/crates/configuration/src/logging.rs similarity index 90% rename from crates/config/src/logging.rs rename to crates/configuration/src/logging.rs index cff1e1cb0..6ddbab70b 100644 --- a/crates/config/src/logging.rs +++ b/crates/configuration/src/logging.rs @@ -20,6 +20,9 @@ use azalia::{ use serde::{Deserialize, Serialize}; use tracing::Level; +const LEVEL: &str = "CHARTED_LOG_LEVEL"; +const JSON: &str = "CHARTED_LOG_JSON"; + #[derive(Debug, Clone, Merge, Serialize, Deserialize)] pub struct Config { /// Configures the log level of the API server's logging capabilities. The higher the level, the more verbose @@ -48,8 +51,8 @@ impl FromEnv for Config { fn from_env() -> Self::Output { Config { - json: env!("CHARTED_LOG_JSON", |val| TRUTHY_REGEX.is_match(&val); or false), - level: env!("CHARETED_LOG_LEVEL", |val| match val.as_str() { + json: env!(JSON, |val| TRUTHY_REGEX.is_match(&val); or false), + level: env!(LEVEL, |val| match &*val.to_ascii_lowercase() { "trace" => Level::TRACE, "debug" => Level::DEBUG, "error" => Level::ERROR, diff --git a/crates/config/src/metrics.rs b/crates/configuration/src/metrics.rs similarity index 100% rename from crates/config/src/metrics.rs rename to crates/configuration/src/metrics.rs diff --git a/crates/config/src/server/ratelimits.rs b/crates/configuration/src/metrics/prometheus.rs similarity index 100% rename from crates/config/src/server/ratelimits.rs rename to crates/configuration/src/metrics/prometheus.rs diff --git a/crates/configuration/src/server.rs b/crates/configuration/src/server.rs new file mode 100644 index 000000000..6b02e273b --- /dev/null +++ b/crates/configuration/src/server.rs @@ -0,0 +1,86 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod ratelimits; +pub mod ssl; + +use crate::util; +use azalia::config::{env, merge::Merge, TryFromEnv}; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, net::SocketAddr}; + +pub const HEADERS: &str = "CHARTED_SERVER_HEADERS"; +pub const HOST: &[&str; 2] = &["CHARTED_SERVER_HOST", "HOST"]; +pub const PORT: &[&str; 2] = &["CHARTED_SERVER_PORT", "PORT"]; + +/// ## `[server]` table +/// This configures the HTTP service that the API server creates. +#[derive(Debug, Clone, Default, Merge, Serialize, Deserialize)] +pub struct Config { + /// A list of headers to append to all responses. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub headers: BTreeMap, + + /// The host to bind towards. + #[serde(default = "__default_host")] + pub host: String, + + /// Port to listen on. + #[serde(default = "__default_port")] + pub port: u16, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ssl: Option, +} + +impl Config { + pub fn to_socket_addr(&self) -> SocketAddr { + format!("{}:{}", self.host, self.port).parse().unwrap() + } +} + +impl TryFromEnv for Config { + type Output = Self; + type Error = eyre::Report; + + fn try_from_env() -> Result { + Ok(Config { + headers: util::btreemap_env(HEADERS)?, + host: util::env_from_result_lazy(env!(HOST[0]), || { + util::env_from_result_lazy(env!(HOST[1]), || Ok(__default_host())) + })?, + + port: match util::env_from_str(PORT[0], __default_port()) { + Ok(value) => value, + Err(_) => util::env_from_str(PORT[1], __default_port())?, + }, + + ssl: match util::bool_env(ssl::ENABLED) { + Ok(true) => ssl::Config::try_from_env().map(Some)?, + Ok(false) => None, + Err(e) => return Err(e), + }, + }) + } +} + +#[inline] +fn __default_host() -> String { + String::from("0.0.0.0") +} + +const fn __default_port() -> u16 { + 3651 +} diff --git a/crates/helm-plugin/src/cmds/auth/delete.rs b/crates/configuration/src/server/ratelimits.rs similarity index 100% rename from crates/helm-plugin/src/cmds/auth/delete.rs rename to crates/configuration/src/server/ratelimits.rs diff --git a/crates/config/src/server/ssl.rs b/crates/configuration/src/server/ssl.rs similarity index 88% rename from crates/config/src/server/ssl.rs rename to crates/configuration/src/server/ssl.rs index 0e30509ea..e5718d885 100644 --- a/crates/config/src/server/ssl.rs +++ b/crates/configuration/src/server/ssl.rs @@ -18,6 +18,10 @@ use eyre::Context; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +pub const CERT_KEY: &str = "CHARTED_SERVER_SSL_CERT_KEY"; +pub const ENABLED: &str = "CHARTED_SERVER_SSL"; +pub const CERT: &str = "CHARTED_SERVER_SSL_CERTIFICATE"; + #[derive(Debug, Clone, Merge, Serialize, Deserialize)] pub struct Config { /// Location to a certificate private key. @@ -43,11 +47,11 @@ impl TryFromEnv for Config { fn try_from_env() -> Result { Ok(Config { - cert_key: env!("CHARTED_SERVER_SSL_CERT_KEY") + cert_key: env!(CERT_KEY) .map(PathBuf::from) .context("unable to load up `CHARTED_SERVER_SSL_CERT_KEY` env")?, - cert: env!("CHARTED_SERVER_SSL_CERT") + cert: env!(CERT) .map(PathBuf::from) .context("unable to load up `CHARTED_SERVER_SSL_CERT` env")?, }) diff --git a/crates/config/src/sessions.rs b/crates/configuration/src/sessions.rs similarity index 52% rename from crates/config/src/sessions.rs rename to crates/configuration/src/sessions.rs index d047c82fe..a391d3470 100644 --- a/crates/config/src/sessions.rs +++ b/crates/configuration/src/sessions.rs @@ -15,68 +15,78 @@ pub mod ldap; -use azalia::{ - config::{env, merge::Merge, TryFromEnv}, - TRUTHY_REGEX, -}; +use azalia::config::{env, merge::Merge, TryFromEnv}; +use eyre::bail; use serde::{Deserialize, Serialize}; -use std::env::VarError; +use std::{collections::BTreeMap, env::VarError}; + +use crate::util; + +pub const BACKEND: &str = "CHARTED_SESSIONS_BACKEND"; +pub const STATIC_USERS: &str = "CHARTED_SESSIONS_STATIC_USERS"; #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Backend { + Static(BTreeMap), Ldap(ldap::Config), #[default] Local, } -impl Backend { - pub const fn is_local(&self) -> bool { - matches!(self, Backend::Local) - } -} - impl Merge for Backend { fn merge(&mut self, other: Self) { - match (self.clone(), other) { - (Backend::Ldap(ref mut ldap1), Backend::Ldap(ldap2)) => { - ldap1.merge(ldap2); + match (self, other) { + (Self::Static(s1), Self::Static(s2)) => { + s1.merge(s2); } - (_, Backend::Ldap(ldap)) => { - *self = Backend::Ldap(ldap); + (Self::Ldap(ldap1), Self::Ldap(ldap2)) => { + ldap1.merge(ldap2); } - (_, Backend::Local) => { - *self = Backend::Local; + (Self::Local, Self::Local) => {} + + // the case from env -> config + (Self::Ldap(_), Self::Local) => {} + (Self::Static(_), Self::Local) => {} + + (me, other) => { + *me = other; } } } } impl TryFromEnv for Backend { - type Output = Backend; + type Output = Self; type Error = eyre::Report; fn try_from_env() -> Result { - match env!("CHARTED_SESSION_BACKEND") { - Ok(res) => match &*res.to_ascii_lowercase() { - "ldap" => Ok(Backend::Ldap(ldap::Config::try_from_env()?)), + match env!(BACKEND) { + Ok(input) => match &*input.to_ascii_lowercase() { "local" | "default" => Ok(Backend::Local), - s => Err(eyre::eyre!("unknown value [{s}]: expected [ldap, local]")), - }, + "static" => Ok(Backend::Static(util::btreemap_env(STATIC_USERS)?)), + "ldap" => Ok(Backend::Ldap(ldap::Config::try_from_env()?)), + input => bail!( + "unexpected input given from environment variable `${}`: expected `local`, `default`, `static`, or `ldap`; received {} instead", + BACKEND, + input + ) + } - Err(VarError::NotPresent) => Ok(Backend::Local), - Err(e) => Err(eyre::eyre!(e)), + Err(VarError::NotPresent) => Ok(Backend::default()), + Err(VarError::NotUnicode(_)) => bail!( + "environment variable `${}` couldn't be loaded due to invalid unicode", + BACKEND + ) } } } -#[derive(Debug, Clone, Default, Serialize, Deserialize, Merge)] +#[derive(Debug, Clone, Default, Merge, Serialize, Deserialize)] pub struct Config { - /// Allows the API server to accept `Authorization: Basic {base64 of username:password}` when using authenticated - /// endpoints. This is not recommended in production environments. #[serde(default)] #[merge(strategy = azalia::config::merge::strategy::bool::only_if_falsy)] pub enable_basic_auth: bool, @@ -85,13 +95,15 @@ pub struct Config { pub backend: Backend, } +pub const ENABLE_BASIC_AUTH: &str = "CHARTED_SESSIONS_ENABLE_BASIC_AUTH"; + impl TryFromEnv for Config { - type Output = Config; + type Output = Self; type Error = eyre::Report; fn try_from_env() -> Result { Ok(Config { - enable_basic_auth: env!("CHARTED_SESSION_ENABLE_BASIC_AUTH", |val| TRUTHY_REGEX.is_match(&val); or false), + enable_basic_auth: util::bool_env(ENABLE_BASIC_AUTH)?, backend: Backend::try_from_env()?, }) } diff --git a/crates/config/src/sessions/ldap.rs b/crates/configuration/src/sessions/ldap.rs similarity index 70% rename from crates/config/src/sessions/ldap.rs rename to crates/configuration/src/sessions/ldap.rs index 2e9430713..a799a353c 100644 --- a/crates/config/src/sessions/ldap.rs +++ b/crates/configuration/src/sessions/ldap.rs @@ -13,19 +13,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::helpers; -use azalia::{ - config::{env, merge::Merge, TryFromEnv}, - TRUTHY_REGEX, -}; -use charted_core::serde::Duration; +use crate::{database::Duration, util}; +use azalia::config::{env, merge::Merge, TryFromEnv}; use serde::{Deserialize, Serialize}; -/// ## `backend "ldap" {}` -/// -/// Configures to use a LDAP server to authenticate users, mainly used for -/// organizations. -#[derive(Debug, Clone, Serialize, Deserialize, Merge)] +pub const INSECURE_SKIP_TLS_VERIFY: &str = "CHARTED_SESSIONS_LDAP_INSECURE_SKIP_TLS_VERIFY"; +pub const SCHEDULE_USER_UPDATES: &str = "CHARTED_SESSIONS_LDAP_SCHEDULE_USER_UPDATES"; +pub const SCHEDULE_NEW_USERS: &str = "CHARTED_SESSIONS_LDAP_SCHEDULE_NEW_USERS"; +pub const CONNECT_TIMEOUT: &str = "CHARTED_SESSIONS_LDAP_CONNECT_TIMEOUT"; +pub const FILTER_QUERY: &str = "CHARTED_SESSIONS_LDAP_FILTER_QUERY"; +pub const STARTTLS: &str = "CHARTED_SESSIONS_LDAP_STARTTLS"; +pub const BIND_DN: &str = "CHARTED_SESSIONS_LDAP_BIND_DN"; +pub const SERVER: &str = "CHARTED_SESSIONS_LDAP_SERVER"; + +#[derive(Debug, Clone, Merge, Serialize, Deserialize)] pub struct Config { /// If `true`, then charted-server will try to establish a TLS connection with the LDAP /// server without certificate verification. This is not recommended for production environments. @@ -47,7 +48,7 @@ pub struct Config { /// Timeout on when the connection should be dropped due to not being responsive. #[serde(default = "__default_conn_timeout")] - pub conn_timeout: Duration, + pub connect_timeout: Duration, /// Query used to authenticate users as. If empty, then `=%u` will be used as the default /// bind DN. @@ -78,32 +79,24 @@ pub struct Config { /// LDAP server to connect to. #[serde(default = "__default_ldap_server")] - pub host: String, + pub server: String, } impl TryFromEnv for Config { - type Output = Config; + type Output = Self; type Error = eyre::Report; fn try_from_env() -> Result { Ok(Config { - insecure_skip_tls_verify: env!("CHARTED_SESSION_LDAP_INSECURE_SKIP_TLS_VERIFY", |val| TRUTHY_REGEX.is_match(&val); or false), - schedule_user_updates: env!("CHARTED_SESSION_LDAP_SCHEDULE_USER_UPDATES", |val| TRUTHY_REGEX.is_match(&val); or false), - schedule_new_users: env!("CHARTED_SESSION_LDAP_SCHEDULE_NEW_USERS", |val| TRUTHY_REGEX.is_match(&val); or true), - conn_timeout: helpers::env_from_str("CHARTED_SESSION_LDAP_CONNECTION_TIMEOUT", __default_duration())?, - filter_query: helpers::env_from_result( - env!("CHARTED_SESSION_LDAP_FILTER_QUERY"), - __default_filter_query(), - )?, - + insecure_skip_tls_verify: util::bool_env(INSECURE_SKIP_TLS_VERIFY)?, + schedule_user_updates: util::bool_env(SCHEDULE_USER_UPDATES)?, + schedule_new_users: util::bool_env(SCHEDULE_NEW_USERS)?, + connect_timeout: util::env_from_str(CONNECT_TIMEOUT, __default_conn_timeout())?, + filter_query: util::env_from_result(env!(FILTER_QUERY), __default_filter_query())?, attributes: Attributes::try_from_env()?, - starttls: env!("CHARTED_SESSION_LDAP_STARTTLS", |val| TRUTHY_REGEX.is_match(&val); or false), - bind_dn: helpers::env_from_result( - env!("CHARTED_SESSION_LDAP_BIND_DN"), - String::from("uid=%u,dc=domain,dc=com"), - )?, - - host: helpers::env_from_result(env!("CHARTED_SESSION_LDAP_SERVER"), __default_ldap_server())?, + starttls: util::bool_env(STARTTLS)?, + bind_dn: util::env_from_result(env!(BIND_DN), String::from("uid=%u,dc=domain,dc=com"))?, + server: util::env_from_result(env!(SERVER), __default_ldap_server())?, }) } } @@ -139,26 +132,19 @@ impl Default for Attributes { } } +pub const ATTRIBUTE_DISPLAY_NAME: &str = "CHARTED_SESSIONS_LDAP_ATTR_DISPLAY_NAME"; +pub const ATTRIBUTE_USERNAME: &str = "CHARTED_SESSIONS_LDAP_ATTR_USERNAME"; +pub const ATTRIBUTE_EMAIL: &str = "CHARTED_SESSIONS_LDAP_ATTR_EMAIL"; + impl TryFromEnv for Attributes { - type Output = Attributes; + type Output = Self; type Error = eyre::Report; fn try_from_env() -> Result { Ok(Attributes { - display_name: helpers::env_from_result( - env!("CHARTED_SESSION_LDAP_ATTR_DISPLAY_NAME"), - __default_ldap_display_name_attribute(), - )?, - - username: helpers::env_from_result( - env!("CHARTED_SESSION_LDAP_ATTR_USERNAME"), - __default_ldap_username_attribute(), - )?, - - email: helpers::env_from_result( - env!("CHARTED_SESSION_LDAP_ATTR_EMAIL"), - __default_ldap_email_attribute(), - )?, + display_name: util::env_from_result(env!(ATTRIBUTE_DISPLAY_NAME), __default_ldap_display_name_attribute())?, + username: util::env_from_result(env!(ATTRIBUTE_USERNAME), __default_ldap_username_attribute())?, + email: util::env_from_result(env!(ATTRIBUTE_EMAIL), __default_ldap_email_attribute())?, }) } } @@ -184,7 +170,7 @@ fn __default_ldap_server() -> String { } fn __default_duration() -> Duration { - Duration::from(std::time::Duration::from_secs(5)) + Duration::from_secs(5) } const fn __default_conn_timeout() -> Duration { diff --git a/crates/configuration/src/storage.rs b/crates/configuration/src/storage.rs new file mode 100644 index 000000000..15d81fb88 --- /dev/null +++ b/crates/configuration/src/storage.rs @@ -0,0 +1,357 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use azalia::{ + config::{env, merge::Merge, TryFromEnv}, + remi, +}; +use eyre::bail; +use serde::{Deserialize, Serialize}; +use std::{env::VarError, path::PathBuf}; + +const SERVICE: &str = "CHARTED_STORAGE_SERVICE"; + +/// ## `[storage]` table +/// Configures how the API server stores data like chart indexes, +/// images for user avatars, repository/organization icons, and much +/// more. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Config { + /// Alows the API server to use the local filesystem to store metadata in. + Filesystem(remi::fs::StorageConfig), + + /// Alows the API server to use Microsoft's Azure Blob Storage service to store metadata in. + Azure(remi::azure::StorageConfig), + + /// Alows the API server to use Amazon S3 (or any compatible service) to store metadata in. + S3(remi::s3::StorageConfig), +} + +impl Default for Config { + fn default() -> Config { + Config::Filesystem(remi::fs::StorageConfig { + directory: PathBuf::from("./data"), + }) + } +} + +impl TryFromEnv for Config { + type Output = Self; + type Error = eyre::Report; + + fn try_from_env() -> Result { + match env!(SERVICE) { + Ok(input) => match &*input.to_ascii_lowercase() { + "filesystem" | "fs" => Ok(Config::Filesystem(remi::fs::StorageConfig { + directory: env!( + filesystem::DIRECTORY, + |val| PathBuf::from(val); + or PathBuf::from("./data") + ), + })), + + "azure" => Ok(Config::Azure(azure::create_config()?)), + "s3" => Ok(Config::S3(s3::create_config()?)), + v => bail!( + "environment variable `${}` received an invalid input: expected either `filesystem`, `fs`, `s3`, or `azure`; received {} instead", + SERVICE, + v + ) + }, + + Err(VarError::NotPresent) => Ok(Default::default()), + Err(VarError::NotUnicode(_)) => bail!("environment variable `${}` received invalid unicode", SERVICE), + } + } +} + +impl Merge for Config { + fn merge(&mut self, other: Self) { + match (self, other) { + (Self::Filesystem(fs1), Self::Filesystem(fs2)) => { + fs1.directory.merge(fs2.directory); + } + + (Self::Azure(azure1), Self::Azure(azure2)) => { + azure::merge_config(azure1, azure2); + } + + (Self::S3(s3_1), Self::S3(s3_2)) => { + s3::merge_config(s3_1, s3_2); + } + + (me, other) => { + *me = other; + } + } + } +} + +pub(crate) mod filesystem { + pub const DIRECTORY: &str = "CHARTED_STORAGE_FILESYSTEM_DIRECTORY"; +} + +pub(crate) mod s3 { + use azalia::{ + config::{env, merge::Merge}, + remi::{ + self, + s3::aws::{ + config::Region, + s3::types::{BucketCannedAcl, ObjectCannedAcl}, + }, + }, + TRUTHY_REGEX, + }; + use eyre::eyre; + use std::{borrow::Cow, env::VarError, str::FromStr}; + + pub const ENABLE_SIGNER_V4_REQUESTS: &str = "CHARTED_STORAGE_S3_ENABLE_SIGNER_V4_REQUESTS"; + pub const ENFORCE_PATH_ACCESS_STYLE: &str = "CHARTED_STORAGE_S3_ENFORCE_PATH_ACCESS_STYLE"; + pub const DEFAULT_OBJECT_ACL: &str = "CHARTED_STORAGE_S3_DEFAULT_OBJECT_ACL"; + pub const DEFAULT_BUCKET_ACL: &str = "CHARTED_STORAGE_S3_DEFAULT_BUCKET_ACL"; + pub const SECRET_ACCESS_KEY: &str = "CHARTED_STORAGE_S3_SECRET_ACCESS_KEY"; + pub const ACCESS_KEY_ID: &str = "CHARTED_STORAGE_S3_ACCESS_KEY_ID"; + pub const APP_NAME: &str = "CHARTED_STORAGE_S3_APP_NAME"; + pub const ENDPOINT: &str = "CHARTED_STORAGE_S3_ENDPOINT"; + pub const PREFIX: &str = "CHARTED_STORAGE_S3_PREFIX"; + pub const REGION: &str = "CHARTED_STORAGE_S3_REGION"; + pub const BUCKET: &str = "CHARTED_STORAGE_S3_BUCKET"; + + const DEFAULT_OBJECT_CANNED_ACL: ObjectCannedAcl = ObjectCannedAcl::BucketOwnerFullControl; + const DEFAULT_BUCKET_CANNED_ACL: BucketCannedAcl = BucketCannedAcl::AuthenticatedRead; + + macro_rules! merge_tuple { + ($first:expr, $second:expr, copyable) => { + match ($first, $second) { + (Some(obj1), Some(obj2)) if obj1 != obj2 => { + $first = Some(obj2); + } + + (None, Some(obj)) => { + $first = Some(obj); + } + + _ => {} + } + }; + + ($first:expr, $second:expr) => { + match (&($first), &($second)) { + (Some(obj1), Some(obj2)) if obj1 != obj2 => { + $first = Some(obj2.clone()); + } + + (None, Some(obj)) => { + $first = Some(obj.clone()); + } + + _ => {} + } + }; + } + + pub fn create_config() -> eyre::Result { + Ok(remi::s3::StorageConfig { + enable_signer_v4_requests: env!(ENABLE_SIGNER_V4_REQUESTS, |val| TRUTHY_REGEX.is_match(&val); or false), + enforce_path_access_style: env!(ENFORCE_PATH_ACCESS_STYLE, |val| TRUTHY_REGEX.is_match(&val); or false), + default_object_acl: env!(DEFAULT_OBJECT_ACL, |val| ObjectCannedAcl::from_str(val.as_str()).ok(); or Some(DEFAULT_OBJECT_CANNED_ACL)), + default_bucket_acl: env!(DEFAULT_BUCKET_ACL, |val| BucketCannedAcl::from_str(val.as_str()).ok(); or Some(DEFAULT_BUCKET_CANNED_ACL)), + secret_access_key: env!(SECRET_ACCESS_KEY).map_err(|e| match e { + VarError::NotPresent => { + eyre!("you're required to add the [{SECRET_ACCESS_KEY}] environment variable") + } + + VarError::NotUnicode(_) => eyre!("wanted valid UTF-8 for env `{SECRET_ACCESS_KEY}`"), + })?, + + access_key_id: env!(ACCESS_KEY_ID).map_err(|e| match e { + VarError::NotPresent => { + eyre!("you're required to add the [{ACCESS_KEY_ID}] environment variable") + } + + VarError::NotUnicode(_) => eyre!("wanted valid UTF-8 for env `{ACCESS_KEY_ID}`"), + })?, + + app_name: env!(APP_NAME, optional), + endpoint: env!(ENDPOINT, optional), + prefix: env!(PREFIX, optional), + region: env!(REGION, |val| Some(Region::new(Cow::Owned(val))); or Some(Region::new(Cow::Borrowed("us-east-1")))), + bucket: env!(BUCKET, optional).unwrap_or("ume".into()), + }) + } + + pub fn merge_config(me: &mut remi::s3::StorageConfig, other: remi::s3::StorageConfig) { + azalia::config::merge::strategy::bool::only_if_falsy( + &mut me.enable_signer_v4_requests, + other.enable_signer_v4_requests, + ); + + azalia::config::merge::strategy::bool::only_if_falsy( + &mut me.enforce_path_access_style, + other.enforce_path_access_style, + ); + + merge_tuple!(me.default_bucket_acl, other.default_bucket_acl); + merge_tuple!(me.default_object_acl, other.default_object_acl); + + me.secret_access_key.merge(other.secret_access_key); + me.access_key_id.merge(other.access_key_id); + + merge_tuple!(me.app_name, other.app_name); + merge_tuple!(me.endpoint, other.endpoint); + merge_tuple!(me.region, other.region); + + me.bucket.merge(other.bucket); + } +} + +pub(crate) mod azure { + use azalia::{ + config::{env, merge::Merge}, + remi::{ + self, + azure::{CloudLocation, Credential}, + }, + }; + use eyre::{eyre, Context}; + use std::env::VarError; + + pub const ACCESS_KEY_ACCOUNT: &str = "CHARTED_STORAGE_AZURE_CREDENTIAL_ACCESSKEY_ACCOUNT"; + pub const ACCESS_KEY: &str = "CHARTED_STORAGE_AZURE_CREDENTIAL_ACCESSKEY"; + pub const SAS_TOKEN: &str = "CHARTED_STORAGE_AZURE_CREDENTIAL_SAS_TOKEN"; + pub const BEARER: &str = "CHARTED_STORAGE_AZURE_CREDENTIAL_BEARER"; + + pub const ACCOUNT: &str = "CHARTED_STORAGE_AZURE_ACCOUNT"; + pub const URI: &str = "CHARTED_STORAGE_AZURE_URI"; + + pub const CREDENTIAL: &str = "CHARTED_STORAGE_AZURE_CREDENTIAL"; + pub const CONTAINER: &str = "CHARTED_STORAGE_AZURE_CONTAINER"; + pub const LOCATION: &str = "CHARTED_STORAGE_AZURE_LOCATION"; + + pub fn create_config() -> eyre::Result { + Ok(remi::azure::StorageConfig { + credentials: create_credentials_config()?, + location: create_location()?, + container: env!(CONTAINER, optional).unwrap_or("ume".into()), + }) + } + + pub fn merge_config(me: &mut remi::azure::StorageConfig, other: remi::azure::StorageConfig) { + me.container.merge(other.container); + + match (&me.location, &other.location) { + (CloudLocation::Public(acc1), CloudLocation::Public(acc2)) if acc1 != acc2 => { + me.location = CloudLocation::Public(acc2.clone()); + } + + (CloudLocation::China(acc1), CloudLocation::China(acc2)) if acc1 != acc2 => { + me.location = CloudLocation::China(acc2.clone()); + } + + (_, other) => { + me.location = other.clone(); + } + } + + match (&me.credentials, &other.credentials) { + ( + Credential::AccessKey { + account: acc1, + access_key: ak1, + }, + Credential::AccessKey { account, access_key }, + ) if acc1 != account || access_key != ak1 => { + me.credentials = Credential::AccessKey { + account: account.clone(), + access_key: access_key.clone(), + }; + } + + (Credential::SASToken(token1), Credential::SASToken(token2)) if token1 != token2 => { + me.credentials = Credential::SASToken(token2.to_owned()); + } + + (Credential::Bearer(token1), Credential::Bearer(token2)) if token1 != token2 => { + me.credentials = Credential::SASToken(token2.to_owned()); + } + + (Credential::Anonymous, Credential::Anonymous) => {} + + // overwrite if they aren't the same at all + (_, other) => { + me.credentials = other.clone(); + } + } + } + + fn create_credentials_config() -> eyre::Result { + match env!(CREDENTIAL) { + Ok(input) => match &*input.to_ascii_lowercase() { + "anonymous" | "anon" | "" => Ok(remi::azure::Credential::Anonymous), + "accesskey" | "access_key" | "access-key" => Ok(remi::azure::Credential::AccessKey { + account: env!(ACCESS_KEY_ACCOUNT).with_context(|| format!("missing required environment variable when `${CREDENTIAL}` is set to Access Key: `${ACCESS_KEY_ACCOUNT}`"))?, + access_key: env!(ACCESS_KEY).with_context(|| format!("missing required environment variable when `${CREDENTIAL}` is set to Access Key: `${ACCESS_KEY}`"))? + }), + + "sastoken" | "sas-token" | "sas_token" => Ok(remi::azure::Credential::SASToken( + env!(SAS_TOKEN).with_context(|| format!("missing required environment variable when `${CREDENTIAL}` is set to SAS Token: `${SAS_TOKEN}`"))? + )), + + "bearer" => Ok(remi::azure::Credential::Bearer( + env!(SAS_TOKEN).with_context(|| format!("missing required environment variable when `${CREDENTIAL}` is set to SAS Token: `${BEARER}`"))? + )), + + input => Err(eyre!("unknown input [{input}] for `${CREDENTIAL}` environment variable")) + }, + + Err(VarError::NotPresent) => Ok(remi::azure::Credential::Anonymous), + Err(VarError::NotUnicode(_)) => Err(eyre!("environment variable `${CREDENTIAL}` was invalid utf-8")) + } + } + + fn create_location() -> eyre::Result { + match env!(LOCATION) { + Ok(res) => match &*res.to_ascii_lowercase() { + "public" | "" => { + Ok(CloudLocation::Public(env!(ACCOUNT).with_context(|| { + format!("missing required environment variable: [{ACCOUNT}]") + })?)) + } + + "china" => { + Ok(CloudLocation::China(env!(ACCOUNT).with_context(|| { + format!("missing required environment variable: [{ACCOUNT}]") + })?)) + } + + "custom" => Ok(CloudLocation::Custom { + account: env!(ACCOUNT) + .with_context(|| format!("missing required environment variable: [{ACCOUNT}]"))?, + + uri: env!(URI).with_context(|| format!("missing required environment variable: [{ACCOUNT}]"))?, + }), + + input => Err(eyre!( + "invalid option given: {input} | expected [public, china, custom]" + )), + }, + + Err(VarError::NotPresent) => Err(eyre!("missing required environment variable: [{LOCATION}]")), + Err(VarError::NotUnicode(_)) => Err(eyre!("environment variable [{LOCATION}] was not in valid unicode")), + } + } +} diff --git a/crates/configuration/src/tracing.rs b/crates/configuration/src/tracing.rs new file mode 100644 index 000000000..433388eea --- /dev/null +++ b/crates/configuration/src/tracing.rs @@ -0,0 +1,77 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::util; +use azalia::config::{env, merge::Merge, TryFromEnv}; +use eyre::bail; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, env::VarError}; +use url::Url; + +pub const ENABLED: &str = "CHARTED_TRACING_ENABLE"; +pub const METRICS: &str = "CHARTED_TRACING_ALLOW_METRICS"; +pub const LABELS: &str = "CHARTED_TRACING_LABELS"; +pub const URL: &str = "CHARTED_TRACING_ENDPOINT"; + +/// ## `[tracing]` table +/// Allows the API server to report traces to any OpenTelemetry supported +/// service like [OpenTelemetry Collector], [Elastic APM], etc. +/// +/// [OpenTelemetry Collector]: https://opentelemetry.io/docs/collector/ +/// [Elastic APM]: https://www.elastic.co/observability/application-performance-monitoring +#[derive(Debug, Clone, Merge, Serialize, Deserialize)] +pub struct Config { + /// If the API server should also send metrics that are generated by us to + /// the supported OpenTelemetry provider or not. + #[serde(default)] + #[merge(strategy = azalia::config::merge::strategy::bool::only_if_falsy)] + pub metrics: bool, + + /// A list of labels to use to detect this instance. + /// + /// By default, the API server will add the following labels: + /// + /// * `charted.version` + /// * `service.name` + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub labels: BTreeMap, + + /// URL to the supported OpenTelemetry collector. + pub url: Url, +} + +impl TryFromEnv for Config { + type Output = Self; + type Error = eyre::Report; + + fn try_from_env() -> Result { + Ok(Config { + metrics: util::bool_env(METRICS)?, + labels: util::btreemap_env(LABELS)?, + url: match env!(URL) { + Ok(url) => Url::parse(&url)?, + Err(VarError::NotPresent) => bail!( + "environment variable `${}` is required when environment variable `${}` is set to true", + URL, + ENABLED + ), + + Err(VarError::NotUnicode(_)) => { + bail!("environment variable `${}` contained invalid unicode characters", URL) + } + }, + }) + } +} diff --git a/crates/configuration/src/util.rs b/crates/configuration/src/util.rs new file mode 100644 index 000000000..b870b0dfb --- /dev/null +++ b/crates/configuration/src/util.rs @@ -0,0 +1,126 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use eyre::{bail, eyre}; +use std::{collections::BTreeMap, env::VarError, fmt::Display, str::FromStr}; + +#[inline(always)] +pub const fn truthy() -> bool { + true +} + +/// Given a [`Result`] and default value: +/// +/// - In variant [`Ok`]\({value}\), return the `{value}`. +/// - In variant [`Err`]\([`VarError::Present`]\), return `default`. +/// - Otherwise, bail out. +pub fn env_from_result(result: Result, default: T) -> eyre::Result { + match result { + Ok(value) => Ok(value), + Err(VarError::NotPresent) => Ok(default), + Err(VarError::NotUnicode(_)) => bail!("received non-unicode value in environment variable"), + } +} + +pub fn env_from_result_lazy( + result: Result, + default: impl FnOnce() -> eyre::Result, +) -> eyre::Result { + match result { + Ok(value) => Ok(value), + Err(VarError::NotPresent) => Ok(default()?), + Err(VarError::NotUnicode(_)) => bail!("received non-unicode value in environment variable"), + } +} + +pub fn env_from_str(key: &str, default: F) -> eyre::Result +where + F::Err: Display, +{ + match azalia::config::env!(key) { + Ok(value) => value + .parse::() + .map_err(|e| eyre!("failed to parse environment variable `${}`: {}", key, e)), + + Err(VarError::NotPresent) => Ok(default), + Err(VarError::NotUnicode(_)) => bail!("received non-unicode in environment variable `${}`", key), + } +} + +pub fn bool_env(key: &str) -> eyre::Result { + env_from_result( + azalia::config::env!(key).map(|x| azalia::TRUTHY_REGEX.is_match(&x)), + false, + ) +} + +pub fn btreemap_env(key: &str) -> eyre::Result> +where + K::Err: Display, + V::Err: Display, +{ + let mut map = azalia::btreemap!(K, V); + let result = match azalia::config::env!(key) { + Ok(res) => res, + Err(VarError::NotPresent) => return Ok(map), + Err(VarError::NotUnicode(_)) => bail!("received non-unicode in environment variable `${}`", key), + }; + + for (i, line) in result.split(',').enumerate() { + if let Some((key, val)) = line.split_once('=') { + if val.contains('=') { + continue; + } + + map.insert( + match K::from_str(val) { + Ok(v) => v, + Err(e) => bail!( + "failed to parse environment variable `${}`: at index #{}, failed to parse key: {}", + key, + i, + e + ), + }, + match V::from_str(val) { + Ok(v) => v, + Err(e) => bail!( + "failed to parse environment variable `${}`: at index #{}, failed to parse value: {}", + key, + i, + e + ), + }, + ); + } + } + + Ok(map) +} + +pub fn env_optional_from_str(key: &str, default: Option) -> eyre::Result> +where + F::Err: Display, +{ + match azalia::config::env!(key) { + Ok(value) => value + .parse::() + .map(Some) + .map_err(|e| eyre!("failed to parse environment variable `${}`: {}", key, e)), + + Err(VarError::NotPresent) => Ok(default), + Err(VarError::NotUnicode(_)) => Err(eyre!("received non-unicode in `${}` environment variable", key)), + } +} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 95626f752..9282586c5 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -15,43 +15,38 @@ [package] name = "charted-core" -description = "🐻‍❄️📦 Represents the core instance of charted." +description = "🐻‍❄️📦 Essential core utilities for the charted project." version.workspace = true documentation.workspace = true edition.workspace = true homepage.workspace = true license.workspace = true -publish.workspace = true +publish = true repository.workspace = true authors.workspace = true +rust-version.workspace = true + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(noeldoc)'] } [features] -testkit = ["dep:charted-testkit", "dep:testcontainers-modules"] default = [] -merge = ["azalia/config"] + +openapi = ["dep:utoipa"] [dependencies] argon2.workspace = true -axum.workspace = true -azalia = { workspace = true, features = ["lazy", "regex"] } -charted-testkit = { version = "0.1.0", features = ["macros"], optional = true } -eyre.workspace = true +axum = { workspace = true, optional = true } +derive_more = { workspace = true, features = ["display"] } humantime = "2.1.0" -rand = "0.8.5" +rand.workspace = true serde.workspace = true serde_json.workspace = true -utoipa.workspace = true -testcontainers-modules = { workspace = true, optional = true, features = [ - "postgres", -] } +schemars = { workspace = true, optional = true } serde_repr = "0.1.19" -schemars.workspace = true -ulid = "1.1.3" +utoipa = { workspace = true, optional = true } [build-dependencies] chrono.workspace = true -rustc_version = "0.4.0" -which.workspace = true - -[dev-dependencies] -serde_json.workspace = true +rustc_version.workspace = true +which = "7.0.1" diff --git a/crates/core/build.rs b/crates/core/build.rs index 19ab67121..3a61636d6 100644 --- a/crates/core/build.rs +++ b/crates/core/build.rs @@ -105,7 +105,7 @@ fn main() { } Err(which::Error::CannotFindBinaryPath) => { - warn!("`git` was not found -- using `d1cebae` as hash instead"); + warn!("`git` was not found: using `d1cebae` as hash instead"); rustc_env!("CHARTED_COMMIT_HASH" = "d1cebae"); } diff --git a/crates/core/src/api.rs b/crates/core/src/api.rs index 2efb618ea..e4930f24a 100644 --- a/crates/core/src/api.rs +++ b/crates/core/src/api.rs @@ -13,26 +13,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! The `api` module contains types that go along with the API server. - -use axum::{ - body::Body, - http::{header, StatusCode}, - response::IntoResponse, -}; -use schemars::{ - gen::SchemaGenerator, - schema::{InstanceType, Schema, SchemaObject, SingleOrVec}, - JsonSchema, -}; +//! Types that are used with the API server. + use serde::Serialize; use serde_json::Value; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::borrow::Cow; -use utoipa::ToSchema; -/// REST Specification version for charted's HTTP API. -#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize_repr, Deserialize_repr)] +/// Specification version for charted's HTTP specification. +#[derive( + Debug, + Clone, + Copy, + Default, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize_repr, + Deserialize_repr, + derive_more::Display, +)] +#[display("{}", self.as_str())] #[repr(u8)] pub enum Version { /// ## `v1` @@ -54,35 +57,6 @@ impl Version { } } -impl std::fmt::Display for Version { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.as_str()) - } -} - -impl JsonSchema for Version { - fn is_referenceable() -> bool { - false - } - - fn schema_id() -> Cow<'static, str> { - Cow::Borrowed("charted_core::api::Version") - } - - fn schema_name() -> String { - String::from("Version") - } - - fn json_schema(_: &mut SchemaGenerator) -> Schema { - Schema::Object(SchemaObject { - instance_type: Some(SingleOrVec::Single(InstanceType::Number.into())), - enum_values: Some(vec![Value::Number(1.into())]), - - ..Default::default() - }) - } -} - impl From for Version { fn from(value: u8) -> Self { match value { @@ -108,134 +82,146 @@ impl From for serde_json::Number { } } +#[cfg(feature = "schemars")] +#[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "schemars")))] +impl ::schemars::JsonSchema for Version { + fn is_referenceable() -> bool { + false + } + + fn schema_id() -> ::std::borrow::Cow<'static, str> { + ::std::borrow::Cow::Borrowed("charted_core::api::Version") + } + + fn schema_name() -> String { + String::from("Version") + } + + fn json_schema(_: &mut ::schemars::gen::SchemaGenerator) -> ::schemars::schema::Schema { + ::schemars::schema::Schema::Object(::schemars::schema::SchemaObject { + instance_type: Some(::schemars::schema::SingleOrVec::Single( + ::schemars::schema::InstanceType::Number.into(), + )), + + enum_values: Some(vec![::serde_json::Value::Number(1.into())]), + + ..Default::default() + }) + } +} + pub type Result = std::result::Result, Response>; -/// Represents a response object for all REST endpoints. -#[derive(Debug, Serialize, ToSchema)] +/// Representation of a response that the API server sends for each request. +#[derive(Debug, Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct Response { #[serde(skip)] - pub(crate) status: StatusCode, + pub(crate) status: axum::http::StatusCode, - /// Was the request a success or not? + /// Was the request that was processed a success? pub success: bool, - /// If the request sends a payload, this is where it'll be sent. `success` is always - /// *true*; if not the case, blame it on Noel. + /// The data that the REST endpoint sends back, if any. + /// + /// When this field is empty, it'll always respond with a `204 No Content` + /// status code if `errors` is also empty. + /// + /// The `success` field will always be set to `true` when + /// the `data` field is avaliable. All errors are handled + /// by the `errors` field. #[serde(default, skip_serializing_if = "Option::is_none")] pub data: Option, - /// If the request failed, this is a list of errors as a "stacktrace." `success` is always - /// *false*; if not the case, blame it on Noel. + /// The error trace for the request that was processed by + /// the API server. + /// + /// The `success` field will always be set to `false` when + /// the `errors` field is avaliable. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub errors: Vec, } -impl IntoResponse for Response { +#[cfg(feature = "axum")] +#[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "axum")))] +impl ::axum::response::IntoResponse for Response { fn into_response(self) -> axum::response::Response { - // Safety: we know that the derive macro for serde will always succeed. If this isn't - // the case, then it is considered undefined behaviour -- please file an issue - // if this is the case. - let data = unsafe { serde_json::to_string(&self).unwrap_unchecked() }; + let data = serde_json::to_string(&self).unwrap(); axum::http::Response::builder() .status(self.status) - .header(header::CONTENT_TYPE, "application/json; charset=utf-8") - .body(Body::from(data)) - .expect("this should succeed") + .header(axum::http::header::CONTENT_TYPE, "application/json; charset=utf-8") + .body(axum::body::Body::from(data)) + .unwrap() } } -/// Error that happened when going through a request. -#[derive(Debug, Serialize, ToSchema)] +/// Representation of a error from an error trace. +#[derive(Debug, Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct Error { - /// A contextual error code that can be looked up from the documentation to see - /// why the request failed. + /// Contextualized error code on why this request failed. + /// + /// This field can be looked up from the documentation to give + /// a better representation of the error. pub code: ErrorCode, - /// Humane message that is based off the contextual [error code][Error::code] to give - /// a brief description. + /// A humane description based off the contextualised `"code"` field. pub message: Cow<'static, str>, - /// Other details to send to the user to give even more context about this error. + /// If provided, this gives more information about the error + /// and why it could've possibly failed. #[serde(default, skip_serializing_if = "Option::is_none")] pub details: Option, } -impl From for Error { - fn from(err: eyre::Report) -> Self { - if cfg!(debug_assertions) { - return Error { - code: ErrorCode::SystemFailure, - message: Cow::Owned(format!("system failure occurred: {err}")), - details: { - let mut values = Vec::new(); - for err in err.chain().take(5) { - values.push(serde_json::Value::String(err.to_string())); - } - - Some(serde_json::json!({ - "causes": values, - })) - }, - }; - } - - Error { - code: ErrorCode::SystemFailure, - message: Cow::Borrowed("system failure occurred"), - details: None, - } - } -} - -/// Error object when a internal error had occurred. -pub const INTERNAL_SERVER_ERROR: Error = Error { - code: ErrorCode::InternalServerError, - message: Cow::Borrowed("internal server error"), - details: None, -}; - -/// Represents what kind this error is. -#[derive(Debug, Serialize, ToSchema)] +/// Contextualized error code on why this request failed. +/// +/// This field can be looked up from the documentation to give +/// a better representation of the error. +#[derive(Debug, Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum ErrorCode { - // ~ COMMON - /// Internal Server Error - InternalServerError, + /// A system failure occurred. + SystemFailure, - /// reached an unexpected 'end-of-file' marker - ReachedUnexpectedEof, + /// Unexpected EOF when encoding or decoding data. + UnexpectedEOF, - /// was unable to process something internally - UnableToProcess, + /// The endpoint that you're trying to reach is not avaliable. + RestEndpointNotFound, - /// given REST handler by your request was not found. - HandlerNotFound, + /// The endpoint that you're trying to reach is using an invalid HTTP method. + InvalidHTTPMethod, - /// given entity to lookup was not found. + /// The entity was not found. EntityNotFound, - /// given entity to create already exists. + /// The entity already exists. EntityAlreadyExists, - /// unable to validate the input data successfully - ValidationFailed, + /// Unexpected internal server error. + InternalServerError, - /// the query given for the CDN was not found. - UnknownCdnQuery, + /// Validation for the input data received failed. + ValidationFailed, - /// received an invalid `Content-Type` header value + /// The `Content-Type` header value was invalid. InvalidContentType, - /// this route requires a `Bearer` session to work. - SessionOnlyRoute, + /// Received an invalid HTTP header name. + InvalidHTTPHeaderName, + + /// Received an invalid HTTP header name. + InvalidHTTPHeaderValue, - /// received an invalid HTTP header key or value - InvalidHttpHeader, + /// This endpoint only allows Bearer tokens. + RequiresSessionToken, - /// was unable to decode expected Base64 data. + /// Unable to decode base64 content given. UnableToDecodeBase64, - /// was unable to decode into a ULID. + /// Unable to decode ULID given. UnableToDecodeUlid, /// received invalid UTF-8 data @@ -269,12 +255,17 @@ pub enum ErrorCode { /// missing a `Content-Type` header in your request MissingContentType, - /// system failure occurred. - SystemFailure, + /// reached an unexpected EOF marker. + ReachedUnexpectedEof, // ~ PATH PARAMETERS - /// received the wrong list of path parameters, this is usually a bug within the code - /// and nothing with you. + /// unable to parse a path parameter. + UnableToParsePathParameter, + + /// missing a required path parameter in the request. + MissingPathParameter, + + /// received the wrong list of path parameters, this is usually a bug within charted itself. WrongParameters, /// the server had failed to validate the path parameter's content. @@ -316,13 +307,6 @@ pub enum ErrorCode { /// the `?per_page` query parameter is maxed out to 100 MaxPerPageExceeded, - // ~ PATH PARAMETERS - /// unable to parse a path parameter. - UnableToParsePathParameter, - - /// missing a required path parameter in the request. - MissingPathParameter, - // ~ JSON BODY /// while parsing through the JSON tree received, something went wrong InvalidJsonPayload, @@ -343,7 +327,7 @@ pub enum ErrorCode { /// missing a multipart boundry to parse MissingMultipartBoundary, - /// expected multipart/form-data; received something else + /// expected `multipart/form-data`; received something else NoMultipartReceived, /// received incomplete multipart stream @@ -454,8 +438,10 @@ impl From<(ErrorCode, Cow<'static, str>, Option)> for Error { } } -/// Returns a successful API response. -pub fn ok(status: StatusCode, data: T) -> Response { +/// Return a successful API response. +#[cfg(feature = "axum")] +#[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "axum")))] +pub fn ok(status: axum::http::StatusCode, data: T) -> Response { Response { success: true, errors: Vec::new(), @@ -464,15 +450,24 @@ pub fn ok(status: StatusCode, data: T) -> Response { } } +/// Returns a empty HTTP API response. +#[cfg(feature = "axum")] +#[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "axum")))] pub fn no_content() -> Response<()> { - from_default(StatusCode::NO_CONTENT) + from_default(axum::http::StatusCode::NO_CONTENT) } -pub fn from_default(status: StatusCode) -> Response { +/// Return a success HTTP API response from `T`'s [`Default`] implementation. +#[cfg(feature = "axum")] +#[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "axum")))] +pub fn from_default(status: axum::http::StatusCode) -> Response { ok(status, T::default()) } -pub fn err>(status: StatusCode, error: E) -> Response { +/// Returns a failed HTTP API response. +#[cfg(feature = "axum")] +#[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "axum")))] +pub fn err>(status: axum::http::StatusCode, error: E) -> Response { let error = error.into(); Response { success: false, @@ -482,13 +477,38 @@ pub fn err>(status: StatusCode, error: E) -> Response { } } -/// Propagate a [`Response`] with the `500 Internal Server Error` HTTP status -/// and the [`INTERNAL_SERVER_ERROR`] error details. +/// Propagate a HTTP API response as a internal server error. +#[cfg(feature = "axum")] +#[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "axum")))] pub fn internal_server_error() -> Response { - err(StatusCode::INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR) + err( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + (ErrorCode::InternalServerError, "Internal Server Error"), + ) } /// Propagate a system failure response. -pub fn system_failure>(error: E) -> Response { - err(StatusCode::INTERNAL_SERVER_ERROR, error.into()) +#[cfg(feature = "axum")] +#[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "axum")))] +pub fn system_failure(error: E) -> Response { + if cfg!(debug_assertions) { + let mut errors = Vec::new(); + for err in error.source().iter().take(5) { + errors.push(Value::String(err.to_string())); + } + + return err( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + ( + ErrorCode::SystemFailure, + format!("system failure occurred: {error}"), + Some(Value::Array(errors)), + ), + ); + } + + err( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + (ErrorCode::SystemFailure, "system failure occurred", None), + ) } diff --git a/crates/core/src/bitflags.rs b/crates/core/src/bitflags.rs index 60471b8da..458c20a09 100644 --- a/crates/core/src/bitflags.rs +++ b/crates/core/src/bitflags.rs @@ -14,17 +14,47 @@ // limitations under the License. mod apikeyscope; -pub use apikeyscope::*; - mod member_permission; + +pub use apikeyscope::*; pub use member_permission::*; -use std::{cmp::min, collections::BTreeMap, fmt::Debug, marker::PhantomData}; +use std::{cmp::min, collections::BTreeMap, marker::PhantomData}; + +/// Trait that implements the "scopes" concept. +/// +/// The "scopes" concept is similar to [Discord's Permissions] where essentially +/// a **bitflags** represents a list of flags that will be serialized as a string +/// which uses bit-wise operations to determine if a flag is included. +/// +/// [Discord's Permissions]: https://github.com/discord/discord-api-docs/blob/main/docs/topics/Permissions.md +pub trait Bitflags: Sized + Send + Sync { + /// Type representation of a single bit. + type Bit: Copy; + + /// Constant that represents a zero of the type representation + /// of this trait. + const ZERO: Self::Bit; + + /// Returns a [`BTreeMap`] of all possible flags avaliable. + fn flags() -> BTreeMap<&'static str, Self::Bit>; + + /// Returns a slice of all avaliable bits from `0..{flags.len()}`. + fn values<'v>() -> &'v [Self::Bit]; + + fn max() -> Self::Bit + where + Self::Bit: Ord, + { + Self::values().iter().max().copied().unwrap_or(Self::ZERO) + } +} +/// Data structure that easily do computations with `F::Bit` easily. #[derive(Debug, Clone, Copy)] pub struct Bitfield(F::Bit, PhantomData); impl Bitfield { - /// Creates a new [`Bitfield`] instance. + /// Create a new [`Bitfield`] data structure. pub const fn new(value: F::Bit) -> Bitfield { Bitfield(value, PhantomData) } @@ -35,13 +65,14 @@ impl Bitfield { } } -// Since both `ApiKeyScope` and `MemberPermission` use `u64` as its `Bit` type, -// we will do our own silly impls here. +// Our implementation of bitflags (via `bitflags!`) use `u64` as the +// bit type repr. so it'll be tailoured to what we use for now. +// +// submit a pull request if you want other number types to be supported. impl> Bitfield { /// Returns all the possible enabled bits in the bitfield to determine pub fn flags(&self) -> Vec<(&'static str, F::Bit)> { - let flags = F::flags(); - flags.into_iter().filter(|(_, bit)| self.contains(*bit)).collect() + F::flags().into_iter().filter(|(_, bit)| self.contains(*bit)).collect() } /// Adds multiple bits to this [`Bitfield`] and updating the current @@ -56,8 +87,8 @@ impl> Bitfield { /// # #[allow(clippy::enum_clike_unportable_variant)] /// # #[repr(u64)] /// # pub Scope[u64] { - /// # Hello["hello"]: 1u64 << 0u64; - /// # World["world"]: 1u64 << 1u64; + /// # Hello["hello"] => 1u64 << 0u64; + /// # World["world"] => 1u64 << 1u64; /// # } /// # } /// # @@ -95,7 +126,7 @@ impl> Bitfield { /// value to what was acculumated. /// /// ## Example - /// ```no_run + /// ``` /// # use charted_core::{bitflags, bitflags::Bitfield}; /// # /// # bitflags! { @@ -103,8 +134,8 @@ impl> Bitfield { /// # #[allow(clippy::enum_clike_unportable_variant)] /// # #[repr(u64)] /// # pub Scope[u64] { - /// # Hello["hello"]: 1u64 << 0u64; - /// # World["world"]: 1u64 << 1u64; + /// # Hello["hello"] => 1u64 << 0u64; + /// # World["world"] => 1u64 << 1u64; /// # } /// # } /// # @@ -140,9 +171,9 @@ impl> Bitfield { } } -impl> Default for Bitfield { +impl Default for Bitfield { fn default() -> Self { - Bitfield(u64::default(), PhantomData) + Bitfield(F::ZERO, PhantomData) } } @@ -155,26 +186,7 @@ impl> FromIterator for Bitfield { } } -/// Trait that is implemented by the [`bitflags`][bitflags] macro. -pub trait Bitflags: Sized + Send + Sync { - /// Type that represents the bit. - type Bit: Copy; - - /// Returns a [`BTreeMap`] of mappings of `flag => bit value` - fn flags() -> BTreeMap<&'static str, Self::Bit>; - - /// Returns an immutable slice of the avaliable bits - fn values<'v>() -> &'v [Self::Bit]; - - /// Returns the maximum element - fn max() -> Self::Bit - where - Self::Bit: Ord, - { - Self::values().iter().max().copied().unwrap() - } -} - +/// Macro that create a enumeration to implement the [`Bitflags`] trait. #[macro_export] macro_rules! bitflags { ( @@ -182,7 +194,7 @@ macro_rules! bitflags { $vis:vis $name:ident[$bit:ty] { $( $(#[$doc:meta])* - $field:ident[$key:literal]: $value:expr; + $field:ident[$key:literal] => $value:expr; )* } ) => { @@ -199,6 +211,12 @@ macro_rules! bitflags { } } + impl ::std::fmt::Display for $name { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "{}", self.as_bit()) + } + } + impl ::core::convert::From<$name> for $bit { fn from(value: $name) -> $bit { value as $bit @@ -207,12 +225,14 @@ macro_rules! bitflags { impl $crate::bitflags::Bitflags for $name { type Bit = $bit; + const ZERO: Self::Bit = 0; #[inline] fn flags() -> ::std::collections::BTreeMap<&'static str, u64> { - ::azalia::btreemap! { - $($key => $value),* - } + let mut map = ::std::collections::BTreeMap::new(); + $(map.insert($key, $value);)* + + map } fn values<'v>() -> &'v [$bit] { diff --git a/crates/core/src/bitflags/apikeyscope.rs b/crates/core/src/bitflags/apikeyscope.rs index 681dd1f69..5991ae12a 100644 --- a/crates/core/src/bitflags/apikeyscope.rs +++ b/crates/core/src/bitflags/apikeyscope.rs @@ -13,12 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use serde::{Deserialize, Serialize}; -use std::borrow::Cow; -use utoipa::{ - openapi::{schema::SchemaType, KnownFormat, ObjectBuilder, OneOfBuilder, RefOr, Schema, SchemaFormat, Type}, - PartialSchema, ToSchema, -}; +use serde::{Deserialize, Serialize, Serializer}; pub type ApiKeyScopes = crate::bitflags::Bitfield; @@ -31,145 +26,175 @@ crate::bitflags! { // User Scopes // +~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+ /// Allows the current authenticated user to access metadata about themselves. - UserAccess["user:access"]: 1u64 << 0u64; + UserAccess["user:access"] => 1u64 << 0u64; /// Allows the authenticated user to patch any user metadata. - UserUpdate["user:update"]: 1u64 << 1u64; + UserUpdate["user:update"] => 1u64 << 1u64; /// Allows to delete the current authenticated user. - UserDelete["user:delete"]: 1u64 << 2u64; + UserDelete["user:delete"] => 1u64 << 2u64; /// Allows the current authenticated user to access their connections like their /// GitHub or GitLab accounts, this is usually for OIDC providers that can query /// a user by an identifier that can identify a user into charted-server. /// /// As of v0.1.0-beta, this scope is not used at all. - UserConnections["user:connections"]: 1u64 << 3u64; - - /// **UNUSED AS OF v0.1.0-beta** - /// - /// Allows the current authenticated user to read from their notifications. - #[allow(unused)] - UserNotifications["user:notifications"]: 1u64 << 4u64; + UserConnections["user:connections"] => 1u64 << 3u64; /// Allows the current authenticated user to update their user avatar. - UserAvatarUpdate["user:avatar:update"]: 1u64 << 5u64; + UserAvatarUpdate["user:avatar:update"] => 1u64 << 4u64; /// Allows the current authenticated user to list their current sessions. - UserSessionsList["user:sessions:list"]: 1u64 << 6u64; + UserSessionsList["user:sessions:list"] => 1u64 << 5u64; // +~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+ // Repository Scopes // +~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+ /// Allows access through private repositories that the current authenticated user /// can access, except repositories that they are a member apart of. - RepoAccess["repo:access"]: 1u64 << 7u64; + RepoAccess["repo:access"] => 1u64 << 6u64; /// Allows the creation of public or private repositories that will be owned /// by the current authenticated user. - RepoCreate["repo:create"]: 1u64 << 8u64; + RepoCreate["repo:create"] => 1u64 << 7u64; /// Allows the deletion of public or private repositories that will be owned /// by the current authenticated user. - RepoDelete["repo:delete"]: 1u64 << 9u64; + RepoDelete["repo:delete"] => 1u64 << 8u64; /// Allows patching a public or private repository that is owned by the current /// authenticated user. - RepoUpdate["repo:update"]: 1u64 << 10u64; - - /// **DEPRECATED AS OF v0.1.0-beta** - /// - /// This is replaced by the `repo:releases:create` API key scope, please - /// use that instead. - #[deprecated(since = "0.1.0-beta", note = "Replaced by ApiKeyScope::RepoReleaseCreate")] - RepoWrite["repo:write"]: 1u64 << 11u64; + RepoUpdate["repo:update"] => 1u64 << 9u64; /// Allows patching a repository icon that the current authenticated user owns. - RepoIconUpdate["repo:icon:update"]: 1u64 << 12u64; + RepoIconUpdate["repo:icon:update"] => 1u64 << 10u64; /// Allows the creation of creating repository releases that the current authenticated /// user owns. - RepoReleaseCreate["repo:releases:create"]: 1u64 << 13u64; + RepoReleaseCreate["repo:releases:create"] => 1u64 << 11u64; /// Allows patching repository releases that the current authenticated user owns. - RepoReleaseUpdate["repo:releases:update"]: 1u64 << 14u64; + RepoReleaseUpdate["repo:releases:update"] => 1u64 << 12u64; /// Allows the deletion of repository releases that the current authenticated user owns. - RepoReleaseDelete["repo:releases:delete"]: 1u64 << 15u64; + RepoReleaseDelete["repo:releases:delete"] => 1u64 << 13u64; /// Allows viewing all repository members. - RepoMembersList["repo:members:list"]: 1u64 << 16u64; + RepoMembersList["repo:members:list"] => 1u64 << 14u64; /// Allows patching repository member metadata. - RepoMemberUpdate["repo:members:update"]: 1u64 << 17u64; + RepoMemberUpdate["repo:members:update"] => 1u64 << 15u64; /// Allows kicking repository members off the repository. - RepoMemberKick["repo:members:kick"]: 1u64 << 18u64; + RepoMemberKick["repo:members:kick"] => 1u64 << 16u64; + + /// Allows viewing all repository member invites. + /// + /// This scope is only used if the [`charted-emails`](https://github.com/charted-dev/emails) gRPC server is + /// running and configured via the [`config.emails_grpc_endpoint`][emails_grpc_endpoint] configuration key. + /// + /// [emails_grpc_endpoint] => https://charts.noelware.org/docs/server/latest/self-hosting/configuration#emails_grpc_endpoint + RepoMemberInviteAccess["repo:members:invites:access"] => 1u64 << 17u64; + + /// Deletes a repository member's invite + RepoMemberInviteDelete["repo:members:invites:delete"] => 1u64 << 18u64; + + /// Allows a user to view a repository's webhooks. + /// + /// This scope is only used via the [HTTP webhooks] feature. + /// + /// [HTTP webhooks]: https://charts.noelware.org/docs/server/latest/features/webhooks + RepoWebhookList["repo:webhooks:list"] => 1u64 << 19u64; + + /// Allows a user to create a HTTP webhook in a repository. + /// + /// This scope is only used via the [HTTP webhooks] feature. + /// + /// [HTTP webhooks]: https://charts.noelware.org/docs/server/latest/features/webhooks + RepoWebhookCreate["repo:webhooks:create"] => 1u64 << 20u64; + + /// Allows a user to update a repository webhook's metadata. + /// + /// This scope is only used via the [HTTP webhooks] feature. + /// + /// [HTTP webhooks]: https://charts.noelware.org/docs/server/latest/features/webhooks + RepoWebhookUpdate["repo:webhooks:update"] => 1u64 << 21u64; - /// Allows viewing all repository member invites. This scope is only used if the - /// [`charted-emails`](https://github.com/charted-dev/emails) gRPC server is running - /// and configured via the [`config.emails_grpc_endpoint`][emails_grpc_endpoint] - /// configuration key. + /// Allows a user to delete repository webhooks. /// - /// [emails_grpc_endpoint]: https://charts.noelware.org/docs/server/latest/self-hosting/configuration#emails_grpc_endpoint - RepoMemberInviteAccess["repo:members:invites:access"]: 1u64 << 19u64; - - /// **DEPRECTEAD AS OF v0.1.0-beta**: This is unused. - #[deprecated(since = "0.1.0-beta", note = "This is unused.")] - RepoMemberInviteUpdate["repo:members:invites:update"]: 1u64 << 20u64; - RepoMemberInviteDelete["repo:members:invites:delete"]: 1u64 << 21u64; - RepoWebhookList["repo:webhooks:list"]: 1u64 << 22u64; - RepoWebhookCreate["repo:webhooks:create"]: 1u64 << 23u64; - RepoWebhookUpdate["repo:webhooks:update"]: 1u64 << 24u64; - RepoWebhookDelete["repo:webhooks:delete"]: 1u64 << 25u64; - RepoWebhookEventAccess["repo:webhooks:events:access"]: 1u64 << 26u64; - RepoWebhookEventDelete["repo:webhooks:events:delete"]: 1u64 << 27u64; + /// This scope is only used via the [HTTP webhooks] feature. + /// + /// [HTTP webhooks]: https://charts.noelware.org/docs/server/latest/features/webhooks + RepoWebhookDelete["repo:webhooks:delete"] => 1u64 << 22u64; + + /// Allows a user to view a repository webhook's event data. + /// + /// This scope is only used via the [HTTP webhooks] feature. + /// + /// [HTTP webhooks]: https://charts.noelware.org/docs/server/latest/features/webhooks + RepoWebhookEventAccess["repo:webhooks:events:access"] => 1u64 << 23u64; + + /// Allows a user to delete a repository webhook's event data. + /// + /// This scope is only used via the [HTTP webhooks] feature. + /// + /// [HTTP webhooks]: https://charts.noelware.org/docs/server/latest/features/webhooks + RepoWebhookEventDelete["repo:webhooks:events:delete"] => 1u64 << 24u64; // +~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+ // API Key Scopes // +~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+ - ApiKeyView["apikeys:view"]: 1u64 << 28u64; - ApiKeyList["apikeys:list"]: 1u64 << 52u64; - ApiKeyCreate["apikeys:create"]: 1u64 << 29u64; - ApiKeyDelete["apikeys:delete"]: 1u64 << 30u64; - ApiKeyUpdate["apikeys:update"]: 1u64 << 31u64; + /// Allows a user to view a single API key. + ApiKeyView["apikeys:view"] => 1u64 << 25u64; + + /// Allows a user to list all API keys. + ApiKeyList["apikeys:list"] => 1u64 << 26u64; + + /// Allows a user to update a API key. + ApiKeyCreate["apikeys:create"] => 1u64 << 27u64; + + /// Allows a user to delete a API key. + ApiKeyDelete["apikeys:delete"] => 1u64 << 28u64; + + /// Allows a user to update a API key's metadata. + ApiKeyUpdate["apikeys:update"] => 1u64 << 29u64; // +~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+ // Organization Scopes // +~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+ - OrgAccess["org:access"]: 1u64 << 32u64; - OrgCreate["org:create"]: 1u64 << 33u64; - OrgUpdate["org:update"]: 1u64 << 34u64; - OrgDelete["org:delete"]: 1u64 << 35u64; - OrgMemberInvites["org:members:invites"]: 1u64 << 36u64; - OrgMemberList["org:members:list"]: 1u64 << 37u64; - OrgMemberKick["org:members:kick"]: 1u64 << 38u64; - OrgMemberUpdate["org:members:update"]: 1u64 << 39u64; - OrgWebhookList["org:webhooks:list"]: 1u64 << 40u64; - OrgWebhookCreate["org:webhooks:create"]: 1u64 << 41u64; - OrgWebhookUpdate["org:webhooks:update"]: 1u64 << 42u64; - OrgWebhookDelete["org:webhooks:delete"]: 1u64 << 43u64; - OrgWebhookEventList["org:webhooks:events:list"]: 1u64 << 44u64; - OrgWebhookEventDelete["org:webhooks:events:delete"]: 1u64 << 45u64; + OrgAccess["org:access"] => 1u64 << 30u64; + OrgCreate["org:create"] => 1u64 << 31u64; + OrgUpdate["org:update"] => 1u64 << 32u64; + OrgDelete["org:delete"] => 1u64 << 33u64; + OrgMemberInvites["org:members:invites"] => 1u64 << 34u64; + OrgMemberList["org:members:list"] => 1u64 << 35u64; + OrgMemberKick["org:members:kick"] => 1u64 << 36u64; + OrgMemberUpdate["org:members:update"] => 1u64 << 37u64; + OrgWebhookList["org:webhooks:list"] => 1u64 << 38u64; + OrgWebhookCreate["org:webhooks:create"] => 1u64 << 39u64; + OrgWebhookUpdate["org:webhooks:update"] => 1u64 << 40u64; + OrgWebhookDelete["org:webhooks:delete"] => 1u64 << 41u64; + OrgWebhookEventList["org:webhooks:events:list"] => 1u64 << 42u64; + OrgWebhookEventDelete["org:webhooks:events:delete"] => 1u64 << 43u64; // +~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+ // Administration Scopes // +~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+ - AdminStats["admin:stats"]: 1u64 << 46u64; - AdminUserCreate["admin:users:create"]: 1u64 << 47u64; - AdminUserDelete["admin:users:delete"]: 1u64 << 48u64; - AdminUserUpdate["admin:users:update"]: 1u64 << 49u64; - AdminOrgDelete["admin:orgs:delete"]: 1u64 << 50u64; - AdminOrgUpdate["admin:orgs:update"]: 1u64 << 51u64; + AdminStats["admin:stats"] => 1u64 << 44u64; + AdminUserCreate["admin:users:create"] => 1u64 << 45u64; + AdminUserDelete["admin:users:delete"] => 1u64 << 46u64; + AdminUserUpdate["admin:users:update"] => 1u64 << 47u64; + AdminOrgDelete["admin:orgs:delete"] => 1u64 << 48u64; + AdminOrgUpdate["admin:orgs:update"] => 1u64 << 49u64; } } impl Serialize for ApiKeyScope { fn serialize(&self, serializer: S) -> Result where - S: serde::Serializer, + S: Serializer, { - serializer.serialize_u64(self.as_bit()) + serializer.serialize_str(&self.to_string()) } } @@ -213,13 +238,21 @@ impl<'de> Deserialize<'de> for ApiKeyScope { } } -impl PartialSchema for ApiKeyScope { - fn schema() -> RefOr { - let flags = ::flags(); - let max = ::max(); +#[cfg(feature = "openapi")] +const _: () = { + use utoipa::{ + openapi::{schema::SchemaType, KnownFormat, ObjectBuilder, OneOfBuilder, RefOr, Schema, SchemaFormat, Type}, + PartialSchema, ToSchema, + }; + + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "openapi")))] + impl PartialSchema for ApiKeyScope { + fn schema() -> RefOr { + let flags = ::flags(); + let max = ::max(); - RefOr::T(Schema::OneOf({ - let oneof = OneOfBuilder::new() + RefOr::T(Schema::OneOf({ + let oneof = OneOfBuilder::new() .description(Some( "Representation of a API key scope. A scope determines a permission between an API key", )) @@ -242,16 +275,14 @@ impl PartialSchema for ApiKeyScope { object.build() })); - oneof.build() - })) + oneof.build() + })) + } } -} -impl ToSchema for ApiKeyScope { - fn name() -> Cow<'static, str> { - Cow::Borrowed("ApiKeyScope") - } -} + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "openapi")))] + impl ToSchema for ApiKeyScope {} +}; impl FromIterator for crate::bitflags::ApiKeyScopes { fn from_iter>(iter: T) -> Self { @@ -287,14 +318,14 @@ mod tests { { let value: ApiKeyScope = serde_json::from_str("16384").unwrap(); - assert_eq!(value, ApiKeyScope::RepoReleaseUpdate); + assert_eq!(value, ApiKeyScope::RepoMembersList); } { let value: ApiKeyScope = serde_json::from_str::(&format!("{}", ::max())).unwrap(); - assert_eq!(value, ApiKeyScope::ApiKeyList); + assert_eq!(value, ApiKeyScope::AdminOrgUpdate); } // error case #1: if we pass in zero diff --git a/crates/core/src/bitflags/member_permission.rs b/crates/core/src/bitflags/member_permission.rs index 2d0eac2de..88baec78b 100644 --- a/crates/core/src/bitflags/member_permission.rs +++ b/crates/core/src/bitflags/member_permission.rs @@ -22,39 +22,39 @@ crate::bitflags! { pub MemberPermission[u64] { /// This member has permission to invite new members into this repository or organization /// and can view all other pending invites. - MemberInvite["member:invite"]: 1u64 << 0u64; + MemberInvite["member:invite"] => 1u64 << 0u64; /// This member has the permission to update any other member's permissions - MemberUpdate["member:update"]: 1u64 << 1u64; + MemberUpdate["member:update"] => 1u64 << 1u64; /// This member has the permission to kick other members from the repository or organization - MemberKick["member:kick"]: 1u64 << 2u64; + MemberKick["member:kick"] => 1u64 << 2u64; /// This member has permission to update any repository or organization metadata - MetadataUpdate["metadata:update"]: 1u64 << 3u64; + MetadataUpdate["metadata:update"] => 1u64 << 3u64; /// > This is only for organization members, this will be nop for repository members /// /// This member has permission to create repositories in an organization. - RepoCreate["repo:create"]: 1u64 << 4u64; + RepoCreate["repo:create"] => 1u64 << 4u64; /// > This is only for organization members, this will be nop for repository members /// /// This member has permission to delete repositories in an organization. - RepoDelete["repo:delete"]: 1u64 << 5u64; + RepoDelete["repo:delete"] => 1u64 << 5u64; /// This member has permission to create additional repository or organization /// webhooks. - WebhookCreate["webhooks:create"]: 1u64 << 6u64; + WebhookCreate["webhooks:create"] => 1u64 << 6u64; /// This member has permission to update repository or organization webhooks. - WebhookUpdate["webhooks:update"]: 1u64 << 7u64; + WebhookUpdate["webhooks:update"] => 1u64 << 7u64; /// This member has permission to delete additional repository or organization webhooks. - WebhookDelete["webhooks:delete"]: 1u64 << 8u64; + WebhookDelete["webhooks:delete"] => 1u64 << 8u64; /// This member has permission to delete external metadata in an organization /// or repository, like repository releases - MetadataDelete["metadata:delete"]: 1u64 << 9u64; + MetadataDelete["metadata:delete"] => 1u64 << 9u64; } } diff --git a/crates/core/src/di.rs b/crates/core/src/di.rs new file mode 100644 index 000000000..735eb20be --- /dev/null +++ b/crates/core/src/di.rs @@ -0,0 +1,65 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Dependency Injection module. + +use std::{ + any::{Any, TypeId}, + collections::HashMap, + fmt::Debug, + sync::Arc, +}; + +#[derive(Debug, derive_more::Display)] +pub enum Error { + #[display("object already exists")] + ObjectAlreadyExists, + + #[display("failed to cast")] + FailedCast, + + #[display("object is unavaliable")] + ObjectUnavaliable, +} + +impl std::error::Error for Error {} + +/// A container that holds all the dependencies of the API server. +#[derive(Debug, Clone, Default)] +pub struct Container { + objects: HashMap>, +} + +impl Container { + /// Installs a injectable of `I` into this container. + pub fn install(&mut self, object: I) -> Result<(), Error> { + let id = TypeId::of::(); + if self.objects.contains_key(&id) { + return Err(Error::ObjectAlreadyExists); + } + + self.objects.insert(id, Arc::new(object)); + Ok(()) + } + + /// Get a [`Injectable`] from this container, returns `None` if it can't be found. + pub fn get(&self) -> Result<&T, Error> { + self.objects + .get(&TypeId::of::()) + .ok_or(Error::FailedCast)? + .downcast_ref() + .ok_or(Error::ObjectUnavaliable) + } +} diff --git a/crates/core/src/distribution.rs b/crates/core/src/distribution.rs index 78d2ddb16..9dd1c2136 100644 --- a/crates/core/src/distribution.rs +++ b/crates/core/src/distribution.rs @@ -15,7 +15,6 @@ use serde::Serialize; use std::{env, fmt::Display, fs, path::PathBuf, sync::OnceLock}; -use utoipa::ToSchema; const KUBERNETES_SERVICE_TOKEN_FILE: &str = "/run/secrets/kubernetes.io/serviceaccount/token"; const KUBERNETES_NAMESPACE_FILE: &str = "/run/secrets/kubernetes.io/serviceaccount/namespace"; @@ -52,7 +51,8 @@ fn is_in_docker_container() -> bool { has_dockerenv || has_cgroup } -#[derive(Debug, Clone, Copy, Serialize, Default, PartialEq, Eq, ToSchema)] +#[derive(Debug, Clone, Copy, Serialize, Default, PartialEq, Eq)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[serde(rename_all = "lowercase")] pub enum Distribution { /// Running on a Kubernetes cluster. diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index e4dfc02c0..5232e724a 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -13,30 +13,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![feature(once_cell_try)] -#![feature(ptr_as_ref_unchecked)] - -mod distribution; -pub use distribution::*; +#![cfg_attr(any(noeldoc, docsrs), feature(doc_cfg))] +#![doc(html_logo_url = "https://cdn.floofy.dev/images/trans.png")] +#![doc(html_favicon_url = "https://cdn.floofy.dev/images/trans.png")] pub mod api; pub mod bitflags; +pub mod di; pub mod serde; -pub mod ulid; - -#[cfg(feature = "testkit")] -pub mod testkit; #[macro_use] -#[path = "macros.rs"] -mod macros_; +mod macros; +mod distribution; + +pub use distribution::*; use argon2::Argon2; -use rand::distributions::{Alphanumeric, DistString}; -use std::{ - fmt, - sync::{LazyLock, OnceLock}, -}; +use rand::distr::{Alphanumeric, SampleString}; +use std::sync::{LazyLock, OnceLock}; /// Type-alias that represents a boxed future. pub type BoxedFuture<'a, Output> = @@ -56,29 +50,45 @@ pub const BUILD_DATE: &str = env!("CHARTED_BUILD_DATE"); /// Returns the current version of `charted-server`. pub const VERSION: &str = env!("CARGO_PKG_VERSION"); -#[allow(clippy::incompatible_msrv)] +/// A lazily cached [`Argon2`] instance that is used within +/// the internal `charted-*` crates. pub static ARGON2: LazyLock = LazyLock::new(Argon2::default); -/// Returns a formtted version string of `v0.0.0+{commit hash}` if [`COMMIT_HASH`] is not empty -/// or `d1cebae`. Otherwise, `v0.0.0` is returned instead. +#[doc(hidden)] +pub static CONTAINER: OnceLock = OnceLock::new(); + +/// Returns a formatted string of the version that combines the [`VERSION`] and [`COMMIT_HASH`] +/// constants as v[{version}][VERSION]+[{commit.hash}][COMMIT_HASH]. +/// +/// If the [`COMMIT_HASH`] is empty (i.e, not by using `git` or wasn't found on system), it'll +/// return v[{version}][VERSION] instead. This is also returned on the `nixpkgs` +/// version of **charted** and **charted-helm-plugin**. pub fn version() -> &'static str { static ONCE: OnceLock = OnceLock::new(); - ONCE.get_or_try_init(|| -> Result { + ONCE.get_or_init(|| { use std::fmt::Write; let mut buf = String::new(); - write!(buf, "v{VERSION}")?; + write!(buf, "v{VERSION}").unwrap(); - #[allow(clippy::const_is_empty)] // lint is right but sometimes `git rev-parse --short=8 HEAD` will return empty + // the lint here is correct, but `git rev-parse --short=8 HEAD` can possibly + // return nothing, so the lint is wrong in that case. + #[allow(clippy::const_is_empty)] if !(COMMIT_HASH == "d1cebae" || COMMIT_HASH.is_empty()) { - write!(buf, "+{COMMIT_HASH}")?; + write!(buf, "+{COMMIT_HASH}").unwrap(); } - Ok(buf) + buf }) - .unwrap_or_else(|e| panic!("internal error: {e}")) } +/// Generates a random string with `len`. pub fn rand_string(len: usize) -> String { - Alphanumeric.sample_string(&mut rand::thread_rng(), len) + Alphanumeric.sample_string(&mut rand::rng(), len) +} + +pub fn set_container(container: di::Container) { + CONTAINER + .set(container) + .expect("di container should be empty when called"); } diff --git a/crates/core/src/macros.rs b/crates/core/src/macros.rs index 81dd1a756..6bffe91d7 100644 --- a/crates/core/src/macros.rs +++ b/crates/core/src/macros.rs @@ -12,56 +12,3 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - -/// Creates a new "newtype" wrapper that implements the following traits: -/// -/// * [`core::ops::Deref`] -/// * [`core::convert::From`] -/// -/// To add more external traits, you can add `#[derive]` at the top of the statement: -/// ```no_run -/// charted_core::create_newtype_wrapper!( -/// /// doc comment is also accepted! -/// #[derive(Debug)] -/// pub S for String; -/// ); -/// -/// println!("{:?}", S::from(String::from("hello world"))); -/// ``` -#[macro_export] -macro_rules! create_newtype_wrapper { - ( - $(#[$meta:meta])* - $vis:vis $name:ident$(<$generics:tt>)? for $pubvis:vis $ty:ty; - ) => { - $(#[$meta])* - $vis struct $name $(<$generics>)? ($pubvis $ty); - - impl$(<$generics>)? ::core::convert::From<$ty> for $name $(<$generics>)? { - fn from(value: $ty) -> Self { - Self(value) - } - } - - impl$(<$generics>)? ::core::ops::Deref for $name$(<$generics>)? { - type Target = $ty; - fn deref(&self) -> &Self::Target { - &self.0 - } - } - }; -} - -/// The `mk_from_newtype!` macro allows to implement [`core::convert::From`] -> U easily. -#[macro_export] -macro_rules! mk_from_newtype { - ($(from $T:ty as $U:ty),*) => { - $( - impl ::core::convert::From<$T> for $U { - fn from(value: $T) -> Self { - value.0 - } - } - )* - }; -} diff --git a/crates/core/src/serde/mod.rs b/crates/core/src/serde.rs similarity index 99% rename from crates/core/src/serde/mod.rs rename to crates/core/src/serde.rs index 931df9c8f..52d9d1082 100644 --- a/crates/core/src/serde/mod.rs +++ b/crates/core/src/serde.rs @@ -14,4 +14,5 @@ // limitations under the License. mod duration; + pub use duration::*; diff --git a/crates/core/src/serde/duration.rs b/crates/core/src/serde/duration.rs index 8bcc54377..2e5ea02d0 100644 --- a/crates/core/src/serde/duration.rs +++ b/crates/core/src/serde/duration.rs @@ -14,21 +14,22 @@ // limitations under the License. use serde::{de, Deserialize, Serialize}; -use std::{ - borrow::Cow, - fmt::{Debug, Display}, - ops::Deref, - str::FromStr, -}; -use utoipa::{ - openapi::{schema::SchemaType, KnownFormat, ObjectBuilder, OneOfBuilder, RefOr, Schema, SchemaFormat, Type}, - PartialSchema, ToSchema, -}; - -/// Newtype wrapper for [`std::time::Duration`] that implements [`serde::Serialize`], [`serde::Deserialize`] -/// and [`utoipa::ToSchema`]. -#[derive(Clone, Copy, Default, PartialEq, PartialOrd, Eq, Ord, Hash)] -pub struct Duration(::std::time::Duration); +use std::{fmt::Display, ops::Deref, str::FromStr}; + +/// Newtype wrapper for [`std::time::Duration`]. +/// +/// This newtype wrapper implements all the standard library types, [`serde::Serialize`], +/// [`serde::Deserialize`], and others provided by feature flags: +/// +#[cfg_attr( + feature = "openapi", + doc = "* [`utoipa::PartialSchema`], [`utoipa::ToSchema`] (via the `openapi` crate feature)" +)] +/// +/// [`utoipa::PartialSchema`]: https://docs.rs/utoipa/*/utoipa/trait.PartialSchema.html +/// [`utoipa::ToSchema`]: https://docs.rs/utoipa/*/utoipa/trait.ToSchema.html +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Duration(std::time::Duration); impl Duration { /// Creates a new `Duration` from the specified number of whole seconds. /// @@ -47,12 +48,6 @@ impl Duration { } } -impl Debug for Duration { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Debug::fmt(&self.0, f) - } -} - impl Display for Duration { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let fmter = humantime::format_duration(self.0); @@ -126,6 +121,12 @@ impl From for std::time::Duration { } } +impl From<&Duration> for std::time::Duration { + fn from(value: &Duration) -> Self { + value.0 + } +} + impl Deref for Duration { type Target = ::std::time::Duration; fn deref(&self) -> &Self::Target { @@ -133,9 +134,18 @@ impl Deref for Duration { } } -impl PartialSchema for Duration { - fn schema() -> RefOr { - let oneof = OneOfBuilder::new() +#[cfg(feature = "openapi")] +const _: () = { + use std::borrow::Cow; + use utoipa::{ + openapi::{schema::SchemaType, KnownFormat, ObjectBuilder, OneOfBuilder, RefOr, Schema, SchemaFormat, Type}, + PartialSchema, ToSchema, + }; + + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "openapi")))] + impl PartialSchema for Duration { + fn schema() -> RefOr { + let oneof = OneOfBuilder::new() .description(Some("`Duration` is represented as a span of time, usually for system timeouts. `charted-server` supports passing in a unsigned 64-bot integer (represented in milliseconds) or with a string literal (i.e, `1s`) to represent time.")) .item({ ObjectBuilder::new() @@ -151,32 +161,17 @@ impl PartialSchema for Duration { .build() }); - RefOr::T(Schema::OneOf(oneof.build())) - } -} - -impl ToSchema for Duration { - fn name() -> Cow<'static, str> { - Cow::Borrowed("Duration") - } -} - -#[cfg(feature = "merge")] -impl ::azalia::config::merge::Merge for Duration { - fn merge(&mut self, other: Duration) { - // if both durations are zero, then don't attempt to merge - if self.is_zero() && other.is_zero() { - return; + RefOr::T(Schema::OneOf(oneof.build())) } + } - // If `self` isn't zero AND `other` is zero, don't attempt to merge - if !self.is_zero() && other.is_zero() { - return; + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "openapi")))] + impl ToSchema for Duration { + fn name() -> Cow<'static, str> { + Cow::Borrowed("Duration") } - - *self = other; } -} +}; #[cfg(test)] mod tests { diff --git a/crates/core/src/testkit/containers.rs b/crates/core/src/testkit/containers.rs deleted file mode 100644 index 8483c409e..000000000 --- a/crates/core/src/testkit/containers.rs +++ /dev/null @@ -1,31 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use testcontainers_modules::{ - postgres::Postgres, - testcontainers::{runners::AsyncRunner, ContainerAsync, ImageExt}, -}; - -// renovate: datasource=docker rev=library/postgres -const TAG: &str = "16.2"; - -pub async fn postgresql(tag: Option<&str>) -> ContainerAsync { - Postgres::default() - .with_db_name("charted") - .with_tag(tag.unwrap_or(TAG)) - .start() - .await - .expect("failed to start container") -} diff --git a/crates/core/src/ulid.rs b/crates/core/src/ulid.rs index 486540adc..e69de29bb 100644 --- a/crates/core/src/ulid.rs +++ b/crates/core/src/ulid.rs @@ -1,152 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! An atomic Ulid generator. - -use crate::api; -use rand::{rngs::OsRng, Rng}; -use std::{ - borrow::Cow, - fmt::Display, - sync::atomic::{AtomicPtr, Ordering}, - time::{Duration, SystemTime}, -}; -use ulid::Ulid; - -/// [`Error`] when the monotonic clock overflows -#[derive(Debug)] -pub struct MonotonicTimeOverflow { - _priv: (), -} - -impl Display for MonotonicTimeOverflow { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("montonic clock overflow?!") - } -} - -impl std::error::Error for MonotonicTimeOverflow {} -impl From for api::Error { - fn from(_: MonotonicTimeOverflow) -> Self { - api::Error { - code: api::ErrorCode::SystemFailure, - message: Cow::Borrowed("monotonic clock when generating ulids overflowed?!"), - details: None, - } - } -} - -/// An atomic Ulid generator. -#[derive(Debug)] -pub struct AtomicGenerator { - previous_ulid: AtomicPtr, -} - -impl Clone for AtomicGenerator { - fn clone(&self) -> Self { - AtomicGenerator { - previous_ulid: AtomicPtr::new(self.previous_ulid.load(Ordering::SeqCst)), - } - } -} - -impl AtomicGenerator { - /// Creates a new [`AtomicGenerator`] instance. - #[allow(clippy::new_without_default)] - pub fn new() -> AtomicGenerator { - AtomicGenerator { - previous_ulid: AtomicPtr::new(&mut Ulid::nil()), - } - } - - pub(self) fn previous(&self) -> *mut Ulid { - self.previous_ulid.load(Ordering::SeqCst) - } - - /// Generates a new [`Ulid`] atomically with the current system time and the OS' random generator - /// as the entropy source. - pub fn generate(&self) -> Result { - self._generate(SystemTime::now(), &mut OsRng) - } - - /// Generates a new [`Ulid`] atomically with a specified [`SystemTime`] and uses the operating - /// system's random generator as the entropy source. - pub fn generate_with_time(&self, time: SystemTime) -> Result { - self._generate(time, &mut OsRng) - } - - // Credit for the implementation: https://github.com/dylanhart/ulid-rs/blob/05499bd1609f9ac4dd5f39e0bbf70da529179aed/src/generator.rs - fn _generate(&self, time: SystemTime, entropy: &mut R) -> Result { - // Safety: we ensured that all the points in the Safety documentation for - // `AtomicPtr::::as_ref_unchecked()` are true: - // - // * `Ulid::nil()` will resolve in a valid ptr - // * The previous ULID constructed from `AtomicGenerator::new()` is not a null pointer. - let ulid = unsafe { self.previous().as_ref_unchecked() }; - let last_timestamp = ulid.timestamp_ms(); - - // We are going to assume either time went backwards OR it is the same - // millisecond timestamp. If so, then we should increment that it is - // monotonic. - if time - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap_or(Duration::ZERO) - .as_millis() - <= u128::from(last_timestamp) - { - if let Some(mut next) = ulid.increment() { - self.previous_ulid.store(&mut next, Ordering::SeqCst); - return Ok(next); - } else { - return Err(MonotonicTimeOverflow { _priv: () }); - } - } - - let mut next = Ulid::from_datetime_with_source(time, entropy); - self.previous_ulid.store(&mut next, Ordering::SeqCst); - - Ok(next) - } -} - -#[cfg(test)] -mod tests { - use super::AtomicGenerator; - use std::time::{Duration, SystemTime}; - use ulid::Ulid; - - fn __assert_send() {} - fn __assert_sync() {} - - #[test] - fn test_monotonicity() { - let now = SystemTime::now(); - let generator = AtomicGenerator::new(); - - let ulid1 = generator.generate_with_time(now).unwrap(); - let ulid2 = generator.generate_with_time(now).unwrap(); - let ulid3 = Ulid::from_datetime(now + Duration::from_millis(1)); - - assert_eq!(ulid1.0 + 1, ulid2.0); - assert!(ulid2 < ulid3); - assert!(ulid2.timestamp_ms() < ulid3.timestamp_ms()); - } - - #[test] - fn test_send_sync() { - __assert_send::(); - __assert_sync::(); - } -} diff --git a/crates/database/Cargo.toml b/crates/database/Cargo.toml index 5b1d2673e..4e75a38f2 100644 --- a/crates/database/Cargo.toml +++ b/crates/database/Cargo.toml @@ -15,7 +15,6 @@ [package] name = "charted-database" -description = "🐻‍❄️📦 Crate that abstracts over the database for charted." version.workspace = true documentation.workspace = true edition.workspace = true @@ -26,16 +25,13 @@ repository.workspace = true authors.workspace = true [dependencies] -charted-config = { version = "0.1.0", path = "../config" } -diesel = { workspace = true, features = [ - "chrono", - "postgres", - "sqlite", - "uuid", - "r2d2", -] } -diesel_migrations.workspace = true +async-trait = "0.1.86" +charted-config.workspace = true +charted-core.workspace = true +charted-types = { workspace = true, features = ["__internal_db"] } eyre.workspace = true -sentry.workspace = true -serde.workspace = true -tracing.workspace = true +sea-orm.workspace = true +sea-orm-migration.workspace = true +sqlx.workspace = true +tracing = { workspace = true, features = ["log"] } +url.workspace = true diff --git a/crates/database/build.rs b/crates/database/build.rs deleted file mode 100644 index 7b1022661..000000000 --- a/crates/database/build.rs +++ /dev/null @@ -1,19 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -fn main() { - println!("cargo::rerun-if-changed=./migrations/postgresql"); - println!("cargo::rerun-if-changed=./migrations/sqlite"); -} diff --git a/crates/database/migrations/postgresql/00000000000000_diesel_initial_setup/down.sql b/crates/database/migrations/postgresql/00000000000000_diesel_initial_setup/down.sql deleted file mode 100644 index a9f526091..000000000 --- a/crates/database/migrations/postgresql/00000000000000_diesel_initial_setup/down.sql +++ /dev/null @@ -1,6 +0,0 @@ --- This file was automatically created by Diesel to setup helper functions --- and other internal bookkeeping. This file is safe to edit, any future --- changes will be added to existing projects as new migrations. - -DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); -DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/crates/database/migrations/postgresql/00000000000000_diesel_initial_setup/up.sql b/crates/database/migrations/postgresql/00000000000000_diesel_initial_setup/up.sql deleted file mode 100644 index d68895b1a..000000000 --- a/crates/database/migrations/postgresql/00000000000000_diesel_initial_setup/up.sql +++ /dev/null @@ -1,36 +0,0 @@ --- This file was automatically created by Diesel to setup helper functions --- and other internal bookkeeping. This file is safe to edit, any future --- changes will be added to existing projects as new migrations. - - - - --- Sets up a trigger for the given table to automatically set a column called --- `updated_at` whenever the row is modified (unless `updated_at` was included --- in the modified columns) --- --- # Example --- --- ```sql --- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); --- --- SELECT diesel_manage_updated_at('users'); --- ``` -CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ -BEGIN - EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s - FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); -END; -$$ LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ -BEGIN - IF ( - NEW IS DISTINCT FROM OLD AND - NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at - ) THEN - NEW.updated_at := current_timestamp; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; diff --git a/crates/database/migrations/postgresql/2024-08-19-024955_init/down.sql b/crates/database/migrations/postgresql/2024-08-19-024955_init/down.sql deleted file mode 100644 index a1728f0d0..000000000 --- a/crates/database/migrations/postgresql/2024-08-19-024955_init/down.sql +++ /dev/null @@ -1,27 +0,0 @@ --- 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust --- Copyright 2022-2025 Noelware, LLC. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -DROP TABLE IF EXISTS "users"; -DROP TABLE IF EXISTS "user_connections"; - -DROP TABLE IF EXISTS "repositories"; -DROP TYPE "chart_type"; -DROP TABLE IF EXISTS "repository_members"; -DROP TABLE IF EXISTS "repository_releases"; - -DROP TABLE IF EXISTS "organizations"; -DROP TABLE IF EXISTS "organization_members"; - -DROP TABLE IF EXISTS "apikeys"; diff --git a/crates/database/migrations/postgresql/2024-08-19-024955_init/up.sql b/crates/database/migrations/postgresql/2024-08-19-024955_init/up.sql deleted file mode 100644 index 8b51e55ec..000000000 --- a/crates/database/migrations/postgresql/2024-08-19-024955_init/up.sql +++ /dev/null @@ -1,147 +0,0 @@ --- 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust --- Copyright 2022-2025 Noelware, LLC. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE TABLE IF NOT EXISTS "users"( - verified_publisher BOOLEAN NOT NULL DEFAULT false, - gravatar_email TEXT NULL DEFAULT NULL, - description VARCHAR(240) NULL DEFAULT NULL, - avatar_hash TEXT NULL DEFAULT NULL, - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), - username VARCHAR(64) NOT NULL, - password TEXT NULL DEFAULT NULL, - email TEXT NOT NULL, - admin BOOLEAN NOT NULL DEFAULT false, - name VARCHAR(64) NULL DEFAULT NULL, - id TEXT NOT NULL PRIMARY KEY -); - -CREATE UNIQUE INDEX idx_users_username ON users(username); -CREATE UNIQUE INDEX idx_users_email ON users(email); -SELECT diesel_manage_updated_at('users'); - -CREATE TABLE IF NOT EXISTS "user_connections"( - noelware_account_id BIGINT NULL DEFAULT NULL, - google_account_id TEXT NULL DEFAULT NULL, - github_account_id TEXT NULL DEFAULT NULL, - apple_account_id TEXT NULL DEFAULT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT(NOW()), - updated_at TIMESTAMPTZ NOT NULL DEFAULT(NOW()), - account TEXT NOT NULL, - id TEXT PRIMARY KEY NOT NULL, - - CONSTRAINT "fk_user_connections_owner" FOREIGN KEY(account) REFERENCES users(id) -); - -SELECT diesel_manage_updated_at('user_connections'); - -CREATE TYPE chart_type AS ENUM('application', 'library', 'operator'); -CREATE TABLE IF NOT EXISTS "repositories"( - description VARCHAR(64) NULL DEFAULT NULL, - deprecated BOOLEAN NOT NULL DEFAULT false, - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), - icon_hash TEXT NULL DEFAULT NULL, - private BOOLEAN NOT NULL DEFAULT false, - - -- `creator` is only null if `owner` is not a *User*. - creator TEXT NULL DEFAULT NULL, - owner TEXT NOT NULL, - name VARCHAR(32) NOT NULL, - type chart_type NOT NULL DEFAULT('application'), - id TEXT NOT NULL PRIMARY KEY -); - -SELECT diesel_manage_updated_at('repositories'); - -CREATE TABLE IF NOT EXISTS "repository_releases"( - repository TEXT NOT NULL UNIQUE, - update_text TEXT NULL DEFAULT NULL, - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), - tag TEXT NOT NULL, - id TEXT NOT NULL PRIMARY KEY, - - CONSTRAINT "fk_repository_release_owner" FOREIGN KEY(repository) REFERENCES repositories(id) -); - -SELECT diesel_manage_updated_at('repository_releases'); - -CREATE TABLE IF NOT EXISTS "repository_members"( - public_visibility BOOLEAN NOT NULL DEFAULT false, - display_name VARCHAR(32) NULL DEFAULT NULL, - permissions BIGINT NOT NULL DEFAULT 0, - repository TEXT NOT NULL, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), - joined_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), - account TEXT NOT NULL, - id TEXT NOT NULL PRIMARY KEY, - - CONSTRAINT "fk_repository_members_repository_id" FOREIGN KEY(repository) REFERENCES repositories(id), - CONSTRAINT "fk_repository_members_account_id" FOREIGN KEY(account) REFERENCES users(id) -); - -SELECT diesel_manage_updated_at('repository_members'); - -CREATE TABLE IF NOT EXISTS "organizations"( - verified_publisher BOOLEAN NOT NULL DEFAULT false, - twitter_handle TEXT NULL DEFAULT NULL, - gravatar_email TEXT NULL DEFAULT NULL, - display_name VARCHAR(32) NULL DEFAULT NULL, - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), - icon_hash TEXT NULL DEFAULT NULL, - private BOOLEAN NOT NULL DEFAULT false, - owner TEXT NOT NULL, - name VARCHAR(32) NOT NULL UNIQUE, - id TEXT NOT NULL PRIMARY KEY, - - CONSTRAINT "fk_organization_owner_id" FOREIGN KEY(owner) REFERENCES users(id) -); - -SELECT diesel_manage_updated_at('organizations'); -CREATE UNIQUE INDEX idx_organizations_name ON organizations(name); - -CREATE TABLE IF NOT EXISTS "organization_members"( - public_visibility BOOLEAN NOT NULL DEFAULT false, - display_name VARCHAR(32) NULL DEFAULT NULL, - organization TEXT NOT NULL, - permissions BIGINT NOT NULL DEFAULT 0, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), - joined_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), - account TEXT NOT NULL, - id TEXT NOT NULL PRIMARY KEY, - - CONSTRAINT "fk_organization_members_organization_id" FOREIGN KEY(organization) REFERENCES organizations(id), - CONSTRAINT "fk_organization_members_account_id" FOREIGN KEY(account) REFERENCES users(id) -); - -SELECT diesel_manage_updated_at('organization_members'); - -CREATE TABLE IF NOT EXISTS "api_keys"( - description VARCHAR(140) NULL DEFAULT NULL, - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), - expires_in TIMESTAMP WITH TIME ZONE NULL DEFAULT NULL, - scopes BIGINT NOT NULL DEFAULT 0, - owner TEXT NOT NULL, - token TEXT NOT NULL, - name VARCHAR(32) NOT NULL, - id TEXT NOT NULL PRIMARY KEY, - - CONSTRAINT "fk_api_keys_owner_id" FOREIGN KEY(owner) REFERENCES users(id) -); - -SELECT diesel_manage_updated_at('api_keys'); diff --git a/crates/database/migrations/postgresql/2024-08-20-234412_add_sessions_table/down.sql b/crates/database/migrations/postgresql/2024-08-20-234412_add_sessions_table/down.sql deleted file mode 100644 index b128d3fd9..000000000 --- a/crates/database/migrations/postgresql/2024-08-20-234412_add_sessions_table/down.sql +++ /dev/null @@ -1,16 +0,0 @@ --- 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust --- Copyright 2022-2025 Noelware, LLC. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -DROP TABLE IF EXISTS "sessions"; diff --git a/crates/database/migrations/postgresql/2024-08-20-234412_add_sessions_table/up.sql b/crates/database/migrations/postgresql/2024-08-20-234412_add_sessions_table/up.sql deleted file mode 100644 index 96d1df63b..000000000 --- a/crates/database/migrations/postgresql/2024-08-20-234412_add_sessions_table/up.sql +++ /dev/null @@ -1,23 +0,0 @@ --- 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust --- Copyright 2022-2025 Noelware, LLC. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE TABLE IF NOT EXISTS "sessions"( - refresh_token TEXT NOT NULL, - access_token TEXT NOT NULL, - owner TEXT NOT NULL, - id TEXT NOT NULL PRIMARY KEY, - - CONSTRAINT "fk_sessions_owner" FOREIGN KEY(owner) REFERENCES users(id) -); diff --git a/crates/database/migrations/postgresql/2024-11-27-005404_add_user_avatar_pref/down.sql b/crates/database/migrations/postgresql/2024-11-27-005404_add_user_avatar_pref/down.sql deleted file mode 100644 index ee5bfe5e0..000000000 --- a/crates/database/migrations/postgresql/2024-11-27-005404_add_user_avatar_pref/down.sql +++ /dev/null @@ -1,16 +0,0 @@ --- 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust --- Copyright 2022-2025 Noelware, LLC. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -ALTER TABLE IF EXISTS "users" DELETE COLUMN "prefers_gravatar"; diff --git a/crates/database/migrations/postgresql/2024-11-27-005404_add_user_avatar_pref/up.sql b/crates/database/migrations/postgresql/2024-11-27-005404_add_user_avatar_pref/up.sql deleted file mode 100644 index 953094ffd..000000000 --- a/crates/database/migrations/postgresql/2024-11-27-005404_add_user_avatar_pref/up.sql +++ /dev/null @@ -1,16 +0,0 @@ --- 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust --- Copyright 2022-2025 Noelware, LLC. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -ALTER TABLE IF EXISTS "users" ADD COLUMN "prefers_gravatar" BOOLEAN DEFAULT false; diff --git a/crates/database/migrations/sqlite/2024-08-19-030100_init/down.sql b/crates/database/migrations/sqlite/2024-08-19-030100_init/down.sql deleted file mode 100644 index bdc50fc6d..000000000 --- a/crates/database/migrations/sqlite/2024-08-19-030100_init/down.sql +++ /dev/null @@ -1,26 +0,0 @@ --- 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust --- Copyright 2022-2025 Noelware, LLC. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -DROP TABLE IF EXISTS "users"; -DROP TABLE IF EXISTS "user_connections"; - -DROP TABLE IF EXISTS "repositories"; -DROP TABLE IF EXISTS "repository_members"; -DROP TABLE IF EXISTS "repository_releases"; - -DROP TABLE IF EXISTS "organizations"; -DROP TABLE IF EXISTS "organization_members"; - -DROP TABLE IF EXISTS "apikeys"; diff --git a/crates/database/migrations/sqlite/2024-08-19-030100_init/up.sql b/crates/database/migrations/sqlite/2024-08-19-030100_init/up.sql deleted file mode 100644 index 041b6d21b..000000000 --- a/crates/database/migrations/sqlite/2024-08-19-030100_init/up.sql +++ /dev/null @@ -1,127 +0,0 @@ --- 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust --- Copyright 2022-2025 Noelware, LLC. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE TABLE IF NOT EXISTS `users`( - verified_publisher BOOLEAN NOT NULL DEFAULT false, - gravatar_email TEXT NULL DEFAULT NULL, - description VARCHAR(240) NULL DEFAULT NULL, - avatar_hash TEXT NULL DEFAULT NULL, - created_at DATETIME NOT NULL DEFAULT(NOW()), - updated_at DATETIME NOT NULL DEFAULT(NOW()), - username VARCHAR(64) NOT NULL, - password TEXT NULL DEFAULT NULL, - email TEXT NOT NULL, - admin BOOLEAN NOT NULL DEFAULT false, - name VARCHAR(64) NULL DEFAULT NULL, - id TEXT NOT NULL PRIMARY KEY -); - -CREATE UNIQUE INDEX idx_users_username ON users(username); -CREATE UNIQUE INDEX idx_users_email ON users(email); - -CREATE TABLE IF NOT EXISTS `user_connections`( - noelware_account_id BIGINT NULL DEFAULT NULL, - google_account_id TEXT NULL DEFAULT NULL, - github_account_id TEXT NULL DEFAULT NULL, - apple_account_id TEXT NULL DEFAULT NULL, - created_at DATETIME NOT NULL DEFAULT(NOW()), - updated_at DATETIME NOT NULL DEFAULT(NOW()), - account TEXT NOT NULL, - id TEXT PRIMARY KEY NOT NULL, - - CONSTRAINT `fk_user_connections_owner` FOREIGN KEY(account) REFERENCES users(id) -); - -CREATE TABLE IF NOT EXISTS `repositories`( - description VARCHAR(64) NULL DEFAULT NULL, - deprecated BOOLEAN NOT NULL DEFAULT false, - created_at DATETIME NOT NULL DEFAULT(NOW()), - updated_at DATETIME NOT NULL DEFAULT(NOW()), - icon_hash TEXT NULL DEFAULT NULL, - private BOOLEAN NOT NULL DEFAULT false, - owner TEXT NOT NULL, - name VARCHAR(32) NOT NULL, - type TEXT CHECK(type IN ('application', 'library', 'operator')) NOT NULL DEFAULT 'application', - id TEXT NOT NULL PRIMARY KEY -); - -CREATE TABLE IF NOT EXISTS `repository_releases`( - repository TEXT NOT NULL UNIQUE, - update_text TEXT NULL DEFAULT NULL, - created_at DATETIME NOT NULL DEFAULT(NOW()), - updated_at DATETIME NOT NULL DEFAULT(NOW()), - tag TEXT NOT NULL, - id TEXT NOT NULL PRIMARY KEY, - - CONSTRAINT `fk_repository_release_owner` FOREIGN KEY(repository) REFERENCES repositories(id) -); - -CREATE TABLE IF NOT EXISTS `repository_members`( - public_visibility BOOLEAN NOT NULL DEFAULT false, - display_name VARCHAR(32) NULL DEFAULT NULL, - permissions BIGINT NOT NULL DEFAULT 0, - repository TEXT NOT NULL, - updated_at DATETIME NOT NULL DEFAULT(NOW()), - joined_at DATETIME NOT NULL DEFAULT(NOW()), - account TEXT NOT NULL, - id TEXT NOT NULL PRIMARY KEY, - - CONSTRAINT `fk_repository_members_repository_id` FOREIGN KEY(repository) REFERENCES repositories(id), - CONSTRAINT `fk_repository_members_account_id` FOREIGN KEY(account) REFERENCES users(id) -); - -CREATE TABLE IF NOT EXISTS `organizations`( - verified_publisher BOOLEAN NOT NULL DEFAULT false, - twitter_handle TEXT NULL DEFAULT NULL, - gravatar_email TEXT NULL DEFAULT NULL, - display_name VARCHAR(32) NULL DEFAULT NULL, - created_at DATETIME NOT NULL DEFAULT(NOW()), - updated_at DATETIME NOT NULL DEFAULT(NOW()), - icon_hash TEXT NULL DEFAULT NULL, - private BOOLEAN NOT NULL DEFAULT false, - owner TEXT NOT NULL, - name VARCHAR(32) NOT NULL UNIQUE, - id TEXT NOT NULL PRIMARY KEY, - - CONSTRAINT `fk_organization_owner_id` FOREIGN KEY(owner) REFERENCES users(id) -); - -CREATE TABLE IF NOT EXISTS `organization_members`( - public_visibility BOOLEAN NOT NULL DEFAULT false, - display_name VARCHAR(32) NULL DEFAULT NULL, - organization TEXT NOT NULL, - permissions BIGINT NOT NULL DEFAULT 0, - updated_at DATETIME NOT NULL DEFAULT(NOW()), - joined_at DATETIME NOT NULL DEFAULT(NOW()), - account TEXT NOT NULL, - id TEXT NOT NULL PRIMARY KEY, - - CONSTRAINT `fk_organization_members_organization_id` FOREIGN KEY(organization) REFERENCES organizations(id), - CONSTRAINT `fk_organization_members_account_id` FOREIGN KEY(account) REFERENCES users(id) -); - -CREATE TABLE IF NOT EXISTS `api_keys`( - description VARCHAR(140) NULL DEFAULT NULL, - created_at DATETIME NOT NULL DEFAULT(NOW()), - updated_at DATETIME NOT NULL DEFAULT(NOW()), - expires_in DATETIME NULL DEFAULT NULL, - scopes BIGINT NOT NULL DEFAULT 0, - owner TEXT NOT NULL, - token TEXT NOT NULL, - name VARCHAR(32) NOT NULL, - id TEXT NOT NULL PRIMARY KEY, - - CONSTRAINT `fk_api_keys_owner_id` FOREIGN KEY(owner) REFERENCES users(id) -); diff --git a/crates/database/migrations/sqlite/2024-08-19-040802_add_creator_field/down.sql b/crates/database/migrations/sqlite/2024-08-19-040802_add_creator_field/down.sql deleted file mode 100644 index a8494cca6..000000000 --- a/crates/database/migrations/sqlite/2024-08-19-040802_add_creator_field/down.sql +++ /dev/null @@ -1,16 +0,0 @@ --- 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust --- Copyright 2022-2025 Noelware, LLC. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -ALTER TABLE `repositories` DROP COLUMN IF EXISTS `creator`; diff --git a/crates/database/migrations/sqlite/2024-08-19-040802_add_creator_field/up.sql b/crates/database/migrations/sqlite/2024-08-19-040802_add_creator_field/up.sql deleted file mode 100644 index fc7efe2e3..000000000 --- a/crates/database/migrations/sqlite/2024-08-19-040802_add_creator_field/up.sql +++ /dev/null @@ -1,16 +0,0 @@ --- 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust --- Copyright 2022-2025 Noelware, LLC. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -ALTER TABLE `repositories` ADD COLUMN `creator` TEXT NULL DEFAULT NULL; diff --git a/crates/database/migrations/sqlite/2024-08-20-234430_add_sessions_table/down.sql b/crates/database/migrations/sqlite/2024-08-20-234430_add_sessions_table/down.sql deleted file mode 100644 index b128d3fd9..000000000 --- a/crates/database/migrations/sqlite/2024-08-20-234430_add_sessions_table/down.sql +++ /dev/null @@ -1,16 +0,0 @@ --- 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust --- Copyright 2022-2025 Noelware, LLC. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -DROP TABLE IF EXISTS "sessions"; diff --git a/crates/database/migrations/sqlite/2024-08-20-234430_add_sessions_table/up.sql b/crates/database/migrations/sqlite/2024-08-20-234430_add_sessions_table/up.sql deleted file mode 100644 index 912d2360b..000000000 --- a/crates/database/migrations/sqlite/2024-08-20-234430_add_sessions_table/up.sql +++ /dev/null @@ -1,23 +0,0 @@ --- 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust --- Copyright 2022-2025 Noelware, LLC. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE TABLE IF NOT EXISTS `sessions`( - refresh_token TEXT NOT NULL, - access_token TEXT NOT NULL, - owner TEXT NOT NULL, - id TEXT NOT NULL PRIMARY KEY, - - CONSTRAINT `fk_sessions_owner` FOREIGN KEY(owner) REFERENCES users(id) -); diff --git a/crates/database/migrations/sqlite/2024-11-27-005358_add_user_avatar_pref/down.sql b/crates/database/migrations/sqlite/2024-11-27-005358_add_user_avatar_pref/down.sql deleted file mode 100644 index e807dcd89..000000000 --- a/crates/database/migrations/sqlite/2024-11-27-005358_add_user_avatar_pref/down.sql +++ /dev/null @@ -1,16 +0,0 @@ --- 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust --- Copyright 2022-2025 Noelware, LLC. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -ALTER TABLE IF EXISTS `users` DELETE COLUMN `prefers_gravatar`; diff --git a/crates/database/migrations/sqlite/2024-11-27-005358_add_user_avatar_pref/up.sql b/crates/database/migrations/sqlite/2024-11-27-005358_add_user_avatar_pref/up.sql deleted file mode 100644 index 020c416bc..000000000 --- a/crates/database/migrations/sqlite/2024-11-27-005358_add_user_avatar_pref/up.sql +++ /dev/null @@ -1,16 +0,0 @@ --- 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust --- Copyright 2022-2025 Noelware, LLC. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -ALTER TABLE `users` ADD COLUMN `prefers_gravatar` BOOLEAN DEFAULT false; diff --git a/crates/database/src/controller.rs b/crates/database/src/controller.rs deleted file mode 100644 index 1a98dbf61..000000000 --- a/crates/database/src/controller.rs +++ /dev/null @@ -1,53 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::{ - any::{Any, TypeId}, - collections::HashMap, - sync::Arc, -}; - -pub trait Controller: Send + Sync { - /// Payload for creating a `Entity`. - type CreatePayload; - - /// Payload for patching a `Entity`. - type PatchPayload; - - /// Entity type itself. - type Entity; -} - -/// A registry of [`Controller`]s. -#[derive(Debug, Clone)] -pub struct Registry { - // `dyn Any` is used instead of `dyn Controller` is because we need the - // GATs inlined and we can't do that since they can be different types. - registered: HashMap>, -} - -impl Registry { - pub fn insert(&mut self, controller: T) { - self.registered.insert(controller.type_id(), Arc::new(controller)); - } - - pub fn get(&self) -> Option<&T> { - let type_id = TypeId::of::(); - self.registered.get(&type_id).and_then(|s| s.downcast_ref()) - } -} - -#[cfg(test)] -mod tests {} diff --git a/crates/database/src/entities.rs b/crates/database/src/entities.rs new file mode 100644 index 000000000..6d2c76b0d --- /dev/null +++ b/crates/database/src/entities.rs @@ -0,0 +1,59 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod apikey; +pub mod organization; +pub mod repository; +pub mod session; +pub mod user; +pub mod user_connections; + +pub use apikey::Entity as ApiKeyEntity; +pub use repository::release::Entity as RepositoryReleaseEntity; +pub use repository::Entity as RepositoryEntity; +pub use session::Entity as SessionEntity; +pub use user::Entity as UserEntity; +pub use user_connections::Entity as UserConnectionsEntity; + +use sea_orm::{ + prelude::Expr, + sea_query::{ColumnDef, IntoIden, Table, TableCreateStatement}, + DeriveIden, +}; +use sea_orm_migration::schema::{text, timestamp}; + +#[derive(DeriveIden)] +#[sea_orm(rename_all = "snake_case")] +enum Idens { + CreatedAt, + UpdatedAt, + Id, +} + +/// Utility function like [`table_auto`][sea_orm_migration::schema::table_auto] but uses +/// `snake_case` on `created_at` and `updated_at` fields. +pub(in crate::entities) fn create_table(name: T) -> TableCreateStatement { + Table::create() + .table(name) + .if_not_exists() + .col(timestamp(Idens::CreatedAt).default(Expr::current_timestamp())) + .col(timestamp(Idens::UpdatedAt).default(Expr::current_timestamp())) + .take() +} + +/// Returns a [column definition][ColumnDef] that returns `id TEXT NOT NULL PRIMARY KEY`. +pub(in crate::entities) fn id() -> ColumnDef { + text(Idens::Id).primary_key().take() +} diff --git a/crates/database/src/entities/apikey.rs b/crates/database/src/entities/apikey.rs new file mode 100644 index 000000000..a37aaa2d0 --- /dev/null +++ b/crates/database/src/entities/apikey.rs @@ -0,0 +1,89 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{create_table, id}; +use charted_types::{name::Name, ApiKey, Ulid}; +use sea_orm::{entity::prelude::*, sea_query::TableCreateStatement}; +use sea_orm_migration::schema::*; + +#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "apikeys")] +pub struct Model { + pub display_name: Option, + pub description: Option, + pub expires_in: Option, + pub created_at: ChronoDateTimeUtc, + pub updated_at: ChronoDateTimeUtc, + pub scopes: i64, + pub owner: Ulid, + pub name: Name, + + #[sea_orm(column_type = "Text", primary_key, auto_increment = false)] + pub id: Ulid, +} + +impl From for ApiKey { + fn from(model: Model) -> Self { + ApiKey { + display_name: model.display_name, + description: model.description, + expires_in: model.expires_in.map(Into::into), + created_at: model.created_at.into(), + updated_at: model.updated_at.into(), + scopes: model.scopes, + owner: model.owner, + name: model.name, + id: model.id, + } + } +} + +#[derive(Debug, Clone, Copy, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::Owner", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(DeriveIden)] +pub(crate) enum Idens { + #[sea_orm(rename = "apikeys")] + Table, +} + +pub(crate) fn table() -> TableCreateStatement { + create_table(Idens::Table) + .col(string_len_null(Column::DisplayName, 32)) + .col(string_len_null(Column::Description, 140)) + .col(timestamp_null(Column::ExpiresIn)) + .col(big_integer(Column::Scopes)) + .col(text(Column::Owner)) + .col(Name::into_column(Column::Name)) + .col(id()) + .to_owned() +} diff --git a/crates/database/src/entities/organization.rs b/crates/database/src/entities/organization.rs new file mode 100644 index 000000000..0fdf6da08 --- /dev/null +++ b/crates/database/src/entities/organization.rs @@ -0,0 +1,103 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod member; + +use charted_types::{name::Name, Organization, Ulid}; +use sea_orm::{entity::prelude::*, sea_query::TableCreateStatement}; +use sea_orm_migration::schema::*; + +use super::{create_table, id}; + +#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "organizations")] +pub struct Model { + pub verified_publisher: bool, + pub prefers_gravatar: bool, + + #[sea_orm(column_type = "Text", nullable)] + pub gravatar_email: Option, + pub display_name: Option, + pub created_at: ChronoDateTimeUtc, + pub updated_at: ChronoDateTimeUtc, + + #[sea_orm(column_type = "Text", nullable)] + pub icon_hash: Option, + pub private: bool, + pub owner: Ulid, + pub name: Name, + + #[sea_orm(column_type = "Text", primary_key, auto_increment = false)] + pub id: Ulid, +} + +impl From for Organization { + fn from(model: Model) -> Self { + Organization { + verified_publisher: model.verified_publisher, + prefers_gravatar: model.prefers_gravatar, + gravatar_email: model.gravatar_email, + display_name: model.display_name, + created_at: model.created_at.into(), + updated_at: model.updated_at.into(), + icon_hash: model.icon_hash, + private: model.private, + owner: model.owner, + name: model.name, + id: model.id, + } + } +} + +#[derive(Debug, Clone, Copy, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::Owner", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(DeriveIden)] +pub(crate) enum Idens { + #[sea_orm(rename = "organizations")] + Table, +} + +pub(crate) fn table() -> TableCreateStatement { + create_table(Idens::Table) + .col(boolean(Column::VerifiedPublisher).default(false)) + .col(boolean(Column::PrefersGravatar).default(false)) + .col(text_null(Column::GravatarEmail)) + .col(string_len_null(Column::DisplayName, 32)) + .col(boolean(Column::Private).default(false)) + .col(text(Column::IconHash)) + .col(text(Column::Owner)) + .col(Name::into_column(Column::Owner)) + .col(Name::into_column(Column::Name)) + .col(id()) + .to_owned() +} diff --git a/crates/features/totp/src/lib.rs b/crates/database/src/entities/organization/member.rs similarity index 53% rename from crates/features/totp/src/lib.rs rename to crates/database/src/entities/organization/member.rs index 7328a79c6..512929cf4 100644 --- a/crates/features/totp/src/lib.rs +++ b/crates/database/src/entities/organization/member.rs @@ -13,32 +13,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![allow(unused)] +/* +CREATE TABLE IF NOT EXISTS "organization_members"( + public_visibility BOOLEAN NOT NULL DEFAULT false, + display_name VARCHAR(32) NULL DEFAULT NULL, + permissions BIGINT NOT NULL DEFAULT 0, + organization TEXT NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), + joined_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), + account TEXT NOT NULL, + id TEXT NOT NULL PRIMARY KEY, -use charted_config::features::totp::Config; -use charted_core::BoxedFuture; -use charted_features::Feature; + CONSTRAINT "fk_organization_members_repository_id" FOREIGN KEY(organization) REFERENCES organizations(id), + CONSTRAINT "fk_organization_members_account_id" FOREIGN KEY(account) REFERENCES users(id) +); -/// Creates the TOTP feature that is opaque as a [`Feature`]. -pub fn new(config: &Config) -> impl Feature { - TotpFeature { - secret: config.secret.clone(), - } -} - -struct TotpFeature { - secret: String, -} - -impl Feature for TotpFeature { - fn extends_db<'feat, 'a>(&'feat self, _pool: &'a charted_database::DbPool) -> BoxedFuture<'a, eyre::Result<()>> - where - 'a: 'feat, - { - Box::pin(async move { - let conn = _pool.get()?; - - Ok(()) - }) - } -} +*/ diff --git a/crates/database/src/entities/repository.rs b/crates/database/src/entities/repository.rs new file mode 100644 index 000000000..60c3ececd --- /dev/null +++ b/crates/database/src/entities/repository.rs @@ -0,0 +1,102 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod member; +pub mod release; + +use charted_types::{name::Name, ChartType, Repository, Ulid}; +use sea_orm::{entity::prelude::*, sea_query::TableCreateStatement}; +use sea_orm_migration::schema::*; + +use super::{create_table, id}; + +#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "repositories")] +pub struct Model { + pub description: Option, + pub deprecated: bool, + pub created_at: ChronoDateTimeUtc, + pub updated_at: ChronoDateTimeUtc, + + #[sea_orm(column_type = "Text", nullable)] + pub icon_hash: Option, + pub private: bool, + pub creator: Option, + pub owner: Ulid, + pub name: Name, + + #[sea_orm(rename = "type")] + pub type_: ChartType, + + #[sea_orm(column_type = "Text", primary_key, auto_increment = false)] + pub id: Ulid, +} + +impl From for Repository { + fn from(model: Model) -> Self { + Repository { + description: model.description, + deprecated: model.deprecated, + created_at: model.created_at.into(), + updated_at: model.updated_at.into(), + icon_hash: model.icon_hash, + private: model.private, + creator: model.creator, + owner: model.owner, + name: model.name, + type_: model.type_, + id: model.id, + } + } +} + +#[derive(Debug, Clone, Copy, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::repository::release::Entity")] + Releases, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Releases.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(DeriveIden)] +pub(crate) enum Idens { + #[sea_orm(iden = "repositories")] + Table, +} + +pub(crate) fn table() -> TableCreateStatement { + create_table(Idens::Table) + .if_not_exists() + .col(string_len_null(Column::Description, 140)) + .col(boolean(Column::Deprecated).default(false)) + .col(text_null(Column::IconHash)) + .col(boolean(Column::Private).default(false)) + .col(text_null(Column::Creator)) + .col(text(Column::Owner)) + .col(string_len(Column::Name, 32)) + .col(enumeration( + Column::Type, + ChartType::name(), + [ChartType::Application, ChartType::Library], + )) + .col(id()) + .to_owned() +} diff --git a/crates/helm-plugin/src/cmds/auth/switch.rs b/crates/database/src/entities/repository/member.rs similarity index 53% rename from crates/helm-plugin/src/cmds/auth/switch.rs rename to crates/database/src/entities/repository/member.rs index b0e13fb24..75b390422 100644 --- a/crates/helm-plugin/src/cmds/auth/switch.rs +++ b/crates/database/src/entities/repository/member.rs @@ -13,23 +13,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{args, auth::Auth}; +/* +CREATE TABLE IF NOT EXISTS "repository_members"( + public_visibility BOOLEAN NOT NULL DEFAULT false, + display_name VARCHAR(32) NULL DEFAULT NULL, + permissions BIGINT NOT NULL DEFAULT 0, + repository TEXT NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), + joined_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT(NOW()), + account TEXT NOT NULL, + id TEXT NOT NULL PRIMARY KEY, -#[derive(Debug, Clone, clap::Parser)] -pub struct Args { - /// context to switch to - context: String, + CONSTRAINT "fk_repository_members_repository_id" FOREIGN KEY(repository) REFERENCES repositories(id), + CONSTRAINT "fk_repository_members_account_id" FOREIGN KEY(account) REFERENCES users(id) +); - #[clap(flatten)] - auth: args::Auth, -} - -pub fn run( - Args { - context, - auth: args::Auth { path }, - }: Args, -) -> eyre::Result<()> { - let mut auth = Auth::load(path.as_ref())?; - auth.switch(path.as_ref(), context) -} +*/ diff --git a/crates/database/src/entities/repository/release.rs b/crates/database/src/entities/repository/release.rs new file mode 100644 index 000000000..d02d45a6e --- /dev/null +++ b/crates/database/src/entities/repository/release.rs @@ -0,0 +1,98 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use charted_types::{RepositoryRelease, Ulid, Version}; +use sea_orm::{ + entity::prelude::*, + sea_query::{ForeignKey, TableCreateStatement}, +}; +use sea_orm_migration::schema::*; + +#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "repository_releases")] +pub struct Model { + #[sea_orm(column_type = "Text", nullable)] + pub update_text: Option, + pub repository: Ulid, + pub created_at: ChronoDateTimeUtc, + pub updated_at: ChronoDateTimeUtc, + pub yanked: bool, + pub title: Option, + + #[sea_orm(column_type = "Text")] + pub tag: Version, + + #[sea_orm(column_type = "Text", primary_key, auto_increment = false)] + pub id: Ulid, +} + +impl From for RepositoryRelease { + fn from(model: Model) -> Self { + RepositoryRelease { + update_text: model.update_text, + repository: model.repository, + created_at: model.created_at.into(), + updated_at: model.updated_at.into(), + yanked: model.yanked, + title: model.title, + tag: model.tag, + id: model.id, + } + } +} + +#[derive(Debug, Clone, Copy, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::Entity", + from = "Column::Repository", + to = "super::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Repository, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Repository.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(DeriveIden)] +pub(crate) enum Idens { + #[sea_orm(iden = "repository_releases")] + Table, +} + +pub(crate) fn table() -> TableCreateStatement { + table_auto(Idens::Table) + .if_not_exists() + .col(text_null(Column::UpdateText)) + .col(text(Column::Repository)) + .col(boolean(Column::Yanked)) + .col(string_len_null(Column::Title, 32)) + .col(text(Column::Tag)) + .col(text(Column::Id).primary_key()) + .foreign_key( + ForeignKey::create() + .name("fk_repository_release_owner") + .from(Idens::Table, Column::Repository) + .to(super::Idens::Table, super::Column::Id), + ) + .to_owned() +} diff --git a/crates/database/src/entities/session.rs b/crates/database/src/entities/session.rs new file mode 100644 index 000000000..a52030bcc --- /dev/null +++ b/crates/database/src/entities/session.rs @@ -0,0 +1,92 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use charted_types::{Session, Ulid}; +use sea_orm::{ + entity::prelude::*, + sea_query::{ForeignKey, TableCreateStatement}, +}; +use sea_orm_migration::schema::*; + +use super::{create_table, id}; + +#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "sessions")] +pub struct Model { + #[sea_orm(column_type = "Text")] + pub refresh_token: String, + + #[sea_orm(column_type = "Text")] + pub access_token: String, + + #[sea_orm(column_type = "Text")] + pub account: Ulid, + + #[sea_orm(column_type = "Text", primary_key, auto_increment = false)] + pub id: Ulid, +} + +impl From for Session { + fn from(model: Model) -> Self { + Session { + refresh_token: Some(model.refresh_token), + access_token: Some(model.access_token), + owner: model.account, + id: model.id, + } + } +} + +#[derive(Debug, Clone, Copy, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::Account", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(DeriveIden)] +pub(crate) enum Idens { + #[sea_orm(iden = "sessions")] + Table, +} + +pub(crate) fn table() -> TableCreateStatement { + create_table(Idens::Table) + .if_not_exists() + .col(text(Column::RefreshToken)) + .col(text(Column::AccessToken)) + .col(text(Column::Account)) + .col(id()) + .foreign_key( + ForeignKey::create() + .name("fk_session_account") + .from(Idens::Table, Column::Account) + .to(super::user::Idens::Table, super::user::Column::Id), + ) + .to_owned() +} diff --git a/crates/database/src/entities/user.rs b/crates/database/src/entities/user.rs new file mode 100644 index 000000000..c1561d5bf --- /dev/null +++ b/crates/database/src/entities/user.rs @@ -0,0 +1,123 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use charted_types::{name::Name, Ulid, User}; +use sea_orm::{entity::prelude::*, sea_query::TableCreateStatement}; +use sea_orm_migration::schema::*; + +use super::create_table; + +#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "users")] +pub struct Model { + pub verified_publisher: bool, + pub prefers_gravatar: bool, + + #[sea_orm(column_type = "Text", nullable)] + pub gravatar_email: Option, + + #[sea_orm(column_type = "Text", nullable)] + pub description: Option, + + #[sea_orm(column_type = "Text", nullable)] + pub avatar_hash: Option, + + pub created_at: ChronoDateTimeUtc, + pub updated_at: ChronoDateTimeUtc, + pub username: Name, + pub password: Option, + pub email: String, + pub admin: bool, + pub name: Option, + + #[sea_orm(primary_key, auto_increment = false, column_type = "Text")] + pub id: Ulid, +} + +impl From for User { + fn from(model: Model) -> Self { + User { + verified_publisher: model.verified_publisher, + prefers_gravatar: model.prefers_gravatar, + gravatar_email: model.gravatar_email, + description: model.description, + avatar_hash: model.avatar_hash, + created_at: model.created_at.into(), + updated_at: model.created_at.into(), + username: model.username, + admin: model.admin, + name: model.name, + id: model.id, + } + } +} + +#[derive(Debug, Clone, Copy, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::apikey::Entity")] + ApiKey, + + #[sea_orm(has_many = "super::organization::Entity")] + Organization, + + #[sea_orm(has_many = "super::session::Entity")] + Session, + + #[sea_orm(has_one = "super::user_connections::Entity")] + UserConnection, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserConnection.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Session.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Organization.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(DeriveIden)] +pub(crate) enum Idens { + #[sea_orm(iden = "users")] + Table, +} + +pub(crate) fn table() -> TableCreateStatement { + create_table(Idens::Table) + .if_not_exists() + .col(boolean(Column::VerifiedPublisher)) + .col(boolean(Column::PrefersGravatar)) + .col(text_null(Column::GravatarEmail)) + .col(string_len_null(Column::Description, 240)) + .col(text_null(Column::AvatarHash)) + .col(string_len_uniq(Column::Username, 64)) + .col(text_null(Column::Password)) + .col(text(Column::Email)) + .col(boolean(Column::Admin)) + .col(string_len_null(Column::Name, 64)) + .col(text(Column::Id).primary_key()) + .to_owned() +} diff --git a/crates/database/src/entities/user_connections.rs b/crates/database/src/entities/user_connections.rs new file mode 100644 index 000000000..e937c9ad2 --- /dev/null +++ b/crates/database/src/entities/user_connections.rs @@ -0,0 +1,104 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use charted_types::{Ulid, UserConnections}; +use sea_orm::{ + entity::prelude::*, + sea_query::{ForeignKey, TableCreateStatement}, +}; +use sea_orm_migration::schema::*; + +use super::{create_table, id}; + +#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "user_connections")] +pub struct Model { + #[sea_orm(column_type = "Text", nullable)] + pub noelware_account_id: Option, + + #[sea_orm(column_type = "Text", nullable)] + pub google_account_id: Option, + + #[sea_orm(column_type = "Text", nullable)] + pub github_account_id: Option, + + #[sea_orm(column_type = "Text", nullable)] + pub gitlab_account_id: Option, + + pub created_at: ChronoDateTimeUtc, + pub updated_at: ChronoDateTimeUtc, + pub account: Ulid, + + #[sea_orm(primary_key, auto_increment = false, column_type = "Text")] + pub id: Ulid, +} + +impl From for UserConnections { + fn from(model: Model) -> Self { + UserConnections { + noelware_account_id: model.noelware_account_id, + google_account_id: model.google_account_id, + github_account_id: model.github_account_id, + gitlab_account_id: model.gitlab_account_id, + created_at: model.created_at.into(), + updated_at: model.updated_at.into(), + id: model.id, + } + } +} + +#[derive(Debug, Clone, Copy, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::Account", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(DeriveIden)] +pub(crate) enum Idens { + #[sea_orm(iden = "user_connections")] + Table, +} + +pub(crate) fn table() -> TableCreateStatement { + create_table(Idens::Table) + .if_not_exists() + .col(text_null(Column::NoelwareAccountId)) + .col(text_null(Column::GoogleAccountId)) + .col(text_null(Column::GithubAccountId)) + .col(text_null(Column::GitlabAccountId)) + .col(text_null(Column::Account)) + .col(id()) + .foreign_key( + ForeignKey::create() + .name("fk_user_connections_account") + .from(Idens::Table, Column::Account) + .to(super::user::Idens::Table, super::user::Column::Id), + ) + .to_owned() +} diff --git a/crates/database/src/lib.rs b/crates/database/src/lib.rs index c85f021df..78629482e 100644 --- a/crates/database/src/lib.rs +++ b/crates/database/src/lib.rs @@ -13,187 +13,57 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod controller; -pub mod migrations; -pub mod paginate; -pub mod schema; +use std::ops::Deref; use charted_config::database::Config; -use diesel::{ - connection::{set_default_instrumentation, InstrumentationEvent}, - prelude::*, - r2d2::{self, ConnectionManager, Pool}, -}; -use eyre::Context; -use tracing::{error, trace}; +use charted_core::serde::Duration; +use migrations::Migrator; +use sea_orm::{metric::Info, ConnectOptions, DatabaseBackend, DatabaseConnection, SqlxPostgresConnector}; +use sea_orm_migration::MigratorTrait; +use tracing::{info, instrument, trace}; -/// [`Pool`] that wraps a [`ConnectionManager`] of our multi-connection type. -pub type DbPool = Pool>; +pub mod entities; +pub mod migrations; -#[derive(diesel::MultiConnection)] -pub enum DbConnection { - PostgreSQL(diesel::pg::PgConnection), - SQLite(diesel::sqlite::SqliteConnection), -} +#[instrument(name = "charted.database.createDbPool", skip_all)] +pub async fn create_pool(config: &Config) -> eyre::Result { + let mut conn = match config { + Config::PostgreSQL(_) => SqlxPostgresConnector::connect(connect_options_with(config)).await?, + Config::SQLite(_) => SqlxPostgresConnector::connect(connect_options_with(config)).await?, + }; -pub fn create_pool(config: &Config) -> eyre::Result { - // connection string is the Display impl for `Config`. - let url = config.to_string(); - trace!(database.url = url, "creating pool for db url"); + conn.set_metric_callback(metric_callback); - let manager = ConnectionManager::new(url); - set_default_instrumentation(|| Some(Box::new(instrumentation)))?; + if config.common().run_migrations { + info!("now running pending migrations!"); - Pool::builder() - .max_size(config.max_connections()) - .error_handler(Box::new(ErrorHandler)) - .build(manager) - .context("failed to create db pool") -} - -#[derive(Debug)] -struct ErrorHandler; -impl r2d2::HandleError for ErrorHandler { - fn handle_error(&self, error: E) { - sentry::capture_error(&error); - error!(%error, "failed to manage connection or perform query"); + Migrator::install(&conn).await?; + Migrator::up(&conn, None).await?; } -} - -pub fn version(pool: &DbPool) -> eyre::Result { - connection!(pool, { - PostgreSQL(conn) { - diesel::define_sql_function! { - fn version() -> diesel::sql_types::Text; - } - diesel::select(version()) - .get_result::(conn) - .context("failed to get database version") - }; - - SQLite(conn) { - diesel::define_sql_function! { - fn sqlite_version() -> diesel::sql_types::Text; - } - - diesel::select(sqlite_version()) - .get_result::(conn) - .context("failed to get database version") - }; - }) + Ok(conn) } -fn instrumentation(event: InstrumentationEvent<'_>) { - match event { - InstrumentationEvent::BeginTransaction { depth, .. } => { - trace!("started transation (depth={depth})"); - } - - InstrumentationEvent::CommitTransaction { depth, .. } => { - trace!("transaction with depth [{depth}] was committed"); - } - - InstrumentationEvent::RollbackTransaction { depth, .. } => { - trace!("transaction with depth [{depth}] was rolled back"); - } - - InstrumentationEvent::FinishQuery { query, error, .. } => { - trace!(sql = %query, "finished query{}", if error.is_some() { " with an error" } else { "" }); - if let Some(err) = error { - sentry::capture_error(err); - } - } - - InstrumentationEvent::StartQuery { query, .. } => { - trace!(sql = %query, "starting query"); - } +fn metric_callback(info: &Info<'_>) { + let elapsed: Duration = info.elapsed.into(); + let backend = match info.statement.db_backend { + DatabaseBackend::Sqlite => "sqlite", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Postgres => "postgres", + }; - _ => {} - } -} - -#[macro_export] -macro_rules! connection { - (@raw $conn:ident { - $( - $db:ident($c:ident) => $code:expr; - )* - }) => {{ - #[allow(unused)] - use ::diesel::prelude::*; - match *$conn { - $( - $crate::DbConnection::$db(ref mut $c) => $code, - )* - } - }}; - - (@raw $conn:ident { - $( - $db:ident($c:ident) $code:block; - )* - }) => {{ - #[allow(unused)] - use ::diesel::prelude::*; - match *$conn { - $( - $crate::DbConnection::$db(ref mut $c) => $code, - )* - } - }}; - - ($pool:expr, { - $( - $db:ident($c:ident) => $code:expr; - )* - }) => {{ - #[allow(unused)] - use ::eyre::Context; - - let mut conn = ($pool).get().context("failed to get db connection")?; - $crate::connection!(@raw conn { - $( - $db($c) => $code; - )* - }) - }}; - - ($pool:expr, { - $( - $db:ident($conn:ident) $code:block; - )* - }) => {{ - #[allow(unused)] - use ::eyre::Context; - - let mut conn = ($pool).get().context("failed to get db connection")?; - $crate::connection!(@raw conn { - $( - $db($conn) $code; - )* - }) - }}; + trace!(%elapsed, failed = %info.failed, %backend, stmt.sql = info.statement.sql, stmt.values = ?info.statement.values); } -#[cfg(test)] -mod tests { - use charted_config::database::{sqlite, Config}; - use std::path::PathBuf; - - #[test] - fn test_sqlite_version() { - let db = crate::create_pool(&Config::SQLite(sqlite::Config { - db_path: PathBuf::from(":memory:"), - max_connections: 1, - run_migrations: false, - })) - .expect("failed to create in-memory sqlite database"); - - let Ok(s) = crate::version(&db) else { - panic!("failed to get sqlite version") - }; - - assert!(!s.is_empty()); - } +fn connect_options_with(config: &Config) -> ConnectOptions { + let common = config.common(); + + ConnectOptions::new(config.to_string()) + .max_connections(common.max_connections) + .acquire_timeout(common.acquire_timeout.deref().into()) + .connect_timeout(common.connect_timeout.deref().into()) + .idle_timeout(common.idle_timeout.deref().into()) + .sqlx_logging_level(tracing::log::LevelFilter::Trace) + .sqlx_slow_statements_logging_settings(tracing::log::LevelFilter::Warn, std::time::Duration::from_secs(3)) + .to_owned() } diff --git a/crates/database/src/migrations.rs b/crates/database/src/migrations.rs index 347f6f2ee..2bdcb453d 100644 --- a/crates/database/src/migrations.rs +++ b/crates/database/src/migrations.rs @@ -13,27 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::DbPool; -use diesel_migrations::{embed_migrations, EmbeddedMigrations}; -use eyre::eyre; +use sea_orm_migration::MigratorTrait; -pub const POSTGRESQL_MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations/postgresql"); -pub const SQLITE_MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations/sqlite"); +pub(crate) mod m02_02_2025_000001_init; -pub fn migrate(pool: &DbPool) -> eyre::Result<()> { - use diesel_migrations::MigrationHarness; +pub struct Migrator; - crate::connection!(pool, { - PostgreSQL(conn) { - conn.run_pending_migrations(POSTGRESQL_MIGRATIONS) - .map(|_| ()) - .map_err(|e| eyre!("failed to run PostgreSQL migrations: {e}")) - }; - - SQLite(conn) { - conn.run_pending_migrations(SQLITE_MIGRATIONS) - .map(|_| ()) - .map_err(|e| eyre!("failed to run SQLite migrations: {e}")) - }; - }) +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m02_02_2025_000001_init::migration())] + } } diff --git a/crates/database/src/migrations/m02_02_2025_000001_init.rs b/crates/database/src/migrations/m02_02_2025_000001_init.rs new file mode 100644 index 000000000..0d9ed4d8c --- /dev/null +++ b/crates/database/src/migrations/m02_02_2025_000001_init.rs @@ -0,0 +1,74 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::entities; +use charted_types::ChartType; +use sea_orm::{sea_query::extension::postgres::Type, ActiveEnum}; +use sea_orm_migration::prelude::*; + +pub fn migration() -> impl MigrationTrait { + Impl +} + +struct Impl; + +impl MigrationName for Impl { + fn name(&self) -> &str { + "init" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Impl { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_type( + Type::create() + .as_enum(ChartType::name()) + .values([ChartType::Application, ChartType::Library]) + .to_owned(), + ) + .await?; + + manager.create_table(entities::user::table()).await?; + manager.create_table(entities::user_connections::table()).await?; + manager.create_table(entities::session::table()).await?; + manager.create_table(entities::repository::table()).await?; + manager.create_table(entities::repository::release::table()).await?; + manager.create_table(entities::organization::table()).await?; + manager.create_table(entities::apikey::table()).await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table( + Table::drop() + .if_exists() + .table(entities::user::Idens::Table) + .table(entities::user_connections::Idens::Table) + .table(entities::session::Idens::Table) + .table(entities::repository::Idens::Table) + .table(entities::repository::release::Idens::Table) + .to_owned(), + ) + .await?; + + manager + .drop_type(Type::drop().name(ChartType::name()).cascade().to_owned()) + .await + } +} diff --git a/crates/database/src/paginate.rs b/crates/database/src/paginate.rs deleted file mode 100644 index 71c00e649..000000000 --- a/crates/database/src/paginate.rs +++ /dev/null @@ -1,105 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::{DbConnection, MultiBackend}; -use diesel::{ - query_builder::{AstPass, Query, QueryFragment, QueryId}, - query_dsl::methods::LoadQuery, - sql_types::BigInt, - QueryResult, RunQueryDsl, -}; - -/// Amount of elements that can be in per page. -pub const PER_PAGE: i64 = 10; - -/// Trait that implements for paginating queries. -pub trait Paginate: Sized { - /// Create a new [paginated][Paginated] query based off the current `page`. - fn paginate(self, page: i64) -> PaginatedQuery { - PaginatedQuery { - query: self, - per_page: PER_PAGE, - page, - offset: (page - 1) * PER_PAGE, - } - } -} - -#[derive(Debug, Clone, QueryId)] -pub struct PaginatedQuery { - query: T, - per_page: i64, - page: i64, - offset: i64, -} - -impl PaginatedQuery { - /// Updates the amount of elements that can be present. - pub fn per_page(self, per: i64) -> Self { - PaginatedQuery { - per_page: per, - offset: (self.page - 1) * per, - ..self - } - } - - pub fn perform<'a, U>(self, conn: &mut DbConnection) -> QueryResult> - where - Self: LoadQuery<'a, DbConnection, (U, i64)>, - { - let per_page = self.per_page; - let results = self.load::<(U, i64)>(conn)?; - let total = results.first().map(|x| x.1).unwrap_or(0); - let records = results.into_iter().map(|x| x.0).collect(); - let total_pages = (total as f64 / per_page as f64).ceil() as i64; - - Ok(Paginated { - data: records, - pages: total, - total: total_pages, - per_page, - }) - } -} - -impl Query for PaginatedQuery { - type SqlType = (T::SqlType, BigInt); -} - -impl RunQueryDsl for PaginatedQuery {} - -impl QueryFragment for PaginatedQuery -where - T: QueryFragment, -{ - fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, MultiBackend>) -> QueryResult<()> { - out.push_sql("SELECT *, COUNT(*) OVER () FROM ("); - self.query.walk_ast(out.reborrow())?; - out.push_sql(") t LIMIT "); - out.push_bind_param::(&self.per_page)?; - out.push_sql(" OFFSET "); - out.push_bind_param::(&self.offset)?; - - Ok(()) - } -} - -#[derive(Debug, Clone, serde::Serialize)] -pub struct Paginated { - pub data: Vec, - pub total: i64, - pub pages: i64, - pub per_page: i64, -} diff --git a/crates/database/src/schema.rs b/crates/database/src/schema.rs deleted file mode 100644 index 0542ac0ff..000000000 --- a/crates/database/src/schema.rs +++ /dev/null @@ -1,25 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod postgresql; -pub mod sqlite; - -/// All of the custom SQL types -pub mod sql_types { - #[derive(Debug, diesel::QueryId, diesel::SqlType, diesel::FromSqlRow)] - #[diesel(postgres_type(name = "chart_type"))] - #[diesel(sqlite_type(name = "Text"))] - pub struct ChartType; -} diff --git a/crates/database/src/schema/patches/sqlite@timestamp.patch b/crates/database/src/schema/patches/sqlite@timestamp.patch deleted file mode 100644 index a12a719f2..000000000 --- a/crates/database/src/schema/patches/sqlite@timestamp.patch +++ /dev/null @@ -1,140 +0,0 @@ -diff --git a/crates/database/src/schema/sqlite.rs b/crates/database/src/schema/sqlite.rs -index 1287d455..7c5c312a 100644 ---- a/crates/database/src/schema/sqlite.rs -+++ b/crates/database/src/schema/sqlite.rs -@@ -16,14 +16,14 @@ - diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - api_keys (id) { - description -> Nullable, -- created_at -> Timestamp, -- updated_at -> Timestamp, -+ created_at -> TimestamptzSqlite, -+ updated_at -> TimestamptzSqlite, -- expires_in -> Nullable, -+ expires_in -> Nullable, - scopes -> BigInt, - owner -> Text, - token -> Text, - name -> Text, - id -> Text, -@@ -36,14 +36,14 @@ diesel::table! { - - organization_members (id) { - public_visibility -> Bool, - display_name -> Nullable, - organization -> Text, - permissions -> BigInt, -- updated_at -> Timestamp, -- joined_at -> Timestamp, -+ updated_at -> TimestamptzSqlite, -+ joined_at -> TimestamptzSqlite, - account -> Text, - id -> Text, - } - } - - diesel::table! { -@@ -52,14 +52,14 @@ diesel::table! { - - organizations (id) { - verified_publisher -> Bool, - twitter_handle -> Nullable, - gravatar_email -> Nullable, - display_name -> Nullable, -- created_at -> Timestamp, -- updated_at -> Timestamp, -+ created_at -> TimestamptzSqlite, -+ updated_at -> TimestamptzSqlite, - icon_hash -> Nullable, - private -> Bool, - owner -> Text, - name -> Text, - id -> Text, - } -@@ -69,14 +69,14 @@ diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - repositories (id) { - description -> Nullable, - deprecated -> Bool, -- created_at -> Timestamp, -- updated_at -> Timestamp, -+ created_at -> TimestamptzSqlite, -+ updated_at -> TimestamptzSqlite, - icon_hash -> Nullable, - private -> Bool, - owner -> Text, - name -> Text, - #[sql_name = "type"] - type_ -> Text, -@@ -91,28 +91,28 @@ diesel::table! { - - repository_members (id) { - public_visibility -> Bool, - display_name -> Nullable, - permissions -> BigInt, - repository -> Text, -- updated_at -> Timestamp, -- joined_at -> Timestamp, -+ updated_at -> TimestamptzSqlite, -+ joined_at -> TimestamptzSqlite, - account -> Text, - id -> Text, - } - } - - diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - repository_releases (id) { - repository -> Text, - update_text -> Nullable, -- created_at -> Timestamp, -- updated_at -> Timestamp, -+ created_at -> TimestamptzSqlite, -+ updated_at -> TimestamptzSqlite, - tag -> Text, - id -> Text, - } - } - - diesel::table! { -@@ -133,14 +133,14 @@ diesel::table! { - - user_connections (id) { - noelware_account_id -> Nullable, - google_account_id -> Nullable, - github_account_id -> Nullable, - apple_account_id -> Nullable, -- created_at -> Timestamp, -- updated_at -> Timestamp, -+ created_at -> TimestamptzSqlite, -+ updated_at -> TimestamptzSqlite, - account -> Text, - id -> Text, - } - } - - diesel::table! { -@@ -149,14 +149,14 @@ diesel::table! { - - users (id) { - verified_publisher -> Bool, - gravatar_email -> Nullable, - description -> Nullable, - avatar_hash -> Nullable, -- created_at -> Timestamp, -- updated_at -> Timestamp, -+ created_at -> TimestamptzSqlite, -+ updated_at -> TimestamptzSqlite, - username -> Text, - password -> Nullable, - email -> Text, - admin -> Bool, - name -> Nullable, - id -> Text, diff --git a/crates/database/src/schema/postgresql.rs b/crates/database/src/schema/postgresql.rs deleted file mode 100644 index 30b1ea52c..000000000 --- a/crates/database/src/schema/postgresql.rs +++ /dev/null @@ -1,198 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - api_keys (id) { - #[max_length = 140] - description -> Nullable, - created_at -> Timestamptz, - updated_at -> Timestamptz, - expires_in -> Nullable, - scopes -> Int8, - owner -> Text, - token -> Text, - #[max_length = 32] - name -> Varchar, - id -> Text, - } -} - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - organization_members (id) { - public_visibility -> Bool, - #[max_length = 32] - display_name -> Nullable, - organization -> Text, - permissions -> Int8, - updated_at -> Timestamptz, - joined_at -> Timestamptz, - account -> Text, - id -> Text, - } -} - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - organizations (id) { - verified_publisher -> Bool, - twitter_handle -> Nullable, - gravatar_email -> Nullable, - #[max_length = 32] - display_name -> Nullable, - created_at -> Timestamptz, - updated_at -> Timestamptz, - icon_hash -> Nullable, - private -> Bool, - owner -> Text, - #[max_length = 32] - name -> Varchar, - id -> Text, - } -} - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - repositories (id) { - #[max_length = 64] - description -> Nullable, - deprecated -> Bool, - created_at -> Timestamptz, - updated_at -> Timestamptz, - icon_hash -> Nullable, - private -> Bool, - creator -> Nullable, - owner -> Text, - #[max_length = 32] - name -> Varchar, - #[sql_name = "type"] - type_ -> ChartType, - id -> Text, - } -} - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - repository_members (id) { - public_visibility -> Bool, - #[max_length = 32] - display_name -> Nullable, - permissions -> Int8, - repository -> Text, - updated_at -> Timestamptz, - joined_at -> Timestamptz, - account -> Text, - id -> Text, - } -} - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - repository_releases (id) { - repository -> Text, - update_text -> Nullable, - created_at -> Timestamptz, - updated_at -> Timestamptz, - tag -> Text, - id -> Text, - } -} - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - sessions (id) { - refresh_token -> Text, - access_token -> Text, - owner -> Text, - id -> Text, - } -} - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - user_connections (id) { - noelware_account_id -> Nullable, - google_account_id -> Nullable, - github_account_id -> Nullable, - apple_account_id -> Nullable, - created_at -> Timestamptz, - updated_at -> Timestamptz, - account -> Text, - id -> Text, - } -} - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - users (id) { - verified_publisher -> Bool, - gravatar_email -> Nullable, - #[max_length = 240] - description -> Nullable, - avatar_hash -> Nullable, - created_at -> Timestamptz, - updated_at -> Timestamptz, - #[max_length = 64] - username -> Varchar, - password -> Nullable, - email -> Text, - admin -> Bool, - #[max_length = 64] - name -> Nullable, - id -> Text, - prefers_gravatar -> Bool, - } -} - -diesel::joinable!(api_keys -> users(owner)); -diesel::joinable!(organization_members -> organizations(organization)); -diesel::joinable!(organization_members -> users(account)); -diesel::joinable!(organizations -> users(owner)); -diesel::joinable!(repository_members -> repositories(repository)); -diesel::joinable!(repository_members -> users(account)); -diesel::joinable!(repository_releases -> repositories(repository)); -diesel::joinable!(sessions -> users(owner)); -diesel::joinable!(user_connections -> users(account)); - -diesel::allow_tables_to_appear_in_same_query!( - api_keys, - organization_members, - organizations, - repositories, - repository_members, - repository_releases, - sessions, - user_connections, - users, -); diff --git a/crates/database/src/schema/sqlite.rs b/crates/database/src/schema/sqlite.rs deleted file mode 100644 index ec7ea258e..000000000 --- a/crates/database/src/schema/sqlite.rs +++ /dev/null @@ -1,187 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - api_keys (id) { - description -> Nullable, - created_at -> TimestamptzSqlite, - updated_at -> TimestamptzSqlite, - expires_in -> Nullable, - scopes -> BigInt, - owner -> Text, - token -> Text, - name -> Text, - id -> Text, - } -} - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - organization_members (id) { - public_visibility -> Bool, - display_name -> Nullable, - organization -> Text, - permissions -> BigInt, - updated_at -> TimestamptzSqlite, - joined_at -> TimestamptzSqlite, - account -> Text, - id -> Text, - } -} - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - organizations (id) { - verified_publisher -> Bool, - twitter_handle -> Nullable, - gravatar_email -> Nullable, - display_name -> Nullable, - created_at -> TimestamptzSqlite, - updated_at -> TimestamptzSqlite, - icon_hash -> Nullable, - private -> Bool, - owner -> Text, - name -> Text, - id -> Text, - } -} - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - repositories (id) { - description -> Nullable, - deprecated -> Bool, - created_at -> TimestamptzSqlite, - updated_at -> TimestamptzSqlite, - icon_hash -> Nullable, - private -> Bool, - owner -> Text, - name -> Text, - #[sql_name = "type"] - type_ -> Text, - id -> Text, - creator -> Nullable, - } -} - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - repository_members (id) { - public_visibility -> Bool, - display_name -> Nullable, - permissions -> BigInt, - repository -> Text, - updated_at -> TimestamptzSqlite, - joined_at -> TimestamptzSqlite, - account -> Text, - id -> Text, - } -} - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - repository_releases (id) { - repository -> Text, - update_text -> Nullable, - created_at -> TimestamptzSqlite, - updated_at -> TimestamptzSqlite, - tag -> Text, - id -> Text, - } -} - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - sessions (id) { - refresh_token -> Text, - access_token -> Text, - owner -> Text, - id -> Text, - } -} - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - user_connections (id) { - noelware_account_id -> Nullable, - google_account_id -> Nullable, - github_account_id -> Nullable, - apple_account_id -> Nullable, - created_at -> TimestamptzSqlite, - updated_at -> TimestamptzSqlite, - account -> Text, - id -> Text, - } -} - -diesel::table! { - use diesel::sql_types::*; - use crate::schema::sql_types::*; - - users (id) { - verified_publisher -> Bool, - gravatar_email -> Nullable, - description -> Nullable, - avatar_hash -> Nullable, - created_at -> TimestamptzSqlite, - updated_at -> TimestamptzSqlite, - username -> Text, - password -> Nullable, - email -> Text, - admin -> Bool, - name -> Nullable, - id -> Text, - prefers_gravatar -> Bool, - } -} - -diesel::joinable!(api_keys -> users (owner)); -diesel::joinable!(organization_members -> organizations (organization)); -diesel::joinable!(organization_members -> users (account)); -diesel::joinable!(organizations -> users (owner)); -diesel::joinable!(repository_members -> repositories (repository)); -diesel::joinable!(repository_members -> users (account)); -diesel::joinable!(repository_releases -> repositories (repository)); -diesel::joinable!(sessions -> users (owner)); -diesel::joinable!(user_connections -> users (account)); - -diesel::allow_tables_to_appear_in_same_query!( - api_keys, - organization_members, - organizations, - repositories, - repository_members, - repository_releases, - sessions, - user_connections, - users, -); diff --git a/crates/devtools/src/commands/cli.rs b/crates/devtools/src/commands/cli.rs deleted file mode 100644 index 9f1614bcc..000000000 --- a/crates/devtools/src/commands/cli.rs +++ /dev/null @@ -1,64 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use super::CommonArgs; - -/// Builds or runs the `charted` binary. -#[derive(clap::Parser)] -pub struct Args { - #[clap(flatten)] - common: CommonArgs, -} - -pub fn run(Args { common }: Args) -> eyre::Result<()> { - crate::cargo( - common.cargo.as_ref(), - match common.args.is_empty() { - true => "build", - false => "run", - }, - |cmd| { - cmd.arg("--locked"); - cmd.arg("--package").arg("charted"); - - if common.release { - cmd.arg("--release"); - } - - for arg in common.cargo_args.iter() { - cmd.arg(arg); - } - - let mut rustflags = common.rustc_flags.clone().unwrap_or_default(); - if !rustflags.is_empty() { - rustflags.push(" "); - } - - rustflags.push("--cfg tokio_unstable"); - - cmd.env("RUSTFLAGS", rustflags) - .env("CHARTED_DISTRIBUTION_KIND", "git") - .env("RUST_BACKTRACE", "1"); - - cmd.args(&common.cargo_args); - if !common.args.is_empty() { - cmd.arg("--").args(&common.args); - } - }, - ) - .map(|output| { - warn!("exited with code {}", output.status.code().unwrap_or(-1)); - }) -} diff --git a/crates/devtools/src/commands/helm_plugin.rs b/crates/devtools/src/commands/helm_plugin.rs deleted file mode 100644 index e46876374..000000000 --- a/crates/devtools/src/commands/helm_plugin.rs +++ /dev/null @@ -1,64 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use super::CommonArgs; - -/// Builds or runs the `charted-helm-plugin` binary. -#[derive(clap::Parser)] -pub struct Args { - #[clap(flatten)] - common: CommonArgs, -} - -pub fn run(Args { common }: Args) -> eyre::Result<()> { - crate::cargo( - common.cargo.as_ref(), - match common.args.is_empty() { - false => "run", - true => "build", - }, - |cmd| { - cmd.arg("--locked"); - cmd.arg("--package").arg("charted-helm-plugin"); - - if common.release { - cmd.arg("--release"); - } - - for arg in common.cargo_args.iter() { - cmd.arg(arg); - } - - let mut rustflags = common.rustc_flags.clone().unwrap_or_default(); - if !rustflags.is_empty() { - rustflags.push(" "); - } - - rustflags.push("--cfg tokio_unstable"); - - cmd.env("RUSTFLAGS", rustflags) - .env("CHARTED_DISTRIBUTION_KIND", "git") - .env("RUST_BACKTRACE", "1"); - - cmd.args(&common.cargo_args); - if !common.args.is_empty() { - cmd.arg("--").args(&common.args); - } - }, - ) - .map(|output| { - warn!("exited with code {}", output.status.code().unwrap_or(-1)); - }) -} diff --git a/crates/devtools/src/commands/internals.rs b/crates/devtools/src/commands/internals.rs deleted file mode 100644 index eee163b0f..000000000 --- a/crates/devtools/src/commands/internals.rs +++ /dev/null @@ -1,45 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use super::CommonArgs; - -/// Runs any internal commands in the `charted-internals` package. -#[derive(clap::Parser)] -pub struct Args { - #[clap(flatten)] - common: CommonArgs, -} - -pub fn run(Args { common }: Args) -> eyre::Result<()> { - crate::cargo(common.cargo.as_ref(), "run", |cmd| { - cmd.args(["--locked", "--package", "charted-internals", "--release"]); - - let mut rustflags = common.rustc_flags.clone().unwrap_or_default(); - if !rustflags.is_empty() { - rustflags.push(" "); - } - - rustflags.push("--cfg tokio_unstable"); - cmd.env("RUSTFLAGS", rustflags).env("RUST_BACKTRACE", "1"); - - cmd.args(&common.cargo_args); - if !common.args.is_empty() { - cmd.arg("--").args(&common.args); - } - }) - .map(|output| { - warn!("exited with code {}", output.status.code().unwrap_or(-1)); - }) -} diff --git a/crates/devtools/src/commands/mod.rs b/crates/devtools/src/commands/mod.rs deleted file mode 100644 index bcb538493..000000000 --- a/crates/devtools/src/commands/mod.rs +++ /dev/null @@ -1,65 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use clap::value_parser; -use std::{ffi::OsString, path::PathBuf}; - -mod cli; -mod helm_plugin; -mod internals; -mod server; - -/// Represents all the common arguments that *mostly* are present in all CLI commands -#[derive(clap::Args)] -pub struct CommonArgs { - /// Appends the `--release` flag to Cargo, which will build the specified - /// project in Release mode and adds other `rustc` flags. - #[arg(long)] - pub release: bool, - - /// Other `$RUSTFLAGS` to append when invoking Cargo. - #[arg(long, env = "RUSTFLAGS")] - pub rustc_flags: Option, - - /// Location as an absolute path to the `cargo` binary. - #[arg(long, env = "CARGO")] - pub cargo: Option, - - /// Append additional arguments to the `cargo` binary. This will removed - /// already defined arguments that a subcommand might need. - #[arg(long, env = "CARGO_ARGS")] - pub cargo_args: Vec, - - /// Additional arguments to pass to the built binary. - #[arg(value_parser = value_parser!(OsString), num_args = 0.., last = true, allow_hyphen_values = true)] - pub args: Vec, -} - -#[derive(clap::Subcommand)] -pub enum Cmd { - HelmPlugin(helm_plugin::Args), - Internals(internals::Args), - Server(server::Args), - Cli(cli::Args), -} - -pub fn run(cmd: Cmd) -> eyre::Result<()> { - match cmd { - Cmd::Server(args) => server::run(args), - Cmd::HelmPlugin(args) => helm_plugin::run(args), - Cmd::Cli(args) => cli::run(args), - Cmd::Internals(args) => internals::run(args), - } -} diff --git a/crates/devtools/src/commands/server.rs b/crates/devtools/src/commands/server.rs deleted file mode 100644 index f49945136..000000000 --- a/crates/devtools/src/commands/server.rs +++ /dev/null @@ -1,64 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use super::CommonArgs; -use std::env; - -/// Runs the API server as a single command. -#[derive(clap::Parser)] -pub struct Args { - #[clap(flatten)] - common: CommonArgs, -} - -pub fn run(Args { common }: Args) -> eyre::Result<()> { - info!("starting API server..."); - - let current_dir = env::current_dir()?; - crate::cargo(common.cargo.as_ref(), "run", |cmd| { - cmd.arg("--locked"); - cmd.arg("--package").arg("charted"); - - if common.release { - cmd.arg("--release"); - } - - for arg in common.cargo_args.iter() { - cmd.arg(arg); - } - - let mut rustflags = common.rustc_flags.clone().unwrap_or_default(); - if !rustflags.is_empty() { - rustflags.push(" "); - } - - rustflags.push("--cfg tokio_unstable"); - - cmd.env("RUSTFLAGS", rustflags) - .env("CHARTED_DISTRIBUTION_KIND", "git") - .env("RUST_BACKTRACE", "1") - .args(["--", "server"]); - - for path in [current_dir.join("config.hcl"), current_dir.join("config/charted.hcl")] { - if path.try_exists().unwrap_or(false) { - cmd.arg("--config").arg(path); - break; - } - } - }) - .map(|output| { - warn!("exited with code {}", output.status.code().unwrap_or(-1)); - }) -} diff --git a/crates/devtools/src/lib.rs b/crates/devtools/src/lib.rs deleted file mode 100644 index f42f0cd7f..000000000 --- a/crates/devtools/src/lib.rs +++ /dev/null @@ -1,143 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! The `charted-devtools` library is *unstable* and shouldn't be used outside -//! of charted itself. - -#[macro_use] -extern crate tracing; - -pub mod commands; - -use azalia::log::{writers::default::Writer, WriteLayer}; -use commands::Cmd; -use eyre::{eyre, Context}; -use std::{ - ffi::OsStr, - io, - path::{Path, PathBuf}, - process::{exit, Command, Output, Stdio}, -}; -use tracing::{level_filters::LevelFilter, Level}; - -#[derive(clap::Parser)] -pub struct Program { - /// Level to apply when configuring the CLI logger. If the range for the configured - /// log level is `DEBUG` or `TRACE`, then file paths will be emitted in the logs where - /// it was invoked. - #[arg(global = true, short = 'l', long = "log-level", env = "DEVTOOLS_LOG_LEVEL", default_value_t = Level::INFO)] - pub level: Level, - - #[command(subcommand)] - pub command: Cmd, -} - -impl Program { - pub fn init_logging(&self) { - use tracing_subscriber::prelude::*; - - let writer = Writer { - print_module: self.level >= Level::DEBUG, - print_thread: false, - - ..Default::default() - }; - - tracing_subscriber::registry() - .with(WriteLayer::new_with(io::stdout(), writer).with_filter(LevelFilter::from_level(self.level))) - .init(); - } -} - -/// Finds a binary with a specific path, or finds it under the `$PATH` variable -/// with a given `bin`. -/// -/// ## Example -/// ``` -/// # use charted_devtools::find_binary; -/// # -/// let bin = find_binary::<&str>(None, "rustc"); -/// assert!(bin.is_some()); -/// ``` -pub fn find_binary>(path: Option

, bin: &str) -> Option { - if let Some(p) = path { - return Some(p.as_ref().to_path_buf()); - } - - which::which(bin).ok() -} - -/// Creates a [`Command`] instance and returns the output of that command. The `builder` parameter -/// is used to customise the output itself or add additional arguments, environment variables, etc. -/// -/// ## Example -/// ``` -/// # use charted_devtools::execute; -/// # -/// let cmd = execute("ls", |_| {}); -/// assert!(cmd.is_ok()); -/// ``` -pub fn execute, F: FnOnce(&mut Command)>(command: C, builder: F) -> eyre::Result { - let name = command.as_ref(); - let mut cmd = Command::new(name); - builder(&mut cmd); - - // By default, `stdin` is closed; `stdout`/`stderr` are piped so you can access the output - // of the command. - cmd.stdin(Stdio::null()); - - let args = cmd.get_args().map(|x| x.to_string_lossy()).collect::>(); - info!("$ {} {}", name.to_string_lossy(), args.join(" ")); - - let output = cmd - .output() - .with_context(|| format!("failed to run command '{}'", name.to_string_lossy()))?; - - if output.status.success() { - return Ok(output); - } - - let code = output.status.code().unwrap_or(-1); - if output.stdout.is_empty() && output.stderr.is_empty() { - exit(code); - } - - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - - error!("-- command has failed with code {code} --"); - if !stdout.is_empty() { - error!("~! STDOUT !~\n{stdout}"); - } - - error!("~! STDERR !~\n{stderr}"); - - Err(eyre!("failed to run command")) -} - -pub fn cargo, F: FnOnce(&mut Command)>( - cargo: Option

, - subcommand: impl AsRef, - builder: F, -) -> eyre::Result { - let cargo = find_binary(cargo, "cargo").ok_or_else(|| eyre!("failed to find `cargo` binary"))?; - execute(cargo, |cmd| { - cmd.arg(subcommand.as_ref()); - cmd.stdout(Stdio::inherit()); - cmd.stderr(Stdio::inherit()); - - builder(cmd); - }) -} diff --git a/crates/features/Cargo.toml b/crates/features/Cargo.toml index 895858bd2..7c296f243 100644 --- a/crates/features/Cargo.toml +++ b/crates/features/Cargo.toml @@ -15,7 +15,6 @@ [package] name = "charted-features" -description = "🐻‍❄️📦 Defines traits and methods for encapsulating features" version.workspace = true documentation.workspace = true edition.workspace = true @@ -27,18 +26,3 @@ authors.workspace = true [lib] path = "lib.rs" - -[features] -default = [] - -extends-openapi = ["dep:utoipa"] -extends-db = ["dep:charted-database"] - -[dependencies] -axum.workspace = true -azalia.workspace = true -charted-app = { version = "0.1.0", path = "../app" } -charted-core.workspace = true -charted-database = { workspace = true, optional = true } -eyre.workspace = true -utoipa = { workspace = true, optional = true } diff --git a/crates/features/audit-logs/README.md b/crates/features/audit-logs/README.md deleted file mode 100644 index 07137a0ac..000000000 --- a/crates/features/audit-logs/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Audit Logging -**Audit Logging** is a way to introspect API calls within entities. diff --git a/crates/features/garbage-collection/README.md b/crates/features/garbage-collection/README.md deleted file mode 100644 index 5f6e6204f..000000000 --- a/crates/features/garbage-collection/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Garbage Collection -**Garbage Collection** is a spawned background task when the API service starts (or as a long-lived process with `charted gc --long-lived`) which will collect entities and check if they need to be thrown away into the void. diff --git a/crates/features/lib.rs b/crates/features/lib.rs index f55953f9a..e69de29bb 100644 --- a/crates/features/lib.rs +++ b/crates/features/lib.rs @@ -1,95 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use axum::Router; -use azalia::rust::AsArcAny; -use charted_app::Context; -use charted_core::BoxedFuture; -use std::any::{Any, TypeId}; - -/// Represents a feature that can be enabled or disabled by the `features` object -/// in the API server configuration file. -/// -/// For now, this is a marker trait for the `ServerContext` object to determine -/// a list of features enabled. -pub trait Feature: AsArcAny + Send + Sync { - // If the feature requires to be initialized before being in use, then this is - // method that does pre-initialization. - fn init<'feat, 'cx>(&'feat self, _cx: &'cx Context) -> BoxedFuture<'cx, eyre::Result<()>> - where - 'cx: 'feat, - { - Box::pin(async { Ok(()) }) - } - - /// Extends the API router to include endpoints. - fn extend_router(&self) -> Router { - Router::new() - } - - /// Extends the database with a given [`DbPool`][charted_database::DbPool]. - /// - /// This is mainly meant to run database migrations. - #[cfg(feature = "extends-db")] - fn extends_db<'feat, 'a>(&'feat self, _pool: &'a charted_database::DbPool) -> BoxedFuture<'a, eyre::Result<()>> - where - 'a: 'feat, - { - Box::pin(async { Ok(()) }) - } - - /// Extends the OpenAPI document. - #[cfg(feature = "extends-openapi")] - fn extends_openapi<'feat, 'a>(&'feat self, _openapi: &'a mut utoipa::openapi::OpenApi) - where - 'a: 'feat, - { - } -} - -impl dyn Feature + 'static { - /// Compares if [`self`] is `T`, similar to [`Any::is`]. - /// - /// This method might fail (as in, returns `false`) if `T` doesn't implement [`Feature`]. - /// - /// [`Any::is`]: https://doc.rust-lang.org/std/any/trait.Any.html#method.is - pub fn is(&self) -> bool { - let us = self.type_id(); - let other = TypeId::of::(); - - us == other - } - - /// Downcast `self` into [`F`], otherwise `None` is returned if `F` is not `self`. - /// - /// ## Example - /// ``` - /// # use charted_features::Feature; - /// # - /// pub struct MyFeature; - /// impl Feature for MyFeature {} - /// - /// let x: Box = Box::new(MyFeature); - /// assert!(x.downcast::().is_some()); - /// ``` - pub fn downcast(&self) -> Option<&F> { - if self.is::() { - // Safety: we ensured that `self` is `F`. - Some(unsafe { &*(self as *const dyn Feature as *const F) }) - } else { - None - } - } -} diff --git a/crates/features/oci-registry/README.md b/crates/features/oci-registry/README.md deleted file mode 100644 index 927318700..000000000 --- a/crates/features/oci-registry/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# OCI Registry -The **oci** feature allows charted-server to act like a proper implementation of the [OCI Registry specification](https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md) v1.1.0. diff --git a/crates/features/totp/README.md b/crates/features/totp/README.md deleted file mode 100644 index 5738218a7..000000000 --- a/crates/features/totp/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# TOTP Feature - -The **totp** feature allows charted's local authorization backend to include 2FA. - -## Configuration - -```hcl -feature "totp" { - secret = "..." -} -``` diff --git a/crates/features/webhooks/README.md b/crates/features/webhooks/README.md deleted file mode 100644 index 71876620b..000000000 --- a/crates/features/webhooks/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# HTTP Webhooks -This is the main implementation of the HTTP webhooks feature that can be enabled with the `feature "webhooks"` flag in the HCL configuration format. **charted-server** has security features placed so you can't easily poll data that you need in a fast interval, so this is where this features comes in. It implements the [Standard Webhooks Specification](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md) to allow transmitting events to HTTP callbacks easily. diff --git a/crates/helm-plugin/Cargo.toml b/crates/helm-plugin/Cargo.toml deleted file mode 100644 index 0d0fd8fc7..000000000 --- a/crates/helm-plugin/Cargo.toml +++ /dev/null @@ -1,53 +0,0 @@ -# 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -# Copyright 2022-2025 Noelware, LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -[package] -name = "charted-helm-plugin" -description = "🐻‍❄️📦 Helm plugin to help aid developing Helm charts with charted" -version.workspace = true -documentation.workspace = true -edition.workspace = true -homepage.workspace = true -license.workspace = true -publish.workspace = true -repository.workspace = true -authors.workspace = true - -[dependencies] -azalia = { workspace = true, features = [ - "log", - "log-writers", - "log-tracing-log", -] } -charted-core.workspace = true -charted-types = { workspace = true, features = ["jsonschema"] } -clap = { workspace = true, features = ["derive", "env"] } -clap_complete.workspace = true -color-eyre = "0.6.3" -comfy-table = "7.1.1" -etcetera = "0.8.0" -eyre.workspace = true -http = "1.1.0" -reqwest = "0.12.7" -reqwest-middleware = { version = "0.4.0", features = ["json", "http2"] } -schemars = { workspace = true, features = ["url"] } -serde.workspace = true -serde_json.workspace = true -tokio = { workspace = true, features = ["rt", "macros"] } -toml = "0.8.19" -tracing.workspace = true -tracing-subscriber.workspace = true -url = { version = "2.5.2", features = ["serde"] } -which = { workspace = true, features = ["tracing"] } diff --git a/crates/helm-plugin/src/args.rs b/crates/helm-plugin/src/args.rs deleted file mode 100644 index 7d2eab3a2..000000000 --- a/crates/helm-plugin/src/args.rs +++ /dev/null @@ -1,43 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::path::PathBuf; - -#[derive(Debug, Clone, clap::Args)] -pub struct Common { - /// Location to a `.charted.hcl` configuration file to be evaluated from. Defaults - /// to `$CWD/.charted.hcl`. - #[arg(short = 'c', long = "config", env = "CHARTED_HELM_CONFIG_PATH")] - pub config_path: Option, - - /// Location to a `helm` binary. By default, it'll look in the system `$PATH`. - #[arg(long, env = "CHARTED_HELM_BINARY")] - pub helm: Option, -} - -#[derive(Debug, Clone, clap::Args)] -pub struct Auth { - /// Location to a `auth.toml` configuration file which represents the ways to authenticate - /// with a [charted-server](https://charts.noelware.org/docs/server/latest) registry. - /// - /// ## Default Locations - /// | Operating System | Filesystem Location | - /// | :--------------- | :----------------------------------------------------------------------------------------------------- | - /// | Windows | `C:\Users\{username}\AppData\Local\Noelware\charted-server\auth.toml` | - /// | macOS | `/Users/{username}/Library/Application Support/Noelware/charted-server/auth.toml` | - /// | Linux | `$XDG_CONFIG_DIR/Noelware/charted-server/auth.toml`, `$HOME/.config/Noelware/charted-server/auth.toml` | - #[arg(long = "auth", short = 'a', env = "CHARTED_HELM_AUTH_TOML_PATH")] - pub path: Option, -} diff --git a/crates/helm-plugin/src/auth.rs b/crates/helm-plugin/src/auth.rs deleted file mode 100644 index ea26d6c0c..000000000 --- a/crates/helm-plugin/src/auth.rs +++ /dev/null @@ -1,184 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs}; -use eyre::{Context as _, ContextCompat}; -use serde::{Deserialize, Serialize}; -use std::{ - borrow::Cow, - collections::HashMap, - fs::{self, File, OpenOptions}, - io::Read, - path::Path, -}; -use tracing::{info, trace, warn}; - -/// Represents what "kind" of authentication for the `environmentVariable` type. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub enum EnvironmentVariableKind { - /// Uses the API key authentication type. - #[default] - ApiKey, - - /// Uses the session authentication type. - Bearer, -} - -/// Determines what type of authentication to perform based off a registry. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum Type { - /// Lookup the authentication key via the system environment variable. - EnvironmentVariable { - /// What kind of authentication is the environment variable for? - kind: EnvironmentVariableKind, - - /// The environment variable name - name: Cow<'static, str>, - }, - - /// Uses an created API key. By default, when `helm charted login` is ran, it'll - /// create an API key on your user and use it indefinitely (or until you remove it - /// via REST API or UI (if enabled)). - ApiKey(String), - - /// Uses basic authentication, not recommended in production! - Basic { username: String, password: String }, - - /// Does no prior authentication. - #[default] - None, -} - -/// Represents the schematic of the `auth.toml` configuration file. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Auth { - /// Represents the current context that we are in. To switch to a different one, - /// you can use the `Auth::switch` method. - pub current: Cow<'static, str>, - - /// All the avaliable contexts. This can be manually edited or can be manipulated - /// with the `helm charted auth` subcommand. - #[serde(rename = "context")] - pub contexts: HashMap, -} - -impl Default for Auth { - fn default() -> Self { - let official = Context { - kind: Type::None, - registry: "https://charts.noelware.org/api".into(), - }; - - Auth { - current: Cow::Borrowed("default"), - contexts: azalia::hashmap!("default" => official), - } - } -} - -impl Auth { - /// Loads the `auth.toml` configuration file with a *optional* file path. - pub fn load>(path: Option

) -> eyre::Result { - let strategy = choose_app_strategy(AppStrategyArgs { - top_level_domain: "org".to_string(), - author: "Noelware".to_string(), - app_name: "charted helm plugin".to_string(), - })?; - - let path = path - .map(|x| x.as_ref().to_path_buf()) - .unwrap_or(strategy.config_dir().join("Noelware/charted-server/auth.toml")); - - trace!(path = %path.display(), "loading `auth.toml`..."); - if !path.try_exists()? { - warn!(path = %path.display(), "`auth.toml` doesn't exist! creating..."); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - - let auth = Auth::default(); - - File::create_new(&path)?; - auth.sync(Some(path))?; - - return Ok(auth); - } - - let mut contents = String::new(); - - { - let mut file = File::open(&path)?; - file.read_to_string(&mut contents)?; - } - - toml::from_str(&contents).context("failed to parse `auth.toml`") - } - - pub fn sync(&self, path: Option>) -> eyre::Result<()> { - use std::io::Write; - - info!("syncing `auth.toml` changes..."); - - let strategy = choose_app_strategy(AppStrategyArgs { - top_level_domain: "org".to_string(), - author: "Noelware".to_string(), - app_name: "charted helm plugin".to_string(), - })?; - - let path = path - .map(|x| x.as_ref().to_path_buf()) - .unwrap_or(strategy.config_dir().join("Noelware/charted-server/auth.toml")); - - let mut file = OpenOptions::new() - .create(false) - .truncate(true) - .write(true) - .read(true) - .open(path)?; - - write!( - file, - "{}", - toml::to_string_pretty(self).context("failed to serialize into toml")? - ) - .context("failed to sync new changes into file") - } - - pub fn switch(&mut self, path: Option>, context: impl Into) -> eyre::Result<()> { - let context = context.into(); - if self.current == context { - warn!("not switching to context `{context}` due to being the default already"); - return Ok(()); - } - - if !self.contexts.contains_key(&context) { - warn!("not switching to context `{context}` due to it not existing"); - return Ok(()); - } - - let old = self.current.clone(); - self.current = Cow::Owned(context.clone()); - - info!("switched from {old} ~> {context}, now syncing changes..."); - self.sync(path) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Context { - pub kind: Type, - pub registry: String, -} diff --git a/crates/helm-plugin/src/cmds/auth/list.rs b/crates/helm-plugin/src/cmds/auth/list.rs deleted file mode 100644 index 7f5d570a6..000000000 --- a/crates/helm-plugin/src/cmds/auth/list.rs +++ /dev/null @@ -1,93 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use comfy_table::{presets, Attribute, Cell, ContentArrangement, Row, Table}; -use serde_json::{json, Map, Value}; - -use crate::{ - args, - auth::{Auth, Type}, -}; - -#[derive(Debug, Clone, clap::Parser)] -pub struct Args { - #[clap(flatten)] - auth: args::Auth, - - /// exports it as json - #[arg(long, short = 'j')] - json: bool, -} - -pub fn run( - Args { - json, - auth: args::Auth { path }, - }: Args, -) -> eyre::Result<()> { - let auth = Auth::load(path.as_ref())?; - if json { - let mut obj = Map::::with_capacity(auth.contexts.len()); - for (name, cx) in &auth.contexts { - obj.insert( - name.clone(), - json!({ - "current": auth.current == *name, - "registry": cx.registry, - "kind": match cx.kind { - Type::EnvironmentVariable { ..} => "EnvironmentVariable", - Type::ApiKey(_) => "ApiKey", - Type::Basic { .. } => "Basic", - Type::None => "None" - } - }), - ); - } - - let data = serde_json::to_string_pretty(&Value::Object(obj))?; - println!("{data}"); - - return Ok(()); - } - - let mut table = Table::new(); - table - .load_preset(presets::ASCII_FULL) - .set_content_arrangement(ContentArrangement::Dynamic) - .set_header(vec![ - Cell::new("Current").add_attribute(Attribute::Bold), - Cell::new("Registry").add_attribute(Attribute::Bold), - Cell::new("Kind").add_attribute(Attribute::Bold), - ]); - - for (name, cx) in &auth.contexts { - let mut row = Row::new(); - let current = if auth.current == *name { "*" } else { "" }; - - row.add_cell(Cell::new(current).add_attribute(Attribute::Bold)); - row.add_cell(Cell::new(&cx.registry)); - row.add_cell(Cell::new(match cx.kind { - Type::EnvironmentVariable { ref name, .. } => format!("Environment Variable ${name}"), - Type::ApiKey(_) => "API Key".into(), - Type::Basic { .. } => "Basic Auth".into(), - Type::None => "None".into(), - })); - - table.add_row(row); - } - - println!("{table}"); - Ok(()) -} diff --git a/crates/helm-plugin/src/cmds/completions.rs b/crates/helm-plugin/src/cmds/completions.rs deleted file mode 100644 index 0dddc2472..000000000 --- a/crates/helm-plugin/src/cmds/completions.rs +++ /dev/null @@ -1,45 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::Program; -use clap::CommandFactory; -use clap_complete::{generate, Shell}; -use eyre::eyre; -use std::io; -use tracing::trace; - -#[derive(Debug, Clone, clap::Parser)] -pub struct Args { - shell: Option, -} - -pub fn run(Args { shell }: Args) -> eyre::Result<()> { - let Some(shell) = shell else { - trace!("figuring out shell based off $SHELL environment variable"); - - let Some(shell) = Shell::from_env() else { - trace!("...it wasn't found or included invalid unicode"); - return Err(eyre!("tried to detect shell based off the `$SHELL` environment variable but wasn't found or included invalid unicode")); - }; - - // re-run the command with a shell set - return run(Args { shell: Some(shell) }); - }; - - let mut cmd = Program::command(); - generate(shell, &mut cmd, "charted", &mut io::stdout()); - - Ok(()) -} diff --git a/crates/helm-plugin/src/cmds/download.rs b/crates/helm-plugin/src/cmds/download.rs deleted file mode 100644 index f90ebbf40..000000000 --- a/crates/helm-plugin/src/cmds/download.rs +++ /dev/null @@ -1,26 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use url::Url; - -/// This command is only used by Helm for the `charted://` downloader protocol. This -/// shouldn't be used outside of that. -#[derive(Debug, Clone, clap::Parser)] -pub struct Args { - cert_file: String, - key_file: String, - ca_file: String, - url: Url, -} diff --git a/crates/helm-plugin/src/cmds/init.rs b/crates/helm-plugin/src/cmds/init.rs deleted file mode 100644 index 6bffe91d7..000000000 --- a/crates/helm-plugin/src/cmds/init.rs +++ /dev/null @@ -1,14 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. diff --git a/crates/helm-plugin/src/cmds/mod.rs b/crates/helm-plugin/src/cmds/mod.rs deleted file mode 100644 index de5222f3f..000000000 --- a/crates/helm-plugin/src/cmds/mod.rs +++ /dev/null @@ -1,39 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -mod auth; -mod completions; -mod download; -mod init; -mod login; -mod pull; -mod version; - -#[derive(Debug, Clone, clap::Subcommand)] -pub enum Cmd { - Completions(completions::Args), - - #[command(subcommand)] - Auth(auth::Cmd), -} - -impl Cmd { - pub async fn run(self) -> eyre::Result<()> { - match self { - Cmd::Auth(cmd) => cmd.run(), - Cmd::Completions(args) => completions::run(args), - } - } -} diff --git a/crates/helm-plugin/src/cmds/pull.rs b/crates/helm-plugin/src/cmds/pull.rs deleted file mode 100644 index 6bffe91d7..000000000 --- a/crates/helm-plugin/src/cmds/pull.rs +++ /dev/null @@ -1,14 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. diff --git a/crates/helm-plugin/src/config/charted.rs b/crates/helm-plugin/src/config/charted.rs deleted file mode 100644 index daa63a879..000000000 --- a/crates/helm-plugin/src/config/charted.rs +++ /dev/null @@ -1,58 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use charted_types::VersionReq; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -const DEFAULT_HELM_VERSION_CONSTRAINT: &str = ">=3.13"; - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct Charted { - /// Version constraint to what `charted-helm-plugin` any repository supports. - /// - /// This is useful to determine if new features of `charted-helm-plugin` can be used or not. - /// - /// The plugin will error if the version constraint is not supported with what - /// our version is. - #[serde(default = "__default_charted_version_constraint")] - pub version: VersionReq, - - /// Version constraint to what version of [Helm] is supported. - /// - /// This is useful to use features of Helm that the plugin might use can be enabled. - /// - /// The plugin will error if the version constraint is not supported with what - /// our version is. - #[serde(default = "__default_helm_version_constraint")] - pub helm: VersionReq, -} - -impl Default for Charted { - fn default() -> Self { - Charted { - version: __default_charted_version_constraint(), - helm: __default_helm_version_constraint(), - } - } -} - -fn __default_charted_version_constraint() -> VersionReq { - VersionReq::parse(&format!(">={}", charted_core::VERSION)).unwrap() -} - -fn __default_helm_version_constraint() -> VersionReq { - VersionReq::parse(DEFAULT_HELM_VERSION_CONSTRAINT).unwrap() -} diff --git a/crates/helm-plugin/src/config/mod.rs b/crates/helm-plugin/src/config/mod.rs deleted file mode 100644 index e64a1ad1e..000000000 --- a/crates/helm-plugin/src/config/mod.rs +++ /dev/null @@ -1,117 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -mod charted; -pub use charted::*; - -mod registry; -pub use registry::*; - -mod repository; -pub use repository::*; - -use eyre::Context; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - env, - fs::{self, File}, - path::Path, -}; -use tracing::{info, warn}; - -/// Configuration schematic for the `.charted.toml` configuration file, which is used -/// by the Helm plugin to see what repositories are avaliable to be used. -/// -/// ## Example -/// ```toml -/// # `version` and `helm` are version constraints to determine -/// # what versions of `charted-helm-plugin` (`version`) and -/// # Helm (`helm`) is supported. -/// [charted] -/// version = ">=0.1.0" -/// helm = ">=3.12" -/// -/// # The `registry` configuration allows to use other registries -/// # as the official instance is registered by default via -/// # the `default` key. -/// [registry.private] -/// version = 1 -/// url = "https://corpo.noelware.dev" -/// -/// [[repository]] -/// source = "./charts/charted" -/// name = "server" -/// path = "charted/server" -/// ``` -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct Config { - /// The `charted` table allows to configure the Helm plugin. - #[serde(default)] - pub charted: Charted, - - /// List of configured registries. - #[serde(default, rename = "registry", skip_serializing_if = "HashMap::is_empty")] - pub registries: HashMap, - - /// List of repositories avaliable. - #[serde(default, rename = "repository", skip_serializing_if = "Vec::is_empty")] - pub repositories: Vec, -} - -impl Config { - /// Loads the configuration file in the given `path` if provided. If not, it'll - /// look in `$CWD/.charted.toml` instead. - pub fn load>(path: Option

) -> eyre::Result { - use std::io::Write; - - let current_dir = env::current_dir()?; - let path = path - .map(|x| x.as_ref().to_path_buf()) - .unwrap_or(current_dir.join(".charted.toml")); - - if !path.try_exists()? { - warn!(path = %path.display(), ".charted.toml doesn't exist! creating default one..."); - - let me = Config::default(); - { - let mut file = File::create_new(&path)?; - write!(file, "{}", toml::to_string_pretty(&me)?)?; - } - - info!(path = %path.display(), "created default `.charted.toml` in given path"); - return Ok(me); - } - - let contents = fs::read_to_string(path)?; - toml::from_str(&contents).context("failed to parse toml configuration file") - } -} - -impl Default for Config { - fn default() -> Self { - Config { - charted: Charted::default(), - repositories: Vec::new(), - registries: azalia::hashmap!( - "default" => Registry { - version: charted_core::api::Version::V1, - url: "https://charts.noelware.org/api".parse().unwrap() - } - ), - } - } -} diff --git a/crates/helm-plugin/src/config/registry.rs b/crates/helm-plugin/src/config/registry.rs deleted file mode 100644 index b4ebeef44..000000000 --- a/crates/helm-plugin/src/config/registry.rs +++ /dev/null @@ -1,75 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use charted_core::api::Version; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::fmt::Display; -use url::Url; - -/// The `[registry.]` table allows to configure all the avaliable registries. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct Registry { - /// API version of the registry. - #[serde(default)] - pub version: Version, - - /// URL of the registry to point to. This doesn't include the API version - /// in the URI itself (i.e, `https://charts.noelware.org/api/v1`). - pub url: Url, -} - -impl Registry { - /// Joins the registry URL via [`Url::join`] and returns a string representation. - pub fn join_url(&self, input: T) -> Result { - // `format!()` is necessary here since if we tried to do 2 joins, it'll only - // return the second join without applying the first one. - self.url - .join(&format!("{}/{input}", self.version)) - .map(|x| x.to_string()) - } -} - -impl Display for Registry { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - self.url.join(self.version.as_str()).map_err(|_| - /* this will map all url::ParseError as formatting errors */ - std::fmt::Error)? - ) - } -} - -#[cfg(test)] -mod tests { - use super::Registry; - use charted_core::api::Version; - use url::Url; - - #[test] - fn url_joins() { - let registry = Registry { - version: Version::default(), - url: Url::parse("https://charts.noelware.org").expect("invalid url"), - }; - - assert_eq!( - Ok(String::from("https://charts.noelware.org/v1/weow/fluff")), - registry.join_url("weow/fluff") - ); - } -} diff --git a/crates/helm-plugin/src/config/repository.rs b/crates/helm-plugin/src/config/repository.rs deleted file mode 100644 index eeffbdcb7..000000000 --- a/crates/helm-plugin/src/config/repository.rs +++ /dev/null @@ -1,102 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use charted_types::name::Name; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -/// The `[[repository]]` array table allows to register a repository that `charted-helm-plugin` -/// can discover and allow to do operations on. -/// -/// ## Example -/// ```toml -/// [[repository]] -/// publish = true -/// registry = "default" -/// source = "./charts/server" -/// name = "charted/server" -/// ``` -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct Repository { - /// whether or not if this repository can be published to a registry. Defaults - /// to `true`. - #[serde(default = "__truthy")] - pub publish: bool, - - /// Registry to publish this repository to. - #[serde(default = "__default_registry")] - pub registry: String, - - /// Source location of the repository. This is relative to the directory - /// where `charted-helm-plugin` is operating in. - pub source: PathBuf, - - /// Path to a README file, this will default to `{repository..source}/README.md` if it exists. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub readme: Option, - - /// Path to the repository's full identifier. This is represented as two [`Name`]s with - /// a slash, i.e: `noel/my-project` - #[serde( - serialize_with = "__repository_name::serialize", - deserialize_with = "__repository_name::deserialize" - )] - pub name: (Name, Name), -} - -const fn __truthy() -> bool { - true -} - -fn __default_registry() -> String { - String::from("default") -} - -mod __repository_name { - use charted_types::name::Name; - use serde::{de, Deserializer, Serializer}; - - pub fn serialize(value: &(Name, Name), serializer: S) -> Result { - // TODO(@auguwu): validate that it is a valid `Name/Name` pairing. - serializer.serialize_str(&format!("{}/{}", value.0, value.1)) - } - - pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<(Name, Name), D::Error> { - use serde::de::Error; - - struct Visitor; - impl de::Visitor<'_> for Visitor { - type Value = (Name, Name); - - fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "valid mapping of {{owner}}/{{repo}}") - } - - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - match v.split_once('/') { - Some((_, repo)) if repo.contains('/') => Err(E::custom("found more than one slash")), - Some((name, repo)) => Ok((name.parse().map_err(E::custom)?, repo.parse().map_err(E::custom)?)), - None => Err(E::custom("failed to parse repo path, expected [name/repo] match")), - } - } - } - - deserializer.deserialize_str(Visitor) - } -} diff --git a/crates/helm-plugin/src/lib.rs b/crates/helm-plugin/src/lib.rs deleted file mode 100644 index 20b5956e9..000000000 --- a/crates/helm-plugin/src/lib.rs +++ /dev/null @@ -1,79 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![allow(clippy::incompatible_msrv, unused)] - -pub(crate) mod args; -pub(crate) mod auth; -pub(crate) mod config; -pub(crate) mod ops; -pub(crate) mod util; - -pub mod cmds; - -use azalia::log::{writers::default::Writer, WriteLayer}; -use std::io; -use tracing::{level_filters::LevelFilter, Level}; -use tracing_subscriber::prelude::*; - -#[derive(Debug, Clone, clap::Parser)] -#[clap( - bin_name = "helm charted", - about = "🐻‍❄️📦 Helm plugin to help aid developing Helm charts with charted", - author = "Noelware, LLC. ", - override_usage = "helm charted [...ARGS]", - arg_required_else_help = true -)] -pub struct Program { - /// Configures the log level for the logs that are transmitted. - #[arg( - global = true, - short = 'l', - long = "log-level", - default_value_t = Level::INFO, - env = "CHARTED_HELM_LOG_LEVEL" - )] - pub level: Level, - - #[command(subcommand)] - pub cmd: cmds::Cmd, -} - -impl Program { - pub fn init_logger(&self) { - tracing_subscriber::registry() - .with( - WriteLayer::new_with( - io::stdout(), - Writer { - print_module: false, - print_thread: false, - - ..Default::default() - }, - ) - .with_filter(LevelFilter::from_level(self.level)), - ) - .init(); - } -} - -#[cfg(test)] -#[test] -fn cli() { - use clap::CommandFactory; - - Program::command().debug_assert(); -} diff --git a/crates/helm-plugin/src/ops/common.rs b/crates/helm-plugin/src/ops/common.rs deleted file mode 100644 index 31356778d..000000000 --- a/crates/helm-plugin/src/ops/common.rs +++ /dev/null @@ -1,18 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub async fn _ping_registry() -> eyre::Result<()> { - Ok(()) -} diff --git a/crates/helm-plugin/src/ops/middleware.rs b/crates/helm-plugin/src/ops/middleware.rs deleted file mode 100644 index 480fadf42..000000000 --- a/crates/helm-plugin/src/ops/middleware.rs +++ /dev/null @@ -1,43 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use charted_core::BoxedFuture; -use http::Extensions; -use reqwest::{Request, Response}; -use reqwest_middleware::Next; -use std::time::Instant; -use tracing::{info, Instrument}; - -pub fn logging<'a>( - req: Request, - extensions: &'a mut Extensions, - next: Next<'a>, -) -> BoxedFuture<'a, reqwest_middleware::Result> { - let future = async move { - info!("-> {} {}", req.method(), req.url()); - - let start = Instant::now(); - let res = next.run(req, extensions).await?; - info!( - duration = %charted_core::serde::Duration::from(start.elapsed()), - "<- {}", - res.status() - ); - - Ok(res) - }; - - Box::pin(future.instrument(tracing::info_span!("charted.helm.http.request"))) -} diff --git a/crates/helm-plugin/src/ops/mod.rs b/crates/helm-plugin/src/ops/mod.rs deleted file mode 100644 index bcb087696..000000000 --- a/crates/helm-plugin/src/ops/mod.rs +++ /dev/null @@ -1,36 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! The `charted_helm_plugin::ops` module contains all the operations. - -mod common; -mod download; -mod middleware; - -use std::sync::LazyLock; - -pub static HTTP: LazyLock = LazyLock::new(|| { - let reqwest = ::reqwest::ClientBuilder::new() - .user_agent(format!( - "Noelware/charted-helm-plugin (+{}; https://github.com/charted-dev/charted/tree/main/crates/helm-plugin)", - charted_core::version() - )) - .build() - .unwrap(); - - reqwest_middleware::ClientBuilder::new(reqwest) - .with(middleware::logging) - .build() -}); diff --git a/crates/helm-plugin/src/util.rs b/crates/helm-plugin/src/util.rs deleted file mode 100644 index 22f17883a..000000000 --- a/crates/helm-plugin/src/util.rs +++ /dev/null @@ -1,179 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::config::Config; -use charted_types::Version; -use std::{ - path::{Path, PathBuf}, - process::{exit, Command}, -}; -use tracing::{error, trace, warn}; - -pub fn get_helm_binary>(helm: Option

) -> Option { - if let Some(helm) = helm { - return Some(helm.as_ref().to_path_buf()); - } - - match which::which("helm") { - Ok(path) => Some(path), - Err(which::Error::CannotFindBinaryPath) => { - warn!("cannot find `helm` bninary in `$PATH`."); - None - } - - Err(e) => { - error!(error = %e, "received an error while trying to find `helm` binary from $PATH"); - None - } - } -} - -pub fn validate_version_constraints>(config: &Config, helm: Option

) -> eyre::Result<()> { - if !config - .charted - .version - .matches(&Version::parse(charted_core::VERSION)?.into()) - { - error!( - ".charted.toml expected that `charted-helm-plugin` to match the version constraint: {}; but `charted-helm-plugin` is at {}", - config.charted.version, - charted_core::VERSION - ); - - exit(1); - } - - let Some(helm) = get_helm_binary(helm) else { - exit(1); - }; - - let mut cmd = Command::new(&helm); - cmd.args(["version", "--template", "'{{ .Version }}'"]); - - trace!( - "$ {} {}", - helm.display(), - cmd.get_args() - .map(|x| x.to_string_lossy()) - .collect::>() - .join(" ") - ); - - match cmd.output() { - Ok(output) if output.status.success() => { - let version = String::from_utf8_lossy(&output.stdout) - .trim() - .replacen('\'', "", 2) - .replacen('v', "", 1); - - let semver = Version::parse(&version)?; - if !config.charted.helm.matches(&semver.into()) { - error!( - ".charted.toml expected Helm version to match version constraint: {}; but `helm version` printed {version} instead", - config.charted.helm - ); - - exit(1); - } - - Ok(()) - } - - Ok(output) => { - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - - error!(helm = %helm.display(), "received an abnormal status code [{}] with `helm version --template '{{ .Version }}'`", output.status.code().unwrap_or(-1)); - error!("report this to Noelware: https://github.com/charted-dev/charted/issues/new"); - error!("~~~ stdout ~~~"); - error!("{}", stdout.trim()); - error!("~~~ stderr ~~~"); - error!("{}", stderr.trim()); - - exit(1); - } - - Err(e) => { - error!(error = %e, helm = %helm.display(), "unable to run `helm version --format '{{ .Version }}'`:"); - exit(1); - } - } -} - -/* -/// Validate the `charted { version = "..." }` and `charted { helm = "..." }` constraints -/// with a one-liner when called. -pub fn validate_version_constraints>(config: &Config, helm: Option

) { - match cmd.output() { - Ok(output) => { - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - - error!(helm = %helm.display(), "received an abnormal status code [{}] with `helm version --template '{{ .Version }}'`", output.status.code().unwrap_or(-1)); - error!("report this to Noelware: https://github.com/charted-dev/charted/issues/new"); - error!("~~~ stdout ~~~"); - error!("{}", stdout.trim()); - error!("~~~ stderr ~~~"); - error!("{}", stderr.trim()); - - exit(1); - } - - Err(e) => { - error!(error = %e, helm = %helm.display(), "unable to run `helm version --format '{{ .Version }}'`:"); - exit(1); - } - } -} - -pub fn set_auth_details(req: &mut RequestBuilder, ty: &Type) -> eyre::Result<()> { - match ty { - Type::ApiKey(key) => { - *req = req - .try_clone() - .ok_or_else(|| eyre!("failed to clone `RequestBuilder`"))? - .header(AUTHORIZATION, format!("ApiKey {key}")); - } - - Type::Session { access, .. } => { - *req = req - .try_clone() - .ok_or_else(|| eyre!("failed to clone `RequestBuilder`"))? - .bearer_auth(access); - } - - Type::EnvironmentVariable { kind, env } => { - let value = - noelware_config::env!(env).with_context(|| format!("environment variable ${env} doesn't exist"))?; - - *req = req - .try_clone() - .ok_or_else(|| eyre!("failed to clone `RequestBuilder`"))? - .header(AUTHORIZATION, format!("{kind} {value}")); - } - - Type::Basic { username, password } => { - *req = req - .try_clone() - .ok_or_else(|| eyre!("failed to clone `RequestBuilder`"))? - .basic_auth(username, Some(password)); - } - - _ => {} - } - - Ok(()) -} -*/ diff --git a/crates/helm-charts/Cargo.toml b/crates/helm/charts/Cargo.toml similarity index 94% rename from crates/helm-charts/Cargo.toml rename to crates/helm/charts/Cargo.toml index 2e6e12268..0400ad3be 100644 --- a/crates/helm-charts/Cargo.toml +++ b/crates/helm/charts/Cargo.toml @@ -24,9 +24,13 @@ publish.workspace = true repository.workspace = true authors.workspace = true +[lib] +path = "lib.rs" + [dependencies] azalia = { workspace = true, features = ["remi", "remi-fs"] } -charted-types = { version = "0.1.0", path = "../types" } +charted-types.workspace = true +charted-helm-types.workspace = true eyre.workspace = true flate2 = "1.0.32" itertools = "0.14.0" diff --git a/crates/helm-charts/__fixtures__/README b/crates/helm/charts/__fixtures__/README similarity index 100% rename from crates/helm-charts/__fixtures__/README rename to crates/helm/charts/__fixtures__/README diff --git a/crates/helm-charts/__fixtures__/hello-world.tgz b/crates/helm/charts/__fixtures__/hello-world.tgz similarity index 100% rename from crates/helm-charts/__fixtures__/hello-world.tgz rename to crates/helm/charts/__fixtures__/hello-world.tgz diff --git a/crates/helm-charts/__fixtures__/youtrack.tgz b/crates/helm/charts/__fixtures__/youtrack.tgz similarity index 100% rename from crates/helm-charts/__fixtures__/youtrack.tgz rename to crates/helm/charts/__fixtures__/youtrack.tgz diff --git a/crates/helm-charts/src/lib.rs b/crates/helm/charts/lib.rs similarity index 99% rename from crates/helm-charts/src/lib.rs rename to crates/helm/charts/lib.rs index 75c7f829c..e2b31522c 100644 --- a/crates/helm-charts/src/lib.rs +++ b/crates/helm/charts/lib.rs @@ -17,7 +17,8 @@ use azalia::remi::{ core::{Blob, StorageService as _, UploadRequest}, StorageService, }; -use charted_types::{helm::ChartIndex, Ulid, Version}; +use charted_helm_types::ChartIndex; +use charted_types::{Ulid, Version}; use eyre::{eyre, Context, Report}; use flate2::bufread::MultiGzDecoder; use itertools::Itertools; diff --git a/crates/helm/plugin/Cargo.toml b/crates/helm/plugin/Cargo.toml new file mode 100644 index 000000000..9432bc58a --- /dev/null +++ b/crates/helm/plugin/Cargo.toml @@ -0,0 +1,29 @@ +# 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +# Copyright 2022-2025 Noelware, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[package] +name = "charted-helm-plugin" +version.workspace = true +documentation.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +publish.workspace = true +repository.workspace = true +authors.workspace = true + +[[bin]] +name = "charted-helm-plugin" +path = "../../../src/helm-plugin/main.rs" diff --git a/crates/helm/plugin/src/auth.rs b/crates/helm/plugin/src/auth.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/helm/plugin/src/cli/commands/authz.rs b/crates/helm/plugin/src/cli/commands/authz.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/helm/plugin/src/cli/commands/authz/add.rs b/crates/helm/plugin/src/cli/commands/authz/add.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/helm/plugin/src/cli/commands/authz/delete.rs b/crates/helm/plugin/src/cli/commands/authz/delete.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/helm/plugin/src/cli/commands/authz/list.rs b/crates/helm/plugin/src/cli/commands/authz/list.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/helm/plugin/src/cli/commands/authz/token.rs b/crates/helm/plugin/src/cli/commands/authz/token.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/helm/plugin/src/cli/commands/completions.rs b/crates/helm/plugin/src/cli/commands/completions.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/helm/plugin/src/cli/commands/context.rs b/crates/helm/plugin/src/cli/commands/context.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/helm/plugin/src/cli/commands/context/add.rs b/crates/helm/plugin/src/cli/commands/context/add.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/helm/plugin/src/cli/commands/context/delete.rs b/crates/helm/plugin/src/cli/commands/context/delete.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/helm/plugin/src/cli/commands/context/list.rs b/crates/helm/plugin/src/cli/commands/context/list.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/helm/plugin/src/cli/commands/context/switch.rs b/crates/helm/plugin/src/cli/commands/context/switch.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/helm/plugin/src/cli/commands/download.rs b/crates/helm/plugin/src/cli/commands/download.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/helm/plugin/src/cli/commands/login.rs b/crates/helm/plugin/src/cli/commands/login.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/helm/plugin/src/cli/commands/logout.rs b/crates/helm/plugin/src/cli/commands/logout.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/helm/plugin/src/cli/mod.rs b/crates/helm/plugin/src/cli/mod.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/helm/plugin/src/lib.rs b/crates/helm/plugin/src/lib.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/app/Cargo.toml b/crates/helm/types/Cargo.toml similarity index 64% rename from crates/app/Cargo.toml rename to crates/helm/types/Cargo.toml index 6f8a5cd5d..1bdf8e42f 100644 --- a/crates/app/Cargo.toml +++ b/crates/helm/types/Cargo.toml @@ -14,28 +14,31 @@ # limitations under the License. [package] -name = "charted-app" +name = "charted-helm-types" +description = "🐻‍❄️📦 Generic crate that holds all types related to Helm (https://helm.sh)" version.workspace = true documentation.workspace = true edition.workspace = true homepage.workspace = true license.workspace = true -publish.workspace = true +publish = true repository.workspace = true authors.workspace = true +rust-version.workspace = true + +[lib] +path = "lib.rs" + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(noeldoc)'] } + +[features] +default = [] + +openapi = ["dep:utoipa", "charted-types/openapi"] [dependencies] -azalia = { workspace = true, features = [ - "remi", - "remi-fs", - "remi-s3", - "remi-azure", -] } -charted-authz = { version = "0.1.0", path = "../authz" } -charted-authz-ldap = { version = "0.1.0", path = "../authz/ldap" } -charted-authz-local = { version = "0.1.0", path = "../authz/local" } -charted-config = { version = "0.1.0", path = "../config" } -charted-core.workspace = true -charted-database.workspace = true -eyre.workspace = true -tracing.workspace = true +charted-types.workspace = true +chrono.workspace = true +serde.workspace = true +utoipa = { workspace = true, optional = true } diff --git a/crates/helm/types/lib.rs b/crates/helm/types/lib.rs new file mode 100644 index 000000000..653130496 --- /dev/null +++ b/crates/helm/types/lib.rs @@ -0,0 +1,281 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # 🐻‍❄️📦 `charted_helm_types` +//! This crate is just a generic crate that has Rust types for +//! the [Helm](https://helm.sh) project. +//! +//! At the moment, it is hand written but in the future it'll probably +//! be code-generated based off which version of Helm we want to support. + +#![cfg_attr(any(noeldoc, docsrs), feature(doc_cfg))] +#![doc(html_logo_url = "https://cdn.floofy.dev/images/trans.png")] +#![doc(html_favicon_url = "https://cdn.floofy.dev/images/trans.png")] + +use charted_types::{DateTime, Version, VersionReq}; +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +pub use charted_types::ChartType; + +/// The `apiVersion` field should be `v2` for Helm charts that require at least Helm 3. +/// +/// Charts supporting previous Helm versions should have an `apiVersion` set to v1 and are +/// installable by Helm 3. +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub enum ChartSpecVersion { + /// Chart supports running on Helm 2 or 3. + V1, + + /// Chart supports running only on Helm 3. + #[default] + V2, +} + +/// Container that holds the mapping of source values to the parent key to be imported. +/// +/// Each item can be a child/parent sublist item or a string, the representation +/// in Rust is [`StringOrImportValue`]. +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct ImportValue { + /// The destination path in the parent chart's values. + pub parent: String, + + /// The source key of the values to be imported + pub child: String, +} + +/// Discriminated enumeration that can either be a [`String`] or a [`ImportValue`] as +/// the import source for referencing parent key items to be imported. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(untagged)] +pub enum StringOrImportValue { + /// String that points to a key to be imported. + String(String), + + /// Parent/child sublist item. + ImportValue(ImportValue), +} + +/// In Helm, one chart may depend on any number of other charts. +/// +/// These dependencies can be dynamically linked using the dependencies' field in `Chart.yaml` or brought in to +/// the `charts/` directory and managed manually. The charts required by the current chart are defined as a list +/// in the `dependencies` field. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct ChartDependency { + /// The name of the chart + pub name: String, + + /// The version of the chart. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub version: Option, + + /// Repository URL or alias that should be used to grab + /// the dependency from. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository: Option, + + /// YAML path that resolves to a boolean to enable or disable charts + /// dynamically. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub condition: Option, + + /// List of tags that can be used to group charts to enable/disable together. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub tags: Vec, + + /// [`ImportValues`][ImportValue] holds the mapping of source values to parent key to be imported. + /// Each item can be a string or pair of child/parent sublist items. + #[serde(default, rename = "import-values", skip_serializing_if = "Vec::is_empty")] + pub import_values: Vec, + + /// Alias that is used to identify a chart. Useful for pointing to the + /// same chart multiple times + #[serde(default, skip_serializing_if = "Option::is_none")] + pub alias: Option, +} + +/// Name and URL/email address combination as a maintainer. +/// +/// The maintainer's name can be a [`ULID`][charted_types::Ulid] or a [`Name`][charted_types::name::Name] +/// and [Hoshi](https://charts.noelware.org/docs/hoshi/latest) can use the information +/// to query the user from the API server and show a "Maintainers" list in the UI. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct ChartMaintainer { + /// The maintainer's name + pub name: String, + + /// The maintainer's email + #[serde(default, skip_serializing_if = "Option::is_none")] + pub email: Option, + + /// URL for the maintainer + #[serde(default, skip_serializing_if = "Option::is_none")] + pub url: Option, +} + +/// Skeleton schema of a `Chart.yaml` file. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct Chart { + /// The `apiVersion` field should be v2 for Helm charts that require at least Helm 3. Charts supporting previous + /// Helm versions have an apiVersion set to v1 and are still installable by Helm 3. + pub api_version: ChartSpecVersion, + + /// The name of the chart. + pub name: String, + + /// A SemVer 2 conformant version string of the chart. + pub version: Version, + + /// The optional `kubeVersion` field can define SemVer constraints on supported Kubernetes versions. + /// Helm will validate the version constraints when installing the chart and fail if the + /// cluster runs an unsupported Kubernetes version. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub kube_version: Option, + + /// A single-sentence description of this project + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + + /// The type of the chart. + #[serde(rename = "type", default)] + pub type_: ChartType, + + /// A list of keywords about this project. These keywords can be searched + /// via the /search endpoint if it's enabled. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub keywords: Vec, + + /// The URL of this project's homepage. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub home: Option, + + /// A list of URLs to the source code for this project + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub sources: Vec, + + /// In Helm, one chart may depend on any number of other charts. + /// + /// These dependencies can be dynamically linked using the dependencies' field in `Chart.yaml` + /// or brought in to the `charts/` directory and managed manually. The charts required by the current chart + /// are defined as a list in the dependencies field. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub dependencies: Vec, + + /// A list of name and URL/email address combinations for the maintainer(s) + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub maintainers: Vec, + + /// A URL or an SVG or PNG image to be used as an icon + #[serde(default, skip_serializing_if = "Option::is_none")] + pub icon: Option, + + /// Note that the `appVersion` field is not related to the `version` field. + /// + /// It is a way of specifying the version of the application. For example, the `drupal` chart may have an + /// `appVersion: "8.2.1"`, indicating that the version of Drupal included in the chart (by default) is `8.2.1`. + /// This field is informational, and has no impact on chart version calculations. + /// + /// Wrapping the version in quotes is highly recommended. It forces the YAML parser to treat the version number as a string. + /// Leaving it unquoted can lead to parsing issues in some cases. For example, YAML interprets 1.0 as a floating point value, + /// and a git commit SHA like 1234e10 as scientific notation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub app_version: Option, + + /// When managing charts in a Chart Repository, it is sometimes necessary to deprecate a chart. + /// + /// The optional `deprecated` field in Chart.yaml can be used to mark a chart as deprecated. If the latest version + /// of a chart in the repository is marked as deprecated, then the chart as a whole is considered to be deprecated. + /// + /// The chart name can be later reused by publishing a newer version that is not marked as deprecated. + #[serde(default)] + pub deprecated: bool, + + /// Mapping of custom metadata that can be used for custom attributes. + /// + /// ## `charted-server` specific notes + /// Some attributes marked with the `charts.noelware.org/` prefix are recognized + /// by [Hoshi], a web UI for `charted-server`. + /// + /// [Hoshi]: https://charts.noelware.org/docs/hoshi/latest + /// + /// ### Non Exhaustive Attributes + /// #### `charts.noelware.org/license` + /// A **SPDX** identified string of the license of the chart. + /// + /// #### `charts.noelware.org/images` + /// A list of Docker images that the chart uses. + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub annotations: HashMap, +} + +/// Specification of the `index.yaml` file used for Helm chart repositories. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct ChartIndexSpec { + /// The Chart specification itself, this will be flatten when (de)serializing. + #[serde(flatten)] + pub spec: Chart, + + // not documented in Helm source code, so I can't really + // add documentation here. + // + // https://github.com/helm/helm/blob/764557c470533fa57aad99f865c9ff75a64d4163/pkg/repo/index.go#L270-L273 + #[serde(default)] + pub urls: Vec, + + #[serde(default)] + pub created: Option, + + #[serde(default)] + pub removed: bool, + + #[serde(default)] + pub digest: Option, +} + +/// Schema skeleton for a `index.yaml` file, that represents a [`Chart`] index. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct ChartIndex { + /// API version for the schema itself, will always be `v1`. + pub api_version: String, + + /// [`DateTime`] constant on when the chart index was generated at, this will not + /// be modified at all. + pub generated: DateTime, + + /// Map of [`ChartIndexSpec`]s for the Helm charts that Helm uses to install a Helm chart. + pub entries: HashMap>, +} + +impl Default for ChartIndex { + fn default() -> ChartIndex { + ChartIndex { + api_version: "v1".into(), + generated: Utc::now().into(), + entries: HashMap::default(), + } + } +} diff --git a/crates/database/diesel.postgresql.toml b/crates/metrics/Cargo.toml similarity index 74% rename from crates/database/diesel.postgresql.toml rename to crates/metrics/Cargo.toml index 68436d32a..243625f20 100644 --- a/crates/database/diesel.postgresql.toml +++ b/crates/metrics/Cargo.toml @@ -13,10 +13,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -[print_schema] -file = "src/schema/postgresql.rs" -generate_missing_sql_type_definitions = false -import_types = ["diesel::sql_types::*", "crate::schema::sql_types::*"] - -[migrations_directory] -dir = "./migrations/postgresql" +[package] +name = "charted-metrics" +version.workspace = true +documentation.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +publish.workspace = true +repository.workspace = true +authors.workspace = true diff --git a/crates/metrics/src/lib.rs b/crates/metrics/src/lib.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/metrics/src/opentelemetry.rs b/crates/metrics/src/opentelemetry.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/metrics/src/prometheus.rs b/crates/metrics/src/prometheus.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 9a0504d91..ebf9a93f5 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -25,60 +25,25 @@ repository.workspace = true authors.workspace = true [dependencies] -azalia = { workspace = true, features = [ - "remi", - "remi-fs", - "remi-s3", - "remi-azure", - "remi-tracing", - "remi-serde", -] } -argon2.workspace = true axum = { workspace = true, features = ["matched-path"] } -axum-server = { version = "0.7.1", features = ["tls-rustls"] } +axum-extra = { version = "0.10.0", features = ["typed-header"] } +axum-server = { version = "0.7.1", features = ["tls-rustls", "tokio-rustls"] } base64 = "0.22.1" -charted-app = { version = "0.1.0", path = "../app" } -charted-authz = { version = "0.1.0", path = "../authz" } -charted-authz-local = { version = "0.1.0", path = "../authz/local" } -charted-core = { version = "0.1.0", path = "../core" } -charted-config = { version = "0.1.0", path = "../config" } -charted-database = { version = "0.1.0", path = "../database" } -charted-features = { version = "0.1.0", path = "../features", features = [ - "extends-db", - "extends-openapi", -] } -charted-helm-charts = { version = "0.1.0", path = "../helm-charts" } -charted-types = { version = "0.1.0", path = "../types" } -chrono.workspace = true -derive_more = { version = "1.0.0", features = ["deref"] } -diesel = { workspace = true, features = ["postgres", "sqlite"] } +charted-authz.workspace = true +charted-authz-local.workspace = true +charted-config.workspace = true +charted-core = { workspace = true, features = ["openapi", "axum"] } +charted-database.workspace = true +charted-helm-charts.workspace = true +charted-helm-types = { workspace = true, features = ["openapi"] } +charted-types = { workspace = true, features = ["openapi"] } +derive_more = { workspace = true, features = ["display", "from", "error"] } eyre.workspace = true -jsonwebtoken = "9.3.0" +jsonwebtoken = "9.3.1" mime = "0.3.17" -multer.workspace = true +sea-orm.workspace = true sentry.workspace = true -sentry-eyre = "0.2.0" -sentry-tower = { version = "0.36.0", features = ["axum", "http"] } serde.workspace = true serde_json.workspace = true serde_path_to_error = "0.1.16" -serde_yaml_ng.workspace = true -tokio = { workspace = true, features = ["signal", "net"] } -tower = "0.5.0" -tower-http = { version = "0.6.0", features = [ - "auth", - "catch-panic", - "compression-gzip", - "cors", -] } tracing.workspace = true -utoipa = { workspace = true, features = ["debug"] } -validator = "0.20.0" - -[dev-dependencies] -charted-testkit.workspace = true -tempfile.workspace = true -tokio = { workspace = true, features = ["rt", "macros"] } - -[package.metadata.cargo-machete] -ignored = ["charted-helm-charts"] diff --git a/crates/server/src/extract/mod.rs b/crates/server/src/extract.rs similarity index 97% rename from crates/server/src/extract/mod.rs rename to crates/server/src/extract.rs index 906fa3456..0f84b8062 100644 --- a/crates/server/src/extract/mod.rs +++ b/crates/server/src/extract.rs @@ -14,10 +14,11 @@ // limitations under the License. mod json; -pub use json::*; - +mod di; mod path; -pub use path::*; - mod query; + +pub use json::*; +pub use di::*; +pub use path::*; pub use query::*; diff --git a/crates/server/src/extract/di.rs b/crates/server/src/extract/di.rs new file mode 100644 index 000000000..1c9771d62 --- /dev/null +++ b/crates/server/src/extract/di.rs @@ -0,0 +1,56 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use axum::extract::{FromRequest, Request}; +use charted_core::api; +use std::any::{type_name, Any}; + +#[derive(Debug, derive_more::Display)] +#[display("api server is not properly set-up")] +struct ServerNotProperlySetup; +impl std::error::Error for ServerNotProperlySetup {} + +#[derive(Debug, derive_more::Display)] +#[display("unknown dependency of type ({}) was not found", type_name)] +struct UnknownDependency { + type_name: &'static str, +} + +impl std::error::Error for UnknownDependency {} + +/// `Dep` is an Axum extractor that implements [`FromRequest`] to get +/// a dependency from a [`di::Container`][charted_core::di::Container]. +#[derive(Debug, Clone)] +pub struct Dep(pub T); +impl FromRequest for Dep { + type Rejection = api::Response; + + async fn from_request(_: Request, _: &S) -> Result { + let Some(container) = &charted_core::CONTAINER.get() else { + return Err(api::system_failure(ServerNotProperlySetup)); + }; + + match container.get::() { + Ok(value) => Ok(Dep(value.to_owned())), + Err(charted_core::di::Error::FailedCast) | Err(charted_core::di::Error::ObjectUnavaliable) => { + Err(api::system_failure(UnknownDependency { + type_name: type_name::(), + })) + } + + _ => unreachable!(), + } + } +} diff --git a/crates/server/src/extract/json.rs b/crates/server/src/extract/json.rs index 22bf93510..d9c50379b 100644 --- a/crates/server/src/extract/json.rs +++ b/crates/server/src/extract/json.rs @@ -24,14 +24,11 @@ use serde_json::{error::Category, json}; use std::borrow::Cow; use tracing::error; -charted_core::create_newtype_wrapper! { - /// `Json` is a Axum extractor that implements [`FromRequest`] to transform - /// `T` into a deserialized struct from a JSON payload via the [`serde_json`] - /// crate. - #[derive(Debug, Clone)] - pub Json for pub T; -} - +/// `Json` is a Axum extractor that implements [`FromRequest`] to transform +/// `T` into a deserialized struct from a JSON payload via the [`serde_json`] +/// crate. +#[derive(Debug, Clone)] +pub struct Json(pub T); impl FromRequest for Json { type Rejection = api::Response; @@ -116,7 +113,7 @@ impl FromRequest for Json { StatusCode::BAD_REQUEST, ( ErrorCode::ReachedUnexpectedEof, - "reached an unexpected 'end of file' marker -- this is a bug, please report this!", + "reached an unexpected 'end of file' marker; this is a bug, please report this!", json!({ "report_url": "https://github.com/charted-dev/charted/issues/new", "path": path @@ -128,7 +125,7 @@ impl FromRequest for Json { StatusCode::INTERNAL_SERVER_ERROR, ( ErrorCode::Io, - "input/output error reached -- this is a bug, please report this!", + "input/output error reached; this is a bug, please report this!", json!({ "report_url": "https://github.com/charted-dev/charted/issues/new", "path": path diff --git a/crates/server/src/extract/path.rs b/crates/server/src/extract/path.rs index 08192b099..ff5d85de8 100644 --- a/crates/server/src/extract/path.rs +++ b/crates/server/src/extract/path.rs @@ -21,12 +21,9 @@ use charted_core::api; use serde::de::DeserializeOwned; use serde_json::json; -charted_core::create_newtype_wrapper! { - /// `Path` is a newtype wrapper for [`axum::extract::Path`], in which it uses - /// [`api::Response`] to transmit errors properly. - pub Path for pub T; -} - +/// `Path` is a newtype wrapper for [`axum::extract::Path`], in which it uses +/// [`api::Response`] to transmit errors properly. +pub struct Path(pub T); impl FromRequestParts for Path { type Rejection = api::Response; diff --git a/crates/server/src/extract/query.rs b/crates/server/src/extract/query.rs index 4871a9f19..4d804de50 100644 --- a/crates/server/src/extract/query.rs +++ b/crates/server/src/extract/query.rs @@ -20,12 +20,9 @@ use axum::{ use charted_core::api; use serde::de::DeserializeOwned; -charted_core::create_newtype_wrapper! { - /// `Query` is a newtype wrapper for [`axum::extract::Query`], in which it uses - /// [`api::Response`] to transmit errors properly. - pub Query for pub T; -} - +/// `Query` is a newtype wrapper for [`axum::extract::Query`], in which it uses +/// [`api::Response`] to transmit errors properly. +pub struct Query(pub T); impl FromRequestParts for Query { type Rejection = api::Response; diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index ebfc29e4d..73ef32006 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -13,146 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![feature(never_type, decl_macro, let_chains)] - -mod state; -pub use state::*; - -mod types; -pub use types::*; - -#[macro_use] -pub mod macros; - pub mod extract; pub mod middleware; pub mod multipart; pub mod openapi; -pub mod ops; -pub mod responses; pub mod routing; -#[cfg(test)] -pub mod test; - -use argon2::{ - password_hash::{rand_core::OsRng, SaltString}, - PasswordHasher, -}; -use axum::Router; -use axum_server::{tls_rustls::RustlsConfig, Handle}; -use charted_core::ARGON2; -use eyre::Context; -use std::time::Duration; -use tracing::{error, info, info_span, warn, Instrument}; - -pub fn hash_password>(password: P) -> eyre::Result { - let salt = SaltString::generate(&mut OsRng); - let password = password.as_ref(); - - ARGON2 - .hash_password(password, &salt) - .map(|hash| hash.to_string()) - .inspect_err(|e| { - error!(error = %e, "failed to compute argon2 password"); - }) - // since `argon2::Error` doesn't implement `std::error::Error`, - // we implicitlly pass it into the `eyre!` macro, which will create - // an adhoc error. - .map_err(|e| eyre::eyre!(e)) -} - -pub async fn start(cx: charted_app::Context) -> eyre::Result<()> { - info!("starting API server..."); - - #[allow(unused)] - let features = Vec::new(); - - let cx = ServerContext::new(cx, features); - for feature in &cx.features { - // Initialize the feature first - feature - .init(&cx) - .instrument(info_span!("charted.server.feature.init")) - .await?; - - // If it needs to extend the database, then we will - // allow it to extend it to have its own attributes. - feature - .extends_db(&cx.pool) - .instrument(info_span!("charted.server.feature.extends[database]")) - .await?; - } - - // Put a clone of `ServerContext` since we still need to access it. - set_global(cx.clone()); - - let server_config = cx.config.server.clone(); - let router: Router = self::routing::create_router(&cx).with_state(cx); - - match server_config.ssl { - Some(ref ssl) => start_as_https(&server_config, ssl, router).await, - None => start_as_http(&server_config, router).await, - } -} - -async fn start_as_https( - config: &charted_config::server::Config, - ssl: &charted_config::server::ssl::Config, - router: Router, -) -> eyre::Result<()> { - info!("starting HTTP service with TLS enabled"); - - let handle = Handle::new(); - tokio::spawn(shutdown_signal(Some(handle.clone()))); - - let addr = config.addr(); - let config = RustlsConfig::from_pem_file(&ssl.cert, &ssl.cert_key).await?; - - info!(address = %addr, "now listening on HTTPS"); - axum_server::bind_rustls(addr, config) - .handle(handle) - .serve(router.into_make_service()) - .await - .context("failed to run HTTPS service") -} - -async fn start_as_http(config: &charted_config::server::Config, router: Router) -> eyre::Result<()> { - info!("starting HTTP service with TLS disabled"); - - let addr = config.addr(); - let listener = tokio::net::TcpListener::bind(addr).await?; - - info!(address = %addr, "listening on HTTP"); - axum::serve(listener, router.into_make_service()) - .with_graceful_shutdown(shutdown_signal(None)) - .await - .context("failed to run HTTP service") -} - -async fn shutdown_signal(handle: Option) { - let ctrl_c = async { - tokio::signal::ctrl_c().await.expect("unable to install CTRL+C handler"); - }; - - #[cfg(unix)] - let terminate = async { - tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) - .expect("unable to install signal handler") - .recv() - .await; - }; - - #[cfg(not(unix))] - let terminate = std::future::pending::<()>(); - - tokio::select! { - _ = ctrl_c => {} - _ = terminate => {} - } +use std::sync::atomic::AtomicUsize; - warn!("received terminal signal! shutting down"); - if let Some(handle) = handle { - handle.graceful_shutdown(Some(Duration::from_secs(10))); - } -} +pub static REQUESTS: AtomicUsize = AtomicUsize::new(0); diff --git a/crates/server/src/macros.rs b/crates/server/src/macros.rs index 71c8bc0d7..6bffe91d7 100644 --- a/crates/server/src/macros.rs +++ b/crates/server/src/macros.rs @@ -12,53 +12,3 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - -pub macro impl_list_response($name:ident as $ref:literal) { - pub(crate) struct $name { - _priv: (), - } - - impl ::utoipa::PartialSchema for $name { - fn schema() -> ::utoipa::openapi::RefOr<::utoipa::openapi::Schema> { - let ::utoipa::openapi::RefOr::T(::utoipa::openapi::Schema::Object(mut object)) = - ::charted_core::api::Response::<()>::schema() - else { - ::core::unreachable!(); - }; - - let _ = object.properties.remove("data"); - object.properties.insert( - "data".into(), - ::utoipa::openapi::RefOr::T(::utoipa::openapi::Schema::Array( - ::utoipa::openapi::ArrayBuilder::new() - .items(::utoipa::openapi::RefOr::Ref(::utoipa::openapi::Ref::from_schema_name( - $ref, - ))) - .build(), - )), - ); - - ::utoipa::openapi::RefOr::T(::utoipa::openapi::Schema::Object(object)) - } - } - - impl ::utoipa::ToSchema for $name { - fn name() -> ::std::borrow::Cow<'static, str> { - ::std::borrow::Cow::Borrowed(::core::stringify!($name)) - } - } -} - -pub macro forward_schema_impl(for $ty:ty) { - impl ::utoipa::PartialSchema for $ty { - fn schema() -> ::utoipa::openapi::RefOr<::utoipa::openapi::Schema> { - ::charted_core::api::Response::<$ty>::schema() - } - } - - impl ::utoipa::ToSchema for $ty { - fn name() -> ::std::borrow::Cow<'static, str> { - ::std::borrow::Cow::Borrowed(stringify!($ty)) - } - } -} diff --git a/crates/server/src/middleware/mod.rs b/crates/server/src/middleware.rs similarity index 91% rename from crates/server/src/middleware/mod.rs rename to crates/server/src/middleware.rs index 2ecf5d7bf..109689b7e 100644 --- a/crates/server/src/middleware/mod.rs +++ b/crates/server/src/middleware.rs @@ -13,11 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod request_id; -pub use request_id::*; +pub mod sessions; -mod logging; -pub use logging::*; +mod log; +mod request_id; -pub mod permissions; -pub mod session; +pub use log::*; +pub use request_id::*; diff --git a/crates/server/src/middleware/logging.rs b/crates/server/src/middleware/log.rs similarity index 91% rename from crates/server/src/middleware/logging.rs rename to crates/server/src/middleware/log.rs index d24a5650b..15492eaa1 100644 --- a/crates/server/src/middleware/logging.rs +++ b/crates/server/src/middleware/log.rs @@ -13,11 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::REQUESTS; + use super::XRequestId; -use crate::ServerContext; use axum::{ body::Body, - extract::{FromRequestParts, MatchedPath, Request, State}, + extract::{FromRequestParts, MatchedPath, Request}, http::{header::USER_AGENT, Extensions, HeaderMap, Method, Uri, Version}, middleware::Next, response::IntoResponse, @@ -43,18 +44,13 @@ pub struct Metadata { http.method = metadata.method.as_str(), http.uri = metadata.uri.path(), ))] -pub async fn log( - metadata: Metadata, - State(cx): State, - req: Request, - next: Next, -) -> impl IntoResponse { +pub async fn log(metadata: Metadata, req: Request, next: Next) -> impl IntoResponse { let uri = metadata.uri.path(); if uri.contains("/heartbeat") { return next.run(req).await; } - cx.requests.fetch_add(1, Ordering::SeqCst); + REQUESTS.fetch_add(1, Ordering::SeqCst); let start = Instant::now(); info!("processing request"); diff --git a/crates/server/src/middleware/permissions/mod.rs b/crates/server/src/middleware/permissions/mod.rs deleted file mode 100644 index 34bd784d2..000000000 --- a/crates/server/src/middleware/permissions/mod.rs +++ /dev/null @@ -1,91 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![allow(unused)] - -use crate::ServerContext; -use axum::{ - body::Body, - http::{Request, Response}, -}; -use charted_core::bitflags::{MemberPermission, MemberPermissions}; -use charted_types::{Organization, Repository, User}; -use tracing::instrument; - -/// Which database table we should use? -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Table { - /// `repository_members` - Repository, - - /// `organization_members` - Organization, -} - -/// Middleware to check a repository or organization member's permission. -#[derive(Clone)] -pub struct Middleware { - require_permissions: MemberPermissions, - table: Table, -} - -impl Middleware { - pub fn new(table: Table) -> Middleware { - Middleware { - require_permissions: MemberPermissions::new(0), - table, - } - } - - pub fn permissions>(self, permissions: I) -> Self { - let mut bitfield = self.require_permissions; - bitfield.add(permissions); - - Self { - require_permissions: bitfield, - ..self - } - } - - #[instrument( - name = "charted.server.permissionCheck.organization", - skip_all, - fields(%user.id, %user.username, %organization.name, %organization.id) - )] - async fn perform_organization_member_permission_check( - self, - req: Request, - ctx: &ServerContext, - organization: Organization, - user: User, - ) -> Result, Response> { - todo!() - } - - #[instrument( - name = "charted.server.permissionCheck.repository", - skip_all, - fields(%user.id, %user.username, %repository.name, %repository.id) - )] - async fn perform_repository_member_permission_check( - self, - req: Request, - ctx: &ServerContext, - repository: Repository, - user: User, - ) -> Result, Response> { - todo!() - } -} diff --git a/crates/server/src/middleware/session/error.rs b/crates/server/src/middleware/session/error.rs deleted file mode 100644 index 54286d234..000000000 --- a/crates/server/src/middleware/session/error.rs +++ /dev/null @@ -1,203 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::ServerContext; -use axum::{http::StatusCode, response::IntoResponse}; -use charted_core::api; -use std::{borrow::Cow, fmt::Display}; - -#[derive(Debug)] -pub enum Error { - /// An unknown authentication type was received. - UnknownAuthenticationType(Cow<'static, str>), - - /// An error occurred while decoding base64 data from user input. - DecodeBase64(base64::DecodeError), - - /// Generic message that will be put into the `message` field of an API error. - Message(Cow<'static, str>), - - /// Something in the database failed. - Database(diesel::result::Error), - - /// Failed to decode from a ULID. - DecodeUlid(charted_types::ulid::DecodeError), - - /// An error occured during JWT validation. - Jwt(jsonwebtoken::errors::Error), - - /// An unknown error that hasn't been handled yet. It is most likely - /// wrapped in a [`eyre::Report`]. - Unknown(eyre::Report), - - /// The request missed the `Authorization` HTTP header. - MissingAuthorizationHeader, - - /// The token received was not the correct refresh token. - RefreshTokenRequired, - - /// The password given from the basic authentication scheme was invalid. - InvalidPassword, - - /// Session queried was unknown to the server. - UnknownSession, -} - -impl Error { - pub(crate) fn invalid_utf8() -> Self { - Error::msg("received invalid utf-8 content") - } - - pub(crate) fn msg>>(msg: I) -> Self { - Error::Message(msg.into()) - } - - pub fn status_code(&self) -> StatusCode { - use Error as E; - - match self { - E::MissingAuthorizationHeader - | E::UnknownAuthenticationType(_) - | E::Message(_) - | E::DecodeBase64(_) - | E::RefreshTokenRequired - | E::DecodeUlid(_) => StatusCode::NOT_ACCEPTABLE, - - E::InvalidPassword => StatusCode::UNAUTHORIZED, - E::UnknownSession => StatusCode::NOT_FOUND, - E::Database(_) | E::Unknown(_) => StatusCode::INTERNAL_SERVER_ERROR, - E::Jwt(err) => match err.kind() { - jsonwebtoken::errors::ErrorKind::InvalidToken => StatusCode::FORBIDDEN, - jsonwebtoken::errors::ErrorKind::ExpiredSignature => StatusCode::UNAUTHORIZED, - _ => StatusCode::INTERNAL_SERVER_ERROR, - }, - } - } - - fn api_error_code(&self) -> api::ErrorCode { - use api::ErrorCode::*; - use Error as E; - - match self { - E::RefreshTokenRequired => RefreshTokenRequired, - E::InvalidPassword => InvalidPassword, - E::UnknownSession => UnknownSession, - E::MissingAuthorizationHeader => MissingAuthorizationHeader, - E::Message(_) => InvalidAuthorizationParts, - E::UnknownAuthenticationType(_) => InvalidAuthenticationType, - E::DecodeBase64(_) => UnableToDecodeBase64, - E::DecodeUlid(_) => UnableToDecodeUlid, - E::Database(_) | E::Unknown(_) => InternalServerError, - E::Jwt(err) => match err.kind() { - jsonwebtoken::errors::ErrorKind::ExpiredSignature => SessionExpired, - jsonwebtoken::errors::ErrorKind::InvalidToken => InvalidSessionToken, - _ => InternalServerError, - }, - } - } -} - -impl IntoResponse for Error { - fn into_response(self) -> axum::response::Response { - >::from(self).into_response() - } -} - -impl From for api::Response { - fn from(value: Error) -> Self { - api::err(value.status_code(), (value.api_error_code(), value.to_string())) - } -} - -impl Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use Error as E; - - match self { - E::UnknownAuthenticationType(ty) => { - let cx = ServerContext::get(); - - #[allow(clippy::obfuscated_if_else)] - let schemes = cx - .config - .sessions - .enable_basic_auth - .then_some("[Bearer, Basic, ApiKey]") - .unwrap_or("[Bearer, ApiKey]"); - - write!( - f, - "received invalid authorization type [{ty}]: expected oneof {schemes}" - ) - } - - E::MissingAuthorizationHeader => f.write_str("missing `Authorization` header from request"), - E::RefreshTokenRequired => f.write_str("endpoint expected a valid refresh token"), - E::InvalidPassword => f.write_str("received invalid password"), - E::UnknownSession => f.write_str("unknown session"), - E::Message(msg) => f.write_str(msg), - E::DecodeBase64(err) => Display::fmt(err, f), - E::DecodeUlid(err) => Display::fmt(err, f), - E::Database(_) => f.write_str("database error: please report this if this is a common occurrence"), - E::Unknown(e) => write!( - f, - "unknown error occurred, report this if this is a common occurrence: {e}" - ), - - E::Jwt(err) => Display::fmt(err, f), - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::DecodeBase64(err) => Some(err), - Error::Jwt(err) => Some(err), - - _ => None, - } - } -} - -impl From for Error { - fn from(value: base64::DecodeError) -> Self { - Self::DecodeBase64(value) - } -} - -impl From for Error { - fn from(value: jsonwebtoken::errors::Error) -> Self { - Self::Jwt(value) - } -} - -impl From for Error { - fn from(value: diesel::result::Error) -> Self { - Self::Database(value) - } -} - -impl From for Error { - fn from(value: charted_types::ulid::DecodeError) -> Self { - Self::DecodeUlid(value) - } -} - -impl From for Error { - fn from(value: eyre::Report) -> Self { - Self::Unknown(value) - } -} diff --git a/crates/server/src/middleware/session/mod.rs b/crates/server/src/middleware/session/mod.rs deleted file mode 100644 index dc69e0979..000000000 --- a/crates/server/src/middleware/session/mod.rs +++ /dev/null @@ -1,470 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -mod error; -pub use error::*; - -mod extract; -pub use extract::*; - -use crate::{ops, ServerContext}; -use axum::{ - body::Body, - extract::Request, - http::{header::AUTHORIZATION, Response, StatusCode}, - response::IntoResponse, -}; -use base64::{engine::general_purpose::STANDARD, Engine}; -use charted_authz::InvalidPassword; -use charted_core::{ - api::{self, internal_server_error}, - bitflags::{ApiKeyScope, ApiKeyScopes}, - BoxedFuture, -}; -use charted_database::{ - connection, - schema::{postgresql, sqlite}, -}; -use charted_types::{name::Name, Ulid}; -use diesel::sqlite::Sqlite; -use jsonwebtoken::{DecodingKey, Validation}; -use serde_json::{json, Value}; -use std::{borrow::Cow, collections::HashMap, str::FromStr}; -use tower_http::auth::AsyncAuthorizeRequest; -use tracing::{error, instrument, trace}; - -pub const JWT_ISS: &str = "Noelware"; -pub const JWT_AUS: &str = "charted-server"; - -#[derive(Clone, Default)] -pub struct Middleware { - allow_unauthorized_requests: bool, - refresh_token_required: bool, - scopes: ApiKeyScopes, -} - -impl Middleware { - pub fn allow_unauthorized_requests(self, yes: bool) -> Self { - Self { - allow_unauthorized_requests: yes, - ..self - } - } - - pub fn refresh_token_required(self, yes: bool) -> Self { - Self { - refresh_token_required: yes, - ..self - } - } - - pub fn scopes>(self, scopes: I) -> Self { - let mut bitfield = self.scopes; - bitfield.add(scopes); - - Self { - scopes: bitfield, - ..self - } - } -} - -impl Middleware { - /// Performs basic authentication if the server has enabled it. - #[instrument( - name = "charted.server.authz.basic", - skip_all, - fields( - req.uri = req.uri().path(), - req.method = req.method().as_str() - ) - )] - async fn basic_auth( - self, - mut req: Request, - ctx: &ServerContext, - token: String, - ) -> Result, Response> { - if self.refresh_token_required { - return Err(api::err( - StatusCode::NOT_ACCEPTABLE, - ( - api::ErrorCode::RefreshTokenRequired, - "cannot use basic authentication scheme on this route", - ), - ) - .into_response()); - } - - let decoded = String::from_utf8( - STANDARD - .decode(&token) - .inspect_err(|e| { - error!(error = %e, "failed to decode base64 from authorization header"); - sentry::capture_error(e); - }) - .map_err(Error::DecodeBase64) - .map_err(IntoResponse::into_response)?, - ) - .map_err(|_| Error::invalid_utf8()) - .map_err(IntoResponse::into_response)?; - - let (username, password) = match decoded.split_once(':') { - Some((_, pass)) if pass.contains(':') => { - let idx = pass.chars().position(|c| c == ':').unwrap_or_default(); - return Err(Error::msg(format!("received more than one ':' @ pos {idx}")).into_response()); - } - - Some(tuple) => tuple, - None => { - return Err( - Error::msg("basic authentication requires the syntax of 'username:password'").into_response(), - ) - } - }; - - let user = username - .parse::() - .map_err(|e| Error::msg(e.to_string()).into_response())?; - - let Some(user) = ops::db::user::get(ctx, user.clone()) - .await - .map_err(Error::Unknown) - .map_err(IntoResponse::into_response)? - else { - return Err(api::err( - StatusCode::NOT_FOUND, - ( - api::ErrorCode::EntityNotFound, - "user with id doesn't exist", - json!({"username":user}), - ), - ) - .into_response()); - }; - - match ctx.authz.authenticate(&user, password.to_owned()).await { - Ok(()) => { - req.extensions_mut().insert(extract::Session { session: None, user }); - Ok(req) - } - - Err(e) => { - if e.downcast_ref::().is_some() { - return Err(Error::InvalidPassword.into_response()); - } - - error!(error = %e, "failed to authenticate from authz backend"); - sentry::capture_error(&*e); - - Err(internal_server_error().into_response()) - } - } - } - - /// Performs JWT-based authentication for `Bearer` tokens. - #[instrument(name = "charted.server.authz.bearer", skip_all)] - async fn bearer_auth( - self, - mut req: Request, - ctx: &ServerContext, - token: String, - ) -> Result, Response> { - let key = DecodingKey::from_secret(ctx.config.jwt_secret_key.as_ref()); - let decoded = jsonwebtoken::decode::>( - &token, - &key, - &Validation::new(jsonwebtoken::Algorithm::HS512), - ) - .inspect_err(|e| { - error!(error = %e, "failed to decode JWT token"); - sentry::capture_error(e); - }) - .map_err(Error::Jwt) - .map_err(IntoResponse::into_response)?; - - // All JWT tokens created by the server will always have a `user_id` which - // will always be a valid ULID. - let uid = decoded - .claims - .get("user_id") - .filter(|x| matches!(x, Value::String(_))) - .and_then(Value::as_str) - .map(Ulid::new) - .ok_or_else(|| Error::msg("missing `user_id` JWT claim").into_response())? - .map_err(Error::DecodeUlid) - .map_err(IntoResponse::into_response)?; - - let session = decoded - .claims - .get("session_id") - .filter(|x| matches!(x, Value::String(_))) - .and_then(Value::as_str) - .map(Ulid::new) - .ok_or_else(|| Error::msg("missing `session_id` JWT claim").into_response())? - .map_err(Error::DecodeUlid) - .map_err(IntoResponse::into_response)?; - - let mut conn = ctx - .pool - .get() - .inspect_err(|e| { - error!(error = %e, "failed to get database connection"); - sentry::capture_error(e); - }) - .map_err(|_| api::internal_server_error().into_response())?; - - let session = connection!(@raw conn { - PostgreSQL(conn) => conn.build_transaction().read_only().run::(|txn| { - use postgresql::sessions::{dsl::*, table}; - use diesel::pg::Pg; - - table - .select(>::as_select()) - .filter(owner.eq(uid)) - .filter(id.eq(&session)) - .first(txn) - .map_err(Into::into) - }); - - SQLite(conn) => conn.immediate_transaction(|txn| { - use sqlite::sessions::{dsl::*, table}; - - table - .select(>::as_select()) - .filter(owner.eq(uid)) - .filter(id.eq(&session)) - .first(txn) - .map_err(Into::into) - }); - }) - .map_err(|e| match e { - Error::Database(diesel::result::Error::NotFound) => api::err( - StatusCode::NOT_FOUND, - ( - api::ErrorCode::EntityNotFound, - "session with id doesn't exist", - json!({"session":session}), - ), - ) - .into_response(), - - err => err.into_response(), - })?; - - if session.owner != uid { - error!("FATAL: assertion of `session.owner` == {uid} failed"); - return Err(Error::UnknownSession.into_response()); - } - - if self.refresh_token_required && session.refresh_token != token { - return Err(Error::RefreshTokenRequired.into_response()); - } - - let Some(user) = ops::db::user::get(ctx, uid) - .await - .map_err(Error::Unknown) - .map_err(IntoResponse::into_response)? - else { - return Err(api::err( - StatusCode::NOT_FOUND, - ( - api::ErrorCode::EntityNotFound, - "user with id doesn't exist", - json!({"user":uid}), - ), - ) - .into_response()); - }; - - req.extensions_mut().insert(Session { - session: Some(session), - user, - }); - - Ok(req) - } - - /// Performs API Key-based authentication - #[instrument(name = "charted.server.authz.apikey", skip_all)] - async fn apikey_auth( - self, - mut req: Request, - ctx: &ServerContext, - token: String, - ) -> Result, Response> { - if self.refresh_token_required { - return Err(api::err( - StatusCode::NOT_ACCEPTABLE, - ( - api::ErrorCode::RefreshTokenRequired, - "cannot use api key authentication on a bearer-only route", - ), - ) - .into_response()); - } - - let Some(apikey) = ops::db::apikey::get(ctx, token, None::) - .await - .map_err(Error::Unknown) - .map_err(IntoResponse::into_response)? - else { - return Err(api::err( - StatusCode::NOT_FOUND, - ( - api::ErrorCode::EntityNotFound, - "api key with received token was not found", - ), - ) - .into_response()); - }; - - let scopes = apikey.bitfield(); - for (scope, bit) in self.scopes.flags() { - trace!(%apikey.name, "checking if api key has scope [{scope}] enabled"); - if !scopes.contains(bit) { - trace!(%apikey.name, %scope, "api key scope is not enabled"); - return Err(api::err( - StatusCode::FORBIDDEN, - ( - api::ErrorCode::AccessNotPermitted, - "api key doesn't have access to this route due to not enabling the required flag", - json!({"scope":scope,"$repr":bit}), - ), - ) - .into_response()); - } - } - - let Some(user) = ops::db::user::get(ctx, apikey.owner) - .await - .map_err(Error::Unknown) - .map_err(IntoResponse::into_response)? - else { - return Err(api::err( - StatusCode::NOT_FOUND, - ( - api::ErrorCode::EntityNotFound, - "user with id doesn't exist", - json!({"user":apikey.owner}), - ), - ) - .into_response()); - }; - - req.extensions_mut().insert(extract::Session { session: None, user }); - Ok(req) - } -} - -impl AsyncAuthorizeRequest for Middleware { - type ResponseBody = Body; - type RequestBody = Body; - type Future = BoxedFuture<'static, Result, Response>>; - - fn authorize(&mut self, request: axum::http::Request) -> Self::Future { - let ctx = ServerContext::get(); - let headers = request.headers(); - - let Some(header) = headers.get(AUTHORIZATION) else { - if self.allow_unauthorized_requests { - return Box::pin(noop(request)); - } - - return Box::pin(error(Error::MissingAuthorizationHeader)); - }; - - let Ok(value) = String::from_utf8(header.as_ref().to_vec()).inspect_err(|err| { - error!(error = %err, "failed to validate UTF-8 contents in header"); - sentry::capture_error(err); - }) else { - return Box::pin(error(Error::invalid_utf8())); - }; - - let (ty, value) = match value.split_once(' ') { - Some((_, value)) if value.contains(' ') => { - let space = value.chars().position(|x| x == ' ').unwrap_or_default(); - return Box::pin(error(Error::msg(format!( - "received extra space at {space} when parsing header" - )))); - } - - Some((ty, value)) => match ty.parse::() { - Ok(ty) => (ty, value), - Err(e) => return Box::pin(error(Error::UnknownAuthenticationType(Cow::Owned(e)))), - }, - - None => { - return Box::pin(error(Error::msg( - "auth header must be in the form of 'Type Value', i.e, 'ApiKey hjdjshdjs'", - ))) - } - }; - - match ty { - AuthType::Bearer => Box::pin(self.clone().bearer_auth(request, ctx, value.to_owned())), - AuthType::ApiKey => Box::pin(self.clone().apikey_auth(request, ctx, value.to_owned())), - AuthType::Basic => Box::pin(self.clone().basic_auth(request, ctx, value.to_owned())), - } - } -} - -async fn noop(request: Request) -> Result, Response> { - Ok(request) -} - -#[cold] -async fn error(error: Error) -> Result, Response> { - Err(error.into_response()) -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum AuthType { - /// `Bearer` authentication type, typically a JWT token that was *possibly* - /// created by the server. - Bearer, - - /// `ApiKey` authentication type, created from the API keys API. - ApiKey, - - /// `Basic` authentication type, if enabled; allows to send HTTP requests - /// with basic credentials (`user:pass` in b64). - Basic, -} - -impl AuthType { - /// Returns a slice of the avaliable [`AuthType`]s. If `basic` is false, then [`AuthType::Basic`] - /// will not be avaliable. - pub const fn values(basic: bool) -> &'static [AuthType] { - if basic { - &[AuthType::ApiKey, AuthType::Basic, AuthType::Bearer] - } else { - &[AuthType::ApiKey, AuthType::Bearer] - } - } -} - -impl FromStr for AuthType { - type Err = String; - - fn from_str(s: &str) -> Result { - match &*s.to_ascii_lowercase() { - "apikey" => Ok(Self::ApiKey), - "bearer" => Ok(Self::Bearer), - "basic" => Ok(Self::Basic), - _ => Err(s.to_owned()), - } - } -} diff --git a/crates/core/src/testkit/mod.rs b/crates/server/src/middleware/sessions.rs similarity index 91% rename from crates/core/src/testkit/mod.rs rename to crates/server/src/middleware/sessions.rs index 199a1bfe8..85b04f550 100644 --- a/crates/core/src/testkit/mod.rs +++ b/crates/server/src/middleware/sessions.rs @@ -13,5 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub use charted_testkit::*; -pub mod containers; +mod error; +mod extract; + +pub use error::*; +pub use extract::*; diff --git a/crates/server/src/middleware/sessions/error.rs b/crates/server/src/middleware/sessions/error.rs new file mode 100644 index 000000000..90197d426 --- /dev/null +++ b/crates/server/src/middleware/sessions/error.rs @@ -0,0 +1,113 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use axum::{http::StatusCode, response::IntoResponse}; +use charted_core::api; +use std::borrow::Cow; + +/// Error type when the session middleware is running and it failed. +#[derive(Debug, derive_more::Display, derive_more::From, derive_more::Error)] +pub enum Error { + #[display("unknown authentication type: {}", _0)] + #[from(ignore)] + UnknownAuthType(#[error(not(source))] Cow<'static, str>), + DecodeBase64(base64::DecodeError), + + #[display("{}", _0)] + Message(#[error(not(source))] Cow<'static, str>), + + DecodeUlid(charted_types::ulid::DecodeError), + Database(sea_orm::DbErr), + Unknown(eyre::Report), + Jwt(jsonwebtoken::errors::Error), + + #[display("request is missing the `Authorization` http header")] + MissingAuthorizationHeader, + + #[display("refresh token is required for this route")] + RefreshTokenRequired, + + #[display("invalid password given")] + InvalidPassword, + + #[display("unknown session with id")] + UnknownSession, +} + +impl Error { + pub(crate) fn invalid_utf8() -> Self { + Error::msg("invalid utf-8 content") + } + + pub(crate) fn msg>>(msg: M) -> Self { + Error::Message(msg.into()) + } + + pub fn status_code(&self) -> StatusCode { + use Error as E; + + match self { + E::MissingAuthorizationHeader + | E::UnknownAuthType(_) + | E::Message(_) + | E::DecodeBase64(_) + | E::RefreshTokenRequired + | E::DecodeUlid(_) => StatusCode::NOT_ACCEPTABLE, + + E::InvalidPassword => StatusCode::UNAUTHORIZED, + E::UnknownSession => StatusCode::NOT_FOUND, + E::Database(_) | E::Unknown(_) => StatusCode::INTERNAL_SERVER_ERROR, + E::Jwt(err) => match err.kind() { + jsonwebtoken::errors::ErrorKind::InvalidToken => StatusCode::FORBIDDEN, + jsonwebtoken::errors::ErrorKind::ExpiredSignature => StatusCode::UNAUTHORIZED, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }, + } + } + + fn api_error_code(&self) -> api::ErrorCode { + use api::ErrorCode::*; + use Error as E; + + match self { + E::RefreshTokenRequired => RefreshTokenRequired, + E::InvalidPassword => InvalidPassword, + E::UnknownSession => UnknownSession, + E::MissingAuthorizationHeader => MissingAuthorizationHeader, + E::Message(_) => InvalidAuthorizationParts, + E::UnknownAuthType(_) => InvalidAuthenticationType, + E::DecodeBase64(_) => UnableToDecodeBase64, + E::DecodeUlid(_) => UnableToDecodeUlid, + E::Database(_) | E::Unknown(_) => InternalServerError, + E::Jwt(err) => match err.kind() { + jsonwebtoken::errors::ErrorKind::ExpiredSignature => SessionExpired, + jsonwebtoken::errors::ErrorKind::InvalidToken => InvalidSessionToken, + _ => InternalServerError, + }, + } + } +} + +impl IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + >::from(self).into_response() + } +} + +impl From for api::Response { + fn from(value: Error) -> Self { + api::err(value.status_code(), (value.api_error_code(), value.to_string())) + } +} diff --git a/crates/server/src/middleware/session/extract.rs b/crates/server/src/middleware/sessions/extract.rs similarity index 92% rename from crates/server/src/middleware/session/extract.rs rename to crates/server/src/middleware/sessions/extract.rs index 9e9ed490c..5acd1bf76 100644 --- a/crates/server/src/middleware/session/extract.rs +++ b/crates/server/src/middleware/sessions/extract.rs @@ -18,7 +18,7 @@ use charted_types::User; /// `Session` is a Axum extractor avaliable when a route has its session middleware configured. #[derive(Debug, Clone)] pub struct Session { - /// Session data from Redis, if this is `Bearer`. Otherwise, `None` is returned. + /// Session data, if this is `Bearer`. Otherwise, `None` is returned. pub session: Option, /// User that is executing this route; always avaliable diff --git a/crates/server/src/multipart.rs b/crates/server/src/multipart.rs index 4614dafa5..6bffe91d7 100644 --- a/crates/server/src/multipart.rs +++ b/crates/server/src/multipart.rs @@ -12,217 +12,3 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - -use axum::{ - extract::{FromRequest, Request}, - http::{header, StatusCode}, - response::IntoResponse, - RequestExt, -}; -use charted_core::api; -use serde_json::json; -use std::{borrow::Cow, fmt::Display, ops::DerefMut}; - -charted_core::create_newtype_wrapper! { - /// `Multipart` is an Axum extractor that implements [`FromRequest`], so it has to be - /// the last parameter in a REST controller. - #[derive(Debug)] - pub Multipart for ::multer::Multipart<'static>; -} - -impl DerefMut for Multipart { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl FromRequest for Multipart { - type Rejection = Rejection; - - async fn from_request(req: Request, _: &S) -> Result { - let Some(ct) = req.headers().get(header::CONTENT_TYPE) else { - return Err(Rejection::NoContentTypeAvaliable); - }; - - let value = ct.to_str().map_err(|e| { - tracing::error!(error = %e, "received invalid utf-8 in multipart body"); - Rejection::InvalidUtf8ForBoundary - })?; - - let boundary = multer::parse_boundary(value).map_err(|e| { - tracing::error!(error = %e, "received invalid multipart body content"); - match e { - multer::Error::NoBoundary => Rejection::NoBoundary, - e => Rejection::Multer(e), - } - })?; - - let stream = req.with_limited_body().into_body(); - Ok(Self(multer::Multipart::new(stream.into_data_stream(), boundary))) - } -} - -///////////////////////// ERRORS ////////////////////////////////// - -#[derive(Debug)] -pub enum Rejection { - /// Error that occurred from the [`multer::Multipart`] instance. - Multer(multer::Error), - - /// The boundary given was an invalid UTF-8 encoded piece of data. - InvalidUtf8ForBoundary, - - /// No `Content-Type` header was given in the request. - NoContentTypeAvaliable, - - /// No multipart boundary was specified. - NoBoundary, -} - -impl Rejection { - fn error_code(&self) -> api::ErrorCode { - match self { - Rejection::NoContentTypeAvaliable => api::ErrorCode::InvalidContentType, - Rejection::NoBoundary => api::ErrorCode::InvalidMultipartBoundary, - Rejection::InvalidUtf8ForBoundary => api::ErrorCode::InvalidUtf8, - Rejection::Multer(err) => match err { - multer::Error::UnknownField { .. } => api::ErrorCode::UnknownMultipartField, - multer::Error::IncompleteFieldData { .. } => api::ErrorCode::IncompleteMultipartFieldData, - multer::Error::ReadHeaderFailed(_) => api::ErrorCode::ReadMultipartHeaderFailed, - multer::Error::DecodeContentType(_) => api::ErrorCode::DecodeMultipartContentTypeFailed, - multer::Error::NoBoundary => api::ErrorCode::MissingMultipartBoundary, - multer::Error::NoMultipart => api::ErrorCode::NoMultipartReceived, - multer::Error::IncompleteStream => api::ErrorCode::IncompleteMultipartStream, - multer::Error::DecodeHeaderName { .. } => api::ErrorCode::DecodeMultipartHeaderNameFailed, - multer::Error::StreamSizeExceeded { .. } => api::ErrorCode::StreamSizeExceeded, - multer::Error::FieldSizeExceeded { .. } => api::ErrorCode::MultipartFieldsSizeExceeded, - multer::Error::StreamReadFailed(_) => api::ErrorCode::MultipartStreamReadFailed, - - _ => unreachable!(), - }, - } - } - - fn err_message(&self) -> Cow<'static, str> { - match self { - Rejection::NoContentTypeAvaliable => Cow::Borrowed("missing `content-type` header"), - Rejection::InvalidUtf8ForBoundary => Cow::Borrowed("received invalid utf-8 in multipart boundary decoding"), - Rejection::NoBoundary => Cow::Borrowed("missing multipart boundary"), - Rejection::Multer(err) => match err { - multer::Error::UnknownField { .. } => Cow::Borrowed("received unknown field"), - multer::Error::IncompleteFieldData { .. } => Cow::Borrowed("received incomplete field data in request"), - multer::Error::ReadHeaderFailed(_) => Cow::Borrowed("was unable to read multipart header"), - multer::Error::NoBoundary => Cow::Borrowed("was missing a multipart boundary"), - multer::Error::NoMultipart => Cow::Borrowed("missing `multipart/form-data` contents"), - multer::Error::IncompleteStream => Cow::Borrowed("received incomplete stream, did it corrupt?"), - multer::Error::DecodeContentType(_) => { - Cow::Borrowed("was unable to decode `Content-Type` header for field") - } - multer::Error::DecodeHeaderName { .. } => Cow::Borrowed("decoding header name failed"), - multer::Error::DecodeHeaderValue { .. } => Cow::Borrowed("decoding header value failed"), - multer::Error::FieldSizeExceeded { .. } => Cow::Borrowed("exceeded field size capacity"), - multer::Error::StreamReadFailed(_) => Cow::Borrowed("reading stream had failed"), - - _ => unreachable!(), - }, - } - } - - fn expand_details(&self) -> Option { - match self { - Rejection::Multer(err) => match err { - multer::Error::UnknownField { field_name } => { - field_name.as_ref().map(|field| json!({ "field": field })) - } - - multer::Error::IncompleteFieldData { field_name } => { - field_name.as_ref().map(|field| json!({ "field": field })) - } - - multer::Error::ReadHeaderFailed(_) => None, - multer::Error::DecodeContentType(_) => None, - multer::Error::NoBoundary => None, - multer::Error::NoMultipart => None, - multer::Error::IncompleteStream => None, - multer::Error::DecodeHeaderName { name, .. } => Some(json!({ "header": name })), - multer::Error::StreamSizeExceeded { limit } => Some(json!({ "limit": limit })), - multer::Error::FieldSizeExceeded { limit, field_name } => field_name.as_ref().map(|field| { - json!({ - "field": field, - "limit": limit, - }) - }), - - multer::Error::StreamReadFailed(_) => None, - - _ => None, - }, - _ => None, - } - } -} - -impl Display for Rejection { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Rejection::Multer(err) => Display::fmt(err, f), - Rejection::NoContentTypeAvaliable => f.write_str("no `content-type` header was specified"), - Rejection::NoBoundary => f.write_str("received no multipart boundary"), - Rejection::InvalidUtf8ForBoundary => f.write_str("received invalid utf-8 in multipart boundary decoding"), - } - } -} - -impl std::error::Error for Rejection { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Rejection::Multer(err) => Some(err), - _ => None, - } - } -} - -impl IntoResponse for Rejection { - fn into_response(self) -> axum::response::Response { - let status = match self { - Rejection::Multer(ref err) => multer_into_status_code(err), - _ => StatusCode::BAD_REQUEST, - }; - - let error = api::Error { - code: self.error_code(), - message: self.err_message(), - details: self.expand_details(), - }; - - api::err(status, error).into_response() - } -} - -pub fn multer_into_status_code(error: &multer::Error) -> StatusCode { - match error { - multer::Error::UnknownField { .. } - | multer::Error::IncompleteFieldData { .. } - | multer::Error::IncompleteHeaders - | multer::Error::ReadHeaderFailed(..) - | multer::Error::DecodeHeaderName { .. } - | multer::Error::DecodeContentType(..) - | multer::Error::NoBoundary - | multer::Error::DecodeHeaderValue { .. } - | multer::Error::NoMultipart - | multer::Error::IncompleteStream => StatusCode::BAD_REQUEST, - multer::Error::FieldSizeExceeded { .. } | multer::Error::StreamSizeExceeded { .. } => { - StatusCode::PAYLOAD_TOO_LARGE - } - - multer::Error::StreamReadFailed(err) => { - if let Some(err) = err.downcast_ref::() { - return multer_into_status_code(err); - } - - StatusCode::INTERNAL_SERVER_ERROR - } - - _ => StatusCode::INTERNAL_SERVER_ERROR, - } -} diff --git a/crates/server/src/openapi.rs b/crates/server/src/openapi.rs index 800f9df96..6bffe91d7 100644 --- a/crates/server/src/openapi.rs +++ b/crates/server/src/openapi.rs @@ -12,315 +12,3 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - -mod modifiers; -use modifiers::*; - -use serde_json::Value; -use std::borrow::Cow; -use utoipa::{ - openapi::{ - schema::SchemaType, ArrayBuilder, ContentBuilder, ObjectBuilder, Ref, RefOr, Response, ResponseBuilder, Schema, - Type, - }, - OpenApi, PartialSchema, ToResponse, ToSchema, -}; - -#[derive(OpenApi)] -#[openapi( - modifiers( - &UpdatePathsToIncludeDefaultVersion, - &IncludeErrorProneDatatypes, - &SecuritySchemes, - &ResponseModifiers, - ), - info( - title = "charted-server", - description = "🐻‍❄️📦 Free, open source, and reliable Helm Chart registry made in Rust", - version = charted_core::VERSION, - terms_of_service = "https://charts.noelware.org/legal/tos", - license( - identifier = "Apache-2.0", - name = "Apache 2.0 License", - url = "https://apache.org/licenses/LICENSE-2.0" - ), - contact( - name = "Noelware, LLC.", - email = "team@noelware.org", - url = "https://noelware.org" - ) - ), - tags( - ( - name = "Main", - description = "Represents all the main routes that don't tie to any entity" - ), - ( - name = "Users", - description = "Endpoints that create, modify, delete, or fetch user metadata" - ), - ( - name = "Users/Avatars", - description = "Endpoints that can create, modify, delete, and fetch user avatars" - ), - ( - name = "Users/Sessions", - description = "Endpoints that allow to login as a user and get an access token." - ), - ( - name = "API Keys", - description = "Endpoints that allow authenticating users with a secret key that is trusted by the server." - ), - ( - name = "Repositories", - description = "Endpoints that create, modify, delete, or fetch user/organization repository metadata" - ), - ( - name = "Repository/Releases", - description = "Endpoints that create, modify, delete, or fetch user/organization repository releases" - ), - ( - name = "Repository/Members", - description = "Endpoints that create, modify, delete, or fetch user/organization repository members" - ), - ( - name = "Organizations", - description = "Endpoints that create, modify, delete, or fetch organization metadata" - ), - ( - name = "Organization/Members", - description = "Endpoints that create, modify, delete, or fetch organization members" - ), - ), - components( - schemas( - // ==== Request Bodies ==== - charted_types::payloads::repository::release::CreateRepositoryReleasePayload, - charted_types::payloads::repository::release::PatchRepositoryReleasePayload, - charted_types::payloads::organization::CreateOrganizationPayload, - charted_types::payloads::organization::PatchOrganizationPayload, - charted_types::payloads::repository::CreateRepositoryPayload, - charted_types::payloads::repository::PatchRepositoryPayload, - charted_types::payloads::apikey::CreateApiKeyPayload, - charted_types::payloads::apikey::PatchApiKeyPayload, - charted_types::payloads::user::CreateUserPayload, - charted_types::payloads::user::PatchUserPayload, - - // ==== scopes ==== - charted_core::bitflags::ApiKeyScope, - - // ==== Response Datatypes ==== - crate::routing::v1::info::Info, - crate::routing::v1::main::Main, - crate::routing::v1::Entrypoint, - - // ==== Helm ==== - charted_types::helm::StringOrImportValue, - charted_types::helm::ChartSpecVersion, - charted_types::helm::ChartMaintainer, - charted_types::helm::ChartDependency, - charted_types::helm::ChartIndexSpec, - charted_types::helm::ImportValue, - charted_types::helm::ChartIndex, - charted_types::helm::ChartType, - charted_types::helm::Chart, - - // ==== Entities ==== - charted_types::RepositoryRelease, - charted_types::RepositoryMember, - charted_types::Repository, - - charted_types::OrganizationMember, - charted_types::Organization, - - charted_types::UserConnections, - charted_types::Session, - charted_types::ApiKey, - charted_types::User, - - // // ==== API Entities ==== - charted_core::api::ErrorCode, - charted_core::api::Error, - - // ==== Generic ==== - //charted_core::serde::Duration, - charted_core::Distribution, - charted_types::name::Name, - charted_types::VersionReq, - crate::types::NameOrUlid, - // charted_types::DateTime, - charted_types::Version, - charted_types::Ulid - ), - responses( - EmptyApiResponse, - ApiErrorResponse - ) - ), - paths( - // === ORGANIZATIONS / AVATARS === - - // === ORGANIZATIONS / MEMBERS === - - // === ORGANIZATIONS / REPOSITORIES === - - // === REPOSITORIES / ICONS === - - // === REPOSITORIES / MEMBERS === - - // === REPOSITORIES / RELEASES === - - // === USERS / REPOSITORIES === - crate::routing::v1::user::repositories::list_self_user_repositories, - crate::routing::v1::user::repositories::list_user_repositories, - crate::routing::v1::user::repositories::create_user_repository, - - // === USERS / AVATARS === - crate::routing::v1::user::avatars::get_all_user_avatars, - crate::routing::v1::user::avatars::get_all_self_user_avatars, - crate::routing::v1::user::avatars::get_user_avatar, - crate::routing::v1::user::avatars::get_self_user_avatar, - crate::routing::v1::user::avatars::upload_avatar, - crate::routing::v1::user::avatars::delete_avatar, - - // === USERS / SESSIONS === - crate::routing::v1::user::sessions::login, - crate::routing::v1::user::sessions::logout, - crate::routing::v1::user::sessions::refresh_session_token, - - // === USERS / APIKEYS === - crate::routing::v1::user::apikeys::list, - crate::routing::v1::user::apikeys::get, - crate::routing::v1::user::apikeys::create, - crate::routing::v1::user::apikeys::patch, - crate::routing::v1::user::apikeys::delete, - - // === USERS === - crate::routing::v1::user::create_user, - crate::routing::v1::user::get_user, - crate::routing::v1::user::get_self, - crate::routing::v1::user::delete, - crate::routing::v1::user::patch, - crate::routing::v1::user::main, - - // === MAIN === - crate::routing::v1::heartbeat::heartbeat, - crate::routing::v1::index::get_chart_index, - crate::routing::v1::info::info, - crate::routing::v1::main::main, - ), - servers( - ( - url = "https://charts.noelware.org/api/v{version}", - description = "Official, Production Service by Noelware, LLC.", - variables( - ("version" = ( - default = "1", - description = "API revision of the charted HTTP specification", - enum_values("1") - )) - ) - ) - ), - external_docs( - url = "https://charts.noelware.org/docs/server/latest", - description = "charted-server :: Documentation" - ) -)] -pub struct Document; - -/// Represents a generic empty API response, please do not use this in actual code, -/// it is only meant for utoipa for OpenAPI code generation. -pub struct EmptyApiResponse; - -impl PartialSchema for EmptyApiResponse { - fn schema() -> RefOr { - let object = ObjectBuilder::new() - .property( - "success", - RefOr::T(Schema::Object({ - ObjectBuilder::new() - .schema_type(SchemaType::Type(Type::Boolean)) - .description(Some("Whether if this request was a success")) - .build() - })), - ) - .build(); - - RefOr::T(Schema::Object(object)) - } -} - -impl ToSchema for EmptyApiResponse { - fn name() -> Cow<'static, str> { - Cow::Borrowed("EmptyApiResponse") - } -} - -impl<'r> ToResponse<'r> for EmptyApiResponse { - fn response() -> (&'r str, RefOr) { - let response = ResponseBuilder::new() - .description("API response that doesn't contain any data") - .content( - "application/json", - ContentBuilder::new().schema(Some(EmptyApiResponse::schema())).build(), - ) - .build(); - - ("EmptyApiResponse", RefOr::T(response)) - } -} - -/// Represents a generic API error response object. Please do not use this in actual code, -/// it is only meant for OpenAPI code generation. -pub struct ApiErrorResponse; - -impl PartialSchema for ApiErrorResponse { - fn schema() -> RefOr { - let object = ObjectBuilder::new() - .property( - "success", - RefOr::T(Schema::Object({ - ObjectBuilder::new() - .schema_type(SchemaType::Type(Type::Boolean)) - .description(Some("Whether if this request was a success or not (always false)")) - .default(Some(Value::Bool(false))) - .build() - })), - ) - .property( - "errors", - RefOr::T(Schema::Array({ - ArrayBuilder::new() - .description(Some( - "List of errors that happened. This can be represented as a stacktrace", - )) - .items(RefOr::Ref(Ref::from_schema_name("Error"))) - .build() - })), - ) - .build(); - - RefOr::T(Schema::Object(object)) - } -} - -impl ToSchema for ApiErrorResponse { - fn name() -> Cow<'static, str> { - Cow::Borrowed("ApiErrorResponse") - } -} - -impl<'r> ToResponse<'r> for ApiErrorResponse { - fn response() -> (&'r str, RefOr) { - let response = ResponseBuilder::new() - .description("API response that is returned during a error path") - .content( - "application/json", - ContentBuilder::new().schema(Some(ApiErrorResponse::schema())).build(), - ) - .build(); - - ("ApiErrorResponse", RefOr::T(response)) - } -} diff --git a/crates/server/src/ops/charts.rs b/crates/server/src/ops/charts.rs deleted file mode 100644 index f6b172e70..000000000 --- a/crates/server/src/ops/charts.rs +++ /dev/null @@ -1,66 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::ServerContext; -use azalia::remi::core::{Bytes, StorageService, UploadRequest}; -use charted_types::{ - helm::{self, ChartIndex}, - Ulid, User, -}; -use eyre::Context; -use tracing::{error, instrument}; - -#[instrument(name = "charted.server.ops.indexes.get", skip(ctx))] -pub async fn get_index(ctx: &ServerContext, id: Ulid) -> eyre::Result> { - let Some(content) = ctx - .storage - .open(format!("./metadata/{id}/index.yaml")) - .await - .inspect_err(|e| { - error!(error = %e, %id, "failed to lookup chart index from data storage"); - sentry::capture_error(e); - })? - else { - return Ok(None); - }; - - serde_yaml_ng::from_slice(&content) - .map(Some) - .inspect_err(|e| { - error!(error = %e, %id, "failed to deserialize chart into `helm::ChartIndex`"); - sentry::capture_error(e); - }) - .context("failed to deserialize chart into `helm::ChartIndex`") -} - -#[instrument(name = "charted.server.ops.indexes.get", skip_all, fields(user.id))] -pub async fn create_index(cx: &ServerContext, user: &User) -> eyre::Result<()> { - let index = ChartIndex::default(); - let serialized = serde_yaml_ng::to_string(&index)?; - - cx.storage - .upload( - format!("./metadata/{}/index.yaml", user.id), - UploadRequest::default() - .with_content_type(Some("application/yaml")) - .with_data(Bytes::from(serialized)), - ) - .await - .inspect_err(|e| { - error!(error = %e, %user.id, "unable to upload chart index for user"); - sentry::capture_error(e); - }) - .context("failed to upload chart index") -} diff --git a/crates/server/src/ops/db/apikey.rs b/crates/server/src/ops/db/apikey.rs deleted file mode 100644 index 368d04a3f..000000000 --- a/crates/server/src/ops/db/apikey.rs +++ /dev/null @@ -1,73 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::{NameOrUlid, ServerContext}; -use charted_database::{ - connection, - schema::{postgresql, sqlite}, -}; -use charted_types::ApiKey; -use eyre::Report; -use tracing::instrument; - -#[instrument(name = "charted.server.ops.db.getApiKey", skip_all)] -pub async fn get( - ctx: &ServerContext, - key: String, - owner: Option>, -) -> eyre::Result> { - let owner_uid: Option = owner.map(Into::into); - let mut conn = ctx.pool.get()?; - - connection!(@raw conn { - PostgreSQL(conn) => conn.build_transaction().read_only().run::<_, eyre::Report, _>(|txn| { - use postgresql::api_keys::{dsl::*, table}; - use diesel::pg::Pg; - - let mut query = table.into_boxed().select(>::as_select()).filter(token.eq(key)); - if let Some(uid) = owner_uid { - query = match uid { - NameOrUlid::Ulid(uid) => query.filter(owner.eq(uid)), - NameOrUlid::Name(user_name) => query.filter(owner.eq(user_name)), - }; - } - - match query.first(txn) { - Ok(user) => Ok(Some(user)), - Err(diesel::result::Error::NotFound) => Ok(None), - Err(e) => Err(Report::from(e)) - } - }); - - SQLite(conn) => conn.immediate_transaction(|txn| { - use sqlite::api_keys::{dsl::*, table}; - use diesel::sqlite::Sqlite; - - let mut query = table.into_boxed().select(>::as_select()).filter(token.eq(key)); - if let Some(uid) = owner_uid { - query = match uid { - NameOrUlid::Ulid(uid) => query.filter(owner.eq(uid)), - NameOrUlid::Name(user_name) => query.filter(owner.eq(user_name)), - }; - } - - match query.first(txn) { - Ok(user) => Ok(Some(user)), - Err(diesel::result::Error::NotFound) => Ok(None), - Err(e) => Err(Report::from(e)) - } - }); - }) -} diff --git a/crates/server/src/ops/db/mod.rs b/crates/server/src/ops/db/mod.rs deleted file mode 100644 index 327306669..000000000 --- a/crates/server/src/ops/db/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Contains the database operations for most entities so it doesn't get repeated as much. - -pub mod apikey; -pub mod organization; -pub mod user; diff --git a/crates/server/src/ops/db/organization.rs b/crates/server/src/ops/db/organization.rs deleted file mode 100644 index d02e4447b..000000000 --- a/crates/server/src/ops/db/organization.rs +++ /dev/null @@ -1,68 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::{NameOrUlid, ServerContext}; -use charted_database::{ - connection, - schema::{postgresql, sqlite}, -}; -use charted_types::Organization; -use eyre::Report; -use tracing::instrument; - -#[instrument(name = "charted.server.ops.db.getOrganization", skip_all)] -pub async fn get>(cx: &ServerContext, id: ID) -> eyre::Result> { - let nou = id.into(); - let mut conn = cx.pool.get()?; - - connection!(@raw conn { - PostgreSQL(conn) => conn.build_transaction().read_only().run::<_, eyre::Report, _>(|txn| { - use postgresql::organizations::{dsl::*, table}; - use diesel::pg::Pg; - - let mut query = table.into_boxed().select(>::as_select()); - query = match nou { - NameOrUlid::Ulid(ulid) => query.filter(id.eq(ulid)), - NameOrUlid::Name(n) => query.filter(name.eq(n)) - }; - - match query.first(txn) { - Ok(org) => Ok(Some(org)), - Err(diesel::result::Error::NotFound) => Ok(None), - Err(e) => Err(Report::from(e)) - } - }); - - SQLite(conn) => conn.immediate_transaction(|txn| { - use sqlite::organizations::{dsl::*, table}; - use diesel::sqlite::Sqlite; - - let mut query = table.into_boxed().select(>::as_select()); - query = match nou { - NameOrUlid::Ulid(ulid) => query.filter(id.eq(ulid)), - NameOrUlid::Name(n) => query.filter(name.eq(n)) - }; - - match query.first(txn) { - Ok(org) => Ok(Some(org)), - Err(diesel::result::Error::NotFound) => Ok(None), - Err(e) => Err(Report::from(e)) - } - }); - }) - .inspect_err(|err| { - sentry_eyre::capture_report(err); - }) -} diff --git a/crates/server/src/ops/db/user.rs b/crates/server/src/ops/db/user.rs deleted file mode 100644 index 4cfa81cc3..000000000 --- a/crates/server/src/ops/db/user.rs +++ /dev/null @@ -1,95 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::{NameOrUlid, ServerContext}; -use charted_database::{ - connection, - schema::{postgresql, sqlite}, -}; -use charted_types::User; -use eyre::Report; -use tracing::{instrument, trace}; - -#[instrument(name = "charted.server.ops.db.getUser", skip_all)] -pub async fn get>(ctx: &ServerContext, id: ID) -> eyre::Result> { - let name_or_ulid = id.into(); - let mut conn = ctx.pool.get()?; - - connection!(@raw conn { - PostgreSQL(conn) => conn.build_transaction().read_only().run::<_, eyre::Report, _>(|txn| { - use postgresql::users::{dsl::*, table}; - use diesel::pg::Pg; - - // We have to box the query since we need to match over either a - // ULID or Username and we can't do that if it isn't boxed. - let mut query = table - .into_boxed() - .select(>::as_select()); - - query = match name_or_ulid { - NameOrUlid::Ulid(uid) => query.filter(id.eq(uid)), - NameOrUlid::Name(user_name) => query.filter(username.eq(user_name)), - }; - - match query.first(txn) { - Ok(user) => Ok(Some(user)), - Err(diesel::result::Error::NotFound) => Ok(None), - Err(e) => Err(Report::from(e)) - } - }); - - SQLite(conn) => conn.immediate_transaction(|txn| { - use sqlite::users::{dsl::*, table}; - use diesel::sqlite::Sqlite; - - let mut query = table - .into_boxed() - .select(>::as_select()); - - query = match name_or_ulid { - NameOrUlid::Ulid(uid) => query.filter(id.eq(uid)), - NameOrUlid::Name(user_name) => query.filter(username.eq(user_name)), - }; - - match query.first(txn) { - Ok(user) => Ok(Some(user)), - Err(diesel::result::Error::NotFound) => Ok(None), - Err(e) => Err(Report::from(e)) - } - }); - }) - .inspect_err(|err| { - sentry_eyre::capture_report(err); - }) -} - -#[instrument(name = "charted.server.users.delete", skip_all, fields(%user.id, %user.username))] -pub async fn delete(_: ServerContext, user: User) -> eyre::Result<()> { - trace!("deleting user from database"); - - Ok(()) -} - -async fn _delete_all_repositories(_: &ServerContext, _: &User) -> eyre::Result<()> { - Ok(()) -} - -async fn _delete_all_organizations(_: &ServerContext, _: &User) -> eyre::Result<()> { - Ok(()) -} - -async fn _delete_persistent_metadata(_: &ServerContext, _: &User) -> eyre::Result<()> { - Ok(()) -} diff --git a/crates/server/src/ops/mod.rs b/crates/server/src/ops/mod.rs deleted file mode 100644 index 6779f26cf..000000000 --- a/crates/server/src/ops/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! The `charted_server::ops` module contains all the operations for most routes. - -pub mod charts; -pub mod db; diff --git a/crates/server/src/responses.rs b/crates/server/src/responses.rs deleted file mode 100644 index ed64bb986..000000000 --- a/crates/server/src/responses.rs +++ /dev/null @@ -1,52 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! The `charted_server::responses` module contains custom response types that don't -//! conform to the usual [api responses][charted_core::api::Response]. - -use axum::{ - body::Body, - http::{header, Response, StatusCode}, - response::IntoResponse, -}; -use serde::Serialize; - -/// Axum response datatype that transmits [YAML](https://yaml.org). -#[derive(Debug, Clone)] -pub struct Yaml { - status: StatusCode, - inner: T, -} - -impl From<(StatusCode, T)> for Yaml { - fn from((status, inner): (StatusCode, T)) -> Self { - Yaml { status, inner } - } -} - -impl IntoResponse for Yaml { - fn into_response(self) -> axum::response::Response { - // Safety: we know that the derive macro for serde will always succeed. If this isn't - // the case, then it is considered undefined behaviour -- please file an issue - // if this is the case. - let serialized = unsafe { serde_yaml_ng::to_string(&self.inner).unwrap_unchecked() }; - - Response::builder() - .status(self.status) - .header(header::CONTENT_TYPE, "application/yaml; charset=utf-8") - .body(Body::from(serialized)) - .expect("this should succeed") - } -} diff --git a/crates/server/src/routing/mod.rs b/crates/server/src/routing/mod.rs index ae23ba54c..e69de29bb 100644 --- a/crates/server/src/routing/mod.rs +++ b/crates/server/src/routing/mod.rs @@ -1,93 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::ServerContext; -use axum::{ - body::Body, - extract::DefaultBodyLimit, - http::{Method, Response, StatusCode}, - response::IntoResponse, - BoxError, Router, -}; -use charted_core::api; -use serde_json::json; -use std::{any::Any, borrow::Cow}; -use tower::ServiceBuilder; -use tower_http::{ - compression::CompressionLayer, - cors::{self, CorsLayer}, -}; - -pub mod v1; - -macro_rules! mk_router { - ($cx:ident, $($version:ident),*) => {{ - let mut router = ::axum::Router::new() - .merge(v1::create_router(&$cx)); - - $( - router = router - .clone() - .nest(concat!("/", stringify!($version)), $crate::routing::$version::create_router(&$cx)); - )* - - router - }}; -} - -fn panic_handler(message: Box) -> Response { - let details = azalia::message_from_panic(message); - tracing::error!(%details, "http server has panicked"); - - api::err(StatusCode::INTERNAL_SERVER_ERROR, api::Error { - code: api::ErrorCode::InternalServerError, - message: Cow::Borrowed("unable to process this request at this time. if this keeps occurring, report this to Noelware via GitHub issues!"), - details: Some(json!({ - "report_url": "https://github.com/charted-dev/charted/issues/new" - })) - }).into_response() -} - -// TODO(@auguwu): properly implement `HandleErrorLayer` -- at the moment, it doesn't -// want to work. -#[allow(unused)] -fn handle_error(error: BoxError) -> api::Response { - todo!() -} - -pub fn create_router(cx: &ServerContext) -> Router { - let stack = ServiceBuilder::new() - .layer(sentry_tower::NewSentryLayer::new_from_top()) - .layer(sentry_tower::SentryHttpLayer::with_transaction()) - .layer(tower_http::catch_panic::CatchPanicLayer::custom(panic_handler)) - .layer(CompressionLayer::new().gzip(true)) - .layer(DefaultBodyLimit::max(100 * 1024 * 1024)) - .layer( - CorsLayer::new() - .allow_methods([ - Method::GET, - Method::PUT, - Method::HEAD, - Method::POST, - Method::PATCH, - Method::DELETE, - ]) - .allow_origin(cors::Any), - ) - .layer(axum::middleware::from_fn(crate::middleware::request_id)) - .layer(axum::middleware::from_fn(crate::middleware::log)); - - Router::new().merge(mk_router!(cx, v1)).layer(stack) -} diff --git a/crates/server/src/routing/v1/heartbeat.rs b/crates/server/src/routing/v1/heartbeat.rs deleted file mode 100644 index e3bdc4afc..000000000 --- a/crates/server/src/routing/v1/heartbeat.rs +++ /dev/null @@ -1,33 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// Healthcheck endpoint to determine if services are OK. -#[utoipa::path( - get, - path = "/v1/heartbeat", - operation_id = "heartbeat", - tags = ["Main"], - responses( - ( - status = 200, - description = "Successful response", - content_type = "text/plain" - ) - ) -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn heartbeat() -> &'static str { - "Ok." -} diff --git a/crates/server/src/routing/v1/index.rs b/crates/server/src/routing/v1/index.rs deleted file mode 100644 index 5dbdc0c57..000000000 --- a/crates/server/src/routing/v1/index.rs +++ /dev/null @@ -1,114 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::{extract::Path, openapi::ApiErrorResponse, ops, responses::Yaml, NameOrUlid, ServerContext}; -use axum::{extract::State, http::StatusCode}; -use charted_core::api; -use charted_types::helm; -use serde_json::json; - -/// Retrieve a chart index for a User or Organization. -#[utoipa::path( - get, - path = "/v1/indexes/{idOrName}", - operation_id = "getChartIndex", - tag = "Main", - params( - ( - "idOrName" = NameOrUlid, - Path, - - description = "Parameter that can take a `Name` or `Ulid`" - ), - ), - responses( - ( - status = 200, - description = "Chart index for a specific [`User`] or [`Organization`]", - body = helm::ChartIndex, - content_type = "application/yaml" - ), - ( - status = 404, - description = "Entity was not found", - body = ApiErrorResponse, - content_type = "application/json" - ), - ( - status = 500, - description = "Internal Server Error", - body = ApiErrorResponse, - content_type = "application/json" - ) - ) -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn get_chart_index( - State(ctx): State, - Path(id_or_name): Path, -) -> Result, api::Response> { - match ops::db::user::get(&ctx, id_or_name.clone()).await { - Ok(Some(user)) => { - let Some(result) = ops::charts::get_index(&ctx, user.id) - .await - .map_err(|_| api::internal_server_error())? - else { - return Err(api::err( - StatusCode::NOT_FOUND, - ( - api::ErrorCode::EntityNotFound, - "index for user doesn't exist, this is definitely a bug", - json!({"class":"User","id_or_name":id_or_name}), - ), - )); - }; - - Ok((StatusCode::OK, result).into()) - } - - Ok(None) => match ops::db::organization::get(&ctx, id_or_name.clone()).await { - Ok(Some(org)) => { - let Some(result) = ops::charts::get_index(&ctx, org.id) - .await - .map_err(|_| api::internal_server_error())? - else { - return Err(api::err( - StatusCode::NOT_FOUND, - ( - api::ErrorCode::EntityNotFound, - "index for organization doesn't exist, this is definitely a bug", - json!({"class":"Organization","id_or_name":id_or_name}), - ), - )); - }; - - Ok((StatusCode::OK, result).into()) - } - - Ok(None) => Err(api::err( - StatusCode::NOT_FOUND, - ( - api::ErrorCode::EntityNotFound, - "unable to find user or organization", - json!({"id_or_name":id_or_name}), - ), - )), - - Err(_) => Err(api::internal_server_error()), - }, - - Err(_) => Err(api::internal_server_error()), - } -} diff --git a/crates/server/src/routing/v1/info.rs b/crates/server/src/routing/v1/info.rs deleted file mode 100644 index 17c4322d3..000000000 --- a/crates/server/src/routing/v1/info.rs +++ /dev/null @@ -1,74 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use axum::http::StatusCode; -use charted_core::{api, Distribution, BUILD_DATE, COMMIT_HASH, VERSION}; -use serde::Serialize; -use utoipa::ToSchema; - -/// Represents the response for the `GET /info` REST handler. -#[derive(Serialize, ToSchema)] -pub struct Info { - /// The distribution the server is running off from - pub distribution: Distribution, - - /// The commit hash from the Git repository. - pub commit_sha: &'static str, - - /// Build date in RFC3339 format - pub build_date: &'static str, - - /// Product name. Will always be "charted-server" - pub product: &'static str, - - /// Valid SemVer 2 of the current version of this instance - pub version: &'static str, - - /// Vendor of charted-server, will always be "Noelware, LLC." - pub vendor: &'static str, -} - -impl Default for Info { - fn default() -> Self { - Self { - distribution: Distribution::detect(), - commit_sha: COMMIT_HASH, - build_date: BUILD_DATE, - product: "charted-server", - version: VERSION, - vendor: "Noelware, LLC.", - } - } -} - -/// Shows information about this running instance. -#[utoipa::path( - get, - path = "/v1/info", - operation_id = "info", - tags = ["Main"], - responses( - ( - status = 200, - description = "Successful response", - body = api::Response, - content_type = "application/json" - ) - ) -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn info() -> api::Response { - api::from_default(StatusCode::OK) -} diff --git a/crates/server/src/routing/v1/main.rs b/crates/server/src/routing/v1/main.rs deleted file mode 100644 index e555d4ec3..000000000 --- a/crates/server/src/routing/v1/main.rs +++ /dev/null @@ -1,99 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use axum::http::StatusCode; -use charted_core::{api, VERSION}; -use serde::Serialize; -use utoipa::{ - openapi::{ContentBuilder, Ref, RefOr, Response, ResponseBuilder}, - ToResponse, ToSchema, -}; - -/// Response object for the `GET /` REST controller. -#[derive(Serialize, ToSchema)] -pub struct Main { - /// The message, which will always be "Hello, world!" - pub message: &'static str, - - /// You know, for Helm charts? - pub tagline: &'static str, - - /// Documentation URL for this generic entrypoint response. - pub docs: String, -} - -impl Default for Main { - fn default() -> Self { - Self { - message: "Hello, world! 👋", - tagline: "You know, for Helm charts?", - docs: format!("https://charts.noelware.org/docs/server/{VERSION}"), - } - } -} - -impl<'r> ToResponse<'r> for Main { - fn response() -> (&'r str, RefOr) { - ( - "Main", - RefOr::T( - ResponseBuilder::new() - .description("Response for the `/` REST handler") - .content( - "application/json", - ContentBuilder::new() - .schema(Some(RefOr::Ref(Ref::from_schema_name("MainResponse")))) - .build(), - ) - .build(), - ), - ) - } -} - -/// Main entrypoint response to the API. Nothing too important. -#[utoipa::path( - get, - path = "/v1", - operation_id = "main", - tags = ["Main"], - responses( - ( - status = 200, - description = "Successful response", - body = api::Response

, - content_type = "application/json" - ) - ) -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn main() -> api::Response
{ - api::from_default(StatusCode::OK) -} - -// #[cfg(test)] -// mod tests { -// use super::*; -// use charted_testkit::TestContext; - -// #[charted_testkit::test(router)] -// async fn test_main_endpoint(cx: &mut TestContext) -> eyre::Result<()> { -// let (tmpdir, ctx) = crate::test::create_server_context(&[|_| {}]).await?; -// let router = crate::routing::create_router(&ctx).with_state(ctx); -// cx.serve(router).await; - -// Ok(()) -// } -// } diff --git a/crates/server/src/routing/v1/metrics.rs b/crates/server/src/routing/v1/metrics.rs deleted file mode 100644 index 6bffe91d7..000000000 --- a/crates/server/src/routing/v1/metrics.rs +++ /dev/null @@ -1,14 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. diff --git a/crates/server/src/routing/v1/mod.rs b/crates/server/src/routing/v1/mod.rs deleted file mode 100644 index a80404669..000000000 --- a/crates/server/src/routing/v1/mod.rs +++ /dev/null @@ -1,85 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod heartbeat; -pub mod index; -pub mod info; -pub mod main; -pub mod openapi; -pub mod organization; -pub mod repository; -pub mod user; - -use crate::ServerContext; -use axum::{extract::Request, http::StatusCode, response::IntoResponse, routing, Router}; -use charted_core::{api, VERSION}; -use serde::Serialize; -use serde_json::json; -use std::{borrow::Cow, ops::Deref}; -use utoipa::ToSchema; - -/// Generic entrypoint message for any API route like `/users`. -#[derive(Serialize, ToSchema)] -pub struct Entrypoint { - /// Humane message to greet you. - pub message: Cow<'static, str>, - - /// URI to the documentation for this entrypoint. - pub docs: Cow<'static, str>, -} - -impl Entrypoint { - pub fn new(entity: impl AsRef) -> Self { - let entity = entity.as_ref(); - Self { - message: Cow::Owned(format!("welcome to the {entity} API")), - docs: Cow::Owned(format!( - "https://charts.noelware.org/docs/server/{VERSION}/api/reference/{}", - entity.to_lowercase().replace(' ', "") - )), - } - } -} - -pub fn create_router(cx: &ServerContext) -> Router { - let mut router = Router::new() - .nest("/users", user::create_router()) - .route("/indexes/{idOrName}", routing::get(index::get_chart_index)) - .route("/heartbeat", routing::get(heartbeat::heartbeat)) - .route("/openapi.json", routing::get(openapi::openapi)) - .route("/info", routing::get(info::info)) - .route("/", routing::get(main::main)) - .fallback(fallback); - - for feature in &cx.features { - router = feature.extend_router().with_state(cx.deref().clone()); - } - - router -} - -async fn fallback(req: Request) -> impl IntoResponse { - api::err( - StatusCode::NOT_FOUND, - ( - api::ErrorCode::HandlerNotFound, - "endpoint was not found", - json!({ - "method": req.method().as_str(), - "uri": req.uri().path() - }), - ), - ) -} diff --git a/crates/server/src/routing/v1/openapi.rs b/crates/server/src/routing/v1/openapi.rs deleted file mode 100644 index dbd9f272a..000000000 --- a/crates/server/src/routing/v1/openapi.rs +++ /dev/null @@ -1,41 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#![allow(clippy::incompatible_msrv)] - -use crate::{openapi::Document, ServerContext}; -use axum::extract::State; -use std::sync::OnceLock; -use utoipa::OpenApi; - -// This is wrapped in a `OnceLock` and initialized on the first request is due to -// that any feature can extend the OpenAPI document to document routes when the -// feature is enabled. -static CACHED: OnceLock = OnceLock::new(); - -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn openapi(State(cx): State) -> String { - let document = CACHED.get_or_init(|| { - let mut doc = Document::openapi(); - - for feature in &cx.features { - feature.extends_openapi(&mut doc); - } - - doc - }); - - serde_json::to_string_pretty(document).expect("serialize") -} diff --git a/crates/server/src/routing/v1/organization/members.rs b/crates/server/src/routing/v1/organization/members.rs deleted file mode 100644 index 6bffe91d7..000000000 --- a/crates/server/src/routing/v1/organization/members.rs +++ /dev/null @@ -1,14 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. diff --git a/crates/server/src/routing/v1/organization/mod.rs b/crates/server/src/routing/v1/organization/mod.rs deleted file mode 100644 index d16ca74cf..000000000 --- a/crates/server/src/routing/v1/organization/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::ServerContext; -use axum::Router; - -pub fn create_router() -> Router { - Router::new() -} diff --git a/crates/server/src/routing/v1/organization/repositories.rs b/crates/server/src/routing/v1/organization/repositories.rs deleted file mode 100644 index 6bffe91d7..000000000 --- a/crates/server/src/routing/v1/organization/repositories.rs +++ /dev/null @@ -1,14 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. diff --git a/crates/server/src/routing/v1/repository/members.rs b/crates/server/src/routing/v1/repository/members.rs deleted file mode 100644 index 6bffe91d7..000000000 --- a/crates/server/src/routing/v1/repository/members.rs +++ /dev/null @@ -1,14 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. diff --git a/crates/server/src/routing/v1/repository/releases.rs b/crates/server/src/routing/v1/repository/releases.rs deleted file mode 100644 index 6bffe91d7..000000000 --- a/crates/server/src/routing/v1/repository/releases.rs +++ /dev/null @@ -1,14 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. diff --git a/crates/server/src/routing/v1/user/apikeys.rs b/crates/server/src/routing/v1/user/apikeys.rs deleted file mode 100644 index 780b7759a..000000000 --- a/crates/server/src/routing/v1/user/apikeys.rs +++ /dev/null @@ -1,414 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::{ - extract::{Json, Path, Query}, - middleware::session::{Middleware, Session}, - openapi::ApiErrorResponse, - NameOrUlid, ServerContext, -}; -use axum::{extract::State, handler::Handler, http::StatusCode, routing, Extension, Router}; -use charted_core::{ - api, - bitflags::{ApiKeyScope, ApiKeyScopes}, - rand_string, -}; -use charted_database::{ - paginate::Paginated, - schema::{postgresql, sqlite}, -}; -use charted_types::{ - payloads::apikey::{CreateApiKeyPayload, PatchApiKeyPayload}, - ApiKey, -}; -use chrono::Local; -use serde_json::json; -use tower_http::auth::AsyncRequireAuthorizationLayer; -use tracing::{error, instrument}; - -crate::macros::impl_list_response!(ListApiKeyResponse as "ApiKey"); - -pub fn create_router() -> Router { - Router::new() - .route( - "/", - routing::get(list.layer(AsyncRequireAuthorizationLayer::new( - Middleware::default().scopes([ApiKeyScope::ApiKeyList]), - ))) - .put(create.layer(AsyncRequireAuthorizationLayer::new( - Middleware::default().scopes([ApiKeyScope::ApiKeyCreate]), - ))), - ) - .route( - "/:idOrName", - routing::get(get.layer(AsyncRequireAuthorizationLayer::new( - Middleware::default().scopes([ApiKeyScope::ApiKeyView]), - ))) - .patch(patch.layer(AsyncRequireAuthorizationLayer::new( - Middleware::default().scopes([ApiKeyScope::ApiKeyUpdate]), - ))) - .delete(delete.layer(AsyncRequireAuthorizationLayer::new( - Middleware::default().scopes([ApiKeyScope::ApiKeyDelete]), - ))), - ) -} - -/// Lists all the user's API keys avaliable. -#[utoipa::path( - get, - path = "/v1/users/@me/apikeys", - operation_id = "listAPIKeys", - tag = "API Keys", - responses( - ( - status = 200, - description = "Successful request", - body = ListApiKeyResponse, - content_type = "application/json" - ) - ) -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -#[instrument(name = "charted.server.ops.user.listAPIKeys", skip_all, fields(user.name = %user.username, %user.id))] -pub async fn list( - State(ctx): State, - Extension(Session { user, .. }): Extension, -) -> api::Result> { - let mut conn = ctx - .pool - .get() - .inspect_err(|e| { - sentry::capture_error(e); - tracing::error!(error = %e, "failed to get db connection"); - }) - .map_err(|e| api::system_failure(eyre::Report::from(e)))?; - - let apikeys = charted_database::connection!(@raw conn { - PostgreSQL(conn) => conn.build_transaction().read_only().run::, diesel::result::Error, _>(|txn| { - use postgresql::api_keys::{dsl, table}; - use diesel::pg::Pg; - - table.select(>::as_select()) - .filter(dsl::owner.eq(&user.id)) - .load(txn) - }); - - SQLite(conn) => conn.immediate_transaction::<_, diesel::result::Error, _>(|txn| { - use sqlite::api_keys::{dsl, table}; - use diesel::sqlite::Sqlite; - - table.select(>::as_select()) - .filter(dsl::owner.eq(&user.id)) - .load(txn) - }); - }) - .inspect_err(|e| { - sentry::capture_error(e); - error!(error = %e, "failed to query API key"); - }) - .map_err(|e| api::system_failure(eyre::Report::from(e)))?; - - Ok(api::ok( - StatusCode::OK, - apikeys.into_iter().map(|x| x.sanitize()).collect::>(), - )) -} - -/// Retrieve a single API key's metadata -#[utoipa::path( - get, - path = "/v1/users/@me/apikeys/{idOrName}", - operation_id = "getAPIKey", - tag = "API Keys", - responses( - ( - status = 200, - description = "Successful request", - body = api::Response, - content_type = "application/json" - ), - ( - status = 404, - description = "API key was not found", - body = ApiErrorResponse, - content_type = "application/json" - ) - ) -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -#[instrument(name = "charted.server.ops.user.getAPIKey", skip_all, fields(user.name = %user.username, %user.id, apikey.name = %id_or_name))] -pub async fn get( - State(ctx): State, - Extension(Session { user, .. }): Extension, - Path(id_or_name): Path, -) -> api::Result { - let mut conn = ctx - .pool - .get() - .inspect_err(|e| { - sentry::capture_error(e); - error!(error = %e, "failed to get db connection"); - }) - .map_err(|e| api::system_failure::(e.into()))?; - - let Some(apikey) = charted_database::connection!(@raw conn { - PostgreSQL(conn) => conn.build_transaction().read_only().run::<_, diesel::result::Error, _>(|txn| { - use postgresql::api_keys::{dsl, table}; - use diesel::pg::Pg; - - let mut query = table - .into_boxed() - .select(>::as_select()) - .filter(dsl::owner.eq(&user.id)); - - query = match &id_or_name { - NameOrUlid::Name(name) => query.filter(dsl::name.eq(name)), - NameOrUlid::Ulid(id) => query.filter(dsl::id.eq(id)) - }; - - match query.first(txn) { - Ok(apikey) => Ok(Some(apikey)), - Err(diesel::result::Error::NotFound) => Ok(None), - Err(e) => Err(e), - } - }); - - SQLite(conn) => conn.immediate_transaction(|txn| { - use sqlite::api_keys::{dsl, table}; - use diesel::sqlite::Sqlite; - - let mut query = table - .into_boxed() - .select(>::as_select()) - .filter(dsl::owner.eq(&user.id)); - - query = match &id_or_name { - NameOrUlid::Name(name) => query.filter(dsl::name.eq(name)), - NameOrUlid::Ulid(id) => query.filter(dsl::id.eq(id)), - }; - - match query.first(txn) { - Ok(apikey) => Ok(Some(apikey)), - Err(diesel::result::Error::NotFound) => Ok(None), - Err(e) => Err(e), - } - }); - }) - .inspect_err(|e| { - sentry::capture_error(e); - error!(error = %e, "failed to query api key"); - }) - .map_err(|e| api::system_failure::(e.into()))? - else { - return Err(api::err( - StatusCode::NOT_FOUND, - ( - api::ErrorCode::EntityNotFound, - "api key with given ID or name doesn't exist", - json!({ - "idOrName": id_or_name - }), - ), - )); - }; - - Ok(api::ok(StatusCode::OK, apikey.sanitize())) -} - -/// Generate an API key from the current authenticated user. -#[utoipa::path( - put, - path = "/v1/users/@me/apikeys", - operation_id = "createAPIKey", - tag = "API Keys", - request_body( - content = ref("#/components/schemas/CreateApiKeyPayload"), - description = "Request body for creating a new API key", - content_type = "application/json" - ), - responses( - ( - status = 201, - description = "API key was created", - body = api::Response, - content_type = "application/json" - ), - ( - status = 409, - description = "If the API key with the name is already registered under the user", - body = ApiErrorResponse, - content_type = "application/json" - ) - ) -)] -#[instrument( - name = "charted.server.ops.v1.createApiKey", - skip_all, - fields( - apikey.owner = %user.username, - apikey.name = %name, - ) -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn create( - State(ctx): State, - Extension(Session { user, .. }): Extension, - Json(CreateApiKeyPayload { - name, - description, - expires_in: _, - scopes, - }): Json, -) -> api::Result { - let mut conn = ctx - .pool - .get() - .inspect_err(|e| { - sentry::capture_error(e); - error!(error = %e, "failed to get db connection"); - }) - .map_err(|e| api::system_failure::(e.into()))?; - - let exists = charted_database::connection!(@raw conn { - PostgreSQL(conn) => conn.build_transaction().read_only().run(|txn| { - use postgresql::api_keys::{dsl, table}; - use diesel::pg::Pg; - - let query = table - .select(>::as_select()) - .filter(dsl::owner.eq(&user.username)) - .filter(dsl::name.eq(&name)); - - match query.first(txn) { - Ok(_) => Ok(true), - Err(diesel::result::Error::NotFound) => Ok(false), - Err(e) => Err(e) - } - }); - - SQLite(conn) => conn.immediate_transaction(|txn| { - use sqlite::api_keys::{dsl, table}; - use diesel::sqlite::Sqlite; - - let query = table - .select(>::as_select()) - .filter(dsl::owner.eq(&user.username)) - .filter(dsl::name.eq(&name)); - - match query.first(txn) { - Ok(_) => Ok(true), - Err(diesel::result::Error::NotFound) => Ok(false), - Err(e) => Err(e) - } - }); - }) - .inspect_err(|e| { - sentry::capture_error(e); - error!(error = %e, "failed to query api key with given name and owner"); - }) - .map_err(|e| api::system_failure::(e.into()))?; - - if exists { - return Err(api::err( - StatusCode::CONFLICT, - ( - api::ErrorCode::EntityAlreadyExists, - "api key with name already exists", - json!({"name": name.as_str()}), - ), - )); - } - - let scopes = scopes.into_iter().collect::(); - let token = rand_string(16); - let id = ctx - .ulid_gen - .generate() - .inspect_err(|e| { - sentry::capture_error(e); - error!("received monotonic overflow -- please inspect this as fast you can!!!!!"); - }) - .map_err(api::system_failure)?; - - let now: charted_types::DateTime = chrono::DateTime::from(Local::now()).into(); - let key = ApiKey { - description, - created_at: now, - updated_at: now, - expires_in: None, - scopes: scopes.value().try_into().unwrap(), - token, - owner: user.id, - name, - id: id.into(), - }; - - charted_database::connection!(@raw conn { - PostgreSQL(conn) => conn.build_transaction().read_write().run(|txn| { - use postgresql::api_keys::table; - - diesel::insert_into(table).values(&key).execute(txn) - }); - - SQLite(conn) => conn.immediate_transaction(|txn| { - use sqlite::api_keys::table; - - diesel::insert_into(table).values(&key).execute(txn) - }); - }) - .inspect_err(|e| { - sentry::capture_error(e); - error!(error = %e, "failed to insert api key into database"); - }) - .map_err(|_| api::internal_server_error())?; - - // TODO(@auguwu): register api key to a scheduled background job - // to be deleted within today + `expires_in` - - Ok(api::ok(StatusCode::CREATED, key)) -} - -/// Patch metadata about a API key. -#[utoipa::path( - patch, - path = "/v1/users/@me/apikeys/{idOrName}", - operation_id = "createAPIKey", - tag = "API Keys" -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn patch( - State(ctx): State, - Extension(Session { user, .. }): Extension, - Path(id_or_name): Path, - Json(PatchApiKeyPayload { description, name, .. }): Json, -) -> api::Result<()> { - todo!() -} - -/// Wipes the API key off the system. -#[utoipa::path( - delete, - path = "/v1/users/@me/apikeys/{idOrName}", - operation_id = "createAPIKey", - tag = "API Keys" -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn delete( - State(ctx): State, - Extension(Session { user, .. }): Extension, - Path(id_or_name): Path, -) -> api::Result<()> { - todo!() -} diff --git a/crates/server/src/routing/v1/user/avatars.rs b/crates/server/src/routing/v1/user/avatars.rs deleted file mode 100644 index 4cbf6ef27..000000000 --- a/crates/server/src/routing/v1/user/avatars.rs +++ /dev/null @@ -1,88 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::ServerContext; -use axum::extract::State; - -#[utoipa::path( - get, - path = "/v1/users/{idOrName}/avatars", - tag = "Users/Avatars", - operation_id = "getUserAvatars" -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn get_all_user_avatars(State(_): State) {} - -#[utoipa::path( - get, - path = "/v1/users/@me/avatars", - tag = "Users/Avatars", - operation_id = "getSelfUserAvatars" -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn get_all_self_user_avatars() {} - -#[utoipa::path( - get, - path = "/v1/users/{idOrName}/avatar", - tag = "Users/Avatars", - operation_id = "getUserCurrentAvatar" -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn get_current_user_avatar() {} - -#[utoipa::path( - get, - path = "/v1/users/@me/avatar", - tag = "Users/Avatars", - operation_id = "getSelfUserAvatar" -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn get_self_user_avatar() {} - -#[utoipa::path( - get, - path = "/v1/users/{idOrName}/avatar/{hash}", - tag = "Users/Avatars", - operation_id = "getUserAvatar" -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn get_user_avatar() {} - -#[utoipa::path( - get, - path = "/v1/users/{idOrName}/avatar/{hash}", - tag = "Users/Avatars", - operation_id = "getSelfUserAvatar" -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn get_self_avatar() {} - -#[utoipa::path( - post, - path = "/v1/users/{idOrName}/avatar", - tag = "Users/Avatars", - operation_id = "uploadUserAvatar" -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn upload_avatar() {} - -#[utoipa::path( - delete, - path = "/v1/users/{idOrName}/avatar/{hash}", - tag = "Users/Avatars", - operation_id = "deleteUserAvatar" -)] -pub async fn delete_avatar() {} diff --git a/crates/server/src/routing/v1/user/mod.rs b/crates/server/src/routing/v1/user/mod.rs deleted file mode 100644 index fbb7c15df..000000000 --- a/crates/server/src/routing/v1/user/mod.rs +++ /dev/null @@ -1,621 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod apikeys; -pub mod avatars; -pub mod repositories; -pub mod sessions; - -use super::Entrypoint; -use crate::{ - extract::{Json, Path}, - hash_password, - middleware::session::{self, Session}, - openapi::{ApiErrorResponse, EmptyApiResponse}, - ops, NameOrUlid, ServerContext, -}; -use axum::{extract::State, http::StatusCode, routing, Extension, Router}; -use charted_core::{api, bitflags::ApiKeyScope}; -use charted_database::{ - connection, - schema::{postgresql, sqlite}, -}; -use charted_types::{ - payloads::user::{CreateUserPayload, PatchUserPayload}, - User, -}; -use eyre::Context; -use serde_json::json; -use tower_http::auth::AsyncRequireAuthorizationLayer; -use tracing::{error, instrument}; -use validator::ValidateEmail; - -pub fn create_router() -> Router { - let id_or_name = Router::new().route("/", routing::get(get_user)); - let at_me = Router::new() - .route( - "/", - routing::get(get_self).layer(AsyncRequireAuthorizationLayer::new( - session::Middleware::default().scopes([ApiKeyScope::UserAccess]), - )), - ) - .nest("/apikeys", apikeys::create_router()); - - Router::new() - .route("/", routing::get(main).put(create_user)) - .nest("/@me", at_me) - .nest("/{idOrName}", id_or_name) -} - -/// Entrypoint to the Users API. -#[utoipa::path( - get, - path = "/v1/users", - operation_id = "users", - tag = "Users", - responses( - ( - status = 200, - description = "Entrypoint response", - body = api::Response, - content_type = "application/json" - ) - ) -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn main() -> api::Response { - api::ok(StatusCode::OK, Entrypoint::new("Users")) -} - -#[utoipa::path( - post, - path = "/v1/users", - tag = "Users", - operation_id = "createUser", - request_body( - content = ref("CreateUserPayload"), - description = "Payload for creating a new user. The `password` field can be omitted if the session backend is not `Local`.", - content_type = "application/json" - ), - responses( - ( - status = 201, - description = "User has been created", - body = api::Response, - content_type = "application/json" - ), - ( - status = 403, - description = "Returned if the server doesn't allow user registrations or if this is a single-user registry", - body = ApiErrorResponse, - content_type = "application/json" - ), - ( - status = 406, - description = "Returned if the authentication backend requires a `password` field or the `email` field is not a valid email", - body = ApiErrorResponse, - content_type = "application/json" - ), - ( - status = 409, - description = "Returned if the `username` or `email` provided is already registered", - body = ApiErrorResponse, - content_type = "application/json" - ) - ) -)] -#[instrument( - name = "charted.server.ops.v1.createUser", - skip_all, - fields(user.name = %username) -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn create_user( - State(cx): State, - Json(CreateUserPayload { - email, - password, - username, - }): Json, -) -> api::Result { - if !cx.config.registrations || cx.config.single_user { - return Err(api::err( - StatusCode::FORBIDDEN, - ( - api::ErrorCode::RegistrationsDisabled, - "this instance has user registrations disabled", - ), - )); - } - - if cx.authz.as_ref().downcast::().is_some() && password.is_none() { - return Err(api::err( - StatusCode::NOT_ACCEPTABLE, - ( - api::ErrorCode::MissingPassword, - "authentication backend requires you to include a password for this new account", - ), - )); - } - - if !email.validate_email() { - return Err(api::err( - StatusCode::NOT_ACCEPTABLE, - ( - api::ErrorCode::ValidationFailed, - "`email` is not a valid email", - json!({"email":&email}), - ), - )); - } - - let mut conn = cx - .pool - .get() - .inspect_err(|e| { - sentry::capture_error(e); - error!(error = %e, "failed to get db connection"); - }) - .map_err(|_| api::internal_server_error())?; - - // Check if we already have this `User` by their username - { - let exists = connection!(@raw conn { - PostgreSQL(conn) => conn.build_transaction().read_only().run::<_, eyre::Report, _>(|txn| { - use postgresql::users::{dsl, table}; - use diesel::pg::Pg; - - match table.select(>::as_select()).filter(dsl::username.eq(&username)).first(txn) { - Ok(_) => Ok(true), - Err(diesel::result::Error::NotFound) => Ok(false), - Err(e) => Err(eyre::Report::from(e)) - } - }); - - SQLite(conn) => conn.immediate_transaction(|txn| { - use sqlite::users::{dsl, table}; - use diesel::sqlite::Sqlite; - - match table.select(>::as_select()).filter(dsl::username.eq(&username)).first(txn) { - Ok(_) => Ok(true), - Err(diesel::result::Error::NotFound) => Ok(false), - Err(e) => Err(eyre::Report::from(e)) - } - }); - }).inspect_err(|e| { - sentry_eyre::capture_report(e); - error!(user.name = %username, error = %e, "failed to query user by username"); - }).map_err(|_| api::internal_server_error())?; - - if exists { - return Err(api::err( - StatusCode::CONFLICT, - ( - api::ErrorCode::EntityAlreadyExists, - "a user with `username` already exists", - json!({"username":username.as_str()}), - ), - )); - } - } - - // Check if we already have this `User` by their email address - { - let exists = connection!(@raw conn { - PostgreSQL(conn) => conn.build_transaction().read_only().run::<_, eyre::Report, _>(|txn| { - use postgresql::users::{dsl, table}; - use diesel::pg::Pg; - - match table.select(>::as_select()).filter(dsl::email.eq(&email)).first(txn) { - Ok(_) => Ok(true), - Err(diesel::result::Error::NotFound) => Ok(false), - Err(e) => Err(eyre::Report::from(e)) - } - }); - - SQLite(conn) => conn.immediate_transaction(|txn| { - use sqlite::users::{dsl, table}; - use diesel::sqlite::Sqlite; - - match table.select(>::as_select()).filter(dsl::email.eq(&email)).first(txn) { - Ok(_) => Ok(true), - Err(diesel::result::Error::NotFound) => Ok(false), - Err(e) => Err(eyre::Report::from(e)) - } - }); - }) - .inspect_err(|e| { - sentry_eyre::capture_report(e); - error!(user.email = email, error = %e, "failed to query user by email"); - }) - .map_err(|_| api::internal_server_error())?; - - if exists { - return Err(api::err( - StatusCode::CONFLICT, - ( - api::ErrorCode::EntityAlreadyExists, - "a user with the `email` given already exists", - json!({"email":email}), - ), - )); - } - } - - let password = if let Some(ref password) = password { - if password.len() < 8 { - return Err(api::err( - StatusCode::NOT_ACCEPTABLE, - ( - api::ErrorCode::InvalidPassword, - "`password` length was expected to be 8 characters or longer", - ), - )); - } - - Some(hash_password(password).map_err(|_| api::internal_server_error())?) - } else { - None - }; - - let id = cx - .ulid_gen - .generate() - .inspect_err(|e| { - sentry::capture_error(e); - error!("received monotonic overflow -- please inspect this as fast you can!!!!!"); - }) - .map_err(|_| api::internal_server_error())?; - - let user = User { - verified_publisher: false, - prefers_gravatar: false, - gravatar_email: None, - description: None, - avatar_hash: None, - created_at: chrono::Utc::now().into(), - updated_at: chrono::Utc::now().into(), - password, - username, - email, - admin: false, - name: None, - id: id.into(), - }; - - connection!(@raw conn { - PostgreSQL(conn) => conn.build_transaction().read_write().run::<_, eyre::Report, _>(|txn| { - use postgresql::users::table; - - diesel::insert_into(table) - .values(&user) - .execute(txn) - .context("failed to insert user into database") - }); - - SQLite(conn) => conn.immediate_transaction(|txn| { - use sqlite::users::table; - - diesel::insert_into(table) - .values(&user) - .execute(txn) - .context("failed to insert user into database") - }); - }) - .inspect_err(|e| { - error!(error = %e, "failed to persist user into database"); - sentry_eyre::capture_report(e); - }) - .map_err(|_| api::internal_server_error())?; - - ops::charts::create_index(&cx, &user) - .await - .map_err(|_| api::internal_server_error())?; - - // TODO(@auguwu): if this is a single organization registry, then add - // them as an organization member - - Ok(api::ok(StatusCode::CREATED, user)) -} - -/// Locate a user by their ID or username. -#[utoipa::path( - get, - path = "/v1/users/{idOrName}", - tags = ["Users"], - operation_id = "getUserByIdOrName", - params( - ( - "idOrName" = NameOrUlid, - Path, - - description = "Parameter that can take a `Name` or `Ulid`" - ) - ), - responses( - ( - status = 200, - description = "A single user found", - body = api::Response, - content_type = "application/json" - ), - ( - status = 400, - description = "Invalid ID or name specified", - body = ApiErrorResponse, - content_type = "application/json" - ), - ( - status = 404, - description = "Entity Not Found", - body = ApiErrorResponse, - content_type = "application/json" - ) - ) -)] -pub async fn get_user(State(cx): State, Path(id_or_name): Path) -> api::Result { - match ops::db::user::get(&cx, id_or_name.clone()).await { - Ok(Some(user)) => Ok(api::ok(StatusCode::OK, user)), - Ok(None) => Err(api::err( - StatusCode::NOT_FOUND, - ( - api::ErrorCode::EntityNotFound, - "user with id or name was not found", - json!({"idOrName":id_or_name}), - ), - )), - - Err(_) => Err(api::internal_server_error()), - } -} - -/// Returns information about yourself via an authenticated request. -#[utoipa::path( - get, - path = "/v1/users/@me", - operation_id = "getSelfUser", - tags = ["Users"], - responses( - ( - status = 200, - description = "A single user found", - body = api::Response, - content_type = "application/json" - ), - ( - status = 4XX, - description = "Any occurrence when authentication fails", - body = ApiErrorResponse, - content_type = "application/json" - ) - ) -)] -pub async fn get_self(Extension(Session { user, .. }): Extension) -> api::Response { - api::ok(StatusCode::OK, user) -} - -/// Patch metadata about the current user. -#[utoipa::path( - patch, - path = "/v1/users/@me", - operation_id = "patchSelf", - tag = "Users", - request_body( - content_type = "application/json", - description = "Update payload for the `User` entity", - content = ref("PatchUserPayload") - ), - responses( - ( - status = 204, - description = "Patch was successfully reflected", - body = EmptyApiResponse, - content_type = "application/json" - ), - ( - status = 4XX, - description = "Any occurrence when authentication fails or if the patch couldn't be reflected", - body = ApiErrorResponse, - content_type = "application/json" - ) - ) -)] -pub async fn patch( - State(cx): State, - Extension(Session { mut user, .. }): Extension, - Json(PatchUserPayload { - prefers_gravatar, - gravatar_email, - description, - username, - password, - email, - name, - }): Json, -) -> api::Result<()> { - if let Some(prefers_gravatar) = prefers_gravatar { - if user.prefers_gravatar != prefers_gravatar { - user.prefers_gravatar = prefers_gravatar; - } - } - - if let Some(gravatar_email) = gravatar_email.as_deref() { - // if `old` == None, then update the description - // if `old` == Some(..) && `old` != `gravatar_email`, commit update - // if `old` == Some(..) && `old` == `""`, commit as `None` - let old = user.gravatar_email.as_deref(); - if old.is_none() && !gravatar_email.is_empty() { - user.gravatar_email = Some(gravatar_email.to_owned()); - } else if let Some(old) = old - && !old.is_empty() - && old != gravatar_email - { - user.gravatar_email = Some(gravatar_email.to_owned()); - } else if gravatar_email.is_empty() { - user.description = None; - } - } - - if let Some(description) = description { - if description.len() > 140 { - let len = description.len(); - return Err(api::err( - StatusCode::NOT_ACCEPTABLE, - ( - api::ErrorCode::ValidationFailed, - "expected `description` to be less than 140 characters", - json!({ - "expected": 140, - "received": { - "over": len - 140, - "length": len - } - }), - ), - )); - } - - // if `old` == None, then update the description - // if `old` == Some(..) && `old` != `descroption`, commit update - // if `old` == Some(..) && `old` == `""`, commit as `None` - let old = user.description.as_deref(); - if old.is_none() { - user.description = Some(description); - } else if let Some(old) = old - && !old.is_empty() - && old != description - { - user.description = Some(description); - } else if description.is_empty() { - user.description = None; - } - } - - if let Some(username) = username { - // We need to validate that the username isn't already taken, so we will get a - // temporary connection. - match ops::db::user::get(&cx, NameOrUlid::Name(username.clone())).await { - Ok(None) => {} - Ok(Some(_)) => { - return Err(api::err( - StatusCode::CONFLICT, - ( - api::ErrorCode::EntityAlreadyExists, - "user with username already exists", - json!({"username":&username}), - ), - )) - } - - Err(e) => return Err(api::system_failure(e)), - }; - - // In deserialization of the request body, it'll validate that - // the name is correct anyway, so it is ok to set it here without - // even more validation. - user.username = username; - } - - if let Some(password) = password.as_deref() { - let authz = cx.authz.as_ref(); - if authz.downcast::().is_none() { - return Err(api::err( - StatusCode::NOT_ACCEPTABLE, - ( - api::ErrorCode::InvalidBody, - "`password` is only supported on the local authz backend", - ), - )); - } - - if password.len() < 8 { - return Err(api::err( - StatusCode::NOT_ACCEPTABLE, - ( - api::ErrorCode::InvalidPassword, - "`password` length was expected to be 8 characters or longer", - ), - )); - } - - user.password = Some(hash_password(password).map_err(|_| api::internal_server_error())?); - } - - let mut conn = cx - .pool - .get() - .inspect_err(|e| { - sentry::capture_error(e); - tracing::error!(error = %e, "failed to establish database connection"); - }) - .map_err(|_| api::internal_server_error())?; - - charted_database::connection!(@raw conn { - PostgreSQL(conn) => conn.build_transaction().run(|txn| { - use postgresql::users::{dsl, table}; - - diesel::update(table.filter(dsl::id.eq(user.id))) - .set(user.into_pg()) - .execute(txn) - .map(|_| ()) - }); - - SQLite(conn) => conn.immediate_transaction(|txn| { - use sqlite::users::{dsl, table}; - - diesel::update(table.filter(dsl::id.eq(user.id))) - .set(user.into_sqlite()) - .execute(txn) - .map(|_| ()) - }); - }) - .inspect_err(|e| { - sentry::capture_error(e); - tracing::error!(error = %e, "failed to update user"); - }) - .map_err(|_| api::internal_server_error())?; - - Ok(api::no_content()) -} - -#[utoipa::path( - delete, - - path = "/v1/users/@me", - operation_id = "deleteSelf", - tag = "Users", - responses( - ( - status = 204, - description = "User is scheduled for deletion and will be deleted", - body = EmptyApiResponse, - content_type = "application/json" - ) - ) -)] -pub async fn delete( - State(cx): State, - Extension(Session { user, .. }): Extension, -) -> api::Result<()> { - ops::db::user::delete(cx, user) - .await - .inspect_err(|e| { - sentry_eyre::capture_report(e); - tracing::error!(error = %e, "failed to delete user"); - }) - .map_err(|_| api::internal_server_error())?; - - Ok(api::no_content()) -} diff --git a/crates/server/src/routing/v1/user/repositories.rs b/crates/server/src/routing/v1/user/repositories.rs deleted file mode 100644 index 7c7f6b3b4..000000000 --- a/crates/server/src/routing/v1/user/repositories.rs +++ /dev/null @@ -1,188 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// Lists all the avaliable user repositories. -/// -/// If the user is logged in with credentials, this will also show their private repositories as well. -#[utoipa::path( - get, - path = "/v1/users/{idOrName}/repositories", - operation_id = "listRepositories", - tag = "Repositories" -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn list_user_repositories() {} - -/// Lists all of this user's repositories. -#[utoipa::path( - get, - path = "/v1/users/@me/repositories", - operation_id = "listMyRepositories", - tag = "Repositories" -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn list_self_user_repositories() {} - -/// Creates a repository under this user. -#[utoipa::path( - put, - path = "/v1/users/@me/repositories", - operation_id = "createRepository", - tag = "Repositories" -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn create_user_repository() {} - -/* -/// Retrieve all of a user's repositories. This filters out private ones. -#[controller( - tags("Users", "Repositories"), - response(200, "List of all the user's repositories", ("application/json", response!("RepositoryPaginatedResponse"))), - pathParameter("idOrName", schema!("NameOrSnowflake"), description = "Path parameter that can take a [`Name`] or [`Snowflake`] identifier."), - queryParameter("cursor", snowflake, description = "Cursor to passthrough to proceed into the next or previous page."), - queryParameter("per_page", int32, description = "How many elements should be present in a page."), - queryParameter("order", schema!("OrderBy"), description = "Order to sort the entries by.") -)] -pub async fn list_user_repositories( - State(Instance { controllers, .. }): State, - NameOrSnowflake(nos): NameOrSnowflake, - Query(PaginationQuery { - mut per_page, - cursor, - order, - }): Query, - session: Option>, -) -> Result> { - let owner = match controllers.users.get_by(&nos).await { - Ok(Some(user)) => user, - Ok(None) => { - return Err(err( - StatusCode::NOT_FOUND, - ( - ErrorCode::EntityNotFound, - "user with id or name doesn't exist", - json!({"idOrName":nos}), - ), - )) - } - - Err(_) => return Err(internal_server_error()), - }; - - let list_private_stuff = match session { - Some(Extension(Session { user, .. })) => owner.id == user.id, - None => false, - }; - - if per_page > 100 { - return Err(err( - StatusCode::NOT_ACCEPTABLE, - ( - ErrorCode::MaxPerPageExceeded, - "`per_page` query parameter can't go over 100 entries", - json!({"perPage": per_page}), - ), - )); - } - - per_page = cmp::min(10, per_page); - controllers - .repositories - .paginate(PaginationRequest { - list_private_stuff, - owner_id: Some(owner.id.try_into().unwrap()), - order_by: order, - per_page, - cursor, - metadata: azalia::hashmap!(), - }) - .await - .map(|data| ok(StatusCode::OK, data)) - .map_err(|_| internal_server_error()) -} - -/// Create a repository with the current authenticated user as the owner of the repository -#[controller( - method = put, - tags("Repositories"), - requestBody("Payload for creating a repository", ("application/json", schema!("CreateRepositoryPayload"))), - response(201, "Repository created", ("application/json", response!("RepositoryResponse"))), - response(400, "Bad Request", ("application/json", response!("ApiErrorResponse"))), - response(409, "Conflict: repository with that name already exists on the user's account", ("application/json", response!("ApiErrorResponse"))), - response(500, "Internal Server Error", ("application/json", response!("ApiErrorResponse"))) -)] -pub async fn create_user_repository( - State(Instance { - controllers, - snowflake, - pool, - .. - }): State, - Extension(Session { user, .. }): Extension, - Json(payload): Json, -) -> Result { - validate(&payload, CreateRepositoryPayload::validate)?; - - match sqlx::query_as::( - "select repositories.id from repositories where name = $1 and owner = $2;", - ) - .bind(&payload.name) - .bind(user.id) - .fetch_optional(&pool) - .await - { - Ok(None) => {} - Ok(Some(_)) => { - return Err(err( - StatusCode::CONFLICT, - ( - ErrorCode::EntityAlreadyExists, - "repository with given name already exists on your account", - json!({"name":payload.name}), - ), - )) - } - - Err(e) => { - error!(error = %e, user.id, %payload.name, "unable to find a user repository with name"); - sentry::capture_error(&e); - - return Err(internal_server_error()); - } - } - - let id = snowflake.generate(); - let now = Local::now(); - let repo = Repository { - description: payload.description.clone(), - created_at: now, - updated_at: now, - private: payload.private, - r#type: payload.r#type, - owner: user.id, - name: payload.name.clone(), - id: i64::try_from(id.value()).unwrap(), - - ..Default::default() - }; - - controllers - .repositories - .create(payload, &repo) - .await - .map(|_| ok(StatusCode::CREATED, repo)) - .map_err(|_| internal_server_error()) -} -*/ diff --git a/crates/server/src/routing/v1/user/sessions.rs b/crates/server/src/routing/v1/user/sessions.rs deleted file mode 100644 index 52aaccae6..000000000 --- a/crates/server/src/routing/v1/user/sessions.rs +++ /dev/null @@ -1,253 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#[utoipa::path( - post, - path = "/v1/users/@me/sessions", - operation_id = "login", - tag = "Users/Sessions" -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn login() {} - -#[utoipa::path( - delete, - path = "/v1/users/@me/sessions", - operation_id = "logout", - tag = "Users/Sessions" -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn logout() {} - -#[utoipa::path( - post, - path = "/v1/users/@me/sessions/refresh", - operation_id = "refreshSessionToken", - tag = "Users/Sessions" -)] -#[cfg_attr(debug_assertions, axum::debug_handler)] -pub async fn refresh_session_token() {} - -/* -use crate::{ - authz, db::controllers::DbController, openapi::generate_response_schema, server::validation::validate, - sessions::Session, Instance, -}; -use axum::{extract::State, http::StatusCode, Extension}; -use charted_entities::payloads::UserLoginPayload; -use charted_server::{controller, err, extract::Json, internal_server_error, ok, ErrorCode, Result}; -use serde_json::json; -use sqlx::Postgres; -use validator::Validate; - -pub struct SessionResponse; -generate_response_schema!(SessionResponse, schema = "Session"); - -/// Creates a new session and returns details about the newly created session. -#[controller( - method = post, - tags("Users", "Sessions"), - requestBody("Payload for creating a new user. `password` can be empty if the server's session manager is not Local", ("application/json", schema!("UserLoginPayload"))), - response(201, "Successful response", ("application/json", response!("SessionResponse"))), - response(400, "Invalid payload received.", ("application/json", response!("ApiErrorResponse"))), - response(403, "Invalid password received", ("application/json", response!("ApiErrorResponse"))), - response(404, "Unknown User", ("application/json", response!("ApiErrorResponse"))), - response(500, "Internal Server Error", ("application/json", response!("ApiErrorResponse"))) -)] -pub async fn login( - State(Instance { - controllers, - sessions, - authz, - pool, - .. - }): State, - Json(payload): Json, -) -> Result { - validate(&payload, UserLoginPayload::validate)?; - - let user = match (payload.username, payload.email) { - (Some(ref username), None) => match controllers.users.get_by(username).await { - Ok(Some(user)) => user, - Ok(None) => { - return Err(err( - StatusCode::NOT_FOUND, - ( - ErrorCode::EntityNotFound, - "user with username doesn't exist", - json!({"username": username}), - ), - )) - } - - Err(_) => return Err(internal_server_error()), - }, - - (None, Some(ref email)) => match sqlx::query_as::("select users.* from users where email = $1;") - .bind(email) - .fetch_optional(&pool) - .await - { - Ok(Some(user)) => user, - Ok(None) => { - return Err(err( - StatusCode::NOT_FOUND, - ( - ErrorCode::EntityNotFound, - "user with username doesn't exist", - json!({"email": email}), - ), - )) - } - - Err(e) => { - error!(error = %e, "unable to query user by email"); - sentry::capture_error(&e); - - return Err(internal_server_error()); - } - }, - - (Some(_), Some(_)) => { - return Err(err( - StatusCode::BAD_REQUEST, - ( - ErrorCode::InvalidJsonPayload, - "`username` and `email` are mutually exclusive", - ), - )) - } - - (None, None) => { - return Err(err( - StatusCode::BAD_REQUEST, - ( - ErrorCode::InvalidJsonPayload, - "either `username` or `email` needs to be available", - ), - )) - } - }; - - // check if we can authenticate - authz - .authenticate(user.clone(), payload.password) - .await - .map_err(|e| match e { - authz::Error::InvalidPassword => err( - StatusCode::FORBIDDEN, - (ErrorCode::InvalidPassword, "password given was not correct"), - ), - - authz::Error::Eyre(e) => { - error!(error = %e, user.id, "unable to complete authentication from authz backend"); - sentry_eyre::capture_report(&e); - - internal_server_error() - } - - authz::Error::Ldap(e) => { - error!(error = %e, user.id, "unable to complete authentication from LDAP authz backend"); - sentry::capture_error(&e); - - internal_server_error() - } - })?; - - let mut sessions = sessions.lock().await; - let session = sessions.create(user).await.map_err(|_| internal_server_error())?; - sessions.create_task(session.session, std::time::Duration::from_secs(604800)); - - Ok(ok(StatusCode::CREATED, session)) -} - -/// Destroy the current authenticated session. -#[controller( - method = delete, - tags("Users", "Sessions"), - response(201, "Session was deleted successfully", ("application/json", response!("EmptyApiResponse"))), - response(403, "If the authenticated user didn't provide a session token", ("application/json", response!("ApiErrorResponse"))), - response(500, "Internal Server Error", ("application/json", response!("ApiErrorResponse"))) -)] -pub async fn destroy_session( - State(Instance { sessions, .. }): State, - Extension(crate::server::middleware::session::Session { session, .. }): Extension< - crate::server::middleware::session::Session, - >, -) -> Result<()> { - let Some(session) = session else { - return Err(err( - StatusCode::FORBIDDEN, - ( - ErrorCode::SessionOnlyRoute, - "this REST route requires only session tokens to be used", - json!({"method": "delete", "uri": "/users/sessions/logout"}), - ), - )); - }; - - let mut mgr = sessions.lock().await; - mgr.kill(session.session) - .map(|_| ok(StatusCode::ACCEPTED, ())) - .map_err(|e| { - sentry_eyre::capture_report(&e); - internal_server_error() - }) -} - -/// Refresh a session with the given refresh token upon creation. -#[controller( - method = post, - tags("Users", "Sessions"), - response(201, "Session was fully restored with a new one", ("application/json", response!("SessionResponse"))), - response(403, "If the authenticated user didn't provide a refresh token", ("application/json", response!("ApiErrorResponse"))), - response(500, "Internal Server Error", ("application/json", response!("ApiErrorResponse"))) -)] -pub async fn refresh_session_token( - State(Instance { sessions, .. }): State, - Extension(crate::server::middleware::session::Session { session, user, .. }): Extension< - crate::server::middleware::session::Session, - >, -) -> Result { - let Some(session) = session else { - return Err(err( - StatusCode::FORBIDDEN, - ( - ErrorCode::SessionOnlyRoute, - "this REST route requires only session tokens to be used", - json!({"method": "post", "uri": "/users/sessions/refresh"}), - ), - )); - }; - - let mut mgr = sessions.lock().await; - mgr.kill(session.session).map_err(|e| { - sentry_eyre::capture_report(&e); - internal_server_error() - })?; - - // create a new session since the old one is destroyed - mgr.create(user) - .await - .map(|sess| { - mgr.create_task(sess.session, std::time::Duration::from_secs(604800)); - ok(StatusCode::CREATED, sess) - }) - .map_err(|e| { - sentry_eyre::capture_report(&e); - internal_server_error() - }) -} -*/ diff --git a/crates/server/src/state.rs b/crates/server/src/state.rs deleted file mode 100644 index ec60cb645..000000000 --- a/crates/server/src/state.rs +++ /dev/null @@ -1,62 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use axum::extract::FromRef; -use charted_features::Feature; -use std::sync::{Arc, OnceLock}; - -static INSTANCE: OnceLock = OnceLock::new(); - -/// Represents the context of the API server. -/// -/// It extends the [`charted_app::Context`] object which also holds the -/// list of features that extend the functionality of the `charted_server` -/// crate. -#[derive(Clone, derive_more::Deref)] -pub struct ServerContext { - #[deref] - inner: charted_app::Context, - - pub features: Vec>, -} - -impl ServerContext { - pub(crate) fn new(cx: charted_app::Context, features: Vec>) -> ServerContext { - ServerContext { inner: cx, features } - } -} - -impl ServerContext { - /// Return a reference to the global [`ServerContext`] that was set from [`set_global`]. If - /// [`set_global`] isn't called, then this will panic. - pub fn get<'ctx>() -> &'ctx ServerContext { - INSTANCE.get().expect("global server context was never initialized") - } -} - -impl FromRef<()> for ServerContext { - fn from_ref(_: &()) -> Self { - INSTANCE - .get() - .cloned() - .expect("global server context was never initialized") - } -} - -pub(crate) fn set_global(ctx: ServerContext) { - if INSTANCE.set(ctx).is_err() { - panic!("a global server context has been set already"); - } -} diff --git a/crates/server/src/test.rs b/crates/server/src/test.rs deleted file mode 100644 index 55da52d71..000000000 --- a/crates/server/src/test.rs +++ /dev/null @@ -1,80 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::ServerContext; -use charted_config::{ - database::{self, sqlite}, - logging, server, - sessions::{self, Backend}, - storage, Config, -}; -use std::path::{Path, PathBuf}; -use tempfile::TempDir; -use tracing::Level; - -const JWT_SECRET_KEY: &str = "pleasedontbedumbandsetthisasyourjwtsecretkeyuseopensslinsteadlol"; - -pub fn create_config(dir: &Path, modifications: &[F]) -> Config { - let mut config = Config { - base_url: Some("http://localhost:3651".parse().unwrap()), - registrations: true, - jwt_secret_key: String::from(JWT_SECRET_KEY), - sentry_dsn: None, - single_user: true, - single_org: false, - - database: database::Config::SQLite(sqlite::Config { - run_migrations: true, - max_connections: 2, - db_path: PathBuf::from(":memory:"), - }), - - storage: storage::Config::Filesystem(azalia::remi::fs::StorageConfig { - directory: dir.join("data"), - }), - - logging: logging::Config { - level: Level::INFO, - json: false, - }, - - sessions: sessions::Config { - backend: Backend::Local, - enable_basic_auth: true, - }, - - server: server::Config { - host: "0.0.0.0".into(), - port: 3651, - ssl: None, - }, - }; - - for m in modifications { - m(&mut config); - } - - config -} - -pub async fn create_server_context(cfgs: &[impl Fn(&mut Config)]) -> eyre::Result<(TempDir, ServerContext)> { - let tmpdir = tempfile::tempdir().expect("temporary directory should be present"); - let config = create_config(tmpdir.path(), cfgs); - - Ok(( - tmpdir, - ServerContext::new(charted_app::Context::new(config).await?, vec![]), - )) -} diff --git a/crates/helm-plugin/src/cmds/auth/token.rs b/crates/server/src/testing.rs similarity index 100% rename from crates/helm-plugin/src/cmds/auth/token.rs rename to crates/server/src/testing.rs diff --git a/crates/server/src/types.rs b/crates/server/src/types.rs deleted file mode 100644 index 6b7910866..000000000 --- a/crates/server/src/types.rs +++ /dev/null @@ -1,104 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use charted_types::{name::Name, Ulid}; -use serde::{Deserialize, Serialize}; -use std::fmt::Display; -use utoipa::ToSchema; - -/// `NameOrUlid` is a "union" enum that can represent either: -/// -/// * [`Name`][charted_types::name::Name] -/// * [`Ulid`][charted_types::Ulid] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)] -#[serde(untagged)] -pub enum NameOrUlid { - Ulid(charted_types::Ulid), - Name(charted_types::name::Name), -} - -impl Display for NameOrUlid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Ulid(ulid) => Display::fmt(ulid, f), - Self::Name(name) => Display::fmt(name, f), - } - } -} - -impl NameOrUlid { - /// Returns [`Some`]\([`Name`]\) that was referenced, otherwise `None` is returned - /// if this is a ULID instance. - pub fn as_name(&self) -> Option<&Name> { - match self { - Self::Name(name) => Some(name), - _ => None, - } - } - - /// Returns [`Some`]\([`Ulid`]\) that was referenced, otherwise `None` is returned - /// if this is a [`Name`]. - pub fn as_ulid(&self) -> Option<&Ulid> { - match self { - Self::Ulid(ulid) => Some(ulid), - _ => None, - } - } -} - -impl From for NameOrUlid { - fn from(value: Name) -> Self { - Self::Name(value) - } -} - -impl From for NameOrUlid { - fn from(value: Ulid) -> Self { - Self::Ulid(value) - } -} - -impl From for NameOrUlid { - fn from(_: !) -> Self { - unimplemented!() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_deserialize_name_or_ulid() { - // Safety: this passes all the validation it requires - let x = unsafe { Name::new_unchecked("noel") }; - let deserialized = serde_json::from_str::("\"noel\""); - assert_eq!(deserialized.expect("shouldn't happen"), NameOrUlid::Name(x)); - - let x = Ulid::new("01J647WVTPF2W5W99H5MBT0YQE").expect("failed to parse as ulid"); - let deserialized = serde_json::from_str::(&format!("\"{x}\"")); - assert_eq!(deserialized.expect("shouldn't happen"), NameOrUlid::Ulid(x)); - - // this could be considered an edge-case since names without `-`, `_`, or `~` - // can be considered "valid" ulids. so, let's see what happens? - assert_eq!( - serde_json::from_str::("\"some3name1with6numbers\"").unwrap(), - NameOrUlid::Name(unsafe { - /* Safety: this passes all the validation it requires */ - Name::new_unchecked("some3name1with6numbers") - }) - ); - } -} diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index 63cc42d24..c7dac8390 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -15,30 +15,40 @@ [package] name = "charted-types" +description = "🐻‍❄️📦 Generic crate that holds all database entity types and newtype wrappers." version.workspace = true documentation.workspace = true edition.workspace = true homepage.workspace = true license.workspace = true -publish.workspace = true +publish = true repository.workspace = true authors.workspace = true +rust-version.workspace = true + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(noeldoc)'] } [features] default = [] -jsonschema = ["dep:schemars", "schemars?/semver", "schemars?/url"] + +jsonschema = ["dep:schemars"] +openapi = ["dep:utoipa"] + +__internal_db = ["dep:sea-orm"] [dependencies] -azalia = { workspace = true, features = ["config"] } chrono.workspace = true -charted-core = { version = "0.1.0", default-features = false, path = "../core" } -charted-database = { version = "0.1.0", path = "../database" } -derive_more = { workspace = true, features = ["display"] } -diesel.workspace = true +charted-core.workspace = true +derive_more = { workspace = true, features = ["display", "from", "deref"] } paste = "1.0.15" schemars = { workspace = true, optional = true } +sea-orm = { workspace = true, optional = true } semver.workspace = true serde.workspace = true serde_json.workspace = true -ulid = { version = "1.1.3", features = ["serde"] } -utoipa.workspace = true +ulid = { version = "1.1.4", features = ["serde"] } +utoipa = { workspace = true, optional = true } + +[package.metadata.docs.rs] +features = ["jsonschema", "openapi"] diff --git a/crates/types/src/db.rs b/crates/types/src/db.rs deleted file mode 100644 index 6b08eb0f3..000000000 --- a/crates/types/src/db.rs +++ /dev/null @@ -1,589 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::{helm::ChartType, name::Name, util, DateTime, Ulid, Version}; -use charted_core::bitflags::ApiKeyScopes; -use diesel::prelude::*; -use serde::Serialize; -use utoipa::ToSchema; - -#[derive(Debug, Clone, Serialize, ToSchema, Queryable, Insertable)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite, diesel::pg::Pg))] -#[diesel(table_name = charted_database::schema::postgresql::users)] -#[diesel(table_name = charted_database::schema::sqlite::users)] -pub struct User { - /// whether or not if this user is considered a verified publisher. - #[schema(read_only)] - #[serde(default)] - pub verified_publisher: bool, - - /// whether or not if the user prefers to use their Gravatar email - /// as their profile picture. - #[schema(read_only)] - #[serde(default)] - pub prefers_gravatar: bool, - - /// Email address that is the Gravatar email to which we should use the user's avatar. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub gravatar_email: Option, - - /// Short description about this user. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub description: Option, - - /// Unique hash that identifies the user's avatar that they uploaded via the REST API. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub avatar_hash: Option, - - /// Date of when this user was created. This uses the host system's local time instead - /// of UTC. - #[schema(read_only, value_type = DateTime)] - pub created_at: DateTime, - - /// Date of when the server has last updated this user's metadata - #[schema(read_only, value_type = DateTime)] - pub updated_at: DateTime, - - /// Name of this user that can be identified easier. - pub username: Name, - - #[serde(skip)] - pub password: Option, - - #[serde(skip)] - pub email: String, - - /// Whether if this User is an Administrator of this instance - #[serde(default)] - #[schema(read_only)] - pub admin: bool, - - /// Display name for this user, it should be displayed as '{name} (@{username})' or just '@{username}' if there is no display name - #[serde(default)] - pub name: Option, - - /// Unique identifier to locate this user via the REST API. - pub id: Ulid, -} - -util::selectable!(users for User => [ - verified_publisher: bool, - prefers_gravatar: bool, - gravatar_email: Option, - description: Option, - avatar_hash: Option, - created_at: DateTime, - updated_at: DateTime, - username: Name, - password: Option, - email: String, - admin: bool, - name: Option, - id: Ulid -]); - -crate::mk_db_based_types!(users for User => [ - verified_publisher: bool, - prefers_gravatar: bool, - gravatar_email: Option, - description: Option, - avatar_hash: Option, - created_at: DateTime, - updated_at: DateTime, - username: Name, - password: Option, - email: String, - admin: bool, - name: Option, - id: Ulid -]); - -#[derive(Debug, Clone, Serialize, ToSchema, Queryable, Insertable)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite, diesel::pg::Pg))] -#[diesel(table_name = charted_database::schema::postgresql::user_connections)] -#[diesel(table_name = charted_database::schema::sqlite::user_connections)] -pub struct UserConnections { - /// Snowflake ID that was sourced from [Noelware's Accounts System](https://accounts.noelware.org) - #[serde(default)] - pub noelware_account_id: Option, - - /// Account ID that was sourced from Google OAuth2 - #[serde(default)] - pub google_account_id: Option, - - /// Account ID that was sourced from GitHub OAuth2. This can differ from - /// GitHub (https://github.com) and GitHub Enterprise usage. - #[serde(default)] - pub github_account_id: Option, - - /// Date of when this entity was created. In most cases, this will be mere milliseconds - /// or seconds to when a [`User`] is created. - #[schema(read_only)] - pub created_at: DateTime, - - /// Last timestamp of when the API server has modified this entity. - pub updated_at: DateTime, - - /// Unique identifier of this entity. - #[schema(read_only)] - pub id: Ulid, -} - -util::selectable!(user_connections for UserConnections => [ - noelware_account_id: Option, - google_account_id: Option, - github_account_id: Option, - created_at: DateTime, - updated_at: DateTime, - id: Ulid -]); - -crate::mk_db_based_types!(user_connections for UserConnections => [ - noelware_account_id: Option, - google_account_id: Option, - github_account_id: Option, - created_at: DateTime, - updated_at: DateTime, - id: Ulid -]); - -#[derive(Debug, Clone, Serialize, ToSchema, Queryable, Insertable)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite, diesel::pg::Pg))] -#[diesel(table_name = charted_database::schema::postgresql::repositories)] -#[diesel(table_name = charted_database::schema::sqlite::repositories)] -pub struct Repository { - /// Short description about this user, can be `null` if none was provided. - #[serde(default)] - pub description: Option, - - /// whether if this repository is deprecated or not. - #[serde(default)] - pub deprecated: bool, - - /// Timestamp of when this entity was created. - #[schema(read_only)] - pub created_at: DateTime, - - /// Timestamp of when the API server has last updated this entity. - pub updated_at: DateTime, - - /// Hash identifier for the repository's icon, if one was uploaded. - #[serde(default)] - #[schema(read_only)] - pub icon_hash: Option, - - /// The "creator" of the repository. This will return `null` if the - /// owner is already a [`User`], otherwise, this will point to the - /// user's ID that made the repository under the organization. - #[serde(default)] - #[schema(read_only)] - pub creator: Option, - - /// whether if the repository is private and only its members can view it. - #[serde(default)] - pub private: bool, - - /// The owner of the repository. This will return either a [`User`] or [`Organization`] - /// identifier. - #[schema(read_only)] - pub owner: Ulid, - - /// Name of the repository. - pub name: Name, - - /// What kind of chart this repository is. - #[serde(rename = "type")] - pub type_: ChartType, - - /// Unique identifier of this entity. - #[schema(read_only)] - pub id: Ulid, -} - -util::selectable!(repositories for Repository => [ - description: Option, - deprecated: bool, - created_at: DateTime, - updated_at: DateTime, - icon_hash: Option, - creator: Option, - private: bool, - owner: Ulid, - name: Name, - type_: ChartType, - id: Ulid -]); - -crate::mk_db_based_types!(repositories for Repository => [ - description: Option, - deprecated: bool, - created_at: DateTime, - updated_at: DateTime, - icon_hash: Option, - creator: Option, - private: bool, - owner: Ulid, - name: Name, - type_: ChartType, - id: Ulid -]); - -/// Represents a resource that contains a release from a [Repository] release. Releases -/// are a way to group releases of new versions of Helm charts that can be easily -/// fetched from the API server. -/// -/// Any repository can have an unlimited amount of releases, but tags cannot clash -/// into each other, so the API server will not accept it. Each tag should be -/// a SemVer 2 comformant string, parsing is related to how Cargo evaluates SemVer 2 tags. -#[derive(Debug, Clone, Serialize, ToSchema, Queryable, Insertable)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite, diesel::pg::Pg))] -#[diesel(table_name = charted_database::schema::postgresql::repository_releases)] -#[diesel(table_name = charted_database::schema::sqlite::repository_releases)] -pub struct RepositoryRelease { - /// Markdown-formatted string that contains a changelog of this release. - #[serde(default)] - pub update_text: Option, - - /// Repository that owns this release - #[schema(read_only)] - pub repository: Ulid, - - /// Date of when this release was registered to this instance - #[schema(read_only)] - pub created_at: DateTime, - - /// Date of when the server has last updated this repository release - #[schema(read_only)] - pub updated_at: DateTime, - - /// SemVer 2 comformant string that represents this tag. - #[schema(read_only)] - pub tag: Version, - - /// Unique identifier to locate this repository release resource from the API. - #[schema(read_only)] - pub id: Ulid, -} - -util::selectable!(repository_releases for RepositoryRelease => [ - update_text: Option, - repository: Ulid, - created_at: DateTime, - updated_at: DateTime, - tag: Version, - id: Ulid -]); - -crate::mk_db_based_types!(repository_releases for RepositoryRelease => [ - update_text: Option, - repository: Ulid, - created_at: DateTime, - updated_at: DateTime, - tag: Version, - id: Ulid -]); - -macro_rules! create_member_struct { - ($name:ident -> $table:ident) => { - paste::paste! { - #[doc = "Resource that correlates to a " $name:lower " member entity."] - #[derive(Debug, Clone, Serialize, ToSchema, Queryable, Insertable)] - #[diesel(check_for_backend(diesel::sqlite::Sqlite, diesel::pg::Pg))] - #[diesel(table_name = charted_database::schema::postgresql::$table)] - #[diesel(table_name = charted_database::schema::sqlite::$table)] - pub struct [<$name Member>] { - /// Display name for this member. - /// - /// This should be formatted as `{display_name} (@{username})` where: - /// - `{display_name}` is this property - /// - `{username}` is the user's username. - /// - /// If a display name is not visible, then using `@{username}` is also possible. - pub display_name: Option, - - /// Bitfield value of this member's permissions. - pub permissions: i64, - - /// Date-time of when this member resource was last updated by the API server. - #[schema(read_only)] - pub updated_at: DateTime, - - /// Date-time of when this member resource was created by the API server. - #[schema(read_only)] - pub joined_at: DateTime, - - /// [User] resource that this member is. - #[schema(read_only)] - pub account: Ulid, - - /// Unique identifier to locate this member with the API - #[schema(read_only)] - pub id: Ulid, - } - - impl [<$name Member>] { - #[doc = "Creates a [`MemberPermissions`][::charted_core::bitflags::MemberPermissions] for"] - #[doc = "this " $name:lower " member."] - pub fn bitfield(&self) -> ::charted_core::bitflags::MemberPermissions { - ::charted_core::bitflags::MemberPermissions::new(self.permissions.try_into().expect("cannot convert to u64")) - } - } - - $crate::util::selectable!($table for [<$name Member>] => [ - display_name: Option, - permissions: i64, - updated_at: DateTime, - joined_at: DateTime, - account: Ulid, - id: Ulid - ]); - } - }; -} - -create_member_struct!(Repository -> repository_members); - -crate::mk_db_based_types!(repository_members for RepositoryMember => [ - display_name: Option, - permissions: i64, - updated_at: DateTime, - joined_at: DateTime, - account: Ulid, - id: Ulid -]); - -create_member_struct!(Organization -> organization_members); - -crate::mk_db_based_types!(organization_members for OrganizationMember => [ - display_name: Option, - permissions: i64, - updated_at: DateTime, - joined_at: DateTime, - account: Ulid, - id: Ulid -]); - -#[derive(Debug, Clone, Serialize, ToSchema, Queryable, Insertable)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite, diesel::pg::Pg))] -#[diesel(table_name = charted_database::schema::postgresql::organizations)] -#[diesel(table_name = charted_database::schema::sqlite::organizations)] -pub struct Organization { - /// Whether if this Organization is a Verified Publisher or not. - #[serde(default)] - #[schema(read_only)] - pub verified_publisher: bool, - - /// Returns the twitter handle for this organization, if populated. - #[serde(default)] - pub twitter_handle: Option, - - /// Valid email address that points to a Gravatar avatar, or `null` if it shouldn't use one as the primary avatar - #[serde(default)] - pub gravatar_email: Option, - - /// Display name for this organization. It should be formatted as '[{display_name}][Organization::display_name] (@[{name}][Organization::name])' - /// or '@[{name}][Organization::name]'. - #[serde(default)] - pub display_name: Option, - - /// Date of when this organization was registered to this instance - #[schema(read_only)] - pub created_at: DateTime, - - /// Date of when the server has last updated this organization - #[schema(read_only)] - pub updated_at: DateTime, - - /// Unique hash to locate an organization's icon, this also includes the extension that this icon is, i.e, `png`. - #[serde(default)] - pub icon_hash: Option, - - /// Whether this organization is private and only its member can access this resource. - #[serde(default)] - pub private: bool, - - /// User ID that owns this organization - #[schema(read_only)] - pub owner: Ulid, - - /// The name for this organization. - #[schema(read_only)] - pub name: Name, - - /// Unique identifier to locate this organization with the API - #[schema(read_only)] - pub id: Ulid, -} - -util::selectable!(organizations for Organization => [ - verified_publisher: bool, - twitter_handle: Option, - gravatar_email: Option, - display_name: Option, - created_at: DateTime, - updated_at: DateTime, - icon_hash: Option, - private: bool, - owner: Ulid, - name: Name, - id: Ulid -]); - -crate::mk_db_based_types!(organizations for Organization => [ - verified_publisher: bool, - twitter_handle: Option, - gravatar_email: Option, - display_name: Option, - created_at: DateTime, - updated_at: DateTime, - icon_hash: Option, - private: bool, - owner: Ulid, - name: Name, - id: Ulid -]); - -/// A resource for personal-managed API tokens that is created by a User. This is useful -/// for command line tools or scripts that need to interact with charted-server, but -/// the main use-case is for the [Helm plugin](https://charts.noelware.org/docs/helm-plugin/current). -#[derive(Debug, Clone, Serialize, ToSchema, Queryable, Insertable)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite, diesel::pg::Pg))] -#[diesel(table_name = charted_database::schema::postgresql::api_keys)] -#[diesel(table_name = charted_database::schema::sqlite::api_keys)] -pub struct ApiKey { - /// Short description about this API key. - #[serde(default)] - pub description: Option, - - /// Date of when this API key was created - #[schema(read_only, value_type = DateTime)] - pub created_at: DateTime, - - /// Date of when the server has last updated this API key - #[schema(read_only, value_type = DateTime)] - pub updated_at: DateTime, - - /// Date-time of when this API token expires in, `null` can be returned - /// if the token doesn't expire - #[serde(default)] - #[schema(read_only, value_type = DateTime)] - pub expires_in: Option, - - /// The scopes that are attached to this API key resource. - pub scopes: i64, - - /// The token itself. This is never revealed when querying, but only revealed - /// when you create the token. - #[serde(default, skip_serializing_if = "String::is_empty")] - #[schema(read_only)] - pub token: String, - - /// User resource that owns this API key. This is skipped - /// when using the API as this is pretty useless. - #[schema(read_only)] - pub owner: Ulid, - - /// The name of the API key. - #[schema(read_only)] - pub name: Name, - - /// Unique identifer to locate this resource in the API server. - #[schema(read_only)] - pub id: Ulid, -} - -impl ApiKey { - /// Returns a new [`Bitfield`], but the API key scopes are filled in - pub fn bitfield(&self) -> ApiKeyScopes { - ApiKeyScopes::new(self.scopes.try_into().unwrap()) - } - - /// Sanitize the output of [`ApiKey`] when serializing it or else the token will be - /// exposed and we don't want that. :( - pub fn sanitize(self) -> ApiKey { - ApiKey { - token: String::new(), - ..self - } - } -} - -util::selectable!(api_keys for ApiKey => [ - description: Option, - created_at: DateTime, - updated_at: DateTime, - expires_in: Option, - scopes: i64, - token: String, - owner: Ulid, - name: Name, - id: Ulid -]); - -crate::mk_db_based_types!(api_keys for ApiKey => [ - description: Option, - created_at: DateTime, - updated_at: DateTime, - expires_in: Option, - scopes: i64, - token: String, - owner: Ulid, - name: Name, - id: Ulid -]); - -/// Resource that represents a user session present. -#[derive(Debug, Clone, Serialize, ToSchema, Queryable, Insertable)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite, diesel::pg::Pg))] -#[diesel(table_name = charted_database::schema::postgresql::sessions)] -#[diesel(table_name = charted_database::schema::sqlite::sessions)] -pub struct Session { - /// Refresh token to refresh this session. - /// - /// When refreshed, the session will still be alive but `access_token` - /// and this field will be different. - #[serde(default, skip_serializing_if = "String::is_empty")] - pub refresh_token: String, - - /// Access token to access data from the REST service. - #[serde(default, skip_serializing_if = "String::is_empty")] - pub access_token: String, - - /// ULID of the user that owns this session - pub owner: Ulid, - - /// Unique identifier of this session. - pub id: Ulid, -} - -impl Session { - /// Sanitize the `access_token` and `refresh_token` fields so that it can be passed - /// from the user sessions API. - pub fn sanitize(self) -> Session { - Session { - refresh_token: String::new(), - access_token: String::new(), - owner: self.owner, - id: self.id, - } - } -} - -util::selectable!(sessions for Session => [ - refresh_token: String, - access_token: String, - owner: Ulid, - id: Ulid -]); diff --git a/crates/types/src/entities.rs b/crates/types/src/entities.rs new file mode 100644 index 000000000..b46ee0eb7 --- /dev/null +++ b/crates/types/src/entities.rs @@ -0,0 +1,426 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{name::Name, ChartType, DateTime, Ulid, Version}; +use charted_core::bitflags::ApiKeyScopes; +use serde::Serialize; + +/// The baseline entity. +/// +/// Users can manage and create repositories and organizations, be apart +/// of repository & organizations and much more. +#[derive(Debug, Clone, Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct User { + /// Determines whether if this user is a verified publisher or not. + #[cfg_attr(feature = "openapi", schema(read_only))] + #[serde(default)] + pub verified_publisher: bool, + + /// Determines whether or not if this user prefers to use their + /// Gravatar email associated as their profile picture. + #[serde(default)] + pub prefers_gravatar: bool, + + /// Valid email address that points to their Gravatar account. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gravatar_email: Option, + + /// Short and concise description about this user. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + + /// Unique hash by the API server to identify their avatar, if they have uploaded one. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub avatar_hash: Option, + + /// datetime of when this user was created + #[cfg_attr(feature = "openapi", schema(read_only))] + pub created_at: DateTime, + + /// datetime of when this user was last updated + #[cfg_attr(feature = "openapi", schema(read_only))] + pub updated_at: DateTime, + + /// the user's username + pub username: Name, + + /// whether if this user is an administrator of this instance + #[serde(default)] + #[cfg_attr(feature = "openapi", schema(read_only))] + pub admin: bool, + + /// the user's display name. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// the user's unique identifier + #[cfg_attr(feature = "openapi", schema(read_only))] + pub id: Ulid, +} + +/// Connections that a [`User`] is connected to. +/// +/// This allows OIDC implementations of charted's authz system to lookup +/// a user by a unique identifier so the flow is easier. +/// +/// ## Supported Providers +/// - [Noelware](https://account.noelware.org) +/// - [Google](https://google.com) +/// - [GitHub](https://github.com) +/// - [GitLab](https://gitlab.com) +#[derive(Debug, Clone, Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct UserConnections { + /// Account ID (formatted as a [`Ulid`]) that is from [Noelware](https://account.noelware.org). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub noelware_account_id: Option, + + /// Account ID that is from [Google](https://google.com) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub google_account_id: Option, + + /// Account ID that is from [GitHub](https://github.com) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub github_account_id: Option, + + /// Account ID that is from [GitLab](https://gitlab.com) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gitlab_account_id: Option, + + /// datetime of when this object was created. + /// + /// this should be in range of when the [`User`] was created, but + /// it is not 100% a guarantee. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub created_at: DateTime, + + /// datetime of when this object was last modified. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub updated_at: DateTime, + + /// the object's unique identifier + pub id: Ulid, +} + +/// A **Helm** chart that can be associated by a [`User`] or [`Organization`]. +#[derive(Debug, Clone, Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct Repository { + /// Short and concise description about this repository. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + + /// whether or not if this repository is marked **deprecated**. + #[serde(default)] + pub deprecated: bool, + + /// datetime of when this repository was created. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub created_at: DateTime, + + /// datetime of when this repository was last modified. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub updated_at: DateTime, + + /// unique icon hash for the repository generated by the API server. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub icon_hash: Option, + + /// The creator of the repository. + /// + /// This field was added to determine if this is a organization + /// or user repository without having another db enumeration + /// to manage. + /// + /// This can be `null` if this is a user repository as the [`owner`] + /// field will be set to the user that created the repository. This + /// is non-null to the organization member that created the repository as + /// the [`owner`] field will always be the organization. + /// + /// [`owner`]: # + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "openapi", schema(read_only))] + pub creator: Option, + + /// whether or not if this repository is marked **private**. + #[serde(default)] + pub private: bool, + + /// the [`User`] account or [`Organization`] that created this repository. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub owner: Ulid, + + /// representation of this repository. + /// + /// Repositories can also be considered as a library chart and can be + /// pulled from a user or organization's Helm chart index. + pub type_: ChartType, + + /// the name of this repository. + pub name: Name, + + /// the repository's unique identifier. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub id: Ulid, +} + +/// Resource that contains a [`Repository`] release. +/// +/// **Releases** are a way to group new releases of Helm charts that can +/// be easily used and fetched from the API server. +/// +/// Any [`Repository`] can have a number of releases but release tags +/// cannot clash between one and another. All release tags must comply +/// to the [SemVer v2] format. +/// +/// [SemVer v2]: https://semver.org/ +#[derive(Debug, Clone, Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct RepositoryRelease { + /// The "changelog" or update text of this release. + /// + /// This can be formatted into Markdown and applications like + /// [Hoshi] can render the Markdown into HTML. + /// + /// [Hoshi]: https://charts.noelware.org/docs/hoshi/latest + #[serde(default, skip_serializing_if = "Option::is_none")] + pub update_text: Option, + + /// [`Repository`] that owns this release. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub repository: Ulid, + + /// datetime of when this release was created. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub created_at: DateTime, + + /// datetime of when this release was last modified. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub updated_at: DateTime, + + /// whether or not if this release was yanked. + #[serde(default)] + pub yanked: bool, + + /// the title of this release. + /// + /// If no title is provided, then consumers can place their own title. For + /// [Hoshi], this will render to "Release [`{tag}`]" + /// + /// [`{tag}`]: # + /// [Hoshi]: https://charts.noelware.org/docs/hoshi/latest + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option, + + /// the release tag. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub tag: Version, + + /// the release's unique identifier. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub id: Ulid, +} + +/// An organization is a shared resource for users to build, test, and push Helm charts. +#[derive(Debug, Clone, Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct Organization { + /// whether if this organization is a verified publisher + #[serde(default)] + pub verified_publisher: bool, + + /// whether if this organization prefers their Gravatar email as their icon + #[serde(default)] + pub prefers_gravatar: bool, + + /// valid email address that points to their Gravatar account. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gravatar_email: Option, + + /// the organization's display name. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display_name: Option, + + /// datetime of when this organization was created. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub created_at: DateTime, + + /// datetime of when this organization was last modified. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub updated_at: DateTime, + + /// a unique hash generated by the API server to the organization's icon. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "openapi", schema(read_only))] + pub icon_hash: Option, + + /// whether if this organization is marked private + #[serde(default)] + pub private: bool, + + /// reference to the owner of this organization + pub owner: Ulid, + + /// the organization's name + pub name: Name, + + /// the organization's unique identifier. + pub id: Ulid, +} + +/// Resource for personal-managed API tokens that is created by a [`User`]. +/// +/// User API keys are useful for command-line tools or scripts that might need +/// to interact with the API server. +#[derive(Debug, Clone, Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct ApiKey { + /// the api key's display name. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display_name: Option, + + /// short and concise description about this api key. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + + /// datetime of when this api key was created. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub created_at: DateTime, + + /// datetime of when this api key was last modified. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub updated_at: DateTime, + + /// datetime of when this api key should be deleted from the server + /// and can no longer be used. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "openapi", schema(read_only))] + pub expires_in: Option, + + /// the list of permissions that this api key has as a [bitfield] data structure. + /// + /// [bitfield]: https://charts.noelware.org/docs/server/latest/api/reference#bitfield-data-structure + #[serde(default)] + pub scopes: i64, + + /// reference to the [`User`] that owns this api key + pub owner: Ulid, + + /// the name of the api key + pub name: Name, + + /// the api key's unique identifier. + pub id: Ulid, +} + +impl ApiKey { + /// Returns a new [`Bitfield`][charted_core::bitflags::Bitfield] of the + /// avaliable scopes. + pub fn bitfield(&self) -> ApiKeyScopes { + ApiKeyScopes::new(self.scopes.try_into().unwrap()) + } +} + +/// Resource that represents a [`User`] session. +#[derive(Debug, Clone, Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct Session { + /// A token that is used to refresh this session via the [`GET /users/@me/sessions/refresh`] REST endpoint. + /// + /// When this session was refreshed, the session is still alive and can still be used + /// but both the [`refresh_token`] and [`access_token`] fields are different values. + /// + /// [`GET /users/@me/sessions/refresh`]: # + /// [`refresh_token`]: # + /// [`access_token`]: # + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "openapi", schema(read_only))] + pub refresh_token: Option, + + /// The token that is used to send API requests. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "openapi", schema(read_only))] + pub access_token: Option, + + /// reference to the [`User`] that owns this api key + pub owner: Ulid, + + /// the api key's unique identifier. + pub id: Ulid, +} + +impl Session { + /// Sanitizes and removes the `refresh_token` and `access_token` fields. + /// + /// Since the `From` implementation from database queries sets the `refresh_token` + /// and `access_token`, we will need to sanitize the input. + pub fn sanitize(self) -> Session { + Session { + refresh_token: None, + access_token: None, + ..self + } + } +} + +macro_rules! mk_member_struct { + ($name:ident) => { + $crate::__private::paste! { + #[doc = "Resource that correlates to a " $name:lower " member."] + #[derive(Debug, Clone, Serialize)] + #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] + pub struct [<$name Member>] { + /// the member's display name. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display_name: Option, + + /// the permissions that this member has as a [bitfield] data structure. + /// + /// [bitfield]: https://charts.noelware.org/docs/server/latest/api/reference#bitfield-data-structure + #[serde(default)] + pub permissions: u64, + + /// datetime of when this release was last modified. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub updated_at: DateTime, + + #[doc = "datetime of when this member joined this " $name:lower "."] + #[cfg_attr(feature = "openapi", schema(read_only))] + pub joined_at: DateTime, + + /// reference to their user account. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub account: Ulid, + + /// the member's unique identifier. + #[cfg_attr(feature = "openapi", schema(read_only))] + pub id: Ulid, + } + + impl [<$name Member>] { + pub fn bitfield(&self) -> ::charted_core::bitflags::MemberPermissions { + let perms_as_u64: u64 = self.permissions.try_into().unwrap(); + ::charted_core::bitflags::MemberPermissions::new( + perms_as_u64 + ) + } + } + } + }; +} + +mk_member_struct!(Repository); +mk_member_struct!(Organization); diff --git a/crates/types/src/helm.rs b/crates/types/src/helm.rs deleted file mode 100644 index ea59f5424..000000000 --- a/crates/types/src/helm.rs +++ /dev/null @@ -1,388 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::{DateTime, Version, VersionReq}; -use charted_database::schema::sql_types; -use chrono::Utc; -use diesel::{ - deserialize::{FromSql, FromSqlRow}, - expression::AsExpression, - pg::Pg, - serialize::ToSql, - sql_types::{Binary, Text}, - sqlite::Sqlite, -}; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, str::FromStr}; -use utoipa::ToSchema; - -/// The [`apiVersion`] field should be `v2` for Helm charts that require at least Helm 3. -/// -/// Charts supporting previous Helm versions should have an [`apiVersion`] set to v1 and are -/// installable by Helm 3. -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, ToSchema)] -#[serde(rename_all = "lowercase")] -pub enum ChartSpecVersion { - /// Chart supports running on Helm 2 or 3. - V1, - - /// Chart supports running only on Helm 3. - #[default] - V2, -} - -/// Represents what type this chart is. Do note that [`ChartType::Operator`] is not supported -/// by Helm, but specific to the API server, this will be switched to [`ChartType::Application`] -/// when serializing to valid Helm objects -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, ToSchema, FromSqlRow, AsExpression)] -#[serde(rename_all = "lowercase")] -#[diesel(sql_type = sql_types::ChartType)] -#[diesel(sql_type = Text)] -pub enum ChartType { - /// Default chart type and represents a standard chart which can operate on a Kubernetes - /// cluster and spawn in Kubernetes objects. - /// - /// **Note**: Application charts can also act as library charts, just set this to [`Library`][ChartType::Library], - /// and it'll act like a library chart instead. - #[default] - Application, - - /// Library charts provide utilities or functions for building Helm charts, it differs - /// from an [`Application`][ChartType::Application] chart because it cannot create Kubernetes - /// objects from `helm install`. - Library, - - /// Operator is a "non standard" Chart type, and is replaced by "application" when a release is made. - /// - /// This will be replaced with "application" and the `charts.noelware.org/kind: "operator"` - /// annotation will be readily avaliable. - Operator, -} - -impl FromSql for ChartType { - fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { - let bytes = bytes.as_bytes(); - match bytes { - b"application" => Ok(ChartType::Application), - b"library" => Ok(ChartType::Library), - b"operator" => Ok(ChartType::Operator), - v => Err(format!("unknown enum variant: {}", String::from_utf8_lossy(v)).into()), - } - } -} - -impl ToSql for ChartType { - fn to_sql<'b>(&'b self, out: &mut diesel::serialize::Output<'b, '_, Pg>) -> diesel::serialize::Result { - >::to_sql( - match self { - ChartType::Application => "application", - ChartType::Library => "library", - ChartType::Operator => "operator", - }, - out, - ) - } -} - -impl FromSql for ChartType { - fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { - let data = >::from_sql(bytes)?; - match data.as_bytes() { - b"application" => Ok(ChartType::Application), - b"library" => Ok(ChartType::Library), - b"operator" => Ok(ChartType::Operator), - v => Err(format!("unknown enum variant: {}", String::from_utf8_lossy(v)).into()), - } - } -} - -impl ToSql for ChartType { - fn to_sql<'b>(&'b self, out: &mut diesel::serialize::Output<'b, '_, Sqlite>) -> diesel::serialize::Result { - >::to_sql( - match self { - ChartType::Application => "application", - ChartType::Library => "library", - ChartType::Operator => "operator", - }, - out, - ) - } -} - -impl FromSql for ChartType { - fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { - let bytes = as FromSql>::from_sql(bytes)?; - match bytes.as_slice() { - b"application" => Ok(ChartType::Application), - b"library" => Ok(ChartType::Library), - b"operator" => Ok(ChartType::Operator), - v => Err(format!("unknown enum variant: {}", String::from_utf8_lossy(v)).into()), - } - } -} - -impl ToSql for ChartType { - fn to_sql<'b>(&'b self, out: &mut diesel::serialize::Output<'b, '_, Sqlite>) -> diesel::serialize::Result { - >::to_sql( - match self { - ChartType::Application => "application", - ChartType::Library => "library", - ChartType::Operator => "operator", - }, - out, - ) - } -} - -impl FromStr for ChartType { - type Err = String; - fn from_str(s: &str) -> Result { - match &*s.to_ascii_lowercase() { - "application" => Ok(ChartType::Application), - "operator" => Ok(ChartType::Operator), - "library" => Ok(ChartType::Library), - _ => Err(format!("unknown type given: '{s}'")), - } - } -} - -/// ImportValues hold the mapping of source values to parent key to be imported. Each -/// item can be a child/parent sublist item or a string. -#[derive(Debug, Clone, Default, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -pub struct ImportValue { - /// The destination path in the parent chart's values. - pub parent: String, - - /// The source key of the values to be imported - pub child: String, -} - -/// Union enum that can contain a String or a [ImportValue] as the import source -/// for referencing parent key items to be imported. -#[derive(Debug, Clone, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -#[serde(untagged)] -pub enum StringOrImportValue { - /// String that points to a key to be imported. - String(String), - - /// Parent/child sublist item. - ImportValue(ImportValue), -} - -/// In Helm, one chart may depend on any number of other charts. These dependencies can be dynamically linked using the dependencies' -/// field in Chart.yaml or brought in to the charts/ directory and managed manually. The charts required by the current chart are defined -/// as a list in the dependencies field. -#[derive(Debug, Clone, Default, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -pub struct ChartDependency { - /// The name of the chart - pub name: String, - - /// The version of the chart. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub version: Option, - - /// Repository URL or alias that should be used to grab - /// the dependency from. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository: Option, - - /// YAML path that resolves to a boolean to enable or disable charts - /// dynamically. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub condition: Option, - - /// List of tags that can be used to group charts to enable/disable together. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub tags: Vec, - - /// [`ImportValues`][ImportValue] holds the mapping of source values to parent key to be imported. - /// Each item can be a string or pair of child/parent sublist items. - #[serde(default, rename = "import-values", skip_serializing_if = "Vec::is_empty")] - pub import_values: Vec, - - /// Alias that is used to identify a chart. Useful for pointing to the - /// same chart multiple times - #[serde(default, skip_serializing_if = "Option::is_none")] - pub alias: Option, -} - -/// Name and URL/email address combination as a maintainer. [ChartMaintainer::name] can be referenced -/// as a `Name` or a ULID. -#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema, PartialEq, Eq, PartialOrd, Ord)] -pub struct ChartMaintainer { - /// The maintainer's name - pub name: String, - - /// The maintainer's email - #[serde(default, skip_serializing_if = "Option::is_none")] - pub email: Option, - - /// URL for the maintainer - #[serde(default, skip_serializing_if = "Option::is_none")] - pub url: Option, -} - -/// Represents the skeleton of a `Chart.yaml` file. -#[derive(Debug, Clone, ToSchema, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct Chart { - /// The `apiVersion` field should be v2 for Helm charts that require at least Helm 3. Charts supporting previous - /// Helm versions have an apiVersion set to v1 and are still installable by Helm 3. - pub api_version: ChartSpecVersion, - - /// The name of the chart. - pub name: String, - - /// A SemVer 2 conformant version string of the chart. - pub version: Version, - - /// The optional `kubeVersion` field can define SemVer constraints on supported Kubernetes versions. - /// Helm will validate the version constraints when installing the chart and fail if the - /// cluster runs an unsupported Kubernetes version. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub kube_version: Option, - - /// A single-sentence description of this project - #[serde(default, skip_serializing_if = "Option::is_none")] - pub description: Option, - - /// The type of the chart. - #[serde(rename = "type", default)] - pub r#type: ChartType, - - /// A list of keywords about this project. These keywords can be searched - /// via the /search endpoint if it's enabled. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub keywords: Vec, - - /// The URL of this project's homepage. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub home: Option, - - /// A list of URLs to the source code for this project - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub sources: Vec, - - /// In Helm, one chart may depend on any number of other charts. These dependencies can be dynamically linked using the dependencies' - /// field in Chart.yaml or brought in to the charts/ directory and managed manually. The charts required by the current chart are defined as a list - /// in the dependencies field. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub dependencies: Vec, - - /// A list of name and URL/email address combinations for the maintainer(s) - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub maintainers: Vec, - - /// A URL or an SVG or PNG image to be used as an icon - #[serde(default, skip_serializing_if = "Option::is_none")] - pub icon: Option, - - /// Note that the appVersion field is not related to the version field. It is a way of specifying the version of the - /// application. For example, the drupal chart may have an appVersion: "8.2.1", indicating that the version of Drupal - /// included in the chart (by default) is 8.2.1. This field is informational, and has no impact on chart version calculations. - /// - /// Wrapping the version in quotes is highly recommended. It forces the YAML parser to treat the version number as a string. - /// Leaving it unquoted can lead to parsing issues in some cases. For example, YAML interprets 1.0 as a floating point value, - /// and a git commit SHA like 1234e10 as scientific notation. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub app_version: Option, - - /// When managing charts in a Chart Repository, it is sometimes necessary to deprecate a chart. The optional deprecated field - /// in Chart.yaml can be used to mark a chart as deprecated. If the latest version of a chart in the repository is marked - /// as deprecated, then the chart as a whole is considered to be deprecated. - /// - /// The chart name can be later reused by publishing a newer version that is not marked as deprecated. - #[serde(default)] - pub deprecated: bool, - - /// Mapping of custom metadata that can be used for custom attributes. Some attributes - /// are regconized for [`Hoshi`] to understand some elements that can be represented - /// in the UI: - /// - /// * `charts.noelware.org/maintainers` ~ a YAML sequence of available maintainers, must be prefixed - /// with `user:` for a user and `org:` for an organization that maintains the Helm chart. - /// - /// ```yaml - /// annotations: - /// charts.noelware.org/maintainers: |- - /// - user:noel - /// - org:noelware - /// ``` - /// - /// * `charts.noelware.org/images` ~ YAML sequence of the Docker images that the chart will install. This - /// is used in Hoshi to allow to go to the registry that owns the Docker image. - /// - /// ```yaml - /// charts.noelware.org/images: |- - /// # maps to `hub.docker.com/r/charted/server` - /// - charted/server:latest - /// - /// # maps to `docker.elastic.co` - /// - docker.elastic.co/elasticsearch/elasticsearch - /// ``` - /// - /// [`Hoshi`]: https://charts.noelware.org/docs/hoshi/latest - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub annotations: HashMap, -} - -/// Represents the specification for a Chart.yaml-schema from a `index.yaml` reference. -#[derive(Debug, Clone, ToSchema, Serialize, Deserialize, PartialEq, Eq)] -pub struct ChartIndexSpec { - /// The Chart specification itself, this will be flatten when (de)serializing. - #[serde(flatten)] - pub spec: Chart, - - // not documented in Helm source code, so I can't really - // add documentation here. - // - // https://github.com/helm/helm/blob/764557c470533fa57aad99f865c9ff75a64d4163/pkg/repo/index.go#L270-L273 - #[serde(default)] - pub urls: Vec, - - #[serde(default)] - pub created: Option, - - #[serde(default)] - pub removed: bool, - - #[serde(default)] - pub digest: Option, -} - -/// Schema skeleton for a `index.yaml` file, that represents a [`Chart`] index. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ChartIndex { - /// API version for the schema itself, will always be `v1`. - pub api_version: String, - - /// [`DateTime`] constant on when the chart index was generated at, this will not - /// be modified at all. - pub generated: DateTime, - - /// Map of [`ChartIndexSpec`]s for the Helm charts that Helm uses to install a Helm chart. - pub entries: HashMap>, -} - -impl Default for ChartIndex { - fn default() -> ChartIndex { - ChartIndex { - api_version: "v1".into(), - generated: Utc::now().into(), - entries: azalia::hashmap!(), - } - } -} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 059674794..8a731e227 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -13,20 +13,128 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![allow(clippy::too_long_first_doc_paragraph)] -#![feature(decl_macro)] +//! # 🐻‍❄️📦 `charted_types` +//! This crate is just a generic crate that exports all newtype wrappers for the +//! API server and database entities. -//! The `charted-types` crate defines types that can be used within the lifecycle -//! of the API server. +#![cfg_attr(any(noeldoc, docsrs), feature(doc_cfg))] +#![doc(html_logo_url = "https://cdn.floofy.dev/images/trans.png")] +#![doc(html_favicon_url = "https://cdn.floofy.dev/images/trans.png")] -mod db; -pub use db::*; +pub mod name; +pub mod payloads; +mod entities; mod newtypes; + +pub use entities::*; pub use newtypes::*; -pub(crate) mod util; +// Not public API, used by macros in this crate. +#[doc(hidden)] +pub mod __private { + pub use paste::paste; +} -pub mod helm; -pub mod name; -pub mod payloads; +mod helm { + use serde::{Deserialize, Serialize}; + use std::{fmt::Write, str::FromStr}; + + #[cfg(feature = "__internal_db")] + use sea_orm::entity::prelude::*; + + /// Representation of a Helm chart. + #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, derive_more::Display)] + #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] + #[cfg_attr(feature = "__internal_db", derive(EnumIter, DeriveActiveEnum))] + #[cfg_attr( + feature = "__internal_db", + sea_orm( + rs_type = "String", + db_type = "String(StringLen::None)", + rename_all = "lowercase", + enum_name = "chart_type" + ) + )] + pub enum ChartType { + /// The default chart type and represents a standard Helm chart. + /// + /// **Note**: Application charts can also act like library charts! Set `type` to `"library"`. + #[default] + #[display("application")] + Application, + + /// The chart provide utilities or functions for building application Helm charts. + /// + /// Library charts cannot expose Helm templates since it cannot create Kubernetes + /// objects from `helm install` + #[display("library")] + Library, + } + + impl FromStr for ChartType { + type Err = (); + + fn from_str(s: &str) -> Result { + match &*s.to_ascii_lowercase() { + "application" => Ok(ChartType::Application), + "library" => Ok(ChartType::Library), + + _ => Err(()), + } + } + } + + #[cfg(feature = "__internal_db")] + impl Iden for ChartType { + fn unquoted(&self, s: &mut dyn Write) { + s.write_str(match self { + ChartType::Application => "application", + ChartType::Library => "library", + }) + .unwrap(); + } + } +} + +pub use helm::*; + +#[cfg(feature = "__internal_db")] +#[macro_export] +#[doc(hidden)] +macro_rules! cfg_sea_orm { + ($($item:item)*) => { + $($item)* + }; +} + +#[cfg(not(feature = "__internal_db"))] +#[macro_export] +#[doc(hidden)] +macro_rules! cfg_sea_orm { + ($(tt:tt)*) => {}; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! cfg_openapi { + ($($item:item)*) => { + #[cfg(feature = "openapi")] + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "openapi")))] + const _: () = { + $($item)* + }; + }; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! cfg_jsonschema { + ($($item:item)*) => { + #[cfg(feature = "jsonschema")] + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "jsonschema")))] + const _: () = { + $($item)* + }; + }; +} diff --git a/crates/types/src/name.rs b/crates/types/src/name.rs index fc0ce974c..e182894c0 100644 --- a/crates/types/src/name.rs +++ b/crates/types/src/name.rs @@ -13,97 +13,102 @@ // See the License for the specific language governing permissions and // limitations under the License. -use diesel::{ - backend::Backend, - deserialize::{FromSql, FromSqlRow}, - expression::AsExpression, - pg::Pg, - serialize::ToSql, - sql_types::Text, - sqlite::Sqlite, -}; +//! Valid UTF-8 string that can be used for names that can be +//! addressed by the API server. +//! +//! * A **Name** is a wrapper for [`Arc`]<[`str`]> as opposed of +//! a [`String`] since a **Name** can be never modified and reflected +//! on the database. +//! +//! * A **Name** is also URL-encoded safe since we only use alphanumeric characters, +//! `-`, `_`, and `~`. +//! +//! * A **Name** can never overflow since we require names to have a minimum +//! length of 2 and a maximum length of 32. + +use crate::{cfg_jsonschema, cfg_openapi}; use serde::{Deserialize, Serialize}; -use std::{borrow::Cow, fmt::Display, ops::Deref, str::FromStr, sync::Arc}; -use utoipa::{ - openapi::{schema::SchemaType, ObjectBuilder, RefOr, Schema, Type}, - PartialSchema, ToSchema, -}; +use std::{borrow::Cow, ops::Deref, str::FromStr, sync::Arc}; -#[cfg(feature = "jsonschema")] -use schemars::{gen::*, schema::*, JsonSchema}; +const MAX_LENGTH: usize = 32; +const MIN_LENGTH: usize = 2; -#[derive(Debug)] +/// Error type when name validation goes wrong. +#[derive(Debug, derive_more::Display)] pub enum Error { - /// When a name was over 32 characters. The first element is how many characters - /// it surpassed. - ExceededMaximumLength(usize), + #[display("name was over 32 characters")] + ExceededLength, - /// Variant where the name was empty. - Empty, + #[display("minimum length is lower or equal to 2.")] + Minimum, - /// Variant that the given input had an invalid character. - InvalidChar { - /// Input that was given + #[display("invalid character '{}' received (index {} in input: \"{}\")", ch, at, input)] + InvalidCharacter { input: Cow<'static, str>, - - /// Index from the input where it was found. at: usize, - - /// The bad character itself ch: char, }, -} - -impl Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use Error as E; - match self { - E::InvalidChar { input, at, ch } => { - write!(f, "invalid character '{ch}' at {at} from given input: [{input}]") - } - E::ExceededMaximumLength(over) => write!(f, "exceeded {over} characters"), - E::Empty => f.write_str("name cannot be empty"), - } - } + #[display("name cannot be empty")] + Empty, } impl std::error::Error for Error {} -/// Name is a valid UTF-8 string that is used to identify a resource from the REST -/// API in a humane fashion. This is meant to help identify a resource without -/// trying to figure out how to calculate their ID. +/// Valid UTF-8 string that can be used for names that can be +/// addressed by the API server. +/// +/// * A **Name** is a wrapper for [`Arc`]<[`str`]> as opposed of +/// a [`String`] since a **Name** can be never modified and reflected +/// on the database. /// -/// **Name** has a strict ruleset on how it can be parsed: +/// * A **Name** is also URL-encoded safe since we only use alphanumeric characters, +/// `-`, `_`, and `~`. /// -/// * Only UTF-8 strings are valid. -/// * Only alphanumeric characters, `-`, `_`, and `~` are allowed. -/// * They must contain a length of two minimum and 32 maximum. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, AsExpression, FromSqlRow)] -#[diesel(sql_type = Text)] +/// * A **Name** can never overflow since we require names to have a minimum +/// length of 2 and a maximum length of 32. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, derive_more::Display)] +#[display("{}", self.as_str())] pub struct Name(Arc); impl Name { - /// Constructs a [`Name`] instance if `input` follows the ruleset, otherwise - /// `Error` is returned. - pub fn try_new>(input: S) -> Result { - let name = input.as_ref(); + /// Create a new [`Name`] without any input validation. + /// + /// ## Safety + /// We marked this method as `unsafe` since it doesn't do any + /// input validation. This should be only used by unit + /// tests. + pub unsafe fn new_unchecked(v: impl AsRef) -> Name { + Name(Arc::from(v.as_ref())) + } + + /// Returns as a string slice. + pub fn as_str(&self) -> &str { + self.0.as_ref() + } + + /// Create a new [`Name`] object if `v` is valid input. + pub fn try_new(v: impl AsRef) -> Result { + let name = v.as_ref(); if name.is_empty() { return Err(Error::Empty); } - if name.len() > 32 { - let over = name.len() - 32; - return Err(Error::ExceededMaximumLength(over)); + if name.len() <= MIN_LENGTH { + return Err(Error::Minimum); + } + + if name.len() > MAX_LENGTH { + return Err(Error::ExceededLength); } - let lower = name.to_ascii_lowercase(); - for (at, ch) in lower.chars().enumerate() { + let as_lower = name.to_ascii_lowercase(); + for (at, ch) in as_lower.chars().enumerate() { if ch.is_alphanumeric() || ch == '_' || ch == '-' || ch == '~' { continue; } - return Err(Error::InvalidChar { - input: Cow::Owned(lower), + return Err(Error::InvalidCharacter { + input: Cow::Owned(as_lower), at, ch, }); @@ -112,20 +117,6 @@ impl Name { // Safety: validated the user input above Ok(unsafe { Name::new_unchecked(name) }) } - - /// Returns a string slice of the given name. - pub fn as_str(&self) -> &str { - self.0.as_ref() - } - - /// Create a [`Name`] while going through no validation. - /// - /// ## Safety - /// The [`Name::new_unchecked`] method is marked *unsafe* due to giving - /// any user input, which violates the validation contract. - pub unsafe fn new_unchecked>(input: S) -> Name { - Name(Arc::from(input.as_ref())) - } } impl Deref for Name { @@ -137,107 +128,130 @@ impl Deref for Name { impl FromStr for Name { type Err = Error; + fn from_str(s: &str) -> Result { - Name::try_new(s) + Self::try_new(s) } } -impl PartialSchema for Name { - fn schema() -> RefOr { - let object = ObjectBuilder::new() - .schema_type(SchemaType::Type(Type::String)) - .description(Some("Valid UTF-8 string that is used to identify a resource from the REST API in a humane fashion. This is meant to help identify a resource without trying to figure out how to calculate their ID.")) - .pattern(Some(r"^(?[A-z]|-|_|~|\d{0,9}){1,32}$")) - .min_length(Some(1)) - .max_length(Some(32)) - .build(); - - RefOr::T(Schema::Object(object)) +#[cfg(feature = "__internal_db")] +impl Name { + pub fn into_column(col: T) -> sea_orm::sea_query::ColumnDef { + sea_orm::sea_query::ColumnDef::new(col).string_len(32).not_null().take() } } -impl ToSchema for Name { - fn name() -> Cow<'static, str> { - Cow::Borrowed("Name") +#[cfg(feature = "__internal_db")] +const _: () = { + use sea_orm::{ + sea_query::{ArrayType, ColumnType, Value, ValueType, ValueTypeErr}, + ColIdx, DbErr, QueryResult, TryGetError, TryGetable, + }; + use std::any::type_name; + + impl From for Value { + fn from(value: Name) -> Self { + Value::String(Some(Box::new(value.as_str().to_owned()))) + } } -} -impl Display for Name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self) + impl TryGetable for Name { + fn try_get_by(query: &QueryResult, idx: I) -> Result { + let contents = ::try_get_by(query, idx)?; + contents.parse::().map_err(|e| { + TryGetError::DbErr(DbErr::TryIntoErr { + from: type_name::(), + into: type_name::(), + source: Box::new(e), + }) + }) + } } -} -impl Serialize for Name { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(self) - } -} + impl ValueType for Name { + fn try_from(v: Value) -> Result { + let contents = ::try_from(v)?; + contents.parse::().map_err(|_| ValueTypeErr) + } -impl<'de> Deserialize<'de> for Name { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - use serde::de::Error; + fn type_name() -> String { + "Name".to_owned() + } - let value = String::deserialize(deserializer)?; - Name::try_new(value).map_err(D::Error::custom) - } -} + fn array_type() -> ArrayType { + ArrayType::String + } -impl FromSql for Name -where - String: FromSql, -{ - fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { - let name = >::from_sql(bytes)?; - Name::try_new(name).map_err(Into::into) + fn column_type() -> ColumnType { + ColumnType::Char(Some(32)) + } } -} +}; -impl ToSql for Name { - fn to_sql<'b>(&'b self, out: &mut diesel::serialize::Output<'b, '_, Pg>) -> diesel::serialize::Result { - >::to_sql(self.as_str(), out) +cfg_openapi! { + use utoipa::{ + PartialSchema, + ToSchema, + openapi::{ + RefOr, + Schema, + ObjectBuilder, + Type, + + schema::SchemaType, + } + }; + + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "openapi")))] + impl PartialSchema for Name { + fn schema() -> RefOr { + let object = ObjectBuilder::new() + .schema_type(SchemaType::Type(Type::String)) + .description(Some("Valid UTF-8 string that is used to identify a resource from the REST API in a humane fashion. This is meant to help identify a resource without trying to figure out how to calculate their ID.")) + .pattern(Some(r"^(?[A-z]|-|_|~|\d{0,9}){1,32}$")) + .min_length(Some(1)) + .max_length(Some(32)) + .build(); + + RefOr::T(Schema::Object(object)) + } } -} -impl ToSql for Name { - fn to_sql<'b>(&'b self, out: &mut diesel::serialize::Output<'b, '_, Sqlite>) -> diesel::serialize::Result { - >::to_sql(self.as_str(), out) - } + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "openapi")))] + impl ToSchema for Name {} } -#[cfg(feature = "jsonschema")] -impl JsonSchema for Name { - fn is_referenceable() -> bool { - false - } +cfg_jsonschema! { + use schemars::{gen::*, schema::*, JsonSchema}; - fn schema_id() -> Cow<'static, str> { - Cow::Borrowed("charted::types::Name") - } + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "openapi")))] + impl JsonSchema for Name { + fn is_referenceable() -> bool { + false + } - fn schema_name() -> String { - String::from("Name") - } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("charted::types::Name") + } - fn json_schema(_: &mut SchemaGenerator) -> schemars::schema::Schema { - schemars::schema::Schema::Object(SchemaObject { - instance_type: Some(SingleOrVec::Single(InstanceType::String.into())), - string: Some( - StringValidation { - min_length: Some(2), - max_length: Some(32), - pattern: Some("^([A-z]{2,}|[0-9]|_|-)*$".into()), - } - .into(), - ), - - ..Default::default() - }) + fn schema_name() -> String { + String::from("Name") + } + + fn json_schema(_: &mut SchemaGenerator) -> schemars::schema::Schema { + schemars::schema::Schema::Object(SchemaObject { + instance_type: Some(SingleOrVec::Single(InstanceType::String.into())), + string: Some( + StringValidation { + min_length: Some(2), + max_length: Some(32), + pattern: Some("^([A-z]{2,}|[0-9]|_|-)*$".into()), + } + .into(), + ), + + ..Default::default() + }) + } } } diff --git a/crates/cli/src/cmds/admin.rs b/crates/types/src/newtypes.rs similarity index 84% rename from crates/cli/src/cmds/admin.rs rename to crates/types/src/newtypes.rs index 222252a2c..30b126882 100644 --- a/crates/cli/src/cmds/admin.rs +++ b/crates/types/src/newtypes.rs @@ -13,8 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod user; +mod datetime; +mod semver; -/// Administration commands. -#[derive(Debug, Clone, clap::Subcommand)] -pub enum Cmd {} +#[path = "newtypes/ulid.rs"] +mod ulid_; + +pub use datetime::*; +pub use semver::*; +pub use ulid_::{ulid, Ulid}; diff --git a/crates/types/src/newtypes/datetime.rs b/crates/types/src/newtypes/datetime.rs index 607b7a3a8..a2721d601 100644 --- a/crates/types/src/newtypes/datetime.rs +++ b/crates/types/src/newtypes/datetime.rs @@ -13,152 +13,128 @@ // See the License for the specific language governing permissions and // limitations under the License. -use diesel::{ - backend::Backend, - deserialize::{FromSql, FromSqlRow}, - expression::AsExpression, - pg::Pg, - serialize::{self, Output, ToSql}, - sql_types::{Timestamp, Timestamptz, TimestamptzSqlite}, - sqlite::Sqlite, -}; +use crate::{cfg_jsonschema, cfg_openapi}; +use chrono::Utc; use serde::{Deserialize, Serialize}; -use std::borrow::Cow; -use utoipa::{ - openapi::{schema::SchemaType, KnownFormat, ObjectBuilder, RefOr, Schema, SchemaFormat, Type}, - PartialSchema, ToSchema, -}; -charted_core::create_newtype_wrapper!( - /// Newtype wrapper for the [`chrono::DateTime`]<[`chrono::Utc`]> type. - /// - /// The wrapper implements the following types: - /// * [`AsExpression`]<[`TimestamptzSqlite`]> - /// * [`AsExpression`]<[`TimestampTz`]> - /// * [`AsExpression`]<[`Timestamp`]> - /// * [`ToSchema`], [`PartialSchema`] for OpenAPI - #[cfg_attr(feature = "jsonschema", doc = "* [`JsonSchema`][schemasrs::JsonSchema] for JSON schemas")] - #[derive( - Debug, - Clone, - Copy, - Default, - Serialize, - Deserialize, - PartialEq, Eq, - PartialOrd, Ord, - AsExpression, - FromSqlRow - )] - #[diesel(sql_type = TimestamptzSqlite)] - #[diesel(sql_type = Timestamptz)] - #[diesel(sql_type = Timestamp)] - pub DateTime for ::chrono::DateTime<::chrono::Utc>; -); - -charted_core::mk_from_newtype!(from DateTime as chrono::DateTime); - -impl PartialSchema for DateTime { - fn schema() -> RefOr { - let object = ObjectBuilder::new() - .schema_type(SchemaType::Type(Type::String)) - .format(Some(SchemaFormat::KnownFormat(KnownFormat::DateTime))) - .build(); - - RefOr::T(Schema::Object(object)) +/// Newtype wrapper for [`chrono::DateTime`]<[`chrono::Utc`]>. +/// +/// This newtype wrapper implements all the standard library types and more +/// configured by feature flags. +/// +#[cfg_attr( + feature = "openapi", + doc = "* [`utoipa::PartialSchema`], [`utoipa::ToSchema`] (via the `openapi` crate feature)" +)] +#[cfg_attr( + feature = "jsonschema", + doc = "* [`schemars::JsonSchema`] (via the `jsonschema` crate feature)" +)] +/// +/// [`utoipa::PartialSchema`]: https://docs.rs/utoipa/*/utoipa/trait.PartialSchema.html +/// [`utoipa::ToSchema`]: https://docs.rs/utoipa/*/utoipa/trait.ToSchema.html +/// [`schemars::JsonSchema`]: https://docs.rs/schemars/*/utoipa/trait.JsonSchema.html +#[derive( + Debug, + Clone, + Copy, + Default, + Serialize, + Deserialize, + PartialEq, + Eq, + PartialOrd, + Ord, + derive_more::Display, + derive_more::From, + derive_more::Deref, +)] +#[display("{}", self.0)] +pub struct DateTime(chrono::DateTime); + +cfg_openapi! { + use utoipa::{ + openapi::{schema::SchemaType, ObjectBuilder, RefOr, Schema, Type, SchemaFormat, KnownFormat}, + PartialSchema, ToSchema, + }; + + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "openapi")))] + impl PartialSchema for DateTime { + fn schema() -> RefOr { + let object = ObjectBuilder::new() + .schema_type(SchemaType::Type(Type::String)) + .format(Some(SchemaFormat::KnownFormat(KnownFormat::DateTime))) + .build(); + + RefOr::T(Schema::Object(object)) + } } -} -impl ToSchema for DateTime { - fn name() -> Cow<'static, str> { - Cow::Borrowed("DateTime") - } + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "openapi")))] + impl ToSchema for DateTime {} } -#[cfg(feature = "jsonschema")] -impl ::schemars::JsonSchema for DateTime { - fn schema_id() -> ::std::borrow::Cow<'static, str> { - ::std::borrow::Cow::Borrowed("chrono::DateTime") - } - - fn schema_name() -> String { - String::from("DateTime") - } - - fn json_schema(_: &mut ::schemars::gen::SchemaGenerator) -> ::schemars::schema::Schema { - ::schemars::schema::SchemaObject { - instance_type: Some(::schemars::schema::InstanceType::String.into()), - format: Some("date-time".into()), - ..Default::default() +cfg_jsonschema! { + use schemars::{ + JsonSchema, + gen::SchemaGenerator, + schema::{ + Schema, + InstanceType, + SchemaObject } - .into() - } -} + }; + use std::borrow::Cow; -////// ============================ TO SQL ============================ \\\\\\ -impl ToSql for DateTime -where - chrono::DateTime: ToSql, -{ - fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result { - as serialize::ToSql>::to_sql(&self.0, &mut out.reborrow()) - } -} - -impl ToSql for DateTime -where - chrono::DateTime: ToSql, -{ - fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> diesel::serialize::Result { - as serialize::ToSql>::to_sql(&self.0, out) - } -} + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "jsonschema")))] + impl JsonSchema for DateTime { + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("chrono::DateTime") + } -////// ============================ FROM SQL ============================ \\\\\\ -impl FromSql for DateTime -where - chrono::DateTime: FromSql, -{ - fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { - let result: chrono::DateTime = - as FromSql>::from_sql(bytes)?; + fn schema_name() -> String { + String::from("DateTime") + } - Ok(Self(result)) + fn json_schema(_: &mut SchemaGenerator) -> Schema { + SchemaObject { + instance_type: Some(InstanceType::String.into()), + format: Some("date-time".into()), + ..Default::default() + } + .into() + } } } -impl FromSql for DateTime -where - chrono::DateTime: FromSql, -{ - fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { - let result: chrono::DateTime<::chrono::Utc> = - as FromSql>::from_sql(bytes)?; +#[cfg(feature = "__internal_db")] +const _: () = { + use sea_orm::{ + sea_query::{ArrayType, ColumnType, Value, ValueType, ValueTypeErr}, + ColIdx, QueryResult, TryGetError, TryGetable, + }; - Ok(Self(result)) + impl TryGetable for DateTime { + fn try_get_by(query: &QueryResult, idx: I) -> Result { + as TryGetable>::try_get_by(query, idx).map(DateTime) + } } -} -impl FromSql for DateTime -where - chrono::NaiveDateTime: FromSql, -{ - fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { - let datetime = >::from_sql(bytes)?; - let converted = chrono::DateTime::::from_naive_utc_and_offset(datetime, chrono::Utc); + impl ValueType for DateTime { + fn try_from(v: Value) -> Result { + as ValueType>::try_from(v).map(Self) + } - Ok(Self(converted)) - } -} + fn type_name() -> String { + as ValueType>::type_name() + } -impl FromSql for DateTime -where - chrono::NaiveDateTime: FromSql, -{ - fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { - let datetime = >::from_sql(bytes)?; - let converted = chrono::DateTime::::from_naive_utc_and_offset(datetime, chrono::Utc); + fn array_type() -> ArrayType { + as ValueType>::array_type() + } - Ok(Self(converted)) + fn column_type() -> ColumnType { + as ValueType>::column_type() + } } -} +}; diff --git a/crates/types/src/newtypes/mod.rs b/crates/types/src/newtypes/mod.rs deleted file mode 100644 index 7127a061c..000000000 --- a/crates/types/src/newtypes/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -mod datetime; -pub use datetime::*; - -// This fixes the "hidden glob shadow" warning since we have our own -// `ulid` path that exports stuff. -#[path = "ulid.rs"] -mod __ulid; -pub use __ulid::*; - -mod version; -pub use version::*; - -mod version_req; -pub use version_req::*; diff --git a/crates/types/src/newtypes/semver.rs b/crates/types/src/newtypes/semver.rs new file mode 100644 index 000000000..7a37943c6 --- /dev/null +++ b/crates/types/src/newtypes/semver.rs @@ -0,0 +1,307 @@ +// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust +// Copyright 2022-2025 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{cfg_jsonschema, cfg_openapi}; +use serde::{Deserialize, Serialize}; + +/// Newtype wrapper for [`semver::Version`]. +/// +/// This newtype wrapper implements all the standard library types and more +/// configured by feature flags. +/// +#[cfg_attr( + feature = "openapi", + doc = "* [`utoipa::PartialSchema`], [`utoipa::ToSchema`] (via the `openapi` crate feature)" +)] +#[cfg_attr( + feature = "jsonschema", + doc = "* [`schemars::JsonSchema`] (via the `jsonschema` crate feature)" +)] +/// +/// [`utoipa::PartialSchema`]: https://docs.rs/utoipa/*/utoipa/trait.PartialSchema.html +/// [`utoipa::ToSchema`]: https://docs.rs/utoipa/*/utoipa/trait.ToSchema.html +/// [`schemars::JsonSchema`]: https://docs.rs/schemars/*/utoipa/trait.JsonSchema.html +#[derive( + Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord, derive_more::Display, derive_more::From, derive_more::Deref, +)] +pub struct Version(semver::Version); +impl Version { + /// Forwarded method to [`semver::Version::parse`] but replaces + /// all instances of `.x` and `.X` with a zero. + pub fn parse(v: &str) -> Result { + let v = v.trim_start_matches('v').replace(['x', 'X'], "0"); + semver::Version::parse(&v).map(Self) + } +} + +impl<'de> Deserialize<'de> for Version { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + impl serde::de::Visitor<'_> for Visitor { + type Value = Version; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("valid semantic version string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + self.visit_string(v.to_string()) + } + + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + Version::parse(&v).map_err(E::custom) + } + } + + deserializer.deserialize_str(Visitor) + } +} + +#[cfg(test)] +#[test] +fn test_deserialization_of_version() { + assert!(serde_json::from_str::("\"1.2.x\"").is_ok()); + assert!(serde_json::from_str::("\"1.x.x\"").is_ok()); + assert!(serde_json::from_str::("\"1.2.X\"").is_ok()); + assert!(serde_json::from_str::("\"1.X.X\"").is_ok()); +} + +cfg_openapi! { + use serde_json::json; + use utoipa::{ + PartialSchema, + ToSchema, + openapi::{ + RefOr, + Schema, + ObjectBuilder, + Type, + schema::SchemaType + } + }; + + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "openapi")))] + impl PartialSchema for Version { + fn schema() -> RefOr { + let object = ObjectBuilder::new() + .schema_type(SchemaType::Type(Type::String)) + .description(Some("Type that represents a semantic version (https://semver.org).")) + .pattern(Some(r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$")) + .examples([json!("1.2.3")]) + .build(); + + RefOr::T(Schema::Object(object)) + } + } + + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "openapi")))] + impl ToSchema for Version {} +} + +cfg_jsonschema! { + use schemars::JsonSchema; + + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "jsonschema")))] + impl JsonSchema for Version { + fn schema_id() -> ::std::borrow::Cow<'static, str> { + ::schema_id() + } + + fn schema_name() -> String { + ::schema_name() + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + ::json_schema(gen) + } + } +} + +#[cfg(feature = "__internal_db")] +const _: () = { + use sea_orm::{ + sea_query::{ArrayType, ColumnType, Value, ValueType, ValueTypeErr}, + ColIdx, DbErr, QueryResult, TryGetError, TryGetable, + }; + use std::any::type_name; + + impl From for Value { + fn from(v: Version) -> Self { + Value::String(Some(Box::new(v.0.to_string()))) + } + } + + impl TryGetable for Version { + fn try_get_by(query: &QueryResult, idx: I) -> Result { + let contents = ::try_get_by(query, idx)?; + contents.parse::().map(Self).map_err(|e| { + TryGetError::DbErr(DbErr::TryIntoErr { + from: type_name::(), + into: type_name::(), + source: Box::new(e), + }) + }) + } + } + + impl ValueType for Version { + fn try_from(v: Value) -> Result { + let contents = ::try_from(v)?; + contents.parse::().map(Self).map_err(|_| ValueTypeErr) + } + + fn type_name() -> String { + "Version".to_owned() + } + + fn array_type() -> ArrayType { + ArrayType::String + } + + fn column_type() -> ColumnType { + ColumnType::Char(None) + } + } +}; + +/// Newtype wrapper for [`semver::VersionReq`]. +/// +/// This newtype wrapper implements all the standard library types and more +/// configured by feature flags. +/// +#[cfg_attr( + feature = "openapi", + doc = "* [`utoipa::PartialSchema`], [`utoipa::ToSchema`] (via the `openapi` crate feature)" +)] +#[cfg_attr( + feature = "jsonschema", + doc = "* [`schemars::JsonSchema`] (via the `jsonschema` crate feature)" +)] +/// +/// [`utoipa::PartialSchema`]: https://docs.rs/utoipa/*/utoipa/trait.PartialSchema.html +/// [`utoipa::ToSchema`]: https://docs.rs/utoipa/*/utoipa/trait.ToSchema.html +/// [`schemars::JsonSchema`]: https://docs.rs/schemars/*/utoipa/trait.JsonSchema.html +#[derive( + Debug, Clone, Serialize, Deserialize, PartialEq, Eq, derive_more::Display, derive_more::From, derive_more::Deref, +)] +pub struct VersionReq(semver::VersionReq); + +cfg_openapi! { + use serde_json::json; + use utoipa::{ + PartialSchema, + ToSchema, + openapi::{ + RefOr, + Schema, + ObjectBuilder, + Type, + schema::SchemaType + } + }; + + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "openapi")))] + impl PartialSchema for VersionReq { + fn schema() -> RefOr { + let object = ObjectBuilder::new() + .schema_type(SchemaType::Type(Type::String)) + .description(Some( + "A semantic version requirement (https://semver.org) that Helm and charted-server supports", + )) + .examples([json!(">=1.2.3"), json!("~1")]) + .build(); + + RefOr::T(Schema::Object(object)) + } + } + + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "openapi")))] + impl ToSchema for VersionReq {} +} + +cfg_jsonschema! { + use schemars::JsonSchema; + + #[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "jsonschema")))] + impl JsonSchema for VersionReq { + fn schema_id() -> ::std::borrow::Cow<'static, str> { + ::std::borrow::Cow::Borrowed("semver::VersionReq") + } + + fn schema_name() -> String { + String::from("VersionReq") + } + + fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + ::schemars::schema::SchemaObject { + instance_type: Some(::schemars::schema::InstanceType::String.into()), + ..Default::default() + } + .into() + } + } +} + +#[cfg(feature = "__internal_db")] +const _: () = { + use sea_orm::{ + sea_query::{ArrayType, ColumnType, Value, ValueType, ValueTypeErr}, + ColIdx, DbErr, QueryResult, TryGetError, TryGetable, + }; + + impl TryGetable for VersionReq { + fn try_get_by(query: &QueryResult, idx: I) -> Result { + let contents = ::try_get_by(query, idx)?; + contents.parse::().map(Self).map_err(|e| { + TryGetError::DbErr(DbErr::TryIntoErr { + from: ::std::any::type_name::(), + into: ::std::any::type_name::(), + source: Box::new(e), + }) + }) + } + } + + impl ValueType for VersionReq { + fn try_from(v: Value) -> Result { + let contents = ::try_from(v)?; + contents + .parse::() + .map(Self) + .map_err(|_| ValueTypeErr) + } + + fn type_name() -> String { + "VersionReq".to_owned() + } + + fn array_type() -> ArrayType { + ArrayType::String + } + + fn column_type() -> ColumnType { + ColumnType::Char(None) + } + } +}; diff --git a/crates/types/src/newtypes/ulid.rs b/crates/types/src/newtypes/ulid.rs index a300fe78b..170288cd0 100644 --- a/crates/types/src/newtypes/ulid.rs +++ b/crates/types/src/newtypes/ulid.rs @@ -13,116 +13,166 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::borrow::Cow; - -use derive_more::derive::Display; -use diesel::{ - backend::Backend, - deserialize::{FromSql, FromSqlRow}, - expression::AsExpression, - pg::Pg, - serialize::{self, IsNull, Output, ToSql}, - sql_types::Text, - sqlite::Sqlite, -}; +use crate::{cfg_jsonschema, cfg_openapi}; use serde::{Deserialize, Serialize}; -use utoipa::{ - openapi::{schema::SchemaType, ObjectBuilder, RefOr, Schema, Type}, - PartialSchema, ToSchema, -}; -charted_core::create_newtype_wrapper!( - /// Newtype wrapper for the [`ulid::Ulid`] type. - /// - /// This newtype wrapper implements the following traits: - /// * [`AsExpression`]<[`Text`]> - /// * [`FromSql`], [`ToSql`] - /// * [`ToSchema`], [`PartialSchema`] for OpenAPI - #[derive( - Debug, - Clone, - Copy, - Display, - Serialize, - Deserialize, - PartialEq, Eq, - PartialOrd, Ord, - AsExpression, - FromSqlRow - )] - #[diesel(sql_type = Text)] - pub Ulid for ::ulid::Ulid; -); - -charted_core::mk_from_newtype!(from Ulid as ::ulid::Ulid); +/// Newtype wrapper for [`ulid::Ulid`]. +/// +/// This newtype wrapper implements all the standard library types and more +/// configured by feature flags. +/// +#[cfg_attr( + feature = "openapi", + doc = "* [`utoipa::PartialSchema`], [`utoipa::ToSchema`] (via the `openapi` crate feature)" +)] +#[cfg_attr( + feature = "jsonschema", + doc = "* [`schemars::JsonSchema`] (via the `jsonschema` crate feature)" +)] +/// +/// [`ulid::Ulid`]: https://docs.rs/ulid/*/ulid/struct.Ulid.html +/// [`utoipa::PartialSchema`]: https://docs.rs/utoipa/*/utoipa/trait.PartialSchema.html +/// [`utoipa::ToSchema`]: https://docs.rs/utoipa/*/utoipa/trait.ToSchema.html +/// [`schemars::JsonSchema`]: https://docs.rs/schemars/*/utoipa/trait.JsonSchema.html +#[derive( + Debug, + Clone, + Copy, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + derive_more::Display, + derive_more::From, + derive_more::Deref, +)] +pub struct Ulid(::ulid::Ulid); impl Ulid { - /// Forwards to the [`Ulid::from_string`][::ulid::Ulid::from_string] method - /// to create this newtype wrapper. + /// Forwards to the [`Ulid::from_string`] method to create this newtype wrapper. + /// + /// [`Ulid::from_string`]: https://docs.rs/ulid/*/ulid/struct.Ulid.html#method.from_string pub fn new(id: &str) -> Result { ::ulid::Ulid::from_string(id).map(Self) } } /// Re-export common types from the [`ulid`][::ulid] crate. +#[allow(clippy::module_inception)] pub mod ulid { + #[allow(unused)] pub use ::ulid::{DecodeError, EncodeError, ULID_LEN}; } -////// ============================ SCHEMAS ============================ \\\\\\ -impl PartialSchema for Ulid { - fn schema() -> RefOr { - let object = ObjectBuilder::new() - .schema_type(SchemaType::Type(Type::String)) - .description(Some("ULID is a unique 128-bit lexicographically sortable identifier")) - .max_length(Some(ulid::ULID_LEN)) - .examples([serde_json::json!("01D39ZY06FGSCTVN4T2V9PKHFZ")]) - .build(); - - RefOr::T(Schema::Object(object)) +cfg_openapi! { + use utoipa::{ + openapi::{schema::SchemaType, ObjectBuilder, RefOr, Schema, Type}, + PartialSchema, ToSchema, + }; + + impl PartialSchema for Ulid { + fn schema() -> RefOr { + let object = ObjectBuilder::new() + .schema_type(SchemaType::Type(Type::String)) + .description(Some("ULID is a unique 128-bit lexicographically sortable identifier")) + .max_length(Some(ulid::ULID_LEN)) + .examples([serde_json::json!("01D39ZY06FGSCTVN4T2V9PKHFZ")]) + .build(); + + RefOr::T(Schema::Object(object)) + } } + + impl ToSchema for Ulid {} } -impl ToSchema for Ulid { - fn name() -> Cow<'static, str> { - Cow::Borrowed("Ulid") +cfg_jsonschema! { + use schemars::{JsonSchema, schema::*, gen::SchemaGenerator}; + use std::borrow::Cow; + + impl JsonSchema for Ulid { + fn is_referenceable() -> bool { + false + } + + fn schema_name() -> String { + "Ulid".to_string() + } + + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("ulid::Ulid") + } + + fn json_schema(_: &mut SchemaGenerator) -> Schema { + SchemaObject { + instance_type: Some(InstanceType::String.into()), + //format: Some("ulid".to_string()), + ..Default::default() + } + .into() + } } } -////// ============================ TO SQL ============================ \\\\\\ -impl ToSql for Ulid -where - str: ToSql, -{ - fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result { - let mut buf = [0; ulid::ULID_LEN]; - let v = self.array_to_str(&mut buf); +#[cfg(feature = "__internal_db")] +const _: () = { + use sea_orm::{ + sea_query::{ArrayType, ColumnType, Nullable, Value, ValueType, ValueTypeErr}, + ColIdx, DbErr, QueryResult, TryFromU64, TryGetError, TryGetable, + }; + use std::any::type_name; - >::to_sql(&(*v), &mut out.reborrow()) + impl TryFromU64 for Ulid { + fn try_from_u64(_: u64) -> Result { + Err(DbErr::ConvertFromU64("ulid")) + } } -} -// We can't rely on `RawBytesBindCollector` since the SQLite backend doesn't implement it. So, -// we need to do it this way. Why? I wish I knew. - Noel -impl ToSql for Ulid { - fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result { - let v = self.to_string(); - out.set_value(v); + impl Nullable for Ulid { + fn null() -> sea_orm::Value { + Value::String(None) + } + } - Ok(IsNull::No) + impl From for Value { + fn from(value: Ulid) -> Self { + Value::String(Some(Box::new(value.0.to_string()))) + } } -} -////// ============================ FROM SQL ============================ \\\\\\ -impl FromSql for Ulid -where - String: FromSql, -{ - fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { - Ok( - ::ulid::Ulid::from_string(&>::from_sql(bytes)?) - .map(Self) - .map_err(Box::new)?, - ) + impl TryGetable for Ulid { + fn try_get_by(query: &QueryResult, idx: I) -> Result { + let contents = ::try_get_by(query, idx)?; + contents.parse::<::ulid::Ulid>().map(Self).map_err(|e| { + TryGetError::DbErr(DbErr::TryIntoErr { + from: type_name::(), + into: type_name::<::ulid::Ulid>(), + source: Box::new(e), + }) + }) + } } -} + + impl ValueType for Ulid { + fn try_from(v: Value) -> Result { + let contents = ::try_from(v)?; + contents.parse::<::ulid::Ulid>().map(Self).map_err(|_| ValueTypeErr) + } + + fn type_name() -> String { + "Ulid".to_owned() + } + + fn array_type() -> ArrayType { + ArrayType::String + } + + fn column_type() -> ColumnType { + ColumnType::Char(None) + } + } +}; diff --git a/crates/types/src/newtypes/version.rs b/crates/types/src/newtypes/version.rs deleted file mode 100644 index 2b705c2e5..000000000 --- a/crates/types/src/newtypes/version.rs +++ /dev/null @@ -1,154 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use derive_more::Display; -use diesel::{ - backend::Backend, - deserialize::{FromSql, FromSqlRow}, - expression::AsExpression, - query_builder::bind_collector::RawBytesBindCollector, - serialize::ToSql, - sql_types::Text, -}; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use std::borrow::Cow; -use utoipa::{ - openapi::{schema::*, *}, - PartialSchema, ToSchema, -}; - -charted_core::create_newtype_wrapper!( - /// Newtype wrapper for the [`semver::Version`] type. - /// - /// The wrapper implements the following types: - /// * [`AsExpression`]<[`Text`]> - /// * [`ToSchema`], [`PartialSchema`] for OpenAPI - #[cfg_attr(feature = "jsonschema", doc = "* [`JsonSchema`][schemasrs::JsonSchema] for JSON schemas")] - #[derive(Debug, Clone, Display, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash, AsExpression, FromSqlRow)] - #[diesel(sql_type = Text)] - pub Version for ::semver::Version; -); - -charted_core::mk_from_newtype!(from Version as semver::Version); - -impl Version { - /// Forwards to [`semver::Version::parse`] to return this newtype wrapper. - pub fn parse(v: &str) -> Result { - semver::Version::parse(&v.trim_start_matches('v').replace(['x', 'X'], "0")).map(Self) - } -} - -impl<'de> Deserialize<'de> for Version { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct Visitor; - impl serde::de::Visitor<'_> for Visitor { - type Value = Version; - - fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - f.write_str("valid semantic version string") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - self.visit_string(v.to_string()) - } - - fn visit_string(self, v: String) -> Result - where - E: serde::de::Error, - { - Version::parse(&v).map_err(E::custom) - } - } - - deserializer.deserialize_str(Visitor) - } -} - -#[cfg(test)] -#[test] -fn test_deserialization_of_version() { - assert!(serde_json::from_str::("\"1.2.x\"").is_ok()); - assert!(serde_json::from_str::("\"1.x.x\"").is_ok()); - assert!(serde_json::from_str::("\"1.2.X\"").is_ok()); - assert!(serde_json::from_str::("\"1.X.X\"").is_ok()); -} - -////// ============================ SCHEMAS ============================ \\\\\\ -impl PartialSchema for Version { - fn schema() -> RefOr { - let object = ObjectBuilder::new() - .schema_type(SchemaType::Type(Type::String)) - .description(Some("Type that represents a semantic version (https://semver.org).")) - .pattern(Some(r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$")) - .examples([json!("1.2.3")]) - .build(); - - RefOr::T(Schema::Object(object)) - } -} - -impl ToSchema for Version { - fn name() -> Cow<'static, str> { - Cow::Borrowed("Version") - } -} - -#[cfg(feature = "jsonschema")] -impl ::schemars::JsonSchema for Version { - fn schema_id() -> ::std::borrow::Cow<'static, str> { - ::schema_id() - } - - fn schema_name() -> String { - ::schema_name() - } - - fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { - ::json_schema(gen) - } -} - -////// ============================ TO SQL ============================ \\\\\\ -// credit to this impl (i had a hard time doing this myself): -// https://github.com/oxidecomputer/omicron/blob/d3257b9d8d48fa94ed11020598a723644aec9f05/nexus/db-model/src/semver_version.rs#L124-L136 -impl ToSql for Version -where - for<'c> B: Backend = RawBytesBindCollector>, - String: ToSql, -{ - fn to_sql<'b>(&'b self, out: &mut diesel::serialize::Output<'b, '_, B>) -> diesel::serialize::Result { - let v = self.0.to_string(); - v.to_sql(&mut out.reborrow()) - } -} - -////// ============================ FROM SQL ============================ \\\\\\ -impl FromSql for Version -where - String: FromSql, -{ - fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { - Ok(semver::Version::parse(&>::from_sql(bytes)?) - .map(Self) - .map_err(Box::new)?) - } -} diff --git a/crates/types/src/newtypes/version_req.rs b/crates/types/src/newtypes/version_req.rs deleted file mode 100644 index eb75d1ac3..000000000 --- a/crates/types/src/newtypes/version_req.rs +++ /dev/null @@ -1,82 +0,0 @@ -// 🐻‍❄️📦 charted-server: Free, open source, and reliable Helm Chart registry made in Rust -// Copyright 2022-2025 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use derive_more::Display; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use std::borrow::Cow; -use utoipa::{ - openapi::{schema::SchemaType, ObjectBuilder, RefOr, Schema, Type}, - PartialSchema, ToSchema, -}; - -charted_core::create_newtype_wrapper!( - /// Newtype wrapper for the [`semver::VersionReq`] type. - /// - /// This wrapper implements the following types: - /// * [`ToSchema`], [`PartialSchema`] for OpenAPI - #[cfg_attr(feature = "jsonschema", doc = "* [`schemars::JsonSchema`](https://docs.rs/schemars/*/schemars/trait.JsonSchema.html) for JSON schemas")] - #[derive(Debug, Clone, Display, PartialEq, Eq, Serialize, Deserialize)] - pub VersionReq for semver::VersionReq; -); - -charted_core::mk_from_newtype!(from VersionReq as semver::VersionReq); - -impl VersionReq { - /// Forwards to [`semver::VersionReq::parse`] to return this newtype wrapper. - pub fn parse(v: &str) -> Result { - semver::VersionReq::parse(v).map(Self) - } -} - -////// ============================ SCHEMAS ============================ \\\\\\ -impl PartialSchema for VersionReq { - fn schema() -> RefOr { - let object = ObjectBuilder::new() - .schema_type(SchemaType::Type(Type::String)) - .description(Some( - "A semantic version requirement (https://semver.org) that Helm and charted-server supports", - )) - .examples([json!(">=1.2.3"), json!("~1")]) - .build(); - - RefOr::T(Schema::Object(object)) - } -} - -impl ToSchema for VersionReq { - fn name() -> Cow<'static, str> { - Cow::Borrowed("VersionReq") - } -} - -#[cfg(feature = "jsonschema")] -impl ::schemars::JsonSchema for VersionReq { - fn schema_id() -> ::std::borrow::Cow<'static, str> { - ::std::borrow::Cow::Borrowed("semver::VersionReq") - } - - fn schema_name() -> String { - String::from("VersionReq") - } - - fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { - ::schemars::schema::SchemaObject { - instance_type: Some(::schemars::schema::InstanceType::String.into()), - ..Default::default() - } - .into() - } -} diff --git a/crates/types/src/payloads.rs b/crates/types/src/payloads.rs index 28a4561a6..f413b5c47 100644 --- a/crates/types/src/payloads.rs +++ b/crates/types/src/payloads.rs @@ -13,16 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! The `payloads` module contains submodules that provide the types -//! for modifying entities. +//! Types that can effictively create or patch a object's metadata. Used by +//! the API server for the `PUT` and `PATCH` REST endpoints. -pub mod apikey; -pub mod member; -pub mod organization; -pub mod repository; -pub mod user; - -macro_rules! create_modifying_payload { +macro_rules! mk_payload_structs { ( $name:ident; @@ -42,7 +36,7 @@ macro_rules! create_modifying_payload { )* } ) => { - paste::paste! { + $crate::__private::paste! { $(#[$create])* pub struct [] { $( @@ -62,4 +56,318 @@ macro_rules! create_modifying_payload { }; } -use create_modifying_payload; +mk_payload_structs! { + ApiKey; + + create {} + + patch {} +} + +mk_payload_structs! { + User; + + create {} + + patch {} +} + +mk_payload_structs! { + Repository; + + create {} + + patch {} +} + +mk_payload_structs! { + RepositoryRelease; + + create {} + + patch {} +} + +mk_payload_structs! { + Organization; + + create {} + + patch {} +} + +mk_payload_structs! { + Member; + + create {} + + patch {} +} + +/* +super::create_modifying_payload! { + ApiKey; + + /// Payload object for constructing an API key. + #[derive(Debug, Clone, Deserialize, ToSchema)] + create { + /// Short description about the API key. Used to visibility distinct + /// an API key other than its name. + #[serde(default)] + pub description: Option, + + /// Maximum of time that this API key can live. Minimum allowed is 30 seconds. + #[serde(default)] + pub expires_in: Option, + + /// List of scopes (which can be either a `u64` or `string`). + #[serde(default)] + pub scopes: Vec, + + /// Name of the API key. + pub name: Name, + } + + /// Payload object for modifying a API key. + #[derive(Debug, Clone, Deserialize, ToSchema)] + patch { + /// Updates or removes the description of the API key. + /// + /// * If this is `null`, this will not do any patching + /// * If this is a empty string, this will act as "removing" it from the metadata + /// * If the comparsion (`old.description == this.description`) is false, then this will update it. + #[serde(default)] + pub description: Option, + + /// key name to use to identify the key + #[serde(default)] + pub name: Option, + } +} + +use crate::name::Name; +use serde::Deserialize; +use utoipa::ToSchema; + +super::create_modifying_payload! { + Organization; + + /// Request body payload for creating a new organization. + #[derive(Debug, Clone, Deserialize, ToSchema)] + create { + /// Short description about this organization. If `description` was set to `null`, then + /// this will not be updated, if `description` was a empty string, the `description` + /// will be set to a empty string and will present as "*no description for this organization*" + /// in Hoshi. + #[serde(default)] + pub description: Option, + + /// Display name for this organization. + #[serde(default)] + pub display_name: Option, + + /// Whether if the organization is private or not. + #[serde(default)] + pub private: bool, + + /// Organization name. + pub name: Name, + } + + /// Request body payload for patching an organization's metadata. + #[derive(Debug, Clone, Deserialize, ToSchema)] + patch { + /// Adds or removes a Twitter handle from this organization's metadata. + /// + /// * If this is `null`, this will not do any patching + /// * If this is a empty string, this will act as "removing" it from the metadata + /// * If the comparsion (`old.twitter_handle == twitter_handle`) is false, then this will update it. + #[serde(default)] + pub twitter_handle: Option, + + /// Optional field to update this organization's gravatar email. If this organization doesn't + /// have an avatar that is used or prefers not to use their previously uploaded + /// avatars and they set their Gravatar email, their Gravatar will be used. + #[serde(default)] + pub gravatar_email: Option, + + /// Display name for this organization. + #[serde(default)] + pub display_name: Option, + + /// Whether if the organization is private or not. + #[serde(default)] + pub private: Option, + + /// Organization name to rename to. + #[serde(default)] + pub name: Option, + } +} + +use crate::{helm::ChartType, name::Name}; +use serde::Deserialize; +use utoipa::ToSchema; + +super::create_modifying_payload! { + Repository; + + /// Request body payload for creating a repository. + #[derive(Debug, Clone, Deserialize, ToSchema)] + create { + /// Short description about this repository. + #[serde(default)] + pub description: Option, + + /// Whether if this repository is private. + #[serde(default)] + pub private: bool, + + /// The contents of the README that will be displayed on the repository. If you're + /// using charted's official Helm plugin, new releases can update its README and it'll + /// be reflected. + /// + /// This should be valid Markdown, but XSS cross scripting is impossible as scripts + /// in codeblocks or via `