diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml
deleted file mode 100644
index 6fab231b70cb24b14db9e841b770d1a9da70503c..0000000000000000000000000000000000000000
--- a/.gitea/workflows/release.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-on:
-  release:
-    types: [created]
-
-jobs:
-  release:
-    name: release ${{ matrix.target }}
-    runs-on: ubuntu-latest
-    strategy:
-      fail-fast: false
-      matrix:
-        include:
-          - target: x86_64-pc-windows-gnu
-            archive: zip
-          - target: x86_64-unknown-linux-musl
-            archive: tar.gz tar.xz tar.zst
-          - target: x86_64-apple-darwin
-            archive: zip
-    steps:
-      - uses: actions/checkout@master
-      - name: Compile and release
-        uses: rust-build/rust-build.action@v1.4.2
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        with:
-          RUSTTARGET: ${{ matrix.target }}
-          ARCHIVE_TYPES: ${{ matrix.archive }}
\ No newline at end of file
diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml
deleted file mode 100644
index 4bcd8372886d53c55d8a6591d2e7848d89a4a23f..0000000000000000000000000000000000000000
--- a/.gitea/workflows/test.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-# name: Format, check and test
-name: format, check and test
-on: [push, pull_request]
-
-jobs:
-  check:
-    name: cargo fmt
-    runs-on: ubuntu-latest
-    steps:
-      - uses: https://github.com/actions/checkout@v3
-      - uses: https://github.com/dtolnay/rust-toolchain@stable   
-        with:
-          components: rustfmt, clippy
-      - run: cargo fmt
-      - run: cargo clippy --all-features
-      - uses: https://github.com/Swatinem/rust-cache@v2   
-  test:
-    name: cargo test
-    runs-on: ubuntu-latest
-    steps:
-      - uses: https://github.com/actions/checkout@v3
-      - uses: https://github.com/dtolnay/rust-toolchain@stable
-      - uses: https://github.com/Swatinem/rust-cache@v2
-      - run: cargo test --all-features
\ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ba902e99e0fb48c506d6542bc60f316b4b6366bd..f44710c4f24a1a8374a710e5b3abfbc2e9292d97 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -47,4 +47,5 @@ test:
     - apt update
     - apt install -y sqlite3 libmagic1 libmagic-dev
     - sqlite3 ephemeral.db < schema.sql
-    - cargo test --verbose
+    - cp config.sample.toml config.toml
+    - cargo test -- --nocapture
diff --git a/.gitlab/issue_templates/file-upload-error.md b/.gitlab/issue_templates/file-upload-error.md
new file mode 100644
index 0000000000000000000000000000000000000000..62f7b84f13ea4e59fd234d497293365477660c52
--- /dev/null
+++ b/.gitlab/issue_templates/file-upload-error.md
@@ -0,0 +1,19 @@
+## Checklist
+
+- [ ] Is the file small enough for you to upload?
+- [ ] Did you make sure the filetype isn't a banned filetype? (If you're allowed to bypass, check this anyway)
+- [ ] Did you re-try the file upload?
+- [ ] Did you try with a different API Key?
+- [ ] Did you try with a different file?
+
+## Error Type
+
+(What does the page say when you try to upload the file?)
+
+(If you can, add the file you're uploading as an attachment to this issue.)
+
+## Possible fixes
+
+(If you know what you're doing, link to the line of code that might be responsible for the problem)
+
+/label ~bug ~needs-investigation
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000000000000000000000000000000000000..352a6265a0dc59187ffa576fac072572036fb463
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+    "rust-analyzer.linkedProjects": [
+        "./Cargo.toml"
+    ]
+}
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index e9ee7a4fe02b84b3de148415bf6c41287fa625e4..3d424ace6518141d2a74607ea7ec996182eef302 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -45,24 +45,26 @@ dependencies = [
 
 [[package]]
 name = "ahash"
-version = "0.7.6"
+version = "0.7.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
 dependencies = [
- "getrandom 0.2.10",
+ "getrandom",
  "once_cell",
  "version_check",
 ]
 
 [[package]]
 name = "ahash"
-version = "0.8.3"
+version = "0.8.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
+checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
 dependencies = [
  "cfg-if",
+ "getrandom",
  "once_cell",
  "version_check",
+ "zerocopy",
 ]
 
 [[package]]
@@ -124,14 +126,14 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.65",
 ]
 
 [[package]]
 name = "atoi"
-version = "1.0.0"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e"
+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
 dependencies = [
  "num-traits",
 ]
@@ -142,6 +144,12 @@ version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
 
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -163,21 +171,21 @@ dependencies = [
 
 [[package]]
 name = "base64"
-version = "0.12.3"
+version = "0.21.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
 
 [[package]]
 name = "base64"
-version = "0.13.1"
+version = "0.22.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
 
 [[package]]
-name = "base64"
-version = "0.21.2"
+name = "base64ct"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
 
 [[package]]
 name = "beef"
@@ -191,6 +199,27 @@ version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
+[[package]]
+name = "bitflags"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
 [[package]]
 name = "block-buffer"
 version = "0.10.4"
@@ -200,11 +229,35 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "borsh"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d4d6dafc1a3bb54687538972158f07b2c948bc57d5890df22c0739098b3028"
+dependencies = [
+ "borsh-derive",
+ "cfg_aliases 0.1.1",
+]
+
+[[package]]
+name = "borsh-derive"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf4918709cc4dd777ad2b6303ed03cb37f3ca0ccede8c1b0d28ac6db8f4710e0"
+dependencies = [
+ "once_cell",
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.65",
+ "syn_derive",
+]
+
 [[package]]
 name = "brotli"
-version = "3.3.4"
+version = "6.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
+checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b"
 dependencies = [
  "alloc-no-stdlib",
  "alloc-stdlib",
@@ -213,9 +266,9 @@ dependencies = [
 
 [[package]]
 name = "brotli-decompressor"
-version = "2.3.4"
+version = "4.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744"
+checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362"
 dependencies = [
  "alloc-no-stdlib",
  "alloc-stdlib",
@@ -227,6 +280,28 @@ version = "3.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
 
+[[package]]
+name = "bytecheck"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
+dependencies = [
+ "bytecheck_derive",
+ "ptr_meta",
+ "simdutf8",
+]
+
+[[package]]
+name = "bytecheck_derive"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "byteorder"
 version = "1.4.3"
@@ -241,11 +316,13 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
 
 [[package]]
 name = "cc"
-version = "1.0.79"
+version = "1.0.98"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
 dependencies = [
  "jobserver",
+ "libc",
+ "once_cell",
 ]
 
 [[package]]
@@ -254,6 +331,18 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "cfg_aliases"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
 [[package]]
 name = "chrono"
 version = "0.4.26"
@@ -288,28 +377,44 @@ dependencies = [
  "inout",
 ]
 
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
 [[package]]
 name = "cookie"
-version = "0.17.0"
+version = "0.18.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24"
+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
 dependencies = [
  "aes-gcm",
- "base64 0.21.2",
+ "base64 0.22.1",
  "hmac",
  "percent-encoding",
- "rand 0.8.5",
+ "rand",
  "sha2",
  "subtle",
  "time 0.3.22",
  "version_check",
 ]
 
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "core-foundation-sys"
-version = "0.8.4"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
 
 [[package]]
 name = "cpufeatures"
@@ -337,9 +442,9 @@ checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484"
 
 [[package]]
 name = "crc32fast"
-version = "1.3.2"
+version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
 dependencies = [
  "cfg-if",
 ]
@@ -363,16 +468,6 @@ dependencies = [
  "cfg-if",
 ]
 
-[[package]]
-name = "cruet"
-version = "0.13.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "113a9e83d8f614be76de8df1f25bf9d0ea6e85ea573710a3d3f3abe1438ae49c"
-dependencies = [
- "once_cell",
- "regex",
-]
-
 [[package]]
 name = "crypto-common"
 version = "0.1.6"
@@ -380,7 +475,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
 dependencies = [
  "generic-array",
- "rand_core 0.6.4",
+ "rand_core",
  "typenum",
 ]
 
@@ -393,6 +488,17 @@ dependencies = [
  "cipher",
 ]
 
+[[package]]
+name = "der"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
 [[package]]
 name = "digest"
 version = "0.10.7"
@@ -400,30 +506,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 dependencies = [
  "block-buffer",
+ "const-oid",
  "crypto-common",
  "subtle",
 ]
 
-[[package]]
-name = "dirs"
-version = "4.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
-dependencies = [
- "dirs-sys",
-]
-
-[[package]]
-name = "dirs-sys"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
-dependencies = [
- "libc",
- "redox_users",
- "winapi",
-]
-
 [[package]]
 name = "dotenvy"
 version = "0.15.7"
@@ -435,6 +522,9 @@ name = "either"
 version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "encoding_rs"
@@ -462,7 +552,7 @@ checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.65",
 ]
 
 [[package]]
@@ -477,11 +567,13 @@ dependencies = [
  "nanoid",
  "once_cell",
  "ramhorns",
- "rand 0.8.5",
+ "rand",
  "salvo",
  "serde",
  "serde_derive",
+ "serde_json",
  "sqlx",
+ "thiserror",
  "tokio",
  "tracing",
  "tracing-subscriber",
@@ -512,7 +604,7 @@ checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
 dependencies = [
  "errno-dragonfly",
  "libc",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -535,6 +627,17 @@ dependencies = [
  "xxhash-rust",
 ]
 
+[[package]]
+name = "etcetera"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
+dependencies = [
+ "cfg-if",
+ "home",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "event-listener"
 version = "2.5.3"
@@ -550,6 +653,12 @@ dependencies = [
  "instant",
 ]
 
+[[package]]
+name = "fastrand"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
+
 [[package]]
 name = "figment"
 version = "0.10.10"
@@ -565,9 +674,9 @@ dependencies = [
 
 [[package]]
 name = "flate2"
-version = "1.0.26"
+version = "1.0.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
+checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
 dependencies = [
  "crc32fast",
  "miniz_oxide",
@@ -575,13 +684,12 @@ dependencies = [
 
 [[package]]
 name = "flume"
-version = "0.10.14"
+version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
+checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
 dependencies = [
  "futures-core",
  "futures-sink",
- "pin-project",
  "spin 0.9.8",
 ]
 
@@ -591,6 +699,21 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
 [[package]]
 name = "form_urlencoded"
 version = "1.2.0"
@@ -600,6 +723,12 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
 [[package]]
 name = "futures-channel"
 version = "0.3.28"
@@ -629,13 +758,13 @@ dependencies = [
 
 [[package]]
 name = "futures-intrusive"
-version = "0.4.2"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5"
+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
 dependencies = [
  "futures-core",
  "lock_api",
- "parking_lot 0.11.2",
+ "parking_lot",
 ]
 
 [[package]]
@@ -652,7 +781,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.65",
 ]
 
 [[package]]
@@ -694,17 +823,6 @@ dependencies = [
  "version_check",
 ]
 
-[[package]]
-name = "getrandom"
-version = "0.1.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
-dependencies = [
- "cfg-if",
- "libc",
- "wasi 0.9.0+wasi-snapshot-preview1",
-]
-
 [[package]]
 name = "getrandom"
 version = "0.2.10"
@@ -712,8 +830,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
 dependencies = [
  "cfg-if",
+ "js-sys",
  "libc",
  "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasm-bindgen",
 ]
 
 [[package]]
@@ -728,17 +848,17 @@ dependencies = [
 
 [[package]]
 name = "h2"
-version = "0.3.20"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
+checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
 dependencies = [
+ "atomic-waker",
  "bytes",
  "fnv",
  "futures-core",
  "futures-sink",
- "futures-util",
  "http",
- "indexmap 1.9.3",
+ "indexmap",
  "slab",
  "tokio",
  "tokio-util",
@@ -750,6 +870,9 @@ name = "hashbrown"
 version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash 0.7.8",
+]
 
 [[package]]
 name = "hashbrown"
@@ -757,7 +880,7 @@ version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
 dependencies = [
- "ahash 0.8.3",
+ "ahash 0.8.11",
  "allocator-api2",
 ]
 
@@ -772,12 +895,11 @@ dependencies = [
 
 [[package]]
 name = "headers"
-version = "0.3.8"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
+checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9"
 dependencies = [
- "base64 0.13.1",
- "bitflags",
+ "base64 0.21.7",
  "bytes",
  "headers-core",
  "http",
@@ -788,9 +910,9 @@ dependencies = [
 
 [[package]]
 name = "headers-core"
-version = "0.2.0"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
+checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
 dependencies = [
  "http",
 ]
@@ -852,11 +974,20 @@ dependencies = [
  "digest",
 ]
 
+[[package]]
+name = "home"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "http"
-version = "0.2.9"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
 dependencies = [
  "bytes",
  "fnv",
@@ -865,9 +996,9 @@ dependencies = [
 
 [[package]]
 name = "http-body"
-version = "1.0.0-rc.2"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "951dfc2e32ac02d67c90c0d65bd27009a635dc9b381a2cc7d284ab01e3a0150d"
+checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
 dependencies = [
  "bytes",
  "http",
@@ -875,12 +1006,12 @@ dependencies = [
 
 [[package]]
 name = "http-body-util"
-version = "0.1.0-rc.2"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92445bc9cc14bfa0a3ce56817dc3b5bcc227a168781a356b702410789cec0d10"
+checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
 dependencies = [
  "bytes",
- "futures-util",
+ "futures-core",
  "http",
  "http-body",
  "pin-project-lite",
@@ -900,13 +1031,12 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
 
 [[package]]
 name = "hyper"
-version = "1.0.0-rc.3"
+version = "1.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b75264b2003a3913f118d35c586e535293b3e22e41f074930762929d071e092"
+checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
 dependencies = [
  "bytes",
  "futures-channel",
- "futures-core",
  "futures-util",
  "h2",
  "http",
@@ -915,11 +1045,66 @@ dependencies = [
  "httpdate",
  "itoa",
  "pin-project-lite",
+ "smallvec",
  "tokio",
- "tracing",
  "want",
 ]
 
+[[package]]
+name = "hyper-rustls"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "908bb38696d7a037a01ebcc68a00634112ac2bbf8ca74e30a2c3d2f4f021302b"
+dependencies = [
+ "futures-util",
+ "http",
+ "hyper",
+ "hyper-util",
+ "log",
+ "rustls 0.23.8",
+ "rustls-native-certs",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d8d52be92d09acc2e01dddb7fde3ad983fc6489c7db4837e605bc3fca4cb63e"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "socket2 0.5.4",
+ "tokio",
+ "tower",
+ "tower-service",
+ "tracing",
+]
+
 [[package]]
 name = "iana-time-zone"
 version = "0.1.57"
@@ -953,16 +1138,6 @@ dependencies = [
  "unicode-normalization",
 ]
 
-[[package]]
-name = "indexmap"
-version = "1.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
-dependencies = [
- "autocfg",
- "hashbrown 0.12.3",
-]
-
 [[package]]
 name = "indexmap"
 version = "2.0.0"
@@ -971,6 +1146,7 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
 dependencies = [
  "equivalent",
  "hashbrown 0.14.0",
+ "serde",
 ]
 
 [[package]]
@@ -991,6 +1167,12 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "inventory"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767"
+
 [[package]]
 name = "io-lifetimes"
 version = "1.0.11"
@@ -999,9 +1181,15 @@ checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
 dependencies = [
  "hermit-abi 0.3.1",
  "libc",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "ipnet"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
+
 [[package]]
 name = "itertools"
 version = "0.10.5"
@@ -1019,9 +1207,9 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
 
 [[package]]
 name = "jobserver"
-version = "0.1.26"
+version = "0.1.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
+checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
 dependencies = [
  "libc",
 ]
@@ -1037,13 +1225,14 @@ dependencies = [
 
 [[package]]
 name = "jsonwebtoken"
-version = "8.3.0"
+version = "9.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378"
+checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f"
 dependencies = [
- "base64 0.21.2",
+ "base64 0.21.7",
+ "js-sys",
  "pem",
- "ring",
+ "ring 0.17.8",
  "serde",
  "serde_json",
  "simple_asn1",
@@ -1054,18 +1243,27 @@ name = "lazy_static"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+dependencies = [
+ "spin 0.5.2",
+]
 
 [[package]]
 name = "libc"
-version = "0.2.147"
+version = "0.2.155"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+
+[[package]]
+name = "libm"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
 
 [[package]]
 name = "libsqlite3-sys"
-version = "0.24.2"
+version = "0.27.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14"
+checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
 dependencies = [
  "cc",
  "pkg-config",
@@ -1096,25 +1294,35 @@ checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
 
 [[package]]
 name = "logos"
-version = "0.12.1"
+version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf8b031682c67a8e3d5446840f9573eb7fe26efe7ec8d195c9ac4c0647c502f1"
+checksum = "161971eb88a0da7ae0c333e1063467c5b5727e7fb6b710b8db4814eade3a42e8"
 dependencies = [
  "logos-derive",
 ]
 
 [[package]]
-name = "logos-derive"
-version = "0.12.1"
+name = "logos-codegen"
+version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1d849148dbaf9661a6151d1ca82b13bb4c4c128146a88d05253b38d4e2f496c"
+checksum = "8e31badd9de5131fdf4921f6473d457e3dd85b11b7f091ceb50e4df7c3eeb12a"
 dependencies = [
  "beef",
  "fnv",
+ "lazy_static",
  "proc-macro2",
  "quote",
- "regex-syntax 0.6.29",
- "syn 1.0.109",
+ "regex-syntax 0.8.3",
+ "syn 2.0.65",
+]
+
+[[package]]
+name = "logos-derive"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c2a69b3eb68d5bd595107c9ee58d7e07fe2bb5e360cc85b0f084dedac80de0a"
+dependencies = [
+ "logos-codegen",
 ]
 
 [[package]]
@@ -1123,7 +1331,7 @@ version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "87142e3acb1f4daa62eaea96605421a534119d4777a9fb43fb2784798fd89665"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "errno 0.2.8",
  "libc",
  "magic-sys",
@@ -1171,10 +1379,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
 
 [[package]]
-name = "mime_guess"
-version = "2.0.4"
+name = "mime-infer"
+version = "3.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+checksum = "91caed19dd472bc88bcd063571df18153529d49301a1918f4cf37f42332bee2e"
 dependencies = [
  "mime",
  "unicase",
@@ -1188,9 +1396,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
 
 [[package]]
 name = "miniz_oxide"
-version = "0.7.1"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae"
 dependencies = [
  "adler",
 ]
@@ -1203,21 +1411,20 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
 dependencies = [
  "libc",
  "wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
 name = "multer"
-version = "2.1.0"
+version = "3.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2"
+checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
 dependencies = [
  "bytes",
  "encoding_rs",
  "futures-util",
  "http",
  "httparse",
- "log",
  "memchr",
  "mime",
  "spin 0.9.8",
@@ -1226,9 +1433,9 @@ dependencies = [
 
 [[package]]
 name = "multimap"
-version = "0.9.0"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70db9248a93dc36a36d9a47898caa007a32755c7ad140ec64eeeb50d5a730631"
+checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
 dependencies = [
  "serde",
 ]
@@ -1239,18 +1446,48 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
 dependencies = [
- "rand 0.8.5",
+ "rand",
 ]
 
 [[package]]
-name = "nom"
-version = "7.1.3"
+name = "native-tls"
+version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
 dependencies = [
- "memchr",
- "minimal-lexical",
-]
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+dependencies = [
+ "bitflags 2.5.0",
+ "cfg-if",
+ "cfg_aliases 0.2.1",
+ "libc",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
 
 [[package]]
 name = "nu-ansi-term"
@@ -1264,32 +1501,59 @@ dependencies = [
 
 [[package]]
 name = "num-bigint"
-version = "0.4.3"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
+checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7"
 dependencies = [
- "autocfg",
  "num-integer",
  "num-traits",
 ]
 
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand",
+ "smallvec",
+ "zeroize",
+]
+
 [[package]]
 name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
 version = "0.1.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
 dependencies = [
  "autocfg",
+ "num-integer",
  "num-traits",
 ]
 
 [[package]]
 name = "num-traits"
-version = "0.2.15"
+version = "0.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
 dependencies = [
  "autocfg",
+ "libm",
 ]
 
 [[package]]
@@ -1304,9 +1568,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.18.0"
+version = "1.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
 
 [[package]]
 name = "opaque-debug"
@@ -1315,44 +1579,63 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
 [[package]]
-name = "overload"
-version = "0.1.1"
+name = "openssl"
+version = "0.10.64"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
+dependencies = [
+ "bitflags 2.5.0",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
 
 [[package]]
-name = "parking_lot"
-version = "0.11.2"
+name = "openssl-macros"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
 dependencies = [
- "instant",
- "lock_api",
- "parking_lot_core 0.8.6",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.65",
 ]
 
 [[package]]
-name = "parking_lot"
-version = "0.12.1"
+name = "openssl-probe"
+version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
 dependencies = [
- "lock_api",
- "parking_lot_core 0.9.8",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
 ]
 
 [[package]]
-name = "parking_lot_core"
-version = "0.8.6"
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
 dependencies = [
- "cfg-if",
- "instant",
- "libc",
- "redox_syscall 0.2.16",
- "smallvec",
- "winapi",
+ "lock_api",
+ "parking_lot_core",
 ]
 
 [[package]]
@@ -1363,9 +1646,9 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
 dependencies = [
  "cfg-if",
  "libc",
- "redox_syscall 0.3.5",
+ "redox_syscall",
  "smallvec",
- "windows-targets",
+ "windows-targets 0.48.0",
 ]
 
 [[package]]
@@ -1382,11 +1665,21 @@ checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
 
 [[package]]
 name = "pem"
-version = "1.1.1"
+version = "3.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
+dependencies = [
+ "base64 0.22.1",
+ "serde",
+]
+
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
 dependencies = [
- "base64 0.13.1",
+ "base64ct",
 ]
 
 [[package]]
@@ -1412,7 +1705,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.65",
 ]
 
 [[package]]
@@ -1427,6 +1720,27 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
 [[package]]
 name = "pkg-config"
 version = "0.3.27"
@@ -1453,12 +1767,12 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
 
 [[package]]
 name = "proc-macro-crate"
-version = "1.3.1"
+version = "2.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
 dependencies = [
- "once_cell",
- "toml_edit",
+ "toml_datetime",
+ "toml_edit 0.20.2",
 ]
 
 [[package]]
@@ -1487,38 +1801,84 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.63"
+version = "1.0.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
+checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43"
 dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.65",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
+name = "ptr_meta"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
+dependencies = [
+ "ptr_meta_derive",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "pulldown-cmark"
-version = "0.9.3"
+version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998"
+checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993"
 dependencies = [
- "bitflags",
+ "bitflags 2.5.0",
  "memchr",
+ "pulldown-cmark-escape",
  "unicase",
 ]
 
+[[package]]
+name = "pulldown-cmark-escape"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3"
+
 [[package]]
 name = "quote"
-version = "1.0.28"
+version = "1.0.36"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
 dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
 [[package]]
 name = "ramhorns"
-version = "0.14.0"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47008ae2e2a9085a3f658203609d79f8a027829cf88a088d0c0084e18ba8f0b9"
+checksum = "8adbbcd308f58fe5348325adcb6646b6e04ce53eca4bb1188ea7952aa435c31c"
 dependencies = [
  "arrayvec",
  "beef",
@@ -1530,30 +1890,18 @@ dependencies = [
 
 [[package]]
 name = "ramhorns-derive"
-version = "0.14.0"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ada9bbdd21adf426f932bf76b3db7d553538dffc16afd5fb8ce2ce2110a75536"
+checksum = "74ead572d301d184a2789e9b7460b08aefd70472a448e9e5e54e4124e598c355"
 dependencies = [
  "bae",
  "fnv",
+ "heck 0.4.1",
  "proc-macro2",
  "quote",
  "syn 1.0.109",
 ]
 
-[[package]]
-name = "rand"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
-dependencies = [
- "getrandom 0.1.16",
- "libc",
- "rand_chacha 0.2.2",
- "rand_core 0.5.1",
- "rand_hc",
-]
-
 [[package]]
 name = "rand"
 version = "0.8.5"
@@ -1561,18 +1909,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "libc",
- "rand_chacha 0.3.1",
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
-dependencies = [
- "ppv-lite86",
- "rand_core 0.5.1",
+ "rand_chacha",
+ "rand_core",
 ]
 
 [[package]]
@@ -1582,16 +1920,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 dependencies = [
  "ppv-lite86",
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
-dependencies = [
- "getrandom 0.1.16",
+ "rand_core",
 ]
 
 [[package]]
@@ -1600,25 +1929,7 @@ version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 dependencies = [
- "getrandom 0.2.10",
-]
-
-[[package]]
-name = "rand_hc"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
-dependencies = [
- "rand_core 0.5.1",
-]
-
-[[package]]
-name = "redox_syscall"
-version = "0.2.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
-dependencies = [
- "bitflags",
+ "getrandom",
 ]
 
 [[package]]
@@ -1627,18 +1938,7 @@ version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
 dependencies = [
- "bitflags",
-]
-
-[[package]]
-name = "redox_users"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
-dependencies = [
- "getrandom 0.2.10",
- "redox_syscall 0.2.16",
- "thiserror",
+ "bitflags 1.3.2",
 ]
 
 [[package]]
@@ -1673,6 +1973,65 @@ version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
 
+[[package]]
+name = "regex-syntax"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
+
+[[package]]
+name = "rend"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
+dependencies = [
+ "bytecheck",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-tls",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pemfile 2.1.2",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper 0.1.2",
+ "system-configuration",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-util",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "winreg",
+]
+
 [[package]]
 name = "ring"
 version = "0.16.20"
@@ -1683,69 +2042,176 @@ dependencies = [
  "libc",
  "once_cell",
  "spin 0.5.2",
- "untrusted",
+ "untrusted 0.7.1",
  "web-sys",
  "winapi",
 ]
 
 [[package]]
-name = "rust-embed"
-version = "6.7.0"
+name = "ring"
+version = "0.17.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b73e721f488c353141288f223b599b4ae9303ecf3e62923f40a492f0634a4dc3"
+checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
 dependencies = [
- "rust-embed-impl",
- "rust-embed-utils",
- "walkdir",
+ "cc",
+ "cfg-if",
+ "getrandom",
+ "libc",
+ "spin 0.9.8",
+ "untrusted 0.9.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
-name = "rust-embed-impl"
-version = "6.6.0"
+name = "rkyv"
+version = "0.7.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e22ce362f5561923889196595504317a4372b84210e6e335da529a65ea5452b5"
+checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0"
 dependencies = [
- "proc-macro2",
- "quote",
- "rust-embed-utils",
- "syn 2.0.22",
- "walkdir",
+ "bitvec",
+ "bytecheck",
+ "bytes",
+ "hashbrown 0.12.3",
+ "ptr_meta",
+ "rend",
+ "rkyv_derive",
+ "seahash",
+ "tinyvec",
+ "uuid",
 ]
 
 [[package]]
-name = "rust-embed-utils"
-version = "7.5.0"
+name = "rkyv_derive"
+version = "0.7.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "512b0ab6853f7e14e3c8754acb43d6f748bb9ced66aa5915a6553ac8213f7731"
+checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65"
 dependencies = [
- "sha2",
- "walkdir",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
 ]
 
 [[package]]
-name = "rustix"
-version = "0.37.20"
+name = "rsa"
+version = "0.9.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0"
+checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
 dependencies = [
- "bitflags",
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rust-embed"
+version = "6.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b73e721f488c353141288f223b599b4ae9303ecf3e62923f40a492f0634a4dc3"
+dependencies = [
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "6.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22ce362f5561923889196595504317a4372b84210e6e335da529a65ea5452b5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "syn 2.0.65",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "7.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512b0ab6853f7e14e3c8754acb43d6f748bb9ced66aa5915a6553ac8213f7731"
+dependencies = [
+ "sha2",
+ "walkdir",
+]
+
+[[package]]
+name = "rust_decimal"
+version = "1.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a"
+dependencies = [
+ "arrayvec",
+ "borsh",
+ "bytes",
+ "num-traits",
+ "rand",
+ "rkyv",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "rustix"
+version = "0.37.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0"
+dependencies = [
+ "bitflags 1.3.2",
  "errno 0.3.1",
  "io-lifetimes",
  "libc",
  "linux-raw-sys",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
 name = "rustls"
-version = "0.20.8"
+version = "0.21.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
+checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
 dependencies = [
- "log",
- "ring",
+ "ring 0.17.8",
+ "rustls-webpki 0.101.7",
  "sct",
- "webpki",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79adb16721f56eb2d843e67676896a61ce7a0fa622dc18d3e372477a029d2740"
+dependencies = [
+ "log",
+ "once_cell",
+ "ring 0.17.8",
+ "rustls-pki-types",
+ "rustls-webpki 0.102.4",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792"
+dependencies = [
+ "openssl-probe",
+ "rustls-pemfile 2.1.2",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
 ]
 
 [[package]]
@@ -1754,7 +2220,44 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
 dependencies = [
- "base64 0.21.2",
+ "base64 0.21.7",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
+dependencies = [
+ "base64 0.22.1",
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.101.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
+dependencies = [
+ "ring 0.17.8",
+ "untrusted 0.9.0",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.102.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e"
+dependencies = [
+ "ring 0.17.8",
+ "rustls-pki-types",
+ "untrusted 0.9.0",
 ]
 
 [[package]]
@@ -1765,95 +2268,182 @@ checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
 
 [[package]]
 name = "salvo"
-version = "0.44.1"
+version = "0.68.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c39d8b982850b656146908b46e99a19366aabdb51d4b4fb31ed3a17175acca27"
+checksum = "9f345e9d1bffcb0b5de5ca91e5ea67b3c9e7d53626bd336a685f853d40102679"
 dependencies = [
+ "salvo-jwt-auth",
+ "salvo-oapi",
+ "salvo-proxy",
  "salvo-serve-static",
  "salvo_core",
  "salvo_extra",
 ]
 
 [[package]]
-name = "salvo-serve-static"
-version = "0.44.1"
+name = "salvo-jwt-auth"
+version = "0.68.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c82d34d8ae23a37c4499702e2f0d2848f4ce16320b74b737f34995616c5d165"
+checksum = "8ed3fb5cecd2f52c2c39d6e4103548d6f6690b277c49b12960843ecaeb0d483f"
 dependencies = [
- "hex",
- "mime",
- "mime_guess",
- "path-slash",
- "percent-encoding",
+ "base64 0.22.1",
+ "bytes",
+ "http-body-util",
+ "hyper-rustls",
+ "hyper-util",
+ "jsonwebtoken",
+ "salvo_core",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "salvo-oapi"
+version = "0.68.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "721cf429b5bf65cefb25c2a85040ec6131c53c5c6becd0bbedbefd4b7c9da08e"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "chrono",
+ "futures-util",
+ "http",
+ "indexmap",
+ "inventory",
+ "mime-infer",
+ "once_cell",
+ "parking_lot",
+ "regex",
  "rust-embed",
+ "rust_decimal",
+ "salvo-oapi-macros",
  "salvo_core",
  "serde",
  "serde_json",
+ "serde_yaml",
+ "smallvec",
+ "thiserror",
  "time 0.3.22",
  "tokio",
  "tracing",
+ "ulid",
+ "url",
+ "uuid",
 ]
 
 [[package]]
-name = "salvo-utils"
-version = "0.0.2"
+name = "salvo-oapi-macros"
+version = "0.68.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcc5955a5ab06f16e9c12e7572fb9a845571f3c6c6f60f4851e870ff112503b3"
+checksum = "ed1bdd3f8c71e2614f116eb59c180ba1db26c546d6cffad8bc1dd11c7d21e68a"
 dependencies = [
- "futures-channel",
+ "proc-macro-crate",
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "regex",
+ "salvo-serde-util",
+ "syn 2.0.65",
+]
+
+[[package]]
+name = "salvo-proxy"
+version = "0.68.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df4da5d8518eec607dbfdc5d0df166adf934403ba9ff2a4d44c81d06ce3b517f"
+dependencies = [
+ "fastrand 2.1.0",
  "futures-util",
- "http",
  "hyper",
- "once_cell",
- "pin-project-lite",
- "socket2",
+ "hyper-rustls",
+ "hyper-util",
+ "percent-encoding",
+ "reqwest",
+ "salvo_core",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "salvo-serde-util"
+version = "0.68.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5497aa540af986e938a52937a4720f461f55a4247540d2428c9493b2ea12f9f2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.65",
+]
+
+[[package]]
+name = "salvo-serve-static"
+version = "0.68.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94784e72649533ba0574823c0de263d4e5afee5bf70f81b80885c9d1a0a73db8"
+dependencies = [
+ "hex",
+ "mime",
+ "mime-infer",
+ "path-slash",
+ "percent-encoding",
+ "rust-embed",
+ "salvo_core",
+ "serde",
+ "serde_json",
+ "time 0.3.22",
  "tokio",
- "tower",
- "tower-service",
  "tracing",
 ]
 
 [[package]]
 name = "salvo_core"
-version = "0.44.1"
+version = "0.68.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "420e9316f430f386560cb42ea12c5d014fdbaae399d64b351593957256c03490"
+checksum = "10b5286f12df1d4be2a7b00d5ee4a0232627688d29e3d1842bc3f7956c5bad5c"
 dependencies = [
  "async-trait",
- "base64 0.21.2",
+ "base64 0.22.1",
  "brotli",
  "bytes",
  "cookie",
- "cruet",
  "encoding_rs",
  "enumflags2",
  "flate2",
  "form_urlencoded",
+ "futures-channel",
  "futures-util",
  "headers",
  "http",
  "http-body-util",
  "hyper",
- "indexmap 1.9.3",
+ "hyper-rustls",
+ "hyper-util",
+ "indexmap",
  "mime",
- "mime_guess",
+ "mime-infer",
  "multer",
  "multimap",
+ "nix",
  "once_cell",
- "parking_lot 0.12.1",
+ "parking_lot",
  "percent-encoding",
  "pin-project",
+ "rand",
  "regex",
- "salvo-utils",
  "salvo_macros",
  "serde",
+ "serde-xml-rs",
  "serde_json",
  "serde_urlencoded",
+ "sync_wrapper 1.0.1",
  "tempfile",
- "textnonce",
  "thiserror",
  "tokio",
- "tokio-stream",
+ "tokio-rustls",
+ "tokio-util",
  "tracing",
  "url",
  "zstd",
@@ -1861,44 +2451,36 @@ dependencies = [
 
 [[package]]
 name = "salvo_extra"
-version = "0.44.1"
+version = "0.68.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "911bd4a8f471dd7b1292845233acaedb6dd7f7b29e609cd755cfaeea695274cf"
+checksum = "90a3321a6f6a4fa8ae3e82058cf8ecad641abfc5851ce0a9cce277daf3b00aff"
 dependencies = [
- "base64 0.21.2",
- "brotli",
- "bytes",
+ "base64 0.22.1",
  "etag",
- "flate2",
  "futures-util",
  "hyper",
- "indexmap 1.9.3",
- "jsonwebtoken",
- "once_cell",
  "pin-project",
  "salvo_core",
  "serde",
  "serde_json",
  "tokio",
- "tokio-stream",
  "tokio-tungstenite",
- "tokio-util",
  "tracing",
- "zstd",
+ "ulid",
 ]
 
 [[package]]
 name = "salvo_macros"
-version = "0.44.1"
+version = "0.68.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e504e3ad06578cc6139e538f1973e92d89093c0fdcf5ec8c1abd7a53902568bb"
+checksum = "5a13b586a77f2ff684097ebc9773e3d11910b0d86ca4cffe46de1080e322138a"
 dependencies = [
- "cruet",
  "proc-macro-crate",
  "proc-macro2",
  "quote",
  "regex",
- "syn 2.0.22",
+ "salvo-serde-util",
+ "syn 2.0.65",
 ]
 
 [[package]]
@@ -1910,6 +2492,15 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "schannel"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "scopeguard"
 version = "1.1.0"
@@ -1922,35 +2513,76 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
 dependencies = [
- "ring",
- "untrusted",
+ "ring 0.16.20",
+ "untrusted 0.7.1",
+]
+
+[[package]]
+name = "seahash"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
+
+[[package]]
+name = "security-framework"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0"
+dependencies = [
+ "bitflags 2.5.0",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
 ]
 
 [[package]]
 name = "serde"
-version = "1.0.164"
+version = "1.0.203"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d"
+checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
 dependencies = [
  "serde_derive",
 ]
 
+[[package]]
+name = "serde-xml-rs"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782"
+dependencies = [
+ "log",
+ "serde",
+ "thiserror",
+ "xml-rs",
+]
+
 [[package]]
 name = "serde_derive"
-version = "1.0.164"
+version = "1.0.203"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68"
+checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.65",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.99"
+version = "1.0.117"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3"
+checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
 dependencies = [
  "itoa",
  "ryu",
@@ -1978,6 +2610,19 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "serde_yaml"
+version = "0.9.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1bf28c79a99f70ee1f1d83d10c875d2e70618417fda01ad1785e027579d9d38"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
 [[package]]
 name = "sha1"
 version = "0.10.5"
@@ -2009,6 +2654,22 @@ dependencies = [
  "lazy_static",
 ]
 
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
+
 [[package]]
 name = "simple_asn1"
 version = "0.6.2"
@@ -2032,9 +2693,9 @@ dependencies = [
 
 [[package]]
 name = "smallvec"
-version = "1.10.0"
+version = "1.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
 
 [[package]]
 name = "socket2"
@@ -2046,6 +2707,16 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "socket2"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
+dependencies = [
+ "libc",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "spin"
 version = "0.5.2"
@@ -2061,6 +2732,16 @@ dependencies = [
  "lock_api",
 ]
 
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
 [[package]]
 name = "sqlformat"
 version = "0.2.1"
@@ -2074,98 +2755,199 @@ dependencies = [
 
 [[package]]
 name = "sqlx"
-version = "0.6.3"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6"
+dependencies = [
+ "ahash 0.8.11",
+ "atoi",
+ "byteorder",
+ "bytes",
+ "crc",
+ "crossbeam-queue",
+ "either",
+ "event-listener",
+ "futures-channel",
+ "futures-core",
+ "futures-intrusive",
+ "futures-io",
+ "futures-util",
+ "hashlink",
+ "hex",
+ "indexmap",
+ "log",
+ "memchr",
+ "once_cell",
+ "paste",
+ "percent-encoding",
+ "rustls 0.21.12",
+ "rustls-pemfile 1.0.2",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlformat",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "url",
+ "webpki-roots",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "sqlx-core",
+ "sqlx-macros-core",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "sqlx-macros-core"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8de3b03a925878ed54a954f621e64bf55a3c1bd29652d0d1a17830405350188"
+checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8"
 dependencies = [
+ "dotenvy",
+ "either",
+ "heck 0.4.1",
+ "hex",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sqlx-core",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+ "syn 1.0.109",
+ "tempfile",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "sqlx-mysql"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
+dependencies = [
+ "atoi",
+ "base64 0.21.7",
+ "bitflags 2.5.0",
+ "byteorder",
+ "bytes",
+ "crc",
+ "digest",
+ "dotenvy",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "generic-array",
+ "hex",
+ "hkdf",
+ "hmac",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rand",
+ "rsa",
+ "serde",
+ "sha1",
+ "sha2",
+ "smallvec",
  "sqlx-core",
- "sqlx-macros",
+ "stringprep",
+ "thiserror",
+ "tracing",
+ "whoami",
 ]
 
 [[package]]
-name = "sqlx-core"
-version = "0.6.3"
+name = "sqlx-postgres"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029"
+checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
 dependencies = [
- "ahash 0.7.6",
  "atoi",
- "base64 0.13.1",
- "bitflags",
+ "base64 0.21.7",
+ "bitflags 2.5.0",
  "byteorder",
- "bytes",
  "crc",
- "crossbeam-queue",
- "dirs",
  "dotenvy",
- "either",
- "event-listener",
- "flume",
+ "etcetera",
  "futures-channel",
  "futures-core",
- "futures-executor",
- "futures-intrusive",
+ "futures-io",
  "futures-util",
- "hashlink",
  "hex",
  "hkdf",
  "hmac",
- "indexmap 1.9.3",
+ "home",
  "itoa",
- "libc",
- "libsqlite3-sys",
  "log",
  "md-5",
  "memchr",
  "once_cell",
- "paste",
- "percent-encoding",
- "rand 0.8.5",
- "rustls",
- "rustls-pemfile",
+ "rand",
  "serde",
  "serde_json",
- "sha1",
  "sha2",
  "smallvec",
- "sqlformat",
- "sqlx-rt",
+ "sqlx-core",
  "stringprep",
  "thiserror",
- "tokio-stream",
- "url",
- "webpki-roots",
+ "tracing",
  "whoami",
 ]
 
 [[package]]
-name = "sqlx-macros"
-version = "0.6.3"
+name = "sqlx-sqlite"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9966e64ae989e7e575b19d7265cb79d7fc3cbbdf179835cb0d716f294c2049c9"
+checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
 dependencies = [
- "dotenvy",
- "either",
- "heck 0.4.1",
- "once_cell",
- "proc-macro2",
- "quote",
- "sha2",
+ "atoi",
+ "flume",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-intrusive",
+ "futures-util",
+ "libsqlite3-sys",
+ "log",
+ "percent-encoding",
+ "serde",
  "sqlx-core",
- "sqlx-rt",
- "syn 1.0.109",
+ "tracing",
  "url",
-]
-
-[[package]]
-name = "sqlx-rt"
-version = "0.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024"
-dependencies = [
- "once_cell",
- "tokio",
- "tokio-rustls",
+ "urlencoding",
 ]
 
 [[package]]
@@ -2203,15 +2985,66 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.22"
+version = "2.0.65"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616"
+checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106"
 dependencies = [
  "proc-macro2",
  "quote",
  "unicode-ident",
 ]
 
+[[package]]
+name = "syn_derive"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b"
+dependencies = [
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.65",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
+
+[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
 [[package]]
 name = "tempfile"
 version = "3.6.0"
@@ -2220,40 +3053,30 @@ checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6"
 dependencies = [
  "autocfg",
  "cfg-if",
- "fastrand",
- "redox_syscall 0.3.5",
+ "fastrand 1.9.0",
+ "redox_syscall",
  "rustix",
- "windows-sys",
-]
-
-[[package]]
-name = "textnonce"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7743f8d70cd784ed1dc33106a18998d77758d281dc40dc3e6d050cf0f5286683"
-dependencies = [
- "base64 0.12.3",
- "rand 0.7.3",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
 name = "thiserror"
-version = "1.0.40"
+version = "1.0.61"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
+checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.40"
+version = "1.0.61"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
+checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.65",
 ]
 
 [[package]]
@@ -2331,9 +3154,9 @@ dependencies = [
  "mio",
  "num_cpus",
  "pin-project-lite",
- "socket2",
+ "socket2 0.4.9",
  "tokio-macros",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -2344,18 +3167,28 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.65",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
 ]
 
 [[package]]
 name = "tokio-rustls"
-version = "0.23.4"
+version = "0.26.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
+checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
 dependencies = [
- "rustls",
+ "rustls 0.23.8",
+ "rustls-pki-types",
  "tokio",
- "webpki",
 ]
 
 [[package]]
@@ -2371,9 +3204,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-tungstenite"
-version = "0.19.0"
+version = "0.21.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec509ac96e9a0c43427c74f003127d953a265737636129424288d27cb5c4b12c"
+checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
 dependencies = [
  "futures-util",
  "log",
@@ -2404,7 +3237,7 @@ dependencies = [
  "serde",
  "serde_spanned",
  "toml_datetime",
- "toml_edit",
+ "toml_edit 0.19.11",
 ]
 
 [[package]]
@@ -2422,11 +3255,22 @@ version = "0.19.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7"
 dependencies = [
- "indexmap 2.0.0",
+ "indexmap",
  "serde",
  "serde_spanned",
  "toml_datetime",
- "winnow",
+ "winnow 0.4.7",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "winnow 0.5.40",
 ]
 
 [[package]]
@@ -2442,7 +3286,6 @@ dependencies = [
  "tokio",
  "tower-layer",
  "tower-service",
- "tracing",
 ]
 
 [[package]]
@@ -2478,7 +3321,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.65",
 ]
 
 [[package]]
@@ -2528,14 +3371,14 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
 
 [[package]]
 name = "tungstenite"
-version = "0.19.0"
+version = "0.21.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67"
+checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
 dependencies = [
  "byteorder",
  "bytes",
  "log",
- "rand 0.8.5",
+ "rand",
  "thiserror",
  "utf-8",
 ]
@@ -2546,6 +3389,17 @@ version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
 
+[[package]]
+name = "ulid"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34778c17965aa2a08913b57e1f34db9b4a63f5de31768b55bf20d2795f921259"
+dependencies = [
+ "getrandom",
+ "rand",
+ "web-time",
+]
+
 [[package]]
 name = "uncased"
 version = "0.9.9"
@@ -2557,9 +3411,9 @@ dependencies = [
 
 [[package]]
 name = "unicase"
-version = "2.6.0"
+version = "2.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
 dependencies = [
  "version_check",
 ]
@@ -2607,12 +3461,24 @@ dependencies = [
  "subtle",
 ]
 
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
 [[package]]
 name = "untrusted"
 version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
 
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
 [[package]]
 name = "url"
 version = "2.4.0"
@@ -2624,12 +3490,24 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
 [[package]]
 name = "utf-8"
 version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
 
+[[package]]
+name = "uuid"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
+
 [[package]]
 name = "valuable"
 version = "0.1.0"
@@ -2667,12 +3545,6 @@ dependencies = [
  "try-lock",
 ]
 
-[[package]]
-name = "wasi"
-version = "0.9.0+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
-
 [[package]]
 name = "wasi"
 version = "0.10.0+wasi-snapshot-preview1"
@@ -2706,10 +3578,22 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.65",
  "wasm-bindgen-shared",
 ]
 
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
 [[package]]
 name = "wasm-bindgen-macro"
 version = "0.2.87"
@@ -2728,7 +3612,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.65",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -2739,6 +3623,19 @@ version = "0.2.87"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
 
+[[package]]
+name = "wasm-streams"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
 [[package]]
 name = "web-sys"
 version = "0.3.64"
@@ -2750,33 +3647,26 @@ dependencies = [
 ]
 
 [[package]]
-name = "webpki"
-version = "0.22.0"
+name = "web-time"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
 dependencies = [
- "ring",
- "untrusted",
+ "js-sys",
+ "wasm-bindgen",
 ]
 
 [[package]]
 name = "webpki-roots"
-version = "0.22.6"
+version = "0.25.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87"
-dependencies = [
- "webpki",
-]
+checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
 
 [[package]]
 name = "whoami"
 version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50"
-dependencies = [
- "wasm-bindgen",
- "web-sys",
-]
 
 [[package]]
 name = "winapi"
@@ -2815,7 +3705,7 @@ version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
 dependencies = [
- "windows-targets",
+ "windows-targets 0.48.0",
 ]
 
 [[package]]
@@ -2824,7 +3714,16 @@ version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
 dependencies = [
- "windows-targets",
+ "windows-targets 0.48.0",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.5",
 ]
 
 [[package]]
@@ -2833,13 +3732,29 @@ version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
 dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.5",
+ "windows_aarch64_msvc 0.52.5",
+ "windows_i686_gnu 0.52.5",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.5",
+ "windows_x86_64_gnu 0.52.5",
+ "windows_x86_64_gnullvm 0.52.5",
+ "windows_x86_64_msvc 0.52.5",
 ]
 
 [[package]]
@@ -2848,42 +3763,90 @@ version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
 
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
+
 [[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
 
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+
 [[package]]
 name = "winnow"
 version = "0.4.7"
@@ -2893,38 +3856,102 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "xml-rs"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193"
+
 [[package]]
 name = "xxhash-rust"
 version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "735a71d46c4d68d71d4b24d03fdc2b98e38cea81730595801db779c04fe80d70"
 
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
+[[package]]
+name = "zerocopy"
+version = "0.7.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.65",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
+
 [[package]]
 name = "zstd"
-version = "0.12.3+zstd.1.5.2"
+version = "0.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806"
+checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a"
 dependencies = [
  "zstd-safe",
 ]
 
 [[package]]
 name = "zstd-safe"
-version = "6.0.5+zstd.1.5.4"
+version = "7.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b"
+checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a"
 dependencies = [
- "libc",
  "zstd-sys",
 ]
 
 [[package]]
 name = "zstd-sys"
-version = "2.0.8+zstd.1.5.5"
+version = "2.0.10+zstd.1.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c"
+checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa"
 dependencies = [
  "cc",
- "libc",
  "pkg-config",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 97a437d27eb38174a04f1525c2b020c0b4a6605a..edf0a148e6184b4e1ec188a52449afaa23bfaee6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,10 +10,10 @@ edition = "2021"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-ramhorns = "0.14.0"
-salvo = { version = "*", features = ["logging", "serve-static"] }
-sqlx = { version = "0.6", features = [ "runtime-tokio-rustls", "sqlite", "postgres" ] }
-once_cell = "1.16.0"
+ramhorns = "1.0.0"
+salvo = { version = "0.68.0", features = ["logging", "serve-static", "oapi"] }
+sqlx = { version = "0.7.4", features = [ "runtime-tokio-rustls", "sqlite", "postgres" ] }
+once_cell = "1.19.0"
 tokio = { version = "1", features = ["macros"] }
 tracing = "0.1"
 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
@@ -26,3 +26,5 @@ chrono-humanize = "0.2.2"
 magic = "0.13.0"
 serde_derive = "1.0.164"
 serde = "1.0.164"
+thiserror = "1.0.61"
+serde_json = "1.0.117"
diff --git a/README.md b/README.md
index 22a5bf5612ad5cbe1cf6a5a6b753fe4a24ce9252..98ec26b688ff20cc2f957e65da558ed917f9b276 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,17 @@ This is the exact setup I'm using to run the test instance <https://cz0.au>
 
 To see the logs (even in colour!) use `sudo journalctl -xefu ephemeral --output cat`
 
+
+### Docker (Production)
+
+I'm trying to keep my stuff clean and simple by using docker to auto-build versions.
+After only using docker as a user, this is annoying and hard.
+
+1. copy the docker-compose.yaml from `/docs` directory.
+2. `docker compose up` to start the server in the foreground to generate configs.
+3. CTRL+C, and edit the configs to your liking.
+4. `docker compose up -d` to start the server in the background.
+
 ### Configuration Options
 
 Configuration options are available in the `config.toml' file.
@@ -62,3 +73,4 @@ TODO: Test this please before I start using it publically, it rEALLY isn't a sma
 - Areganno - improved upon the StableDiffusion logo. (and by that I mean completely remade it)  
 - Shadowhand & hailey - I had a moment of weakness while writing nginx configs, and they were there when I needed them the most. 
 - rdqsr - For the brilliant work with the progress bar uploading thingy, it looks and works wonderfully!
+- nnppccc - For guiding me to how to do some of the API Key handling on the frontend, even if I ignored most of what they said.
\ No newline at end of file
diff --git a/config.sample.toml b/config.sample.toml
index 89fdbfe2257cae1108394ab1c4b87a6d886cba23..d0114eb0066e71e3d6f850de1fca00ec616c2ecd 100644
--- a/config.sample.toml
+++ b/config.sample.toml
@@ -7,6 +7,7 @@ port = 8282
 # TODO: benchmarks
 nginx_sendfile = false
 
+
 [database] # DATABASE DATABASE JUST LIVING IN THE DATABASE WOOAH
 # What SQL Backend to use
 ## Available SQL backends: sqlite, postgres
@@ -18,6 +19,7 @@ sql_backend = "sqlite"
 
 url = "sqlite://ephemeral.db"
 
+
 [logging]
 # Controls the logging level of the ephemeral program, and the crates it uses.
 
@@ -29,6 +31,7 @@ level = "ephemeral=info,sqlx=error,hyper=error,salvo_extra=error,salvo=error,mio
 # Enables/Disables metrics generation
 metrics = true
 
+
 [operations]
 # These change the character length of the filename, admin key, and API Key when generated.
 # Leaving it at 6 is good enough for most, since it'll /try/ to regenerate in the case of a collision.
@@ -55,11 +58,23 @@ file_expiry_max = 31536000
 # size in bytes - default: 1 Gibibyte
 max_filesize = 1073741824
 
-
 # Mimetypes that are not allowed to be uploaded.
 # Leave empty to allow all files, and please enter these in lowercase.
-banned_mimetype = ["application/x-msdownload", "application/x-msi", "application/x-silverlight", "application/x-ms-dos-executable"]
+banned_mimetype = ["application/x-msdownload", "application/x-msi", "application/x-silverlight", "application/x-ms-dos-executable", "application/vnd.microsoft.portable-executable"]
 
 # Files that should only ever be rendered as plaintext.
 # I REALLY recommend not removing these.
-unsafe_mimetype = ["text/html", "text/javascript", "application/javascript", "text/css", "application/x-httpd-php", "application/xhtml+xml"]
\ No newline at end of file
+unsafe_mimetype = ["text/html", "text/javascript", "application/javascript", "text/css", "application/x-httpd-php", "application/xhtml+xml"]
+
+
+[uploading]
+# Should we allow any key to be valid?
+# This essentially turns off keys. You still need to send a key in, but we don't track them in the database.
+# This is as close to the legacy 1.0 version of ephemeral. Users can type whatever they want, and as long as they keep that keep secure, nobody can touch their files.
+allow_unverified_keys = false
+
+# This is the number of keys a top level (generated by a master key) can generate.
+max_key_generations = 25
+
+# The fallback key that should be used for files that already existed but don't have an owner.
+fallback_key = "set-me"
\ No newline at end of file
diff --git a/docs/API.md b/docs/API.md
deleted file mode 100644
index 403ad5cc62c26e6d8234e56ae9bfef87c15dfd68..0000000000000000000000000000000000000000
--- a/docs/API.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# Ephemeral API Documentation
-
-Ephemeral uses a cool hidden input type on the upload form to say that the upload is coming from a browser.
-If the "source" value is not set to "web", then the server will return json.
-Additionally, if the program detects that you don't have `Mozilla/5.0` in the user agent, the server will return the JSON api response instead.
-
-This API Documentation is for the JSON responses.
-
-This is not entirely accurate, as it includes planned features like
-editing ttd, and looking at file information.
-
-## File Uploading - `/`
-
-POST - endpoint receives a file, returns the following.
-
-`{"file": "FAFHHSW.txt", "url": "https://cz0.au/FAFHHSW.txt", "adminurl": "https://cz0.au/edit/64f2fhdsFSGQ", "expiry": 1677123536}`
-
-- File: The name of the file uploaded.
-- url: the URL of the file.
-- adminurl: the admin URL of the file, currently, this just deletes the file.
-- expiry: The expiry of the file, in unix time.
-
-Returns `{"error": "BlockedMimetype"}` if the Mimetype is not allowed to be uploaded (as set in config.toml)
-Returns `{"error": "MissingUploadKey"}` if the server requires a key to upload files.
-Returns `{"error": "InternalServerError"}` if there was something wrong with copying the file to the filestore.
-Returns `{"error": "BadRequest"}` if there was a error parsing the request that was sent.
-
-## File Serving - `/<file>`
-
-GET - Retrieves the file with the given ID, performing syntax highlighting and rendering a template if matching known plain-text file. (otherwise just plaintext, OR rendered by browser)
-
-## File information - `/edit/<key>`
-
-Returns information about the file with the corresponding key.
-
-- URL
-- filename
-- current expiry
-- popularity/fileviews
-
-If the key is invalid returns a 401, same with all children API Endpoints
-
-## File Deletion - `/edit/<key>` (also `/delete/<key>` for backwards compatibility.)
-
-Deletes the file with the corresponding key. This is immediate and permanent.
-
-When successful, returns `{"deletion": "success"}`
-
-Accepts GET and DELETE - as long as it has the keys.
-
-## File expiry Change - `/edit/<key>/ttd`
-
-If GET, reads the expiry from the DB and responds with it.
-If POST, tries it's best to overwrite the expiry. This will fail if the expiry is futher away than the current.
-
-This is in unix time.
diff --git a/docs/DB.md b/docs/DB.md
deleted file mode 100644
index 97d57b4e902e2d4310b208b47d1a84d701053834..0000000000000000000000000000000000000000
--- a/docs/DB.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# Database documentation
-
-The database connection is saved in a pool, and the pool serves out a connection to any object that needs them.
-
-At the moment, we use SQLX, with SQLite, however I'd really like to support Postgres and the rest soon, SQLite is slow, buggy and annoying.
-
-## files.db
-
-### files table
-
-- **file** as text - contains the filename (with filetype!)
-- **filetype** as text - contains the file extension. (this is onl used for stats, and easy moderation) - at this point it is only grabbed from the extension, but later on we could use mimetype guessing to guess files without a type. However it's almost always text, and will be rendered as so.
-- **expiry** as int - contains unix time of when the file will be deleted - AS COMPUTED BY ENGINE MODE. (this is kept as is, never changed after initial upload)
-- **expiry_override** - _can_ contain unixtime of when the file will actaully be deleted, if it's set, this is used for overriding files.
-- **views** - counts up for each view to the file.
-- **isDeleted** - simple flag to archive the thing in the DB.
-- **adminkey** - key for deletion and setting expiry_override
-- **accessed** - last time the file was accessed
-- **filesize** - size of the file, in kb or something idk
-- **ip** - IP Address of the uploader, for legal and statistics purposes!
-- **domain** - Domain the file was originally uploaded to.
-
-### qrscan table
-
-- **scanid** as autoincrement int, for primary key.
-- **time** as int - when the qr code was scanned
-- **ip** as text? - who scanned it.
-- **version** as int - simple int for the version of qrcode, so I can track what edition the qrcodes are. (/qr1/2/3/4/5/6)
-
-### stats table
-
-Hopefully we can move away from having a separate stats table, and use metrics generated from the files table instead.
-(or maybe not for performance - might be cheaper to keep the stats table and update it every minute or so.)
-
-### API Keys Table
-
-This is used for premium users.
-API Keys have a few permission flags, they can be `true` or `false`.
-
-1. server_admin: Allows the user to generate new API Keys
-2. expiry_override: Allows the user to bypass the default expiry of a file. (but not past the file_expiry_max)
-3. upload_permanent: Allows the user to upload a file that's stored permanently.
-4. default_name: Allows the user to upload a file, keeping the filename.
-5. bypass_mimetypes: Allows the user to upload a file that normally would be banned.
\ No newline at end of file
diff --git a/docs/benchmarks.md b/docs/benchmarks.md
deleted file mode 100644
index ab3f8c67840425a6eeb8a43269b272549146c16c..0000000000000000000000000000000000000000
--- a/docs/benchmarks.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Ephemeral Benchmarks
-
-Benchmarking this software will be compared with QuadFile, the direct python equivalent.
-
-## Ephemeral vs QuadFile (No NGiNX)
-
-### Serving index
-
-### Uploading a file
-
-### Serving a file
-
-### Deleting a file
-
-## Ephemeral vs QuadFile (with NGiNX and X-Accel-Redirect)
-
-### Serving index
-
-macro serving - 27k
-
-### Uploading a file
-
-macro upload - 42/s
-
-### Serving a file
-
-macro serving - 48/s
-( with a few 5xx's)
-
-### Deleting a file
-
-## Notes
-
-Changing from functions to macros didn't really speed anything up
-is it an I/O bottleneck?
diff --git a/docs/docker-compose.yaml b/docs/docker-compose.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..1a28b36716c7bd436ac4efcb7ceea8a6333c86ca
--- /dev/null
+++ b/docs/docker-compose.yaml
@@ -0,0 +1 @@
+# Maybe for later. sorry :(
\ No newline at end of file
diff --git a/docs/engines.md b/docs/engines.md
deleted file mode 100644
index 6456de073e966d4481531562152e6f9a2969fdaf..0000000000000000000000000000000000000000
--- a/docs/engines.md
+++ /dev/null
@@ -1,68 +0,0 @@
-# Ephemeral Deletion Engines
-
-Ephemeral has a few deletion engines. Historically, there was only one, however I saw how 0x0.st does stuff, and liked that more than what I had at the time (now engine mode 1)
-
-## Engine Mode 1 (Time Based)
-
-Files are deleted purely depending on their last view.
-
-This lets the least popular files be deleted after they expire, and the most popular files stay around longer.
-However, this allows people to keep a file alive indefinitely by just simply making a web request every `deletiontime` days.
-
-## Engine Mode 2 (Sized Based)
-
-Files are deleted purely depending on their filesize.
-TL;DR: Big files are deleted sooner, than small files.
-
-This solves the disadvantage of engine mode 1, where files can be kept alive indefinitely by the user.
-This is an exact recreation of 0x0.st's algorithm, so I'll also steal their graph.
-
-```text
-
-   days
-    365 |  \
-        |   \
-        |    \
-        |     \
-        |      \
-        |       \
-        |        ..
-        |          \
-  197.5 | ----------..-------------------------------------------
-        |             ..
-        |               \
-        |                ..
-        |                  ...
-        |                     ..
-        |                       ...
-        |                          ....
-        |                              ......
-     30 |                                    ....................
-          0                      512.0                     1024.0
-                                                              MiB
-```
-
-The numbers are a little different, depending on how your config looks, but it works the same.
-
-```python3
-stat = os.stat(os.path.join(config['UPLOAD_FOLDER'], f))
-  systime = time.time()
-  age = timedelta(seconds = systime - stat.st_mtime).days
-  maxage = mind + (-maxd + mind) * (stat.st_size / maxs - 1) ** 3
-  if age >= maxage:
-    delete_file(f)
-```
-
-## Engine Mode 3 (Hybrid 'Popularity' Based)
-
-Files are deleted on a aggregate score based on size and popularity (large, popular files are kept alive longer, vs unpopular files most likely just used once)
-
-## Engine Mode 4 (Lazy-Hybrid 'Popularity' Based)
-
-Files are deleted on size AND last view, depending on what comes last (small files stay the same, long files have potential to stay alive longer)
-
-## Expiry Overrides
-
-Overriding the expiry will work by having a (normally) null expiry_override column in the DB, when set, will override the expiry of the file.
-
-This can be longer or shorter than the normal expiry. However there should be checks for if it's longer, as I'd like for that to be a 'premium' action, requiring a api key or some other method of limitation.
diff --git a/docs/expiry mockup.png b/docs/expiry mockup.png
deleted file mode 100644
index ca236de12d39345f49090bb28de80f84dedddbd3..0000000000000000000000000000000000000000
Binary files a/docs/expiry mockup.png and /dev/null differ
diff --git a/docs/info.md b/docs/info.md
deleted file mode 100644
index 639954a9f32766b894133f71cf2b81cd752060e1..0000000000000000000000000000000000000000
--- a/docs/info.md
+++ /dev/null
@@ -1,73 +0,0 @@
-# Stuff and things
-
-This means each website can have a completely independant theme, and if you put each server behind cloudflare, you would never really know that they're running on the same server.
-
-Additionally, I'll have to add file uploading in a similar way. where there is a domain under `files/` that organises uploaded files.
-
-## Better Grafana Stuff
-
-Generating decent, useable stats is a problem with the quadfile that annoyed me, my first implementation didn't track stats well enough.
-I'm grouping the stats into 3 categories, each with a different scope "global", "filetype" and "file". This makes it easy to figure out what I'll need to query from the database in the future.
-
-Deleted files should no longer show up in most stats generated, since storing the same block of data forever is a waste. They're not going to change anyway.
-
-### General Stats
-
-- Total number of files alive /right now/ `total_alive{filetype='*', host="localhost"}`
-- Total number of files uploaded `total_alive{filetype='*', host="localhost"}`
-- Total number of files uploaded (by x ip) `filecount_ip`
-- Total filesize of alive files `filesize_alive`
-- Total filesize of dead files `filesize_dead`
-- Total bandwidth served for alive files(calculated by views * filesize) `bandwidth_alive{file="*"}` (this will need to be done per file)
-- Total bandwidth served for dead files(calculated by views * filesize) `bandwidth_dead{file="*"}` (this will need to be done per file)
-- Geo-map of files uploaded (I'm not too sure how we can do this)
-- Geo-map of files served (would require nginx logs)
-- Scrape render time `render_time`
-
-### Filetype Stats (Pick a filetype from the list, or even multiple?)
-
-- Total number of alive files uploaded (by x filetype) `total_alive{filetype=mp4}`
-- Total number of dead files (by x filetype) `total_dead{filetype=mp4}`
-- Total filesize of alive files `filesize_alive{filetype=mp4}`
-- Total filesize of dead files `filesize_dead{filetype=mp4}`
-- Total number of views for dead files `views_dead{filetype=mp4}`
-- Total number of views for alive files `views_alive{filetype=mp4}`
-- Total bandwidth for mimetype `file_bandwidth{mimetype="*"}` ()
-- Filesize Graph (Average/total? filesize per filetype) ?
-- Filesize Graph (Filesize vs lifetime) ?
-
-### File Stats (Pick a individual file from a list, or even multiple?)
-
-- Total Views on file `file_views{file="bleh.txt", mimetype="text/plain"}`
-- Total Bandwidth per file `file_bandwidth{file="bleh.txt", mimetype="text/plain"}`
-- File Size `filesize{file=hUMZCp.jpg}`
-
-### Malicious/Error Stats
-
-- Total Number of files blocked by x filetype
-- Total number of files blocked (by x ip)
-- Total number of errors (by x error type)
-- Filename generation collision errors
-- Adminkey generation collision errors
-
-## File Deletion
-
-### Adminkey deletion
-
-1. DELETE or GET `/delete/<adminkey>` request.
-2. Check in db if adminkey has a valid filename that isn't already deleted.
-3. if it is valid, and not deleted, flag it in the DB as deleted, else we're done here.
-4. if that worked, delete the file from the DB.
-5. all done!
-
-### expiry deletion
-
-1. todo thread runs, and checks all files that aren't deleted already for expirydates PAST the current time.
-2. iterates through all files, and marks them as deleted and delete from disk.
-3. Wait for next timer.
-
-# TODO's
-
-TODO: See how many .unwraps I can handle to make this thing potentially crash less, and offer more decent errors.
-TODO: Test the guess_ip function behind nginx properly, I don't know if ipv6/unix socket works at all, or if they're passed through the ipv4.
-TODO: Figure out why you get "There was a problem with the file upload request. Please try again." when you upload a file with `cat bleh | supload -Vd http://127.0.0.1:8282/ -`
diff --git a/schema.sql b/schema.sql
index e23ec8989e2722efa53ac91d4f2e8e499a7ed56b..48c37ca2841e69c8d08bbe05d9bba88d50de8e59 100644
--- a/schema.sql
+++ b/schema.sql
@@ -11,9 +11,10 @@ CREATE TABLE IF NOT EXISTS 'files' (
             expiry INTEGER NOT NULL,
             expiry_override INTEGER,
             views INTEGER DEFAULT '0',
-            isDeleted INTEGER DEFAULT '0',
-            adminkey TEXT NOT NULL,
-            accessed INTEGER NOT NULL,
+            is_deleted INTEGER DEFAULT '0',
+            owner TEXT NOT NULL,
+            uploaded INTEGER NOT NULL,
+            last_access INTEGER NOT NULL,
             filesize INTEGER NOT NULL,
             IP TEXT NOT NULL,
             domain TEXT NOT NULL);
@@ -28,19 +29,36 @@ CREATE TABLE 'qrscan' (
             version INTEGER NOT NULL
         );
 
--- Admin API Key Table
-CREATE TABLE IF NOT EXISTS 'apikeys' (
-            key TEXT NOT NULL,
-            server_admin INTEGER NOT NULL,
-            expiry_override INTEGER NOT NULL,
-            upload_permanent INTEGER NOT NULL,
-            default_name INTEGER NOT NULL,
-            bypass_mimetypes INTEGER NOT NULL,
-            comment TEXT
+
+-- API Key Table - This is the first time I've used Foreign keys since uni. wow. --
+CREATE TABLE IF NOT EXISTS 'keys' (
+            uuid TEXT NOT NULL PRIMARY KEY,
+            key_level INTEGER NOT NULL,
+            parent_key TEXT,
+            revoked TEXT,
+            creation_time INTEGER NOT NULL,
+            comment TEXT,
+            FOREIGN KEY (parent_key) REFERENCES keys(uuid)
         );
 
--- Dummy file to stop compile error, temporary fix --
--- This is some seriously wacked up formatting.
+-- View showing keys and json list of their children --
+CREATE VIEW IF NOT EXISTS keys_with_children AS
+SELECT
+    parent.uuid AS uuid,
+    parent.key_level AS key_level,
+    parent.parent_key AS parent_key,
+    parent.revoked AS revoked,
+    parent.creation_time AS creation_time,
+    parent.comment AS comment,
+    json_group_array(child.uuid) AS child_uuids
+FROM keys parent
+LEFT JOIN keys child ON parent.uuid = child.parent_key
+GROUP BY parent.uuid
+ORDER BY parent.creation_time;
+
+-- Dummy file to stop sqlx compile error --
+-- This is some seriously wacked up formatting. --
+-- 2024-05-22: I don't think this is a temporary fix anymore. --
 INSERT INTO files (
-                         file,    mimetype,   expiry, adminkey,       accessed, filesize, ip,         domain)
-                VALUES ( 'dummy','text/plain','1',    'dummyadminkey','0',      '0',      '127.0.0.1','localhost');
\ No newline at end of file
+                         file,     mimetype,    expiry,  owner,           last_access, uploaded, filesize, ip,          domain)
+                VALUES ( 'dummy', 'text/plain', '1',     'dummyownerkey', '0',         '0',      '0',      '127.0.0.1', 'localhost');
\ No newline at end of file
diff --git a/src/config.rs b/src/config.rs
index 3733643ce8bd3c7d6ac3a032abc00f6fe1d2b692..eefa15aa0f27da6bff778f28b59263225bf1b33b 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -39,4 +39,12 @@ pub struct AppConfig {
     pub database: DatabaseConfig,
     pub logging: LoggingConfig,
     pub operations: OperationsConfig,
+    pub uploading: UploadConfig,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct UploadConfig {
+    pub allow_unverified_keys: bool,
+    pub max_key_generations: i32,
+    pub fallback_key: String,
 }
diff --git a/src/db.rs b/src/db.rs
index f5fb28e1c0637e6374957b172b85767010103065..6e1aa4daa08da5a4ebb26bf16f5f45245ebbd41e 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -2,6 +2,8 @@ use sqlx::{sqlite::SqliteQueryResult, Pool, Sqlite};
 use std::{collections::HashMap, time::SystemTime};
 use tracing::{debug, error};
 
+use crate::keys::Key;
+
 /// This struct represents a single file, with a few statistics about said file instance.
 pub struct FileMetric {
     pub filename: String,
@@ -30,7 +32,6 @@ pub enum DatabaseType {
     Sqlite,
     Postgres,
     Mysql,
-    Mssql,
     Oraclecloud, // https://github.com/kubo/rust-oracle because why not?
 }
 
@@ -41,7 +42,6 @@ impl DatabaseType {
             "sqlite" => Some(Self::Sqlite),
             "postgres" => Some(Self::Postgres),
             "mysql" => Some(Self::Mysql),
-            "mssql" => Some(Self::Mssql),
             "oraclecloud" => Some(Self::Oraclecloud),
             _ => None,
         }
@@ -60,28 +60,30 @@ pub async fn add_file(
     mimetype: &str,
     expiry: i32,
     // expiry_override: Option<i32>, // Optional - for future use where you can specify expiry at upload.
-    adminkey: &str,
-    accessed: i32, // Just the currenttime.
-    filesize: i32, // I dread calculating this.
-    ip: &str,      // set to the end-user IP of the upload request.
-    domain: &str,  // set to the HOST header of the upload request.
+    owner: &str,
+    last_access: i32, // Just the currenttime.
+    filesize: i32,    // I dread calculating this.
+    ip: &str,         // set to the end-user IP of the upload request.
+    domain: &str,     // set to the HOST header of the upload request.
 ) -> Result<SqliteQueryResult, sqlx::Error> {
     let result = sqlx::query!(
         "INSERT INTO files (
                 file,
                 mimetype,
                 expiry, 
-                adminkey, 
-                accessed,
+                owner, 
+                uploaded,
+                last_access,
                 filesize, 
                 ip, 
                 domain)
-                VALUES ( ?,?,?,?,?,?,?,? )",
+                VALUES ( ?,?,?,?,?,?,?,?,? )",
         file,
         mimetype,
         expiry,
-        adminkey,
-        accessed,
+        owner,
+        last_access,
+        last_access, // This one is saved as "uploaded" time.
         filesize,
         ip,
         domain
@@ -116,7 +118,7 @@ where
     let result = sqlx::query!(
         "SELECT COUNT(*) as count 
             FROM files 
-            WHERE file = ? and isDeleted = 0",
+            WHERE file = ? and is_deleted = 0",
         fname
     )
     .fetch_one(sqlconn)
@@ -138,25 +140,25 @@ where
     }
 }
 
-/// This function returns a filename that corresponds to the adminkey that is provided.
-/// This is used for deleting files, so this is mostly a 'single-use' operation, but I imagine that it could be used for something else.
-pub async fn check_adminkey(sqlconn: &Pool<Sqlite>, adminkey: &str) -> Result<String, sqlx::Error> {
+/// This function checks the owner of a file.
+/// Give it a filename, it'll return the owner of the file.
+pub async fn check_owner(sqlconn: &Pool<Sqlite>, filename: &str) -> Result<String, sqlx::Error> {
     let row = sqlx::query!(
-        "SELECT file FROM files WHERE adminkey = ? AND isDeleted = 0",
-        adminkey
+        "SELECT owner FROM files WHERE file = ? AND is_deleted = 0",
+        filename
     )
     .fetch_one(sqlconn)
     .await?;
 
-    let filename = row.file;
-    Ok(filename)
+    let owner = row.owner;
+    Ok(owner)
 }
 
-/// This returns the mimetype of the file from the database. line 141 end
+/// This returns the mimetype of the file from the database.
 /// This is used for serving files, so that we can set the mimetype of unsafe files.
 pub async fn get_mimetype(sqlconn: &Pool<Sqlite>, file: &str) -> Result<String, sqlx::Error> {
     let row = sqlx::query!(
-        "SELECT mimetype FROM files WHERE file = ? AND isDeleted = 0",
+        "SELECT mimetype FROM files WHERE file = ? AND is_deleted = 0",
         file
     )
     .fetch_one(sqlconn)
@@ -172,7 +174,7 @@ pub async fn get_mimetype(sqlconn: &Pool<Sqlite>, file: &str) -> Result<String,
 /// Returns the number of the number of rows affected (files deleted).
 pub async fn delete_file(sqlconn: &Pool<Sqlite>, filename: &String) -> Result<u64, sqlx::Error> {
     debug!("delete_file(adminkey: {})", filename);
-    let result = sqlx::query!("UPDATE files SET isDeleted = 1 WHERE file = ?", filename)
+    let result = sqlx::query!("UPDATE files SET is_deleted = 1 WHERE file = ?", filename)
         .execute(sqlconn)
         .await?;
 
@@ -184,15 +186,15 @@ pub async fn delete_file(sqlconn: &Pool<Sqlite>, filename: &String) -> Result<u6
 pub async fn update_fileview<S>(
     sqlconn: &Pool<Sqlite>,
     filename: S,
-    accessed: i32,
+    last_access: i32,
 ) -> Result<u64, sqlx::Error>
 where
     S: AsRef<str>,
 {
     let filename = filename.as_ref();
     let result = sqlx::query!(
-        "UPDATE files SET accessed = ?, views = views + 1 WHERE file = ?",
-        accessed,
+        "UPDATE files SET last_access = ?, views = views + 1 WHERE file = ?",
+        last_access,
         filename
     )
     .execute(sqlconn)
@@ -203,11 +205,11 @@ where
 /// Returns the unix timestamp of the last access - 0 if unviewed.
 /// This will break in 2038
 pub async fn get_accesss_time(sqlconn: &Pool<Sqlite>, filename: &str) -> Result<i64, sqlx::Error> {
-    let result = sqlx::query!("SELECT accessed FROM files WHERE file = ?", filename)
+    let result = sqlx::query!("SELECT last_access FROM files WHERE file = ?", filename)
         .fetch_one(sqlconn)
         .await?;
 
-    Ok(result.accessed)
+    Ok(result.last_access)
 }
 
 /// Generating a list of files that should be deleted
@@ -221,7 +223,7 @@ pub async fn get_old_files(sqlconn: &Pool<Sqlite>) -> Result<Vec<String>, sqlx::
     // Query for files where the expiry has passed (or the expiry_override has passed, if set.)
     // Also only returns files that haven't been deleted yet.
     let result = sqlx::query!(
-        "SELECT file FROM files WHERE (expiry_override <= ? AND expiry_override NOT NULL) OR (expiry <= ?) AND isDeleted = 0",
+        "SELECT file FROM files WHERE (expiry_override <= ? AND expiry_override NOT NULL) OR (expiry <= ?) AND is_deleted = 0",
         time,
         time
     )
@@ -267,29 +269,29 @@ pub async fn add_qrscan(
     .await
 }
 
-/// Updating the expiry_override of a file.
-/// There are a few limitations and considerations for expiry_override:
-///
-/// 1. The expiry_override must be set lower than the expiry of the file.
-/// 2. EXCEPT when the file has been uploaded by an API Key that has permission to change expiry_override beyond the expiry
-/// 3. The expiry_override cannot be set longer than file_expiry_max, unless the file has been uploaded by an API Key that allows 'permanent' uploads.
-///
-/// If /any/ of those limits are broken, this function will not update and return an error, with the reason why.
-fn update_expiry_override() {
-    todo!()
-}
-
-/// This function gets a array of all the files uploaded by a specific API Key.
+// /// Updating the expiry_override of a file.
+// /// There are a few limitations and considerations for expiry_override:
+// ///
+// /// 1. The expiry_override must be set lower than the expiry of the file.
+// /// 2. EXCEPT when the file has been uploaded by an API Key that has permission to change expiry_override beyond the expiry
+// /// 3. The expiry_override cannot be set longer than file_expiry_max, unless the file has been uploaded by an API Key that allows 'permanent' uploads.
+// ///
+// /// If /any/ of those limits are broken, this function will not update and return an error, with the reason why.
+// fn update_expiry_override() {
+//     todo!()
+// }
+
+/// This function gets a Vec of all the files uploaded by a specific API Key.
 /// This allows for the select few who hold an API key to view the information about the files they upload.
-pub async fn get_my_files(
+pub async fn get_files_by_key(
     sqlconn: &Pool<Sqlite>,
-    ip: &str,
+    owner: &Key,
 ) -> Result<Vec<FileMetric>, sqlx::Error> {
     let result = sqlx::query!(
-        "SELECT file, filesize, mimetype, views, expiry, expiry_override, ip, isDeleted
+        "SELECT file, filesize, mimetype, views, expiry, expiry_override, ip, is_deleted
             FROM files
-            WHERE ip = ?",
-        ip
+            WHERE owner = ?",
+        owner.uuid
     )
     .fetch_all(sqlconn)
     .await?;
@@ -297,8 +299,8 @@ pub async fn get_my_files(
     let mut files: Vec<FileMetric> = Vec::new();
 
     for row in result {
-        // Convert the isDeleted from a int to a bool
-        let is_deleted = row.isDeleted.map(|v| v != 0).unwrap_or(false);
+        // Convert the is_deleted from a int to a bool
+        let is_deleted = row.is_deleted.map(|v| v != 0).unwrap_or(false);
 
         // For each file in the result, make a new FileMetric object and add it to the list
         let file = FileMetric {
@@ -314,6 +316,19 @@ pub async fn get_my_files(
     Ok(files)
 }
 
+/// Returns how many files exist on the server.
+pub async fn file_count(sqlconn: &Pool<Sqlite>) -> Result<i32, sqlx::Error> {
+    let count = sqlx::query!(
+        r#"
+            SELECT COUNT(*) as count FROM files
+        "#
+    )
+    .fetch_one(sqlconn)
+    .await?;
+
+    Ok(count.count)
+}
+
 // Globally important stats.
 
 /// This counts the total files that have been uploaded per ip address.
@@ -338,7 +353,7 @@ pub async fn get_total_uploads_ip(sqlconn: &Pool<Sqlite>) -> Option<HashMap<Stri
         None
     } else {
         for row in result.unwrap() {
-            ipcount.insert(row.IP, row.filecount.expect("You shouldn't see this - this means there was an ip with 0 uploaded files in the db. HOW??"));
+            ipcount.insert(row.IP, row.filecount);
         }
         Some(ipcount)
     }
@@ -355,7 +370,7 @@ pub async fn total_alive_filesize(sqlconn: &Pool<Sqlite>) -> Option<u128> {
     let result = sqlx::query!(
         "SELECT filesize
             FROM files 
-            WHERE isDeleted == 0"
+            WHERE is_deleted == 0"
     )
     .fetch_all(sqlconn)
     .await;
@@ -378,13 +393,13 @@ pub async fn total_alive_filesize(sqlconn: &Pool<Sqlite>) -> Option<u128> {
 ///
 /// 3.095×10^26 TiB, or 309500000000000000000000000 TiB
 ///
-/// or 3700000000000000000000 * more data than exists on the internet (as of 2014).
+/// or 3700000000000000000000 times more data than exists on the internet (as of 2014).
 pub async fn total_dead_filesize(sqlconn: &Pool<Sqlite>) -> Option<u128> {
     let mut total_dead_filesize: u128 = 0;
     let result = sqlx::query!(
         "SELECT filesize
             FROM files 
-            WHERE isDeleted == 1"
+            WHERE is_deleted == 1"
     )
     .fetch_all(sqlconn)
     .await;
@@ -408,7 +423,7 @@ pub async fn get_file_metrics(sqlconn: &Pool<Sqlite>) -> Option<Vec<FileMetric>>
     let result = sqlx::query!(
         "SELECT file, filesize, mimetype, views, expiry
         FROM files 
-        WHERE isdeleted == 0",
+        WHERE is_deleted == 0",
     )
     .fetch_all(sqlconn)
     .await;
@@ -448,12 +463,12 @@ pub async fn get_mimetype_metrics(
     // I cannot wait until it takes our jobs.
     let result = sqlx::query!(
         "SELECT mimetype, domain, 
-            SUM(CASE WHEN isDeleted = 0 THEN views ELSE 0 END) AS alive_views, 
-            SUM(CASE WHEN isDeleted = 1 THEN views ELSE 0 END) AS dead_views, 
-            SUM(CASE WHEN isDeleted = 0 THEN 1 ELSE 0 END) AS alive_files, 
-            SUM(CASE WHEN isDeleted = 1 THEN 1 ELSE 0 END) AS dead_files, 
-            SUM(CASE WHEN isDeleted = 0 THEN filesize ELSE 0 END) AS alive_filesize,
-            SUM(CASE WHEN isDeleted = 1 THEN filesize ELSE 0 END) AS dead_filesize
+            SUM(CASE WHEN is_deleted = 0 THEN views ELSE 0 END) AS alive_views, 
+            SUM(CASE WHEN is_deleted = 1 THEN views ELSE 0 END) AS dead_views, 
+            SUM(CASE WHEN is_deleted = 0 THEN 1 ELSE 0 END) AS alive_files, 
+            SUM(CASE WHEN is_deleted = 1 THEN 1 ELSE 0 END) AS dead_files, 
+            SUM(CASE WHEN is_deleted = 0 THEN filesize ELSE 0 END) AS alive_filesize,
+            SUM(CASE WHEN is_deleted = 1 THEN filesize ELSE 0 END) AS dead_filesize
         FROM files
         GROUP BY mimetype, domain;",
     )
@@ -465,12 +480,12 @@ pub async fn get_mimetype_metrics(
         .map(|row| MimetypeMetric {
             mimetype: row.mimetype.clone(),
             host: row.domain.clone(),
-            alive_files: row.alive_files.unwrap_or_default() as i64,
-            dead_files: row.dead_files.unwrap_or_default() as i64,
-            filesize_alive: row.alive_filesize.unwrap_or_default() as i64,
-            filesize_dead: row.dead_filesize.unwrap_or_default() as i64,
-            views_alive: row.alive_views.unwrap_or_default() as i64,
-            views_dead: row.dead_views.unwrap_or_default() as i64,
+            alive_files: row.alive_files as i64,
+            dead_files: row.dead_files as i64,
+            filesize_alive: row.alive_filesize as i64,
+            filesize_dead: row.dead_filesize as i64,
+            views_alive: row.alive_views.unwrap_or(0) as i64,
+            views_dead: row.dead_views.unwrap_or(0) as i64,
         })
         .collect::<Vec<MimetypeMetric>>();
 
diff --git a/src/db_tests.rs b/src/db_tests.rs
index 8cf1e27f00ece044c29166198760eefa06dadff2..44d6de360b79868fbfa684d73c75493635fc8bf2 100644
--- a/src/db_tests.rs
+++ b/src/db_tests.rs
@@ -1,3 +1,4 @@
+#[cfg(test)]
 pub(crate) mod tests {
     use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
 
@@ -9,19 +10,19 @@ pub(crate) mod tests {
         // Create the necessary tables and indices
         sqlx::query(
             "
-                CREATE TABLE IF NOT EXISTS 'files' (
+            CREATE TABLE IF NOT EXISTS 'files' (
                 file TEXT NOT NULL,
                 mimetype TEXT NOT NULL,
                 expiry INTEGER NOT NULL,
                 expiry_override INTEGER,
                 views INTEGER DEFAULT '0',
-                isDeleted INTEGER DEFAULT '0',
-                adminkey TEXT NOT NULL,
-                accessed INTEGER NOT NULL,
+                is_deleted INTEGER DEFAULT '0',
+                owner TEXT NOT NULL,
+                uploaded INTEGER NOT NULL,
+                last_access INTEGER NOT NULL,
                 filesize INTEGER NOT NULL,
                 IP TEXT NOT NULL,
-                domain TEXT NOT NULL
-            );
+                domain TEXT NOT NULL);
         ",
         )
         .execute(&pool)
diff --git a/src/engine.rs b/src/engine.rs
index 5bc01056bd133dd6d671ce06878ceac5af71e905..b7a7ae583ef1c0b85dc0186da7a08bdc60014e01 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -34,24 +34,6 @@ where
     random_filename
 }
 
-// Generate a random admin for the uploaded file
-// This should check if the adminkey exists for another file, and regenerate if it does.
-pub async fn generate_adminkey(sqlconn: &Pool<Sqlite>) -> String {
-    let adminkey_size: usize = CONFIG
-        .operations
-        .adminkey_size
-        .try_into()
-        .expect("Problem with converting adminkey_size to usize");
-
-    let adminkey = nanoid!(adminkey_size);
-    let result = db::check_adminkey(sqlconn, &adminkey).await;
-    if result.is_ok() {
-        // Adminkey resolves to a filename, so we better regenerate it.
-        return nanoid!(adminkey_size);
-    }
-    adminkey
-}
-
 pub async fn calculate_expiry<S>(sqlconn: &Pool<Sqlite>, filename: S, filesize: i32) -> i32
 where
     S: AsRef<str>,
@@ -61,7 +43,7 @@ where
     let expiry = match engine_mode {
         1 => engine_1(sqlconn, filename.as_ref()).await,
         2 => engine_2(filesize).await,
-        3 => engine_3(sqlconn, filename.as_ref(), filesize).await,
+        // 3 => engine_3(sqlconn, filename.as_ref(), filesize).await,
         _ => {
             error!("Unknown engine mode: {}", engine_mode);
             std::process::exit(2);
@@ -137,12 +119,13 @@ async fn engine_2(filesize: i32) -> i32 {
     expiry as i32
 }
 
-async fn engine_3<S>(sqlconn: &Pool<Sqlite>, filename: S, filesize: i32) -> i32
-where
-    S: AsRef<str>,
-{
-    todo!()
-}
+// /// Engine 3 deletes files based on both size and last view (large files can stay alive longer, if they're viewed regularly)
+// async fn engine_3<S>(sqlconn: &Pool<Sqlite>, filename: S, filesize: i32) -> i32
+// where
+//     S: AsRef<str>,
+// {
+//     todo!()
+// }
 
 /// This is a quick one that just runs mia's 0x0.st lifetime algorithm
 /// This is split from engine_2 so engine_3 can call it too
diff --git a/src/handlers/admin.rs b/src/handlers/admin.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2935d2aa9e209739f7700bf15a439e5158c2cc18
--- /dev/null
+++ b/src/handlers/admin.rs
@@ -0,0 +1,366 @@
+use std::collections::HashMap;
+
+use chrono::{DateTime, NaiveDateTime, Utc};
+use chrono_humanize::{Accuracy, HumanTime, Tense};
+use salvo::Writer;
+use salvo::{
+    handler,
+    hyper::header::HOST,
+    oapi::{endpoint, extract::QueryParam},
+    prelude::StatusCode,
+    writing::Text,
+    Depot, Request, Response,
+};
+use tracing::{error, info, warn};
+
+use crate::db;
+use crate::{
+    handlers::{render_template, Generic, TemplateEnum},
+    keys::{
+        db_keys, {Key, KeyLevel},
+    },
+    SQLITE,
+};
+
+use super::Admin;
+
+/// This generates the admin page that allows admins to view all keys, edit them, and investigate files uploaded by them.
+#[handler]
+pub async fn admin(req: &mut Request, res: &mut Response, depot: &mut Depot) {
+    let headers = req.headers().clone();
+    let remote_addr = &req.remote_addr().clone();
+    let sqlconn = SQLITE.get().unwrap();
+
+    let client_key = depot
+        .get::<Key>("client_key")
+        .expect("Failed to grab API Key from depot")
+        .clone();
+
+    // We should make sure the user is an admin, since the auth middleware only checks if the key isn't revoked.
+    if client_key.key_level == KeyLevel::Master {
+        let key_list = db_keys::get_all_keys(sqlconn).await.unwrap();
+
+        let key_count_result = db_keys::count_keys(sqlconn).await;
+        let key_count = match key_count_result {
+            Ok(count) => count,
+            Err(e) => {
+                error!("Failed to count keys: {}", e);
+                res.status_code(StatusCode::INTERNAL_SERVER_ERROR);
+                res.render(Text::Json(r#"{"error": "CountKeysFailed"}"#));
+                return;
+            }
+        };
+
+        let file_count_result = db::file_count(sqlconn).await;
+        let file_count = match file_count_result {
+            Ok(count) => count,
+            Err(e) => {
+                error!("Failed to count files: {}", e);
+                res.status_code(StatusCode::INTERNAL_SERVER_ERROR);
+                res.render(Text::Json(r#"{"error": "CountFilesFailed"}"#));
+                return;
+            }
+        };
+
+        let tree = construct_tree(key_list);
+
+        let html = generate_html_tree(&tree);
+
+        let template_filename = "admin.html";
+        let template = TemplateEnum::Admin(Admin {
+            domain: String::from(headers[HOST].to_str().unwrap()),
+            apikey_count: key_count.to_string(),
+            file_count: file_count.to_string(),
+            key_tree: html,
+        });
+        let status_code = StatusCode::OK;
+
+        render_template(res, &headers, template_filename, template, status_code).await;
+        return;
+    } else {
+        info!(
+            "[{:?} - {:?}] User attempted to view '/admin' without a valid master key. Blocking.",
+            headers[HOST].to_str().unwrap(),
+            remote_addr
+        );
+        let template_filename = "error.html";
+        let template = TemplateEnum::Generic(Generic {
+            domain: String::from(headers[HOST].to_str().unwrap()),
+            message1: String::from("Error 403"),
+            message2: String::from("You don't seem very 'adminy' to me."),
+            ..Default::default()
+        });
+        let status_code = StatusCode::FORBIDDEN;
+        render_template(res, &headers, template_filename, template, status_code).await;
+        return;
+    }
+}
+
+/// Struct for handling the hashmap.
+pub struct TreeNode {
+    pub key: Key,
+    pub children: Vec<TreeNode>,
+}
+
+/// Constructs the tree used for the admin page.
+/// This might take quite a bit of processing and ram. Who really knows though?
+/// Imagine if sqlx didn't break when I tried adding references to other keys for their children.
+/// That would be truly amazing, and it would be nowhere near as complicated as this.
+fn construct_tree(keys: Vec<Key>) -> Vec<TreeNode> {
+    // Create a HashMap that stores the keys.
+    let mut key_map = HashMap::new();
+
+    // Populate the HashMap with key UUID mapping to the actual Key struct
+    for key in keys.into_iter() {
+        key_map.insert(key.uuid.clone(), key);
+    }
+
+    // Initialise a (empty) List of nodes with no parents.
+    let mut root_nodes: Vec<TreeNode> = Vec::new();
+
+    // Recursive functions? You can do that??
+    fn build_tree_node(
+        key: Key,
+        key_map: &HashMap<String, Key>,
+        children_map: &HashMap<String, Vec<String>>,
+    ) -> TreeNode {
+        // Get the list of children UUIDs.
+        let binding = Vec::new();
+        let children_uuids = children_map.get(&key.uuid).unwrap_or(&binding);
+
+        // Build the children recursively.
+        let children: Vec<TreeNode> = children_uuids
+            .iter()
+            .filter_map(|uuid| key_map.get(uuid))
+            .map(|child_key| build_tree_node(child_key.clone(), key_map, children_map))
+            .collect();
+
+        // Return a TreeNode with the key itself and its children.
+        TreeNode { key, children }
+    }
+
+    // Create a HashMap for child UUIDs
+    let mut children_map = HashMap::new();
+    for key in key_map.values() {
+        let child_uuids: Vec<String> = key.child_uuids.clone(); // Assume `Key` struct now has `child_uuids: Vec<String>`
+        children_map.insert(key.uuid.clone(), child_uuids);
+    }
+
+    // Iterate on all keys again
+    for key in key_map.values() {
+        // Find the children of keys with no parents (master keys)
+        if key.parent_key.is_none() {
+            root_nodes.push(build_tree_node(key.clone(), &key_map, &children_map));
+        }
+    }
+
+    // root_nodes now contains the full forest of trees
+    root_nodes
+}
+
+// Top-level function to generate the entire HTML tree
+fn generate_html_tree(tree: &[TreeNode]) -> String {
+    let mut html = String::new();
+    html.push_str("<ul id=\"hide_bullets\">");
+    for root in tree {
+        html.push_str(&generate_html(root));
+    }
+    html.push_str("</ul>");
+    html
+}
+
+// Helper function to recursively generate HTML for the tree
+fn generate_html(node: &TreeNode) -> String {
+    // Humanize the time.
+    let key_creation = {
+        let datetime: DateTime<Utc> = DateTime::from_utc(
+            NaiveDateTime::from_timestamp_opt(node.key.creation_time.into(), 0).unwrap(),
+            Utc,
+        );
+        let human_time = HumanTime::from(datetime);
+        human_time.to_text_en(Accuracy::Rough, Tense::Past)
+    };
+
+    let details = format!(
+        "🔑 {} 🔒 {}{} 🕛 {}{}",
+        node.key.uuid,
+        node.key.key_level,
+        if let Some(ref comment) = node.key.comment {
+            format!(
+                " <span class=\"editable\" data-key=\"{}\">💬 {}</span>",
+                node.key.uuid, comment
+            )
+        } else {
+            format!(
+                " <span class=\"editable\" data-key=\"{}\">💬 Add Comment</span>",
+                node.key.uuid
+            )
+        },
+        key_creation,
+        if let Some(ref revoked_reason) = node.key.revoked {
+            format!(" 🚫 {}", revoked_reason)
+        } else {
+            "".to_string()
+        }
+    );
+
+    // Grab the number of children a key has.
+    let child_count = node.children.len();
+
+    // Add the start of the key.
+    let mut html = "<li class=\"key-node\">".to_string();
+
+    // If there's children keys, show them.
+    html.push_str(&format!(
+        "<span class=\"toggle\" collapsed=\"{}\">{}</span> ",
+        if child_count > 0 { "false" } else { "none" },
+        if child_count > 0 { "▼" } else { "" }
+    ));
+
+    // Show the details of keys.
+    html.push_str(&details.to_string());
+
+    // Add delete button
+    html.push_str(&format!(
+        " <span class=\"delete\" data-key=\"{}\">❌</span>",
+        node.key.uuid
+    ));
+
+    // If there's children, make a new list and call this again on them.
+    if child_count > 0 {
+        html.push_str("<ul class=\"children\" style=\"display: block;\">");
+        for child in &node.children {
+            html.push_str(&generate_html(child));
+        }
+        html.push_str("</ul>");
+    }
+
+    // Close the list item.
+    html.push_str("</li>");
+
+    html
+}
+
+/// Handler to process key revocation via the API.
+#[endpoint(
+    tags("admin"),
+    parameters(
+        ("apikey", description = "The API key you're about to ban."),
+        ("revoke_message", description = "The 'ban message' to show the user when they try to interact with the website."),
+        ("recursive", description = "Do we also delete all children keys?"),
+    )
+)]
+pub async fn admin_revoke_key(
+    req: &mut Request,
+    res: &mut Response,
+    depot: &mut Depot,
+    revoke_message: QueryParam<String, true>,
+    recursive: QueryParam<bool, false>,
+) {
+    // Base Stuffs
+    let sqlconn = SQLITE.get().unwrap();
+    let client_key = depot
+        .get::<Key>("client_key")
+        .expect("Failed to grab API Key from depot")
+        .clone();
+
+    let revoke_key: String = req.param("apikey").expect("Invalid API Key.");
+
+    // Make sure the user is a master key.
+    if client_key.key_level == KeyLevel::Master {
+        // Parse the key and message from the request.
+        let revoke_message = revoke_message.as_str();
+
+        // was the delete recursive, if so we want to grab all the children, ban them too.
+        match recursive.into_inner().unwrap_or(false) {
+            true => {
+                // Build a vector of all children keys. Recursively.
+                // let mut keys_to_revoke = [revoke_key.clone()];
+                todo!()
+            }
+            false => {
+                // Just delete the provided key.
+                match db_keys::revoke_key(sqlconn, &revoke_key, revoke_message).await {
+                    Ok(keys_revoked) => {
+                        // 0 keys revoked must mean it wasn't a key.
+                        if keys_revoked == 0 {
+                            res.status_code(StatusCode::OK);
+                            res.render(Text::Json("{\"error\": \"NoKeyFound\"}"));
+                            return;
+                        } else {
+                            res.status_code(StatusCode::OK);
+                            res.render(Text::Json("{\"success\": \"KeyRevoked\"}"));
+                            return;
+                        }
+                    }
+                    Err(e) => {
+                        error!("Error while revoking key: {}", e);
+                        res.status_code(StatusCode::INTERNAL_SERVER_ERROR);
+                        res.render(Text::Json(r#"{"error": "RevokeKeyFailed"}"#));
+                        return;
+                    }
+                };
+            }
+        }
+    } else {
+        res.status_code(StatusCode::FORBIDDEN);
+        res.render(Text::Json(r#"{"error": "Unauthorized"}"#));
+    }
+}
+
+/// This handler process editing key comments via the API.
+#[endpoint(
+    tags("admin"),
+    parameters(
+        ("apikey", description = "The API key you're about to edit."),
+        ("comment", description = "The new comment to set for the provided API Key."),
+    )
+)]
+pub async fn admin_edit_comment(
+    req: &mut Request,
+    res: &mut Response,
+    depot: &mut Depot,
+    comment: QueryParam<String, true>,
+) {
+    // Base Stuffs
+    let sqlconn = SQLITE.get().unwrap();
+    let client_key = depot
+        .get::<Key>("client_key")
+        .expect("Failed to grab API Key from depot")
+        .clone();
+
+    let edit_key: String = req
+        .param("apikey")
+        .expect("Failed to set the filename variable");
+
+    // Make sure the user is a master key.
+    if client_key.key_level == KeyLevel::Master {
+        let comment = comment.into_inner();
+
+        // Update the comment for the provided key.
+        match db_keys::edit_comment(sqlconn, &edit_key, &comment).await {
+            Ok(success) => {
+                if success {
+                    res.status_code(StatusCode::OK);
+                    info!(
+                        "[ADMIN] Updated comment of key ({}) to: {}",
+                        &edit_key, &comment
+                    );
+                    res.render(Text::Json("{\"success\": \"CommentUpdated\"}"));
+                } else {
+                    res.status_code(StatusCode::OK);
+                    warn!("Cosmic Ray Detected, impossible we got to an impossible path!");
+                    res.render(Text::Json("{\"error\": \"UpdateFailed\"}"));
+                }
+            }
+            Err(e) => {
+                res.status_code(StatusCode::INTERNAL_SERVER_ERROR);
+                error!("Error while editing comment: {}", e);
+                res.render(Text::Json(r#"{"error": "EditCommentFailed"}"#));
+            }
+        }
+    } else {
+        res.status_code(StatusCode::FORBIDDEN);
+        res.render(Text::Json(r#"{"error": "Unauthorized"}"#));
+    }
+}
diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs
new file mode 100644
index 0000000000000000000000000000000000000000..63b70a6c811cb731c31a853676d7c04fdcfe86e8
--- /dev/null
+++ b/src/handlers/auth.rs
@@ -0,0 +1,181 @@
+use salvo::{
+    http::header::{self, HOST},
+    prelude::*,
+};
+use tracing::{debug, error, info};
+
+use crate::{
+    handlers::{is_browser, render_template, Generic, TemplateEnum},
+    keys::{self, db_keys},
+    SQLITE,
+};
+
+/// This handles authentication as a middleware.
+/// We accept Authorization: Bearer <token> keys. While it's not directly OAuth2 compliant, it'll work.
+/// If unverified_keys is true, we essentially just say "all keys are valid"
+/// The Depot stores the Key (object, including the uuid), ip and host in the Depot, so we can check it later when handling the actual requests.
+#[handler]
+pub async fn require_auth(res: &mut Response, req: &mut Request, depot: &mut Depot) {
+    // Initialise the basic stuff we need
+    // Clone out the header and remote_addr so we can guess the ip later
+    let headers = &req.headers().clone();
+    let remote_addr = &req.remote_addr().clone();
+    // Get the host header for nicely setting up the response.
+    debug!("require_auth(req): {:?}", req);
+
+    // Setup the Sqlite pool stuff for later
+    let sqlconn = SQLITE.get().unwrap();
+    let is_user_web = is_browser(req).await;
+    // Debug print the headers found.
+    for header in headers.iter() {
+        debug!("header: {:?}", header);
+    }
+    // Debug print cookies found.
+    for cookie in req.cookies().iter() {
+        debug!("cookie: {:?}", cookie)
+    }
+    // Check for a token in the header.
+    let header_key: Option<&str> = req.header(header::AUTHORIZATION);
+    // Split token into apikey if present.
+    let token_from_header = match header_key {
+        Some(token) => token.strip_prefix("Bearer "),
+        None => None,
+    };
+    // Check, and map the token from cookie.
+    let token_from_cookie: Option<&str> = req.cookies().get("api-key").map(|cookie| cookie.value());
+    // Let the token be either one of these.
+    let token = token_from_cookie.or(token_from_header);
+    if token.is_none() {
+        info!(
+            "[{:?} - {:?}] Attempted Access of authorised endpoint. Blocking.",
+            headers[HOST].to_str().unwrap(),
+            remote_addr
+        );
+        // Key is not Present. Failing.
+        if is_user_web {
+            // We need to check if they're a web user, since this middleware protects both API and frontend pages.
+            let template_filename = "error.html";
+            let template = TemplateEnum::Generic(Generic {
+                domain: String::from(headers[HOST].to_str().unwrap()),
+                message1: String::from("Error 401"),
+                message2: String::from("This server is configured to require authentication for this action. Did you forget your API Key?"),
+                ..Default::default()
+            });
+            let status_code = StatusCode::FORBIDDEN;
+            render_template(res, headers, template_filename, template, status_code).await;
+            return;
+        } else {
+            // Otherwise, render the error in json for the API.
+            res.status_code(StatusCode::FORBIDDEN);
+            res.render(Text::Json(r#"{"error": "MissingAPIKey"}"#));
+            return;
+        }
+    } else {
+        // Retrieve the Key from the database, if it exists.
+        let key_result =
+            db_keys::get_key(sqlconn, token.expect("Somehow there were no tokens.")).await;
+        // Do the actual check to handle possible outcomes
+        match key_result {
+            Ok(valid_key) => {
+                // Key exists.
+
+                // Check if the key (from key_result) has been revoked.
+                if let Some(revoke_message) = valid_key.revoked {
+                    // Banned, show ban message.
+                    info!(
+                        "[{:?} - {:?}] Attempted File upload with revoked API Key. Blocking.",
+                        headers[HOST].to_str().unwrap(),
+                        remote_addr
+                    );
+                    // Check if the upload came from the web
+                    if is_user_web {
+                        let template_filename = "error.html";
+                        let template = TemplateEnum::Generic(Generic {
+                            domain: String::from(headers[HOST].to_str().unwrap()),
+                            message1: String::from("Key Revoked"),
+                            message2: revoke_message,
+                            ..Default::default()
+                        });
+
+                        let status_code = StatusCode::FORBIDDEN;
+
+                        render_template(res, headers, template_filename, template, status_code)
+                            .await;
+                        return;
+                    } else {
+                        // Otherwise, render the error in json for the API.
+                        res.status_code(StatusCode::FORBIDDEN);
+                        res.render(Text::Json(format!(
+                            r#"{{"error": "APIKeyRevoked", "reason": "{}"}}"#,
+                            revoke_message
+                        )));
+                        return;
+                    }
+                } else {
+                    // Not banned, push the key object to the depot and continue on with the upload.
+                    // Clone it so we can pass it to the debug. That's literally it. The function ends here so it's not really a performance problem. (I hope)
+                    depot.insert("client_key", valid_key.clone());
+                    debug!(
+                        "Key {} @ level: {:?} passed the vibe check. Proceeding to page",
+                        &valid_key.uuid, &valid_key.key_level
+                    );
+                    // Return out of the function, we've done all the checks! \o/
+                    return;
+                }
+            }
+            Err(keys::db_keys::KeyError::NotFound) => {
+                // Key not found.
+                info!(
+                    "[{:?} - {:?}] Attempted Access with an invalid API Key ({}). Blocking.",
+                    headers[HOST].to_str().unwrap(),
+                    remote_addr,
+                    token.expect("Somehow failed to unwrap token.")
+                );
+                // Check if the upload came from the web
+                if is_user_web {
+                    let template_filename = "error.html";
+                    let template = TemplateEnum::Generic(Generic {
+                        domain: String::from(headers[HOST].to_str().unwrap()),
+                        message1: String::from("Error 401"),
+                        message2: String::from("You're using an invalid API Key. Sorry."),
+                        ..Default::default()
+                    });
+
+                    let status_code = StatusCode::UNAUTHORIZED;
+
+                    render_template(res, headers, template_filename, template, status_code).await;
+                    return;
+                } else {
+                    // Otherwise, render the error in json for the API.
+                    res.status_code(StatusCode::UNAUTHORIZED);
+                    res.render(Text::Json(r#"{"error": "InvalidAPIKey"}"#));
+                    return;
+                }
+            }
+            Err(keys::db_keys::KeyError::DatabaseError(e)) => {
+                // Error when reading the database.
+                error!("Error when reading the database: {:?}", e);
+                // Check if the upload came from the web
+                if is_user_web {
+                    let template_filename = "error.html";
+                    let template = TemplateEnum::Generic(Generic {
+                        domain: String::from(headers[HOST].to_str().unwrap()),
+                        message1: String::from("Error 500"),
+                        message2: String::from("Database error when matching client key."),
+                        ..Default::default()
+                    });
+
+                    let status_code = StatusCode::SERVICE_UNAVAILABLE;
+
+                    render_template(res, headers, template_filename, template, status_code).await;
+                    return;
+                } else {
+                    // Otherwise, render the error in json for the API.
+                    res.status_code(StatusCode::SERVICE_UNAVAILABLE);
+                    res.render(Text::Json(r#"{"error": "DatabaseFailure"}"#));
+                    return;
+                }
+            }
+        }
+    }
+}
diff --git a/src/handlers/delete_file.rs b/src/handlers/delete_file.rs
index 471602f69ed9cc6d89f8688042d8f5622c232861..c702a7a260e4ba073591d5869fc7c681a3c82bc2 100644
--- a/src/handlers/delete_file.rs
+++ b/src/handlers/delete_file.rs
@@ -1,101 +1,77 @@
 use salvo::{
-    handler,
-    hyper::header::{self, HOST},
-    prelude::StatusCode,
-    writer::Text,
-    Request, Response,
+    hyper::header::HOST, oapi::endpoint, prelude::StatusCode, writing::Text, Depot, Request,
+    Response,
 };
 use tracing::{debug, error, info};
 
-use crate::{
-    db, engine,
-    handlers::{render_template, TemplateStruct},
-    SQLITE,
-};
+use crate::{db, engine, keys::Key, SQLITE};
 
-/// This takes the adminkey in, and deletes the file that matches it in the DB.
-#[handler]
-pub async fn delete_file(req: &mut Request, res: &mut Response) {
+/// Endpoint for deleting files.
+///
+/// This permanently deletes the physical file from disk, and marks the file deleted in the database.
+#[endpoint(
+    tags("file-management"),
+    parameters(
+        ("filename", description = "The name of the file you're about to delete."),
+    )
+)]
+pub async fn delete_file(req: &mut Request, res: &mut Response, depot: &mut Depot) {
     // Set up all the things we need for the function later.
     let headers = req.headers().clone();
+    let remote_addr = &req.remote_addr().clone();
     let sqlconn = SQLITE.get().unwrap();
-    let adminkey: &String = &req.param("adminkey").unwrap();
-    debug!("delete_file(adminkey): {:?}", adminkey);
+    let filename: &String = &req.param("filename").unwrap();
+
+    debug!("Received request to delete file: {}", filename);
+
+    // Grab the key from the auth handler.
+    let client_key = depot
+        .get::<Key>("client_key")
+        .expect("Failed to grab API Key from depot")
+        .clone();
 
-    // Returns true if the "source" field in the HTTP request's form data is "web", false otherwise or if an error occurs.
-    // Since we don't have a 'source' field in the request, we guess from the user agent. It's not perfect, but it works. and it's not really a security problem.
-    // Check if the request came from a web browser or something else.
-    let user_agent = headers
-        .get(header::USER_AGENT)
-        .and_then(|value| value.to_str().ok())
-        .unwrap_or("");
-    let upload_by_web = user_agent.contains("Mozilla");
+    // Grab they key from the database.
+    let owner = db::check_owner(sqlconn, filename).await;
 
-    // Sends the adminkey to the db and gets the filename to delete back.
-    let adminkey_result = db::check_adminkey(sqlconn, adminkey).await;
-    // Handle any potential errors.
-    match adminkey_result {
-        // adminkey returned a filename
-        Ok(fname) => {
-            // Actually delete the file from disk
-            db::delete_file(sqlconn, &fname)
-                .await
-                .unwrap_or_else(|err| {
-                    error!(
-                        "Failed to delete file from the database: {} error: {:?}",
-                        &fname, err
-                    );
-                    0
-                });
+    // Compare the client_key and owner.
+    let authorized = match owner {
+        Ok(owner_key) => client_key.uuid == owner_key,
+        Err(_) => false,
+    };
 
-            engine::delete_file(&fname).await.unwrap_or_else(|err| {
+    // Proceed with deletion if authorized, otherwise respond with an error.
+    if authorized {
+        // Actually delete the file from disk
+        db::delete_file(sqlconn, filename)
+            .await
+            .unwrap_or_else(|err| {
                 error!(
-                    "Failed to delete file from the filesystem: {} error: {:?}",
-                    &fname, err
+                    "Failed to delete file from the database: {} error: {:?}",
+                    filename, err
                 );
+                0
             });
 
-            info!("File deleted: {}", &fname);
+        engine::delete_file(&filename).await.unwrap_or_else(|err| {
+            error!(
+                "Failed to delete file from the filesystem: {} error: {:?}",
+                filename, err
+            );
+        });
 
-            // Respond to the client.
-            if upload_by_web {
-                let template = TemplateStruct {
-                    domain: String::from(headers[HOST].to_str().unwrap()),
-                    message1: String::from("File Deleted"),
-                    message2: String::from("woah where'd the file go???"),
-                    ..Default::default()
-                };
-                render_template(res, &headers, "error.html", template, StatusCode::OK).await
-            } else {
-                res.status_code(StatusCode::OK);
-                res.render(Text::Json(r#"{"deletion": "success"}"#));
-            }
-        }
+        info!(
+            "[{:?} - {:?}] User deleted file '{}'",
+            headers[HOST].to_str().unwrap(),
+            remote_addr,
+            filename
+        );
 
-        // adminkey didn't return a filename
-        Err(_) => {
-            // Respond to the client
-            if upload_by_web {
-                let template = TemplateStruct {
-                    domain: String::from(headers[HOST].to_str().unwrap()),
-                    message1: String::from("Error 401: Unauthorised"),
-                    message2: String::from(
-                        "You have entered an invalid adminkey, please enter a valid adminkey",
-                    ),
-                    ..Default::default()
-                };
-                render_template(
-                    res,
-                    &headers,
-                    "error.html",
-                    template,
-                    StatusCode::UNAUTHORIZED,
-                )
-                .await
-            } else {
-                res.status_code(StatusCode::UNAUTHORIZED);
-                res.render(Text::Json(r#"{"error": "InvalidAdminKey"}"#));
-            }
-        }
-    };
+        res.status_code(StatusCode::OK);
+        res.render(Text::Json(r#"{"deletion": "success"}"#));
+
+    // Invalid API Key. The API Exists, but isn't right.
+    } else {
+        res.status_code(StatusCode::UNAUTHORIZED);
+        res.render(Text::Json(r#"{"error": "InvalidAdminKey"}"#));
+    }
 }
diff --git a/src/handlers/index.rs b/src/handlers/index.rs
index 2380f4de64d4e12ea89f9713935ea15ad8ee3e1e..380e917906a7b8ae08dcc4775c6f9bf3c145ae10 100644
--- a/src/handlers/index.rs
+++ b/src/handlers/index.rs
@@ -2,27 +2,28 @@ use salvo::{handler, hyper::header::HOST, prelude::StatusCode, Request, Response
 use std::path::PathBuf;
 use tracing::{debug, info};
 
-use crate::handlers::{render_template, TemplateStruct};
+use crate::handlers::{render_template, Generic, TemplateEnum};
 
 /// This file handles (heh) the main index page for the application.
 #[handler]
 pub async fn index(req: &mut Request, res: &mut Response) {
     // Get the headers (for Host header stuff thats needed later)
     let headers = req.headers();
+    let remote_addr = &req.remote_addr().clone();
     let domain = headers[HOST].to_str().unwrap_or("localhost:8282");
-    info!("[{}]New Request: /", &domain);
+    info!(
+        "[{:?} - {:?}] New Request: /",
+        headers[HOST].to_str().unwrap(),
+        remote_addr
+    );
 
     // Renders the template based on `/templates/$host/upload.html`
     let template_host_path = PathBuf::from("./templates/").join(domain);
     debug!("template_with_host: {:?}", template_host_path);
 
-    let template = TemplateStruct {
+    let template = TemplateEnum::Generic(Generic {
         domain: String::from(headers[HOST].to_str().unwrap()),
-        message1: String::from("Error 401: Unauthorised"),
-        message2: String::from(
-            "You have entered an invalid adminkey, please enter a valid adminkey",
-        ),
         ..Default::default()
-    };
+    });
     render_template(res, headers, "upload.html", template, StatusCode::OK).await
 }
diff --git a/src/handlers/list_files.rs b/src/handlers/list_files.rs
deleted file mode 100644
index ae7009453f7aa251559acacf10a3bce9b6227a3b..0000000000000000000000000000000000000000
--- a/src/handlers/list_files.rs
+++ /dev/null
@@ -1,107 +0,0 @@
-use salvo::{handler, hyper::header::HOST, prelude::StatusCode, Request, Response};
-use tracing::{debug, error, info};
-
-use crate::{
-    db,
-    handlers::{
-        convert_file_size, convert_unix_timestamp, count_file_metrics, render_template,
-        TemplateStruct,
-    },
-    SQLITE,
-};
-
-use super::guess_ip;
-
-/// This file contains the handler that serves the users stats back to them.
-/// We expect a API Key to be provided in the cookie or header here.
-/// This will list only files that were uploaded with the provided API Key.
-#[handler]
-pub async fn list_files(req: &mut Request, res: &mut Response) {
-    // Setup db pool
-    let sqlconn = SQLITE.get().unwrap();
-
-    // Get the IP of the person viewing, so we can show their files
-    let headers = req.headers();
-    let remote_addr = &req.remote_addr().clone();
-    let ip = guess_ip(headers, remote_addr);
-
-    debug!("list_files(remote_addr, ip): {:?}, {:?}", remote_addr, ip);
-
-    // This returns a Vec of all the files uploaded by a specific IP Address
-    let files = db::get_my_files(sqlconn, &ip).await;
-
-    let mut html = String::new();
-    // For each file in the list, add the html to the rendered string
-    match files.as_ref() {
-        Ok(file_metrics) => {
-            for f in file_metrics {
-                // Change the filename to a file url
-                let fileurl = format!("/{}", &f.filename);
-
-                // Change the filesize to a human readable value
-                let human_size = convert_file_size(f.filesize);
-
-                // Change the expiry to a human readable value
-                let human_time = convert_unix_timestamp(f.expiry);
-
-                // If isDeleted, change colour to red, else green
-                let colour = if f.is_deleted {
-                    "background-color: #ff00000a;".to_string()
-                } else {
-                    "background-color: #00ff100a;".to_string()
-                };
-
-                // Add a new file to the rendered string
-                html.push_str(&format!("<tr style='{}'><td><a href={}>{}</a></td><td data-sort='{}'>{}</td><td>{}</td><td>{}</td><td data-sort='{}'>{}</td></tr>",
-                    colour,
-                    fileurl.as_str(),
-                    f.filename.as_str(),
-                    f.filesize,
-                    human_size.as_str(),
-                    f.mimetype.as_str(),
-                    f.views.to_owned(),
-                    f.expiry.to_owned(),
-                    human_time.to_owned(),
-                ));
-            }
-        }
-        Err(err) => {
-            // Log error to console
-            error!("Error retrieving file metrics: {:?}", err);
-
-            // Render a nice error page for the poor user
-            let template_filename = "error.html";
-            let template = TemplateStruct {
-                domain: String::from(headers[HOST].to_str().unwrap()),
-                message1: String::from("Error 500: Internal Server Error"),
-                message2: String::from(
-                    "Something went wrong while reading the result from the database, uh oh :(",
-                ),
-                ..Default::default()
-            };
-
-            let status_code = StatusCode::INTERNAL_SERVER_ERROR;
-            render_template(res, headers, template_filename, template, status_code).await;
-        }
-    }
-
-    // Close the table when all files are 'rendered'
-    html.push_str("</table>");
-
-    // Log this to the server console
-    info!(
-        "[{}]New Request: /my_files ({} Files)",
-        &headers[HOST].to_str().unwrap(),
-        count_file_metrics(&files)
-    );
-
-    let template_filename = "my_files.html";
-    let template = TemplateStruct {
-        domain: String::from(headers[HOST].to_str().unwrap()),
-        message1: html,
-        ..Default::default()
-    };
-    let status_code = StatusCode::OK;
-
-    render_template(res, headers, template_filename, template, status_code).await
-}
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
index e2ac2741f0e9aa6ab05ddf5bebc6a3c7f92595a8..04e0b6684a525ae03a1011ef451af288f5bf7a3b 100644
--- a/src/handlers/mod.rs
+++ b/src/handlers/mod.rs
@@ -12,8 +12,8 @@ use salvo::{
     conn::SocketAddr,
     hyper::{header::HOST, HeaderMap},
     prelude::StatusCode,
-    writer::Text,
-    Response,
+    writing::Text,
+    Request, Response,
 };
 use sqlx::{sqlite::SqliteQueryResult, Error};
 use tokio::{task, time};
@@ -21,27 +21,36 @@ use tracing::{debug, error, info};
 
 use crate::{
     db::{self, FileMetric},
-    engine::{self, calculate_expiry, generate_adminkey},
+    engine::{self, calculate_expiry},
     CONFIG, COOKIE, SQLITE,
 };
 
 // Submodules for each request handler
+pub mod admin;
+pub mod auth;
 pub mod delete_file;
 pub mod index;
-pub mod list_files;
 pub mod serve_file;
 pub mod serve_metrics;
 pub mod serve_static;
 pub mod upload;
+pub mod user_profile;
 
 /// This file contains sub-modules that are the handlers for each type of web request.
 /// Thanks audron for making me split my work up into multiple files.
 /// like seriously this /is/ more clean
 /// https://stackoverflow.com/questions/26435102/in-rust-what-is-the-purpose-of-a-mod-rs-file
 
-// This is needed for templating, all the 'variables' go here!
+/// This enum stores all valid Template Structs
+pub enum TemplateEnum {
+    Generic(Generic),
+    Profile(Profile),
+    Admin(Admin),
+}
+
+/// This struct stores the custom data shown on most pages.
 #[derive(Content, Default)]
-pub struct TemplateStruct {
+pub struct Generic {
     domain: String,
     fileurl: String,
     adminurl: String,
@@ -49,6 +58,25 @@ pub struct TemplateStruct {
     message2: String,
 }
 
+/// This struct stores the information shown to the user when browsing the /profile page.
+#[derive(Content, Default)]
+pub struct Profile {
+    domain: String,         // The domain the user is visiting from.
+    key_creation: String,   // A nice humanised time when the key was created.
+    key_generator: String,  // The HTML for the Key Generator frontend
+    key_list_html: String,  // The HTML for listing keys.
+    file_list_html: String, // The HTML for listing files.
+}
+
+/// This struct stores the information shown to the user when browsing the /profile page.
+#[derive(Content, Default)]
+pub struct Admin {
+    domain: String,       // The domain the user is visiting from.
+    apikey_count: String, // How many API Keys exist.
+    file_count: String,   // How many files exist.
+    key_tree: String,     // A nice humanised time when the key was created.
+}
+
 // Import tests for handlers
 #[cfg(test)]
 mod mod_tests;
@@ -88,20 +116,15 @@ pub fn guess_ip(headers: &HeaderMap, ip: &SocketAddr) -> String {
             match ip {
                 SocketAddr::IPv4(addr) => return addr.ip().to_string(),
                 SocketAddr::IPv6(addr) => return addr.ip().to_string(),
-                SocketAddr::Unix(_) => return "0.0.0.0".to_string(),
-                _ => return "0.0.0.0".to_string(),
+                SocketAddr::Unix(_) => "0.0.0.0".to_string(),
+                _ => "0.0.0.0".to_string(),
             }
         } else {
-            return x_real_ip.unwrap_or("0.0.0.0".to_string());
+            x_real_ip.unwrap_or("0.0.0.0".to_string())
         }
     } else {
-        return cloudflare_ip.unwrap_or("0.0.0.0".to_string());
+        cloudflare_ip.unwrap_or("0.0.0.0".to_string())
     }
-
-    // Unable to determine ip, use 0.0.0.0
-    debug!("guess_ip(headers): {:?}", headers);
-    error!("guess_ip(Failed to guess ip, falling back to 0.0.0.0)");
-    "0.0.0.0".to_string()
 }
 
 // Template rendering function
@@ -118,7 +141,7 @@ pub async fn render_template(
     res: &mut Response,
     headers: &HeaderMap,
     template_filename: &str,
-    template: TemplateStruct,
+    template: TemplateEnum,
     status_code: StatusCode,
 ) {
     // Parse the host from the headers
@@ -141,10 +164,21 @@ pub async fn render_template(
     };
 
     // Tell the engine to render the template assigned above
-    let rendered = tpls
-        .get(template_filename)
-        .expect("Couldn't find the template filename")
-        .render(&template);
+    // Render the template based on the given TemplateEnum
+    let rendered = match template {
+        TemplateEnum::Generic(data) => tpls
+            .get(template_filename)
+            .expect("Couldn't find the template filename")
+            .render(&data),
+        TemplateEnum::Profile(data) => tpls
+            .get(template_filename)
+            .expect("Couldn't find the template filename")
+            .render(&data),
+        TemplateEnum::Admin(data) => tpls
+            .get(template_filename)
+            .expect("Couldn't find the template filename")
+            .render(&data),
+    };
 
     // Set the status code and render the completed page
     res.status_code(status_code);
@@ -238,25 +272,25 @@ pub fn cleaner_thread(period: i32) {
 
             match old_files_result {
                 Ok(file_list) => {
-                    info!("Running Cleaner");
+                    info!("[Cleaner] Running Cleaner");
                     for file in file_list {
                         // Delete the file from the database
                         db::delete_file(sqlconn, &file).await.unwrap_or_else(|err| {
                             error!(
-                                "Failed to delete file from database: {}, error: {:?}",
+                                "[Cleaner] Failed to delete file from database: {}, error: {:?}",
                                 &file, err
                             );
                             0
                         });
                         // Delete the file from the filesystem
                         engine::delete_file(&file).await.unwrap_or_else(|err| {
-                            error!("Failed to delete file from database: {:?}", err);
+                            error!("[Cleaner] Failed to delete file from database: {:?}", err);
                         });
                     }
-                    info!("Cleaner finished");
+                    info!("[Cleaner] Cleaner finished");
                 }
                 Err(err) => {
-                    error!("Error getting files to expire: {}", err);
+                    error!("[Cleaner] Error getting files to expire: {}", err);
                     // Return a empty Vec so it doesn't delete anything
                     return Vec::<String>::new();
                 }
@@ -300,8 +334,9 @@ pub async fn add_undeleted_file(filename: &str) -> Result<SqliteQueryResult, sql
 
     // Calculate the expiry
     let expiry = calculate_expiry(sqlconn, filename, filesize).await;
-    // Generate an adminkey
-    let adminkey = generate_adminkey(sqlconn).await;
+    // LEGACY: This file shouldn't be able to be seen by anyone, since we don't really know the owner, and then nobody should be able to delete it.
+    // Grab the first master key we can find.
+    let masterkey = CONFIG.uploading.fallback_key.as_str();
     // Set accessed to now
     let accessed = SystemTime::now()
         .duration_since(SystemTime::UNIX_EPOCH)
@@ -317,7 +352,7 @@ pub async fn add_undeleted_file(filename: &str) -> Result<SqliteQueryResult, sql
         filename,
         &mimetype,
         expiry,
-        &adminkey,
+        masterkey,
         accessed.as_secs() as i32,
         filesize,
         "127.0.0.1",
@@ -325,3 +360,38 @@ pub async fn add_undeleted_file(filename: &str) -> Result<SqliteQueryResult, sql
     )
     .await
 }
+
+/// This function tries it's best at guessing if the user is a web browser or a API Request.
+/// We do this buy checking if the form data exists from the upload request, but failing that, we check if the user agent is a browser.
+/// If it is, we return true, otherwise false.
+/// TODO: In the future, we'll move the api to a /api/... endpoint, and we won't need to check at all.
+pub async fn is_browser(req: &mut Request) -> bool {
+    // Unpack the form data, if it exists.
+    let form_data = req.form_data().await;
+
+    // We only need to check if the form exists for uploads, so this goes first.
+    if let Ok(data) = form_data {
+        // Form data exists, so this is at least a file upload
+        // Check the form data itself for the "source" field.
+        if data.fields.get("source").is_some() {
+            debug!("Source detected from form, user is a web browser!");
+            // source exists, so we know this is a web browser uploading a file.
+            return true;
+        }
+    }
+
+    // Check based on user-agent "Mozilla". Sorry to the obscure-ass web browsers I guess.
+    if let Some(agent) = req.headers().get("user-agent") {
+        if agent.to_str().unwrap_or("").contains("Mozilla") {
+            debug!("User Agent says this is likely a web browser!");
+            return true;
+        } else {
+            // We couldn't find "Mozilla" in the user-agent, so we'll assume it's an API user.
+            debug!("User Agent says this is likely a API Request!");
+            return false;
+        }
+    }
+    // If somehow, there is NO user agent at all, just assume it's some idiot who removed it.
+    debug!("We got nothing, assuming web browser!");
+    true
+}
diff --git a/src/handlers/mod_tests.rs b/src/handlers/mod_tests.rs
index 4e571ca230a6e8033d3c89b9d89870e2c68ae806..a3293850878fa8ed623d03cf1a80d0a352133743 100644
--- a/src/handlers/mod_tests.rs
+++ b/src/handlers/mod_tests.rs
@@ -123,47 +123,40 @@ mod tests {
         // filetype
         let small_png_data: &[u8] = &[
             // a random screenshot lol
-			0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
-			0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x20,
-			0x08, 0x06, 0x00, 0x00, 0x00, 0x82, 0x71, 0x21, 0x79, 0x00, 0x00, 0x00,
-			0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00,
-			0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc,
-			0x61, 0x05, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00,
-			0x0e, 0xc3, 0x00, 0x00, 0x0e, 0xc3, 0x01, 0xc7, 0x6f, 0xa8, 0x64, 0x00,
-			0x00, 0x01, 0x23, 0x49, 0x44, 0x41, 0x54, 0x58, 0x47, 0xed, 0x97, 0xb1,
-			0xad, 0x84, 0x30, 0x0c, 0x86, 0x33, 0x00, 0x4b, 0xb0, 0x03, 0x2b, 0xb0,
-			0x01, 0x0b, 0xd0, 0x43, 0x4f, 0x4f, 0x49, 0x4b, 0x4b, 0x47, 0x4d, 0x4b,
-			0x4f, 0xcd, 0x4e, 0x39, 0x7d, 0x91, 0x8c, 0xa2, 0x28, 0xb9, 0x3b, 0xbd,
-			0xc6, 0xef, 0x24, 0x17, 0x9f, 0x50, 0xfc, 0xc7, 0xce, 0x1f, 0xc7, 0x0d,
-			0xae, 0xaa, 0x2a, 0xff, 0xab, 0x98, 0x79, 0x2d, 0xcc, 0xbc, 0x16, 0x6e,
-			0x18, 0x06, 0xff, 0xab, 0x98, 0x79, 0x2d, 0xcc, 0xbc, 0x16, 0x66, 0x5e,
-			0x0b, 0x33, 0xaf, 0x85, 0x99, 0xd7, 0xc2, 0xcc, 0x6b, 0x61, 0xe6, 0xb5,
-			0x30, 0xf3, 0x5a, 0xd8, 0x6f, 0xa0, 0x16, 0x6a, 0xe6, 0xf7, 0x7d, 0xf7,
-			0xf7, 0x7d, 0x87, 0x6f, 0xdb, 0xb6, 0xd9, 0x3d, 0x9f, 0x50, 0x33, 0x8f,
-			0xe1, 0xbe, 0xef, 0xfd, 0x71, 0x1c, 0xfe, 0xba, 0xae, 0x3f, 0x5d, 0x40,
-			0x7d, 0x6c, 0xea, 0xba, 0xf6, 0xe7, 0x79, 0x86, 0x17, 0xc8, 0xe9, 0xef,
-			0xf8, 0x17, 0x33, 0x3f, 0x4d, 0x53, 0x18, 0x21, 0x2e, 0x92, 0xd3, 0x4b,
-			0x38, 0x92, 0x62, 0x44, 0xa0, 0xd0, 0xba, 0xae, 0x4f, 0x3c, 0x9d, 0x4d,
-			0xd6, 0xcb, 0xb2, 0x3c, 0xb3, 0x4b, 0xf7, 0xba, 0xae, 0xfb, 0x3a, 0x3f,
-			0x86, 0x38, 0x7b, 0xe2, 0x7c, 0x81, 0x3c, 0x6a, 0xa7, 0x71, 0x70, 0x24,
-			0x30, 0x7b, 0x24, 0x6f, 0xdb, 0xf6, 0x08, 0x1c, 0xcc, 0x2c, 0x8e, 0xe3,
-			0x18, 0x74, 0x0a, 0x30, 0x9f, 0xa2, 0x53, 0x54, 0x74, 0x6a, 0xa0, 0xc5,
-			0x87, 0x88, 0x4e, 0x6e, 0x4e, 0x4f, 0xe1, 0x7c, 0x6a, 0xa5, 0xf1, 0xb7,
-			0xe6, 0xe9, 0x10, 0x85, 0x21, 0x7e, 0x36, 0x8a, 0xd1, 0x59, 0x59, 0x53,
-			0x98, 0x98, 0x74, 0x8f, 0xa2, 0x5c, 0x50, 0xf4, 0x79, 0x9e, 0x83, 0x2e,
-			0xeb, 0xd4, 0x8c, 0x74, 0xb7, 0xd4, 0x7d, 0x34, 0xc6, 0x27, 0xa7, 0x95,
-			0x70, 0x39, 0xe3, 0x40, 0x31, 0x0c, 0xc9, 0x9a, 0xee, 0x11, 0x93, 0xa7,
-			0xc5, 0x7c, 0xac, 0xc7, 0xe6, 0x65, 0x6f, 0x0e, 0xc9, 0x8f, 0x69, 0x9a,
-			0xa6, 0xa8, 0x95, 0xa9, 0xfc, 0x0b, 0x8b, 0x4a, 0xde, 0xf6, 0xdf, 0xf9,
-			0xf2, 0xac, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42,
-		];
-		let result = detect_mime_type(small_png_data);
-	        println!("Test case 3: small_png: {:?}", result);
-	        assert!(result.is_ok());
-	        assert_eq!(
-	            result.unwrap(),
-	            "image/pngf"
-	        );
+            0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48,
+            0x44, 0x52, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x20, 0x08, 0x06, 0x00, 0x00,
+            0x00, 0x82, 0x71, 0x21, 0x79, 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00,
+            0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00,
+            0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73,
+            0x00, 0x00, 0x0e, 0xc3, 0x00, 0x00, 0x0e, 0xc3, 0x01, 0xc7, 0x6f, 0xa8, 0x64, 0x00,
+            0x00, 0x01, 0x23, 0x49, 0x44, 0x41, 0x54, 0x58, 0x47, 0xed, 0x97, 0xb1, 0xad, 0x84,
+            0x30, 0x0c, 0x86, 0x33, 0x00, 0x4b, 0xb0, 0x03, 0x2b, 0xb0, 0x01, 0x0b, 0xd0, 0x43,
+            0x4f, 0x4f, 0x49, 0x4b, 0x4b, 0x47, 0x4d, 0x4b, 0x4f, 0xcd, 0x4e, 0x39, 0x7d, 0x91,
+            0x8c, 0xa2, 0x28, 0xb9, 0x3b, 0xbd, 0xc6, 0xef, 0x24, 0x17, 0x9f, 0x50, 0xfc, 0xc7,
+            0xce, 0x1f, 0xc7, 0x0d, 0xae, 0xaa, 0x2a, 0xff, 0xab, 0x98, 0x79, 0x2d, 0xcc, 0xbc,
+            0x16, 0x6e, 0x18, 0x06, 0xff, 0xab, 0x98, 0x79, 0x2d, 0xcc, 0xbc, 0x16, 0x66, 0x5e,
+            0x0b, 0x33, 0xaf, 0x85, 0x99, 0xd7, 0xc2, 0xcc, 0x6b, 0x61, 0xe6, 0xb5, 0x30, 0xf3,
+            0x5a, 0xd8, 0x6f, 0xa0, 0x16, 0x6a, 0xe6, 0xf7, 0x7d, 0xf7, 0xf7, 0x7d, 0x87, 0x6f,
+            0xdb, 0xb6, 0xd9, 0x3d, 0x9f, 0x50, 0x33, 0x8f, 0xe1, 0xbe, 0xef, 0xfd, 0x71, 0x1c,
+            0xfe, 0xba, 0xae, 0x3f, 0x5d, 0x40, 0x7d, 0x6c, 0xea, 0xba, 0xf6, 0xe7, 0x79, 0x86,
+            0x17, 0xc8, 0xe9, 0xef, 0xf8, 0x17, 0x33, 0x3f, 0x4d, 0x53, 0x18, 0x21, 0x2e, 0x92,
+            0xd3, 0x4b, 0x38, 0x92, 0x62, 0x44, 0xa0, 0xd0, 0xba, 0xae, 0x4f, 0x3c, 0x9d, 0x4d,
+            0xd6, 0xcb, 0xb2, 0x3c, 0xb3, 0x4b, 0xf7, 0xba, 0xae, 0xfb, 0x3a, 0x3f, 0x86, 0x38,
+            0x7b, 0xe2, 0x7c, 0x81, 0x3c, 0x6a, 0xa7, 0x71, 0x70, 0x24, 0x30, 0x7b, 0x24, 0x6f,
+            0xdb, 0xf6, 0x08, 0x1c, 0xcc, 0x2c, 0x8e, 0xe3, 0x18, 0x74, 0x0a, 0x30, 0x9f, 0xa2,
+            0x53, 0x54, 0x74, 0x6a, 0xa0, 0xc5, 0x87, 0x88, 0x4e, 0x6e, 0x4e, 0x4f, 0xe1, 0x7c,
+            0x6a, 0xa5, 0xf1, 0xb7, 0xe6, 0xe9, 0x10, 0x85, 0x21, 0x7e, 0x36, 0x8a, 0xd1, 0x59,
+            0x59, 0x53, 0x98, 0x98, 0x74, 0x8f, 0xa2, 0x5c, 0x50, 0xf4, 0x79, 0x9e, 0x83, 0x2e,
+            0xeb, 0xd4, 0x8c, 0x74, 0xb7, 0xd4, 0x7d, 0x34, 0xc6, 0x27, 0xa7, 0x95, 0x70, 0x39,
+            0xe3, 0x40, 0x31, 0x0c, 0xc9, 0x9a, 0xee, 0x11, 0x93, 0xa7, 0xc5, 0x7c, 0xac, 0xc7,
+            0xe6, 0x65, 0x6f, 0x0e, 0xc9, 0x8f, 0x69, 0x9a, 0xa6, 0xa8, 0x95, 0xa9, 0xfc, 0x0b,
+            0x8b, 0x4a, 0xde, 0xf6, 0xdf, 0xf9, 0xf2, 0xac, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45,
+            0x4e, 0x44, 0xae, 0x42,
+        ];
+        let result = detect_mime_type(small_png_data);
+        println!("Test case 3: small_png: {:?}", result);
+        assert!(result.is_ok());
+        assert_eq!(result.unwrap(), "image/png");
 
         // Test case 4: putty.exe
         let file_data: &[u8] = &[
diff --git a/src/handlers/serve_file.rs b/src/handlers/serve_file.rs
index a86c15469510ab36a15024a3f9168984a4795922..072bce56ec0189753b3174bb3d57f79437d55fb8 100644
--- a/src/handlers/serve_file.rs
+++ b/src/handlers/serve_file.rs
@@ -7,19 +7,21 @@ use salvo::{
         HeaderMap,
     },
     prelude::StatusCode,
-    writer::Text,
+    writing::Text,
     Request, Response,
 };
 use std::time::SystemTime;
-use tracing::{debug, error, info};
+use tracing::{debug, error, info, warn};
 
 use crate::{
     db::{self, FileCheckResult},
     engine,
-    handlers::{add_undeleted_file, check_file_exists, render_template, TemplateStruct},
+    handlers::{add_undeleted_file, check_file_exists, render_template},
     CONFIG, SQLITE,
 };
 
+use super::{Generic, TemplateEnum};
+
 /// Render the file.
 /// This function converts any unsafe mimetypes to text/plain, and should /always/ nicely render an error if something goes wrong.
 pub async fn render_file(req: &mut Request, res: &mut Response, headers: HeaderMap) {
@@ -28,10 +30,13 @@ pub async fn render_file(req: &mut Request, res: &mut Response, headers: HeaderM
         .param("file")
         .expect("Failed to set the filename variable");
 
+    let remote_addr = &req.remote_addr().clone();
+
     info!(
-        "[{}]File View: {:?}",
-        &headers[HOST].to_str().unwrap(),
-        &filename.to_string()
+        "[{:?} - {:?}] New View on file '{}'",
+        headers[HOST].to_str().unwrap(),
+        remote_addr,
+        filename
     );
 
     // Get a new sql pool thingy
@@ -51,7 +56,12 @@ pub async fn render_file(req: &mut Request, res: &mut Response, headers: HeaderM
         .any(|mime| mime.clone() == mimetype.clone())
     {
         // Check if any of the unsafe mime types match the given mimetype
-        info!("Unsafe Extension Filtered: {:?}", mimetype);
+        info!(
+            "[{:?} - {:?}] Setting mimetype of '{}' to safe fallback (text/plain)",
+            headers[HOST].to_str().unwrap(),
+            remote_addr,
+            filename
+        );
         mimetype = String::from("text/plain"); // If there is a match, change the mimetype to text/plain
     }
 
@@ -123,14 +133,14 @@ pub async fn render_404(res: &mut Response, upload_by_web: bool, headers: Header
             // Render a nice 404 in html
             let template_filename = "error.html";
 
-            let template = TemplateStruct {
+            let template = TemplateEnum::Generic(Generic {
                 domain: String::from(headers[HOST].to_str().unwrap()),
                 message1: String::from("Error 404: File not Found"),
                 message2: String::from(
                     "We couldn't find that. Are you sure you know what you're looking for?",
                 ),
                 ..Default::default()
-            };
+            });
 
             let status_code = StatusCode::NOT_FOUND;
             render_template(res, &headers, template_filename, template, status_code).await;
@@ -147,6 +157,7 @@ pub async fn render_404(res: &mut Response, upload_by_web: bool, headers: Header
 #[handler]
 pub async fn serve_file(req: &mut Request, res: &mut Response) {
     let headers = req.headers().clone();
+    let remote_addr = &req.remote_addr().clone();
     let sqlconn = SQLITE.get().expect("Failed to grab sqlite pool");
 
     // Check if the filename exists in the DB
@@ -172,19 +183,22 @@ pub async fn serve_file(req: &mut Request, res: &mut Response) {
             match check_file_exists(&filename, "files/").await {
                 // File does exist in the filesystem!
                 true => {
-                    info!(
-                        "Adding previously unknown file to the database: {}",
-                        &filename
-                    );
+                    warn!("[{:?} - {:?}] Existing file '{}' missing from database. Attempting to re-add.", headers[HOST].to_str().unwrap(), remote_addr, filename);
+
                     // Add the file to the database
                     match add_undeleted_file(&filename).await {
                         Ok(_) => {
-                            info!("Successfully added {} back into the database", &filename);
+                            info!(
+                                "[{:?} - {:?}] Missing file '{}' re-added correctly.",
+                                headers[HOST].to_str().unwrap(),
+                                remote_addr,
+                                filename
+                            );
                             // Now we can render the file!
                             render_file(req, res, headers).await
                         }
-                        Err(_) => {
-                            error!("Error while adding {} back into the database", &filename);
+                        Err(e) => {
+                            error!("[{:?} - {:?}] Error while adding file '{}' back into the database: {}", headers[HOST].to_str().unwrap(), remote_addr, filename, e);
                             // TODO: Refactor a nice error rendering function, were we just pass the errorcode, and message
                             render_404(res, upload_by_web, headers).await
                         }
@@ -200,19 +214,25 @@ pub async fn serve_file(req: &mut Request, res: &mut Response) {
 
         // Error while finding file in the database
         Err(err) => {
-            error!("Error while trying to check the filename: {:?}", err);
+            error!(
+                "[{:?} - {:?}] Error while finding file '{}' in the database: {}",
+                headers[HOST].to_str().unwrap(),
+                remote_addr,
+                filename,
+                err
+            );
 
             if upload_by_web {
                 let template_filename = "error.html";
 
-                let template = TemplateStruct {
+                let template = TemplateEnum::Generic(Generic {
                     domain: String::from(headers[HOST].to_str().unwrap()),
                     message1: String::from("Error 500: Internal Server Error"),
                     message2: String::from(
                         "We couldn't find the file specified, even though it should exist.",
                     ),
                     ..Default::default()
-                };
+                });
 
                 let status_code = StatusCode::INTERNAL_SERVER_ERROR;
                 render_template(res, &headers, template_filename, template, status_code).await;
diff --git a/src/handlers/serve_metrics.rs b/src/handlers/serve_metrics.rs
index 4a2d5e8208b5ad88d6e5fae2661f796cbe3235ea..4e991db32f48ebd5c2bee78682ffb7272b10fe86 100644
--- a/src/handlers/serve_metrics.rs
+++ b/src/handlers/serve_metrics.rs
@@ -1,5 +1,5 @@
 use core::fmt;
-use salvo::{handler, writer::Text, Response};
+use salvo::{handler, writing::Text, Response};
 use std::time::Instant;
 
 use crate::{db, SQLITE};
@@ -158,8 +158,8 @@ pub async fn serve_metrics(res: &mut Response) {
     ////
     // QR Scanning Stats
     ////
-
     // TODO
+
     // Add how long it took to get all of those metrics to the page!
     let end = Instant::now();
     rendered = format!(
diff --git a/src/handlers/serve_static.rs b/src/handlers/serve_static.rs
index 6704d09ce334449f22f33012b598d2667a2d972d..cf05f24c42fe0a19e82a9412579f72e3a928a351 100644
--- a/src/handlers/serve_static.rs
+++ b/src/handlers/serve_static.rs
@@ -1,17 +1,20 @@
 use ramhorns::Ramhorns;
-use rand::Rng;
 use salvo::{
     handler,
     hyper::header::{HOST, USER_AGENT},
     prelude::StatusCode,
-    writer::Text,
+    writing::Text,
     Request, Response,
 };
 use std::path::PathBuf;
-use tracing::{error, info};
+use tracing::{error, info, warn};
 
 use super::guess_ip;
-use crate::{db, handlers::TemplateStruct, SQLITE};
+use crate::{
+    db,
+    handlers::{Generic, TemplateEnum},
+    SQLITE,
+};
 
 /// This handles the requests for all static files
 #[handler]
@@ -24,99 +27,169 @@ pub async fn serve_static(req: &mut Request, res: &mut Response) {
     let ip = guess_ip(headers, remote_addr);
     let useragent = headers[USER_AGENT].to_str().unwrap();
 
-    // I imagine I could initialise a mutable string here, and have the match add the templating stuff
-    // Then, pass it over to a single res.set_status_code and res.render.
-    // Although I'm not sure how much it would actually improve things, maybe just for readability?
+    // Create the shared template from the template folder.
+    let tpls: Ramhorns = Ramhorns::from_folder(template_host_path).unwrap_or(
+        Ramhorns::from_folder(template_host_path_default).expect("Couldn't find default template"),
+    );
+
+    // Render each static endpoint.
     match req.uri().path() {
         "/services" => {
-            info!("[{}]New Request: /services", &host);
-            let tpls: Ramhorns = Ramhorns::from_folder(template_host_path).unwrap_or(
-                Ramhorns::from_folder(template_host_path_default)
-                    .expect("Couldn't find default template"),
+            info!(
+                "[{:?} - {:?}] New Request: /services",
+                headers[HOST].to_str().unwrap(),
+                remote_addr
             );
-            let template = TemplateStruct {
+
+            // Create the template from the generic.
+            let template = TemplateEnum::Generic(Generic {
                 domain: String::from(headers[HOST].to_str().unwrap()),
                 ..Default::default()
+            });
+
+            // This is annoying, but since the template can be either of these, we need to check it.
+            let rendered = match template {
+                TemplateEnum::Generic(ref data) => tpls.get("services.html").unwrap().render(data),
+                // And you may ask yourself: "Well, how did I get here?!"
+                _ => {
+                    panic!("Cosmic ray detected: Well, how did I get here?!")
+                }
             };
-            let rendered = tpls.get("services.html").unwrap().render(&template);
+
             res.status_code(StatusCode::OK);
             res.render(Text::Html(rendered));
         }
         "/about" => {
-            info!("[{}]New Request: /about", &host);
-            let tpls: Ramhorns = Ramhorns::from_folder(template_host_path).unwrap_or(
-                Ramhorns::from_folder(template_host_path_default)
-                    .expect("Couldn't find default template"),
+            info!(
+                "[{:?} - {:?}] New Request: /about",
+                headers[HOST].to_str().unwrap(),
+                remote_addr
             );
-            let template = TemplateStruct {
+
+            // Create the template from the generic.
+            let template = TemplateEnum::Generic(Generic {
                 domain: String::from(headers[HOST].to_str().unwrap()),
                 ..Default::default()
+            });
+
+            // This is annoying, but since the template can be either of these, we need to check it.
+            let rendered = match template {
+                TemplateEnum::Generic(ref data) => tpls.get("about.html").unwrap().render(data),
+                // And you may ask yourself: "Well, how did I get here?!"
+                _ => {
+                    panic!("Cosmic ray detected: Well, how did I get here?!")
+                }
             };
-            let rendered = tpls.get("about.html").unwrap().render(&template);
+
             res.status_code(StatusCode::OK);
             res.render(Text::Html(rendered));
         }
         "/faq" => {
-            info!("[{}]New Request: /faq", &host);
-            let tpls: Ramhorns = Ramhorns::from_folder(template_host_path).unwrap_or(
-                Ramhorns::from_folder(template_host_path_default)
-                    .expect("Couldn't find default template"),
+            info!(
+                "[{:?} - {:?}] New Request: /faq",
+                headers[HOST].to_str().unwrap(),
+                remote_addr
             );
-            let template = TemplateStruct {
+
+            // Create the template from the generic.
+            let template = TemplateEnum::Generic(Generic {
                 domain: String::from(headers[HOST].to_str().unwrap()),
                 ..Default::default()
+            });
+
+            // This is annoying, but since the template can be either of these, we need to check it.
+            let rendered = match template {
+                TemplateEnum::Generic(ref data) => tpls.get("faq.html").unwrap().render(data),
+                // And you may ask yourself: "Well, how did I get here?!"
+                _ => {
+                    panic!("Cosmic ray detected: Well, how did I get here?!")
+                }
             };
-            let rendered = tpls.get("faq.html").unwrap().render(&template);
+
             res.status_code(StatusCode::OK);
             res.render(Text::Html(rendered));
         }
         "/dmca" => {
-            info!("[{}]New Request: /dmca", &host);
-            let tpls: Ramhorns = Ramhorns::from_folder(template_host_path).unwrap_or(
-                Ramhorns::from_folder(template_host_path_default)
-                    .expect("Couldn't find default template"),
+            info!(
+                "[{:?} - {:?}] New Request: /dmca",
+                headers[HOST].to_str().unwrap(),
+                remote_addr
             );
-            let template = TemplateStruct {
+
+            // Create the template from the generic.
+            let template = TemplateEnum::Generic(Generic {
                 domain: String::from(headers[HOST].to_str().unwrap()),
-                message1: rand::thread_rng().gen_range(0..1).to_string(),
                 ..Default::default()
+            });
+
+            // This is annoying, but since the template can be either of these, we need to check it.
+            let rendered = match template {
+                TemplateEnum::Generic(ref data) => tpls.get("dmca.html").unwrap().render(data),
+                // And you may ask yourself: "Well, how did I get here?!"
+                _ => {
+                    panic!("Cosmic ray detected: Well, how did I get here?!")
+                }
             };
-            let rendered = tpls.get("dmca.html").unwrap().render(&template);
+
             res.status_code(StatusCode::OK);
             res.render(Text::Html(rendered));
         }
         "/welcome" => {
-            info!("[{}]New Request: /welcome", &host);
-            let tpls: Ramhorns = Ramhorns::from_folder(template_host_path).unwrap_or(
-                Ramhorns::from_folder(template_host_path_default)
-                    .expect("Couldn't find default template"),
+            info!(
+                "[{:?} - {:?}] New Request: /welcome",
+                headers[HOST].to_str().unwrap(),
+                remote_addr
             );
-            let template = TemplateStruct {
+
+            // Create the template from the generic.
+            let template = TemplateEnum::Generic(Generic {
                 domain: String::from(headers[HOST].to_str().unwrap()),
-                message1: rand::thread_rng().gen_range(0..1).to_string(),
                 ..Default::default()
+            });
+
+            // This is annoying, but since the template can be either of these, we need to check it.
+            let rendered = match template {
+                TemplateEnum::Generic(ref data) => tpls.get("welcome.html").unwrap().render(data),
+                // And you may ask yourself: "Well, how did I get here?!"
+                _ => {
+                    panic!("Cosmic ray detected: Well, how did I get here?!")
+                }
             };
-            let rendered = tpls.get("welcome.html").unwrap().render(&template);
+
             res.status_code(StatusCode::OK);
             res.render(Text::Html(rendered));
         }
         "/czb" => {
-            info!("[{}]New Request: /czb", &host);
-            let tpls: Ramhorns = Ramhorns::from_folder(template_host_path).unwrap_or(
-                Ramhorns::from_folder(template_host_path_default)
-                    .expect("Couldn't find default template"),
+            info!(
+                "[{:?} - {:?}] New Request: /czb",
+                headers[HOST].to_str().unwrap(),
+                remote_addr
             );
-            let template = TemplateStruct {
+
+            // Create the template from the generic.
+            let template = TemplateEnum::Generic(Generic {
                 domain: String::from(headers[HOST].to_str().unwrap()),
-                message1: rand::thread_rng().gen_range(0..1).to_string(),
                 ..Default::default()
+            });
+
+            // This is annoying, but since the template can be either of these, we need to check it.
+            let rendered = match template {
+                TemplateEnum::Generic(ref data) => tpls.get("czb.html").unwrap().render(data),
+                // And you may ask yourself: "Well, how did I get here?!"
+                _ => {
+                    panic!("Cosmic ray detected: Well, how did I get here?!")
+                }
             };
-            let rendered = tpls.get("czb.html").unwrap().render(&template);
+
             res.status_code(StatusCode::OK);
             res.render(Text::Html(rendered));
         }
         "/qr" => {
-            info!("[{}]New Request: /qr", &host);
+            info!(
+                "[{:?} - {:?}] New Request: /qr",
+                headers[HOST].to_str().unwrap(),
+                remote_addr
+            );
             // Setup the Sqlite pool stuff for later
             let sqlconn = SQLITE.get().unwrap();
             // Get the headers (for Host header stuff thats needed later)
@@ -126,24 +199,38 @@ pub async fn serve_static(req: &mut Request, res: &mut Response) {
                 .await
                 .is_err()
             {
-                error!("Failed to add QR Scan to the database.");
+                error!(
+                    "[{:?} - {:?}] Error when adding QR Code scan to the database",
+                    headers[HOST].to_str().unwrap(),
+                    remote_addr
+                );
             }
 
-            let tpls: Ramhorns = Ramhorns::from_folder(template_host_path).unwrap_or(
-                Ramhorns::from_folder(template_host_path_default)
-                    .expect("Couldn't find default template"),
-            );
-            let template = TemplateStruct {
+            // Create the template from the generic.
+            let template = TemplateEnum::Generic(Generic {
                 domain: String::from(headers[HOST].to_str().unwrap()),
-                message1: rand::thread_rng().gen_range(0..1).to_string(),
                 ..Default::default()
+            });
+
+            // This is annoying, but since the template can be either of these, we need to check it.
+            let rendered = match template {
+                TemplateEnum::Generic(ref data) => tpls.get("qr.html").unwrap().render(data),
+                // And you may ask yourself: "Well, how did I get here?!"
+                _ => {
+                    panic!("Cosmic ray detected: Well, how did I get here?!")
+                }
             };
-            let rendered = tpls.get("qr.html").expect("Failed to read qr.html").render(&template);
+
             res.status_code(StatusCode::OK);
             res.render(Text::Html(rendered));
         }
-        _ => {
-            info!("Bad Request Received");
+        e => {
+            warn!(
+                "[{:?} - {:?}] Bad Request Received: {}",
+                headers[HOST].to_str().unwrap(),
+                remote_addr,
+                e
+            );
             // TODO: Nice Render page
             res.status_code(StatusCode::BAD_REQUEST);
         }
diff --git a/src/handlers/upload.rs b/src/handlers/upload.rs
index 27432ad3758d2131b891f5163c9441cce64d194b..817279710802093f732c33daa34fb130a36e3a95 100644
--- a/src/handlers/upload.rs
+++ b/src/handlers/upload.rs
@@ -1,22 +1,33 @@
 use chrono::{TimeZone, Utc};
-use salvo::{handler, hyper::header::HOST, prelude::StatusCode, writer::Text, Request, Response};
+use salvo::{
+    hyper::header::HOST, oapi::endpoint, prelude::StatusCode, writing::Text, Depot, Request,
+    Response,
+};
 use std::{
     path::{Path, PathBuf},
     time::SystemTime,
 };
-use tokio::{fs::{self, File}, io::AsyncReadExt};
+use tokio::{
+    fs::{self, File},
+    io::AsyncReadExt,
+};
 use tracing::{debug, error, info, trace};
 
 use super::guess_ip;
 use crate::{
     db, engine,
-    handlers::{detect_mime_type, is_mimetype_banned, render_template, TemplateStruct},
+    handlers::{
+        detect_mime_type, is_browser, is_mimetype_banned, render_template, Generic, TemplateEnum,
+    },
+    keys::Key,
     CONFIG, SQLITE,
 };
 
-/// This file handles (heh) uploading files to the server.
-#[handler]
-pub async fn upload(req: &mut Request, res: &mut Response) {
+/// This endpoint handles file uploading.
+///
+/// This does a lot of checks to make sure the user isn't breaking any rules.
+#[endpoint(tags("file-management"))]
+pub async fn upload(req: &mut Request, res: &mut Response, depot: &mut Depot) {
     // Setup the Sqlite pool stuff for later
     let sqlconn = SQLITE.get().unwrap();
     // Clone out the header and remote_addr so we can guess the ip later
@@ -32,14 +43,14 @@ pub async fn upload(req: &mut Request, res: &mut Response) {
         https = "https".to_string();
     }
 
-    // This is ugly, but this grabs the invisible form data that determines if the file was uploaded via the web.
-    let upload_by_web = req
-        .form_data()
-        .await
-        .unwrap()
-        .fields
-        .get("source")
-        .is_some();
+    // Try to figure out if the user is on a web browser.
+    let is_user_web = is_browser(req).await;
+
+    // Grab the key from the auth handler.
+    let key = depot
+        .get::<Key>("client_key")
+        .expect("Failed to grab API Key from depot")
+        .clone();
 
     // This is the start of the file upload process
     // This handles both the web and json responses
@@ -56,175 +67,177 @@ pub async fn upload(req: &mut Request, res: &mut Response) {
         let mut buffer = Vec::with_capacity(512);
 
         // Load up 512 bytes (This breaks with a few filetypes, but meh. https://en.wikipedia.org/wiki/List_of_file_signatures)
-        let n = mime_file.take(512).read_to_end(&mut buffer).await.expect("Error reading file");
-
-        // mime_file
-        //     .take(512)
-        //     .read_to_end(&mut buffer);
-
+        let _n = mime_file
+            .take(512)
+            .read_to_end(&mut buffer)
+            .await
+            .expect("Error reading file");
         trace!("read bytes for mime parsing: {:x?}", buffer);
 
         // Guess the mimetype
         let mimetype: &str = &detect_mime_type(&buffer).unwrap_or("text/plain".to_string());
-
         debug!("upload(mimetype): {:?}", mimetype);
 
-        // Check if the mimetype is banned, and render the error pages.
-        if is_mimetype_banned(mimetype).await {
-            // Check if the upload came from the web
-            if upload_by_web {
-                let template_filename = "error.html";
-                let template = TemplateStruct {
-                    domain: String::from(headers[HOST].to_str().unwrap()),
-                    message1: String::from("Error 403"),
-                    message2: String::from("That filetype is not allowed."),
-                    ..Default::default()
-                };
-
-                let status_code = StatusCode::FORBIDDEN;
-
-                render_template(res, headers, template_filename, template, status_code).await;
-            } else {
-                // Otherwise, render the error in json for the API.
-                res.status_code(StatusCode::FORBIDDEN);
-                res.render(Text::Json(r#"{"error": "BlockedFiletype"}"#));
-            }
-        } else {
-            // Continue on if the file is allowed!
-
-            // Generate the admin key.
-            let adminkey = engine::generate_adminkey(sqlconn).await;
-            debug!("upload(filename, adminkey): {:?}, {:?}", filename, adminkey);
-
-            // Set Destination
-            let dest: PathBuf = PathBuf::from("files/").join(&filename);
-
-            // Copy the file
-            if let Err(e) = std::fs::copy(file.path(), Path::new(&dest)) {
-                error!(
-                    "There was a problem uploading file: {:?}, error: {}",
-                    dest, e
-                );
-                // If web is true, render html, otherwise json.
-                if upload_by_web {
+        // Check mimetypes for normal keys only.
+        // If keys are disabled, the key is initialised with 2 (normal) so it's always checked. (i hope)
+        if key.key_level.as_i32() == 2 {
+            info!(
+                "[{:?} - {:?}] Validating mimetype for file '{}'",
+                headers[HOST].to_str().unwrap(),
+                remote_addr,
+                filename.to_str().unwrap_or("")
+            );
+            // Check if the mimetype is banned, and render the error pages.
+            if is_mimetype_banned(mimetype).await {
+                // Check if the upload came from the web
+                if is_user_web {
                     let template_filename = "error.html";
-
-                    let template = TemplateStruct {
+                    let template = TemplateEnum::Generic(Generic {
                         domain: String::from(headers[HOST].to_str().unwrap()),
-                        message1: String::from("Error 500"),
-                        message2: String::from(
-                            "There was a problem uploading the file, please try again.",
-                        ),
+                        message1: String::from("Error 403"),
+                        message2: String::from("That filetype is not allowed."),
                         ..Default::default()
-                    };
-
-                    let status_code = StatusCode::INTERNAL_SERVER_ERROR;
-                    render_template(res, headers, template_filename, template, status_code).await;
-                } else {
-                    res.status_code(StatusCode::INTERNAL_SERVER_ERROR);
-                    res.render(Text::Json(r#"{"error": "InternalServerError"}"#));
-                }
-            } else {
-                // This branch is if the file upload worked properly.
-
-                // Calculate the expiry
-                // file.path is the path to the temp file.
-                // file.size seems to be always 'None', so we'll ask the filesystem.
-                let path = Path::new(file.path());
-                let filesize = fs::metadata(path).await.unwrap().len() as i32;
-                debug!("upload(filesize): {:?}", filesize);
-                let expiry =
-                    engine::calculate_expiry(sqlconn, &filename.to_str().unwrap(), filesize).await;
-                // Convert that expiry to actual time and date (for display)
-                let expiry_dt = Utc.timestamp_opt(expiry as i64, 0).unwrap();
-                debug!("upload(expiry): {:?}", expiry);
-
-                // Determine what ip type it is.
-                debug!("upload(headers): {:?}", headers);
-
-                let ip = guess_ip(headers, remote_addr);
-                debug!("upload(ip): {:?}", ip);
-
-                // Get the current time
-                let accessed = SystemTime::now()
-                    .duration_since(SystemTime::UNIX_EPOCH)
-                    .unwrap();
-                debug!("upload(accessed): {:?}", accessed);
-
-                // Add file to the database, or print an error.
-                if let Err(e) = db::add_file(
-                    sqlconn,
-                    filename.as_os_str().to_str().unwrap(), // wow
-                    mimetype,
-                    expiry,
-                    // expiry_override,
-                    &adminkey,
-                    accessed.as_secs() as i32,
-                    filesize,
-                    ip.as_str(),
-                    host,
-                )
-                .await
-                {
-                    error!("File upload error: {:?}", e);
-                }
-
-                info!("[{}]File uploaded to {:?}", &host, dest);
+                    });
 
-                let fileurl = format!(
-                    "{}://{}/{}",
-                    https,
-                    String::from(headers[HOST].to_str().unwrap()),
-                    filename.to_string_lossy()
-                );
+                    let status_code = StatusCode::FORBIDDEN;
 
-                let adminurl = format!(
-                    "{}://{}/delete/{}",
-                    https,
-                    String::from(headers[HOST].to_str().unwrap()),
-                    &adminkey
-                );
-
-                if upload_by_web {
-                    let template_filename = "link.html";
-
-                    let template = TemplateStruct {
-                        domain: String::from(headers[HOST].to_str().unwrap()),
-                        fileurl: String::from(&fileurl),
-                        adminurl: String::from(&adminurl),
-                        message1: expiry_dt.to_string(),
-                        message2: String::from(""),
-                    };
-
-                    let status_code = StatusCode::OK;
                     render_template(res, headers, template_filename, template, status_code).await;
+                    return;
                 } else {
-                    res.status_code(StatusCode::OK);
-
-                    res.render(Text::Json(format!(
-                        r#"{{"file": "{}", "url": "{}", "adminurl": "{}", "expiry": {}}}"#,
-                        &filename.to_string_lossy().to_string(),
-                        fileurl,
-                        adminurl,
-                        expiry,
-                    )));
+                    // Otherwise, render the error in json for the API.
+                    res.status_code(StatusCode::FORBIDDEN);
+                    res.render(Text::Json(r#"{"error": "BlockedMimetype"}"#));
+                    return;
                 }
-            };
+            }
+        } else {
+            info!(
+                "[{:?} - {:?}] Bypassing Mimetype check for non-Normal-ranked user.",
+                headers[HOST].to_str().unwrap(),
+                remote_addr
+            );
         }
-    } else if upload_by_web {
-        let template_filename = "error.html";
 
-        let template = TemplateStruct {
+        // Set Destination path.
+        let dest: PathBuf = PathBuf::from("files/").join(&filename);
+        // Copy the file
+        if let Err(e) = std::fs::copy(file.path(), Path::new(&dest)) {
+            // ... but if there was an error, complain about it.
+            error!(
+                "There was a problem uploading file: {:?}, error: {}",
+                dest, e
+            );
+            // If web is true, render html, otherwise json.
+            if is_user_web {
+                let template_filename = "error.html";
+                let template = TemplateEnum::Generic(Generic {
+                    domain: String::from(headers[HOST].to_str().unwrap()),
+                    message1: String::from("Error 500"),
+                    message2: String::from(
+                        "There was a problem uploading the file, please try again.",
+                    ),
+                    ..Default::default()
+                });
+                let status_code = StatusCode::INTERNAL_SERVER_ERROR;
+                render_template(res, headers, template_filename, template, status_code).await;
+                return;
+            } else {
+                res.status_code(StatusCode::INTERNAL_SERVER_ERROR);
+                res.render(Text::Json(r#"{"error": "FileUploadServerError"}"#));
+                return;
+            }
+        } else {
+            // This branch is if the file upload worked properly. \o/
+
+            // Calculate the expiry
+            // file.path is the path to the temp file.
+            // file.size seems to be always 'None', so we'll ask the filesystem.
+            let path = Path::new(file.path());
+            let filesize = fs::metadata(path).await.unwrap().len() as i32;
+            debug!("upload(filesize): {:?}", filesize);
+            let expiry =
+                engine::calculate_expiry(sqlconn, &filename.to_str().unwrap(), filesize).await;
+            // Convert that expiry to actual time and date (for display)
+            let expiry_dt = Utc.timestamp_opt(expiry as i64, 0).unwrap();
+            debug!("upload(expiry): {:?}", expiry);
+            // Determine what ip type it is.
+            debug!("upload(headers): {:?}", headers);
+            let ip = guess_ip(headers, remote_addr);
+            debug!("upload(ip): {:?}", ip);
+            // Get the current time
+            let accessed = SystemTime::now()
+                .duration_since(SystemTime::UNIX_EPOCH)
+                .unwrap()
+                .as_secs() as i32;
+            debug!("upload(accessed): {:?}", accessed);
+            // Add file to the database, or print an error.
+            if let Err(e) = db::add_file(
+                sqlconn,
+                filename.as_os_str().to_str().unwrap(), // wow
+                mimetype,
+                expiry,
+                // expiry_override,
+                &key.uuid, // The API Key from the user.
+                accessed,
+                filesize,
+                ip.as_str(),
+                host,
+            )
+            .await
+            {
+                error!("File upload error: {:?}", e);
+            }
+            info!(
+                "[{:?}] New Upload: {:?}, owner: {}, ip: {:?}",
+                headers[HOST].to_str().unwrap(),
+                filename,
+                &key.uuid,
+                remote_addr
+            );
+            let fileurl = format!(
+                "{}://{}/{}",
+                https,
+                String::from(headers[HOST].to_str().unwrap()),
+                filename.to_string_lossy()
+            );
+
+            // Upload is done, lets show the stuff to the user!
+            if is_user_web {
+                let template_filename = "link.html";
+                let template = TemplateEnum::Generic(Generic {
+                    domain: String::from(headers[HOST].to_str().unwrap()),
+                    fileurl: String::from(&fileurl),
+                    adminurl: String::from(""),
+                    message1: expiry_dt.to_string(),
+                    message2: String::from(""),
+                });
+                let status_code = StatusCode::OK;
+                render_template(res, headers, template_filename, template, status_code).await;
+            } else {
+                res.status_code(StatusCode::OK);
+                res.render(Text::Json(format!(
+                    r#"{{"file": "{}", "url": "{}", "expiry": {}}}"#,
+                    &filename.to_string_lossy().to_string(),
+                    fileurl,
+                    expiry,
+                )));
+            }
+        };
+    } else if is_user_web {
+        let template_filename = "error.html";
+        let template = TemplateEnum::Generic(Generic {
             domain: String::from(headers[HOST].to_str().unwrap()),
-            message1: String::from("400"),
-            message2: String::from("There was a problem with the file upload. Please try again."),
+            message1: String::from("Error 400"),
+            message2: String::from("We couldn't read the file from the request."),
             ..Default::default()
-        };
+        });
 
         let status_code = StatusCode::BAD_REQUEST;
         render_template(res, headers, template_filename, template, status_code).await;
+        return;
     } else {
         res.status_code(StatusCode::BAD_REQUEST);
         res.render(Text::Json(r#"{"error": "BadRequest"}"#));
+        return;
     };
 }
diff --git a/src/handlers/user_profile.rs b/src/handlers/user_profile.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ec4f81c4ee48a402ed6fe088a97f415db8504b6e
--- /dev/null
+++ b/src/handlers/user_profile.rs
@@ -0,0 +1,377 @@
+use salvo::Writer;
+use std::str::FromStr;
+
+use chrono::{DateTime, NaiveDateTime, Utc};
+use chrono_humanize::{Accuracy, HumanTime, Tense};
+use salvo::{
+    handler,
+    hyper::header::HOST,
+    oapi::{endpoint, extract::QueryParam},
+    prelude::StatusCode,
+    writing::Text,
+    Depot, Request, Response,
+};
+use tracing::{debug, error, info};
+
+use crate::{
+    db,
+    handlers::{
+        convert_file_size, convert_unix_timestamp, count_file_metrics, render_template, Generic,
+        TemplateEnum,
+    },
+    keys::{
+        db_keys, {self, Key, KeyLevel},
+    },
+    SQLITE,
+};
+
+use super::Profile;
+
+/// This handlers serves the user a nice page for managing uploaded files, generating new keys, and doing any other user actions
+#[handler]
+pub async fn user_profile(req: &mut Request, res: &mut Response, depot: &mut Depot) {
+    let headers = req.headers().clone();
+    // Setup db pool
+    let sqlconn = SQLITE.get().unwrap();
+
+    // Grab the key from the auth handler.
+    let client_key = depot
+        .get::<Key>("client_key")
+        .expect("Failed to grab API Key from depot")
+        .clone();
+
+    // Humanise the creation date of the API Key.
+    let key_creation = {
+        let datetime: DateTime<Utc> = DateTime::from_utc(
+            NaiveDateTime::from_timestamp_opt(client_key.creation_time.into(), 0).unwrap(),
+            Utc,
+        );
+        let human_time = HumanTime::from(datetime);
+        human_time.to_text_en(Accuracy::Rough, Tense::Present)
+    };
+
+    // Key Generation and handling.
+
+    // Get how many keys total, and available to generate.
+    // If these fail, it's 0 out of 0 keys.
+    let available_keys = keys::keys_utilisation(sqlconn, &client_key)
+        .await
+        .unwrap_or(0);
+    let max_keys = keys::max_keys(sqlconn, &client_key).await.unwrap_or(0);
+
+    debug!(
+        "Key: {} has {} of {} key generations left.",
+        client_key.uuid, available_keys, max_keys
+    );
+
+    // Render the key generation bar.
+    let mut key_generator = String::new();
+
+    // Calculate progress bar stuffs.
+    let progress_percentage = (max_keys - available_keys) as f64 / max_keys as f64 * 100.0;
+    let progress_class = if progress_percentage >= 75.0 {
+        "critical"
+    } else if progress_percentage >= 50.0 {
+        "high"
+    } else {
+        "normal"
+    };
+
+    // Add the progress bar
+    key_generator.push_str(&format!(
+        r#"
+        <div class='progress-container'>
+            <span class='progress-text'>{}/{} keys used</span>
+            <div class='progress-bar'>
+                <div class='progress-bar-fill {}' style='width: {}%;'></div>
+            </div>
+        </div>
+        "#,
+        max_keys - available_keys,
+        max_keys,
+        progress_class,
+        progress_percentage
+    ));
+
+    // If there's more than 0 available keys, show the generator for keys.
+    if available_keys > 0 {
+        debug!(
+            "Key {} is allowed to generate keys, showing generator.",
+            client_key.uuid
+        );
+
+        // First half of the generator html.
+        let form_html_start = r#"
+            <br>
+            <form id="keygen-form">
+                <textarea name="comment" id="comment" placeholder="Enter a comment"></textarea>
+        "#;
+        key_generator.push_str(form_html_start);
+
+        // Second half of the generator html.
+        let form_html_end = r#"
+                <div class='keygen-submit'>
+                    <button type="button" onclick="submitForm()">Generate Key</button>
+                </div>
+            </form>
+            <div id="response-message"></div>
+        "#;
+
+        // Only add the dropdown when the user is a master key.
+        if client_key.key_level == KeyLevel::Master {
+            debug!("Key is master.");
+            let key_level_dropdown = r#"
+                <div class="keygen-level">
+                    <select name="keygen-level" id="keygen-level">
+                        <option value="Normal">Normal</option>
+                        <option value="Super">Super</option>
+                    </select>
+                </div>
+            "#;
+            key_generator.push_str(key_level_dropdown);
+        }
+
+        // Append the rest of the form after the optional dropdown.
+        key_generator.push_str(form_html_end);
+    }
+
+    // Get a list of all children keys.
+    let child_uuid = client_key.child_uuids.clone();
+
+    let mut key_list_html = String::new();
+
+    if child_uuid.is_empty() {
+        debug!("No Keys, pushing message");
+        key_list_html.push_str("<p>You don't have any generated Keys....yet.</p>");
+    } else {
+        // Add the table header.
+        debug!("We found some keys, lets add the table!");
+        key_list_html.push_str(
+            r#"
+            <table class="sortable">
+                <thead><tr><th>Key</th><th>Creation Time</th><th>Comment</th></tr></thead>
+                <tbody>
+        "#,
+        );
+        // Loop through all keys, adding them into a table.
+        for child_uuid in child_uuid {
+            debug!("Starting loop for key {}", child_uuid);
+            // Error handle the fetching of child keys
+            match db_keys::get_key(sqlconn, &child_uuid).await {
+                Ok(key) => {
+                    // Humanise the creation_time
+                    let human_creation_time = {
+                        let datetime: DateTime<Utc> = DateTime::from_utc(
+                            NaiveDateTime::from_timestamp_opt(key.creation_time.into(), 0).unwrap(),
+                            Utc,
+                        );
+                        let human_time = HumanTime::from(datetime);
+                        human_time.to_text_en(Accuracy::Rough, Tense::Past)
+                    };
+                    key_list_html.push_str(&format!(
+                        "<tr><td data-sort='{}'>{}</td><td data-sort='{}'>{}</td><td>{}</td></tr>",
+                        key.uuid,
+                        key.uuid,
+                        key.creation_time,
+                        human_creation_time,
+                        key.comment.unwrap_or("".to_string()),
+                    ));
+                }
+                Err(err) => {
+                    error!(
+                        "Failed to fetch child key (UUID: {}): {:?}",
+                        child_uuid, err
+                    );
+                    key_list_html.push_str(&format!(
+                        "<tr><td data-sort='{}'>{}</td><td data-sort='ERR'>Database Error</td><td>Database Error</td></tr>", child_uuid, child_uuid));
+                    continue; // Skip to the next child UUID if an error occurs
+                }
+            };
+        }
+        // Add the table footer. (yeah that's it lol)
+        key_list_html.push_str("</tbody></table>");
+        debug!("key_list: {}", key_list_html);
+    }
+
+    // File Listing and handling.
+    // This shows no matter the key generation part.
+
+    // This returns a Vec of all the files uploaded by the given user key.
+    let files = db::get_files_by_key(sqlconn, &client_key).await;
+
+    // Generate HTML to list all the files in a table.
+    let mut file_list_html = String::new();
+    // For each file in the list, add the html to the rendered string
+    match files.as_ref() {
+        Ok(file_metrics) => {
+            for f in file_metrics {
+                // Change the filename to a file url
+                let fileurl = format!("/{}", &f.filename);
+
+                // Change the filesize to a human readable value
+                let human_size = convert_file_size(f.filesize);
+
+                // Change the expiry to a human readable value
+                let human_time = convert_unix_timestamp(f.expiry);
+
+                // Generate a deletion URL unique to the file.
+                let deletion_url = format!("/api/v1/file/{}", &f.filename);
+
+                // If is_deleted, change colour to red, else green
+                let colour = if f.is_deleted {
+                    "background-color: #ff00000a;".to_string()
+                } else {
+                    "background-color: #00ff100a;".to_string()
+                };
+
+                // Add a new file to the rendered string
+                file_list_html.push_str(&format!(
+                    "<tr style='{}'><td><a href={}>{}</a></td><td data-sort='{}'>{}</td><td>{}</td><td>{}</td><td data-sort='{}'>{}</td><td><button class=\"delete-button\" onclick=\"changeRowColor(this, '{}')\">Delete</button></td></tr>",
+                    colour,
+                    fileurl.as_str(),
+                    f.filename.as_str(),
+                    f.filesize,
+                    human_size.as_str(),
+                    f.mimetype.as_str(),
+                    f.views.to_owned(),
+                    f.expiry.to_owned(),
+                    human_time.to_owned(),
+                    deletion_url.to_owned()
+                ));
+            }
+        }
+        Err(err) => {
+            // Log error to console
+            error!("Error retrieving file metrics: {:?}", err);
+
+            // Render a nice error page for the poor user
+            let template_filename = "error.html";
+            let template = TemplateEnum::Generic(Generic {
+                domain: String::from(headers[HOST].to_str().unwrap_or_default()),
+                message1: String::from("Error 500: Internal Server Error"),
+                message2: String::from(
+                    "Something went wrong while reading the result from the database, uh oh :(",
+                ),
+                ..Generic::default() // Use default to fill in remaining fields
+            });
+
+            let status_code = StatusCode::INTERNAL_SERVER_ERROR;
+            render_template(res, &headers, template_filename, template, status_code).await;
+        }
+    }
+
+    // Close the table when all files are 'rendered'
+    file_list_html.push_str("</table>");
+
+    // Log this to the server console
+    info!(
+        "[{}] New Request: /user_profile ({} Files)",
+        &headers[HOST].to_str().unwrap(),
+        count_file_metrics(&files)
+    );
+
+    let template_filename = "user_profile.html";
+    let template = TemplateEnum::Profile(Profile {
+        domain: String::from(headers[HOST].to_str().unwrap()),
+        key_creation,
+        key_generator,
+        key_list_html,
+        file_list_html,
+    });
+    let status_code = StatusCode::OK;
+
+    render_template(res, &headers, template_filename, template, status_code).await
+}
+
+/// This handlers works as the API for generating new keys.
+/// It handles the POST request for making a new key.
+#[endpoint(
+    tags("user-management"),
+    parameters(
+        ("comment", description = "The user comment for the newly created key."),
+        ("key_level", description = "The permission level of the key. (Normal or Super)"),
+    )
+)]
+pub async fn generate_key(
+    req: &mut Request,
+    res: &mut Response,
+    depot: &mut Depot,
+    key_level: QueryParam<String, false>,
+    comment: QueryParam<String, false>,
+) {
+    let headers = req.headers().clone();
+    // Setup db pool
+    let sqlconn = SQLITE.get().unwrap();
+
+    // Grab the key from the auth handler.
+    let client_key = depot
+        .get::<Key>("client_key")
+        .expect("Failed to grab API Key from depot")
+        .clone();
+
+    // Turns out we haven't checked if the user is allowed to generate keys.
+    // This lets the user to create as many keys as they want from the endpoint. oops!
+    let available_keys = keys::keys_utilisation(sqlconn, &client_key)
+        .await
+        .unwrap_or(0);
+    debug!("Available keys {}", available_keys);
+    if available_keys <= 0 {
+        // User shouldn't be able to generate any more keys.
+        res.status_code(StatusCode::FORBIDDEN);
+        res.render(Text::Json(r#"{"error": "NoAvailableKeys"}"#));
+        return;
+    }
+
+    // Bug: this sucks since the QueryParam returns some odd option type, lets get the actual option inside.
+    let comment = comment.into_inner();
+    let key_level = key_level.into_inner();
+
+    debug!("key_level to create: {:?}", key_level);
+    debug!("comment: {:?}", comment);
+
+    let key_level = KeyLevel::from_str(&key_level.unwrap_or("Normal".to_string()));
+    // Unpack key level
+    match key_level {
+        Ok(key_level) => {
+            // Check if the key is actually allowed to generate superkey.
+            // If the client key isn't master, but the requested key level to generate is super, fail.
+            if client_key.key_level != KeyLevel::Master && key_level == KeyLevel::Super {
+                // User isn't allowed to generate super keys. We should complain.
+                res.status_code(StatusCode::FORBIDDEN);
+                res.render(Text::Json(r#"{"error": "NoPermissionForSuperKey"}"#));
+                return;
+            }
+            // Generate key
+            let new_key = db_keys::create_key(sqlconn, key_level, Some(client_key), comment).await;
+            match new_key {
+                Ok(new_key) => {
+                    info!(
+                        "[{}] Created new {} key - {}",
+                        headers[HOST].to_str().unwrap(),
+                        new_key.key_level,
+                        new_key.uuid
+                    );
+                    // Generate a response saying that we did it!
+                    res.status_code(StatusCode::OK);
+                    let render_json = serde_json::json!({
+                        "success": "Created Key",
+                        "key": new_key.uuid,
+                    })
+                    .to_string();
+                    res.render(Text::Json(render_json));
+                }
+                Err(e) => {
+                    error!("Error while generating new key: {}", e);
+                    res.status_code(StatusCode::INTERNAL_SERVER_ERROR);
+                    res.render(Text::Json(r#"{"error": "UnknownErrorCreatingKey"}"#));
+                    return;
+                }
+            }
+        }
+        Err(_) => {
+            res.status_code(StatusCode::FORBIDDEN);
+            res.render(Text::Json(r#"{"error": "NoPermissionForSuperKey"}"#));
+            return;
+        }
+    };
+}
diff --git a/src/keys/db_keys.rs b/src/keys/db_keys.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e4c2203b1966029f306af348ac3b7b0eba04b985
--- /dev/null
+++ b/src/keys/db_keys.rs
@@ -0,0 +1,323 @@
+use serde_json::from_str;
+use sqlx::{Pool, Sqlite};
+use std::time::SystemTime;
+use thiserror::Error;
+use tracing::{debug, error, info, warn};
+
+use crate::CONFIG;
+
+use super::{generate_key, Key, KeyLevel};
+
+// Enum for Key Checking.
+#[derive(Error, Debug)]
+pub enum KeyCheckError {
+    #[error("Key not found")]
+    NotFound,
+    #[error("Key revoked: {0}")]
+    Revoked(String),
+    #[error("Database error: {0}")]
+    DatabaseError(#[from] sqlx::Error),
+}
+
+#[derive(Error, Debug)]
+pub enum KeyError {
+    #[error("Key not found")]
+    NotFound,
+    #[error("Database error: {0}")]
+    DatabaseError(#[from] sqlx::Error),
+}
+
+/// Initialise a new key for a new database.
+/// This should only run once per server (unless you mess with the db)
+pub async fn init_key(sqlconn: &Pool<Sqlite>) {
+    // Count the total number of keys in the database.
+    let key_count = sqlx::query!(
+        r#"
+            SELECT COUNT(*) as count FROM keys
+        "#
+    )
+    .fetch_one(sqlconn)
+    .await;
+
+    if key_count
+        .expect("Failed to check the key count from the db")
+        .count
+        == 0
+    {
+        warn!("Couldn't find any keys in the database, making a new master key now!");
+        let gen_key = create_key(
+            sqlconn,
+            KeyLevel::Master,
+            None,
+            Some("Initial Master Key".to_string()),
+        )
+        .await;
+        let gen_key = gen_key.expect("Failed to generate a new mater key... oops!");
+        info!("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+        info!("Generated New Master Key: {}", gen_key.uuid);
+        info!("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+    }
+}
+
+/// This adds a generated key into the DB.
+/// We don't check any key validation here, this is done before calling.
+pub async fn create_key(
+    sqlconn: &Pool<Sqlite>,
+    level: KeyLevel,
+    parent_key: Option<Key>,
+    comment: Option<String>,
+) -> Result<Key, sqlx::Error> {
+    // Ensure parent key is present for non-master keys
+    if parent_key.is_none() && level != KeyLevel::Master {
+        return Err(sqlx::Error::Configuration(
+            "Parent key is missing".to_string().into(),
+        ));
+    }
+
+    // Generate a new key
+    let uuid = generate_key(sqlconn).await.unwrap();
+    // Check if the comment is present, and add it if so.
+    let comment_value = comment.as_deref();
+    // Convert the KeyLevel to a i32.
+    let keylevel = level.as_i32();
+    // Convert `Option<Key>` to `Option<String>`
+    let parent_key = parent_key.map(|key| key.uuid);
+    // Generate the current unix time for key creation.
+    let creation_time = SystemTime::now()
+        .duration_since(SystemTime::UNIX_EPOCH)
+        .unwrap()
+        .as_secs() as i32;
+
+    // Insert the key into the DB
+    let _result = sqlx::query!(
+        "INSERT INTO keys (
+                uuid,
+                key_level,
+                parent_key,
+                creation_time,
+                comment)
+                VALUES ( ?,?,?,?,? )",
+        uuid,
+        keylevel,
+        parent_key,
+        creation_time,
+        comment_value,
+    )
+    .execute(sqlconn)
+    .await?;
+
+    // Construct the key.
+    let final_key = Key {
+        uuid,
+        key_level: level,
+        parent_key,
+        creation_time,
+        revoked: None,
+        comment,
+        child_uuids: vec![], // New keys can't have kids!
+    };
+
+    // Return the Key
+    Ok(final_key)
+}
+
+/// This returns a key when given a client key.
+/// It can also return a few errors.
+/// If allow_unverified_keys is true, we immediately return a Fake Key result, since we don't need to validate anything.
+pub async fn get_key(sqlconn: &Pool<Sqlite>, client_key: &str) -> Result<Key, KeyError> {
+    if CONFIG.uploading.allow_unverified_keys {
+        debug!("Ephemeral is not configured to do key validation, returning 'fake' Key.");
+        return Ok(Key::new(
+            client_key.to_string(),
+            KeyLevel::Normal,
+            None,
+            None,
+            0_i32,
+            None,
+            vec![],
+        ));
+    }
+
+    let row = sqlx::query!(
+        r#"
+            SELECT 
+                uuid, 
+                key_level, 
+                parent_key, 
+                revoked, 
+                creation_time, 
+                comment, 
+                child_uuids 
+            FROM keys_with_children 
+            WHERE uuid = ?
+        "#,
+        client_key
+    )
+    .fetch_one(sqlconn)
+    .await;
+
+    match row {
+        Ok(row) => {
+            // Convert the key_level from i32 to KeyLevel
+            let key_level = KeyLevel::from_i32(
+                row.key_level
+                    .try_into()
+                    .expect("Failed to convert key from i64 to i32"),
+            )
+            .ok_or_else(|| {
+                KeyError::DatabaseError(sqlx::Error::Decode("Invalid key level".into()))
+            })?;
+
+            // Convert child_uuids from JSON string to Vec<String>
+            let child_uuids: Vec<String> = from_str(&row.child_uuids.unwrap_or("[]".to_owned()))
+                .unwrap_or_else(|_| {
+                    debug!("child_uuids is empty or invalid, returning an empty Vec<String>");
+                    vec![]
+                });
+            debug!("Conversion to Vec<Str>: {:#?}", child_uuids);
+
+            let key = Key::new(
+                row.uuid,
+                key_level,
+                row.parent_key,
+                row.revoked,
+                row.creation_time as i32,
+                row.comment,
+                child_uuids,
+            );
+            Ok(key)
+        }
+        Err(e) => {
+            error!("Couldn't find key: {}", e);
+            Err(KeyError::NotFound)
+        }
+    }
+}
+
+/// This checks if a key already exists in the table.
+/// Used to avoid collisions (not that I can see many happening)
+pub async fn check_key(sqlconn: &Pool<Sqlite>, key: &str) -> Result<(), KeyCheckError> {
+    // Select the uuid and revoked columns from the table.
+    let row = sqlx::query!("SELECT uuid, revoked FROM keys WHERE uuid = ?", key)
+        .fetch_optional(sqlconn)
+        .await?;
+
+    match row {
+        Some(record) => {
+            if let Some(revoked_reason) = record.revoked {
+                Err(KeyCheckError::Revoked(revoked_reason))
+            } else {
+                Ok(())
+            }
+        }
+        None => Err(KeyCheckError::NotFound),
+    }
+}
+
+/// Count the total number of keys in the database.
+pub async fn count_keys(sqlconn: &Pool<Sqlite>) -> Result<i32, sqlx::Error> {
+    // Count the total number of keys in the database.
+    let key_count = sqlx::query!(
+        r#"
+            SELECT COUNT(*) as count FROM keys
+        "#
+    )
+    .fetch_one(sqlconn)
+    .await?;
+
+    Ok(key_count.count)
+}
+
+/// This function retrieves every single key from the database.
+/// This is used for viewing the full 'key tree' on the admin page.
+pub async fn get_all_keys(sqlconn: &Pool<Sqlite>) -> Result<Vec<Key>, KeyError> {
+    let results = sqlx::query!(
+        r#"
+            SELECT 
+                uuid, 
+                key_level, 
+                parent_key, 
+                revoked, 
+                creation_time, 
+                comment, 
+                child_uuids 
+            FROM keys_with_children 
+        "#,
+    )
+    .fetch_all(sqlconn)
+    .await?;
+
+    // Initialise the empty vec
+    let mut keys: Vec<Key> = vec![];
+
+    // Iterate over all keys returned.
+    for row in results {
+        // Convert all key_level(i32) to KeyLevel
+        let key_level = KeyLevel::from_i32(
+            row.key_level
+                .try_into()
+                .expect("Failed to convert key from i64 to i32"),
+        )
+        .ok_or_else(|| KeyError::DatabaseError(sqlx::Error::Decode("Invalid key level".into())))?;
+
+        // Convert child_uuids from JSON string to Vec<String>
+        let child_uuids: Vec<String> = from_str(&row.child_uuids.unwrap_or("[]".to_owned()))
+            .unwrap_or_else(|_| {
+                debug!("child_uuids is empty or invalid, returning an empty Vec<String>");
+                vec![]
+            });
+
+        // Create a new key
+        let key = Key::new(
+            row.uuid,
+            key_level,
+            row.parent_key,
+            row.revoked,
+            row.creation_time as i32,
+            row.comment,
+            child_uuids,
+        );
+        // Push the new key to the vec
+        keys.push(key)
+    }
+    // Now that we're done, return the Vector
+    Ok(keys)
+}
+
+/// This revokes a key.
+/// Requires a revoke message.
+/// Returns a bool if it worked.
+pub async fn revoke_key(
+    sqlconn: &Pool<Sqlite>,
+    key: &str,
+    revoke_message: &str,
+) -> Result<u64, sqlx::Error> {
+    // Update the revoked message for the provided key.
+    let result = sqlx::query!(
+        "UPDATE keys SET revoked = ? WHERE uuid = ?",
+        revoke_message,
+        key
+    )
+    .execute(sqlconn)
+    .await?;
+
+    let rows_changed = result.rows_affected();
+
+    Ok(rows_changed)
+}
+
+/// This revokes a key.
+/// Requires a revoke message.
+/// Returns a bool if it worked.
+pub async fn edit_comment(
+    sqlconn: &Pool<Sqlite>,
+    uuid: &str,
+    comment: &str,
+) -> Result<bool, sqlx::Error> {
+    // Update the revoked message for the provided key.
+    let _result = sqlx::query!("UPDATE keys SET comment = ? WHERE uuid = ?", comment, uuid)
+        .execute(sqlconn)
+        .await?;
+
+    Ok(true)
+}
diff --git a/src/keys/mod.rs b/src/keys/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..433a008cf24aa1a244b8551dd8ca5faa83249a4c
--- /dev/null
+++ b/src/keys/mod.rs
@@ -0,0 +1,224 @@
+// Submodules for each request handler
+pub mod db_keys;
+
+// This page contains the backend logic for generating new keys
+use core::fmt;
+use std::{str::FromStr, thread::current};
+
+use db_keys::KeyError;
+use nanoid::nanoid;
+use sqlx::{Pool, Sqlite};
+use tracing::{debug, error};
+use tracing_subscriber::field::debug;
+
+use crate::{
+    keys::db_keys::{check_key, get_key, KeyCheckError},
+    CONFIG,
+};
+
+/// Key Object
+/// The key contains basically everything you need to handle keys within the system.
+/// It stores the users permissions, the creator, and a few optionals.
+#[derive(Clone, Debug)]
+pub struct Key {
+    pub uuid: String,               // Actual Key
+    pub key_level: KeyLevel,        // Permission Level
+    pub parent_key: Option<String>, // Optional (since master keys) Parent Key.
+    pub revoked: Option<String>,    // Optional Ban Reason.
+    pub creation_time: i32,         // Key Creation Time.
+    pub comment: Option<String>,    // Optional Key Name.
+    pub child_uuids: Vec<String>,   // Vector of children.
+}
+impl Key {
+    pub fn new(
+        uuid: String,
+        key_level: KeyLevel,
+        parent_key: Option<String>,
+        revoked: Option<String>,
+        creation_time: i32,
+        comment: Option<String>,
+        child_uuids: Vec<String>,
+    ) -> Self {
+        Key {
+            uuid,
+            key_level,
+            parent_key,
+            revoked,
+            creation_time,
+            comment,
+            child_uuids,
+        }
+    }
+}
+
+/// Possible Permission Levels a key can be.
+#[derive(Debug, PartialEq, Clone)]
+pub enum KeyLevel {
+    Master,
+    Super,
+    Normal,
+}
+
+impl KeyLevel {
+    pub fn as_i32(&self) -> i32 {
+        match self {
+            KeyLevel::Master => 0,
+            KeyLevel::Super => 1,
+            KeyLevel::Normal => 2,
+        }
+    }
+
+    pub fn from_i32(val: i32) -> Option<KeyLevel> {
+        match val {
+            0 => Some(KeyLevel::Master),
+            1 => Some(KeyLevel::Super),
+            2 => Some(KeyLevel::Normal),
+            _ => None,
+        }
+    }
+
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            KeyLevel::Master => "Master",
+            KeyLevel::Super => "Super",
+            KeyLevel::Normal => "Normal",
+        }
+    }
+}
+
+impl FromStr for KeyLevel {
+    type Err = ();
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "Master" => Ok(KeyLevel::Master),
+            "Super" => Ok(KeyLevel::Super),
+            "Normal" => Ok(KeyLevel::Normal),
+            _ => Err(()),
+        }
+    }
+}
+
+impl fmt::Display for KeyLevel {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.as_str())
+    }
+}
+
+/// Generate a new Key
+/// This should check if the key exists for another file, and regenerate if it does.
+/// This does NOT give it a permission level, this literally just generates the key itself.
+pub async fn generate_key(sqlconn: &Pool<Sqlite>) -> Result<String, KeyCheckError> {
+    // Get the size of the apikey from the config
+    let key_size: usize = CONFIG.operations.apikey_size.into();
+    // Maximum attempts we'll try to generate a valid key.
+    let max_attempts = 8;
+
+    // Loop until we get a valid key
+    for _ in 0..max_attempts {
+        let key = nanoid!(key_size);
+
+        match check_key(sqlconn, &key).await {
+            Ok(_) => {
+                debug!("generate_key: Found Existing Key, trying again.");
+            }
+            Err(KeyCheckError::NotFound) => {
+                debug!("generate_key: Generated new key: {:?}", key);
+                return Ok(key);
+            }
+            Err(KeyCheckError::Revoked(reason)) => {
+                // Log revoked keys for debugging purposes
+                debug!("generate_key: Found revoked key: {}", reason);
+                // Continue to the next iteration to generate a new key
+            }
+            Err(KeyCheckError::DatabaseError(e)) => {
+                // Log the database error
+                debug!("generate_key: Database error: {}", e);
+                return Err(KeyCheckError::DatabaseError(e));
+            }
+        }
+    }
+    // If the whole loop runs, and we don't return a valid key, we've hit the max attempts.
+    error!("generate_key: Exceeded maximum key generations, generating a super long key.");
+    // Generate a REALLY big id. If we ever see one of these, we're in trouble.
+    Ok(nanoid!(32))
+}
+
+/// This function checks if key has been fully utilised.
+/// It returns an i32, which shows how many keys generations are left. 0 being fully utilised.
+pub async fn keys_utilisation(sqlconn: &Pool<Sqlite>, key: &Key) -> Result<i32, KeyError> {
+    // If we fail to get the number of children, we say it's the max and we can't do any.
+    let children_count: i32 = key.child_uuids.len().try_into().unwrap_or(i32::MAX);
+    debug!("[{}] Children count: {}", key.uuid, children_count);
+
+    let max_allowed_keys = max_keys(sqlconn, key).await?;
+
+    // I wonder what would happen if children count is larger?
+    let remaining_keys = max_allowed_keys - children_count;
+    debug!("[{}] remaining_keys: {}", key.uuid, remaining_keys);
+
+    Ok(remaining_keys)
+}
+
+/// This calculates how many keys a key is allowed to generate.
+/// The below function uses this to show how many keys are left to generate.
+pub async fn max_keys(sqlconn: &Pool<Sqlite>, key: &Key) -> Result<i32, KeyError> {
+    match key.key_level {
+        KeyLevel::Master => {
+            // Master keys have 'infinite' key gens.
+            Ok(i32::MAX)
+        }
+        KeyLevel::Super => {
+            // Super keys have the maximum configured key gens.
+            Ok(CONFIG.uploading.max_key_generations)
+        }
+        KeyLevel::Normal => {
+            // Normal keys are halved for each step it takes to get to a Super/Master Key
+            let steps = count_to_super(sqlconn, key.clone()).await?;
+            let max =
+                CONFIG.uploading.max_key_generations / 2_i32.pow(steps.try_into().unwrap_or(0));
+            Ok(max)
+        }
+    }
+}
+
+/// Count how many parent keys it takes to get to a Super key.
+/// This code is only run for Normal Keys.
+pub async fn count_to_super(sqlconn: &Pool<Sqlite>, key: Key) -> Result<i32, KeyError> {
+    // Initialize the current key and count to super.
+    let mut current_key = key;
+    let mut count = 0;
+
+    while current_key.key_level == KeyLevel::Normal {
+        // Increment the count
+        count += 1;
+
+        // Get the parent key
+        if let Some(parent_uuid) = current_key.parent_key.as_ref() {
+            match get_key(sqlconn, parent_uuid).await {
+                Ok(parent_key) => match parent_key.key_level {
+                    KeyLevel::Master => {
+                        debug!("Root Parent is Master!");
+                        break;
+                    }
+                    KeyLevel::Super => {
+                        debug!("Found the root Super Key!");
+                        break;
+                    }
+                    KeyLevel::Normal => {
+                        debug!("We haven't found the root key, lets keep going!");
+                        current_key = parent_key;
+                    }
+                },
+                Err(e) => {
+                    // Failed to get key - passing up.
+                    return Err(e);
+                }
+            }
+        } else {
+            // If there is no parent key, return an error
+            return Err(KeyError::NotFound);
+        }
+    }
+    Ok(count)
+}
diff --git a/src/main.rs b/src/main.rs
index f16d774d53da18d62f1201ea18ec41db33d3d666..dbe5e11cf9e07ea38df78ac51fa01696f6b83470 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,29 +8,27 @@ use sqlx::SqlitePool;
 
 use lazy_static::lazy_static;
 use std::fs::create_dir_all;
+use tracing::{error, info};
 use tracing_subscriber::filter::EnvFilter;
 use tracing_subscriber::fmt;
 use tracing_subscriber::prelude::*;
 
 use crate::config::AppConfig;
 use crate::db::DatabaseType;
+use crate::handlers::auth::require_auth;
 use crate::handlers::cleaner_thread;
 
-// Import sub-modules.
 mod config;
 mod db;
 mod engine;
+mod handlers;
+mod keys;
 
-// Import tests for main, db and engine.
 mod db_tests;
 #[cfg(test)]
 mod engine_tests;
 
-// Import the web request handlers modules.
-mod handlers;
-
 // I /don't think/ there's any harm to adding multiple of thse for each sql backend.
-// Setup the global sqlite db
 static SQLITE: OnceCell<SqlitePool> = OnceCell::new();
 
 // Initialise the magic db
@@ -40,7 +38,6 @@ static SQLITE: OnceCell<SqlitePool> = OnceCell::new();
 thread_local! {
     static COOKIE: Cookie = {
         let cookie = Cookie::open(magic::CookieFlags::MIME_TYPE).unwrap();
-        // Load the default database
         cookie.load::<&str>(&[]).unwrap();
         cookie
     };
@@ -65,67 +62,87 @@ lazy_static! {
 
 #[tokio::main]
 async fn main() {
-    // Read log level from config
-    let log_config = &CONFIG.logging.level;
-
-    // Construct tracing_subscriber with the filter from config.
+    // Initialise Logging
+    let log_level = &CONFIG.logging.level;
     tracing_subscriber::registry()
         .with(fmt::layer().compact())
-        .with(EnvFilter::new(log_config))
+        .with(EnvFilter::new(log_level))
         .init();
 
-    // Determine what SQL engine we're running.
     let sql_backend = &CONFIG.database.sql_backend;
-
     let db_url = &CONFIG.database.url;
 
-    // Match on the database type
     match DatabaseType::from_str(sql_backend) {
         Some(DatabaseType::Sqlite) => {
-            tracing::info!("Using SQLite backend");
+            info!("DB: Using SQLite backend");
             let pool = SqlitePool::connect(db_url).await.unwrap();
             SQLITE.set(pool).unwrap();
         }
         Some(DatabaseType::Postgres) => {
-            tracing::info!("Using Postgres backend");
-            tracing::error!("https://github.com/launchbadge/sqlx/issues/964");
+            info!("DB: Using Postgres backend");
+            error!("https://github.com/launchbadge/sqlx/issues/964");
             todo!("Postgres not implemented yet")
             // let pool = PgPool::connect(&psql).await;
             // TODO: figure out how to use sqlx::Pool struct https://docs.rs/sqlx/latest/sqlx/struct.Pool.html
         }
         Some(DatabaseType::Mysql) => {
-            tracing::info!("Using MySQL backend");
-            tracing::error!("https://github.com/launchbadge/sqlx/issues/964");
+            info!("DB: Using MySQL backend");
+            error!("https://github.com/launchbadge/sqlx/issues/964");
             todo!("MySQL not implemented yet")
         }
-        Some(DatabaseType::Mssql) => {
-            tracing::info!("Using MSSQL backend");
-            tracing::error!("https://github.com/launchbadge/sqlx/issues/964");
-            unimplemented!("MSSQL not implemented yet, and honestly my hatred for microsoft will probably keep it this way")
-        }
         Some(DatabaseType::Oraclecloud) => {
-            tracing::info!("Using OracleCloud backend");
-            tracing::error!("https://github.com/launchbadge/sqlx/issues/964");
+            info!("DB: Using OracleCloud backend");
+            error!("https://github.com/launchbadge/sqlx/issues/964");
             todo!("OracleCloud not implemented yet")
         }
         _ => panic!("Unsupported SQL backend: {}", sql_backend),
     };
 
-    // Initialise the cleaner task
-    tracing::info!(
+    // There's probably a better way, but this is where we generate a new master key.
+    keys::db_keys::init_key(SQLITE.get().unwrap()).await;
+
+    info!(
         "Starting cleaner thread every {} seconds",
         CONFIG.operations.cleaner_interval
     );
     cleaner_thread(CONFIG.operations.cleaner_interval);
 
     // Attempt to create the files directory
-    create_dir_all("files").unwrap();
+    if let Err(e) = create_dir_all("files") {
+        error!("Couldn't create files directory: {}", e);
+        std::process::exit(1);
+    }
 
     // Internal routing
     let router = Router::new()
-        // Main / functions
         .get(handlers::index::index)
-        .post(handlers::upload::upload)
+        // Child Router for authenticated endpoints
+        .push(
+            Router::new()
+                // Hoops run a middleware before processing the actual request.
+                .hoop(require_auth)
+                .post(handlers::upload::upload)
+                // File Management API
+                .push(
+                    Router::with_path("/api/v1/file/<filename>")
+                        .delete(handlers::delete_file::delete_file), // .get(handlers::delete_file::delete_file), // TODO: serve RAW file, for programs. (when we add text/linting whatever)
+                )
+                // User Management Dashboard
+                .push(Router::with_path("/profile").get(handlers::user_profile::user_profile))
+                // User Management API
+                .push(
+                    Router::with_path("/api/v1/profile/generate_key")
+                        .post(handlers::user_profile::generate_key),
+                )
+                // Admin Dashboard
+                .push(Router::with_path("/admin").get(handlers::admin::admin))
+                // Admin API
+                .push(
+                    Router::with_path("/api/v1/admin/<apikey>")
+                        .delete(handlers::admin::admin_revoke_key)
+                        .patch(handlers::admin::admin_edit_comment),
+                ),
+        )
         // Static Pages
         .push(Router::with_path("/services").get(handlers::serve_static::serve_static))
         .push(Router::with_path("/about").get(handlers::serve_static::serve_static))
@@ -138,29 +155,22 @@ async fn main() {
         // Might want to actually make a .ico instead of serving a webp image.
         .push(Router::with_path("/favicon.ico").get(StaticFile::new("static/favicon32.webp")))
         .push(Router::with_path("/robots.txt").get(StaticFile::new("static/robots.txt")))
-        // Static File Serving
-        .push(Router::with_path("static/<**path>").get(StaticDir::new("static/").listing(true)))
-        // Deletion API
-        .push(
-            Router::with_path("/delete/<adminkey>")
-                .delete(handlers::delete_file::delete_file)
-                .get(handlers::delete_file::delete_file),
-        )
-        .push(
-            Router::with_path("/edit/<adminkey>")
-                .delete(handlers::delete_file::delete_file)
-                .get(handlers::delete_file::delete_file),
-        )
-        // List of files the requester IP has uploaded.
-        .push(Router::with_path("/my_files").get(handlers::list_files::list_files))
+        // Static File Serving (show auto index, which should be an option instead....)
+        .push(Router::with_path("static/<**path>").get(StaticDir::new("static/").auto_list(true)))
         // Serving uploaded files, or 404.
         .push(Router::with_path("<file>").get(handlers::serve_file::serve_file));
 
+    // Add OpenAPI Stuff here
+    let doc = OpenApi::new("Ephemeral API", "0.1.0").merge_router(&router);
+    let router = router
+        .unshift(doc.into_router("/api-doc/openapi.json"))
+        .unshift(SwaggerUi::new("/api-doc/openapi.json").into_router("/swagger-ui"));
+
     // Read environment variables for host and port
     let host = &CONFIG.server.host;
     let port = CONFIG.server.port;
     let server_url = format!("{}:{}", host, port);
-    tracing::info!("Listening on http://{}", server_url);
+    info!("Listening on http://{}", server_url);
     // TODO: Allow us to listen on a socket too?
     // let acceptor = UnixListener::new("/tmp/salvo.sock").bind().await;
     let acceptor = TcpListener::new(&server_url).bind().await;
@@ -168,7 +178,7 @@ async fn main() {
     Server::new(acceptor).serve(router).await;
 
     // Close SQLite before closing
-    tracing::info!("Attempting to safely shut down Ephemeral.");
+    info!("Attempting to safely shut down Ephemeral.");
     SQLITE
         .get()
         .expect("Problem while safely closing the database :(")
diff --git a/static/example.com.sxcu b/static/example.com.sxcu
index 8ed870a4791756519a56195ae78a139cc9518648..b370882261fa288efc3326cb3ac2cc8ffb661517 100644
--- a/static/example.com.sxcu
+++ b/static/example.com.sxcu
@@ -2,8 +2,11 @@
   "Name": "example.com file uploader",
   "DestinationType": "FileUploader",
   "RequestURL": "https://example.com",
+    "Headers": {
+        "Authorization": "Bearer <your API token goes here>",
+        "Accept": "application/json"
+    },
   "FileFormName": "file",
   "ResponseType": "Text",
   "URL": "$json:url$",
-  "DeletionURL": "$json:deletionurl$"
 }
diff --git a/static/sort.css b/static/sort.css
index 7a5f4251bd2415e2ee33db43fe5972b4a06a0ebc..1f0d5da65eb521a37b40f3a35df89751e882e6a3 100644
--- a/static/sort.css
+++ b/static/sort.css
@@ -42,3 +42,106 @@
     color: inherit;
     content: "▴";
   }
+  .delete-button {
+    background-color: #333;
+    color: #ff5555;
+    border: none;
+    padding: 10px 20px;
+    cursor: pointer;
+}
+/* Styles for the entire form displayed as a flex container */
+form#keygen-form {
+  display: flex;
+  flex-wrap: nowrap;
+  /* gap: 20px; */
+}
+
+/* Styles for the textarea */
+textarea[name="comment"] {
+  resize: none;
+  color: #292;
+  padding: 5px;
+  border: 1px solid #cccccc50;
+  border-radius: 2px;
+  box-shadow: none;
+  background-color: #1d1d1d;
+}
+
+/* Placeholder styles */
+textarea[name="comment"]::placeholder {
+  color: #bbb; /* Lighter color for placeholder text */
+}
+
+/* Container for the key level part */
+.keygen-level {
+  display: flex;
+  flex-direction: column;
+}
+
+.keygen-level label {
+  text-align: center;
+  font-size: 0.8em;
+}
+
+/* Styles for the select dropdown */
+select[name="keygen-level"] {
+  height: 100%;
+  color: #292;
+  padding: 5px;
+  border: 1px solid #cccccc50;
+  border-radius: 2px;
+  box-shadow: none;
+  background-color: #1d1d1d;
+}
+
+/* Styles for the submit button */
+button[type="button"] {
+  align-self: flex-end; /* Align the button to the bottom of the form */
+  height: 100%;
+  color: #292;
+  padding: 5px;
+  border: 1px solid #cccccc50;
+  border-radius: 2px;
+  box-shadow: none;
+  background-color: #1d1d1d;
+}
+
+/* Styles for the progress bar */
+.progress-container {
+  position: relative;
+  width: 100%;
+}
+
+.progress-text {
+  position: absolute;
+  left: 10px;
+  top: 50%;
+  transform: translateY(-50%);
+  font-size: 14px;
+  color: black;
+  z-index: 1;
+}
+
+.progress-bar {
+  position: relative;
+  width: 100%;
+  height: 25px;
+  background-color: lightgray;
+  border-radius: 5px;
+  overflow: hidden;
+}
+
+.progress-bar-fill {
+  height: 100%;
+  width: 0; /* Default to 0%, will be set by inline style */
+  background-color: #229922;
+  transition: width 0.25s ease-in-out, background-color 0.25s ease-in-out;
+}
+
+.progress-bar-fill.high {
+  background-color: orange;
+}
+
+.progress-bar-fill.critical {
+  background-color: red;
+}
\ No newline at end of file
diff --git a/static/sort.js b/static/sort.js
index 05ffb484cda46c707da5828aa1e79e2ec0454e50..2c23f0cf9302c23f349c440df86f33d5b0938b33 100644
--- a/static/sort.js
+++ b/static/sort.js
@@ -1,4 +1,36 @@
 // https://github.com/tofsjonas/sortable
 document.addEventListener("click",function(c){try{function f(a,b){return a.nodeName===b?a:f(a.parentNode,b)}var x=c.shiftKey||c.altKey,d=f(c.target,"TH"),m=f(d,"TR"),g=f(m,"TABLE");function n(a,b){a.classList.remove("dir-d");a.classList.remove("dir-u");b&&a.classList.add(b)}function p(a){return x&&a.dataset.sortAlt||a.dataset.sort||a.textContent}if(g.classList.contains("sortable")){var q,e=m.cells,r=parseInt(d.dataset.sortTbr);for(c=0;c<e.length;c++)e[c]===d?q=parseInt(d.dataset.sortCol)||c:n(e[c],
     "");e="dir-d";if(d.classList.contains("dir-d")||g.classList.contains("asc")&&!d.classList.contains("dir-u"))e="dir-u";n(d,e);var t="dir-u"===e,v=function(a,b,h){var u=p((t?a:b).cells[h]);a=p((t?b:a).cells[h]);b=parseFloat(u)-parseFloat(a);return isNaN(b)?u.localeCompare(a):b};for(c=0;c<g.tBodies.length;c++){var k=g.tBodies[c],w=[].slice.call(k.rows,0);w.sort(function(a,b){var h=v(a,b,q);return 0!==h||isNaN(r)?h:v(a,b,r)});var l=k.cloneNode();l.append.apply(l,w);g.replaceChild(l,k)}}}catch(f){}});
-    
\ No newline at end of file
+
+function submitForm() {
+    const comment = document.getElementById('comment').value;
+    const keyLevelElement = document.getElementById('keygen-level');
+    const keyLevel = keyLevelElement ? keyLevelElement.value : null;
+
+    let url = '/api/v1/profile/generate_key?';
+    
+    // Conditionally add comment if it has a value
+    if (comment) {
+        url += `comment=${encodeURIComponent(comment)}&`;
+    }
+    
+    // Conditionally add key_level if it exists
+    if (keyLevel) {
+        url += `key_level=${encodeURIComponent(keyLevel)}`;
+    }
+
+    // Remove any trailing '&' or '?' if necessary
+    url = url.replace(/[&?]$/, '');
+
+    fetch(url, {
+        method: 'POST',
+    })
+    .then(response => response.json())
+    .then(data => {
+        document.getElementById('response-message').innerText = `Created Key: ${data.key} (refresh to see in the table)`;
+    })
+    .catch(error => {
+        console.error('Error:', error);
+        document.getElementById('response-message').innerText = 'An error occurred.';
+    });
+}
\ No newline at end of file
diff --git a/static/style.css b/static/style.css
index 39cb59f6212016fa998183789f1c632ccf4c562c..d382f2fbebe8553f0c90d172bdf76cb9a13ceada 100644
--- a/static/style.css
+++ b/static/style.css
@@ -33,3 +33,36 @@ to{opacity:1;}
 .uploadButton p{padding:20px;font-weight:300;font-size:24pt;}
 .uploadButton input[type="file"]{position:absolute;display:block;width:100%;height:100%;top:0;right:0;opacity:0;}
 input {background-color: #1d1d1d;border: none;text-align: center;}
+/* CSS for the specific input form */
+.api-key-form {
+    position: fixed;
+    bottom: 10px;
+    left: 10px;
+    margin: 0;
+    padding: 0;
+    list-style-type: none;
+    z-index: 1000;
+}
+.api-key-form .input-container {
+    padding: 10px;
+    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+    display: flex;
+    align-items: center;  /* Align items horizontally aligned in the center */
+}
+.api-key-form #apiKeyInput {
+    color: #292;
+    padding: 5px;
+    border: 1px solid #cccccc50;
+    border-radius: 2px;
+    box-shadow: none;
+}
+.api-key-form #savedMessage {
+    opacity: 0;  /* Start with opacity set to 0 (hidden) */
+    transition: opacity 0.5s ease-out;  /* Smooth transition for the opacity */
+    color: green;
+    font-size: 0.9em;
+    margin-left: 10px;  /* Add margin to the left for spacing */
+}
+.api-key-form #savedMessage.show {
+    opacity: 1;  /* Show element by setting opacity to 1 */
+}
\ No newline at end of file
diff --git a/static/upload.js b/static/upload.js
index a323ab3c4d725b1bd95d314f840edf648e9d12c6..a984f64c03902124b2bee1dbd633e6cc7c4dbb5d 100644
--- a/static/upload.js
+++ b/static/upload.js
@@ -61,4 +61,4 @@ function progressHandler(event) {
 
 function responseHandler(event) {
     document.open('text/html').write(event.target.response).close();
-}
+}
\ No newline at end of file
diff --git a/templates/localhost:8282/admin.html b/templates/localhost:8282/admin.html
new file mode 100644
index 0000000000000000000000000000000000000000..dbee4991e87fe2e54e04f623e3220cc9aa5bb765
--- /dev/null
+++ b/templates/localhost:8282/admin.html
@@ -0,0 +1,111 @@
+{{>regular.html}}
+<style>
+ul, #hide_bullets {
+  list-style-type: none;
+  padding-left: 1.8em;
+}
+
+ul, #hide_bullets li::before {
+  content: "" !important; /* Overriding regular.css */
+  font-family: monospace;
+}
+
+.toggle {
+    cursor: pointer;
+    user-select: none;
+    color: gray;
+}
+
+.no-toggle {
+    width: 1em;
+    display: inline-block;
+}
+
+.child-count {
+    display: none;
+    font-style: italic;
+    color: grey;
+}
+.editable {
+    cursor: pointer;
+    text-decoration: underline;
+}
+
+.delete {
+    cursor: pointer;
+    color: red;
+}
+</style>
+<script>
+document.addEventListener('DOMContentLoaded', () => {
+    const toggles = document.querySelectorAll('#hide_bullets .toggle');
+    toggles.forEach((toggle, index) => {
+        toggle.addEventListener('click', function() {
+            const parentLi = this.closest('li');
+            const childUl = parentLi.querySelector('.children');
+            if (childUl) {
+                if (childUl.style.display === 'block') {
+                    childUl.style.display = 'none';
+                    const nodeCount = childUl.querySelectorAll('.key-node').length;
+                    this.textContent = `▶ (${nodeCount} items)`;
+                } else {
+                    childUl.style.display = 'block';
+                    this.textContent = '▼';
+                }
+            }
+        });
+    });
+
+    const editables = document.querySelectorAll('.editable');
+
+    editables.forEach(editSpan => {
+        editSpan.addEventListener('click', function() {
+            const newComment = prompt('Enter new comment:');
+            if (newComment !== null) {
+                const key = this.dataset.key;
+                fetch(`http://localhost:8282/api/v1/admin/${key}?comment=${encodeURIComponent(newComment)}`, {
+                    method: 'PATCH'
+                }).then(response => {
+                    if (response.ok) {
+                        this.textContent = `💬 ${newComment}`;
+                    } else {
+                        alert('Failed to update comment.');
+                    }
+                });
+            }
+        });
+    });
+  
+    // Debugging: Check delete elements found
+    const deletes = document.querySelectorAll('.delete');
+    deletes.forEach(deleteSpan => {
+       deleteSpan.addEventListener('click', function() {
+            const revokeMessage = prompt('Enter revoke message:');
+            if (revokeMessage !== null) {
+                const key = this.dataset.key;
+                fetch(`http://localhost:8282/api/v1/admin/${key}?revoke_message=${encodeURIComponent(revokeMessage)}&recursive=false`, {
+                    method: 'DELETE'
+                }).then(response => {
+                    if (response.ok) {
+                        this.closest('.key-node').remove();
+                    } else {
+                        alert('Failed to revoke key.');
+                    }
+                });
+            }
+        });
+    });
+});
+</script>
+      <h1>
+        Admin Dashboard
+      </h1>
+      <h2>
+        Key Tree
+      </h2>
+        <p> 
+          There are {{apikey_count}} keys on this server, uploading {{file_count}} files!
+        </p>
+        <br>
+        {{{key_tree}}}
+</div></body></html>
\ No newline at end of file
diff --git a/templates/localhost:8282/link.html b/templates/localhost:8282/link.html
index eaacb0ff31eecda24a8dbb6af959f683fb0f0c40..072ee01388938ea1843ffcf2048575dd2af0b441 100644
--- a/templates/localhost:8282/link.html
+++ b/templates/localhost:8282/link.html
@@ -12,9 +12,6 @@
 <p style="text-align: center;">
   <input readonly class="fileLink" type="url" name="link" size="55" value="{{fileurl}}" autofocus="autofocus"
     onfocus="this.select()" onmouseover='this.select()'>
-  <br><br>
-  <input readonly class="fileLink" type="url" name="deletionlink" size="55" value="{{adminurl}}"
-    onmouseover='this.select()'>
 </p>
 <div style="text-align: center;"><img height="24" src="/static/trash.svg" alt="File Expiry (UTC)"> {{message1}}</div>
 <br><br>
diff --git a/templates/localhost:8282/my_files.html b/templates/localhost:8282/my_files.html
deleted file mode 100644
index f8cef7ebcc51d86a5285929dccf2df5aea96d610..0000000000000000000000000000000000000000
--- a/templates/localhost:8282/my_files.html
+++ /dev/null
@@ -1,17 +0,0 @@
-{{>regular.html}}
-      <h1>
-        Your Uploaded Files
-      </h1>
-      <p>
-        These are the files you have uploaded from this IP Address.<br>
-        Green = File is alive and active<br>
-        Red = File has been deleted
-      </p>
-      <link href="/static/sort.css" rel="stylesheet" />
-      <script src="/static/sort.js"></script>
-
-      <table class="sortable">
-        <thead><tr><th>File</th><th>Filesize</th><th>Mimetype</th><th>Views</th><th>Expiry</th></tr></thead>
-        <tbody>{{&message1}}</tbody>
-      </table>
-</div></body></html>
\ No newline at end of file
diff --git a/templates/localhost:8282/shell.html b/templates/localhost:8282/shell.html
index 3fa5f4c7cf0d368808c654ea84844b39d88495df..b1bf6f7352f5ee626fc2c68b5192ab51579af932 100644
--- a/templates/localhost:8282/shell.html
+++ b/templates/localhost:8282/shell.html
@@ -18,4 +18,75 @@
     <link href="/static/favicon64.webp" rel="icon" sizes="64x64">
     <link href="/static/favicon128.webp" rel="icon" sizes="128x128">
 </head>
+<ul class="api-key-form">
+  <li>
+      <div class="input-container">
+          <input type="text" id="apiKeyInput" placeholder="Enter your API key">
+          <span class="saved-message" id="savedMessage">💾</span>
+      </div>
+  </li>
+</ul>
+<script>
+  // Function to save API key in a cookie with a 1-year expiration
+  function saveApiKeyInCookie(apiKey) {
+      const date = new Date();
+      date.setFullYear(date.getFullYear() + 1); // Add 1 year
+      const expires = "expires=" + date.toUTCString();
+      document.cookie = `api-key=${apiKey}; ${expires}; path=/; secure; SameSite=Strict`;
+  }
+  
+  // Function to get API key from a cookie
+  function getApiKeyFromCookie() {
+      const name = 'api-key=';
+      const decodedCookie = decodeURIComponent(document.cookie);
+      const cookieArray = decodedCookie.split(';');
+      for (let i = 0; i < cookieArray.length; i++) {
+          let cookie = cookieArray[i].trim();
+          if (cookie.indexOf(name) === 0) {
+              return cookie.substring(name.length, cookie.length);
+          }
+      }
+      return "";
+  }
+  
+  // Variable to store the timeout ID
+  let typingTimer;
+  const typingDelay = 2000; // 2 seconds delay after the last keystroke
+  const apiKeyInput = document.getElementById('apiKeyInput');
+  const savedMessage = document.getElementById('savedMessage');
+  
+  // Function to show saved message with fade-in effect
+  function showSavedMessage() {
+      savedMessage.classList.add('show');
+  
+      // After 2 seconds, fade out the message
+      setTimeout(() => {
+          savedMessage.classList.remove('show');
+      }, 2000);
+  }
+  
+  // Event listener for the input box
+  apiKeyInput.addEventListener('input', function() {
+      // Clear the previous timer
+      clearTimeout(typingTimer);
+  
+      // Set a new timer
+      typingTimer = setTimeout(() => {
+          let apiKey = this.value;
+          // Save the API key in a cookie
+          saveApiKeyInCookie(apiKey);
+          // Show the saved message
+          showSavedMessage();
+      }, typingDelay);
+  });
+  
+  // Load the API key on page load
+  window.addEventListener('load', () => {
+      const savedApiKey = getApiKeyFromCookie();
+      if (savedApiKey) {
+          apiKeyInput.value = savedApiKey;
+          apiKeyInput.placeholder = 'Using Saved Key';
+      }
+  });
+  </script>
 <body>
\ No newline at end of file
diff --git a/templates/localhost:8282/upload.html b/templates/localhost:8282/upload.html
index 6f5819a3b1f8891c707c635ed1e2ed55ab301ae8..fe5ab9fd5f404b5dc1139e4648f5abb41f6f7468 100644
--- a/templates/localhost:8282/upload.html
+++ b/templates/localhost:8282/upload.html
@@ -27,7 +27,7 @@
     Privacy Policy</p>
   <ul class="links">
     <li>
-      <a href="/my_files">Your Files</a>
+      <a href="/profile">Your Profile</a>
     </li>
     <li>
       <a href="/services">Services</a>
diff --git a/templates/localhost:8282/user_profile.html b/templates/localhost:8282/user_profile.html
new file mode 100644
index 0000000000000000000000000000000000000000..8f781e307e0c340223f14b89fde7eea481e861f6
--- /dev/null
+++ b/templates/localhost:8282/user_profile.html
@@ -0,0 +1,58 @@
+{{>regular.html}}
+<title>User Profile</title>
+      <h1>
+        User Profile
+      </h1>
+      <h2>
+        User Keys
+      </h2>
+        <p> 
+          Your key has been active for <b>{{key_creation}}</b>!
+        </p>
+        <p>
+          This section shows the keys you have generated, and allows you to generate more, if allowed.<br>
+          The comment is optional, but recommended to keep track of who is who. Only you (and the server admin) can see comments.<br>
+          Ideally, one key is used per real world user of the service. However, I'm not your dad, so do whatever you like. Just don't share them publically, that's how you get the key revoked.
+        </p>
+        <br>
+        <h3>
+          Key Generator
+        </h3>
+        <br>
+          {{{key_generator}}}
+        <br>
+        <h3>
+          Generated Keys
+        </h3>
+          <br>
+          {{{key_list_html}}}
+      <h2>
+        Your Uploaded Files
+      </h2>
+      <p>
+        These are the files you have uploaded from this IP Address.<br>
+        Green = File is alive and active<br>
+        Red = File has been deleted
+      </p>
+      <link href="/static/sort.css" rel="stylesheet" />
+      <script src="/static/sort.js"></script>
+      <script>
+        function changeRowColor(button, deletionUrl) {
+            var row = button.closest('tr');
+            fetch(deletionUrl, { method: 'DELETE' })
+            .then(response => {
+                if (response.ok) {
+                    row.style.backgroundColor = '#ff00000a';
+                } else {
+                    console.error("Failed to delete the row. Status: " + response.status);
+                }
+            })
+            .catch(error => console.error("Error deleting row:", error));
+        }
+        </script>
+
+      <table class="sortable">
+        <thead><tr><th>File</th><th>Filesize</th><th>Mimetype</th><th>Views</th><th>Expiry</th><th></th></tr></thead>
+        <tbody>{{&file_list_html}}</tbody>
+      </table>
+</div></body></html>
\ No newline at end of file