Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e08210e52d
|
|||
|
3679c7e8cd
|
|||
|
b5a83f100d
|
|||
|
3c86e9eb36
|
|||
|
c700d65b34
|
|||
|
8bf2917eb7
|
|||
|
fc8dc805a9
|
|||
|
5b6dd0c4f7
|
|||
|
b29a095a38
|
|||
|
85621d9364
|
|||
|
5baa73d272
|
|||
|
ff6aa10d91
|
|||
|
598af596c7
|
|||
|
9f576d7509
|
|||
|
2216d7da58
|
|||
|
d4fdc2f468
|
|||
|
dcb3dc60a4
|
|||
|
b38e6110d2
|
|||
|
afd399b84f
|
|||
|
b923f3bdb0
|
|||
|
ad4133cf3d
|
|||
|
17c916445e
|
@@ -40,9 +40,21 @@ jobs:
|
|||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
skipPush: ${{ github.event_name == 'pull_request' }}
|
skipPush: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
- name: Format Check
|
||||||
|
shell: bash -c "nix develop --no-pure-eval --accept-flake-config --command {0}"
|
||||||
|
run: just format-check
|
||||||
|
|
||||||
|
- name: Audit
|
||||||
|
shell: bash -c "nix develop --no-pure-eval --accept-flake-config --command {0}"
|
||||||
|
run: just audit
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
shell: bash -c "nix develop --no-pure-eval --accept-flake-config --command {0}"
|
||||||
|
run: just lint-report
|
||||||
|
|
||||||
- name: Coverage
|
- name: Coverage
|
||||||
run: |
|
shell: bash -c "nix develop --no-pure-eval --accept-flake-config --command {0}"
|
||||||
nix develop --no-pure-eval --accept-flake-config --command just coverage
|
run: just coverage-ci
|
||||||
|
|
||||||
- name: Sonar analysis
|
- name: Sonar analysis
|
||||||
uses: SonarSource/sonarqube-scan-action@v6
|
uses: SonarSource/sonarqube-scan-action@v6
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"vueCompilerOptions": {
|
|
||||||
"target": 3.5,
|
|
||||||
"extensions": [".vue"]
|
|
||||||
},
|
|
||||||
"typescript": {
|
|
||||||
"tsdk": "frontend/node_modules/typescript/lib"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
+583
-687
File diff suppressed because it is too large
Load Diff
@@ -321,7 +321,7 @@ backend/
|
|||||||
|
|
||||||
The contact form supports multiple SMTP configurations:
|
The contact form supports multiple SMTP configurations:
|
||||||
- **Implicit TLS (SMTPS)** - typically port 465
|
- **Implicit TLS (SMTPS)** - typically port 465
|
||||||
- **STARTTLS (Always/Opportunistic)** - typically port 587
|
- **STARTTLS (Always)** - typically port 587
|
||||||
- **Unencrypted** (for local dev) - with or without authentication
|
- **Unencrypted** (for local dev) - with or without authentication
|
||||||
|
|
||||||
The `SmtpTransport` is built dynamically from `EmailSettings` based on
|
The `SmtpTransport` is built dynamically from `EmailSettings` based on
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
feature-depth = 1
|
feature-depth = 1
|
||||||
|
|
||||||
[advisories]
|
[advisories]
|
||||||
ignore = []
|
ignore = [
|
||||||
|
"RUSTSEC-2025-0134" # Temporary, see https://github.com/poem-web/poem/issues/1182
|
||||||
|
]
|
||||||
|
|
||||||
[licenses]
|
[licenses]
|
||||||
# List of explicitly allowed licenses
|
# List of explicitly allowed licenses
|
||||||
@@ -17,7 +19,6 @@ allow = [
|
|||||||
"ISC",
|
"ISC",
|
||||||
"MIT",
|
"MIT",
|
||||||
"MPL-2.0",
|
"MPL-2.0",
|
||||||
"OpenSSL",
|
|
||||||
"Unicode-3.0",
|
"Unicode-3.0",
|
||||||
"Zlib",
|
"Zlib",
|
||||||
]
|
]
|
||||||
@@ -30,7 +31,7 @@ registries = []
|
|||||||
|
|
||||||
[bans]
|
[bans]
|
||||||
multiple-versions = "allow"
|
multiple-versions = "allow"
|
||||||
wildcards = "allow"
|
wildcards = "deny"
|
||||||
highlight = "all"
|
highlight = "all"
|
||||||
workspace-default-features = "allow"
|
workspace-default-features = "allow"
|
||||||
external-default-features = "allow"
|
external-default-features = "allow"
|
||||||
|
|||||||
Generated
+653
-31
@@ -42,11 +42,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1760971495,
|
"lastModified": 1767714506,
|
||||||
"narHash": "sha256-IwnNtbNVrlZIHh7h4Wz6VP0Furxg9Hh0ycighvL5cZc=",
|
"narHash": "sha256-WaTs0t1CxhgxbIuvQ97OFhDTVUGd1HA+KzLZUZBhe0s=",
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"repo": "cachix",
|
"repo": "cachix",
|
||||||
"rev": "c5bfd933d1033672f51a863c47303fc0e093c2d2",
|
"rev": "894c649f0daaa38bbcfb21de64be47dfa7cd0ec9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -56,23 +56,142 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cachix_2": {
|
||||||
|
"inputs": {
|
||||||
|
"devenv": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix"
|
||||||
|
],
|
||||||
|
"flake-compat": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix"
|
||||||
|
],
|
||||||
|
"git-hooks": "git-hooks",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1767714506,
|
||||||
|
"narHash": "sha256-WaTs0t1CxhgxbIuvQ97OFhDTVUGd1HA+KzLZUZBhe0s=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "cachix",
|
||||||
|
"rev": "894c649f0daaa38bbcfb21de64be47dfa7cd0ec9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"ref": "latest",
|
||||||
|
"repo": "cachix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cachix_3": {
|
||||||
|
"inputs": {
|
||||||
|
"devenv": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"crate2nix_stable"
|
||||||
|
],
|
||||||
|
"flake-compat": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"crate2nix_stable"
|
||||||
|
],
|
||||||
|
"git-hooks": "git-hooks_2",
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1767714506,
|
||||||
|
"narHash": "sha256-WaTs0t1CxhgxbIuvQ97OFhDTVUGd1HA+KzLZUZBhe0s=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "cachix",
|
||||||
|
"rev": "894c649f0daaa38bbcfb21de64be47dfa7cd0ec9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"ref": "latest",
|
||||||
|
"repo": "cachix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"crate2nix": {
|
||||||
|
"inputs": {
|
||||||
|
"cachix": "cachix_2",
|
||||||
|
"crate2nix_stable": "crate2nix_stable",
|
||||||
|
"devshell": "devshell_2",
|
||||||
|
"flake-compat": "flake-compat_2",
|
||||||
|
"flake-parts": "flake-parts_2",
|
||||||
|
"nix-test-runner": "nix-test-runner_2",
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"pre-commit-hooks": "pre-commit-hooks_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1773440526,
|
||||||
|
"narHash": "sha256-OcX1MYqUdoalY3/vU67PEx8m6RvqGxX0LwKonjzXn7I=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "crate2nix",
|
||||||
|
"rev": "e697d3049c909580128caa856ab8eb709556a97b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "crate2nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"crate2nix_stable": {
|
||||||
|
"inputs": {
|
||||||
|
"cachix": "cachix_3",
|
||||||
|
"crate2nix_stable": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"crate2nix_stable"
|
||||||
|
],
|
||||||
|
"devshell": "devshell",
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"flake-parts": "flake-parts",
|
||||||
|
"nix-test-runner": "nix-test-runner",
|
||||||
|
"nixpkgs": "nixpkgs_3",
|
||||||
|
"pre-commit-hooks": "pre-commit-hooks"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1769627083,
|
||||||
|
"narHash": "sha256-SUuruvw1/moNzCZosHaa60QMTL+L9huWdsCBN6XZIic=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "crate2nix",
|
||||||
|
"rev": "7c33e664668faecf7655fa53861d7a80c9e464a2",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"ref": "0.15.0",
|
||||||
|
"repo": "crate2nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"devenv": {
|
"devenv": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"cachix": "cachix",
|
"cachix": "cachix",
|
||||||
"flake-compat": "flake-compat",
|
"crate2nix": "crate2nix",
|
||||||
"flake-parts": "flake-parts",
|
"flake-compat": "flake-compat_3",
|
||||||
"git-hooks": "git-hooks",
|
"flake-parts": "flake-parts_3",
|
||||||
|
"git-hooks": "git-hooks_3",
|
||||||
"nix": "nix",
|
"nix": "nix",
|
||||||
|
"nixd": "nixd",
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
]
|
],
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763136231,
|
"lastModified": 1774475276,
|
||||||
"narHash": "sha256-QVtIjPSQ/xVhuXSSENYOYZPfrjjc/W/djuxcJyKxGTw=",
|
"narHash": "sha256-z4erC+oMEuBHtox+B46FCv77IPvNy4SyXw/EeBxsD4I=",
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"repo": "devenv",
|
"repo": "devenv",
|
||||||
"rev": "4b8c2bbdb4e01ef8c4093ee1224fe21ed5ea1a5e",
|
"rev": "f8ca2c061ec2feceee1cf1c5e52c92f58b6aec9c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -93,6 +212,51 @@
|
|||||||
"url": "file:///dev/null"
|
"url": "file:///dev/null"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"devshell": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"crate2nix_stable",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1768818222,
|
||||||
|
"narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "devshell",
|
||||||
|
"rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "devshell",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devshell_2": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1768818222,
|
||||||
|
"narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "devshell",
|
||||||
|
"rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "devshell",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"fenix": {
|
"fenix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
@@ -116,13 +280,41 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flake-compat": {
|
"flake-compat": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1733328505,
|
||||||
|
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
|
||||||
|
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
|
||||||
|
"revCount": 69,
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1733328505,
|
||||||
|
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
|
||||||
|
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
|
||||||
|
"revCount": 69,
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat_3": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1761588595,
|
"lastModified": 1767039857,
|
||||||
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
|
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||||
"owner": "edolstra",
|
"owner": "edolstra",
|
||||||
"repo": "flake-compat",
|
"repo": "flake-compat",
|
||||||
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
|
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -135,15 +327,60 @@
|
|||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs-lib": [
|
"nixpkgs-lib": [
|
||||||
"devenv",
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"crate2nix_stable",
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1760948891,
|
"lastModified": 1768135262,
|
||||||
"narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=",
|
"narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=",
|
||||||
"owner": "hercules-ci",
|
"owner": "hercules-ci",
|
||||||
"repo": "flake-parts",
|
"repo": "flake-parts",
|
||||||
"rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04",
|
"rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-parts_2": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-lib": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1768135262,
|
||||||
|
"narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-parts_3": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-lib": [
|
||||||
|
"devenv",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1772408722,
|
||||||
|
"narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -190,20 +427,82 @@
|
|||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-compat": [
|
"flake-compat": [
|
||||||
"devenv",
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"cachix",
|
||||||
"flake-compat"
|
"flake-compat"
|
||||||
],
|
],
|
||||||
"gitignore": "gitignore",
|
"gitignore": "gitignore",
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"cachix",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1765404074,
|
||||||
|
"narHash": "sha256-+ZDU2d+vzWkEJiqprvV5PR26DVFN2vgddwG5SnPZcUM=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"rev": "2d6f58930fbcd82f6f9fd59fb6d13e37684ca529",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"git-hooks_2": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"crate2nix_stable",
|
||||||
|
"cachix",
|
||||||
|
"flake-compat"
|
||||||
|
],
|
||||||
|
"gitignore": "gitignore_2",
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"crate2nix_stable",
|
||||||
|
"cachix",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1765404074,
|
||||||
|
"narHash": "sha256-+ZDU2d+vzWkEJiqprvV5PR26DVFN2vgddwG5SnPZcUM=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"rev": "2d6f58930fbcd82f6f9fd59fb6d13e37684ca529",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"git-hooks_3": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": [
|
||||||
|
"devenv",
|
||||||
|
"flake-compat"
|
||||||
|
],
|
||||||
|
"gitignore": "gitignore_5",
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"devenv",
|
"devenv",
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1760663237,
|
"lastModified": 1772893680,
|
||||||
"narHash": "sha256-BflA6U4AM1bzuRMR8QqzPXqh8sWVCNDzOdsxXEguJIc=",
|
"narHash": "sha256-JDqZMgxUTCq85ObSaFw0HhE+lvdOre1lx9iI6vYyOEs=",
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"repo": "git-hooks.nix",
|
"repo": "git-hooks.nix",
|
||||||
"rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37",
|
"rev": "8baab586afc9c9b57645a734c820e4ac0a604af9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -213,6 +512,102 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"gitignore": {
|
"gitignore": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"cachix",
|
||||||
|
"git-hooks",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1709087332,
|
||||||
|
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitignore_2": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"crate2nix_stable",
|
||||||
|
"cachix",
|
||||||
|
"git-hooks",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1709087332,
|
||||||
|
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitignore_3": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"crate2nix_stable",
|
||||||
|
"pre-commit-hooks",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1709087332,
|
||||||
|
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitignore_4": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"pre-commit-hooks",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1709087332,
|
||||||
|
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitignore_5": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"devenv",
|
"devenv",
|
||||||
@@ -260,27 +655,153 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1761648602,
|
"lastModified": 1774103430,
|
||||||
"narHash": "sha256-H97KSB/luq/aGobKRuHahOvT1r7C03BgB6D5HBZsbN8=",
|
"narHash": "sha256-MRNVInSmvhKIg3y0UdogQJXe+omvKijGszFtYpd5r9k=",
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"repo": "nix",
|
"repo": "nix",
|
||||||
"rev": "3e5644da6830ef65f0a2f7ec22830c46285bfff6",
|
"rev": "e127c1c94cefe02d8ca4cca79ef66be4c527510e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"ref": "devenv-2.30.6",
|
"ref": "devenv-2.32",
|
||||||
"repo": "nix",
|
"repo": "nix",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nix-test-runner": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1588761593,
|
||||||
|
"narHash": "sha256-FKJykltAN/g3eIceJl4SfDnnyuH2jHImhMrXS2KvGIs=",
|
||||||
|
"owner": "stoeffel",
|
||||||
|
"repo": "nix-test-runner",
|
||||||
|
"rev": "c45d45b11ecef3eb9d834c3b6304c05c49b06ca2",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "stoeffel",
|
||||||
|
"repo": "nix-test-runner",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nix-test-runner_2": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1588761593,
|
||||||
|
"narHash": "sha256-FKJykltAN/g3eIceJl4SfDnnyuH2jHImhMrXS2KvGIs=",
|
||||||
|
"owner": "stoeffel",
|
||||||
|
"repo": "nix-test-runner",
|
||||||
|
"rev": "c45d45b11ecef3eb9d834c3b6304c05c49b06ca2",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "stoeffel",
|
||||||
|
"repo": "nix-test-runner",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixd": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-parts": [
|
||||||
|
"devenv",
|
||||||
|
"flake-parts"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"treefmt-nix": "treefmt-nix"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1773634079,
|
||||||
|
"narHash": "sha256-49qb4QNMv77VOeEux+sMd0uBhPvvHgVc0r938Bulvbo=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixd",
|
||||||
|
"rev": "8ecf93d4d93745e05ea53534e8b94f5e9506e6bd",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixd",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1761313199,
|
"lastModified": 1765186076,
|
||||||
"narHash": "sha256-wCIACXbNtXAlwvQUo1Ed++loFALPjYUA3dpcUJiXO44=",
|
"narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1773840656,
|
||||||
|
"narHash": "sha256-9tpvMGFteZnd3gRQZFlRCohVpqooygFuy9yjuyRL2C0=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "9cf7092bdd603554bd8b63c216e8943cf9b12512",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1765186076,
|
||||||
|
"narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_3": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1769433173,
|
||||||
|
"narHash": "sha256-Gf1dFYgD344WZ3q0LPlRoWaNdNQq8kSBDLEWulRQSEs=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "13b0f9e6ac78abbbb736c635d87845c4f4bee51b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_4": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-src": "nixpkgs-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1774287239,
|
||||||
|
"narHash": "sha256-W3krsWcDwYuA3gPWsFA24YAXxOFUL6iIlT6IknAoNSE=",
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"repo": "devenv-nixpkgs",
|
"repo": "devenv-nixpkgs",
|
||||||
"rev": "d1c30452ebecfc55185ae6d1c983c09da0c274ff",
|
"rev": "fa7125ea7f1ae5430010a6e071f68375a39bd24c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -290,14 +811,72 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pre-commit-hooks": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"crate2nix_stable",
|
||||||
|
"flake-compat"
|
||||||
|
],
|
||||||
|
"gitignore": "gitignore_3",
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"crate2nix_stable",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1769069492,
|
||||||
|
"narHash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "pre-commit-hooks.nix",
|
||||||
|
"rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "pre-commit-hooks.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pre-commit-hooks_2": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"flake-compat"
|
||||||
|
],
|
||||||
|
"gitignore": "gitignore_4",
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"crate2nix",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1769069492,
|
||||||
|
"narHash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "pre-commit-hooks.nix",
|
||||||
|
"rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "pre-commit-hooks.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"alejandra": "alejandra",
|
"alejandra": "alejandra",
|
||||||
"devenv": "devenv",
|
"devenv": "devenv",
|
||||||
"devenv-root": "devenv-root",
|
"devenv-root": "devenv-root",
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs_4",
|
||||||
"rust-overlay": "rust-overlay"
|
"rust-overlay": "rust-overlay_2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rust-analyzer-src": {
|
"rust-analyzer-src": {
|
||||||
@@ -320,15 +899,36 @@
|
|||||||
"rust-overlay": {
|
"rust-overlay": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763174172,
|
"lastModified": 1773630837,
|
||||||
"narHash": "sha256-u6dcvXk2K6eYVYhmfiN3xmhIf3yUo5KPwm79UOD37Jo=",
|
"narHash": "sha256-zJhgAGnbVKeBMJOb9ctZm4BGS/Rnrz+5lfSXTVah4HQ=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "89af6762b01409edbb595888a69311e8e5954110",
|
"rev": "f600ea449c7b5bb596fa1cf21c871cc5b9e31316",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay_2": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1774408260,
|
||||||
|
"narHash": "sha256-Jn9d9r85dmf3gTMnSRt6t+DP2nQ5uJns/MMXg2FpzfM=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "d6471ee5a8f470251e6e5b83a20a182eb6c46c9b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -351,6 +951,28 @@
|
|||||||
"repo": "default",
|
"repo": "default",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"treefmt-nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"nixd",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1772660329,
|
||||||
|
"narHash": "sha256-IjU1FxYqm+VDe5qIOxoW+pISBlGvVApRjiw/Y/ttJzY=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"rev": "3710e0e1218041bbad640352a0440114b1e10428",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|||||||
@@ -21,14 +21,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
nixConfig = {
|
nixConfig = {
|
||||||
extra-trusted-public-keys = [
|
extra-trusted-public-keys = ["devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=" "phundrak.cachix.org-1:osJAkYO0ioTOPqaQCIXMfIRz1/+YYlVFkup3R2KSexk="];
|
||||||
"devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw="
|
extra-substituters = ["https://devenv.cachix.org" "https://phundrak.cachix.org"];
|
||||||
"phundrak-dot-com.cachix.org-1:c02/xlCknJIDoaQPUzEWSJHPoXcmIXYzCa+hVRhbDgE="
|
|
||||||
];
|
|
||||||
extra-substituters = [
|
|
||||||
"https://devenv.cachix.org"
|
|
||||||
"https://phundrak-dot-com.cachix.org"
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = {
|
outputs = {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ format-check:
|
|||||||
cargo fmt --check --all
|
cargo fmt --check --all
|
||||||
|
|
||||||
audit:
|
audit:
|
||||||
cargo deny
|
cargo deny check
|
||||||
|
|
||||||
build:
|
build:
|
||||||
cargo build
|
cargo build
|
||||||
@@ -24,6 +24,10 @@ build-release:
|
|||||||
lint:
|
lint:
|
||||||
cargo clippy --all-targets
|
cargo clippy --all-targets
|
||||||
|
|
||||||
|
lint-report:
|
||||||
|
mkdir -p coverage
|
||||||
|
cargo clippy --all-targets --message-format=json > coverage/clippy.json
|
||||||
|
|
||||||
release-build:
|
release-build:
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
|
|||||||
+3
-6
@@ -24,7 +24,7 @@ pub mod startup;
|
|||||||
/// Logging and tracing setup
|
/// Logging and tracing setup
|
||||||
pub mod telemetry;
|
pub mod telemetry;
|
||||||
|
|
||||||
type MaybeListener = Option<poem::listener::TcpListener<String>>;
|
type MaybeListener = Option<std::net::TcpListener>;
|
||||||
|
|
||||||
fn prepare(listener: MaybeListener) -> startup::Application {
|
fn prepare(listener: MaybeListener) -> startup::Application {
|
||||||
dotenvy::dotenv().ok();
|
dotenvy::dotenv().ok();
|
||||||
@@ -70,11 +70,8 @@ pub async fn run(listener: MaybeListener) -> Result<(), std::io::Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn make_random_tcp_listener() -> poem::listener::TcpListener<String> {
|
fn make_random_tcp_listener() -> std::net::TcpListener {
|
||||||
let tcp_listener =
|
std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind a random TCP listener")
|
||||||
std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind a random TCP listener");
|
|
||||||
let port = tcp_listener.local_addr().unwrap().port();
|
|
||||||
poem::listener::TcpListener::bind(format!("127.0.0.1:{port}"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ use std::{net::IpAddr, num::NonZeroU32, sync::Arc, time::Duration};
|
|||||||
|
|
||||||
use governor::{
|
use governor::{
|
||||||
Quota, RateLimiter,
|
Quota, RateLimiter,
|
||||||
clock::DefaultClock,
|
clock::{Clock, DefaultClock},
|
||||||
state::{InMemoryState, NotKeyed},
|
state::keyed::DefaultKeyedStateStore,
|
||||||
};
|
};
|
||||||
use poem::{Endpoint, Error, IntoResponse, Middleware, Request, Response, Result};
|
use poem::{Endpoint, Error, IntoResponse, Middleware, Request, Response, Result};
|
||||||
|
|
||||||
|
type BakitRateLimiter = RateLimiter<IpAddr, DefaultKeyedStateStore<IpAddr>, DefaultClock>;
|
||||||
|
|
||||||
/// Rate limiting configuration.
|
/// Rate limiting configuration.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RateLimitConfig {
|
pub struct RateLimitConfig {
|
||||||
@@ -37,17 +39,26 @@ impl RateLimitConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return default values for disabling rate limiting.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn disabled() -> Self {
|
||||||
|
Self {
|
||||||
|
burst_size: u32::MAX,
|
||||||
|
per_seconds: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a rate limiter from this configuration.
|
/// Creates a rate limiter from this configuration.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics if `burst_size` is zero.
|
/// Panics if `burst_size` is zero.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn create_limiter(&self) -> RateLimiter<NotKeyed, InMemoryState, DefaultClock> {
|
pub fn create_limiter(&self) -> BakitRateLimiter {
|
||||||
let quota = Quota::with_period(Duration::from_secs(self.per_seconds))
|
let quota = Quota::with_period(Duration::from_secs(self.per_seconds))
|
||||||
.expect("Failed to create quota")
|
.expect("Failed to create quota")
|
||||||
.allow_burst(NonZeroU32::new(self.burst_size).expect("Burst size must be non-zero"));
|
.allow_burst(NonZeroU32::new(self.burst_size).expect("Burst size must be non-zero"));
|
||||||
RateLimiter::direct(quota)
|
RateLimiter::keyed(quota)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +71,7 @@ impl Default for RateLimitConfig {
|
|||||||
|
|
||||||
/// Middleware for rate limiting based on IP address.
|
/// Middleware for rate limiting based on IP address.
|
||||||
pub struct RateLimit {
|
pub struct RateLimit {
|
||||||
limiter: Arc<RateLimiter<NotKeyed, InMemoryState, DefaultClock>>,
|
limiter: Arc<BakitRateLimiter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RateLimit {
|
impl RateLimit {
|
||||||
@@ -87,7 +98,7 @@ impl<E: Endpoint> Middleware<E> for RateLimit {
|
|||||||
/// The endpoint wrapper that performs rate limiting checks.
|
/// The endpoint wrapper that performs rate limiting checks.
|
||||||
pub struct RateLimitEndpoint<E> {
|
pub struct RateLimitEndpoint<E> {
|
||||||
endpoint: E,
|
endpoint: E,
|
||||||
limiter: Arc<RateLimiter<NotKeyed, InMemoryState, DefaultClock>>,
|
limiter: Arc<BakitRateLimiter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: Endpoint> Endpoint for RateLimitEndpoint<E> {
|
impl<E: Endpoint> Endpoint for RateLimitEndpoint<E> {
|
||||||
@@ -95,20 +106,22 @@ impl<E: Endpoint> Endpoint for RateLimitEndpoint<E> {
|
|||||||
|
|
||||||
async fn call(&self, req: Request) -> Result<Self::Output> {
|
async fn call(&self, req: Request) -> Result<Self::Output> {
|
||||||
// Check rate limit
|
// Check rate limit
|
||||||
if self.limiter.check().is_err() {
|
let client_ip =
|
||||||
let client_ip = Self::get_client_ip(&req)
|
Self::get_client_ip(&req).unwrap_or(IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED));
|
||||||
.map_or_else(|| "unknown".to_string(), |ip| ip.to_string());
|
if let Err(negative) = self.limiter.check_key(&client_ip) {
|
||||||
|
|
||||||
tracing::event!(
|
tracing::event!(
|
||||||
target: "backend::middleware::rate_limit",
|
target: "backend::middleware::rate_limit",
|
||||||
tracing::Level::WARN,
|
tracing::Level::WARN,
|
||||||
client_ip = %client_ip,
|
client_ip = %client_ip,
|
||||||
"Rate limit exceeded"
|
"Rate limit exceeded"
|
||||||
);
|
);
|
||||||
|
let clock = DefaultClock::default();
|
||||||
return Err(Error::from_status(
|
let wait = negative.wait_time_from(clock.now());
|
||||||
poem::http::StatusCode::TOO_MANY_REQUESTS,
|
let response = Response::builder()
|
||||||
));
|
.status(poem::http::StatusCode::TOO_MANY_REQUESTS)
|
||||||
|
.header("Retry-After", wait.as_secs().to_string())
|
||||||
|
.finish();
|
||||||
|
return Err(Error::from_response(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the request
|
// Process the request
|
||||||
@@ -148,14 +161,15 @@ mod tests {
|
|||||||
fn rate_limit_config_creates_limiter() {
|
fn rate_limit_config_creates_limiter() {
|
||||||
let config = RateLimitConfig::new(5, 1);
|
let config = RateLimitConfig::new(5, 1);
|
||||||
let limiter = config.create_limiter();
|
let limiter = config.create_limiter();
|
||||||
|
let ip = IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED);
|
||||||
|
|
||||||
// First 5 requests should succeed
|
// First 5 requests should succeed
|
||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
assert!(limiter.check().is_ok());
|
assert!(limiter.check_key(&ip).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6th request should fail
|
// 6th request should fail
|
||||||
assert!(limiter.check().is_err());
|
assert!(limiter.check_key(&ip).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ impl Error for ContactError {}
|
|||||||
/// issues beyond the client's control.
|
/// issues beyond the client's control.
|
||||||
impl From<lettre::transport::smtp::Error> for ContactError {
|
impl From<lettre::transport::smtp::Error> for ContactError {
|
||||||
fn from(value: lettre::transport::smtp::Error) -> Self {
|
fn from(value: lettre::transport::smtp::Error) -> Self {
|
||||||
tracing::event!(target: "contact", tracing::Level::ERROR, "SMTP Error details: {}", format!("{value:?}"));
|
tracing::event!(target: "backend::contact", tracing::Level::ERROR, "SMTP Error details: {}", format!("{value:?}"));
|
||||||
Self::OtherError(value.to_string())
|
Self::OtherError(value.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,15 +89,16 @@ impl std::fmt::Display for ContactError {
|
|||||||
/// If no specific field can be identified, returns a generic `ValidationError`.
|
/// If no specific field can be identified, returns a generic `ValidationError`.
|
||||||
impl From<ValidationErrors> for ContactError {
|
impl From<ValidationErrors> for ContactError {
|
||||||
fn from(value: ValidationErrors) -> Self {
|
fn from(value: ValidationErrors) -> Self {
|
||||||
if validator::ValidationErrors::has_error(&Err(value.clone()), "name") {
|
let errors = value.field_errors();
|
||||||
|
if errors.contains_key("name") {
|
||||||
return Self::ValidationNameError("backend.contact.errors.validation.name".to_owned());
|
return Self::ValidationNameError("backend.contact.errors.validation.name".to_owned());
|
||||||
}
|
}
|
||||||
if validator::ValidationErrors::has_error(&Err(value.clone()), "email") {
|
if errors.contains_key("email") {
|
||||||
return Self::ValidationEmailError(
|
return Self::ValidationEmailError(
|
||||||
"backend.contact.errors.validation.email".to_owned(),
|
"backend.contact.errors.validation.email".to_owned(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if validator::ValidationErrors::has_error(&Err(value), "message") {
|
if errors.contains_key("message") {
|
||||||
return Self::ValidationMessageError(
|
return Self::ValidationMessageError(
|
||||||
"backend.contact.errors.validation.message".to_owned(),
|
"backend.contact.errors.validation.message".to_owned(),
|
||||||
);
|
);
|
||||||
|
|||||||
+105
-25
@@ -18,6 +18,23 @@ use crate::settings::{EmailSettings, Starttls};
|
|||||||
pub mod errors;
|
pub mod errors;
|
||||||
use errors::ContactError;
|
use errors::ContactError;
|
||||||
|
|
||||||
|
/// Strips control characters that could enable protocol injection
|
||||||
|
///
|
||||||
|
/// When `keep_newlines` is true, `\n` is preserved (needed for
|
||||||
|
/// multi-line fields). For name and email fields, all control
|
||||||
|
/// characters are removed - no assumptions are made about valid name
|
||||||
|
/// *content*.
|
||||||
|
fn strip_control_chars(s: &str, keep_newlines: bool) -> String {
|
||||||
|
s.chars()
|
||||||
|
.filter(|c| {
|
||||||
|
if keep_newlines && (*c == '\n') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
!c.is_control()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<&EmailSettings> for SmtpTransport {
|
impl TryFrom<&EmailSettings> for SmtpTransport {
|
||||||
type Error = lettre::transport::smtp::Error;
|
type Error = lettre::transport::smtp::Error;
|
||||||
|
|
||||||
@@ -45,7 +62,7 @@ impl TryFrom<&EmailSettings> for SmtpTransport {
|
|||||||
Ok(builder.credentials(creds).build())
|
Ok(builder.credentials(creds).build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Starttls::Opportunistic | Starttls::Always => {
|
Starttls::Always => {
|
||||||
// STARTTLS - typically port 587
|
// STARTTLS - typically port 587
|
||||||
tracing::event!(target: "backend::contact", tracing::Level::DEBUG, "Using STARTTLS");
|
tracing::event!(target: "backend::contact", tracing::Level::DEBUG, "Using STARTTLS");
|
||||||
let creds = Credentials::new(settings.user.clone(), settings.password.clone());
|
let creds = Credentials::new(settings.user.clone(), settings.password.clone());
|
||||||
@@ -72,6 +89,14 @@ struct ContactRequest {
|
|||||||
honeypot: Option<String>,
|
honeypot: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ContactRequest {
|
||||||
|
fn sanitize(&mut self) {
|
||||||
|
self.name = strip_control_chars(&self.name, false);
|
||||||
|
self.email = strip_control_chars(&self.email, false);
|
||||||
|
self.message = strip_control_chars(&self.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<&ContactRequest> for lettre::message::Mailbox {
|
impl TryFrom<&ContactRequest> for lettre::message::Mailbox {
|
||||||
type Error = ContactError;
|
type Error = ContactError;
|
||||||
|
|
||||||
@@ -160,7 +185,7 @@ impl ContactApi {
|
|||||||
body: Json<ContactRequest>,
|
body: Json<ContactRequest>,
|
||||||
remote_addr: Option<poem::web::Data<&poem::web::RemoteAddr>>,
|
remote_addr: Option<poem::web::Data<&poem::web::RemoteAddr>>,
|
||||||
) -> ContactApiResponse {
|
) -> ContactApiResponse {
|
||||||
let body = body.0;
|
let mut body = body.0;
|
||||||
if let Some(ref honeypot) = body.honeypot
|
if let Some(ref honeypot) = body.honeypot
|
||||||
&& !honeypot.trim().is_empty()
|
&& !honeypot.trim().is_empty()
|
||||||
{
|
{
|
||||||
@@ -172,6 +197,7 @@ impl ContactApi {
|
|||||||
);
|
);
|
||||||
return ContactApiResponse::Ok(ContactResponse::honeypot_response().into());
|
return ContactApiResponse::Ok(ContactResponse::honeypot_response().into());
|
||||||
}
|
}
|
||||||
|
body.sanitize();
|
||||||
if let Err(e) = body.validate() {
|
if let Err(e) = body.validate() {
|
||||||
return ContactApiResponse::BadRequest(
|
return ContactApiResponse::BadRequest(
|
||||||
<validator::ValidationErrors as std::convert::Into<ContactResponse>>::into(e)
|
<validator::ValidationErrors as std::convert::Into<ContactResponse>>::into(e)
|
||||||
@@ -182,9 +208,10 @@ impl ContactApi {
|
|||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
tracing::event!(
|
tracing::event!(
|
||||||
target: "backend::contact",
|
target: "backend::contact",
|
||||||
tracing::Level::INFO, "Message from \"{} <{}>\" sent successfully",
|
tracing::Level::INFO,
|
||||||
body.name,
|
name = %body.name,
|
||||||
body.email
|
email = %body.email,
|
||||||
|
"Contact form message sent successfully"
|
||||||
);
|
);
|
||||||
ContactApiResponse::Ok(ContactResponse::success().into())
|
ContactApiResponse::Ok(ContactResponse::success().into())
|
||||||
}
|
}
|
||||||
@@ -216,15 +243,15 @@ impl ContactApi {
|
|||||||
"New contact form submission:\n\nName: {}\nEmail: {}\n\nMessage:\n{}",
|
"New contact form submission:\n\nName: {}\nEmail: {}\n\nMessage:\n{}",
|
||||||
request.name, request.email, request.message
|
request.name, request.email, request.message
|
||||||
);
|
);
|
||||||
tracing::event!(target: "email", tracing::Level::DEBUG, "Sending email content to recipient: {}", email_body);
|
tracing::event!(target: "backend::contact", tracing::Level::DEBUG, "Sending email content to recipient: {}", email_body);
|
||||||
let email = Message::builder()
|
let email = Message::builder()
|
||||||
.from(self.settings.try_sender_into_mailbox()?)
|
.from(self.settings.try_sender_into_mailbox()?)
|
||||||
.reply_to(request.try_into()?)
|
.reply_to(request.try_into()?)
|
||||||
.to(self.settings.try_recpient_into_mailbox()?)
|
.to(self.settings.try_recipient_into_mailbox()?)
|
||||||
.subject(format!("Contact Form: {}", request.name))
|
.subject(format!("Contact Form: {}", request.name))
|
||||||
.header(ContentType::TEXT_PLAIN)
|
.header(ContentType::TEXT_PLAIN)
|
||||||
.body(email_body)?;
|
.body(email_body)?;
|
||||||
tracing::event!(target: "contact", tracing::Level::DEBUG, "Email to be sent: {}", format!("{email:?}"));
|
tracing::event!(target: "backend::contact", tracing::Level::DEBUG, "Email to be sent: {}", format!("{email:?}"));
|
||||||
Ok(email)
|
Ok(email)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,23 +429,6 @@ mod tests {
|
|||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn smtp_transport_starttls_opportunistic() {
|
|
||||||
let settings = EmailSettings {
|
|
||||||
host: "smtp.example.com".to_string(),
|
|
||||||
port: 587,
|
|
||||||
user: "user@example.com".to_string(),
|
|
||||||
password: "password".to_string(),
|
|
||||||
from: "from@example.com".to_string(),
|
|
||||||
recipient: "to@example.com".to_string(),
|
|
||||||
tls: false,
|
|
||||||
starttls: Starttls::Opportunistic,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = SmtpTransport::try_from(&settings);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn smtp_transport_no_encryption_with_credentials() {
|
fn smtp_transport_no_encryption_with_credentials() {
|
||||||
let settings = EmailSettings {
|
let settings = EmailSettings {
|
||||||
@@ -1001,4 +1011,74 @@ mod tests {
|
|||||||
e => panic!("Expected CouldNotSendEmail, got {e:?}"),
|
e => panic!("Expected CouldNotSendEmail, got {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strip_control_chars_removes_null_bytes() {
|
||||||
|
let result = strip_control_chars("John\x00Doe", false);
|
||||||
|
assert_eq!(result, "JohnDoe");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn contact_request_sanatize_strips_all_control_chars() {
|
||||||
|
let mut request = ContactRequest {
|
||||||
|
name: "John\x00Doe".into(),
|
||||||
|
email: "john\x00@example.com".into(),
|
||||||
|
message: "Test\x00message".into(),
|
||||||
|
honeypot: None,
|
||||||
|
};
|
||||||
|
request.sanitize();
|
||||||
|
assert_eq!(request.name, "JohnDoe");
|
||||||
|
assert_eq!(request.email, "john@example.com");
|
||||||
|
assert_eq!(request.message, "Testmessage");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn contact_request_sanitize_preserves_newlines_in_message() {
|
||||||
|
let mut request = ContactRequest {
|
||||||
|
name: "John\nDoe".into(),
|
||||||
|
email: "john@example.com".into(),
|
||||||
|
message: "Line 1\nLine 2\r\nLine 3".into(),
|
||||||
|
honeypot: None,
|
||||||
|
};
|
||||||
|
request.sanitize();
|
||||||
|
assert_eq!(request.name, "JohnDoe");
|
||||||
|
assert_eq!(request.email, "john@example.com");
|
||||||
|
assert_eq!(request.message, "Line 1\nLine 2\nLine 3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn contact_request_sanatize_preserves_unicode_name() {
|
||||||
|
let mut request_jp = ContactRequest {
|
||||||
|
name: "田中さん".into(),
|
||||||
|
email: "tanaka@example.com".into(),
|
||||||
|
message: "こんにちは!".into(),
|
||||||
|
honeypot: None,
|
||||||
|
};
|
||||||
|
request_jp.sanitize();
|
||||||
|
assert_eq!(request_jp.name, "田中さん");
|
||||||
|
assert_eq!(request_jp.email, "tanaka@example.com");
|
||||||
|
assert_eq!(request_jp.message, "こんにちは!");
|
||||||
|
|
||||||
|
let mut request_ar = ContactRequest {
|
||||||
|
name: "عبدالله".into(),
|
||||||
|
email: "abdullah@example.com".into(),
|
||||||
|
message: "مرحباً".into(),
|
||||||
|
honeypot: None,
|
||||||
|
};
|
||||||
|
request_ar.sanitize();
|
||||||
|
assert_eq!(request_ar.name, "عبدالله");
|
||||||
|
assert_eq!(request_ar.email, "abdullah@example.com");
|
||||||
|
assert_eq!(request_ar.message, "مرحباً");
|
||||||
|
|
||||||
|
let mut request_uk = ContactRequest {
|
||||||
|
name: "Олексáндр".into(),
|
||||||
|
email: "oleksandr@example.com".into(),
|
||||||
|
message: "Привіт".into(),
|
||||||
|
honeypot: None,
|
||||||
|
};
|
||||||
|
request_uk.sanitize();
|
||||||
|
assert_eq!(request_uk.name, "Олексáндр");
|
||||||
|
assert_eq!(request_uk.email, "oleksandr@example.com");
|
||||||
|
assert_eq!(request_uk.message, "Привіт");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-7
@@ -28,11 +28,14 @@ impl HealthApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[cfg(test)]
|
||||||
async fn health_check_works() {
|
mod tests {
|
||||||
let app = crate::get_test_app();
|
#[tokio::test]
|
||||||
let cli = poem::test::TestClient::new(app);
|
async fn health_check_works() {
|
||||||
let resp = cli.get("/api/health").send().await;
|
let app = crate::get_test_app();
|
||||||
resp.assert_status_is_ok();
|
let cli = poem::test::TestClient::new(app);
|
||||||
resp.assert_text("").await;
|
let resp = cli.get("/api/health").send().await;
|
||||||
|
resp.assert_status_is_ok();
|
||||||
|
resp.assert_text("").await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ pub(crate) struct Api {
|
|||||||
|
|
||||||
impl From<&Settings> for Api {
|
impl From<&Settings> for Api {
|
||||||
fn from(value: &Settings) -> Self {
|
fn from(value: &Settings) -> Self {
|
||||||
let contact = contact::ContactApi::from(value.clone().email);
|
let contact = contact::ContactApi::from(value.email.clone());
|
||||||
let health = health::HealthApi;
|
let health = health::HealthApi;
|
||||||
let meta = meta::MetaApi::from(&value.application);
|
let meta = meta::MetaApi::from(&value.application);
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
+8
-44
@@ -163,12 +163,13 @@ impl EmailSettings {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns a `ContactError` if the email address in the `from` field cannot be parsed
|
/// Returns a `ContactError` if the email address in the `from`
|
||||||
/// into a valid mailbox. This can occur if:
|
/// field of `recipient` cannot be parsed into a valid mailbox.
|
||||||
|
/// This can occur if:
|
||||||
/// - The email address format is invalid
|
/// - The email address format is invalid
|
||||||
/// - The email address contains invalid characters
|
/// - The email address contains invalid characters
|
||||||
/// - The email address structure is malformed
|
/// - The email address structure is malformed
|
||||||
pub fn try_recpient_into_mailbox(
|
pub fn try_recipient_into_mailbox(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<lettre::message::Mailbox, crate::errors::ContactError> {
|
) -> Result<lettre::message::Mailbox, crate::errors::ContactError> {
|
||||||
Ok(self.recipient.parse::<lettre::message::Mailbox>()?)
|
Ok(self.recipient.parse::<lettre::message::Mailbox>()?)
|
||||||
@@ -196,8 +197,6 @@ pub enum Starttls {
|
|||||||
/// Never use STARTTLS (unencrypted connection)
|
/// Never use STARTTLS (unencrypted connection)
|
||||||
#[default]
|
#[default]
|
||||||
Never,
|
Never,
|
||||||
/// Use STARTTLS if available (opportunistic encryption)
|
|
||||||
Opportunistic,
|
|
||||||
/// Always use STARTTLS (required encryption)
|
/// Always use STARTTLS (required encryption)
|
||||||
Always,
|
Always,
|
||||||
}
|
}
|
||||||
@@ -208,10 +207,9 @@ impl TryFrom<&str> for Starttls {
|
|||||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||||
match value.to_lowercase().as_str() {
|
match value.to_lowercase().as_str() {
|
||||||
"off" | "no" | "never" => Ok(Self::Never),
|
"off" | "no" | "never" => Ok(Self::Never),
|
||||||
"opportunistic" => Ok(Self::Opportunistic),
|
|
||||||
"yes" | "always" => Ok(Self::Always),
|
"yes" | "always" => Ok(Self::Always),
|
||||||
other => Err(format!(
|
other => Err(format!(
|
||||||
"{other} is not a supported option. Use either `yes`, `no`, or `opportunistic`"
|
"{other} is not a supported option. Use either `yes` or `no`"
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,7 +232,6 @@ impl std::fmt::Display for Starttls {
|
|||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let self_str = match self {
|
let self_str = match self {
|
||||||
Self::Never => "never",
|
Self::Never => "never",
|
||||||
Self::Opportunistic => "opportunistic",
|
|
||||||
Self::Always => "always",
|
Self::Always => "always",
|
||||||
};
|
};
|
||||||
write!(f, "{self_str}")
|
write!(f, "{self_str}")
|
||||||
@@ -252,7 +249,7 @@ impl<'de> serde::Deserialize<'de> for Starttls {
|
|||||||
type Value = Starttls;
|
type Value = Starttls;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
formatter.write_str("a string or boolean representing STARTTLS setting (e.g., 'yes', 'no', 'opportunistic', true, false)")
|
formatter.write_str("a string or boolean representing STARTTLS setting (e.g., 'yes', 'no', true, false)")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_str<E>(self, value: &str) -> Result<Starttls, E>
|
fn visit_str<E>(self, value: &str) -> Result<Starttls, E>
|
||||||
@@ -434,13 +431,6 @@ mod tests {
|
|||||||
assert_eq!(result, Starttls::Always);
|
assert_eq!(result, Starttls::Always);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn startls_deserialize_from_string_opportunistic() {
|
|
||||||
let json = r#""opportunistic""#;
|
|
||||||
let result: Starttls = serde_json::from_str(json).unwrap();
|
|
||||||
assert_eq!(result, Starttls::Opportunistic);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn startls_deserialize_from_bool() {
|
fn startls_deserialize_from_bool() {
|
||||||
let json = "true";
|
let json = "true";
|
||||||
@@ -482,18 +472,6 @@ mod tests {
|
|||||||
assert_eq!(Starttls::try_from("Yes").unwrap(), Starttls::Always);
|
assert_eq!(Starttls::try_from("Yes").unwrap(), Starttls::Always);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn startls_try_from_str_opportunistic() {
|
|
||||||
assert_eq!(
|
|
||||||
Starttls::try_from("opportunistic").unwrap(),
|
|
||||||
Starttls::Opportunistic
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Starttls::try_from("OPPORTUNISTIC").unwrap(),
|
|
||||||
Starttls::Opportunistic
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn startls_try_from_str_invalid() {
|
fn startls_try_from_str_invalid() {
|
||||||
let result = Starttls::try_from("invalid");
|
let result = Starttls::try_from("invalid");
|
||||||
@@ -517,14 +495,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn startls_try_from_string_opportunistic() {
|
|
||||||
assert_eq!(
|
|
||||||
Starttls::try_from("opportunistic".to_string()).unwrap(),
|
|
||||||
Starttls::Opportunistic
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn startls_try_from_string_invalid() {
|
fn startls_try_from_string_invalid() {
|
||||||
let result = Starttls::try_from("invalid".to_string());
|
let result = Starttls::try_from("invalid".to_string());
|
||||||
@@ -553,12 +523,6 @@ mod tests {
|
|||||||
assert_eq!(startls.to_string(), "always");
|
assert_eq!(startls.to_string(), "always");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn startls_display_opportunistic() {
|
|
||||||
let startls = Starttls::Opportunistic;
|
|
||||||
assert_eq!(startls.to_string(), "opportunistic");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rate_limit_settings_default() {
|
fn rate_limit_settings_default() {
|
||||||
let settings = RateLimitSettings::default();
|
let settings = RateLimitSettings::default();
|
||||||
@@ -696,7 +660,7 @@ mod tests {
|
|||||||
tls: false,
|
tls: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = settings.try_recpient_into_mailbox();
|
let result = settings.try_recipient_into_mailbox();
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
let mailbox = result.unwrap();
|
let mailbox = result.unwrap();
|
||||||
assert_eq!(mailbox.email.to_string(), "recipient@example.com");
|
assert_eq!(mailbox.email.to_string(), "recipient@example.com");
|
||||||
@@ -715,7 +679,7 @@ mod tests {
|
|||||||
tls: false,
|
tls: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = settings.try_recpient_into_mailbox();
|
let result = settings.try_recipient_into_mailbox();
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+74
-34
@@ -6,10 +6,12 @@
|
|||||||
//! - Configuring CORS
|
//! - Configuring CORS
|
||||||
//! - Starting the HTTP server
|
//! - Starting the HTTP server
|
||||||
|
|
||||||
|
use poem::listener::{Listener, TcpAcceptor};
|
||||||
use poem::middleware::{AddDataEndpoint, Cors, CorsEndpoint};
|
use poem::middleware::{AddDataEndpoint, Cors, CorsEndpoint};
|
||||||
use poem::{EndpointExt, Route};
|
use poem::{EndpointExt, Route};
|
||||||
use poem_openapi::OpenApiService;
|
use poem_openapi::OpenApiService;
|
||||||
|
|
||||||
|
use crate::settings::Starttls;
|
||||||
use crate::{
|
use crate::{
|
||||||
middleware::rate_limit::{RateLimit, RateLimitConfig},
|
middleware::rate_limit::{RateLimit, RateLimitConfig},
|
||||||
route::Api,
|
route::Api,
|
||||||
@@ -18,10 +20,21 @@ use crate::{
|
|||||||
|
|
||||||
use crate::middleware::rate_limit::RateLimitEndpoint;
|
use crate::middleware::rate_limit::RateLimitEndpoint;
|
||||||
|
|
||||||
type Server = poem::Server<poem::listener::TcpListener<String>, std::convert::Infallible>;
|
type Server = poem::Server<poem::listener::BoxListener, std::convert::Infallible>;
|
||||||
|
|
||||||
/// The configured application with rate limiting, CORS, and settings data.
|
/// The configured application with rate limiting, CORS, and settings data.
|
||||||
pub type App = AddDataEndpoint<CorsEndpoint<RateLimitEndpoint<Route>>, Settings>;
|
pub type App = AddDataEndpoint<CorsEndpoint<RateLimitEndpoint<Route>>, Settings>;
|
||||||
|
|
||||||
|
struct PreBoundListener(std::net::TcpListener);
|
||||||
|
|
||||||
|
impl Listener for PreBoundListener {
|
||||||
|
type Acceptor = TcpAcceptor;
|
||||||
|
|
||||||
|
async fn into_acceptor(self) -> std::io::Result<Self::Acceptor> {
|
||||||
|
TcpAcceptor::from_std(self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Application builder that holds the server configuration before running.
|
/// Application builder that holds the server configuration before running.
|
||||||
pub struct Application {
|
pub struct Application {
|
||||||
server: Server,
|
server: Server,
|
||||||
@@ -77,13 +90,24 @@ impl From<Application> for RunnableApplication {
|
|||||||
"Rate limiting disabled (using very high limits)"
|
"Rate limiting disabled (using very high limits)"
|
||||||
);
|
);
|
||||||
// Use very high limits to effectively disable rate limiting
|
// Use very high limits to effectively disable rate limiting
|
||||||
RateLimitConfig::new(u32::MAX, 1)
|
RateLimitConfig::disabled()
|
||||||
|
};
|
||||||
|
let frontend_url = value.settings.frontend_url.clone();
|
||||||
|
let cors = if value.settings.debug {
|
||||||
|
Cors::new()
|
||||||
|
} else {
|
||||||
|
if !cfg!(test) {
|
||||||
|
assert!(
|
||||||
|
!frontend_url.is_empty(),
|
||||||
|
"CORS: frontend_url must be configured in production"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Cors::new().allow_origin(frontend_url)
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = value
|
let app = value
|
||||||
.app
|
.app
|
||||||
.with(RateLimit::new(&rate_limit_config))
|
.with(RateLimit::new(&rate_limit_config))
|
||||||
.with(Cors::new())
|
.with(cors)
|
||||||
.data(value.settings);
|
.data(value.settings);
|
||||||
|
|
||||||
let server = value.server;
|
let server = value.server;
|
||||||
@@ -93,45 +117,63 @@ impl From<Application> for RunnableApplication {
|
|||||||
|
|
||||||
impl Application {
|
impl Application {
|
||||||
fn setup_app(settings: &Settings) -> poem::Route {
|
fn setup_app(settings: &Settings) -> poem::Route {
|
||||||
|
Self::prevent_unencrypted_smtp_with_credentials(settings);
|
||||||
let api_service = OpenApiService::new(
|
let api_service = OpenApiService::new(
|
||||||
Api::from(settings).apis(),
|
Api::from(settings).apis(),
|
||||||
settings.application.clone().name,
|
settings.application.name.clone(),
|
||||||
settings.application.clone().version,
|
settings.application.version.clone(),
|
||||||
)
|
)
|
||||||
.url_prefix("/api");
|
.url_prefix("/api");
|
||||||
let ui = api_service.swagger_ui();
|
let ui = api_service.swagger_ui();
|
||||||
poem::Route::new()
|
let mut route = poem::Route::new().nest("/api", api_service.clone());
|
||||||
.nest("/api", api_service.clone())
|
if settings.debug {
|
||||||
.nest("/specs", api_service.spec_endpoint_yaml())
|
route = route
|
||||||
.nest("/", ui)
|
.nest("/", ui)
|
||||||
|
.nest("/specs", api_service.spec_endpoint_yaml());
|
||||||
|
}
|
||||||
|
route
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prevent_unencrypted_smtp_with_credentials(settings: &Settings) {
|
||||||
|
if !settings.email.tls
|
||||||
|
&& settings.email.starttls == Starttls::Never
|
||||||
|
&& !settings.email.user.is_empty()
|
||||||
|
&& settings.email.host != "localhost"
|
||||||
|
&& settings.email.host != "127.0.0.1"
|
||||||
|
{
|
||||||
|
panic!("Refusing to send SMTP credentials over cleartext to non-local host");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_server(
|
fn setup_server(
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
tcp_listener: Option<poem::listener::TcpListener<String>>,
|
tcp_listener: Option<std::net::TcpListener>,
|
||||||
) -> Server {
|
) -> (Server, u16, String) {
|
||||||
let tcp_listener = tcp_listener.unwrap_or_else(|| {
|
tcp_listener.map_or_else(
|
||||||
let address = format!(
|
|| {
|
||||||
"{}:{}",
|
let port = settings.application.port;
|
||||||
settings.application.host, settings.application.port
|
let host = settings.application.host.clone();
|
||||||
);
|
let address = format!("{host}:{port}");
|
||||||
poem::listener::TcpListener::bind(address)
|
let server = poem::Server::new(poem::listener::TcpListener::bind(address).boxed());
|
||||||
});
|
(server, port, host)
|
||||||
poem::Server::new(tcp_listener)
|
},
|
||||||
|
|listener| {
|
||||||
|
let addr = listener.local_addr().expect("Failed to get bound address");
|
||||||
|
let port = addr.port();
|
||||||
|
let host = addr.ip().to_string();
|
||||||
|
let server = poem::Server::new(PreBoundListener(listener).boxed());
|
||||||
|
(server, port, host)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a new application with the given settings and optional TCP listener.
|
/// Builds a new application with the given settings and optional TCP listener.
|
||||||
///
|
///
|
||||||
/// If no listener is provided, one will be created based on the settings.
|
/// If no listener is provided, one will be created based on the settings.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn build(
|
pub fn build(settings: Settings, tcp_listener: Option<std::net::TcpListener>) -> Self {
|
||||||
settings: Settings,
|
let (server, port, host) = Self::setup_server(&settings, tcp_listener);
|
||||||
tcp_listener: Option<poem::listener::TcpListener<String>>,
|
|
||||||
) -> Self {
|
|
||||||
let port = settings.application.port;
|
|
||||||
let host = settings.application.clone().host;
|
|
||||||
let app = Self::setup_app(&settings);
|
let app = Self::setup_app(&settings);
|
||||||
let server = Self::setup_server(&settings, tcp_listener);
|
|
||||||
Self {
|
Self {
|
||||||
server,
|
server,
|
||||||
app,
|
app,
|
||||||
@@ -149,8 +191,8 @@ impl Application {
|
|||||||
|
|
||||||
/// Returns the host address the application is configured to bind to.
|
/// Returns the host address the application is configured to bind to.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn host(&self) -> String {
|
pub fn host(&self) -> &str {
|
||||||
self.host.clone()
|
&self.host
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the port the application is configured to bind to.
|
/// Returns the port the application is configured to bind to.
|
||||||
@@ -216,13 +258,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn application_with_custom_listener() {
|
fn application_with_custom_listener() {
|
||||||
let settings = create_test_settings();
|
let settings = create_test_settings();
|
||||||
let tcp_listener =
|
let listener =
|
||||||
std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port");
|
std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port");
|
||||||
let port = tcp_listener.local_addr().unwrap().port();
|
let expected_port = listener.local_addr().unwrap().port();
|
||||||
let listener = poem::listener::TcpListener::bind(format!("127.0.0.1:{port}"));
|
|
||||||
|
|
||||||
let app = Application::build(settings, Some(listener));
|
let app = Application::build(settings, Some(listener));
|
||||||
assert_eq!(app.host(), "127.0.0.1");
|
assert_eq!(app.host(), "127.0.0.1");
|
||||||
assert_eq!(app.port(), 8080);
|
assert_eq!(app.port(), expected_port);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-8
@@ -14,16 +14,13 @@ pub fn get_subscriber(debug: bool) -> impl tracing::Subscriber + Send + Sync {
|
|||||||
let env_filter = if debug { "debug" } else { "info" }.to_string();
|
let env_filter = if debug { "debug" } else { "info" }.to_string();
|
||||||
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(env_filter));
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(env_filter));
|
||||||
let stdout_log = tracing_subscriber::fmt::layer().pretty();
|
let subscriber = tracing_subscriber::Registry::default().with(env_filter);
|
||||||
let subscriber = tracing_subscriber::Registry::default()
|
let (stdout_log, json_log) = if debug {
|
||||||
.with(env_filter)
|
(Some(tracing_subscriber::fmt::layer().pretty()), None)
|
||||||
.with(stdout_log);
|
|
||||||
let json_log = if debug {
|
|
||||||
None
|
|
||||||
} else {
|
} else {
|
||||||
Some(tracing_subscriber::fmt::layer().json())
|
(None, Some(tracing_subscriber::fmt::layer().json()))
|
||||||
};
|
};
|
||||||
subscriber.with(json_log)
|
subscriber.with(stdout_log).with(json_log)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes the global tracing subscriber.
|
/// Initializes the global tracing subscriber.
|
||||||
|
|||||||
Reference in New Issue
Block a user