Compare commits

...
Author SHA1 Message Date
Brandon Thomas 358c1efc45 add logging 2026-06-14 18:08:42 -04:00
Brandon Thomas 1dbe3d8b1e enhance database connection pooling 2026-06-14 17:26:53 -04:00
BFlat 8da68b009c Desktop create pages redesign: shared generation feed, embedded model selector, gallery folders sidebar (#1635)
## Summary

Reworks the desktop (Tauri) app's Create Image / Create Video pages to
match the webapp,
extracts the webapp's generation gallery into a shared library used by
both apps, embeds the
model selector into every prompt box, adds a folders sidebar + marquee
selection to the gallery
modal, restyles the login/pricing/credits modals to the webapp look, and
turns the Apps page
into the default landing tab.

### Shared generation-list lib

- Extracted the webapp's generation feed into
`@storyteller/ui-generation-list`: list + masonry
grid views, pending/failed/completed rows and cards, thumbnail handling,
gallery/prompt data
hooks, and the shared grid/list view store + toggle. Host-specific
actions (recreate,
make-video, share, download, toasts) inject via render props — webapp
markup unchanged, its
  old files are now thin wrappers.

### Desktop create pages

- Prompt box fixed at the bottom, `max-w-5xl` like the webapp; pages
renamed to
  "Create Image" / "Create Video".
- Generation feed: in-progress/failed from the Tauri task queue, history
from the library API,
rendered as grid or list (toggle in the TopBar next to credits). Typed
Tauri completion events
  push finished media in instantly; the poll reconciles and dedupes.
- Row/card quick actions (recreate, make video, copy link, download)
backed by a shared
  `desktopMediaActions` module extracted from the TopBar gallery wiring.
- Hero + background act as a splash that fades when the feed loads;
bottom gradient behind the
prompt box. Batch generations show "Generating N images" on the single
pending chip.
- Video prompt box now expands vertically as far as the image prompt box
(viewport-relative).

### Embedded model selector

- `ClassyModelSelector` gained `variant="embedded"`: a compact pill that
opens the webapp-style
rich-list popover while keeping the desktop's provider selection.
Embedded on all five
prompt-input pages; floating selectors removed; legacy "10 sec." badges
dropped.

### Gallery modal

- Left sidebar lists the user's folders (drag-and-drop, right-click
management, new-folder
button) and shows on the Folders tab too, highlighting the active
folder's root branch.
- Marquee (drag-rectangle) selection in view mode, with edge
auto-scroll; starts only on blank
  background so it doesn't interfere with drag-to-folder/canvas.

### Restyled modals (desktop)

- Login/signup: webapp 2-column layout (form left, Vimeo showcase
right), no Google button
(Google auth isn't wired for Tauri), `artcraft-icon` logo, eye toggles.
The dismiss button is
removed so login is required; the post-auth Discord + Success onboarding
steps are kept, as is
  the existing Tauri auth flow.
- Pricing + credits: match the webapp visually (unified monochrome plan
cards with
Most Popular / Best Value highlight frames; `#161618` credit-pack cards
with Popular badge).
All billing stays on the existing Tauri `invoke` calls — no billing
logic changed.

### Apps page / navigation

- The Apps page is now the first tab and the default landing tab, with a
Home icon. Restyled to
the webapp home: "What will you craft today?" hero, Create / Edit
sections, gradient-hover
cards sized to their content, full-width scrollbar. The TopBar
quick-menu header reads
  "Create" instead of "Generate".

### Fixes

- Video Editor no longer crashes on the desktop app (`Tooltip must be
used within
TooltipProvider`): the editor shell now mounts its own Radix
`TooltipProvider`. The webapp got
one app-wide via `SidebarProvider`; the Tauri app had none, so panel
tooltips threw.

### Misc

- `getCreatorIconPathForModelId()` added to `@storyteller/model-list`
(model id → creator icon
  path, desktop/webapp aware).
2026-06-13 17:25:40 -04:00
Brandon Thomas e4bc031fbb modernize require_user_session_extended() (#1643) 2026-06-13 17:03:21 -04:00
Brandon Thomas d937a7f78f cleanup require_user_session (#1642) 2026-06-13 05:54:01 -04:00
Brandon Thomas fef96c3835 cleanup usage of require_moderator further (#1641) 2026-06-13 05:26:36 -04:00
Brandon Thomas b9e793098c cleanup session and require_moderator (#1640) 2026-06-13 04:52:35 -04:00
Brandon Thomas 222f3e958d actix crates upgrades (#1638) 2026-06-12 21:47:36 -04:00
Brandon Thomas 4ab06030dc fix cors with PUT methods (#1637) 2026-06-12 20:02:44 -04:00
205 changed files with 6037 additions and 4669 deletions
Generated
+127 -157
View File
@@ -2,31 +2,6 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "actix"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b"
dependencies = [
"actix-macros",
"actix-rt",
"actix_derive",
"bitflags 2.11.0",
"bytes",
"crossbeam-channel",
"futures-core",
"futures-sink",
"futures-task",
"futures-util",
"log",
"once_cell",
"parking_lot 0.12.5",
"pin-project-lite",
"smallvec",
"tokio",
"tokio-util",
]
[[package]]
name = "actix-codec"
version = "0.5.2"
@@ -46,13 +21,13 @@ dependencies = [
[[package]]
name = "actix-cors"
version = "0.6.4"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b340e9cfa5b08690aae90fb61beb44e9b06f44fe3d0f93781aaa58cfba86245e"
checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d"
dependencies = [
"actix-utils",
"actix-web",
"derive_more 0.99.17",
"derive_more 2.0.1",
"futures-util",
"log",
"once_cell",
@@ -61,25 +36,25 @@ dependencies = [
[[package]]
name = "actix-http"
version = "3.7.0"
version = "3.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eb9843d84c775696c37d9a418bbb01b932629d01870722c0f13eb3f95e2536d"
checksum = "93acb4a42f64936f9b8cae4a433b237599dd6eb6ed06124eb67132ef8cc90662"
dependencies = [
"actix-codec",
"actix-rt",
"actix-service",
"actix-utils",
"ahash 0.8.12",
"base64 0.22.1",
"bitflags 2.11.0",
"brotli 6.0.0",
"brotli",
"bytes",
"bytestring",
"derive_more 0.99.17",
"derive_more 2.0.1",
"encoding_rs",
"flate2",
"foldhash 0.1.5",
"futures-core",
"h2 0.3.26",
"h2 0.3.27",
"http 0.2.11",
"httparse",
"httpdate",
@@ -89,8 +64,8 @@ dependencies = [
"mime",
"percent-encoding",
"pin-project-lite",
"rand 0.8.5",
"sha1 0.10.6",
"rand 0.10.1",
"sha1 0.11.0",
"smallvec",
"tokio",
"tokio-util",
@@ -110,14 +85,13 @@ dependencies = [
[[package]]
name = "actix-multipart"
version = "0.6.2"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d974dd6c4f78d102d057c672dcf6faa618fafa9df91d44f9c466688fc1275a3a"
checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53"
dependencies = [
"actix-multipart-derive",
"actix-utils",
"actix-web",
"bytes",
"derive_more 0.99.17",
"futures-core",
"futures-util",
@@ -136,9 +110,9 @@ dependencies = [
[[package]]
name = "actix-multipart-derive"
version = "0.6.1"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d"
checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b"
dependencies = [
"darling 0.20.10",
"parse-size",
@@ -149,9 +123,9 @@ dependencies = [
[[package]]
name = "actix-router"
version = "0.5.3"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8"
checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7"
dependencies = [
"bytestring",
"cfg-if",
@@ -164,9 +138,9 @@ dependencies = [
[[package]]
name = "actix-rt"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208"
checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63"
dependencies = [
"actix-macros",
"futures-core",
@@ -175,16 +149,16 @@ dependencies = [
[[package]]
name = "actix-server"
version = "2.4.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b02303ce8d4e8be5b855af6cf3c3a08f3eff26880faad82bab679c22d3650cb5"
checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502"
dependencies = [
"actix-rt",
"actix-service",
"actix-utils",
"futures-core",
"futures-util",
"mio 0.8.11",
"mio",
"socket2 0.5.10",
"tokio",
"tracing",
@@ -212,9 +186,9 @@ dependencies = [
[[package]]
name = "actix-web"
version = "4.7.0"
version = "4.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6316df3fa569627c98b12557a8b6ff0674e5be4bb9b5e4ae2550ddb4964ed6"
checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf"
dependencies = [
"actix-codec",
"actix-http",
@@ -225,15 +199,16 @@ dependencies = [
"actix-service",
"actix-utils",
"actix-web-codegen",
"ahash 0.8.12",
"bytes",
"bytestring",
"cfg-if",
"cookie 0.16.2",
"derive_more 0.99.17",
"derive_more 2.0.1",
"encoding_rs",
"foldhash 0.1.5",
"futures-core",
"futures-util",
"impl-more 0.1.6",
"itoa 1.0.11",
"language-tags",
"log",
@@ -246,29 +221,12 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"smallvec",
"socket2 0.5.10",
"socket2 0.6.2",
"time",
"tracing",
"url",
]
[[package]]
name = "actix-web-actors"
version = "4.3.1+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98c5300b38fd004fe7d2a964f9a90813fdbe8a81fed500587e78b1b71c6f980"
dependencies = [
"actix",
"actix-codec",
"actix-http",
"actix-web",
"bytes",
"bytestring",
"futures-core",
"pin-project-lite",
"tokio",
"tokio-util",
]
[[package]]
name = "actix-web-codegen"
version = "4.3.0"
@@ -283,9 +241,9 @@ dependencies = [
[[package]]
name = "actix-web-lab"
version = "0.20.2"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7675c1a84eec1b179c844cdea8488e3e409d8e4984026e92fa96c87dd86f33c6"
checksum = "1a89606b86fd2f8c0ddd72ef0be171e58c919ee0b6914aa7b52354063e5ee361"
dependencies = [
"actix-http",
"actix-router",
@@ -295,37 +253,36 @@ dependencies = [
"actix-web-lab-derive",
"ahash 0.8.12",
"arc-swap",
"async-trait",
"bytes",
"bytestring",
"csv",
"derive_more 0.99.17",
"derive_more 2.0.1",
"form_urlencoded",
"futures-core",
"futures-util",
"http 0.2.11",
"impl-more",
"itertools 0.12.1",
"impl-more 0.3.1",
"itertools 0.14.0",
"local-channel",
"mediatype",
"mime",
"once_cell",
"pin-project-lite",
"regex",
"serde",
"serde_html_form",
"serde_json",
"serde_path_to_error",
"tokio",
"tokio-stream",
"tracing",
"url",
]
[[package]]
name = "actix-web-lab-derive"
version = "0.20.0"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aa0b287c8de4a76b691f29dbb5451e8dd5b79d777eaf87350c9b0cbfdb5e968"
checksum = "2ac4d6e04e97fe707286509b4f338e99c5fb7249c770e1da074af5e27faa96b3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
@@ -363,17 +320,6 @@ dependencies = [
"workspace-build-acceleration",
]
[[package]]
name = "actix_derive"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c7db3d5a9718568e4cf4a537cfd7070e6e6ff7481510d0237fb529ac850f6d3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "actix_helpers"
version = "0.0.1"
@@ -405,7 +351,7 @@ checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8"
dependencies = [
"cfg-if",
"cipher 0.3.0",
"cpufeatures",
"cpufeatures 0.2.12",
"opaque-debug",
]
@@ -417,7 +363,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher 0.4.4",
"cpufeatures",
"cpufeatures 0.2.12",
]
[[package]]
@@ -893,7 +839,7 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8"
dependencies = [
"brotli 8.0.2",
"brotli",
"flate2",
"futures-core",
"memchr",
@@ -1777,17 +1723,6 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "brotli"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor 4.0.1",
]
[[package]]
name = "brotli"
version = "8.0.2"
@@ -1796,17 +1731,7 @@ checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor 5.0.0",
]
[[package]]
name = "brotli-decompressor"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
@@ -2155,6 +2080,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.1",
]
[[package]]
name = "chrono"
version = "0.4.44"
@@ -2733,6 +2669,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.0.1"
@@ -4686,6 +4631,7 @@ dependencies = [
"js-sys",
"libc",
"r-efi",
"rand_core 0.10.1",
"wasip2",
"wasip3",
"wasm-bindgen",
@@ -5062,9 +5008,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.26"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
dependencies = [
"bytes",
"fnv",
@@ -5503,7 +5449,7 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
"h2 0.3.26",
"h2 0.3.27",
"http 0.2.11",
"http-body 0.4.6",
"httparse",
@@ -5888,6 +5834,12 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d"
[[package]]
name = "impl-more"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35a84fd5aa25fae5c0f4a33d9cac2ca017fc622cbd089be2229993514990f870"
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -6102,6 +6054,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "0.4.8"
@@ -6349,7 +6310,7 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
dependencies = [
"cpufeatures",
"cpufeatures 0.2.12",
]
[[package]]
@@ -6983,12 +6944,6 @@ dependencies = [
"workspace-build-acceleration",
]
[[package]]
name = "mediatype"
version = "0.19.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf0bc9784973713e4a90d515a4302991ca125a7c4516951cb607f2298cb757e5"
[[package]]
name = "memchr"
version = "2.7.4"
@@ -7189,18 +7144,6 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
[[package]]
name = "mio"
version = "1.0.2"
@@ -7209,6 +7152,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [
"hermit-abi 0.3.9",
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
@@ -9268,6 +9212,17 @@ dependencies = [
"rand_core 0.9.3",
]
[[package]]
name = "rand"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20",
"getrandom 0.4.1",
"rand_core 0.10.1",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
@@ -9340,6 +9295,12 @@ dependencies = [
"getrandom 0.3.1",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_hc"
version = "0.2.0"
@@ -9661,7 +9622,7 @@ dependencies = [
"encoding_rs",
"futures-core",
"futures-util",
"h2 0.3.26",
"h2 0.3.27",
"http 0.2.11",
"http-body 0.4.6",
"hyper 0.14.28",
@@ -10685,15 +10646,15 @@ dependencies = [
[[package]]
name = "serde_html_form"
version = "0.2.3"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224e6a14f315852940f3ec103125aa6482f0e224732ed91ed3330ed633077c34"
checksum = "0946d52b4b7e28823148aebbeceb901012c595ad737920d504fa8634bb099e6f"
dependencies = [
"form_urlencoded",
"indexmap 2.13.0",
"itoa 1.0.11",
"ryu",
"serde",
"serde_core",
]
[[package]]
@@ -10957,10 +10918,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.12",
"digest 0.10.7",
]
[[package]]
name = "sha1"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"digest 0.11.2",
]
[[package]]
name = "sha1_smol"
version = "1.0.0"
@@ -10975,7 +10947,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [
"block-buffer 0.9.0",
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.12",
"digest 0.9.0",
"opaque-debug",
]
@@ -10987,7 +10959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.12",
"digest 0.10.7",
]
@@ -10998,7 +10970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.12",
"digest 0.11.2",
]
@@ -11519,12 +11491,10 @@ checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e"
name = "storyteller-web"
version = "0.0.1"
dependencies = [
"actix",
"actix-http",
"actix-multipart",
"actix-service",
"actix-web",
"actix-web-actors",
"actix-web-lab",
"actix_artcraft",
"actix_cors_configs",
@@ -12370,7 +12340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a"
dependencies = [
"base64 0.22.1",
"brotli 8.0.2",
"brotli",
"ico",
"json-patch",
"plist",
@@ -12606,7 +12576,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212"
dependencies = [
"anyhow",
"brotli 8.0.2",
"brotli",
"cargo_metadata",
"ctor",
"dunce",
@@ -12894,7 +12864,7 @@ checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
dependencies = [
"bytes",
"libc",
"mio 1.0.2",
"mio",
"parking_lot 0.12.5",
"pin-project-lite",
"signal-hook-registry",
@@ -12968,9 +12938,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.16"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -13164,7 +13134,7 @@ dependencies = [
"axum",
"base64 0.21.7",
"bytes",
"h2 0.3.26",
"h2 0.3.27",
"http 0.2.11",
"http-body 0.4.6",
"hyper 0.14.28",
@@ -15269,7 +15239,7 @@ checksum = "f79937f6c4df65b3f6f78715b9de2977afe9ee3b3436483c7949a24511e25935"
dependencies = [
"ahash 0.8.12",
"boring2",
"brotli 8.0.2",
"brotli",
"bytes",
"cookie 0.18.1",
"flate2",
+6 -8
View File
@@ -295,16 +295,14 @@ worldlabs_consumer_client = { path = "crates/api_clients/worldlabs_consumer_clie
# External libs
# Centrally pinned versions so that we can uniformly upgrade/downgrade versions across the workspace
# If a crate needs to use an older/newer version, the dependency should be managed in that crate's own Cargo.toml
actix = "0.13.5"
actix-cors = "0.6.4" # TODO(bt): Carefully upgrade this. CORS broke last time.
actix-http = "3.7.0"
actix-multipart = "0.6.2"
actix-rt = "2.10.0"
actix-cors = "0.7.1"
actix-http = "3.12.1"
actix-multipart = "0.7.2"
actix-rt = "2.11.0"
actix-service = "2.0.3"
actix-utils = "3.0.1"
actix-web = "4.7.0" # TODO(bt): We're behind and need to bump versions.
actix-web-actors = "4.3.1+deprecated"
actix-web-lab = "0.20.2"
actix-web = "4.13.0"
actix-web-lab = "0.26.0"
anyhow = "1.0.102"
assertor = "0.0.3"
base64 = "0.22.1"
+66 -1
View File
@@ -65,7 +65,13 @@ fn do_build_cors_config(is_production: bool) -> Cors {
}
// Remaining setup
cors.allowed_methods(vec!["GET", "POST", "OPTIONS", "DELETE"])
//
// NB: actix-cors 0.7 flipped the `block_on_origin_mismatch` default to
// false (mismatched origins get a 200 without CORS headers and the browser
// enforces the block). We keep the long-standing 0.6 behavior of rejecting
// mismatched origins server-side with a 400.
cors.block_on_origin_mismatch(true)
.allowed_methods(vec!["GET", "POST", "PUT", "OPTIONS", "DELETE"])
.supports_credentials()
.allowed_headers(vec![
actix_http::header::ACCEPT,
@@ -82,11 +88,70 @@ fn do_build_cors_config(is_production: bool) -> Cors {
mod tests {
use server_environment::ServerEnvironment;
use crate::testing::assert_no_origin_header_ok;
use crate::testing::assert_origin_invalid;
use crate::testing::assert_origin_ok;
use crate::testing::assert_preflight_method_invalid;
use crate::testing::assert_preflight_method_ok;
use super::build_cors_config;
/// Every production origin that hosts a product (or IS a backend that
/// serves same-origin pages like swagger) must be allowed. This is the
/// guard against `block_on_origin_mismatch(true)` locking out a real
/// frontend after a CORS-crate upgrade.
#[actix_rt::test]
async fn test_production_product_and_backend_origins_are_allowed() {
let production_cors = build_cors_config(ServerEnvironment::Production);
let origins = [
// Backends (same-origin browser requests still carry an Origin header).
"https://api.fakeyou.com",
"https://api.storyteller.ai",
// Products
"https://fakeyou.com",
"https://storyteller.ai",
"https://app.getartcraft.com",
"https://getartcraft.com",
"https://www.getartcraft.com",
"https://artcraft.ai",
"https://www.artcraft.ai",
"https://artcraft-dashboard.netlify.app",
];
for origin in origins {
assert_origin_ok(&production_cors, origin).await;
}
}
/// Non-browser clients (curl, python API clients, server-to-server) send
/// no Origin header and must never be blocked, even with
/// `block_on_origin_mismatch(true)`.
#[actix_rt::test]
async fn test_requests_without_origin_header_are_not_blocked() {
let production_cors = build_cors_config(ServerEnvironment::Production);
assert_no_origin_header_ok(&production_cors).await;
}
#[actix_rt::test]
async fn test_preflight_allows_every_supported_method() {
let production_cors = build_cors_config(ServerEnvironment::Production);
// PUT is used by e.g. the folders routes (rename/star/color_code/cover_image).
for method in ["GET", "POST", "PUT", "DELETE"] {
assert_preflight_method_ok(&production_cors, "https://app.getartcraft.com", method).await;
assert_preflight_method_ok(&production_cors, "https://storyteller.ai", method).await;
}
}
#[actix_rt::test]
async fn test_preflight_rejects_unsupported_methods() {
let production_cors = build_cors_config(ServerEnvironment::Production);
for method in ["PATCH", "TRACE", "CONNECT"] {
assert_preflight_method_invalid(&production_cors, "https://app.getartcraft.com", method).await;
}
}
#[actix_rt::test]
async fn test_fakeyou_production() {
let production_cors = build_cors_config(ServerEnvironment::Production);
@@ -2,6 +2,7 @@ use actix_cors::Cors;
use actix_http::body::{BoxBody, EitherBody};
use actix_http::StatusCode;
use actix_web::dev::{ServiceResponse, Transform};
use actix_web::http::Method;
use actix_web::test;
use actix_web::test::TestRequest;
use speculoos::asserting;
@@ -20,6 +21,39 @@ pub (crate) async fn assert_origin_invalid(cors: &Cors, hostname: &str) {
.is_equal_to(StatusCode::BAD_REQUEST);
}
/// Assert that a CORS preflight (OPTIONS + Access-Control-Request-Method)
/// for the given method is accepted from the given origin.
pub (crate) async fn assert_preflight_method_ok(cors: &Cors, hostname: &str, method: &str) {
let response = make_preflight_request(cors, hostname, method).await;
asserting(&format!("Preflight for {} from {} is allowed", method, hostname))
.that(&response.status())
.is_equal_to(StatusCode::OK);
}
/// Assert that a CORS preflight for the given method is rejected.
pub (crate) async fn assert_preflight_method_invalid(cors: &Cors, hostname: &str, method: &str) {
let response = make_preflight_request(cors, hostname, method).await;
asserting(&format!("Preflight for {} from {} is rejected", method, hostname))
.that(&response.status())
.is_equal_to(StatusCode::BAD_REQUEST);
}
/// Assert that a request WITHOUT an Origin header (curl, server-to-server,
/// native API clients) is never blocked by CORS, regardless of
/// `block_on_origin_mismatch`.
pub (crate) async fn assert_no_origin_header_ok(cors: &Cors) {
let cors = cors.new_transform(test::ok_service())
.await
.unwrap();
let request = TestRequest::default().to_srv_request();
let response = test::call_service(&cors, request).await;
asserting("Requests without an Origin header are not blocked")
.that(&response.status())
.is_equal_to(StatusCode::OK);
}
async fn make_test_request(cors: &Cors, hostname: &str) -> ServiceResponse<EitherBody<BoxBody>> {
let cors= cors.new_transform(test::ok_service())
.await
@@ -31,3 +65,21 @@ async fn make_test_request(cors: &Cors, hostname: &str) -> ServiceResponse<Eithe
test::call_service(&cors, request).await
}
async fn make_preflight_request(
cors: &Cors,
hostname: &str,
method: &str,
) -> ServiceResponse<EitherBody<BoxBody>> {
let cors = cors.new_transform(test::ok_service())
.await
.unwrap();
let request = TestRequest::default()
.method(Method::OPTIONS)
.insert_header(("Origin", hostname))
.insert_header(("Access-Control-Request-Method", method))
.to_srv_request();
test::call_service(&cors, request).await
}
@@ -84,9 +84,8 @@ pub struct SessionUserRecord {
}
impl SessionUserRecord {
// TODO(bt, 2022-12-20): Convert all users of the bare record to using `UserToken`, then get rid of this method.
pub fn get_strongly_typed_user_token(&self) -> UserToken {
self.user_token.clone()
pub fn get_user_token(&self) -> &UserToken {
&self.user_token
}
pub fn is_mod(&self) -> bool {
@@ -1,8 +1,7 @@
use chrono::{DateTime, Utc};
use enums::common::payments_namespace::PaymentsNamespace;
use errors::AnyhowResult;
use sqlx::pool::PoolConnection;
use sqlx::MySql;
use sqlx::{Executor, MySql};
pub struct ActiveUserSubscription {
pub user_token: String,
@@ -19,10 +18,12 @@ pub struct ActiveUserSubscription {
pub subscription_expires_at: DateTime<Utc>,
}
pub async fn list_active_user_subscriptions(
mysql_connection: &mut PoolConnection<MySql>,
pub async fn list_active_user_subscriptions<'e, 'c : 'e, E>(
mysql_executor: E,
user_token: &str
) -> AnyhowResult<Vec<ActiveUserSubscription>> {
) -> AnyhowResult<Vec<ActiveUserSubscription>>
where E: 'e + Executor<'c, Database = MySql>
{
// NB: "status=incomplete" subscriptions can happen when a user submits credit card info,
// but the CRC is wrong. Eg. what happened with ftx on 2022.11.18.
//
@@ -45,7 +46,7 @@ WHERE
"#,
user_token,
)
.fetch_all(&mut **mysql_connection)
.fetch_all(mysql_executor)
.await?;
let records = records.into_iter()
@@ -162,12 +162,10 @@ tempfile.workspace = true
# https://github.com/durch/rust-s3/pull/254
rust-s3.workspace = true
actix.workspace = true
actix-http.workspace = true
actix-multipart.workspace = true
actix-service.workspace = true
actix-web.workspace = true
actix-web-actors.workspace = true
actix-web-lab.workspace = true
# Crypto
@@ -1,15 +1,36 @@
use std::time::Duration;
use log::info;
use errors::AnyhowResult;
use shared_env_var_config::mysql::env_get_mysql_connection_string_or_default;
use sqlx::mysql::MySqlPoolOptions;
use sqlx::MySqlPool;
pub async fn connect_to_database() -> AnyhowResult<MySqlPool>{
pub async fn connect_to_database() -> AnyhowResult<MySqlPool> {
let db_connection_string = env_get_mysql_connection_string_or_default();
let pool_options = MySqlPoolOptions::new()
.max_connections(easyenv::get_env_num("MYSQL_MAX_CONNECTIONS", 5)?);
// NB: The managed DB's connection ceiling is far above what we use, so the binding
// constraint is this per-pod pool. Keep `replicas * MYSQL_MAX_CONNECTIONS` (plus the
// background jobs' small pools) comfortably under the server's `max_connections`.
let max_connections = easyenv::get_env_num("MYSQL_MAX_CONNECTIONS", 20)?;
let min_connections = easyenv::get_env_num("MYSQL_MIN_CONNECTIONS", 0)?;
// TODO(bt,2024-09-17): Configure logging, etc. after we've upgraded sqlx
// NB: Fail fast under pool saturation rather than stacking up on sqlx's 30s default — a
// shorter timeout sheds load and surfaces the problem instead of hiding it behind long waits.
let acquire_timeout_seconds = easyenv::get_env_num::<u64>("MYSQL_ACQUIRE_TIMEOUT_SECONDS", 10)?;
// NB: Log the settings before building/connecting so a connect failure has context. We do NOT
// log the connection string (it contains credentials).
info!(
"Building MySQL pool: max_connections={}, min_connections={}, acquire_timeout_seconds={}",
max_connections, min_connections, acquire_timeout_seconds,
);
let pool_options = MySqlPoolOptions::new()
.max_connections(max_connections)
.min_connections(min_connections)
.acquire_timeout(Duration::from_secs(acquire_timeout_seconds));
let pool = pool_options
.connect(&db_connection_string)
@@ -2,7 +2,6 @@ use std::fmt::{Display, Formatter};
use std::sync::Arc;
use crate::http_server::session::session_checker_error::SessionCheckerError;
use crate::http_server::web_utils::user_session::require_user_session::RequireUserSessionError;
use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_payload_error::AvtCookiePayloadError;
use actix_artcraft::sessions::user_sessions::http_user_session_payload_error::HttpUserSessionPayloadError;
use actix_http::StatusCode;
@@ -19,7 +18,7 @@ use serde::Serialize;
/// ```ignore
/// pub async fn my_handler(...) -> Result<Json<MyResponse>, CommonWebError> {
/// // sqlx errors, anyhow errors, session errors — all convert via ?
/// let user = require_user_session_using_connection(&req, ...)?;
/// let user = require_user_session(&req, ...)?;
/// let data = some_db_query(&pool).await?;
///
/// if data.is_none() {
@@ -317,15 +316,6 @@ impl From<serde_json::Error> for CommonWebError {
}
}
impl From<RequireUserSessionError> for CommonWebError {
fn from(value: RequireUserSessionError) -> Self {
match value {
RequireUserSessionError::NotAuthorized => Self::NotAuthorized,
RequireUserSessionError::ServerError => Self::from_error(value),
}
}
}
impl From<HttpUserSessionPayloadError> for CommonWebError {
fn from(err: HttpUserSessionPayloadError) -> Self {
if err.is_server_error() {
@@ -545,22 +535,6 @@ mod tests {
assert!(source.is_some());
}
#[test]
fn require_user_session_not_authorized_maps_to_401() {
let error: CommonWebError = RequireUserSessionError::NotAuthorized.into();
assert_eq!(error.status_code(), StatusCode::UNAUTHORIZED);
assert!(!error.is_server_error());
}
#[test]
fn require_user_session_server_error_wraps_cause() {
let error: CommonWebError = RequireUserSessionError::ServerError.into();
assert_eq!(error.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
assert!(error.is_server_error());
assert!(error.cause().is_some());
assert!(format!("{}", error.cause().unwrap()).contains("ServerError"));
}
#[test]
fn serde_json_error_converts_to_500() {
let bad_json = "not json at all{{{";
@@ -120,7 +120,8 @@ pub async fn create_scene_handler(
let ip_address = get_request_ip(&http_request);
let maybe_user_token = maybe_user_session
.map(|session| session.get_strongly_typed_user_token());
.as_ref()
.map(|session| session.get_user_token());
// ==================== FILE DATA ==================== //
@@ -166,7 +167,7 @@ pub async fn create_scene_handler(
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
maybe_media_class: Some(MediaFileClass::Dimensional),
media_file_type: MediaFileType::SceneRon,
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
@@ -30,7 +30,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
let mut field_name = None;
let mut field_filename = None;
if let content_disposition = field.content_disposition() {
if let Some(content_disposition) = field.content_disposition() {
field_name = content_disposition.get_name()
.map(|s| s.to_string());
field_filename = content_disposition.get_filename() // NB: Only used for the file bytes.
@@ -127,7 +127,8 @@ pub async fn process_upload_media_file(
let ip_address = get_request_ip(&http_request);
let maybe_user_token = maybe_user_session
.map(|session| session.get_strongly_typed_user_token());
.as_ref()
.map(|session| session.get_user_token());
let maybe_file_size_bytes = upload_media_request.file_bytes
.as_ref()
@@ -325,7 +326,7 @@ pub async fn process_upload_media_file(
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
maybe_media_class: None,
media_file_type,
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility,
@@ -36,7 +36,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
let mut field_name = None;
let mut field_filename = None;
if let content_disposition = field.content_disposition() {
if let Some(content_disposition) = field.content_disposition() {
field_name = content_disposition.get_name()
.map(|s| s.to_string());
field_filename = content_disposition.get_filename() // NB: Only used for the file bytes.
@@ -129,7 +129,8 @@ pub async fn handle_upload(
let ip_address = get_request_ip(&http_request);
let maybe_user_token = maybe_user_session
.map(|session| session.get_strongly_typed_user_token());
.as_ref()
.map(|session| session.get_user_token());
let maybe_file_size_bytes = upload_media_request.file_bytes
.as_ref()
@@ -300,7 +301,7 @@ pub async fn handle_upload(
checksum_sha2: &hash,
public_upload_path: &public_upload_path,
maybe_extra_file_modification_info: None,
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: None,
creator_ip_address: &ip_address,
creator_set_visibility,
@@ -317,12 +318,12 @@ pub async fn handle_upload(
let firehose_result = match media_upload_source {
MediaUploadSource::DeviceApi => {
server_state.firehose_publisher.publish_device_media_recorded(
maybe_user_token.as_ref(),
maybe_user_token,
&token).await
}
_ => {
server_state.firehose_publisher.publish_media_uploaded(
maybe_user_token.as_ref(),
maybe_user_token,
&token).await
}
};
@@ -28,8 +28,7 @@ use crate::http_server::requests::request_headers::get_routing_tag_header::get_r
use crate::http_server::requests::request_headers::has_debug_header::has_debug_header;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::http_server::web_utils::user_session::require_user_session::RequireUserSessionError;
use crate::http_server::web_utils::user_session::require_user_session_extended_using_connection::require_user_session_extended_using_connection;
use crate::http_server::web_utils::user_session::require_user_session_extended::require_user_session_extended;
use crate::state::server_state::ServerState;
use crate::util::cleaners::empty_media_file_token_to_null::empty_media_file_token_to_null;
@@ -114,15 +113,10 @@ pub async fn enqueue_face_fusion_workflow_handler(
let maybe_avt_token = server_state.avt_cookie_manager
.get_avt_token_from_request(&http_request);
let user_session = require_user_session_extended_using_connection(
let user_session = require_user_session_extended(
&http_request,
&server_state.session_checker,
&mut mysql_connection)
.await
.map_err(|err| match err {
RequireUserSessionError::ServerError => CommonWebError::from_error(err),
RequireUserSessionError::NotAuthorized => CommonWebError::NotAuthorized,
})?;
&mut *mysql_connection).await?;
// ==================== PAID PLAN + PRIORITY ==================== //
@@ -29,8 +29,7 @@ use crate::http_server::requests::request_headers::get_routing_tag_header::get_r
use crate::http_server::requests::request_headers::has_debug_header::has_debug_header;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::http_server::web_utils::user_session::require_user_session::RequireUserSessionError;
use crate::http_server::web_utils::user_session::require_user_session_extended_using_connection::require_user_session_extended_using_connection;
use crate::http_server::web_utils::user_session::require_user_session_extended::require_user_session_extended;
use crate::state::server_state::ServerState;
use crate::util::cleaners::empty_media_file_token_to_null::empty_media_file_token_to_null;
@@ -122,15 +121,10 @@ pub async fn enqueue_live_portrait_workflow_handler(
let maybe_avt_token = server_state.avt_cookie_manager
.get_avt_token_from_request(&http_request);
let user_session = require_user_session_extended_using_connection(
let user_session = require_user_session_extended(
&http_request,
&server_state.session_checker,
&mut mysql_connection)
.await
.map_err(|err| match err {
RequireUserSessionError::ServerError => CommonWebError::from_error(err),
RequireUserSessionError::NotAuthorized => CommonWebError::NotAuthorized,
})?;
&mut *mysql_connection).await?;
// ==================== PAID PLAN + PRIORITY ==================== //
@@ -1,272 +0,0 @@
use std::fmt;
use std::sync::Arc;
use std::time::Instant;
use actix::*;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::{Json, Path};
use actix_web::{
web, App, Error, HttpRequest, HttpResponse, HttpServer, Responder,
};
use actix_web_actors::ws;
use chrono::{DateTime, Utc};
use log::error;
use redis::{Commands, RedisResult};
use utoipa::ToSchema;
use web::Data;
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use enums::common::job_status_plus::JobStatusPlus;
use mysql_queries::queries::generic_inference::web::get_inference_job_status::get_inference_job_status;
use mysql_queries::queries::generic_inference::web::job_status::GenericInferenceJobStatus;
use redis_schema::keys::inference_job::style_transfer_progress_key::StyleTransferProgressKey;
use redis_schema::payloads::inference_job::style_transfer_progress_state::InferenceProgressDetailsResponse;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use crate::http_server::deprecated_endpoints::workflows::enqueue::{progress_tracker_server, progress_tracker_session};
use crate::http_server::endpoints::inference_job::utils::estimates::estimate_job_progress::estimate_job_progress;
use crate::http_server::endpoints::inference_job::utils::extractors::extract_polymorphic_inference_args::extract_polymorphic_inference_args;
use crate::http_server::web_utils::filter_model_name::maybe_filter_model_name;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
/// For certain jobs or job classes (eg. non-premium), we kill the jobs if the user hasn't
/// maintained a keepalive. This prevents wasted work when users who are unlikely to return
/// navigate away. Premium users have accounts and can always return to the site, so they
/// typically do not require keepalive.
const JOB_KEEPALIVE_TTL_SECONDS : usize = 60 * 3;
/// For the URL PathInfo
#[derive(Deserialize, ToSchema)]
pub struct GetInferenceJobStatusPathInfo {
token: InferenceJobToken,
}
#[derive(Serialize, ToSchema)]
pub struct GetInferenceJobStatusSuccessResponse {
pub success: bool,
pub state: InferenceJobStatusResponsePayload,
}
#[derive(Serialize, ToSchema)]
pub struct InferenceJobStatusResponsePayload {
pub job_token: InferenceJobToken,
pub request: RequestDetailsResponse,
pub status: StatusDetailsResponse,
pub maybe_result: Option<InferenceProgressDetailsResponse>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// Details about what the user requested for generation
#[derive(Serialize, ToSchema)]
pub struct RequestDetailsResponse {
}
/// Details about the ongoing job status
#[derive(Serialize, ToSchema)]
pub struct StatusDetailsResponse {
/// Primary status from the database (a state machine).
pub status: JobStatusPlus,
/// Extra, temporary status from Redis.
/// This can denote inference progress, and the Python code can write to it.
pub maybe_extra_status_description: Option<String>,
pub maybe_assigned_worker: Option<String>,
pub maybe_assigned_cluster: Option<String>,
pub maybe_first_started_at: Option<DateTime<Utc>>,
pub attempt_count: u8,
/// Whether the frontend needs to maintain a keepalive check.
/// This is typically only for non-premium users.
pub requires_keepalive: bool,
/// An enum the frontend can use to display localized/I18N error
/// messages. These pertain to both transient and permanent failures.
pub maybe_failure_category: Option<FrontendFailureCategory>,
/// This is an integer number between 0 and 100 (both inclusive) that
/// reports the completeness.
pub progress_percentage: u8,
}
// #[derive(Serialize, ToSchema)]
// pub struct InferenceStageDetails {
// pub stage_progress: u8,
// pub expected_frame_count: u8,
// pub stage_complete: bool,
// pub frames: Vec<Url>,
// }
/// Details about the completed result (if any)
// #[derive(Serialize, ToSchema)]
// pub struct InferenceProgressDetailsResponse {
// pub expected_stages: u8,
// pub currently_active_stage: u8,
// pub per_stage_frame_count: u8,
// pub stages: Vec<InferenceStageDetails>,
// }
#[derive(Debug, ToSchema)]
pub enum GetInferenceJobStatusError {
ServerError,
NotFound,
}
impl ResponseError for GetInferenceJobStatusError {
fn status_code(&self) -> StatusCode {
match *self {
GetInferenceJobStatusError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
GetInferenceJobStatusError::NotFound => StatusCode::NOT_FOUND,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
Self::ServerError => "server error".to_string(),
Self::NotFound => "not found".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl fmt::Display for GetInferenceJobStatusError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
pub async fn get_inference_preview_status_handler_ws(
http_request: HttpRequest,
path: Path<GetInferenceJobStatusPathInfo>,
server_state: Data<Arc<ServerState>>) -> Result<Json<GetInferenceJobStatusSuccessResponse>, GetInferenceJobStatusError>
{
if path.token.as_str().trim() == "None" {
// NB: A bunch of Python clients use our API and can fail in this manner.
// This was a large traffic driver during the 2023-03-08 outage.
// Presumably it's this client: https://github.com/shards-7/fakeyou.py
return Err(GetInferenceJobStatusError::NotFound);
}
// NB: Since this is publicly exposed, we don't query sensitive data.
let maybe_status = get_inference_job_status(
&path.token,
&server_state.mysql_pool
).await;
let record = match maybe_status {
Ok(Some(record)) => record,
Ok(None) => return Err(GetInferenceJobStatusError::NotFound),
Err(err) => {
error!("tts job query error: {:?}", err);
return Err(GetInferenceJobStatusError::ServerError);
}
};
let mut redis = server_state.redis_pool
.get()
.map_err(|e| {
error!("redis error: {:?}", e);
GetInferenceJobStatusError::ServerError
})?;
let progress_key = StyleTransferProgressKey::new_for_job_id(record.job_token.clone());
let progress_value: RedisResult<Option<String>> = redis.get(progress_key.to_string());
let maybe_result = match progress_value {
Ok(Some(value)) => {
let progress_details: InferenceProgressDetailsResponse = serde_json::from_str(&value)
.map_err(|e| {
error!("redis error: {:?}", e);
GetInferenceJobStatusError::ServerError
})?;
Some(progress_details)
},
Ok(None) => None,
Err(e) => {
error!("redis error: {:?}", e);
return Err(GetInferenceJobStatusError::ServerError);
}
};
Ok(Json(GetInferenceJobStatusSuccessResponse {
success: true,
state: record_to_payload(record, maybe_result, None),
}))
}
fn record_to_payload(
record: GenericInferenceJobStatus,
progress_details: Option<InferenceProgressDetailsResponse>,
maybe_extra_status_description: Option<String>,
) -> InferenceJobStatusResponsePayload {
let inference_category = record.request_details.inference_category;
// NB: Fail open. We don't want to fail the request if we can't extract the args.
let maybe_polymorphic_args = extract_polymorphic_inference_args(&record)
.ok()
.flatten();
let progress_percentage = estimate_job_progress(&record, maybe_polymorphic_args.as_ref());
InferenceJobStatusResponsePayload {
job_token: record.job_token,
request: RequestDetailsResponse {
// inference_category: record.request_details.inference_category,
// maybe_model_type: maybe_filter_model_name(record.request_details.maybe_model_type.as_deref()),
// maybe_model_token: record.request_details.maybe_model_token,
// maybe_model_title: record.request_details.maybe_model_title,
// maybe_raw_inference_text: record.request_details.maybe_raw_inference_text,
// maybe_style_name: record.request_details.maybe_style_name,
},
status: StatusDetailsResponse {
status: record.status,
maybe_extra_status_description,
maybe_assigned_worker: maybe_filter_model_name(record.maybe_assigned_worker.as_deref()),
maybe_assigned_cluster: record.maybe_assigned_cluster,
maybe_first_started_at: record.maybe_first_started_at,
attempt_count: record.attempt_count as u8,
requires_keepalive: record.is_keepalive_required,
maybe_failure_category: record.maybe_frontend_failure_category,
progress_percentage,
},
maybe_result: progress_details,
created_at: record.created_at,
updated_at: record.updated_at,
}
}
pub struct ProgressTrackerState {
}
pub async fn ws_progress_route(
req: HttpRequest,
stream: web::Payload,
srv: web::Data<Addr<progress_tracker_server::ChatServer>>,
) -> Result<HttpResponse, Error> {
ws::start(
progress_tracker_session::WsChatSession {
id: 0,
hb: Instant::now(),
room: "main".to_owned(),
name: None,
addr: srv.get_ref().clone(),
},
&req,
stream,
)
}
@@ -3,7 +3,4 @@ pub mod enqueue_live_portrait_workflow_handler;
pub mod enqueue_studio_workflow_handler;
pub mod enqueue_video_style_transfer_workflow_handler;
pub mod get_inference_preview_status;
pub mod get_inference_preview_status_ws;
pub mod progress_tracker_server;
pub mod progress_tracker_session;
pub mod vst_common;
@@ -1,216 +0,0 @@
//! `ChatServer` is an actor. It maintains a list of connection client sessions.
//! It also manages available rooms. Peers send messages to other peers in same
//! room through `ChatServer`.
use std::{
collections::{HashMap, HashSet},
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use actix::prelude::*;
use log::info;
use rand::{rngs::ThreadRng, Rng};
/// Chat server sends this messages to session
#[derive(Message)]
#[rtype(result = "()")]
pub struct Message(pub String);
/// Message for chat server communications
/// New chat session is created
#[derive(Message)]
#[rtype(usize)]
pub struct Connect {
pub addr: Recipient<Message>,
}
/// Session is disconnected
#[derive(Message)]
#[rtype(result = "()")]
pub struct Disconnect {
pub id: usize,
}
/// Send message to specific room
#[derive(Message)]
#[rtype(result = "()")]
pub struct ClientMessage {
/// Id of the client session
pub id: usize,
/// Peer message
pub msg: String,
/// Room name
pub room: String,
}
/// List of available rooms
pub struct ListRooms;
impl actix::Message for ListRooms {
type Result = Vec<String>;
}
/// Join room, if room does not exists create new one.
#[derive(Message)]
#[rtype(result = "()")]
pub struct Join {
/// Client ID
pub id: usize,
/// Room name
pub name: String,
}
/// `ChatServer` manages chat rooms and responsible for coordinating chat session.
///
/// Implementation is very naïve.
#[derive(Debug)]
pub struct ChatServer {
sessions: HashMap<usize, Recipient<Message>>,
rooms: HashMap<String, HashSet<usize>>,
rng: ThreadRng,
visitor_count: Arc<AtomicUsize>,
}
impl ChatServer {
pub fn new(visitor_count: Arc<AtomicUsize>) -> ChatServer {
// default room
let mut rooms = HashMap::new();
rooms.insert("main".to_owned(), HashSet::new());
ChatServer {
sessions: HashMap::new(),
rooms,
rng: rand::thread_rng(),
visitor_count,
}
}
}
impl ChatServer {
/// Send message to all users in the room
fn send_message(&self, room: &str, message: &str, skip_id: usize) {
if let Some(sessions) = self.rooms.get(room) {
for id in sessions {
if *id != skip_id {
if let Some(addr) = self.sessions.get(id) {
addr.do_send(Message(message.to_owned()));
}
}
}
}
}
}
/// Make actor from `ChatServer`
impl Actor for ChatServer {
/// We are going to use simple Context, we just need ability to communicate
/// with other actors.
type Context = Context<Self>;
}
/// Handler for Connect message.
///
/// Register new session and assign unique id to this session
impl Handler<Connect> for ChatServer {
type Result = usize;
fn handle(&mut self, msg: Connect, _: &mut Context<Self>) -> Self::Result {
info!("Someone joined");
// notify all users in same room
self.send_message("main", "Someone joined", 0);
// register session with random id
let id = self.rng.random::<u64>() as usize;
self.sessions.insert(id, msg.addr);
// auto join session to main room
self.rooms.entry("main".to_owned()).or_default().insert(id);
let count = self.visitor_count.fetch_add(1, Ordering::SeqCst);
self.send_message("main", &format!("Total visitors {count}"), 0);
// send id back
id
}
}
/// Handler for Disconnect message.
impl Handler<Disconnect> for ChatServer {
type Result = ();
fn handle(&mut self, msg: Disconnect, _: &mut Context<Self>) {
info!("Someone disconnected");
let mut rooms: Vec<String> = Vec::new();
// remove address
if self.sessions.remove(&msg.id).is_some() {
// remove session from all rooms
for (name, sessions) in &mut self.rooms {
if sessions.remove(&msg.id) {
rooms.push(name.to_owned());
}
}
}
// send message to other users
for room in rooms {
self.send_message(&room, "Someone disconnected", 0);
}
}
}
/// Handler for Message message.
impl Handler<ClientMessage> for ChatServer {
type Result = ();
fn handle(&mut self, msg: ClientMessage, _: &mut Context<Self>) {
self.send_message(&msg.room, msg.msg.as_str(), msg.id);
}
}
/// Handler for `ListRooms` message.
impl Handler<ListRooms> for ChatServer {
type Result = MessageResult<ListRooms>;
fn handle(&mut self, _: ListRooms, _: &mut Context<Self>) -> Self::Result {
let mut rooms = Vec::new();
for key in self.rooms.keys() {
rooms.push(key.to_owned())
}
MessageResult(rooms)
}
}
/// Join room, send disconnect message to old room
/// send join message to new room
impl Handler<Join> for ChatServer {
type Result = ();
fn handle(&mut self, msg: Join, _: &mut Context<Self>) {
let Join { id, name } = msg;
let mut rooms = Vec::new();
// remove session from all rooms
for (n, sessions) in &mut self.rooms {
if sessions.remove(&id) {
rooms.push(n.to_owned());
}
}
// send message to other users
for room in rooms {
self.send_message(&room, "Someone disconnected", 0);
}
self.rooms.entry(name.clone()).or_default().insert(id);
self.send_message(&name, "Someone connected", id);
}
}
@@ -1,205 +0,0 @@
use std::time::{Duration, Instant};
use actix::prelude::*;
use actix_web_actors::ws;
use log::{debug, info, warn};
use crate::http_server::deprecated_endpoints::workflows::enqueue::progress_tracker_server;
use crate::http_server::deprecated_endpoints::workflows::enqueue::progress_tracker_server::ChatServer;
/// How often heartbeat pings are sent
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
/// How long before lack of client response causes a timeout
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Debug)]
pub struct WsChatSession {
/// unique session id
pub id: usize,
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
/// otherwise we drop connection.
pub hb: Instant,
/// joined room
pub room: String,
/// peer name
pub name: Option<String>,
/// Chat server
pub addr: Addr<ChatServer>,
}
impl WsChatSession {
/// helper method that sends ping to client every 5 seconds (HEARTBEAT_INTERVAL).
///
/// also this method checks heartbeats from client
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
// check client heartbeats
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
// heartbeat timed out
warn!("Websocket Client heartbeat failed, disconnecting!");
// notify chat server
act.addr.do_send(progress_tracker_server::Disconnect { id: act.id });
// stop actor
ctx.stop();
// don't try to send a ping
return;
}
ctx.ping(b"");
});
}
}
impl Actor for WsChatSession {
type Context = ws::WebsocketContext<Self>;
/// Method is called on actor start.
/// We register ws session with ChatServer
fn started(&mut self, ctx: &mut Self::Context) {
// we'll start heartbeat process on session start.
self.hb(ctx);
// register self in chat server. `AsyncContext::wait` register
// future within context, but context waits until this future resolves
// before processing any other events.
// HttpContext::state() is instance of WsChatSessionState, state is shared
// across all routes within application
let addr = ctx.address();
self.addr
.send(progress_tracker_server::Connect {
addr: addr.recipient(),
})
.into_actor(self)
.then(|res, act, ctx| {
match res {
Ok(res) => act.id = res,
// something is wrong with chat server
_ => ctx.stop(),
}
fut::ready(())
})
.wait(ctx);
}
fn stopping(&mut self, _: &mut Self::Context) -> Running {
// notify chat server
self.addr.do_send(progress_tracker_server::Disconnect { id: self.id });
Running::Stop
}
}
/// Handle messages from chat server, we simply send it to peer websocket
impl Handler<progress_tracker_server::Message> for WsChatSession {
type Result = ();
fn handle(&mut self, msg: progress_tracker_server::Message, ctx: &mut Self::Context) {
ctx.text(msg.0);
}
}
/// WebSocket message handler
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsChatSession {
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
log::debug!("WEBSOCKET MESSAGE: {msg:?}");
let msg = match msg {
Err(_) => {
ctx.stop();
return;
}
Ok(msg) => msg,
};
log::debug!("WEBSOCKET MESSAGE: {msg:?}");
match msg {
ws::Message::Ping(msg) => {
self.hb = Instant::now();
ctx.pong(&msg);
}
ws::Message::Pong(_) => {
self.hb = Instant::now();
}
ws::Message::Text(text) => {
let m = text.trim();
// we check for /sss type of messages
if m.starts_with('/') {
let v: Vec<&str> = m.splitn(2, ' ').collect();
match v[0] {
"/list" => {
// Send ListRooms message to chat server and wait for
// response
debug!("List rooms");
self.addr
.send(progress_tracker_server::ListRooms)
.into_actor(self)
.then(|res, _, ctx| {
match res {
Ok(rooms) => {
for room in rooms {
ctx.text(room);
}
}
_ => warn!("Something is wrong"),
}
fut::ready(())
})
.wait(ctx)
// .wait(ctx) pauses all events in context,
// so actor wont receive any new messages until it get list
// of rooms back
}
"/join" => {
if v.len() == 2 {
v[1].clone_into(&mut self.room);
self.addr.do_send(progress_tracker_server::Join {
id: self.id,
name: self.room.clone(),
});
ctx.text("joined");
} else {
ctx.text("!!! room name is required");
}
}
"/name" => {
if v.len() == 2 {
self.name = Some(v[1].to_owned());
} else {
ctx.text("!!! name is required");
}
}
_ => ctx.text(format!("!!! unknown command: {m:?}")),
}
} else {
let msg = if let Some(ref name) = self.name {
format!("{name}: {m}")
} else {
m.to_owned()
};
// send message to chat server
self.addr.do_send(progress_tracker_server::ClientMessage {
id: self.id,
msg,
room: self.room.clone(),
})
}
}
ws::Message::Binary(_) => warn!("Unexpected binary"),
ws::Message::Close(reason) => {
ctx.close(reason);
ctx.stop();
}
ws::Message::Continuation(_) => {
ctx.stop();
}
ws::Message::Nop => (),
}
}
}
@@ -14,7 +14,7 @@ use mysql_queries::queries::users::user_profiles::get_user_profile_by_username::
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, RequireModeratorError, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
const MAXIMUM_KEYS : u32 = 100;
@@ -60,12 +60,7 @@ pub async fn create_beta_keys_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<HttpResponse, CommonWebError>
{
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|err| match err {
RequireModeratorError::ServerError => CommonWebError::from_error(err),
RequireModeratorError::NotAuthorized => CommonWebError::NotAuthorized,
})?;
let user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let mut maybe_referrer_user_token = None;
@@ -14,7 +14,7 @@ use tokens::tokens::w2l_results::W2lResultToken;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, RequireModeratorError, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
/// For the URL PathInfo
@@ -58,12 +58,7 @@ pub async fn edit_beta_key_distributed_flag_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<HttpResponse, CommonWebError>
{
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|err| match err {
RequireModeratorError::ServerError => CommonWebError::from_error(err),
RequireModeratorError::NotAuthorized => CommonWebError::NotAuthorized,
})?;
let user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
edit_beta_key_distributed_flag(&path.token, request.is_distributed, &server_state.mysql_pool)
.await
@@ -13,7 +13,7 @@ use tokens::tokens::beta_keys::BetaKeyToken;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, RequireModeratorError, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
/// For the URL PathInfo
@@ -57,12 +57,7 @@ pub async fn edit_beta_key_note_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<HttpResponse, CommonWebError>
{
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|err| match err {
RequireModeratorError::ServerError => CommonWebError::from_error(err),
RequireModeratorError::NotAuthorized => CommonWebError::NotAuthorized,
})?;
let user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let maybe_note = request.note.as_deref()
.map(|note| note.trim())
@@ -17,7 +17,7 @@ use tokens::tokens::beta_keys::BetaKeyToken;
use crate::http_server::common_responses::pagination_page::PaginationPage;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::user_details_lite::UserDetailsLight;
use crate::http_server::web_utils::user_session::require_user_session::{require_user_session, RequireUserSessionError};
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
#[derive(Copy, Clone, Deserialize, ToSchema)]
@@ -108,12 +108,8 @@ pub async fn list_beta_keys_handler(
query: Query<ListBetaKeysQueryParams>,
server_state: web::Data<Arc<ServerState>>
) -> Result<HttpResponse, CommonWebError> {
let user_session = require_user_session(&http_request, &server_state)
.await
.map_err(|err| match err {
RequireUserSessionError::ServerError => CommonWebError::from_error(err),
RequireUserSessionError::NotAuthorized => CommonWebError::NotAuthorized,
})?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &server_state.mysql_pool)
.await?;
let mut is_mod = user_session.can_ban_users;
@@ -52,7 +52,7 @@ pub async fn redeem_beta_key_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<HttpResponse, CommonWebError>
{
let user_session = require_user_session(&http_request, &server_state).await?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let rate_limiter = &server_state.redis_rate_limiters.logged_out;
@@ -10,7 +10,7 @@ use mysql_queries::queries::characters::delete_character::delete_character;
use mysql_queries::queries::characters::get_character_by_token_including_deleted::get_character_by_token_including_deleted;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
/// Delete a character.
@@ -40,11 +40,7 @@ pub async fn delete_character_handler(
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
let user_session = require_user_session_using_connection(
&http_request,
&server_state.session_checker,
&mut mysql_connection,
).await?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *mysql_connection).await?;
let user_token = &user_session.user_token;
let is_mod = user_session.is_mod();
@@ -11,7 +11,7 @@ use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use seedance2pro_client::requests::update_character::update_character::{update_character, UpdateCharacterArgs};
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
/// Edit a character's name or description.
@@ -38,11 +38,7 @@ pub async fn edit_character_handler(
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
let user_session = require_user_session_using_connection(
&http_request,
&server_state.session_checker,
&mut mysql_connection,
).await?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *mysql_connection).await?;
let user_token = &user_session.user_token;
let is_mod = user_session.is_mod();
@@ -16,7 +16,7 @@ use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
/// List characters for the current session.
@@ -39,11 +39,7 @@ pub async fn list_characters_handler(
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
let user_session = require_user_session_using_connection(
&http_request,
&server_state.session_checker,
&mut mysql_connection,
).await?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *mysql_connection).await?;
let user_token = &user_session.user_token;
@@ -17,7 +17,7 @@ use mysql_queries::queries::folders::folder::update_folder_color_code::{
use tokens::tokens::folders::FolderToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
const MAX_COLOR_CODE_LEN: usize = 16;
@@ -47,9 +47,7 @@ pub async fn color_code_folder_handler(
CommonWebError::from_error(err)
})?;
let user_session = require_user_session_using_connection(
&http_request, &server_state.session_checker, &mut conn,
).await.map_err(|_| CommonWebError::NotAuthorized)?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
// Normalize: trim, treat empty after trim as `None`. This lets clients
// clear by passing `""` or by omitting the field entirely.
@@ -23,7 +23,7 @@ use tokens::tokens::folders::FolderToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
/// Set or clear the custom cover image on a folder.
@@ -64,9 +64,7 @@ pub async fn cover_image_folder_handler(
CommonWebError::from_error(err)
})?;
let user_session = require_user_session_using_connection(
&http_request, &server_state.session_checker, &mut conn,
).await.map_err(|_| CommonWebError::NotAuthorized)?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
// Confirm the folder exists + is owned. Authoritative 404 source.
get_folder_for_owner(GetFolderForOwnerArgs {
@@ -17,7 +17,7 @@ use crate::http_server::endpoints::folders::folder::folder_info_conversion::{
build_folder_thumbnails_lookup, folder_row_to_info,
};
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
const MAX_NAME_LEN: usize = 255;
@@ -45,9 +45,7 @@ pub async fn create_folder_handler(
CommonWebError::from_error(err)
})?;
let user_session = require_user_session_using_connection(
&http_request, &server_state.session_checker, &mut conn,
).await.map_err(|_| CommonWebError::NotAuthorized)?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
let name = request.name.trim().to_string();
@@ -12,7 +12,7 @@ use mysql_queries::queries::folders::folder::soft_delete_folder::{
use tokens::tokens::folders::FolderToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
/// Soft-delete a folder. Children retain their parent pointer and become
@@ -39,9 +39,7 @@ pub async fn delete_folder_handler(
CommonWebError::from_error(err)
})?;
let user_session = require_user_session_using_connection(
&http_request, &server_state.session_checker, &mut conn,
).await.map_err(|_| CommonWebError::NotAuthorized)?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
let rows_affected = soft_delete_folder(SoftDeleteFolderArgs {
@@ -16,7 +16,7 @@ use crate::http_server::endpoints::folders::folder::folder_info_conversion::{
build_folder_thumbnails_lookup, folder_row_to_info,
};
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
/// Fetch a single folder owned by the logged-in user.
@@ -42,9 +42,7 @@ pub async fn get_folder_handler(
CommonWebError::from_error(err)
})?;
let user_session = require_user_session_using_connection(
&http_request, &server_state.session_checker, &mut conn,
).await.map_err(|_| CommonWebError::NotAuthorized)?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
let row = get_folder_for_owner(GetFolderForOwnerArgs {
folder_token: &path.folder_token,
@@ -17,7 +17,7 @@ use crate::http_server::endpoints::folders::folder::folder_info_conversion::{
build_folder_thumbnails_lookup, folder_row_to_info,
};
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
const CURSOR_NAME: &str = "folders_all";
@@ -48,9 +48,7 @@ pub async fn list_folders_handler(
CommonWebError::from_error(err)
})?;
let user_session = require_user_session_using_connection(
&http_request, &server_state.session_checker, &mut conn,
).await.map_err(|_| CommonWebError::NotAuthorized)?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
let limit = query.limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
@@ -17,7 +17,7 @@ use mysql_queries::queries::folders::folder::update_folder_name::{
use tokens::tokens::folders::FolderToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
const MAX_NAME_LEN: usize = 255;
@@ -47,9 +47,7 @@ pub async fn rename_folder_handler(
CommonWebError::from_error(err)
})?;
let user_session = require_user_session_using_connection(
&http_request, &server_state.session_checker, &mut conn,
).await.map_err(|_| CommonWebError::NotAuthorized)?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
let new_name = request.new_name.trim().to_string();
@@ -17,7 +17,7 @@ use mysql_queries::queries::folders::folder::update_folder_has_star::{
use tokens::tokens::folders::FolderToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
#[utoipa::path(
@@ -44,9 +44,7 @@ pub async fn star_folder_handler(
CommonWebError::from_error(err)
})?;
let user_session = require_user_session_using_connection(
&http_request, &server_state.session_checker, &mut conn,
).await.map_err(|_| CommonWebError::NotAuthorized)?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
// Confirm the folder exists + is owned before issuing the update.
// A 404 here is the authoritative "not found" signal — the update
@@ -27,7 +27,7 @@ use mysql_queries::queries::folders::media_files::recompute_folder_last_media_fi
use tokens::tokens::folders::FolderToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
const MAX_BULK: usize = 500;
@@ -61,9 +61,7 @@ pub async fn bulk_add_folder_media_files_handler(
CommonWebError::from_error(err)
})?;
let user_session = require_user_session_using_connection(
&http_request, &server_state.session_checker, &mut conn,
).await.map_err(|_| CommonWebError::NotAuthorized)?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
if request.media_file_tokens.len() > MAX_BULK {
return Err(CommonWebError::BadInputWithSimpleMessage(
@@ -30,7 +30,7 @@ use mysql_queries::queries::folders::media_files::recompute_folder_last_media_fi
use tokens::tokens::folders::FolderToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
const MAX_BULK: usize = 500;
@@ -72,9 +72,7 @@ pub async fn bulk_move_folder_media_files_handler(
CommonWebError::from_error(err)
})?;
let user_session = require_user_session_using_connection(
&http_request, &server_state.session_checker, &mut conn,
).await.map_err(|_| CommonWebError::NotAuthorized)?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
if request.media_file_tokens.len() > MAX_BULK {
return Err(CommonWebError::BadInputWithSimpleMessage(
@@ -24,7 +24,7 @@ use tokens::tokens::folders::FolderToken;
use tokens::tokens::media_files::MediaFileToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
const MAX_BULK: usize = 500;
@@ -56,9 +56,7 @@ pub async fn bulk_remove_folder_media_files_handler(
CommonWebError::from_error(err)
})?;
let user_session = require_user_session_using_connection(
&http_request, &server_state.session_checker, &mut conn,
).await.map_err(|_| CommonWebError::NotAuthorized)?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
if request.media_file_tokens.len() > MAX_BULK {
return Err(CommonWebError::BadInputWithSimpleMessage(
@@ -32,7 +32,7 @@ use crate::http_server::common_responses::media::media_domain::MediaDomain;
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
const CURSOR_NAME: &str = "folder_mf";
@@ -127,9 +127,7 @@ pub async fn list_folder_media_files_handler(
CommonWebError::from_error(err)
})?;
let user_session = require_user_session_using_connection(
&http_request, &server_state.session_checker, &mut conn,
).await.map_err(|_| CommonWebError::NotAuthorized)?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
let folder = get_folder_for_owner(GetFolderForOwnerArgs {
folder_token: &path.folder_token,
@@ -25,7 +25,7 @@ use mysql_queries::queries::folders::subfolder::filter_existing_owned_folder_tok
use tokens::tokens::folders::FolderToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
const MAX_BULK: usize = 500;
@@ -58,9 +58,7 @@ pub async fn bulk_add_subfolders_handler(
CommonWebError::from_error(err)
})?;
let user_session = require_user_session_using_connection(
&http_request, &server_state.session_checker, &mut conn,
).await.map_err(|_| CommonWebError::NotAuthorized)?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
if request.subfolder_tokens.len() > MAX_BULK {
return Err(CommonWebError::BadInputWithSimpleMessage(
@@ -14,7 +14,7 @@ use mysql_queries::queries::folders::subfolder::bulk_clear_parent_folder::{
use tokens::tokens::folders::FolderToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
const MAX_BULK: usize = 500;
@@ -45,9 +45,7 @@ pub async fn bulk_remove_subfolders_handler(
CommonWebError::from_error(err)
})?;
let user_session = require_user_session_using_connection(
&http_request, &server_state.session_checker, &mut conn,
).await.map_err(|_| CommonWebError::NotAuthorized)?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
if request.subfolder_tokens.len() > MAX_BULK {
return Err(CommonWebError::BadInputWithSimpleMessage(
@@ -21,7 +21,7 @@ use crate::http_server::endpoints::folders::folder::folder_info_conversion::{
build_folder_thumbnails_lookup, folder_row_to_info,
};
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
const CURSOR_NAME: &str = "subfolders";
@@ -55,9 +55,7 @@ pub async fn list_subfolders_handler(
CommonWebError::from_error(err)
})?;
let user_session = require_user_session_using_connection(
&http_request, &server_state.session_checker, &mut conn,
).await.map_err(|_| CommonWebError::NotAuthorized)?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
// Confirm the parent exists + is owned by the caller before listing.
@@ -119,7 +119,7 @@ pub async fn upload_snapshot_media_file_handler(
let maybe_user_token = maybe_user_session
.as_ref()
.map(|session| session.get_strongly_typed_user_token());
.map(|session| session.get_user_token());
let maybe_avt_token = server_state
.avt_cookie_manager
@@ -247,7 +247,7 @@ pub async fn upload_snapshot_media_file_handler(
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
maybe_media_class: Some(MediaFileClass::Image),
media_file_type,
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility,
@@ -8,7 +8,7 @@ use utoipa::ToSchema;
use mysql_queries::queries::generic_inference::web::dismiss_finished_jobs_for_user::dismiss_finished_jobs_for_user;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
#[derive(Serialize, ToSchema)]
@@ -40,8 +40,7 @@ pub async fn dismiss_finished_session_jobs_handler(
CommonWebError::from_error(e)
})?;
let user_session = require_user_session_using_connection(
&http_request, &server_state.session_checker, &mut mysql_connection)
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *mysql_connection)
.await?;
dismiss_finished_jobs_for_user(&mut mysql_connection, &user_session.user_token)
@@ -56,12 +56,8 @@ pub async fn change_media_file_animation_type_handler(
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<SimpleResponse>, CommonWebError> {
let user_session = require_user_session(&http_request, &server_state)
.await
.map_err(|e| {
warn!("Not authorized: {:?}", e);
CommonWebError::NotAuthorized
})?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &server_state.mysql_pool)
.await?;
let media_file_token = path.token.clone();
let is_mod = user_session.is_mod();
@@ -53,12 +53,8 @@ pub async fn change_media_file_engine_category_handler(
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<SimpleResponse>, CommonWebError> {
let user_session = require_user_session(&http_request, &server_state)
.await
.map_err(|e| {
warn!("Not authorized: {:?}", e);
CommonWebError::NotAuthorized
})?;
let user_session = require_user_session(&http_request, &server_state.session_checker, &server_state.mysql_pool)
.await?;
let media_file_token = path.token.clone();
let is_mod = user_session.is_mod();
@@ -135,7 +135,7 @@ pub async fn upload_audio_media_file_handler(
let maybe_user_token = maybe_user_session
.as_ref()
.map(|session| session.get_strongly_typed_user_token());
.map(|session| session.get_user_token());
let maybe_avt_token = server_state
.avt_cookie_manager
@@ -334,7 +334,7 @@ pub async fn upload_audio_media_file_handler(
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
maybe_media_class: Some(MediaFileClass::Audio),
media_file_type: MediaFileType::Audio,
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility,
@@ -47,7 +47,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
let mut field_name = None;
let mut field_filename = None;
if let content_disposition = field.content_disposition() {
if let Some(content_disposition) = field.content_disposition() {
field_name = content_disposition.get_name()
.map(|s| s.to_string());
field_filename = content_disposition.get_filename() // NB: Only used for the file bytes.
@@ -140,7 +140,8 @@ pub async fn upload_engine_asset_media_file_handler(
let ip_address = get_request_ip(&http_request);
let maybe_user_token = maybe_user_session
.map(|session| session.get_strongly_typed_user_token());
.as_ref()
.map(|session| session.get_user_token());
// ==================== FILE DATA ==================== //
@@ -205,7 +206,7 @@ pub async fn upload_engine_asset_media_file_handler(
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
maybe_media_class: Some(MediaFileClass::Dimensional),
media_file_type,
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility,
@@ -47,7 +47,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
let mut field_name = None;
let mut field_filename = None;
if let content_disposition = field.content_disposition() {
if let Some(content_disposition) = field.content_disposition() {
field_name = content_disposition.get_name()
.map(|s| s.to_string());
field_filename = content_disposition.get_filename() // NB: Only used for the file bytes.
@@ -142,7 +142,8 @@ pub async fn process_upload_media_file(
let ip_address = get_request_ip(&http_request);
let maybe_user_token = maybe_user_session
.map(|session| session.get_strongly_typed_user_token());
.as_ref()
.map(|session| session.get_user_token());
let maybe_file_size_bytes = upload_media_request.file_bytes
.as_ref()
@@ -392,7 +393,7 @@ pub async fn process_upload_media_file(
})?;
let media_token = MediaFileInsertBuilder::new()
.maybe_creator_user(maybe_user_token.as_ref())
.maybe_creator_user(maybe_user_token)
.maybe_creator_anonymous_visitor(maybe_avt_token.as_ref())
.creator_ip_address(&ip_address)
.public_bucket_directory_hash(&public_upload_path)
@@ -417,7 +418,7 @@ pub async fn process_upload_media_file(
//let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
// maybe_media_class: Some(media_file_class), // DONE
// media_file_type, // DONE
// maybe_creator_user_token: maybe_user_token.as_ref(), // DONE
// maybe_creator_user_token: maybe_user_token, // DONE
// maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(), // DONE
// creator_ip_address: &ip_address, // DONE
// creator_set_visibility, // DONE
@@ -145,7 +145,7 @@ pub async fn upload_image_media_file_handler(
let maybe_user_token = maybe_user_session
.as_ref()
.map(|session| session.get_strongly_typed_user_token());
.map(|session| session.get_user_token());
let maybe_avt_token = server_state
.avt_cookie_manager
@@ -310,7 +310,7 @@ pub async fn upload_image_media_file_handler(
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
maybe_media_class: Some(MediaFileClass::Image),
media_file_type,
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility,
@@ -179,7 +179,7 @@ pub async fn upload_new_engine_asset_media_file_handler(
let maybe_user_token = maybe_user_session
.as_ref()
.map(|session| session.get_strongly_typed_user_token());
.map(|session| session.get_user_token());
// ==================== FILE DATA ==================== //
@@ -211,7 +211,7 @@ pub async fn upload_new_engine_asset_media_file_handler(
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
maybe_media_class: Some(file_info.media_class),
media_file_type: file_info.media_type,
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: file_info.creator_set_visibility,
@@ -112,7 +112,7 @@ pub async fn upload_new_scene_media_file_handler(
let maybe_user_token = maybe_user_session
.as_ref()
.map(|session| session.get_strongly_typed_user_token());
.map(|session| session.get_user_token());
let maybe_avt_token = server_state
.avt_cookie_manager
@@ -226,7 +226,7 @@ pub async fn upload_new_scene_media_file_handler(
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
maybe_media_class: Some(MediaFileClass::Dimensional),
media_file_type: MediaFileType::SceneJson,
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility,
@@ -23,7 +23,8 @@ use tokens::tokens::media_files::MediaFileToken;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::endpoints::media_files::upload::upload_pmx::extract_and_upload_pmx_files::{extract_and_upload_pmx_files, PmxError};
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, RequireModeratorError, UseDatabase};
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
/// Form-multipart request fields.
@@ -115,11 +116,11 @@ pub async fn upload_pmx_media_file_handler(
// ==================== READ SESSION ==================== //
// NB: We require a moderator to upload PMX files.
let user_session = require_moderator(&http_request, &server_state, UseDatabase::FromPool(&mut mysql_connection))
let user_session = require_moderator(&http_request, &server_state.session_checker, &mut *mysql_connection)
.await
.map_err(|err| match err {
RequireModeratorError::ServerError => MediaFileUploadError::ServerError,
RequireModeratorError::NotAuthorized => MediaFileUploadError::NotAuthorized,
CommonWebError::NotAuthorized => MediaFileUploadError::NotAuthorized,
_ => MediaFileUploadError::ServerError,
})?;
let maybe_avt_token = server_state
@@ -206,7 +207,7 @@ pub async fn upload_pmx_media_file_handler(
let ip_address = get_request_ip(&http_request);
//let maybe_user_token = maybe_user_session
// .map(|session| session.get_strongly_typed_user_token());
// .map(|session| session.get_user_token());
// ==================== FILE DATA ==================== //
@@ -257,7 +258,7 @@ pub async fn upload_pmx_media_file_handler(
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
maybe_media_class: Some(MediaFileClass::Dimensional),
media_file_type: MediaFileType::Pmx,
maybe_creator_user_token: Some(&user_session.get_strongly_typed_user_token()),
maybe_creator_user_token: Some(user_session.get_user_token()),
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility,
@@ -109,7 +109,7 @@ pub async fn upload_saved_scene_media_file_handler(
let maybe_user_token = maybe_user_session
.as_ref()
.map(|session| session.get_strongly_typed_user_token());
.map(|session| session.get_user_token());
let maybe_avt_token = server_state
.avt_cookie_manager
@@ -148,7 +148,7 @@ pub async fn upload_saved_scene_media_file_handler(
let creator_check = check_creator_tokens(CheckCreatorTokenArgs {
maybe_creator_user_token: media_file.maybe_creator_user_token.as_ref(),
maybe_current_request_user_token: maybe_user_token.as_ref(),
maybe_current_request_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: media_file.maybe_creator_anonymous_visitor_token.as_ref(),
maybe_current_request_anonymous_visitor_token: maybe_avt_token.as_ref(),
});
@@ -111,7 +111,7 @@ pub async fn upload_scene_snapshot_media_file_handler(
let maybe_user_token = maybe_user_session
.as_ref()
.map(|session| session.get_strongly_typed_user_token());
.map(|session| session.get_user_token());
let maybe_avt_token = server_state
.avt_cookie_manager
@@ -219,7 +219,7 @@ pub async fn upload_scene_snapshot_media_file_handler(
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
maybe_media_class: Some(MediaFileClass::Dimensional),
media_file_type: MediaFileType::SceneJson,
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Hidden,
@@ -119,7 +119,7 @@ pub async fn upload_spz_media_file_handler(
let maybe_user_token = maybe_user_session
.as_ref()
.map(|session| session.get_strongly_typed_user_token());
.map(|session| session.get_user_token());
let maybe_avt_token = server_state
.avt_cookie_manager
@@ -255,7 +255,7 @@ pub async fn upload_spz_media_file_handler(
let media_token = MediaFileInsertBuilder::new()
.media_file_class(MediaFileClass::Dimensional)
.media_file_type(MediaFileType::Spz)
.maybe_creator_user(maybe_user_token.as_ref())
.maybe_creator_user(maybe_user_token)
.maybe_creator_anonymous_visitor(maybe_avt_token.as_ref())
.creator_ip_address(&ip_address)
.creator_set_visibility(creator_set_visibility)
@@ -19,13 +19,13 @@ use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::create::insert_media_file_from_studio_scene_render::{insert_media_file_from_studio_scene_render, InsertStudioSceneRenderArgs};
use tokens::tokens::media_files::MediaFileToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::endpoints::media_files::upload::upload_studio_shot::extract_frames_from_zip::{extract_frames_from_zip, ExtractFramesError};
use crate::http_server::endpoints::media_files::upload::upload_studio_shot::ffmpeg_frames_to_mp4::ffmpeg_frames_to_mp4;
use crate::http_server::requests::request_headers::has_debug_header::has_debug_header;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::user_session::require_user_session_extended_using_connection::require_user_session_extended_using_connection;
use crate::http_server::web_utils::user_session::require_user_session_extended::require_user_session_extended;
use crate::state::server_state::ServerState;
const DEFAULT_FPS : u8 = 12;
@@ -111,11 +111,14 @@ pub async fn upload_studio_shot_media_file_handler(
// ==================== READ SESSION ==================== //
// NB: We require a moderator to upload PMX files.
let user_session = require_user_session_extended_using_connection(&http_request, &server_state.session_checker, &mut mysql_connection)
let user_session = require_user_session_extended(&http_request, &server_state.session_checker, &mut *mysql_connection)
.await
.map_err(|e| {
error!("User session error: {:?}", e);
MediaFileUploadError::NotAuthorized
match e {
CommonWebError::NotAuthorized => MediaFileUploadError::NotAuthorized,
_ => MediaFileUploadError::ServerError,
}
})?;
let maybe_avt_token = server_state
@@ -176,7 +179,7 @@ pub async fn upload_studio_shot_media_file_handler(
let ip_address = get_request_ip(&http_request);
//let maybe_user_token = maybe_user_session
// .map(|session| session.get_strongly_typed_user_token());
// .map(|session| session.get_user_token());
// ==================== FILE DATA ==================== //
@@ -177,7 +177,7 @@ pub async fn upload_new_video_media_file_handler(
let maybe_user_token = maybe_user_session
.as_ref()
.map(|session| session.get_strongly_typed_user_token());
.map(|session| session.get_user_token());
let maybe_avt_token = server_state
.avt_cookie_manager
@@ -507,7 +507,7 @@ pub async fn upload_new_video_media_file_handler(
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
maybe_media_class: Some(MediaFileClass::Video),
media_file_type: MediaFileType::Video,
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility,
@@ -43,7 +43,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
let mut field_name = None;
let mut field_filename = None;
if let content_disposition = field.content_disposition() {
if let Some(content_disposition) = field.content_disposition() {
field_name = content_disposition.get_name()
.map(|s| s.to_string());
field_filename = content_disposition.get_filename() // NB: Only used for the file bytes.
@@ -158,7 +158,8 @@ pub async fn upload_video_media_file_handler(
let ip_address = get_request_ip(&http_request);
let maybe_user_token = maybe_user_session
.map(|session| session.get_strongly_typed_user_token());
.as_ref()
.map(|session| session.get_user_token());
// ==================== FILE DATA ==================== //
@@ -221,7 +222,7 @@ pub async fn upload_video_media_file_handler(
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
maybe_media_class: Some(MediaFileClass::Video),
media_file_type: MediaFileType::Video,
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility,
@@ -45,7 +45,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Result
let mut field_name = None;
let mut field_filename = None;
if let content_disposition = field.content_disposition() {
if let Some(content_disposition) = field.content_disposition() {
field_name = content_disposition.get_name()
.map(|s| s.to_string());
field_filename = content_disposition.get_filename() // NB: Only used for the file bytes.
@@ -73,7 +73,7 @@ pub async fn write_engine_asset_media_file_handler(
let maybe_user_token = maybe_user_session
.as_ref()
.map(|session| session.get_strongly_typed_user_token());
.map(|session| session.get_user_token());
let maybe_avt_token = server_state
.avt_cookie_manager
@@ -117,7 +117,7 @@ pub async fn write_engine_asset_media_file_handler(
let creator_check = check_creator_tokens(CheckCreatorTokenArgs {
maybe_creator_user_token: media_file.maybe_creator_user_token.as_ref(),
maybe_current_request_user_token: maybe_user_token.as_ref(),
maybe_current_request_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: media_file.maybe_creator_anonymous_visitor_token.as_ref(),
maybe_current_request_anonymous_visitor_token: maybe_avt_token.as_ref(),
});
@@ -232,7 +232,7 @@ pub async fn write_engine_asset_media_file_handler(
maybe_engine_category: upload_media_request.maybe_engine_category,
maybe_animation_type: upload_media_request.maybe_animation_type,
maybe_media_subtype: None,
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility,
@@ -34,7 +34,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Result
let mut field_name = None;
let mut field_filename = None;
if let content_disposition = field.content_disposition() {
if let Some(content_disposition) = field.content_disposition() {
field_name = content_disposition.get_name()
.map(|s| s.to_string());
field_filename = content_disposition.get_filename() // NB: Only used for the file bytes.
@@ -75,7 +75,7 @@ pub async fn write_scene_file_media_file_handler(
let maybe_user_token = maybe_user_session
.as_ref()
.map(|session| session.get_strongly_typed_user_token());
.map(|session| session.get_user_token());
let maybe_avt_token = server_state
.avt_cookie_manager
@@ -119,7 +119,7 @@ pub async fn write_scene_file_media_file_handler(
let creator_check = check_creator_tokens(CheckCreatorTokenArgs {
maybe_creator_user_token: media_file.maybe_creator_user_token.as_ref(),
maybe_current_request_user_token: maybe_user_token.as_ref(),
maybe_current_request_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: media_file.maybe_creator_anonymous_visitor_token.as_ref(),
maybe_current_request_anonymous_visitor_token: maybe_avt_token.as_ref(),
});
@@ -220,7 +220,7 @@ pub async fn write_scene_file_media_file_handler(
maybe_engine_category: Some(MediaFileEngineCategory::Scene),
maybe_animation_type: None,
maybe_media_subtype: None,
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility,
@@ -28,8 +28,7 @@ use crate::http_server::requests::request_headers::get_routing_tag_header::get_r
use crate::http_server::requests::request_headers::has_debug_header::has_debug_header;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::validations::validate_model_title::validate_model_title;
use crate::http_server::web_utils::user_session::require_user_session::RequireUserSessionError;
use crate::http_server::web_utils::user_session::require_user_session_extended_using_connection::require_user_session_extended_using_connection;
use crate::http_server::web_utils::user_session::require_user_session_extended::require_user_session_extended;
use crate::state::server_state::ServerState;
#[derive(Deserialize, ToSchema)]
@@ -82,15 +81,14 @@ pub async fn enqueue_gptsovits_model_download_handler(
let maybe_avt_token = server_state.avt_cookie_manager
.get_avt_token_from_request(&http_request);
let user_session = require_user_session_extended_using_connection(
let user_session = require_user_session_extended(
&http_request,
&server_state.session_checker,
&mut mysql_connection)
.await
.map_err(|err| match err {
RequireUserSessionError::ServerError => CommonWebError::from_error(err),
RequireUserSessionError::NotAuthorized => CommonWebError::NotAuthorized,
})?;
&mut *mysql_connection).await?;
// NB: The connection isn't needed past this point (the enqueue below uses the pool directly),
// so release it now rather than holding a pool slot for the rest of the handler.
drop(mysql_connection);
// ==================== PAID PLAN + PRIORITY ==================== //
@@ -14,7 +14,7 @@ use mysql_queries::queries::staff_audit_logs::insert_staff_audit_log::{
use pager::notification::notification_details_builder::NotificationDetailsBuilder;
use pager::notification::notification_urgency::NotificationUrgency;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
/// Send a test alert via the pager system. Moderators only.
@@ -35,12 +35,7 @@ pub async fn moderation_send_alert_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModerationSendAlertResponse>, CommonWebError> {
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
CommonWebError::NotAuthorized
})?;
let user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let ip_address = get_request_ip(&http_request);
@@ -15,9 +15,7 @@ use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{
require_moderator, UseDatabase,
};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
// ── Path params ──
@@ -74,14 +72,7 @@ pub async fn moderation_list_debug_logs_for_token_handler(
query: web::Query<ListDebugLogsQueryParams>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListDebugLogsSuccessResponse>, CommonWebError> {
let _user_session = require_moderator(
&http_request,
&server_state,
UseDatabase::GrabNewConnection,
).await.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
CommonWebError::NotAuthorized
})?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
@@ -25,7 +25,7 @@ use tokens::tokens::users::UserToken;
use crate::http_server::web_utils::serialize_as_json_error::serialize_as_json_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, RequireModeratorError, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
/// For the URL PathInfo
@@ -60,12 +60,7 @@ pub async fn moderator_get_token_info_handler(
server_state: web::Data<Arc<ServerState>>
) -> Result<HttpResponse, CommonWebError> {
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|err| match err {
RequireModeratorError::ServerError => CommonWebError::from_error(err),
RequireModeratorError::NotAuthorized => CommonWebError::NotAuthorized,
})?;
let user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let token = path.token.trim();
let maybe_result = get_entity_from_token(&server_state.mysql_pool, &path.token)
@@ -16,9 +16,7 @@ use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{
require_moderator, UseDatabase,
};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
// ── Path params ──
@@ -92,14 +90,7 @@ pub async fn moderation_get_job_by_token_handler(
path: web::Path<GetJobByTokenPathInfo>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<GetJobByTokenSuccessResponse>, CommonWebError> {
let _user_session = require_moderator(
&http_request,
&server_state,
UseDatabase::GrabNewConnection,
).await.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
CommonWebError::NotAuthorized
})?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
@@ -13,7 +13,7 @@ use mysql_queries::queries::generic_inference::web::list_user_jobs_for_moderatio
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
/// List jobs for a user (moderation)
@@ -36,9 +36,7 @@ pub async fn list_user_jobs_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListUserJobsResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let results = list_user_jobs_for_moderation(&path.user_token, &server_state.mysql_pool)
.await
@@ -13,9 +13,7 @@ use tokens::tokens::staff_audit_logs::StaffAuditLogToken;
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{
require_moderator, UseDatabase,
};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
const CURSOR_NAME: &str = "modstfaud";
@@ -76,14 +74,7 @@ pub async fn moderator_list_staff_audit_logs_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListStaffAuditLogsSuccessResponse>, CommonWebError> {
let _user_session = require_moderator(
&http_request,
&server_state,
UseDatabase::GrabNewConnection,
).await.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
CommonWebError::NotAuthorized
})?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let limit = query.limit
.unwrap_or(DEFAULT_LIMIT)
@@ -15,7 +15,7 @@ use mysql_queries::queries::users::user::list::list_subscribing_users_by_signup_
};
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
/// List subscribing users by signup date (descending), for moderators.
@@ -36,9 +36,7 @@ pub async fn moderator_list_subscribing_users_by_signup_date_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModeratorListSubscribingUsersBySignupDateResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let result = list_subscribing_users_by_signup_date_for_moderation(
ListSubscribingUsersBySignupDateArgs {
@@ -15,7 +15,7 @@ use mysql_queries::queries::users::user::list::list_users_by_signup_date_for_mod
};
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
/// List users by signup date (descending), for moderators.
@@ -36,9 +36,7 @@ pub async fn moderator_list_users_by_signup_date_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModeratorListUsersBySignupDateResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let result = list_users_by_signup_date_for_moderation(
ListUsersBySignupDateArgs {
@@ -12,7 +12,7 @@ use artcraft_api_defs::moderation::user::user_lookup_by_stripe_customer_id::{
use mysql_queries::queries::users::user_subscriptions::lookup_users_by_stripe_customer_id::lookup_users_by_stripe_customer_id;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
/// Moderator User Lookup by Stripe Customer ID
@@ -33,9 +33,7 @@ pub async fn moderator_user_lookup_by_stripe_customer_id_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModeratorUserLookupByStripeCustomerIdResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let stripe_customer_id = request.stripe_customer_id.trim();
@@ -17,7 +17,7 @@ use mysql_queries::queries::users::user::get::lookup_user_for_moderation::{
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
const LEGACY_USER_TOKEN_PREFIX: &str = "U:";
@@ -40,9 +40,7 @@ pub async fn moderator_user_lookup_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModeratorUserLookupSuccessResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let search = request.search.trim();
@@ -18,9 +18,7 @@ use mysql_queries::queries::users::user::update::set_user_ban_status::{
use mysql_queries::queries::users::user_profiles::get_user_profile_by_username::get_user_profile_by_username;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{
require_moderator, UseDatabase,
};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
// --- Request ---
@@ -61,14 +59,7 @@ pub async fn moderation_ban_user_handler(
) -> Result<Json<ModerationBanUserSuccessResponse>, CommonWebError> {
// 1. Require moderator with ban permissions.
let user_session = require_moderator(
&http_request,
&server_state,
UseDatabase::GrabNewConnection,
).await.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
CommonWebError::NotAuthorized
})?;
let user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
if !user_session.can_ban_users {
warn!("User {} is not allowed to ban users", user_session.user_token.as_str());
@@ -24,9 +24,7 @@ use users::email::email_to_gravatar_hash::email_to_gravatar_hash;
use users::email::validate_email_address_format::validate_email_address_format;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{
require_moderator, UseDatabase,
};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
// --- Request ---
@@ -71,14 +69,7 @@ pub async fn moderator_change_user_email_handler(
) -> Result<Json<ModeratorChangeUserEmailSuccessResponse>, CommonWebError> {
// 1. Require moderator.
let user_session = require_moderator(
&http_request,
&server_state,
UseDatabase::GrabNewConnection,
).await.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
CommonWebError::NotAuthorized
})?;
let user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
// 2. Normalize + validate the new email.
let new_email = request.new_email.trim().to_lowercase();
@@ -15,9 +15,7 @@ use mysql_queries::queries::users::user_email_changes::list_user_email_changes_f
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{
require_moderator, UseDatabase,
};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
const CURSOR_NAME: &str = "modemchg";
@@ -103,14 +101,7 @@ pub async fn moderator_list_email_address_changes_for_user_handler(
) -> Result<Json<ModeratorListUserEmailChangesSuccessResponse>, CommonWebError> {
// 1. Require moderator.
let _user_session = require_moderator(
&http_request,
&server_state,
UseDatabase::GrabNewConnection,
).await.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
CommonWebError::NotAuthorized
})?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
// 2. Resolve username → user_token. Not-found → 404; everything else → 500.
let username = path.username.trim().to_lowercase();
@@ -27,9 +27,7 @@ use crate::http_server::session::lookup::user_session_feature_flags::UserSession
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
use crate::http_server::web_utils::user_session::require_moderator::{
require_moderator, UseDatabase,
};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
use artcraft_api_defs::common::responses::simple_generic_json_success::SimpleGenericJsonSuccess;
@@ -89,14 +87,7 @@ pub async fn moderator_edit_user_feature_flags_handler(
redis_pool: Data<r2d2::Pool<Client>>,
) -> Result<HttpResponse, CommonWebError> {
let user_session = require_moderator(
&http_request,
&server_state,
UseDatabase::GrabNewConnection,
).await.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
CommonWebError::NotAuthorized
})?;
let user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let username_or_token = path.username_or_token.trim();
@@ -8,9 +8,7 @@ use utoipa::ToSchema;
use enums::by_table::users::user_feature_flag::UserFeatureFlag;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{
require_moderator, UseDatabase,
};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
// ── Response ──
@@ -49,14 +47,7 @@ pub async fn moderator_list_all_available_user_feature_flags_handler(
http_request: HttpRequest,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModeratorListUserFeatureFlagsResponse>, CommonWebError> {
let _user_session = require_moderator(
&http_request,
&server_state,
UseDatabase::GrabNewConnection,
).await.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
CommonWebError::NotAuthorized
})?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let feature_flags: Vec<FeatureFlagDescriptor> = UserFeatureFlag::all_variants()
.into_iter()
@@ -12,9 +12,7 @@ use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::session::lookup::user_session_feature_flags::UserSessionFeatureFlags;
use crate::http_server::web_utils::user_session::require_moderator::{
require_moderator, UseDatabase,
};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
// ── Request ──
@@ -56,14 +54,7 @@ pub async fn moderator_list_user_feature_flags_handler(
path: Path<ListUserFeatureFlagsPathInfo>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModeratorListUserFeatureFlagsResponse>, CommonWebError> {
let _user_session = require_moderator(
&http_request,
&server_state,
UseDatabase::GrabNewConnection,
).await.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
CommonWebError::NotAuthorized
})?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let username_or_token = path.username_or_token.trim();
@@ -13,9 +13,7 @@ use mysql_queries::queries::user_referrals::list_global_user_referrals::{
};
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{
require_moderator, UseDatabase,
};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
const CURSOR_NAME: &str = "modusrref";
@@ -42,14 +40,7 @@ pub async fn moderator_list_global_user_referrals_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListGlobalUserReferralsSuccessResponse>, CommonWebError> {
let _user_session = require_moderator(
&http_request,
&server_state,
UseDatabase::GrabNewConnection,
).await.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
CommonWebError::NotAuthorized
})?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let limit = query.limit
.unwrap_or(DEFAULT_LIMIT)
@@ -16,9 +16,7 @@ use mysql_queries::queries::user_referrals::list_user_referrals_for_user::{
};
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{
require_moderator, UseDatabase,
};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
const CURSOR_NAME: &str = "modusrrefu";
@@ -47,14 +45,7 @@ pub async fn moderator_list_user_referrals_for_user_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListUserReferralsForUserSuccessResponse>, CommonWebError> {
let _user_session = require_moderator(
&http_request,
&server_state,
UseDatabase::GrabNewConnection,
).await.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
CommonWebError::NotAuthorized
})?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let limit = query.limit
.unwrap_or(DEFAULT_LIMIT)
@@ -12,9 +12,7 @@ use mysql_queries::queries::user_impersonation_requests::list_user_impersonation
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{
require_moderator, UseDatabase,
};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
const CURSOR_NAME: &str = "moduserimp";
@@ -81,14 +79,7 @@ pub async fn moderator_list_user_session_impersonation_requests_for_user_handler
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListUserImpersonationRequestsSuccessResponse>, CommonWebError> {
let _user_session = require_moderator(
&http_request,
&server_state,
UseDatabase::GrabNewConnection,
).await.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
CommonWebError::NotAuthorized
})?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let limit = query.limit
.unwrap_or(DEFAULT_LIMIT)
@@ -12,9 +12,7 @@ use mysql_queries::queries::user_impersonation_requests::list_user_impersonation
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{
require_moderator, UseDatabase,
};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
const CURSOR_NAME: &str = "modallimp";
@@ -75,14 +73,7 @@ pub async fn moderator_list_user_session_impersonation_requests_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListAllImpersonationRequestsSuccessResponse>, CommonWebError> {
let _user_session = require_moderator(
&http_request,
&server_state,
UseDatabase::GrabNewConnection,
).await.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
CommonWebError::NotAuthorized
})?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let limit = query.limit
.unwrap_or(DEFAULT_LIMIT)
@@ -25,9 +25,7 @@ use mysql_queries::queries::users::user::get::lookup_user_for_moderation::{
};
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{
require_moderator, UseDatabase,
};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
// --- Request ---
@@ -71,14 +69,7 @@ pub async fn moderator_user_session_impersonation_request_handler(
) -> Result<Json<ModerationImpersonateSuccessResponse>, CommonWebError> {
// 1. Require moderator session.
let user_session = require_moderator(
&http_request,
&server_state,
UseDatabase::GrabNewConnection,
).await.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
CommonWebError::NotAuthorized
})?;
let user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
if user_session.is_banned {
warn!("Banned moderator tried to impersonate: {}", user_session.user_token.as_str());
@@ -16,7 +16,7 @@ use mysql_queries::queries::users::user_subscriptions::list_stripe_customer_ids_
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
/// Get all stripe customer ids on file for a user (moderation)
@@ -43,13 +43,7 @@ pub async fn moderator_get_user_stripe_customer_ids_handler(
.acquire()
.await?;
let _moderator_session = require_moderator(
&http_request,
&server_state,
UseDatabase::FromPool(&mut mysql_connection),
)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let _moderator_session = require_moderator(&http_request, &server_state.session_checker, &mut *mysql_connection).await?;
let customer_links = list_user_stripe_customer_links_for_user(&path.user_token, &mut mysql_connection)
.await
@@ -13,7 +13,7 @@ use mysql_queries::queries::wallet_ledger_entries::list_wallet_ledger_entries_by
use tokens::tokens::wallets::WalletToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
/// List wallet ledger entries by wallet token (moderation)
@@ -36,9 +36,7 @@ pub async fn list_wallet_ledger_entries_by_wallet_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListWalletLedgerEntriesByWalletResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let results = list_wallet_ledger_entries_by_wallet(&path.wallet_token, &server_state.mysql_pool)
.await
@@ -13,7 +13,7 @@ use mysql_queries::queries::wallet_ledger_entries::get_wallet_ledger_entry_for_m
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
/// Get a single wallet ledger entry by token (moderation)
@@ -36,9 +36,7 @@ pub async fn moderator_get_wallet_ledger_entry_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModeratorGetWalletLedgerEntryResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let maybe_entry = get_wallet_ledger_entry_for_moderation(&path.wallet_ledger_entry_token, &server_state.mysql_pool)
.await
@@ -13,7 +13,7 @@ use mysql_queries::queries::wallets::list_user_wallets_for_moderation::list_user
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
/// List wallets for a user (moderation)
@@ -36,9 +36,7 @@ pub async fn list_user_wallets_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListUserWalletsResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let results = list_user_wallets_for_moderation(&path.user_token, &server_state.mysql_pool)
.await
@@ -21,7 +21,7 @@ use mysql_queries::queries::wallets::add_durable_banked_balance_to_wallet::add_d
use tokens::tokens::wallets::WalletToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
/// Add banked credits to a wallet (moderation)
@@ -46,12 +46,7 @@ pub async fn moderator_add_banked_balance_to_wallet_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModeratorAddBankedBalanceToWalletResponse>, CommonWebError> {
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|err| {
warn!("Moderator check failed: {:?}", err);
CommonWebError::NotAuthorized
})?;
let user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let ip_address = get_request_ip(&http_request);
@@ -15,7 +15,7 @@ use mysql_queries::queries::wallets::create_new_wallet_for_owner_user::create_ne
use mysql_queries::queries::wallets::find_primary_wallet_token_for_owner::find_primary_wallet_token_for_owner_using_connection;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
/// Create a wallet for a user (moderation)
@@ -37,9 +37,7 @@ pub async fn moderator_create_wallet_for_user_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModeratorCreateWalletForUserResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let user_token = request.user_token.as_ref()
.ok_or_else(|| CommonWebError::BadInputWithSimpleMessage("user_token is required".to_string()))?;
@@ -13,7 +13,7 @@ use mysql_queries::queries::wallets::get_wallet_for_moderation::get_wallet_for_m
use tokens::tokens::wallets::WalletToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
/// Get a single wallet by token (moderation)
@@ -36,9 +36,7 @@ pub async fn moderator_get_wallet_handler(
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModeratorGetWalletResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let maybe_wallet = get_wallet_for_moderation(&path.wallet_token, &server_state.mysql_pool)
.await
@@ -143,14 +143,20 @@ pub async fn omni_gen_image_generate_handler(
// ==================== PIPELINE ==================== //
// NB: Release the pooled DB connection before the (slow, external) generation call so we don't
// hold a pool slot idle while waiting on the provider — that's what starves the pool and causes
// PoolTimedOut on unrelated endpoints. We re-acquire below to write the result.
drop(mysql_connection);
let pipeline_result = run_pipeline_v2(RunPipelineV2Args {
router_builder: &router_builder,
server_state: &server_state,
mysql_connection: &mut mysql_connection,
user_token,
resolved_media: &resolved_media,
}).await?;
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
// ==================== DEBUG LOG: FAL REQUEST ==================== //
if let GenerateImageResponse::Fal(ref fal_payload) = pipeline_result.response {
@@ -25,18 +25,20 @@ use crate::util::lookup::lookup_media_files_as_cdn_url_list_and_map::MediaFilesA
pub struct RunPipelineV2Args<'a> {
pub router_builder: &'a GenerateImageRequestBuilder,
pub server_state: &'a ServerState,
pub mysql_connection: &'a mut sqlx::pool::PoolConnection<sqlx::MySql>,
pub user_token: &'a UserToken,
pub resolved_media: &'a MediaFilesAsCdnUrlListAndMap,
}
// NB: This pipeline does an external generation call (`finalize_and_generate`) that can take many
// seconds. It deliberately does NOT hold a pooled DB connection across that call — it acquires a
// short-lived connection only for the wallet deduction. Holding a pooled connection across the
// external call is what starves the pool and causes `PoolTimedOut`.
pub async fn run_pipeline_v2(
args: RunPipelineV2Args<'_>,
) -> Result<ImagePipelineResult, CommonWebError> {
let RunPipelineV2Args {
router_builder,
server_state,
mysql_connection,
user_token,
resolved_media,
} = args;
@@ -55,17 +57,24 @@ pub async fn run_pipeline_v2(
let apriori_job_token = InferenceJobToken::generate();
let maybe_wallet_ledger_entry_token = if cost > 0 {
// NB: Short-lived connection — released before the external call below.
let mut billing_connection = server_state.mysql_pool.acquire().await
.map_err(|err| {
warn!("Failed to acquire MySQL connection for image billing: {:?}", err);
CommonWebError::from_error(err)
})?;
let deduction = attempt_wallet_deduction_else_common_web_error(
user_token,
Some(apriori_job_token.as_str()),
cost,
mysql_connection,
&mut billing_connection,
).await?;
Some(deduction.ledger_entry_token)
} else {
None
};
// NB: No pooled DB connection is held across this external generation call.
let response = finalize_and_generate(draft_or_request, server_state, resolved_media).await?;
Ok(ImagePipelineResult {
@@ -200,16 +200,22 @@ pub async fn omni_gen_video_generate_handler(
warn!("Failed to insert HTTP request debug log: {:?}", err);
}
// NB: Release the pooled DB connection before the (slow, external) generation call so we don't
// hold a pool slot idle while waiting on the provider — that's what starves the pool and causes
// PoolTimedOut on unrelated endpoints. We re-acquire below to write the result.
drop(mysql_connection);
let pipeline_result = run_pipeline_v2(RunPipelineV2Args {
router_builder: &router_builder,
server_state: &server_state,
mysql_connection: &mut mysql_connection,
user_token,
media_file_to_url_map: &media_file_to_url_map,
kinovi_character_id_map: &kinovi_character_id_map,
kinovi_account,
}).await?;
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
// ==================== DEBUG LOG: FAL REQUEST ==================== //
if let GenerateVideoResponse::Fal(ref fal_payload) = pipeline_result.response {
@@ -3,7 +3,6 @@ use std::convert::TryFrom;
use std::collections::HashMap;
use log::{error, info, warn};
use sqlx::pool::PoolConnection;
use artcraft_router::api::router_video_model::RouterVideoModel;
use artcraft_router::api::router_provider::RouterProvider;
use artcraft_router::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
@@ -28,18 +27,20 @@ use crate::state::server_state::ServerState;
pub struct RunPipelineV2Args<'a> {
pub router_builder: &'a GenerateVideoRequestBuilder,
pub server_state: &'a ServerState,
pub mysql_connection: &'a mut PoolConnection<sqlx::MySql>,
pub user_token: &'a UserToken,
pub media_file_to_url_map: &'a Option<HashMap<MediaFileToken, String>>,
pub kinovi_character_id_map: &'a Option<HashMap<CharacterToken, String>>,
pub kinovi_account: KinoviAccount,
}
// NB: This pipeline does an external generation call (`upload_and_generate`) that can take many
// seconds. It deliberately does NOT hold a pooled DB connection across that call — it acquires
// short-lived connections only for the billing and (on failure) refund writes. Holding a pooled
// connection across the external call is what starves the pool and causes `PoolTimedOut`.
pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResult, CommonWebError> {
let RunPipelineV2Args {
router_builder,
server_state,
mysql_connection,
user_token,
media_file_to_url_map,
kinovi_character_id_map,
@@ -137,11 +138,19 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
info!("v2 estimated cost: {} credits (estimates: {:?})", cost, cost_estimates);
// 3. Bill wallet
let billing = bill_wallet(user_token, cost, mysql_connection).await?;
// 3. Bill wallet (short-lived connection — released before the external call below).
let billing = {
let mut billing_connection = server_state.mysql_pool.acquire().await
.map_err(|err| {
error!("Failed to acquire MySQL connection for billing: {:?}", err);
CommonWebError::from_error(err)
})?;
bill_wallet(user_token, cost, &mut billing_connection).await?
};
// 4. Upload media (if draft) and generate video.
// The entire block is wrapped so Kinovi failures trigger a refund.
// NB: No pooled DB connection is held across this call.
let result = upload_and_generate(
draft_or_request,
server_state,
@@ -156,10 +165,15 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
if let Some(ledger_entry_token) = billing.maybe_wallet_ledger_entry_token.as_ref() {
warn!("Kinovi v2 generation failed, issuing refund for {}: {:?}", ledger_entry_token.as_str(), err);
let result = refund_wallet_after_api_failure(ledger_entry_token, mysql_connection).await;
if let Err(refund_err) = result {
error!("Failed to refund wallet after Kinovi v2 failure: {:?}", refund_err);
match server_state.mysql_pool.acquire().await {
Ok(mut refund_connection) => {
if let Err(refund_err) = refund_wallet_after_api_failure(ledger_entry_token, &mut refund_connection).await {
error!("Failed to refund wallet after Kinovi v2 failure: {:?}", refund_err);
}
}
Err(acquire_err) => {
error!("Failed to acquire MySQL connection to refund wallet after Kinovi v2 failure: {:?}", acquire_err);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More