mirror of
https://git.dirksys.ovh/dirk/bankserver.git
synced 2025-12-19 18:49:20 +01:00
more work and ci
This commit is contained in:
parent
0af10df617
commit
f9d9639244
79
.forgejo/workflows/ci.yaml
Normal file
79
.forgejo/workflows/ci.yaml
Normal file
@ -0,0 +1,79 @@
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
ATTIC_ENDPOINT: ${{ vars.ATTIC_ENDPOINT }}
|
||||
ATTIC_CACHE: ${{ vars.ATTIC_CACHE }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: nix
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Configure Attic
|
||||
shell: bash
|
||||
run: |
|
||||
attic login --set-default central "$ATTIC_ENDPOINT" "$ATTIC_TOKEN"
|
||||
attic use "$ATTIC_CACHE"
|
||||
env:
|
||||
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}
|
||||
- name: Build
|
||||
run: nix-fast-build --skip-cached --no-nom --attic-cache $ATTIC_CACHE --flake .#packages
|
||||
- name: Process results
|
||||
run: |
|
||||
mkdir images
|
||||
for file in result-*dockerImage*; do
|
||||
if [ ! -f "$file" ]; then
|
||||
continue
|
||||
fi
|
||||
info=$(echo "$file" | sd '^result-([a-z0-9_-]+)\.([a-zA-Z]+)(?:-cross-([a-z0-9_-]+))?$' '$1\x1F$2\x1F$3')
|
||||
IFS=$'\x1F' read -r hostArch name crossArch <<< "$info"
|
||||
arch=${crossArch:-$hostArch}
|
||||
containerArch=$(arch=$arch nix eval --raw --impure --expr '(import <nixpkgs> { system = builtins.getEnv "arch";}).go.GOARCH')
|
||||
mv $file images/image-$containerArch.tar.gz
|
||||
done
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docker-images
|
||||
path: images/
|
||||
- name: Login to Forgejo Container Registry
|
||||
if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch'
|
||||
shell: bash
|
||||
run: podman login git.dirksys.ovh --username $USERNAME --password $PASSWORD
|
||||
env:
|
||||
USERNAME: ${{ github.actor }}
|
||||
PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Push docker images
|
||||
if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch'
|
||||
shell: bash
|
||||
run: |
|
||||
manifest_id=$(podman manifest create git.dirksys.ovh/dirk/bankserver:latest)
|
||||
sleep 1
|
||||
for file in images/*; do
|
||||
echo "Loading $file"
|
||||
podman image load < $file
|
||||
image_id=$(podman image ls --format '{{.ID}}' | head -n 2 | tail -1)
|
||||
architecture=$(podman image inspect $image_id | jq -r ".[0].Architecture")
|
||||
tag=$(podman image ls --format "{{.Tag}}" | head -n 2 | tail -1)
|
||||
tag="git.dirksys.ovh/dirk/bankserver:$tag-$architecture"
|
||||
podman image untag $image_id
|
||||
echo "Adding $architecture image to manifest"
|
||||
podman manifest add $manifest_id $image_id
|
||||
done
|
||||
echo "Pushing manifest"
|
||||
podman manifest push git.dirksys.ovh/dirk/bankserver:latest
|
||||
- name: Notify server
|
||||
if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
run: |
|
||||
eval $(ssh-agent -s)
|
||||
ssh-add <(echo "$DEPLOY_KEY")
|
||||
mkdir -p ~/.ssh
|
||||
echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
|
||||
ssh $DEPLOY_USER@$DEPLOY_HOST "systemctl try-restart podman-deployment-bankserver-staging"
|
||||
sleep 2
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,4 @@
|
||||
/target
|
||||
/result-*
|
||||
/result
|
||||
/schemas
|
||||
|
||||
1
.helix/ignore
Normal file
1
.helix/ignore
Normal file
@ -0,0 +1 @@
|
||||
/schemas
|
||||
502
Cargo.lock
generated
502
Cargo.lock
generated
@ -26,6 +26,33 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"cpufeatures",
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.86"
|
||||
@ -117,15 +144,24 @@ name = "bankserver"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
"config",
|
||||
"dbmigrator",
|
||||
"deadpool",
|
||||
"deadpool-postgres",
|
||||
"futures-util",
|
||||
"garde",
|
||||
"jsonwebtoken",
|
||||
"password-auth",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-postgres",
|
||||
"tracing",
|
||||
"tracing-error",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -134,12 +170,27 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@ -168,10 +219,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.15"
|
||||
name = "castaway"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af"
|
||||
checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
@ -182,6 +242,53 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"rustversion",
|
||||
"ryu",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "config"
|
||||
version = "0.15.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cf9dc8d4ef88e27a8cb23e85cb116403dedd57f7971964dc4b18ccead548901"
|
||||
dependencies = [
|
||||
"pathdiff",
|
||||
"serde",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@ -201,6 +308,44 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbmigrator"
|
||||
version = "0.4.4-alpha"
|
||||
source = "git+https://github.com/DSeeLP/dbmigrator.git?branch=macros#8075e58f28110cd8b2db97048ae0eb1f87acf54a"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dbmigrator_core",
|
||||
"dbmigrator_macros",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-postgres",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbmigrator_core"
|
||||
version = "0.4.3-alpha"
|
||||
source = "git+https://github.com/DSeeLP/dbmigrator.git?branch=macros#8075e58f28110cd8b2db97048ae0eb1f87acf54a"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
"version-compare",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbmigrator_macros"
|
||||
version = "0.4.3-alpha"
|
||||
source = "git+https://github.com/DSeeLP/dbmigrator.git?branch=macros#8075e58f28110cd8b2db97048ae0eb1f87acf54a"
|
||||
dependencies = [
|
||||
"dbmigrator_core",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool"
|
||||
version = "0.12.2"
|
||||
@ -237,6 +382,15 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@ -248,6 +402,12 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.2.0"
|
||||
@ -323,6 +483,32 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "garde"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a989bd2fd12136080f7825ff410d9239ce84a2a639487fc9d924ee42e2fb84f"
|
||||
dependencies = [
|
||||
"compact_str",
|
||||
"garde_derive",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "garde_derive"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f7f0545bbbba0a37d4d445890fa5759814e0716f02417b39f6fab292193df68"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@ -460,6 +646,29 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.14"
|
||||
@ -571,7 +780,7 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -584,6 +793,21 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.16.0"
|
||||
@ -638,6 +862,35 @@ dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-auth"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a2a4764cc1f8d961d802af27193c6f4f0124bd0e76e8393cf818e18880f0524"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"getrandom 0.2.15",
|
||||
"password-hash",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
@ -699,11 +952,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
"fallible-iterator",
|
||||
"postgres-protocol",
|
||||
"time",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.20"
|
||||
@ -738,8 +999,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"zerocopy 0.8.20",
|
||||
"rand_core 0.9.2",
|
||||
"zerocopy 0.8.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -749,7 +1010,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -759,7 +1029,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a509b1a2ffbe92afab0e55c8fd99dea1c280e8171bd2d88682bb20bc41cbc2c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"zerocopy 0.8.20",
|
||||
"zerocopy 0.8.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -771,6 +1041,26 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931"
|
||||
dependencies = [
|
||||
"ref-cast-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast-impl"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
@ -826,7 +1116,7 @@ dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -847,6 +1137,42 @@ version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "1.0.0-alpha.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88ef2a6523400a2228db974a8ddc9e1d3deaa04f51bddd7832ef8d7e531bafcc"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "1.0.0-alpha.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6d4e1945a3c9e58edaa708449b026519f7f4197185e1b5dbc689615c1ad724d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@ -873,6 +1199,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.29.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.139"
|
||||
@ -953,6 +1290,9 @@ name = "smallvec"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
@ -961,9 +1301,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@ -998,6 +1344,26 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.8"
|
||||
@ -1008,6 +1374,37 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.8.1"
|
||||
@ -1037,7 +1434,7 @@ dependencies = [
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1151,6 +1548,16 @@ dependencies = [
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-error"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
|
||||
dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
@ -1224,6 +1631,10 @@ name = "uuid"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
|
||||
dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
@ -1231,12 +1642,28 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
@ -1266,6 +1693,7 @@ checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
@ -1352,12 +1780,36 @@ 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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@ -1367,6 +1819,15 @@ dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
@ -1431,6 +1892,15 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.33.0"
|
||||
@ -1452,11 +1922,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.20"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dde3bb8c68a8f3f1ed4ac9221aad6b10cece3e60a8e2ea54a6a2dec806d0084c"
|
||||
checksum = "dcf01143b2dd5d134f11f545cf9f1431b13b749695cb33bcce051e7568f99478"
|
||||
dependencies = [
|
||||
"zerocopy-derive 0.8.20",
|
||||
"zerocopy-derive 0.8.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1472,9 +1942,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.20"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eea57037071898bf96a6da35fd626f4f27e9cee3ead2a6c703cf09d472b2e700"
|
||||
checksum = "712c8386f4f4299382c9abee219bee7084f78fb939d88b6840fcc1320d5f6da2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
17
Cargo.toml
17
Cargo.toml
@ -3,14 +3,27 @@ name = "bankserver"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = ["schemas"]
|
||||
schemas = ["dep:schemars"]
|
||||
|
||||
[dependencies]
|
||||
axum = "0.8"
|
||||
chrono = { version = "0.4.40", features = ["serde"] }
|
||||
config = { version = "0.15.8", default-features = false }
|
||||
dbmigrator = { git = "https://github.com/DSeeLP/dbmigrator.git", branch = "macros", version = "0.4.4-alpha", features = ["tokio-postgres"] }
|
||||
deadpool = "0.12"
|
||||
deadpool-postgres = { version = "0.14", features = ["serde"] }
|
||||
futures-util = "0.3.31"
|
||||
garde = { version = "0.22.0", features = ["serde", "derive", "regex", "pattern"] }
|
||||
jsonwebtoken = { version = "9.3", default-features = false }
|
||||
password-auth = "1.0.0"
|
||||
schemars = { version = "1.0.0-alpha.17", optional = true, features = ["chrono04", "uuid1"] }
|
||||
serde = { version = "1.0.218", features = ["derive"] }
|
||||
serde_json = "1.0.139"
|
||||
tokio = { version = "1.43", features = ["tracing", "time", "sync", "net", "io-std", "io-util", "macros"] }
|
||||
tokio-postgres = { version = "0.7.13", features = ["with-uuid-1"] }
|
||||
tokio = { version = "1.43", features = ["tracing", "time", "sync", "net", "io-std", "io-util", "macros", "rt-multi-thread"] }
|
||||
tokio-postgres = { version = "0.7.13", features = ["with-chrono-0_4", "with-uuid-1"] }
|
||||
tracing = "0.1.41"
|
||||
tracing-error = "0.2.1"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
uuid = { version = "1.15.1", features = ["serde", "v7"] }
|
||||
|
||||
23
flake.lock
generated
23
flake.lock
generated
@ -14,7 +14,28 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1740709839,
|
||||
"narHash": "sha256-4dF++MXIXna/AwlZWDKr7bgUmY4xoEwvkF1GewjNrt0=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "b4270835bf43c6f80285adac6f66a26d83f0f277",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
72
flake.nix
72
flake.nix
@ -3,25 +3,83 @@
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "flake:nixpkgs";
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ self, nixpkgs }:
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
rust-overlay,
|
||||
}:
|
||||
let
|
||||
forAllSystems =
|
||||
function:
|
||||
nixpkgs.lib.genAttrs [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
] (system: function nixpkgs.legacyPackages.${system});
|
||||
nixpkgs.lib.genAttrs
|
||||
[
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
]
|
||||
(
|
||||
system:
|
||||
function (
|
||||
import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [
|
||||
(import rust-overlay)
|
||||
(_: final: overlay final)
|
||||
];
|
||||
}
|
||||
)
|
||||
);
|
||||
overlay =
|
||||
pkgs:
|
||||
let
|
||||
rustVersion = pkgs.pkgsBuildHost.rust-bin.stable.latest.minimal;
|
||||
rustPlatform = pkgs.makeRustPlatform {
|
||||
cargo = rustVersion;
|
||||
rustc = rustVersion;
|
||||
};
|
||||
bankserver = pkgs.callPackage ./package.nix {
|
||||
inherit rustPlatform;
|
||||
rev = self.shortRev or "dirty";
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit bankserver;
|
||||
bankserverDocker = pkgs.dockerTools.buildLayeredImage {
|
||||
name = "bankserver";
|
||||
# tag = "latest";
|
||||
created = "now";
|
||||
contents = [ bankserver ];
|
||||
config = {
|
||||
Env = [ "HOST=::" ];
|
||||
Entrypoint = [ "/bin/bankserver" ];
|
||||
ExposedPorts = {
|
||||
"3845/tcp" = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
packages = forAllSystems (pkgs: {
|
||||
default = pkgs.callPackage ./package.nix { rev = self.shortRev or "dirty"; };
|
||||
default = pkgs.bankserver;
|
||||
dockerImage = pkgs.bankserverDocker;
|
||||
dockerImage-cross-aarch64-linux = pkgs.pkgsCross.aarch64-multiplatform.bankserverDocker;
|
||||
});
|
||||
devShells = forAllSystems (pkgs: {
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [ hurl ];
|
||||
packages = with pkgs; [
|
||||
hurl
|
||||
redocly
|
||||
yq-go
|
||||
just
|
||||
];
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
12
justfile
Normal file
12
justfile
Normal file
@ -0,0 +1,12 @@
|
||||
[private]
|
||||
default:
|
||||
just --list
|
||||
|
||||
dev:
|
||||
just openapi
|
||||
cargo run
|
||||
|
||||
openapi:
|
||||
yq eval-all -n 'load("openapi-def.yaml") *n load("schemas/schemas.json")' > openapi-temp.yaml
|
||||
redocly bundle openapi-temp.yaml -o openapi.json
|
||||
rm openapi-temp.yaml
|
||||
25
migrations/000000_baseline.sql
Normal file
25
migrations/000000_baseline.sql
Normal file
@ -0,0 +1,25 @@
|
||||
create table users(
|
||||
id uuid primary key,
|
||||
name varchar(32) not null unique,
|
||||
password varchar(128)
|
||||
);
|
||||
|
||||
create table accounts(
|
||||
id uuid primary key,
|
||||
"user" uuid not null references users(id),
|
||||
name varchar(32) not null unique,
|
||||
balance bigint not null default 0 constraint positive_balance check (balance >= 0)
|
||||
);
|
||||
|
||||
create table transactions(
|
||||
"from" uuid not null references accounts(id),
|
||||
"to" uuid not null references accounts(id),
|
||||
amount bigint not null constraint positive_amount check (amount > 0),
|
||||
timestamp timestamp without time zone not null default now(),
|
||||
message text
|
||||
);
|
||||
|
||||
create index transactions_from on transactions ("from");
|
||||
create index transactions_to on transactions ("to");
|
||||
|
||||
create view transactions_with_user as select t.*, (select user from accounts where id = t.from) as from_user, (select user from accounts where id = t.to) as to_user from transactions t;
|
||||
302
openapi-def.yaml
Normal file
302
openapi-def.yaml
Normal file
@ -0,0 +1,302 @@
|
||||
# yaml-language-server: $schema=https://spec.openapis.org/oas/3.1/schema/2024-11-14
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Bankserver
|
||||
version: 0.0.1
|
||||
tags:
|
||||
- name: Authentication
|
||||
- name: Users
|
||||
- name: Accounts
|
||||
- name: Transactions
|
||||
paths:
|
||||
/api/login:
|
||||
post:
|
||||
operationId: login
|
||||
tags:
|
||||
- Authentication
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Credentials'
|
||||
responses:
|
||||
200:
|
||||
description: Login successful
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LoginSuccess'
|
||||
default:
|
||||
$ref: '#/components/responses/Default'
|
||||
/api/register:
|
||||
post:
|
||||
operationId: register
|
||||
tags:
|
||||
- Authentication
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Credentials'
|
||||
responses:
|
||||
201:
|
||||
description: Registration successful
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RegisterSuccess'
|
||||
409:
|
||||
description: User already exists
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiError'
|
||||
example:
|
||||
id: conflict
|
||||
message: string
|
||||
default:
|
||||
$ref: '#/components/responses/Default'
|
||||
|
||||
/api/users/{userId}:
|
||||
get:
|
||||
operationId: user-info
|
||||
summary: User Info
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UserId'
|
||||
tags:
|
||||
- Users
|
||||
security:
|
||||
- bearer: []
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserInfo'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
default:
|
||||
$ref: '#/components/responses/Default'
|
||||
/api/users/@me:
|
||||
get:
|
||||
operationId: self-get-info
|
||||
summary: User Info
|
||||
tags:
|
||||
- Users
|
||||
security:
|
||||
- bearer: []
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserInfo'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
default:
|
||||
$ref: '#/components/responses/Default'
|
||||
/api/users/@me/balance:
|
||||
get:
|
||||
operationId: self-get-balance
|
||||
summary: Sum of all account balances
|
||||
tags:
|
||||
- Users
|
||||
security:
|
||||
- bearer: []
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserBalance'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
default:
|
||||
$ref: '#/components/responses/Default'
|
||||
/api/users/@me/accounts:
|
||||
get:
|
||||
operationId: self-list-accounts
|
||||
summary: User accounts
|
||||
tags:
|
||||
- Users
|
||||
- Accounts
|
||||
security:
|
||||
- bearer: []
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserAccounts'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
default:
|
||||
$ref: '#/components/responses/Default'
|
||||
/api/users/@me/transactions:
|
||||
get:
|
||||
operationId: self-transaction-history
|
||||
summary: Transaction history
|
||||
tags:
|
||||
- Users
|
||||
- Transactions
|
||||
security:
|
||||
- bearer: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Direction'
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TransactionHistory'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
default:
|
||||
$ref: '#/components/responses/Default'
|
||||
/api/transactions:
|
||||
post:
|
||||
operationId: pay
|
||||
summary: Make payment
|
||||
tags:
|
||||
- Transactions
|
||||
security:
|
||||
- bearer: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MakePayment'
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Transaction'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
description: Forbidden
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiError'
|
||||
default:
|
||||
$ref: '#/components/responses/Default'
|
||||
/api/accounts:
|
||||
get:
|
||||
operationId: accounts-list-all
|
||||
summary: List all accounts
|
||||
tags:
|
||||
- Accounts
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ListAccounts'
|
||||
default:
|
||||
$ref: '#/components/responses/Default'
|
||||
|
||||
/api/accounts/{accountId}/transactions:
|
||||
get:
|
||||
operationId: account-transaction-history
|
||||
summary: Transaction history
|
||||
tags:
|
||||
- Accounts
|
||||
- Transactions
|
||||
security:
|
||||
- bearer: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/AccountId'
|
||||
- $ref: '#/components/parameters/Direction'
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TransactionHistory'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
default:
|
||||
$ref: '#/components/responses/Default'
|
||||
/api/users:
|
||||
get:
|
||||
operationId: users-list-all
|
||||
summary: List all users
|
||||
tags:
|
||||
- Users
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ListUsers'
|
||||
default:
|
||||
$ref: '#/components/responses/Default'
|
||||
components:
|
||||
parameters:
|
||||
Direction:
|
||||
name: direction
|
||||
in: query
|
||||
schema:
|
||||
$ref: '#/components/schemas/Direction'
|
||||
required: false
|
||||
AccountId:
|
||||
name: accountId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
UserId:
|
||||
name: userId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
securitySchemes:
|
||||
bearer:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
responses:
|
||||
InternalServerEror:
|
||||
description: Internal Server Error
|
||||
Default:
|
||||
description: Other Errors
|
||||
Unauthorized:
|
||||
description: Access token is missing or invalid
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiError'
|
||||
examples:
|
||||
missing_header:
|
||||
value:
|
||||
id: auth.missing_header
|
||||
message: string
|
||||
invalid_jwt:
|
||||
value:
|
||||
id: auth.jwt.invalid
|
||||
message: string
|
||||
UnprocessableEntity:
|
||||
description: Unprocessable Entity
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiError'
|
||||
examples:
|
||||
malformed_body:
|
||||
value:
|
||||
id: malformed_body
|
||||
message: string
|
||||
1
openapi.json
Normal file
1
openapi.json
Normal file
@ -0,0 +1 @@
|
||||
|
||||
31
package.nix
31
package.nix
@ -1,12 +1,41 @@
|
||||
{
|
||||
lib,
|
||||
rustPlatform,
|
||||
stdenv,
|
||||
redocly,
|
||||
yq-go,
|
||||
targetPlatform,
|
||||
rev ? "dirty",
|
||||
}:
|
||||
rustPlatform.buildRustPackage {
|
||||
pname = "bankingserver";
|
||||
version = "unstable-${rev}";
|
||||
|
||||
src = ./.;
|
||||
src = lib.cleanSource ./.;
|
||||
|
||||
nativeBuildInputs = [
|
||||
redocly
|
||||
yq-go
|
||||
];
|
||||
|
||||
preBuild = ''
|
||||
cargo test --features schemas --target ${stdenv.buildPlatform.rust.rustcTarget}
|
||||
|
||||
yq eval-all -n 'load("openapi-def.yaml") *n load("schemas/schemas.json")' > openapi-temp.yaml
|
||||
redocly bundle openapi-temp.yaml -o openapi.json
|
||||
'';
|
||||
|
||||
buildType = "debug";
|
||||
|
||||
useFetchCargoVendor = false;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
cargoLock.outputHashes = {
|
||||
"dbmigrator-0.4.4-alpha" = "sha256-Nwxw74IyZeZ9dODb+aneQmuQe0grO+g45B3zv1XaihE=";
|
||||
};
|
||||
|
||||
CARGO_BUILD_TARGET = targetPlatform.config;
|
||||
|
||||
TARGET_CC = "${stdenv.cc}/bin/${stdenv.cc.targetPrefix}cc";
|
||||
|
||||
doCheck = false;
|
||||
}
|
||||
|
||||
26
src/api/account.rs
Normal file
26
src/api/account.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{Router, extract::Path, routing::get};
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::model::AccountInfo;
|
||||
|
||||
use super::{AppState, EState, State, auth::Auth, make_schemas};
|
||||
|
||||
pub(super) fn router() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/{id}", get(account_info))
|
||||
.route("/", get(list_accounts))
|
||||
}
|
||||
|
||||
make_schemas!((); (ListAccounts));
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
pub struct ListAccounts {
|
||||
accounts: Vec<AccountInfo>,
|
||||
}
|
||||
|
||||
pub async fn account_info(EState(state): State, _: Auth, Path(id): Path<Uuid>) {}
|
||||
pub async fn list_accounts(EState(state): State, _: Auth) {}
|
||||
216
src/api/auth.rs
Normal file
216
src/api/auth.rs
Normal file
@ -0,0 +1,216 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
extract::FromRequestParts,
|
||||
http::{StatusCode, request::Parts},
|
||||
routing::post,
|
||||
};
|
||||
use garde::Validate;
|
||||
use jsonwebtoken::{Algorithm, Header, Validation};
|
||||
use password_auth::verify_password;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
api::{ApiError, InnerError},
|
||||
model::User,
|
||||
};
|
||||
|
||||
use super::{AppState, EState, Error, Json, State, make_schemas};
|
||||
|
||||
pub(super) fn router() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/register", post(register))
|
||||
.route("/login", post(login))
|
||||
}
|
||||
|
||||
make_schemas!((Credentials); (RegisterSuccess, LoginSuccess));
|
||||
|
||||
#[derive(Deserialize, Validate)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
pub struct Credentials {
|
||||
#[garde(length(min = 3, max = 32), alphanumeric, pattern("^[a-z0-9_-]+$"))]
|
||||
pub name: String,
|
||||
#[garde(length(min = 8, max = 96))]
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Credentials {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Credentials")
|
||||
.field("name", &self.name)
|
||||
.field("password", &"...")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
async fn register(
|
||||
EState(state): State,
|
||||
Json(credentials): Json<Credentials>,
|
||||
) -> Result<(StatusCode, Json<RegisterSuccess>), Error> {
|
||||
let mut conn = state.conn().await?;
|
||||
let id = User::create(&mut conn, &credentials.name, &credentials.password).await?;
|
||||
let token = Claims::new(id).encode(&state.encoding_key).unwrap();
|
||||
Ok((StatusCode::CREATED, Json(RegisterSuccess { token })))
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
async fn login(
|
||||
EState(state): State,
|
||||
Json(credentials): Json<Credentials>,
|
||||
) -> Result<Json<LoginSuccess>, Error> {
|
||||
let conn = state.conn().await?;
|
||||
let Some((hash, info)) =
|
||||
User::get_password_and_info_by_username(&conn, &credentials.name).await?
|
||||
else {
|
||||
return Err(invalid_username_or_password().into());
|
||||
};
|
||||
verify_password(credentials.password, &hash).map_err(|err| match err {
|
||||
password_auth::VerifyError::Parse(parse_error) => {
|
||||
Error::new(InnerError::PHCParse(parse_error))
|
||||
}
|
||||
password_auth::VerifyError::PasswordInvalid => invalid_username_or_password().into(),
|
||||
})?;
|
||||
let jwt = Claims::new(info.id).encode(&state.encoding_key).unwrap();
|
||||
Ok(Json(LoginSuccess { token: jwt }))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
pub struct LoginSuccess {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
pub struct RegisterSuccess {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Claims<'a> {
|
||||
#[serde(rename = "iss")]
|
||||
issuer: Cow<'a, str>,
|
||||
#[serde(rename = "sub")]
|
||||
subject: Uuid,
|
||||
#[serde(rename = "iat")]
|
||||
issued_at: u64,
|
||||
#[serde(rename = "exp")]
|
||||
expires: u64,
|
||||
}
|
||||
|
||||
fn duration_to_unix(duration: Duration) -> u64 {
|
||||
duration.as_millis() as u64
|
||||
}
|
||||
|
||||
impl<'a> Claims<'a> {
|
||||
pub fn new(id: Uuid) -> Self {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap();
|
||||
Self {
|
||||
issuer: Cow::Borrowed("bankserver"),
|
||||
subject: id,
|
||||
issued_at: duration_to_unix(now),
|
||||
expires: duration_to_unix(now + Duration::from_secs(60 * 60 * 3)),
|
||||
}
|
||||
}
|
||||
pub fn encode(
|
||||
&self,
|
||||
key: &jsonwebtoken::EncodingKey,
|
||||
) -> Result<String, jsonwebtoken::errors::Error> {
|
||||
jsonwebtoken::encode(&Header::new(Algorithm::HS512), self, key)
|
||||
}
|
||||
|
||||
pub fn decode(
|
||||
token: &str,
|
||||
key: &jsonwebtoken::DecodingKey,
|
||||
) -> Result<Self, jsonwebtoken::errors::Error> {
|
||||
Ok(jsonwebtoken::decode(token, key, &Validation::new(Algorithm::HS512))?.claims)
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_username_or_password() -> ApiError<'static> {
|
||||
ApiError::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
"invalid_username_or_password",
|
||||
"Invalid username or password",
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Auth {
|
||||
user: Uuid,
|
||||
}
|
||||
|
||||
impl Auth {
|
||||
pub const fn user_id(&self) -> Uuid {
|
||||
self.user
|
||||
}
|
||||
}
|
||||
|
||||
enum AuthError {
|
||||
MissingHeader,
|
||||
InvalidHeader,
|
||||
}
|
||||
|
||||
impl From<AuthError> for InnerError {
|
||||
fn from(value: AuthError) -> Self {
|
||||
match value {
|
||||
AuthError::MissingHeader => InnerError::Plain(ApiError::const_new(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"auth.missing_header",
|
||||
"Missing Authorization header",
|
||||
)),
|
||||
AuthError::InvalidHeader => InnerError::Plain(ApiError::const_new(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"auth.jwt.invalid",
|
||||
"Invalid JWT",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_header(parts: &Parts) -> Result<&str, AuthError> {
|
||||
let header = parts
|
||||
.headers
|
||||
.get("Authorization")
|
||||
.ok_or(AuthError::MissingHeader)?;
|
||||
let header = header.to_str().map_err(|_| AuthError::InvalidHeader)?;
|
||||
let token = header
|
||||
.strip_prefix("Bearer ")
|
||||
.ok_or(AuthError::InvalidHeader)?;
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
impl FromRequestParts<Arc<AppState>> for Auth {
|
||||
type Rejection = Error;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
state: &Arc<AppState>,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let token = auth_header(parts)?;
|
||||
let claims = Claims::decode(token, &state.decoding_key).map_err(|err| {
|
||||
let _ = 0;
|
||||
match err.kind() {
|
||||
jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
|
||||
ApiError::const_new(StatusCode::UNAUTHORIZED, "auth.jwt.expired", "Expired JWT")
|
||||
}
|
||||
_ => {
|
||||
ApiError::const_new(StatusCode::UNAUTHORIZED, "auth.jwt.invalid", "Invalid JWT")
|
||||
}
|
||||
}
|
||||
})?;
|
||||
Ok(Self {
|
||||
user: claims.subject,
|
||||
})
|
||||
}
|
||||
}
|
||||
46
src/api/docs.rs
Normal file
46
src/api/docs.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
http::{HeaderValue, StatusCode},
|
||||
response::{Html, IntoResponse, Response},
|
||||
routing::get,
|
||||
};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
static OPENAPI_JSON: &'static str = include_str!("../../openapi.json");
|
||||
|
||||
static SCALAR_DOCS: &'static str = include_str!("./docs/scalar.html");
|
||||
static SWAGGER_DOCS: &'static str = include_str!("./docs/swagger.html");
|
||||
static RAPIDOC_DOCS: &'static str = include_str!("./docs/rapidoc.html");
|
||||
|
||||
pub(super) fn router() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/openapi.json", get(openapi_json))
|
||||
.route("/scalar", get(scalar_html))
|
||||
.route("/swagger", get(swagger_html))
|
||||
.route("/rapidoc", get(rapidoc_html))
|
||||
}
|
||||
|
||||
fn with_content_type(body: &'static str, content_type: &'static str) -> Response {
|
||||
let mut response = (StatusCode::OK, body).into_response();
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("Content-Type", HeaderValue::from_static(content_type));
|
||||
response
|
||||
}
|
||||
|
||||
async fn openapi_json() -> Response {
|
||||
with_content_type(OPENAPI_JSON, "application/json")
|
||||
}
|
||||
|
||||
async fn scalar_html() -> Html<&'static str> {
|
||||
Html(SCALAR_DOCS)
|
||||
}
|
||||
async fn swagger_html() -> Html<&'static str> {
|
||||
Html(SWAGGER_DOCS)
|
||||
}
|
||||
async fn rapidoc_html() -> Html<&'static str> {
|
||||
Html(RAPIDOC_DOCS)
|
||||
}
|
||||
13
src/api/docs/rapidoc.html
Normal file
13
src/api/docs/rapidoc.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html> <!-- Important: must specify -->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8"> <!-- Important: rapi-doc uses utf8 characters -->
|
||||
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<rapi-doc spec-url="openapi.json"> </rapi-doc>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
16
src/api/docs/scalar.html
Normal file
16
src/api/docs/scalar.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Scalar API Reference</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Need a Custom Header? Check out this example https://codepen.io/scalarorg/pen/VwOXqam -->
|
||||
<script id="api-reference" data-url="openapi.json"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
32
src/api/docs/swagger.html
Normal file
32
src/api/docs/swagger.html
Normal file
@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="SwaggerUI" />
|
||||
<title>SwaggerUI</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-standalone-preset.js" crossorigin></script>
|
||||
<script>
|
||||
url = new URL("openapi.json", window.location.href).href;
|
||||
window.onload = () => {
|
||||
window.ui = SwaggerUIBundle({
|
||||
url: url,
|
||||
dom_id: '#swagger-ui',
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: "StandaloneLayout",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
301
src/api/mod.rs
Normal file
301
src/api/mod.rs
Normal file
@ -0,0 +1,301 @@
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{FromRequest, Request},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use tracing_error::SpanTrace;
|
||||
|
||||
pub use axum::extract::State as EState;
|
||||
|
||||
mod account;
|
||||
mod auth;
|
||||
mod docs;
|
||||
mod transactions;
|
||||
mod user;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Json<T>(T);
|
||||
|
||||
macro_rules! rejection_error {
|
||||
($id:expr, $rejection:expr) => {{
|
||||
let rejection = $rejection;
|
||||
$crate::api::ApiError {
|
||||
status: rejection.status(),
|
||||
id: Cow::Borrowed($id),
|
||||
message: Cow::Owned(rejection.body_text()),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
impl<T: DeserializeOwned, S: Send + Sync> FromRequest<S> for Json<T> {
|
||||
type Rejection = ApiError<'static>;
|
||||
|
||||
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
||||
match axum::Json::<T>::from_request(req, state).await {
|
||||
Ok(value) => Ok(Self(value.0)),
|
||||
Err(error) => Err(rejection_error!("malformed_body", error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> IntoResponse for Json<T> {
|
||||
#[inline]
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
axum::Json(self.0).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Error {
|
||||
trace: Option<SpanTrace>,
|
||||
inner: InnerError,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InnerError {
|
||||
Pool(deadpool_postgres::PoolError),
|
||||
Postgres(tokio_postgres::Error),
|
||||
PHCParse(password_auth::ParseError),
|
||||
Plain(ApiError<'static>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
pub struct ApiError<'a> {
|
||||
#[serde(skip)]
|
||||
pub status: StatusCode,
|
||||
pub id: Cow<'a, str>,
|
||||
pub message: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl<'a> ApiError<'a> {
|
||||
pub fn const_new(status: StatusCode, id: &'static str, message: &'static str) -> Self {
|
||||
Self {
|
||||
status,
|
||||
id: Cow::Borrowed(id),
|
||||
message: Cow::Borrowed(message),
|
||||
}
|
||||
}
|
||||
pub fn new(
|
||||
status: StatusCode,
|
||||
id: impl Into<Cow<'a, str>>,
|
||||
message: impl Into<Cow<'a, str>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
status,
|
||||
id: id.into(),
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
pub fn into_static(self) -> ApiError<'static> {
|
||||
ApiError {
|
||||
status: self.status,
|
||||
id: Cow::Owned(self.id.into_owned()),
|
||||
message: Cow::Owned(self.message.into_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoResponse for ApiError<'a> {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
(self.status, axum::Json(self)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new(inner: InnerError) -> Self {
|
||||
let trace = if cfg!(debug_assertions) || inner.internal() {
|
||||
Some(SpanTrace::capture())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::error!(error = ?inner, "An error occurred");
|
||||
if let Some(trace) = &trace {
|
||||
tracing::error!("Trace:\n{}", trace);
|
||||
}
|
||||
}
|
||||
Self { trace, inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<InnerError>> From<T> for Error {
|
||||
fn from(value: T) -> Self {
|
||||
Error::new(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<deadpool_postgres::PoolError> for InnerError {
|
||||
fn from(value: deadpool_postgres::PoolError) -> Self {
|
||||
Self::Pool(value)
|
||||
}
|
||||
}
|
||||
impl From<tokio_postgres::Error> for InnerError {
|
||||
fn from(value: tokio_postgres::Error) -> Self {
|
||||
Self::Postgres(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<password_auth::ParseError> for InnerError {
|
||||
fn from(value: password_auth::ParseError) -> Self {
|
||||
Self::PHCParse(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiError<'static>> for InnerError {
|
||||
fn from(value: ApiError<'static>) -> Self {
|
||||
InnerError::Plain(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl InnerError {
|
||||
pub const fn internal(&self) -> bool {
|
||||
match self {
|
||||
InnerError::Pool(_) => true,
|
||||
InnerError::Postgres(_) => true,
|
||||
InnerError::PHCParse(_) => true,
|
||||
InnerError::Plain(err) => matches!(err.status, StatusCode::INTERNAL_SERVER_ERROR),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for InnerError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
match self {
|
||||
InnerError::Pool(_) | InnerError::Postgres(_) | InnerError::PHCParse(_) => {
|
||||
INTERNAL_SERVER_ERROR.clone().into_response()
|
||||
}
|
||||
InnerError::Plain(api_error) => api_error.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static INTERNAL_SERVER_ERROR: ApiError<'static> = ApiError {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
id: Cow::Borrowed("internal_server_error"),
|
||||
message: Cow::Borrowed("Internal Server Error"),
|
||||
};
|
||||
|
||||
impl IntoResponse for Error {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
self.inner.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub pool: deadpool_postgres::Pool,
|
||||
pub encoding_key: EncodingKey,
|
||||
pub decoding_key: DecodingKey,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub async fn conn(&self) -> Result<deadpool_postgres::Object, deadpool_postgres::PoolError> {
|
||||
self.pool.get().await
|
||||
}
|
||||
}
|
||||
|
||||
pub type State = axum::extract::State<Arc<AppState>>;
|
||||
|
||||
pub fn router() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.merge(auth::router())
|
||||
.nest("/user", user::router())
|
||||
.nest("/docs", docs::router())
|
||||
.nest("/accounts", account::router())
|
||||
.nest("/transactions", transactions::router())
|
||||
}
|
||||
|
||||
make_schemas!((ApiError); (), [crate::model::schemas, auth::schemas, user::schemas, transactions::schemas, account::schemas]);
|
||||
|
||||
macro_rules! make_schemas {
|
||||
(($($request_bodies:ty),*); ($($response_bodies:ty),*)$(, [$($deps:expr),*])? ) => {
|
||||
#[cfg(feature = "schemas")]
|
||||
#[allow(unused)]
|
||||
pub const fn schemas() -> $crate::api::Schemas {
|
||||
fn request_bodies(generator: &mut schemars::SchemaGenerator, schemas: &mut std::collections::HashMap<std::borrow::Cow<'static, str>, schemars::Schema>) {
|
||||
$(
|
||||
generator.subschema_for::<$request_bodies>();
|
||||
// schemas.insert(<$request_bodies as schemars::JsonSchema>::schema_name(), generator.root_schema_for::<$request_bodies>());
|
||||
)*
|
||||
}
|
||||
fn response_bodies(generator: &mut schemars::SchemaGenerator, schemas: &mut std::collections::HashMap<std::borrow::Cow<'static, str>, schemars::Schema>) {
|
||||
$(
|
||||
generator.subschema_for::<$response_bodies>();
|
||||
// schemas.insert(<$response_bodies as schemars::JsonSchema>::schema_name(), generator.root_schema_for::<$response_bodies>());
|
||||
)*
|
||||
}
|
||||
static DEPS: &'static [$crate::api::Schemas] = &[$($($deps()),*)?];
|
||||
$crate::api::Schemas {
|
||||
request_bodies,
|
||||
response_bodies,
|
||||
contains: DEPS
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "schemas"))]
|
||||
pub const fn schemas() -> $crate::api::Schemas {
|
||||
$crate::api::Schemas
|
||||
}
|
||||
};
|
||||
([$($deps:expr),*]) => {
|
||||
#[cfg(feature = "schemas")]
|
||||
pub const fn schemas() -> $crate::api::Schemas {
|
||||
fn ident(_: &mut schemars::SchemaGenerator, _: &mut std::collections::HashMap<std::borrow::Cow<'static, str>, schemars::Schema>) {}
|
||||
static DEPS: &'static [$crate::api::Schemas] = &[$($deps()),*];
|
||||
$crate::api::Schemas {
|
||||
request_bodies: ident,
|
||||
response_bodies: ident,
|
||||
contains: DEPS
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "schemas"))]
|
||||
pub const fn schemas() -> $crate::api::Schemas {
|
||||
$crate::api::Schemas
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use make_schemas;
|
||||
|
||||
#[cfg(not(feature = "schemas"))]
|
||||
pub struct Schemas;
|
||||
#[cfg(feature = "schemas")]
|
||||
pub struct Schemas {
|
||||
pub request_bodies: fn(
|
||||
&mut schemars::SchemaGenerator,
|
||||
&mut std::collections::HashMap<std::borrow::Cow<'static, str>, schemars::Schema>,
|
||||
),
|
||||
pub response_bodies: fn(
|
||||
&mut schemars::SchemaGenerator,
|
||||
&mut std::collections::HashMap<std::borrow::Cow<'static, str>, schemars::Schema>,
|
||||
),
|
||||
pub contains: &'static [Self],
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemas")]
|
||||
impl Schemas {
|
||||
pub fn generate(
|
||||
&self,
|
||||
requests: &mut schemars::SchemaGenerator,
|
||||
responses: &mut schemars::SchemaGenerator,
|
||||
request_schemas: &mut std::collections::HashMap<
|
||||
std::borrow::Cow<'static, str>,
|
||||
schemars::Schema,
|
||||
>,
|
||||
response_schemas: &mut std::collections::HashMap<
|
||||
std::borrow::Cow<'static, str>,
|
||||
schemars::Schema,
|
||||
>,
|
||||
) {
|
||||
for schemas in self.contains {
|
||||
schemas.generate(requests, responses, request_schemas, response_schemas);
|
||||
}
|
||||
(self.request_bodies)(requests, request_schemas);
|
||||
(self.response_bodies)(requests, response_schemas);
|
||||
}
|
||||
}
|
||||
57
src/api/transactions.rs
Normal file
57
src/api/transactions.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, post},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::model::{Transaction, UserInfo};
|
||||
|
||||
use super::{AppState, EState, Json, State, auth::Auth, make_schemas};
|
||||
|
||||
pub(super) fn router() -> Router<Arc<AppState>> {
|
||||
Router::new().route("/", get(transaction_history).post(make_payment))
|
||||
}
|
||||
|
||||
make_schemas!((Direction, MakePayment); (TransactionHistory));
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
pub struct TransactionHistory {
|
||||
transactions: Vec<Transaction>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Direction {
|
||||
Received,
|
||||
Sent,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum NameOrUuid {
|
||||
Id(Uuid),
|
||||
Name(String),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
pub struct MakePayment {
|
||||
pub from: NameOrUuid,
|
||||
pub to: NameOrUuid,
|
||||
/// amount in cents
|
||||
#[garde(min = 1)]
|
||||
pub amount: u64,
|
||||
}
|
||||
|
||||
pub async fn transaction_history(EState(state): State, auth: Auth) {
|
||||
// Transaction::get_all_for_user(client, id)
|
||||
}
|
||||
pub async fn make_payment(EState(state): State, auth: Auth, Json(body): Json<MakePayment>) {
|
||||
// Transaction::get_all_for_user(client, id)
|
||||
}
|
||||
59
src/api/user.rs
Normal file
59
src/api/user.rs
Normal file
@ -0,0 +1,59 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{Router, extract::Path, routing::get};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::model::{UserAccountInfo, UserInfo};
|
||||
|
||||
use super::{AppState, EState, State, auth::Auth, make_schemas};
|
||||
|
||||
pub(super) fn router() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/{target}", get(user_info))
|
||||
.route("/@me/balance", get(user_balance))
|
||||
.route("/", get(list_users))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum UserTarget {
|
||||
#[serde(rename = "@me")]
|
||||
Me,
|
||||
Id(Uuid),
|
||||
}
|
||||
|
||||
impl UserTarget {
|
||||
pub fn user_id(&self, auth: &Auth) -> Uuid {
|
||||
match self {
|
||||
UserTarget::Me => auth.user_id(),
|
||||
UserTarget::Id(uuid) => *uuid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
make_schemas!((); (ListUsers, UserAccounts, UserBalance));
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
pub struct ListUsers {
|
||||
users: Vec<UserInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
pub struct UserAccounts {
|
||||
pub accounts: Vec<UserAccountInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
pub struct UserBalance {
|
||||
pub balance: u64,
|
||||
}
|
||||
|
||||
pub async fn user_info(EState(state): State, auth: Auth, Path(target): Path<UserTarget>) {
|
||||
let user = target.user_id(&auth);
|
||||
}
|
||||
pub async fn user_balance(EState(state): State, auth: Auth) {}
|
||||
pub async fn list_users(EState(state): State, _: Auth) {}
|
||||
36
src/config.rs
Normal file
36
src/config.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub database: deadpool_postgres::Config,
|
||||
#[serde(default = "default_host")]
|
||||
pub host: IpAddr,
|
||||
#[serde(default = "default_port")]
|
||||
pub port: u16,
|
||||
pub jwt_key: String,
|
||||
}
|
||||
|
||||
fn default_host() -> IpAddr {
|
||||
IpAddr::V6(Ipv6Addr::LOCALHOST)
|
||||
}
|
||||
|
||||
fn default_port() -> u16 {
|
||||
3845
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Self {
|
||||
let config = config::Config::builder()
|
||||
.add_source(config::Environment::default().separator("__"))
|
||||
.build()
|
||||
.unwrap();
|
||||
config.try_deserialize().unwrap()
|
||||
}
|
||||
|
||||
pub fn socket_addr(&self) -> SocketAddr {
|
||||
SocketAddr::new(self.host, self.port)
|
||||
}
|
||||
}
|
||||
36
src/db.rs
Normal file
36
src/db.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use tokio_postgres::NoTls;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
mod migrations {
|
||||
use dbmigrator::embed_migrations;
|
||||
embed_migrations!("migrations");
|
||||
pub(super) async fn migrate(conn: &mut dyn dbmigrator::AsyncClient) {
|
||||
let mut migrator = migrator(
|
||||
dbmigrator::Config {
|
||||
auto_initialize: true,
|
||||
..Default::default()
|
||||
},
|
||||
dbmigrator::simple_compare,
|
||||
);
|
||||
migrator.read_changelog(conn).await.unwrap();
|
||||
migrator.make_plan().unwrap();
|
||||
migrator.check_updated_log().unwrap();
|
||||
for plan in migrator.plans() {
|
||||
migrator.apply_plan(conn, plan).await.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn setup_db(config: &Config) -> deadpool_postgres::Pool {
|
||||
let pool = config
|
||||
.database
|
||||
.create_pool(Some(deadpool::Runtime::Tokio1), NoTls)
|
||||
.unwrap();
|
||||
|
||||
let mut conn = pool.get().await.unwrap();
|
||||
|
||||
migrations::migrate(&mut **conn).await;
|
||||
|
||||
pool
|
||||
}
|
||||
106
src/lib.rs
Normal file
106
src/lib.rs
Normal file
@ -0,0 +1,106 @@
|
||||
pub mod api;
|
||||
mod config;
|
||||
mod db;
|
||||
pub mod model;
|
||||
pub use config::Config;
|
||||
pub use db::setup_db;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[cfg(feature = "schemas")]
|
||||
#[test]
|
||||
fn generate_schemas() {
|
||||
use schemars::{
|
||||
SchemaGenerator,
|
||||
generate::{Contract, SchemaSettings},
|
||||
transform::{Transform, transform_subschemas},
|
||||
};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path::PathBuf,
|
||||
};
|
||||
let directory = PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/schemas"));
|
||||
std::fs::create_dir_all(&directory).unwrap();
|
||||
let mut settings = SchemaSettings::draft2020_12();
|
||||
settings.definitions_path = "".to_owned();
|
||||
|
||||
let mut request_generator = SchemaGenerator::new(settings.clone());
|
||||
settings.contract = Contract::Serialize;
|
||||
let mut response_generator = SchemaGenerator::new(settings);
|
||||
|
||||
let mut request_schemas = HashMap::new();
|
||||
let mut response_schemas = HashMap::new();
|
||||
|
||||
struct RefTransform;
|
||||
|
||||
impl schemars::transform::Transform for RefTransform {
|
||||
fn transform(&mut self, schema: &mut schemars::Schema) {
|
||||
if schema
|
||||
.get("$ref")
|
||||
.map(|value| value.as_str().map(|v| v.starts_with("#/")))
|
||||
.flatten()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if let Some(value) = schema.remove("$ref") {
|
||||
let mut value = value
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.strip_prefix("#/")
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
value.push_str(".json");
|
||||
schema.insert("$ref".into(), value.into());
|
||||
}
|
||||
}
|
||||
transform_subschemas(self, schema);
|
||||
}
|
||||
}
|
||||
|
||||
crate::api::schemas().generate(
|
||||
&mut request_generator,
|
||||
&mut response_generator,
|
||||
&mut request_schemas,
|
||||
&mut response_schemas,
|
||||
);
|
||||
let mut request_defs = request_generator.take_definitions();
|
||||
let mut response_defs = response_generator.take_definitions();
|
||||
|
||||
let request_keys: HashSet<String> = request_defs.keys().cloned().collect();
|
||||
let response_keys: HashSet<String> = response_defs.keys().cloned().collect();
|
||||
|
||||
let mut schemas = HashMap::new();
|
||||
|
||||
for key in request_keys.union(&response_keys) {
|
||||
let mut path = directory.join(key);
|
||||
path.set_extension("json");
|
||||
let schema = match (request_defs.remove(key), response_defs.remove(key)) {
|
||||
(None, Some(schema)) | (Some(schema), None) => schema,
|
||||
(Some(request_schema), Some(response_schema)) => {
|
||||
if request_schema != response_schema {
|
||||
panic!("Diverging schema for {key}");
|
||||
}
|
||||
request_schema
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
let mut schema: schemars::Schema = schema.try_into().unwrap();
|
||||
RefTransform.transform(&mut schema);
|
||||
schemas.insert(
|
||||
key,
|
||||
serde_json::json!({"$ref": format!("schemas/{key}.json")}),
|
||||
);
|
||||
std::fs::write(
|
||||
path,
|
||||
serde_json::to_string_pretty(schema.as_value()).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
std::fs::write(
|
||||
directory.join("schemas.json"),
|
||||
serde_json::to_string_pretty(&serde_json::json!({"components": {"schemas": schemas}}))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
50
src/main.rs
50
src/main.rs
@ -1,3 +1,49 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::Router;
|
||||
use bankserver::{Config, setup_db};
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey};
|
||||
use tokio::net::TcpListener;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::{EnvFilter, Registry, layer::SubscriberExt};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let registry = Registry::default()
|
||||
.with(
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy(),
|
||||
)
|
||||
.with(ErrorLayer::default())
|
||||
.with(tracing_subscriber::fmt::layer());
|
||||
tracing::subscriber::set_global_default(registry).unwrap();
|
||||
// tracing_subscriber::fmt()
|
||||
// .with_env_filter(
|
||||
// EnvFilter::builder()
|
||||
// .with_default_directive(LevelFilter::INFO.into())
|
||||
// .from_env_lossy(),
|
||||
// )
|
||||
// .with(ErrorLayer::default())
|
||||
// .init();
|
||||
let config = Config::load();
|
||||
let pool = setup_db(&config).await;
|
||||
|
||||
let encoding_key = EncodingKey::from_secret(config.jwt_key.as_bytes());
|
||||
let decoding_key = DecodingKey::from_secret(config.jwt_key.as_bytes());
|
||||
|
||||
let router = Router::new()
|
||||
.nest("/api", bankserver::api::router())
|
||||
.with_state(Arc::new(bankserver::api::AppState {
|
||||
pool,
|
||||
encoding_key,
|
||||
decoding_key,
|
||||
}));
|
||||
|
||||
let listener = TcpListener::bind(config.socket_addr()).await.unwrap();
|
||||
|
||||
axum::serve(listener, router.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
111
src/model/account.rs
Normal file
111
src/model/account.rs
Normal file
@ -0,0 +1,111 @@
|
||||
use deadpool_postgres::GenericClient;
|
||||
use serde::Serialize;
|
||||
use tokio_postgres::Row;
|
||||
use tracing::instrument;
|
||||
use uuid::{NoContext, Timestamp, Uuid};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
pub struct Account {
|
||||
pub id: Uuid,
|
||||
pub user: Uuid,
|
||||
pub name: String,
|
||||
pub balance: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
pub struct AccountInfo {
|
||||
pub id: Uuid,
|
||||
pub user: Uuid,
|
||||
pub name: String,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
pub struct UserAccountInfo {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub balance: u64,
|
||||
}
|
||||
|
||||
impl From<Row> for AccountInfo {
|
||||
fn from(value: Row) -> Self {
|
||||
Self {
|
||||
id: value.get("id"),
|
||||
user: value.get("user"),
|
||||
name: value.get("name"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Row> for UserAccountInfo {
|
||||
fn from(value: Row) -> Self {
|
||||
Self {
|
||||
id: value.get("id"),
|
||||
name: value.get("name"),
|
||||
balance: value.get::<_, i64>("balance") as u64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Account {
|
||||
#[instrument(skip(client))]
|
||||
pub async fn create(
|
||||
client: &mut impl GenericClient,
|
||||
id: Option<Uuid>,
|
||||
user: Uuid,
|
||||
name: &str,
|
||||
) -> Result<Uuid, tokio_postgres::Error> {
|
||||
let stmt = client
|
||||
.prepare_cached("insert into accounts(id, \"user\", name) values ($1, $2, $3)")
|
||||
.await?;
|
||||
let id = id.unwrap_or_else(|| Uuid::new_v7(Timestamp::now(NoContext)));
|
||||
client.execute(&stmt, &[&id, &user, &name]).await?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[instrument(skip(client))]
|
||||
pub async fn get_password_by_username(
|
||||
client: &impl GenericClient,
|
||||
name: &str,
|
||||
) -> Result<Option<String>, tokio_postgres::Error> {
|
||||
let stmt = client
|
||||
.prepare_cached("select password from users where name = $1")
|
||||
.await?;
|
||||
let res = client.query_opt(&stmt, &[&name]).await?;
|
||||
Ok(res.map(|res| res.get(0)))
|
||||
}
|
||||
|
||||
#[instrument(skip(client))]
|
||||
pub async fn list_all(
|
||||
client: &impl GenericClient,
|
||||
) -> Result<Vec<AccountInfo>, tokio_postgres::Error> {
|
||||
let stmt = client
|
||||
.prepare_cached("select id,\"user\",name from accounts")
|
||||
.await?;
|
||||
let users = client
|
||||
.query(&stmt, &[])
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(AccountInfo::from)
|
||||
.collect();
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
#[instrument(skip(client))]
|
||||
pub async fn list_for_user(
|
||||
client: &impl GenericClient,
|
||||
user: Uuid,
|
||||
) -> Result<Vec<UserAccountInfo>, tokio_postgres::Error> {
|
||||
let stmt = client
|
||||
.prepare_cached("select id,name,balance from accounts where \"user\" = $1")
|
||||
.await?;
|
||||
let users = client
|
||||
.query(&stmt, &[&user])
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(UserAccountInfo::from)
|
||||
.collect();
|
||||
Ok(users)
|
||||
}
|
||||
}
|
||||
11
src/model/mod.rs
Normal file
11
src/model/mod.rs
Normal file
@ -0,0 +1,11 @@
|
||||
mod account;
|
||||
mod transaction;
|
||||
mod user;
|
||||
|
||||
pub use account::{Account, AccountInfo, UserAccountInfo};
|
||||
pub use transaction::Transaction;
|
||||
pub use user::{User, UserInfo};
|
||||
|
||||
use crate::api::make_schemas;
|
||||
|
||||
make_schemas!((); (UserInfo, Account, AccountInfo, UserAccountInfo, Transaction));
|
||||
121
src/model/transaction.rs
Normal file
121
src/model/transaction.rs
Normal file
@ -0,0 +1,121 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use deadpool_postgres::GenericClient;
|
||||
use serde::Serialize;
|
||||
use tokio_postgres::Row;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
pub struct Transaction {
|
||||
pub from: Uuid,
|
||||
pub to: Uuid,
|
||||
pub amount: u64,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
impl From<Row> for Transaction {
|
||||
fn from(value: Row) -> Self {
|
||||
Self {
|
||||
from: value.get("from"),
|
||||
to: value.get("to"),
|
||||
amount: value.get::<_, i64>("amount") as u64,
|
||||
timestamp: value.get("timestamp"),
|
||||
message: value.get("message"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Transaction {
|
||||
pub async fn create(
|
||||
client: &mut impl GenericClient,
|
||||
from: Uuid,
|
||||
to: Uuid,
|
||||
amount: u64,
|
||||
message: Option<String>,
|
||||
) -> Result<Transaction, tokio_postgres::Error> {
|
||||
let stmt = client
|
||||
.prepare_cached(r#"insert into transactions("from", "to", amount, message) values ($1, $2, $3, $4) returning timestamp"#)
|
||||
.await?;
|
||||
let row = client
|
||||
.query_one(&stmt, &[&from, &to, &(amount as i64), &message])
|
||||
.await?;
|
||||
let timestamp = row.get(0);
|
||||
Ok(Self {
|
||||
from,
|
||||
to,
|
||||
amount,
|
||||
timestamp,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_to_account(
|
||||
client: &mut impl GenericClient,
|
||||
id: Uuid,
|
||||
) -> Result<Vec<Transaction>, tokio_postgres::Error> {
|
||||
let stmt = client
|
||||
.prepare_cached("select * from transactions where \"to\" = $1")
|
||||
.await?;
|
||||
let transactions = client
|
||||
.query(&stmt, &[&id])
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Transaction::from)
|
||||
.collect();
|
||||
Ok(transactions)
|
||||
}
|
||||
|
||||
pub async fn get_from_account(
|
||||
client: &mut impl GenericClient,
|
||||
id: Uuid,
|
||||
) -> Result<Vec<Transaction>, tokio_postgres::Error> {
|
||||
let stmt = client
|
||||
.prepare_cached("select * from transactions where \"from\" = $1")
|
||||
.await?;
|
||||
let transactions = client
|
||||
.query(&stmt, &[&id])
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Transaction::from)
|
||||
.collect();
|
||||
Ok(transactions)
|
||||
}
|
||||
|
||||
pub async fn get_all_for_account(
|
||||
client: &mut impl GenericClient,
|
||||
id: Uuid,
|
||||
) -> Result<Vec<Transaction>, tokio_postgres::Error> {
|
||||
let stmt = client
|
||||
.prepare_cached("select * from transactions where \"from\" = $1 or \"to\" = $1")
|
||||
.await?;
|
||||
let transactions = client
|
||||
.query(&stmt, &[&id])
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Transaction::from)
|
||||
.collect();
|
||||
Ok(transactions)
|
||||
}
|
||||
|
||||
pub async fn get_all_to_user(
|
||||
_client: &mut impl GenericClient,
|
||||
_id: Uuid,
|
||||
) -> Result<Vec<Transaction>, tokio_postgres::Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub async fn get_all_from_user(
|
||||
_client: &mut impl GenericClient,
|
||||
_id: Uuid,
|
||||
) -> Result<Vec<Transaction>, tokio_postgres::Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub async fn get_all_for_user(
|
||||
_client: &mut impl GenericClient,
|
||||
_id: Uuid,
|
||||
) -> Result<Vec<Transaction>, tokio_postgres::Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
96
src/model/user.rs
Normal file
96
src/model/user.rs
Normal file
@ -0,0 +1,96 @@
|
||||
use axum::http::StatusCode;
|
||||
use deadpool_postgres::GenericClient;
|
||||
use serde::Serialize;
|
||||
use tokio_postgres::{Row, error::SqlState};
|
||||
use tracing::instrument;
|
||||
use uuid::{NoContext, Timestamp, Uuid};
|
||||
|
||||
use crate::api::{ApiError, Error};
|
||||
|
||||
use super::Account;
|
||||
|
||||
pub struct User {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||
pub struct UserInfo {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl From<Row> for UserInfo {
|
||||
fn from(value: Row) -> Self {
|
||||
Self {
|
||||
id: value.get("id"),
|
||||
name: value.get("name"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn conflict_error() -> ApiError<'static> {
|
||||
ApiError::new(StatusCode::CONFLICT, "conflict", "Conflict")
|
||||
}
|
||||
|
||||
fn unique_violation(error: tokio_postgres::Error, api_error: fn() -> ApiError<'static>) -> Error {
|
||||
let Some(code) = error.code() else {
|
||||
return error.into();
|
||||
};
|
||||
if *code == SqlState::UNIQUE_VIOLATION {
|
||||
return api_error().into();
|
||||
}
|
||||
return error.into();
|
||||
}
|
||||
|
||||
impl User {
|
||||
#[instrument(skip(client, password))]
|
||||
pub async fn create(
|
||||
client: &mut impl GenericClient,
|
||||
name: &str,
|
||||
password: &str,
|
||||
) -> Result<Uuid, Error> {
|
||||
let stmt = client
|
||||
.prepare_cached("insert into users(id, name, password) values ($1, $2, $3)")
|
||||
.await?;
|
||||
let mut tx = client.transaction().await?;
|
||||
let id = Uuid::new_v7(Timestamp::now(NoContext));
|
||||
tx.execute(&stmt, &[&id, &name, &password])
|
||||
.await
|
||||
.map_err(|err| unique_violation(err, conflict_error))?;
|
||||
Account::create(&mut tx, Some(id), id, name)
|
||||
.await
|
||||
.map_err(|err| unique_violation(err, conflict_error))?;
|
||||
tx.commit().await?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[instrument(skip(client))]
|
||||
pub async fn get_password_and_info_by_username(
|
||||
client: &impl GenericClient,
|
||||
name: &str,
|
||||
) -> Result<Option<(String, UserInfo)>, tokio_postgres::Error> {
|
||||
let stmt = client
|
||||
.prepare_cached("select id,name,password from users where name = $1")
|
||||
.await?;
|
||||
let res = client.query_opt(&stmt, &[&name]).await?;
|
||||
Ok(res.map(|res| {
|
||||
let password = res.get("password");
|
||||
(password, res.into())
|
||||
}))
|
||||
}
|
||||
|
||||
#[instrument(skip(client))]
|
||||
pub async fn list(client: &impl GenericClient) -> Result<Vec<UserInfo>, tokio_postgres::Error> {
|
||||
let stmt = client.prepare_cached("select id,name from users").await?;
|
||||
let users = client
|
||||
.query(&stmt, &[])
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(UserInfo::from)
|
||||
.collect();
|
||||
Ok(users)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user